feat: 项目级成本预算系统(按模型×token 精确计费 + 超额自动暂停)
精确到项目级的成本/预算护栏,成本按「模型 × token 用量」折算(⑧ ★ 新特性)。
定价(单源):
- src/model/pricing.ts:MODEL_PRICES(opus/sonnet/haiku,USD/1M token,分
input/output/cacheRead/cacheWrite)+ computeCost/addUsage,前缀容错、未知模型记 0
token 捕获链(worker → daemon):
- cc.ts:从 SDK result 消息抓 usage,跨 resume/回退累计;CCResult.usage
- runner/reviewer:结果对象带 usage + modelUsed
- protocol.ts:新增 'usage' outbox 记录;pipeline 每次 CC(executor/复审/planner/
conflict)后 emit usage(worker 侧写文件,不碰 DB)
- ingest.ts:'usage' → store.setRunUsage(累加、按模型折算)→ enforceBudget
存储 + 护栏:
- schema/db.ts:runs.usage/cost_usd、projects.budget_usd/budget_period(幂等迁移)
- store.ts:setRunUsage(累加)、projectSpend、costSummary(按项目/模型/总计)、
enforceBudget(超额 → 置 paused + 广播 budget.exceeded)
- 编排器天然停领:orchestrator 既有「跳过 paused 项目」即生效,无需改
API:
- GET /api/usage:并入成本明细 {session,weekly,cost:{period,total,byProject,byModel}},?period/?project
- GET /api/health:{ok,db,inflight,at}
- PATCH /api/projects:支持 budgetUsd/budgetPeriod
前端:
- adapt.js:agentSummary 用真实成本/token;adaptProject 透传预算字段
- app.jsx:budget.exceeded → 告警 toast
- ConfigPanel:预算 $ + 周期(day|month)受控输入
验证:typecheck 干净;206 测试通过(含新增 budget.test.ts 4 例:定价折算/累加/
costSummary/enforceBudget);前端 build + 截图确认预算输入渲染、0 错误。
注:生效需合并后重启 daemon(迁移幂等加列、向后兼容旧库)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-2
@@ -83,6 +83,8 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
if (b.logo !== undefined) patch.logo = b.logo === null || b.logo === '' ? null : String(b.logo);
|
||||
if (b.maxRetries !== undefined) patch.maxRetries = Number(b.maxRetries);
|
||||
if (b.timeoutMs !== undefined) patch.timeoutMs = Number(b.timeoutMs);
|
||||
if (b.budgetUsd !== undefined) patch.budgetUsd = b.budgetUsd === null || b.budgetUsd === '' ? null : Number(b.budgetUsd);
|
||||
if (b.budgetPeriod !== undefined) patch.budgetPeriod = b.budgetPeriod as 'day' | 'month';
|
||||
return projectOut(store.patchProject(id, patch));
|
||||
});
|
||||
|
||||
@@ -145,8 +147,23 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
return { totalActive: runs.length, agents, usage: await getUsage() };
|
||||
});
|
||||
|
||||
// Claude 订阅额度(5 小时 + 周窗口)单独透传;查询失败 → null(前端/CLI 降级显示)
|
||||
app.get('/api/usage', async () => await getUsage());
|
||||
// 用量:Claude 订阅额度(5h+周配额,查询失败→null)+ 成本明细(按项目/模型/总计,按 period 窗口)。
|
||||
// ?period=day|month(默认 month);?project=<id> 限定单项目成本。
|
||||
app.get('/api/usage', async (req) => {
|
||||
const q = req.query as { period?: string; project?: string };
|
||||
const period = q.period === 'day' ? 'day' : 'month';
|
||||
const quota = await getUsage();
|
||||
const cost = store.costSummary(period, q.project && q.project.trim() ? q.project.trim() : undefined);
|
||||
return { ...(quota ?? {}), cost };
|
||||
});
|
||||
|
||||
// 健康检查:daemon 存活 + DB 可读 + 在途 run 数。
|
||||
app.get('/api/health', () => {
|
||||
let db = true;
|
||||
let inflight = 0;
|
||||
try { inflight = store.activeRuns().length; } catch { db = false; }
|
||||
return { ok: db, db, inflight, at: new Date().toISOString() };
|
||||
});
|
||||
|
||||
app.get('/api/projects/:id/tasks', (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
Reference in New Issue
Block a user