diff --git a/PROGRESS-billing-pricing-20260830.md b/PROGRESS-billing-pricing-20260830.md index 4609b7b6..585da601 100644 --- a/PROGRESS-billing-pricing-20260830.md +++ b/PROGRESS-billing-pricing-20260830.md @@ -67,11 +67,23 @@ ## 任务 4 · 上下文缓存 -Blocked/skipped:当前没有 staging/production 的真实成本与 provider 缓存能力证据;本轮不接缓存,也不引入 provider 特殊降级逻辑。待有真实成本与 provider 级缓存计价/兼容性数据后再单独评估。 +已完成安全的 provider-aware usage 贯通: + +- `agent-generation-settings.ts` 统一解析 AI SDK `inputTokenDetails`、legacy `cachedInputTokens` / `cacheCreationInputTokens` 与 OpenAI `prompt_tokens_details.cached_tokens`。 +- Anthropic 请求在稳定 system 前缀上加 `cacheControl: { type: "ephemeral" }`;非 Anthropic provider 返回 `null`,保持原请求形状,不报错、不改变行为。 +- 对话、校正、报告的实际 usage 都把 `readTokens`、`writeTokens`、`noCacheTokens` 与 `hit` 写入既有 `usage_events` / `usage_ledger` 的结构化 `metadata.cache`,未新增列,避免重复存储并保持账本兼容。 +- 已增加 helper contract tests,覆盖 AI SDK、OpenAI、legacy 形状、聚合、命中判定与非 Anthropic 降级。 + +真实成本验收仍未完成:当前没有 staging/production 的第二轮真实命中与可比较基线,因此不能诚实宣称命中率或 p50 `cost_microusd` 降幅;需在部署后用同一 case/稳定前缀采集两轮账本数据并计算。 ## 任务 5 · 会员档参数与权益 -Blocked:未取得 staging/production 非零真实单位成本,不能重设 `minuteLimit`、`dayLimit`、`billingLimit`,不能移除 `rectification` / `report.full` 现有权益行,也不修改商品售价。保持现有公平使用与配额配置不变。 +已完成语义与提示约束,但保留参数/权益不变: + +- admin 商品/权益编辑区明确说明:`billingLimit` 是异常账号熔断,不是“随便聊”的总量承诺;会员总量主要由 `minuteLimit` + `dayLimit` 保护真人容量;生时校正与完整报告单独计费。 +- `fair_use_billing_period` 的用户提示使用“当前账户触发安全熔断”,不写成“配额用完了”,避免与“随便聊”承诺冲突。 +- 由于尚无 staging/production 非零真实单位成本,且未取得存量订阅依赖核查证据,本轮不凭空重设 `minuteLimit` / `dayLimit` / `billingLimit`,不移除 standard 月卡/年卡既有 `rectification` / `report.full` 权益,也不修改售价。后续必须先完成成本与存量订阅审计,再用幂等迁移或既有 admin 审计路径调整。 +- 新增 contract test 锁住熔断语义、会员速率/日限说明与公平使用文案。 ## 任务 6 · 管理端定价测算页 @@ -95,7 +107,7 @@ Blocked:未取得 staging/production 非零真实单位成本,不能重设 ` `npm run db:migrate:check` 尚未能执行有效检查:环境未提供 `SCHEMA_DATABASE_URL`,命令会 fail-closed 为 `SCHEMA_DATABASE_URL is required`。补充 schema database 连接串后需连续运行三次;当前不因该环境缺口修改迁移内容。 -本轮不 push、不 merge、不 rebase;主工作树保持不变。 +本轮变更集中在隔离 worktree;主工作树保持不变。push 前仍需在本分支重新 fetch 并确认与最新 `origin/staging` 的关系。 ### 全量测试补充 diff --git a/frontend/src/components/admin/product-management.tsx b/frontend/src/components/admin/product-management.tsx index ef42b097..2acf3482 100644 --- a/frontend/src/components/admin/product-management.tsx +++ b/frontend/src/components/admin/product-management.tsx @@ -282,7 +282,14 @@ export default function ProductManagement() { - + + + diff --git a/frontend/src/lib/pricing-simulation.ts b/frontend/src/lib/pricing-simulation.ts index 989e6153..59b72637 100644 --- a/frontend/src/lib/pricing-simulation.ts +++ b/frontend/src/lib/pricing-simulation.ts @@ -67,6 +67,7 @@ export function weightedAverageMessages(distribution: readonly DistributionBand[ return usable.reduce((sum, band) => sum + (band.weight / totalWeight) * Math.max(0, band.monthlyMessages), 0); } +// billingLimit is an anomaly-account circuit breaker, not the member-facing total allowance. export function membershipCost(distribution: readonly DistributionBand[], unitCostCny: number | null, billingLimit: number | null) { const averageMessages = weightedAverageMessages(distribution); return { diff --git a/frontend/tests/membership-fair-use-contract.test.ts b/frontend/tests/membership-fair-use-contract.test.ts new file mode 100644 index 00000000..26c06fff --- /dev/null +++ b/frontend/tests/membership-fair-use-contract.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const pricingSimulation = readFileSync( + fileURLToPath(new URL("../src/lib/pricing-simulation.ts", import.meta.url)), + "utf8", +); +const productManagement = readFileSync( + fileURLToPath(new URL("../src/components/admin/product-management.tsx", import.meta.url)), + "utf8", +); +const rectificationRoute = readFileSync( + fileURLToPath(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url)), + "utf8", +); + +test("documents billingLimit as a circuit breaker rather than a chat quota", () => { + assert.match(pricingSimulation, /billingLimit is an anomaly-account circuit breaker/); + assert.match(productManagement, /billingLimit 仅用于异常账号熔断/); + assert.match(productManagement, /minuteLimit 与 dayLimit/); + assert.match(productManagement, /生时校正与完整报告单独计费/); +}); + +test("fair-use billing-period messaging avoids promising a quota", () => { + assert.match(rectificationRoute, /code === "fair_use_billing_period"[\s\S]*安全熔断,请稍后再试或联系支持/); + assert.doesNotMatch(rectificationRoute, /fair_use_billing_period[\s\S]{0,240}配额用完/); +});