Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 覆盖。
Expand Down Expand Up @@ -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` | 最近采集开始时间 |

Expand Down
1 change: 1 addition & 0 deletions docs/five-hour-window-anchoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ limitping ping --until-anchored
2. 否则发送 ping,并在每次 ping 后等待片刻再回读用量,确认窗口是否已开始计时。
3. 为在不过度消耗的前提下提高单轮成功率,起始输入取一个较大的长度,之后每轮把输入
长度翻倍逐步增加单次消耗;一旦检测到窗口开始计时立即停止,或到达轮数上限后停止。
4. 配合 `--push-metric` 时加载 align 配置,并在每次指标推送中包含未来计划 ping 时间。

长输入通过标准输入传给 `codex exec`,避免过长的命令行参数。`--dry-run` 只打印将要执行
的命令,不实际调用模型。
Expand Down
36 changes: 22 additions & 14 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,25 +291,20 @@ 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
pushEndpoint := params.pushEndpoint
// 相位对齐是 --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 \"<cron>\"),本次退化为满额即 ping。")
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/ShawnKung/limitping/internal/align"
"github.com/ShawnKung/limitping/internal/usage"
)

Expand Down Expand Up @@ -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 {
Expand Down
Loading