diff --git a/.externals/CLIProxyAPI b/.externals/CLIProxyAPI index ee2c494..85d2fad 160000 --- a/.externals/CLIProxyAPI +++ b/.externals/CLIProxyAPI @@ -1 +1 @@ -Subproject commit ee2c494788f8a089c58f7ae9aa6ebd1b422211cb +Subproject commit 85d2faddd17e6f4f8675a84ee28b131f702e8eaa diff --git a/AGENTS.md b/AGENTS.md index 3dd9d6c..c7007e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,8 @@ - 本地 CPA 在 WSL 中运行,项目运行目录统一使用 `cpa-plugin/.runtime`;Windows 客户端经 `127.0.0.1` 访问 WSL 时,localhost 转发层可能不会立即向 CPA 传递客户端断开,取消链路测试优先在 WSL 内直连执行。 - 从 PowerShell 跨 WSL 或 SSH 执行包含 Bash 变量、命令替换或多层引号的逻辑时,优先在 `.runtime/tmp/` 创建临时脚本后传入目标环境执行,不拼接复杂的单行 shell;临时脚本不提交。 -- CLIProxyAPI 宿主补丁保存在 `patch/`,当前已验证基线为 `v7.2.135` / `ee2c4947`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后按 `docs/operations.md` 核对补丁和已知上游测试例外。 -- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.135-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。 +- CLIProxyAPI 宿主补丁保存在 `patch/`,当前已验证基线为 `v7.2.137` / `85d2fadd`,必须按 `usage-context`、`usage-identity`、`request-lifecycle-cancel` 的顺序应用;升级 CPA 后按 `docs/operations.md` 核对补丁和已知上游测试例外。 +- 构建本项目 patched CPA 时必须按上游 release workflow 使用 `-ldflags` 注入 `main.Version`、`main.Commit` 和 `main.BuildDate`。`Version` 固定使用“当前上游最新 tag + `-dev`”格式(例如 `v7.2.137-dev`),用于明确区分官方 Release 与本项目补丁构建;不得发布只显示 `dev`、`none` 或 `unknown` 的产物。上传前先在本地执行二进制并核对完整版本输出。 - CPA Linux 可执行文件解压后约 60 MiB,`gzip -9` 后约 20 MiB。上传远端时使用 gzip/tar.gz 或 `scp -C`,不要直接传输未压缩二进制;部署后的运行文件仍需解压为原始可执行文件。 - 实际服务器为 `root@akko.pchuan.top`,CPA 根目录为 `/root/cpa`。只维护本项目的 `billing` 和已要求启用的 `keeper`,不启用或部署其他参考插件。 - 远端替换 CPA 或 billing 前,先停止 `/root/cpa/cpa.sh` 管理的进程,并把旧二进制、插件和 `data/cpa-ext.db*` 备份到 `/root/cpa/backups//`;新版本启动失败时恢复旧二进制和插件。 diff --git a/README.md b/README.md index aeaa27a..dc59e05 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ CLIProxyAPI 负责 HTTP 接入、协议转换、上游凭证和实际请求执 ## 当前兼容目标 -- CLIProxyAPI 基线:`v7.2.135` / `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁 +- CLIProxyAPI 基线:`v7.2.137` / `85d2faddd17e6f4f8675a84ee28b131f702e8eaa`,由 `.externals/CLIProxyAPI` submodule 锁定,需依次应用 `patch/` 中三项宿主补丁 - 插件版本:`0.1.0` - Native ABI:`1` - RPC schema:最高 `4`,注册时按宿主版本向下协商;schema v4 提供 Usage 请求身份 diff --git a/docs/operations.md b/docs/operations.md index 49074fb..ca0ef0c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -4,12 +4,12 @@ | 项目 | 当前值 | |---|---| -| CLIProxyAPI Tag | `v7.2.135` | -| submodule 提交 | `ee2c494788f8a089c58f7ae9aa6ebd1b422211cb` | -| patched 构建版本 | `v7.2.135-dev` | +| CLIProxyAPI Tag | `v7.2.137` | +| submodule 提交 | `85d2faddd17e6f4f8675a84ee28b131f702e8eaa` | +| patched 构建版本 | `v7.2.137-dev` | | Native ABI | `1` | | RPC schema | `4` | -| Go | `1.26.6` | +| Go | `1.26.x` | | 本地运行目录 | `.runtime` | | 本地管理页面 | `http://127.0.0.1:8317/management.html` | | 远端目录 | `root@akko.pchuan.top:/root/cpa` | @@ -29,19 +29,21 @@ DeepSeek 必须配置在 CPA 的 `codex-api-key` 通道,`base-url` 使用 `htt ## 已知上游测试失败 -CLIProxyAPI `ee2c4947` 在 Debian WSL 中运行 `go test ./... -count=1` 时有以下失败: +CLIProxyAPI `85d2fadd` 在 Debian WSL 中运行 `go test ./... -count=1` 时有以下失败: | 测试 | 实际值 | 期望值 | |---|---|---| | `TestApplyClaudeHeaders_DisableDeviceProfileStabilization` | `X-Stainless-Os=MacOS` | `Linux` | | `TestApplyClaudeHeaders_LegacyModePreservesConfiguredUserAgentOverrideForClaudeClients` | `X-Stainless-Os=MacOS` | `Linux` | | `TestClaudeExecutor_NonClaudeRequestUsesClaudeCode220CLIFingerprint` | `X-Stainless-Os=MacOS` | `Linux` | +| `TestInPlaceByteWritesAreReviewed` | `internal/home/client.go` 清单项已过期 | 文件已无原地字节写入 | +| `TestInPlaceByteWritesAreReviewed` | `internal/pluginstore/auth.go` 清单项已过期 | 文件已无原地字节写入 | -2026-08-18 已在未应用任何 billing 补丁的纯上游 `ee2c4947` 临时 worktree 中复现相同结果。三项补丁没有修改 `internal/runtime/executor`,因此同一提交、同一 WSL 环境、同一失败内容再次出现时,直接记为已知上游失败,不再创建干净 worktree复测。 +2026-08-20 已在未应用任何 billing 补丁的纯上游 `85d2fadd` 临时 worktree 中复现以上 5 项。三项补丁不修改 `internal/runtime/executor`、`internal/util`、`internal/home` 或 `internal/pluginstore`,因此同一提交、同一 WSL 环境、同一失败内容再次出现时,直接记为已知上游失败,不修复、不再创建干净 worktree复测。 出现以下任一情况时重新验证: -- submodule 提交不再是 `ee2c4947`; +- submodule 提交不再是 `85d2fadd`; - 上游修改了 `internal/runtime/executor/claude_executor_test.go`、`claude_executor_request.go` 或 `helps/claude_device_profile.go`; - 失败测试、行号、实际值或期望值发生变化; - 三项补丁开始修改 `internal/runtime/executor`。 @@ -108,7 +110,7 @@ CPA 版本必须使用当前最新 Tag 加 `-dev`,提交取 submodule HEAD: ```bash cd .externals/CLIProxyAPI -version=v7.2.135-dev +version=v7.2.137-dev commit="$(git rev-parse --short=8 HEAD)" build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)" CGO_ENABLED=1 go build -buildvcs=false \ diff --git a/internal/plugin/access.go b/internal/plugin/access.go index 7ab036e..c7c5e0a 100644 --- a/internal/plugin/access.go +++ b/internal/plugin/access.go @@ -75,6 +75,7 @@ func (a *App) interceptRequest(raw []byte, afterAuth bool) ([]byte, error) { if err := json.Unmarshal(raw, &req); err != nil { return nil, fmt.Errorf("解析请求拦截事件: %w", err) } + a.markCompactRequest(req) keyID := metadataString(req.Metadata, callerScopeMetadata) if keyID == "" && req.RequestID != "" { if pending, ok := a.pending.Load(req.RequestID); ok { diff --git a/internal/plugin/app.go b/internal/plugin/app.go index e49780d..4d7353a 100644 --- a/internal/plugin/app.go +++ b/internal/plugin/app.go @@ -29,6 +29,7 @@ type App struct { closed bool seen atomic.Uint64 pending sync.Map + compactRequests sync.Map scopes sync.Map observedUpstreams sync.Map } @@ -177,6 +178,7 @@ func (a *App) handleUsage(raw []byte) ([]byte, error) { return nil, fmt.Errorf("用量数据库尚未初始化") } // CPA wire 类型只存在于适配层,采集模块接收与协议无关的观察值。 + compact := a.isCompactRequest(record.RequestID) observed := collection.Record{ RequestID: record.RequestID, TraceID: record.TraceID, @@ -200,11 +202,20 @@ func (a *App) handleUsage(raw []byte) ([]byte, error) { TTFT: record.TTFT, Latency: record.Latency, } + if compact { + observed.Endpoint = compactEndpoint + } if key, keyErr := a.resolveUsageManagedKey(a.store, record.APIKey); keyErr == nil { observed.ManagedKeyID = key.ID observed.KeyAlias = key.Name } - if policy, found := a.prices[normalizeModelName(record.Model)]; found { + if compact { + zero := int64(0) + observed.CostMicros = &zero + observed.PriceTier = "compact" + observed.PriceMultiplierNumerator = 1 + observed.PriceMultiplierDenominator = 1 + } else if policy, found := a.prices[normalizeModelName(record.Model)]; found { cost, calculateErr := pricing.Calculate(policy, pricing.Usage{ InputTokens: record.Detail.InputTokens, CacheReadTokens: record.Detail.CacheReadTokens, CacheWriteTokens: record.Detail.CacheCreationTokens, OutputTokens: record.Detail.OutputTokens, @@ -262,6 +273,9 @@ func (a *App) handleRequestComplete(raw []byte) ([]byte, error) { return nil, fmt.Errorf("释放并发占用: %w", err) } endpoint, _ := completion.Metadata["request_path"].(string) + if a.isCompactRequest(completion.RequestID) { + endpoint = compactEndpoint + } managedKeyID := metadataString(completion.Metadata, callerScopeMetadata) if key, keyErr := a.store.ManagedKeyByReference(context.Background(), managedKeyID); keyErr == nil { managedKeyID = key.ID @@ -293,6 +307,36 @@ func isCompactEndpoint(endpoint string) bool { return strings.HasSuffix(strings.TrimRight(path, "/"), "/responses/compact") } +const compactEndpoint = "/v1/responses/compact" + +func (a *App) markCompactRequest(req RequestInterceptRequest) { + requestID := strings.TrimSpace(req.RequestID) + if requestID == "" { + return + } + endpoint := metadataString(req.Metadata, "request_path") + if isCompactEndpoint(endpoint) || hasCompactionTrigger(req.Body) { + a.compactRequests.Store(requestID, struct{}{}) + } +} + +func (a *App) isCompactRequest(requestID string) bool { + _, ok := a.compactRequests.Load(strings.TrimSpace(requestID)) + return ok +} + +func hasCompactionTrigger(body []byte) bool { + var payload struct { + Input []struct { + Type string `json:"type"` + } `json:"input"` + } + if err := json.Unmarshal(body, &payload); err != nil || len(payload.Input) == 0 { + return false + } + return strings.EqualFold(strings.TrimSpace(payload.Input[len(payload.Input)-1].Type), "compaction_trigger") +} + func (a *App) Seen() uint64 { return a.seen.Load() } diff --git a/internal/plugin/management_test.go b/internal/plugin/management_test.go index 9ea1850..9712851 100644 --- a/internal/plugin/management_test.go +++ b/internal/plugin/management_test.go @@ -374,7 +374,7 @@ func TestCanceledRequestWithoutUsageRemainsVisible(t *testing.T) { } } -func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) { +func TestCompactUsageIsZeroPricedAndUsesJSONMetrics(t *testing.T) { app := NewApp() if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil { t.Fatal(err) @@ -384,8 +384,15 @@ func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) { t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body) } startedAt := time.Now() + interceptRaw, _ := json.Marshal(RequestInterceptRequest{ + RequestID: "compact-success", Body: []byte(`{"model":"deepseek-v4-flash"}`), + Metadata: map[string]any{"request_path": "/v1/responses/compact"}, + }) + if _, err := app.HandleMethod(MethodRequestBefore, interceptRaw); err != nil { + t.Fatal(err) + } record := UsageRecord{ - Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt, + RequestID: "compact-success", Provider: "openai", Model: "deepseek-v4-flash", RequestedAt: startedAt, Latency: time.Second, TTFT: 200 * time.Millisecond, Detail: UsageDetail{InputTokens: 1_000, OutputTokens: 100, TotalTokens: 1_100}, } @@ -413,11 +420,62 @@ func TestCompactUsageIsPricedAndUsesJSONMetrics(t *testing.T) { if got.RequestType != "JSON" || got.Endpoint != "/v1/responses/compact" || got.TTFTMilliseconds != 0 || got.SpeedTPS != nil { t.Fatalf("unexpected compact metadata: %+v", got) } - if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0.004 { + if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0 { t.Fatalf("unexpected compact cost: %+v", got) } } +func TestRemoteV2CompactTriggerDisplaysCompactAndCostsZero(t *testing.T) { + app := NewApp() + if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\ncodex_only: false\n"))); err != nil { + t.Fatal(err) + } + priceBody := []byte(`{"model":"gpt-5.6-sol","base":{"input_per_1m":"2.5","cache_read_per_1m":"0.25","cache_write_per_1m":"3.125","output_per_1m":"15"}}`) + if response := managementCallBody(t, app, http.MethodPut, managementBase+routePrices, priceBody); response.StatusCode != http.StatusOK { + t.Fatalf("put price status = %d, body = %s", response.StatusCode, response.Body) + } + startedAt := time.Now() + interceptRaw, _ := json.Marshal(RequestInterceptRequest{ + RequestID: "compact-v2", Model: "gpt-5.6-sol", Stream: true, + Body: []byte(`{"model":"gpt-5.6-sol","input":[{"type":"message","role":"user","content":"history"},{"type":"compaction_trigger"}]}`), + Metadata: map[string]any{"request_path": "/v1/responses"}, + }) + if _, err := app.HandleMethod(MethodRequestBefore, interceptRaw); err != nil { + t.Fatal(err) + } + usageRaw, _ := json.Marshal(UsageRecord{ + RequestID: "compact-v2", Provider: "codex", Model: "gpt-5.6-sol", RequestedAt: startedAt, + Latency: time.Second, TTFT: 200 * time.Millisecond, + Detail: UsageDetail{InputTokens: 17_235, OutputTokens: 144, TotalTokens: 17_379}, + }) + if _, err := app.HandleMethod(MethodUsageHandle, usageRaw); err != nil { + t.Fatal(err) + } + completionRaw, _ := json.Marshal(RequestCompletion{ + RequestID: "compact-v2", Model: "gpt-5.6-sol", Stream: true, Outcome: RequestCompletionSucceeded, + StartedAt: startedAt, CompletedAt: startedAt.Add(time.Second), + Metadata: map[string]any{"request_path": "/v1/responses"}, + }) + if _, err := app.HandleMethod(MethodRequestComplete, completionRaw); err != nil { + t.Fatal(err) + } + response := managementCall(t, app, http.MethodGet, managementBase+routeUsage) + var payload usageListResponse + if err := json.Unmarshal(response.Body, &payload); err != nil { + t.Fatal(err) + } + if len(payload.Records) != 1 { + t.Fatalf("records = %d, want 1", len(payload.Records)) + } + got := payload.Records[0] + if got.Endpoint != compactEndpoint || got.TTFTMilliseconds != 0 || got.SpeedTPS != nil { + t.Fatalf("unexpected compact v2 metadata: %+v", got) + } + if !got.CostAvailable || got.CostUSD == nil || *got.CostUSD != 0 { + t.Fatalf("unexpected compact v2 cost: %+v", got) + } +} + func TestCompactFailureAndCancellationRemainVisible(t *testing.T) { for _, test := range []struct { name string @@ -636,7 +694,7 @@ func TestUsageResourceServesFeatureModules(t *testing.T) { } javascript.Write(response.Body) } - for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "剩余额度", `document.body.classList.toggle("read-only"`, `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} { + for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "剩余额度", "今日使用 / 剩余额度", "quotaUsage(item.cost, item.balance)", "resetTime(item)", `document.body.classList.toggle("read-only"`, "compactTokens(price.long_context.threshold_input_tokens)", "appendPriceRow(table, \"长上下文\", price.long_context, true)", `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} { if !strings.Contains(javascript.String(), feature) { t.Fatalf("UI modules do not contain feature %q", feature) } diff --git a/internal/web/app/features/keys.js b/internal/web/app/features/keys.js index f74b70e..afce8b7 100644 --- a/internal/web/app/features/keys.js +++ b/internal/web/app/features/keys.js @@ -192,11 +192,13 @@ function renderUserCharts() { const byUser = new Map(managedKeys.map(key => [key.name, { name: key.name, requests: 0, tokens: 0, cost: 0, balance: key.billing ? Number(key.billing.balance_usd || 0) : null, + resetPeriod: key.billing?.reset_period || null, + nextResetAt: key.billing?.next_reset_at ? new Date(key.billing.next_reset_at) : null, days: [], last: null }])); (usageDashboard.users || []).forEach(user => { const name = user.key_alias || "未识别"; - if (!byUser.has(name)) byUser.set(name, { name, requests: 0, tokens: 0, cost: 0, balance: null, days: [], last: null }); + if (!byUser.has(name)) byUser.set(name, { name, requests: 0, tokens: 0, cost: 0, balance: null, resetPeriod: null, nextResetAt: null, days: [], last: null }); const value = byUser.get(name); const today = user.today || {}; value.requests = today.requests || 0; @@ -208,7 +210,7 @@ function renderUserCharts() { const userRows = [...byUser.values()].sort((left, right) => right.tokens - left.tokens || left.name.localeCompare(right.name)); const header = document.createElement("div"); header.className = "user-usage-row header"; - ["用户", "今日请求", "今日 Token", "今日成本", "剩余额度", "最近使用"].forEach(label => { + ["用户", "今日请求", "今日 Token", "今日使用 / 剩余额度", "最近使用", "重置时间"].forEach(label => { const cell = document.createElement("span"); cell.textContent = label; header.append(cell); @@ -216,7 +218,7 @@ function renderUserCharts() { document.querySelector("#user-usage").replaceChildren(header, ...userRows.map(item => { const row = document.createElement("div"); row.className = "user-usage-row"; - [item.name, number(item.requests), number(item.tokens), money(item.cost), item.balance === null ? "—" : money(item.balance), item.last ? item.last.toLocaleString() : "从未"].forEach(value => { + [item.name, number(item.requests), number(item.tokens), quotaUsage(item.cost, item.balance), item.last ? item.last.toLocaleString() : "从未", resetTime(item)].forEach(value => { const cell = document.createElement("span"); cell.textContent = value; row.append(cell); @@ -227,6 +229,24 @@ function renderUserCharts() { renderQuotaMatrix(days, [...byUser.values()]); } +function quotaUsage(cost, balance) { + const used = Number(cost || 0); + const remaining = Number(balance); + return `${(Number.isFinite(used) ? used : 0).toFixed(2)} / ${balance === null || !Number.isFinite(remaining) ? "—" : remaining.toFixed(2)}`; +} + +function resetTime(item) { + if (item.resetPeriod === "none") return "永不"; + if (!item.nextResetAt || !Number.isFinite(item.nextResetAt.getTime())) return "—"; + const value = item.nextResetAt; + const parts = [ + value.getFullYear(), + String(value.getMonth() + 1).padStart(2, "0"), + String(value.getDate()).padStart(2, "0") + ]; + return `${parts.join("-")} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`; +} + function renderQuotaMatrix(days, users) { const dates = days.map(day => dateKey(day.date)); const rows = users.map(user => { diff --git a/internal/web/app/features/pricing.js b/internal/web/app/features/pricing.js index f586674..ed832d0 100644 --- a/internal/web/app/features/pricing.js +++ b/internal/web/app/features/pricing.js @@ -144,7 +144,10 @@ function renderPriceCard(price) { name.textContent = price.model; const meta = document.createElement("div"); meta.className = "model-price-meta"; - if (price.long_context) meta.append(priceBadge("长上下文")); + if (price.long_context) { + const comparison = price.long_context.comparison === "gte" ? "≥" : ">"; + meta.append(priceBadge(`${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`)); + } if (price.fast_pricing_enabled) meta.append(priceBadge(`Fast ×${price.fast_multiplier || "2.5"}`)); header.append(name, meta); @@ -153,8 +156,7 @@ function renderPriceCard(price) { ["", "输入", "缓存读取", "缓存写入", "输出"].forEach(label => table.append(priceCell(label, "model-price-column"))); appendPriceRow(table, "标准", price.base); if (price.long_context) { - const comparison = price.long_context.comparison === "gte" ? "≥" : ">"; - appendPriceRow(table, `长上下文\n${comparison} ${compactTokens(price.long_context.threshold_input_tokens)}`, price.long_context, true); + appendPriceRow(table, "长上下文", price.long_context, true); } else { table.append( priceCell("长上下文", "model-price-row-label model-price-last-row"), diff --git a/internal/web/styles/keys.css b/internal/web/styles/keys.css index a0ace3c..46438a7 100644 --- a/internal/web/styles/keys.css +++ b/internal/web/styles/keys.css @@ -2,16 +2,16 @@ .chart-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; margin-bottom: 12px; } .chart-card { min-height: 180px; padding: 13px 14px; border: 1px solid var(--border-soft); border-radius: 9px; background: var(--panel); } .quota-matrix-wrap { overflow-x: auto; } -.quota-matrix { display: grid; min-width: 720px; border-top: 1px solid var(--border-soft); border-left: 1px solid var(--border-soft); font-size: 9px; } +.quota-matrix { display: grid; min-width: 720px; border-top: 1px solid var(--border-soft); border-left: 1px solid var(--border-soft); font-size: 12px; } .quota-cell { display: grid; min-width: 0; min-height: 32px; padding: 7px 6px; place-items: center; overflow: hidden; border-right: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); color: var(--text-secondary); text-align: center; text-overflow: ellipsis; white-space: nowrap; } .quota-cell.header { min-height: 27px; background: var(--bg-primary); color: var(--muted); } .quota-cell.user { text-align: center; } .quota-cell.total { background: var(--bg-tertiary); color: var(--text); font-weight: 600; } .quota-cell.usage { background: color-mix(in srgb, var(--primary-active) var(--quota-intensity), var(--bg-primary)); color: var(--text); } .user-usage { display: grid; overflow-x: auto; } -.user-usage-row { display: grid; grid-template-columns: minmax(120px, 1.25fr) repeat(4, minmax(100px, 1fr)) minmax(150px, 1.4fr); align-items: center; gap: 10px; min-width: 790px; min-height: 31px; border-bottom: 1px solid var(--border-soft); font-size: 10px; } +.user-usage-row { display: grid; grid-template-columns: 120px repeat(5, minmax(150px, 1fr)); align-items: center; gap: 10px; min-width: 900px; min-height: 34px; border-bottom: 1px solid var(--border-soft); font-size: 12px; } .user-usage-row:last-child { border-bottom: 0; } -.user-usage-row.header { min-height: 26px; color: var(--muted); font-size: 9px; } +.user-usage-row.header { min-height: 30px; color: var(--muted); font-size: 12px; } .user-usage-row span:not(:first-child) { text-align: right; } .user-management { display: grid; gap: 14px; } .user-management-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; } diff --git a/internal/web/styles/pricing.css b/internal/web/styles/pricing.css index 8e27ec6..842f10c 100644 --- a/internal/web/styles/pricing.css +++ b/internal/web/styles/pricing.css @@ -40,14 +40,14 @@ .read-only #view-pricing .price-editor { display: none; } .model-price-card { min-width: 0; overflow: hidden; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-primary); } .model-price-card-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 49px; padding: 12px 14px; border-bottom: 1px solid var(--border-soft); } -.model-price-card-header h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } +.model-price-card-header h2 { min-width: 0; margin: 0; overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } .model-price-meta { display: flex; flex: 0 0 auto; gap: 5px; } .model-price-meta span { padding: 3px 6px; border: 1px solid var(--border); border-radius: 5px; color: var(--text-secondary); font-size: 8px; white-space: nowrap; } .model-price-table { display: grid; grid-template-columns: minmax(72px, .9fr) repeat(4, minmax(65px, 1fr)); } .model-price-table > div { display: flex; align-items: center; min-width: 0; min-height: 35px; padding: 8px 10px; border-right: 1px solid var(--border-soft); border-bottom: 1px solid var(--border-soft); } .model-price-table > div:nth-child(5n) { border-right: 0; } -.model-price-column { color: var(--muted); font-size: 8px; } -.model-price-row-label { color: var(--muted); font-size: 8px; line-height: 1.35; white-space: pre-line; } -.model-price-value { justify-content: flex-end; font-size: 10px; font-weight: 600; font-variant-numeric: tabular-nums; } +.model-price-column { justify-content: center; color: var(--muted); font-size: 12px; } +.model-price-row-label { color: var(--muted); font-size: 12px; line-height: 1.35; white-space: pre-line; } +.model-price-value { justify-content: center; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; } .model-price-last-row { min-height: 39px !important; border-bottom: 0 !important; } -.model-price-unavailable { grid-column: span 4; justify-content: center; border-right: 0 !important; color: var(--muted); font-size: 9px; } +.model-price-unavailable { grid-column: span 4; justify-content: center; border-right: 0 !important; color: var(--muted); font-size: 12px; } diff --git a/internal/web/styles/usage.css b/internal/web/styles/usage.css index 439e8bd..73e486b 100644 --- a/internal/web/styles/usage.css +++ b/internal/web/styles/usage.css @@ -7,7 +7,7 @@ summary::-webkit-details-marker { display: none; } .table-wrap { overflow: auto; } table { width: 100%; border-collapse: collapse; white-space: nowrap; font-size: 13px; } th, td { padding: 8px 10px; border-bottom: 1px solid var(--border-soft); text-align: right; } -th { position: sticky; top: 0; z-index: 1; background: var(--bg-quinary); color: var(--muted); font-size: 10px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase; } +th { position: sticky; top: 0; z-index: 1; background: var(--bg-quinary); color: var(--muted); font-size: 12px; font-weight: 650; letter-spacing: .03em; text-transform: uppercase; } th.left, td.left { text-align: left; } tr:last-child td { border-bottom: 0; } tbody tr:hover { background: color-mix(in srgb, var(--text-primary) 3%, transparent); } diff --git a/patch/README.md b/patch/README.md index c645143..eff84ae 100644 --- a/patch/README.md +++ b/patch/README.md @@ -1,6 +1,6 @@ # CLIProxyAPI 宿主补丁 -这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.135` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用: +这里保存 billing 最初为 CLIProxyAPI `v7.2.133` 制作、已在 `v7.2.137` 重新验证的三项宿主修复。三项补丁都不改变计费规则或数据库结构,必须按下列顺序应用: 1. [`cli-proxy-api-usage-context.patch`](cli-proxy-api-usage-context.patch) 2. [`cli-proxy-api-usage-identity.patch`](cli-proxy-api-usage-identity.patch) @@ -8,8 +8,8 @@ 适用基线: -- CLIProxyAPI:`v7.2.135` -- 提交:`ee2c494788f8a089c58f7ae9aa6ebd1b422211cb` +- CLIProxyAPI:`v7.2.137` +- 提交:`85d2faddd17e6f4f8675a84ee28b131f702e8eaa` - Native ABI:`1` - 第二项补丁把 RPC schema 从 `3` 提升到 `4` @@ -84,8 +84,8 @@ git -C .externals/CLIProxyAPI apply ../../patch/cli-proxy-api-request-lifecycle- cd .externals/CLIProxyAPI gofmt -w internal/pluginhost sdk/api/handlers sdk/cliproxy/usage sdk/pluginabi sdk/pluginapi go test -race ./internal/pluginhost ./sdk/api/handlers ./sdk/cliproxy/usage -count=1 -version=v7.2.135-dev -commit=ee2c4947 +version=v7.2.137-dev +commit=85d2fadd build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)" CGO_ENABLED=1 go build -buildvcs=false \ -ldflags="-s -w -X main.Version=$version -X main.Commit=$commit -X main.BuildDate=$build_date" \ @@ -129,7 +129,7 @@ billing 必须使用 RPC schema v4 重新构建;旧 billing 动态库会向下 - billing 全部测试、billing race 测试及 CLIProxyAPI 相关包 race 测试通过; - schema v3 对照插件仍能正常注册,新增字段不会发送给旧 schema。 -升级到 CLIProxyAPI `v7.2.135` / `ee2c4947` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试、billing 全仓 race 测试和两个 Linux amd64 产物构建均通过。宿主全仓 `go test ./...` 有 3 个与补丁无关的 Claude 指纹测试失败;相同失败已在未应用补丁的纯上游 `ee2c4947` 上复现,详见 [`../docs/operations.md`](../docs/operations.md)。 +升级到 CLIProxyAPI `v7.2.137` / `85d2fadd` 后,三项补丁可从干净基线按顺序重放,补丁涉及的三个包及其 race 测试通过。宿主全仓测试中的 3 个 Claude 指纹失败和 2 个 `reviewedInPlaceByteWrites` 清单过期失败均已在未应用补丁的纯上游 `85d2fadd` 上原样复现,属于上游例外,详见 [`../docs/operations.md`](../docs/operations.md)。 ## 性能影响 diff --git a/scripts/package.sh b/scripts/package.sh index 63fed79..f9e9562 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -30,7 +30,7 @@ printf '%s\n' \ "target=linux/amd64" \ "native_abi=1" \ "rpc_schema=4" \ - "cliproxyapi_revision=ee2c494788f8a089c58f7ae9aa6ebd1b422211cb+usage-context+usage-identity+request-lifecycle-cancel" \ + "cliproxyapi_revision=85d2faddd17e6f4f8675a84ee28b131f702e8eaa+usage-context+usage-identity+request-lifecycle-cancel" \ >"$package_dir/VERSION.txt" archive="$root/dist/$name.tar.gz"