diff --git a/README.md b/README.md index 62342cb..2f53648 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ CCLimitPing;如果只需要一个可嵌入现有 cron/plist/监控体系的 Co - `--dry-run` 在执行模型调用或推送指标前预览操作。 - `--if-5h-full` 只在 5 小时额度恢复到 100% 时执行 ping;此时默认还会结合已配置的 cron 目标做相位对齐:仅在满额且到达对齐时间后才 ping(指标仍正常上报)。 - `--without-align` 关闭相位对齐,只保留满额判断(满额即 ping);仅在配合 `--if-5h-full` 时有意义。不带任何 flag 时 `ping` 立即执行,与对齐无关。 -- `--until-anchored` 持续 ping 直到 5 小时窗口开始计时(被首次使用锚定):起始输入较大,每轮翻倍,每次 ping 后回读用量确认是否已计时,计时后立即停止。 +- `--until-anchored` 持续 ping 直到 5 小时窗口开始计时(被首次使用锚定):起始输入较大,每轮翻倍,每次 ping 后回读用量确认是否已计时,计时后立即停止;配合 `--push-metric` 时也会上报已配置 align 目标的未来计划。 - `align` 子命令管理多条 cron 目标(每条带唯一 id 和自己的延时预算 max-delay,可按 id 单独删除),并预览未来的 ping/刷新时间。 - 将用量、重置券数量和最近成功 ping 时间推送到 Pushgateway。 - 成功 ping 时间持久化,后续上报不会被 `ping_completed=0` 覆盖。 @@ -283,7 +283,7 @@ $T/2=2.5$ 小时,也就是约 **50%**。这也是“随机时刻到达”直 | `limitping_reset_credit_expiration_timestamp_seconds` | 最早到期的可用重置券时间戳 | | `limitping_ping_completed` | 本次是否完成 ping(0/1) | | `limitping_last_successful_ping_timestamp_seconds` | 最近一次成功 ping 的时间戳 | -| `limitping_planned_ping_timestamp_seconds` | 未来计划 ping 时间戳(`index="1..5"`,1 为最近,需配置对齐目标并以 `--if-5h-full` 运行) | +| `limitping_planned_ping_timestamp_seconds` | 未来计划 ping 时间戳(`index="1..5"`,1 为最近;需配置对齐目标,并通过 `--if-5h-full` 或 `--until-anchored` 配合 `--push-metric` 运行) | | `metrics_pusher_collector_success` | 本次采集是否成功 | | `metrics_pusher_last_run_timestamp_seconds` | 最近采集开始时间 | diff --git a/docs/five-hour-window-anchoring.md b/docs/five-hour-window-anchoring.md index 6c500c8..edc8939 100644 --- a/docs/five-hour-window-anchoring.md +++ b/docs/five-hour-window-anchoring.md @@ -50,6 +50,7 @@ limitping ping --until-anchored 2. 否则发送 ping,并在每次 ping 后等待片刻再回读用量,确认窗口是否已开始计时。 3. 为在不过度消耗的前提下提高单轮成功率,起始输入取一个较大的长度,之后每轮把输入 长度翻倍逐步增加单次消耗;一旦检测到窗口开始计时立即停止,或到达轮数上限后停止。 +4. 配合 `--push-metric` 时加载 align 配置,并在每次指标推送中包含未来计划 ping 时间。 长输入通过标准输入传给 `codex exec`,避免过长的命令行参数。`--dry-run` 只打印将要执行 的命令,不实际调用模型。 diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d7b39b2..29917d0 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -291,8 +291,12 @@ const ( ) func executePing(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, params pingParams) error { + planner, err := plannerForPing(params) + if err != nil { + return err + } if params.untilAnchored { - return executePingUntilAnchored(ctx, stdout, stderr, params) + return executePingUntilAnchored(ctx, stdout, stderr, params, planner) } dryRun := params.dryRun ifFull := params.ifFull @@ -300,16 +304,7 @@ func executePing(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, // 相位对齐是 --if-5h-full 的默认行为;--without-align 关闭它,退化为“满额即 ping”。 useAlign := ifFull && !params.withoutAlign - var planner *align.Planner if useAlign { - cfg, err := align.Load() - if err != nil { - return err - } - planner, err = align.NewPlanner(cfg) - if err != nil { - return err - } if !planner.HasTargets() { fmt.Fprintln(stderr, "提示:未配置对齐目标(limitping align add \"\"),本次退化为满额即 ping。") } @@ -396,17 +391,30 @@ func executePing(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, return wrapStateError(stateErr) } +func plannerForPing(params pingParams) (*align.Planner, error) { + needsPlanner := params.ifFull && !params.withoutAlign + needsPlannedMetrics := params.untilAnchored && params.pushEndpoint != "" + if !needsPlanner && !needsPlannedMetrics { + return nil, nil + } + cfg, err := align.Load() + if err != nil { + return nil, err + } + return align.NewPlanner(cfg) +} + // executePingUntilAnchored 持续发送 ping 直到 5h 窗口开始计时(被首次使用锚定)。 // // 极小的 ping 未必能立刻让窗口进入计时,且窗口是否计时无法从单次调用即时得知, // 需要回读用量确认。为提高单轮成功率同时避免一开始就发过大的请求,这里从一个较大的 // 起始 prompt 长度出发,每轮把长度翻倍,逐步增加单次消耗;每次 ping 后等待片刻再回读 // 用量,一旦检测到窗口在计时立即停止,或到达轮数上限后停止。 -func executePingUntilAnchored(ctx context.Context, stdout, stderr io.Writer, params pingParams) error { +func executePingUntilAnchored(ctx context.Context, stdout, stderr io.Writer, params pingParams, planner *align.Planner) error { if snapshot, err := readUsage(ctx); err == nil { if window := snapshot.FiveHour; window != nil && window.Active { fmt.Fprintln(stdout, "5h 窗口已在计时,无需 ping。") - return maybePush(ctx, stdout, snapshot, params, false, nil) + return maybePush(ctx, stdout, snapshot, params, false, planner) } } @@ -440,11 +448,11 @@ func executePingUntilAnchored(ctx context.Context, stdout, stderr io.Writer, par window := snapshot.FiveHour if params.dryRun { fmt.Fprintln(stdout, "dry-run:跳过实际 ping 与锚定检查。") - return maybePush(ctx, stdout, snapshot, params, false, nil) + return maybePush(ctx, stdout, snapshot, params, false, planner) } if window != nil && window.Active { fmt.Fprintf(stdout, "5h 窗口已开始计时,%s 后重置。\n", formatDurationCN(time.Duration(window.RemainingSeconds)*time.Second)) - return maybePush(ctx, stdout, snapshot, params, true, nil) + return maybePush(ctx, stdout, snapshot, params, true, planner) } fmt.Fprintln(stdout, "窗口尚未开始计时,加大输入后重试。") runes *= 2 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 7a92af7..4142536 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/ShawnKung/limitping/internal/align" "github.com/ShawnKung/limitping/internal/usage" ) @@ -73,6 +74,36 @@ func TestPingUntilAnchoredFlag(t *testing.T) { } } +func TestPlannerForUntilAnchoredMetrics(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + if err := align.Save(&align.Config{Targets: []align.Target{{ + ID: "123456abcdef", + Cron: "0 0 * * *", + MaxDelayMinutes: 90, + }}}); err != nil { + t.Fatal(err) + } + + planner, err := plannerForPing(pingParams{ + untilAnchored: true, + pushEndpoint: "http://push.example:9091", + }) + if err != nil { + t.Fatal(err) + } + if planner == nil || !planner.HasTargets() { + t.Fatal("expected --until-anchored metric push to load align targets") + } + + planner, err = plannerForPing(pingParams{untilAnchored: true}) + if err != nil { + t.Fatal(err) + } + if planner != nil { + t.Fatal("expected --until-anchored without metric push to skip align config") + } +} + func TestCobraHelp(t *testing.T) { var out bytes.Buffer if err := Run([]string{"--help"}, strings.NewReader(""), &out, &out); err != nil {