Commit Graph
23 Commits
Author SHA1 Message Date
xsl e8208d2972 feat(report): orchestration skeleton with staged pipeline and refund-on-failure
POST /reports 骨架:资格校验→终身配额预扣→tk_task→cpu 队列;动态装配
流水线(dotted-path 注册、缺阶段即 CONFIG_ERROR);中间态逐段落库、失败
退款、derived_features 跨重试/重生成复用(T-P3-07/09)。

实现:packages/report 的 pipeline.py(Stage 契约保持不变,新增 assemble_stages/
run_pipeline/StageFailureError,ReportContext 增可选 task_id 参数、向后兼容)、
repo.py、schemas.py、service.py(重放优先于资格校验、reserve+建行+建任务同事务、
提交后才投递、并发同键落败回滚);apps/api/routers/reports.py 填桩(send_task 按
任务名投递,不 import apps.worker);apps/worker/report_tasks.py(worker 侧自建
gateway 装配、状态归一 QUEUED→RUNNING、成功 commit 配额+成本聚合写 model_info、
基础设施异常重试一次、重试耗尽兜底失败退款)。测试:单测 6 个(装配缺失/空配置/
协议违约/顺序执行/失败保留产物),集成 6 场景(预扣 lifetime/PENDING、成功
committed+两阶段键、同 request_key 幂等不重复扣、二次首生成 40920、阶段失败
refund+derived_features 保留、regenerate 走 m: 月配额 version+1 复用视觉中间态)。

偏差说明:
1) apps/worker/celery_app.py 的 include 只含 apps.worker.tasks(禁改文件),真
worker 进程注册 meida.report.generate 需协调者合并时把 include 补上
apps.worker.report_tasks;API 侧已按任务名 send_task,两侧只靠名字衔接,测试
不受影响。2) ReportContext 增加可选 task_id 关键字参数(失败结案要推动
tk_task),Stage 协议/DEFAULT_STAGE_PATHS/stage_names 未动,T16/T17 兼容。
3) JSONB 深变更用 flag_modified 显式标脏(重新赋相等新 dict 会被 flush 的按值
比较跳过,集成测试抓到过该坑)。4) POST 后 tk_task 保持 PENDING(任务文档
§4 场景 1 口径),QUEUED→RUNNING 由 worker 入口归一。
2026-08-14 22:18:14 +08:00
xsl d974526904 chore(prep): shared groundwork for batch-4 (report pipeline)
第四批(T15 编排/T16 视觉诊断/T17 文案校验)协调提交:
- 迁移 0006:rp_report(result JSONB 按阶段键承载中间态)/rp_input
 (derived_features 视觉中间态)/rp_share(V1 只建结构)
- ORM 三模型;错误码 40420/40920(文案无价格字样)
- 配置:report_pipeline_stages(dotted path 动态装配)+ vision/tier
- packages/report/pipeline.py:Stage Protocol + ReportContext(三任务共同契约,
  动态 import 的动机与代价写在 docstring)
- 路由桩 reports.py 已注册
2026-08-14 21:51:05 +08:00
xsl 4764235343 fix(retrieval): list_gaps returns ORM entities; admin switches back to the domain API
T12 集成实测暴露:select(KbGap) 的结果调 .mappings(),行键是实体类,
GapSummary 按列名取值必然 AttributeError。改为 .scalars() 返回实体;
admin 的缺口看板从临时自实现切回 retrieval.list_gaps(语义单一定义)。
2026-08-14 21:22:51 +08:00
xsl 3961b6fbfa feat(admin): minimal console for KB content operations
apps/admin 独立应用:X-Admin-Token 鉴权(空=全部路由不注册=404)、实体/规则
维护与状态机流转、发布编排(publish/archive → 规则缓存 invalidate +
retrieval invalidate_cache 双失效)、素材登记与审核(tryon_ready 门禁透传)、
缺口看板(T-P2-09,FR-ADM-01~05)。V1 共享令牌鉴权,无角色无审计字段,
操作留证靠 structlog request_id。

测试:单测 tests/unit/test_admin_app.py(10 例:空令牌 404、错/缺/非 ASCII
令牌 40100、正确令牌 200 假服务、发布编排接线、幂等重放无副作用);
集成 tests/integration/test_admin_console.py(5 例:顾问全流程不碰客户端
API、双失效副作用可观察、非法跃迁 40900、素材审核门禁、缺口看板排序、
令牌 401)。483 单测 + 43 集成全绿;ruff/mypy/lint-imports 全绿。

偏差说明:
1. packages/retrieval.repo.list_gaps 有缺陷:对 select(KbGap) 的结果调
   .mappings(),行键是实体名而非列名,GapSummary(row) 取 row.gap_id 必然
   AttributeError(该路径无既有测试覆盖,接真库即暴露)。packages/** 本
   任务禁改,故缺口查询在 apps/admin/routers/gaps.py 以等价 SQL 自实现
   (hit_count 降序、同频次按 last_seen_at 降序、limit/offset 语义一致);
   建议合并阶段修复上游后切回 retrieval.list_gaps。
2. 实体 transition 的 target 收全量四态(任务文档示例仅列 reviewing/
   published/archived):按任务测试要求 published→draft 必须返回 40900
   (状态机语义冲突)而非 schema 422;draft 作为目标由服务层跃迁矩阵恒拒。
3. 规则路由以 rule_key 为路径参数(任务文档写 /rules/{id},但 RuleService
   全程以 rule_key 寻址,无按数字 id 的接口,以代码为准)。
4. 发布编排触发面含 archived(撤稿):依据 retrieval.invalidate_cache 的
   自身契约「发布/归档知识后调用」,比文档字面的「发布成功后」多了撤稿
   方向;reviewing 等其余状态不触发。
5. updated_by/author 记 "admin-console":V1 共享令牌无操作者身份,字段
   留渠道来源;运营账号体系属 V2(见包 docstring 的升级路径)。
2026-08-14 21:19:31 +08:00
xsl 81117a0104 merge: T6 知识库实体与图谱(T-P2-01/05) 2026-08-14 20:09:59 +08:00
xsl d9249400af feat(kb): entity store with lifecycle, graph traversal and color palette
12 类实体统一表 + 状态机 + version 递增 + kb_relation 递归 CTE 1~3 跳
可解释路径(防环)+ kb_color 色板(T-P2-01/05,TD-09/18)。

实现:
- packages/kb/schemas.py:12 类 EntityType 与状态 Literal 校验、
  EntityCreate/Update/Out、RelationCreate、GraphNode/Hop/Path/QueryResult、
  ColorOut;雪花 ID 全部字符串出参(JS 精度)。
- packages/kb/repo.py:实体 CRUD/按名查重/过滤分页搜索;traverse 递归
  CTE(path 数组 + NOT @> 防环,锚点与递归段 varchar[] 显转型);色板读写。
- packages/kb/service.py:状态机矩阵(draft→reviewing→published→archived
  + draft/reviewing→archived 撤稿,非法跃迁 40900,矩阵单测 16 组全覆盖)、
  内容修改仅 draft/reviewing 且 version+1、published 只读;关系写入
  (自环/三元组重复 400、端点缺失 404);色板 role/hex(#RRGGBB) 校验。
- apps/api/routers/kb.py:GET/POST /kb/items、GET/PATCH /kb/items/{id}、
  GET /kb/items/{id}/graph?max_hops=1..3(超界 422);KbService 装配就近
  放路由文件(apps/api/deps.py 本轮禁改)。

测试:单测 77 例(跃迁矩阵、12 类 Literal、非法类型拒绝、hex/role/hops
校验);集成 5 例(真实 PG:生命周期/改 published 被拒/版本递增、关系
约束、2 跳可解释路径+防环、列表过滤分页、色板读写)。全套门禁绿:
ruff check/format、mypy(指定包列表)、lint-imports、单测 315 通过、
集成 21 通过。

偏差说明:
1. 新增 tests/unit/__init__.py 与 tests/integration/__init__.py(仅注释
   的包标记):任务文档要求单测/集成同名 test_kb_entity.py,pytest
   prepend 导入模式按 basename 注册模块会撞名,加包标记唯一化。
2. 关系与色板未挂 HTTP 路由:文档 §3.4 路由清单仅列 items+graph,
   录入入口属运营后台 T-P2-09,本批在 service 层落地并集成直连验证。
3. published→draft 按文档字面矩阵(其余跃迁非法)拦为 40900:改已发布
   内容的既定路径是撤稿归档后重录新草稿(错误 hint 已说明),未私开
   回退通道。
2026-08-14 20:09:12 +08:00
xsl ce429b4011 merge: T11 手机号验证码登录与三重防刷(FR-ACC-04/14,TD-32) 2026-08-14 20:05:43 +08:00
xsl cf9d500bfa feat(identity): phone SMS login with triple rate limiting and anti-abuse
SmsSender Protocol(fake/tencent)+ 验证码生命周期(哈希存储/一次性/
5 次作废)+ 号码-IP-设备三重频控 + 掩码纪律(FR-ACC-04/14,TD-32;
真实发送待签名审核)。

实现:packages/identity/sms_login.py(SmsStore/SmsSender 双 Protocol、
FakeSmsSender 打日志、TencentSmsSender 包官方 SDK 失败映射 61000、
SmsAuthService 验证码生命周期 + UTC+8 时段桶频控);路由 POST
/auth/sms/send、POST /auth/sms/verify(provider="phone" 映射 +
issue_session + TokenPairResponse,与华为通道零耦合)。测试:单测 37 个
(内存 dict 实现 store、注入时钟模拟 TTL;号码规范化/掩码/频控矩阵/
验证码四要素/腾讯通道桩测),集成 7 个(send→verify 双令牌闭环、同号
同 user、脚本化刷量 11 发第 11 次 42911、IP/设备维度、错 5 次作废)。
门禁:ruff/mypy(identity+apps/api)/lint-imports 全绿,单测 275 通过、
集成 23 通过。

偏差说明:
1. SmsSender.send_code 收 (phone, masked, code) 三参而非单一「掩码键」:
   腾讯通道下发必须有真实号码(掩码/哈希无法投递);防泄漏由「实现层
   不得记录 phone」约定 + structlog 全局手机号脱敏兜底。
2. 频控计数按「请求」计(含被拒请求)且在限额检查前自增:否则被拒
   请求不烧额度,攻击者可无限试探;同时这是「连发 11 次第 11 次
   42911」脚本化验收在真实时钟下成立的前提。
3. request_code 对 sender 逃逸的非 AppError 异常兜底包成 61000:
   通道故障不得以 5xxxx 内部错误形式稀释告警(TencentSmsSender 自身
   映射为主,此处仅接漏网)。
4. wire 请求模型(SmsSendRequest/SmsVerifyRequest)定义在 sms_login.py
   内(任务文档允许的两选一,未新建 sms_schemas.py)。
2026-08-14 20:04:46 +08:00
xsl 6c77ff4989 feat(identity): Huawei Account Kit login via OIDC JWKS verification
POST /auth/huawei:JWKS 验签(kid 缓存/轮换)+ claims 严格校验 +
UnionID 映射 + T4 双令牌签发(FR-ACC-01;真实联调待 AGC 配置)。

- packages/identity/huawei.py(新建):HuaweiIdTokenVerifier——RS256 验签,
  JWKS 内存缓存(TTL 可配 + 单调钟;kid 未命中强制刷新一次重试,锁内用
  拉取代数防止强制刷新被新鲜缓存短路);aud/iss/exp/iat 全查,audience
  空配置直接拒绝而非跳过;union_id 缺失拒绝(降级 open_id 策略不做,
  留联调期决定);错误映射 40102/40101/60000,无新错误码。
  HuaweiLoginRequest/HuaweiClaims 一并定义于此,未动 schemas.py。
- apps/api/routers/auth.py:追加 /huawei 端点;get_huawei_verifier 依赖
  建于路由模块内(缓存到 app.state,可预注入替换),未动 deps.py;
  复用 T4 的 get_or_create_by_external_identity + issue_session +
  TokenPairResponse 与既有 _token_pair/_access_token。
- tests/unit/test_huawei_verifier.py:cryptography 自造 RSA + 本地 JWKS
  (零外网),19 例覆盖合法 claims/错误签名/过期/aud/iss/iat 缺失/
  audience 未配置/union_id 缺失/HS256 混淆/JWKS 拉取失败/kid 轮换/缓存
  TTL。
- tests/integration/test_auth_huawei.py:FakeFetcher 预注入 app.state,
  真实 PG 验证首次建用户 + 双令牌、同 union_id 复登同 user_id、新 access
  调 /quota、refresh 可轮换、伪造 token 40102。

检查:ruff/format/mypy(T6 列表)/lint-imports 全绿;单测 257 passed
(基线 238 + 新增 19);集成 18 passed(含新增 2)。

偏差说明:无
2026-08-14 19:59:15 +08:00
xsl d5f366fa4c chore(prep): shared groundwork for the Sprint-2 parallel batch
第二批(T6~T11)协调提交:
- 迁移 0005:kb 七表(entity/relation/rule/chunk vector1024+tsv 生成列/
  asset/color/gap),HNSW+GIN 索引,FK 级联
- ORM:七个 Kb* 模型(pgvector Vector/TSVECTOR/Computed)
- 配置:检索与 embedding、华为 JWKS、短信全参数组
- 错误码:40110/40111/42910/42911/61000
- 骨架路由:kb.py + auth_sms.py(已注册);依赖:tencentcloud-sdk-python-sms
  + spike extra(onnxruntime/opencv);gitignore:models/ 与 spike 输出
2026-08-14 17:01:43 +08:00
xsl ddec3973ad merge: T1 四档分级降级熔断(T-P1-25b,NFR-COST-02) 2026-08-14 16:28:22 +08:00
xsl a4d59aca30 feat(llm): four-tier daily budget degradation with manual override
把单一日预算熔断升级为架构 §13.3 的四档分级降级(T-P1-25b / NFR-COST-02):

- budget.py:新增 BudgetTier 配置模型与档位判定(current_tier/tier_name/
  blocked_scenes)。阈值相对 LLM_DAILY_BUDGET_CENTS:warn ≥60% 只告警;
  degrade_secondary ≥80% 拒 hd_export/report_regen;degrade_generation ≥90%
  追加拒 hairstyle/tryon;block_all ≥100% 拒全部非 critical、allow 名单
  (默认 agent_chat)放行。高档位 blocked 取「截至当前档的并集」(架构原文
  「追加拒绝」)。critical 场景全档豁免但照常计成本。
- 手动 override:Redis 键 llm:budget:override(值 "1"~"4"),优先于成本水
  位,读不到/非法则忽略;set_override TTL 到当日 UTC+8 24 点,跨预算日必然
  失效。CostCounter 协议相应增加 set/delete。
- 指标:Gauge llm_budget_tier + Counter llm_budget_blocked_total{scene}
  挂到共享 REGISTRY 随 /metrics 暴露;档位上升沿只告警一次
  (budget_tier_raised,实例内记 last_tier)。
- errors.py 新增 SceneDegradedError(tier2/3 拒绝);gateway.py 调用点换成
  ensure_scene_allowed(保留 ensure_within_budget 兼容入口);agent.py 映射
  LLM_SCENE_DEGRADED(60006);100% 档维持 DailyBudgetExceededError/60003 语义。
- 单测:新增 tests/unit/test_tiered_budget.py(27 例:四档边界、各档放行/
  拒绝、critical 豁免、override 加严/放宽/非法值/TTL、配置驱动)。
- 集成:tests/integration/test_vertical_slice.py 新增用例——预算 1 分 +
  block_all allow 清空 + Redis 注入当日成本,POST /agent/chat 返回 60003,
  断言 bl_ledger 中 consume(refunded,-1)+refund(committed,+1) 净和为 0,
  覆盖「reserve 之后才熔断 → refund」路径;fixture 重构为共享
  FakeProvider/fake_client,断言语义未弱化。

门禁:ruff check/format、mypy(packages/llm strict + 其余包)、lint-imports、
单测 196 passed、集成 4 passed。

偏差说明:
1. tests/unit/test_gateway.py 的 test_daily_budget_rejects_non_critical_
   scenes_before_spending 把被测场景 AGENT_CHAT 换成 REPORT_COPY(test_daily_
   budget.py 同用例换 tryon):block_all 档默认 allow=["agent_chat"](架构
   「仅保留纯文本对话」)后 agent_chat 在 100% 档不再被拒,原场景选择与新
   语义冲突;断言本身(进 provider 前被拒)未弱化。
2. 集成测试的 block_all 档配置为 blocked="*"+allow=[](文档示例为
   blocked=["agent_chat"]):后者按档位规则会走 SceneDegradedError/60006,
   前者才能按设计走既有 60003 语义(文档允许二者取其一且"按上面设计应为
   60003")。
2026-08-14 16:27:21 +08:00
xsl fcd7e63ce5 merge: T2 任务奖励领取(POST /quota/tasks/{task_key}/claim,FR-BILL-03) 2026-08-14 16:23:46 +08:00
xsl ecdfbb64c3 feat(billing): task bonus claim endpoint with idempotency and cap
POST /quota/tasks/{task_key}/claim:配置驱动任务注册表、
grant_bonus 纯域函数、ledger 幂等键防重、task_bonus_max 封顶(FR-BILL-03/04)。

实现:packages/domain/billing/quota.py 新增 BonusDecision/grant_bonus(mypy strict,
恰好到顶允许、超一格整体拒绝)、grant_idempotency_key、PERIOD_TYPES;
packages/billing/service.py 新增 claim_task_bonus(幂等快捷路径 → 行锁 → 封顶判定
→ 同事务更新 bonus_granted 并写 system_grant 流水,delta=0 且 balance_after 按
remaining 口径;IntegrityError 并发兜底回滚为 40910);响应模型 TaskBonusClaimResponse
放新建的 packages/billing/schemas.py;路由挂在 apps/api/routers/quota.py,
rules 由 settings.task_bonus_rules 注入(amount 为占位值,产品定稿后改 env)。
测试:新增单测 11 个(封顶边界、amount<=0、不动 used、hard_cap 独立生效、
默认规则形状与 env 覆盖通路);新增集成 4 个(领取+重复 40910+纯 DB 流水断言、
五类任务全部到账、未知任务 40410、amount=11>max=10 → 40310 且无新流水)。
门禁全绿:ruff check / ruff format --check / mypy 两段 / lint-imports;
单测 180 passed,集成 7 passed。

偏差说明:1) 任务文档提到的 BillingService._load_rule 实际名为 _require_rule,
   直接复用现有助手;2) packages/billing/repo.py 无需新增函数(现有
   get_ledger_by_idempotency_key/lock_or_create_quota/add_ledger 已覆盖本任务);
   3) 文档设想的「TASK_BONUS_RULES_JSON 空值容错」经 env 源实测不成立——
   pydantic-settings 对 dict 字段先做 JSON 解码,空白字符串在 Settings 构造时即
   报错(共享校验器/config.py 属禁改,未动);单测改为覆盖受支持的空配置形态 "{}"。
2026-08-14 16:23:14 +08:00
xsl ae53196657 merge: T4 刷新令牌与服务端可撤销会话(FR-ACC-05) 2026-08-14 16:22:47 +08:00
xsl 140a63effa feat(identity): refresh token rotation with server-side revocable sessions
id_session 落地:opaque refresh(token_urlsafe(48))+ sha256 哈希存储 +
一次性轮换(旧行撤销与新行同一事务)+ logout 幂等撤销 + 已撤销重放检测
告警(V1 不做家族连坐);错误码 40103/40104/40105 接入。新增 POST
/auth/refresh、POST /auth/logout;dev-token 扩展为双令牌,旧字段
access_token/token_type/expires_in/user_id 原名保留,新增
refresh_token/refresh_expires_in(smoke.sh 不受影响)。

测试:tests/unit/test_refresh_tokens.py 覆盖 token 长度/字符域/唯一性、
哈希稳定性与不可逆(7 例,不连网不连库);tests/integration/
test_auth_refresh.py 覆盖轮换后新 access 可调 /quota、旧 refresh 二次
使用 40105、logout 后失效 40105、伪造 40103、TTL=0 过期 40104(5 例)。
门禁全绿:ruff check/format、mypy 两段、lint-imports 4 契约、
单测 176 通过、集成 8 通过。

偏差说明:1) SessionIssued 较任务文档字段清单增加 user_id ——
/auth/refresh 换发 access JWT 必须知道用户,opaque refresh 本身不携带
信息,只能随签发结果带回;2) 过期分支「顺手落库 revoked=true」在 HTTP
路径会随 401 的统一回滚一起丢失(仅事务内标记,已注释说明,不影响判定
语义);3) repo 撤销用 SELECT FOR UPDATE + ORM 变更而非 UPDATE
rowcount —— 对齐 billing.repo 现行模式,且 rowcount 在 mypy 下类型
不可用。
2026-08-14 16:21:52 +08:00
xsl d0522cd495 merge: T5 COS 直传预签名(POST /uploads/presign,FR-SYS-03) 2026-08-14 16:18:16 +08:00
xsl 367b610e93 feat(storage): COS presigned PUT URL endpoint for direct upload
POST /uploads/presign:场景/类型/大小三重校验 + 可审计 object_key 布局 +
SDK 隔离在 CosSigner Protocol 后(FR-SYS-03,T-P1-27)。

实现:packages/storage/schemas.py(PresignRequest/PresignResponse)、
packages/storage/service.py(CosSigner Protocol / QcloudCosSigner /
StorageService.build_presign,bucket 四别名映射到 cos_bucket_*,
object_key 写死 {scene}/{user_id}/{yyyyMMdd}/{uuid4hex}.{ext})、
apps/api/routers/uploads.py(POST /presign,CurrentUserId 认证 + ok 信封)。

测试:tests/unit/test_upload_presign.py 16 例全部经 FakeSigner 注入,
零网络、零 SDK client 构造;覆盖 key 布局逐段断言、三重校验统一 40010、
bucket 别名映射、白名单字符串解析(空串/空格容错)、配置错误分支、
schema 层 size>=1。门禁全绿:ruff check/format、mypy(宽松 48 文件 +
domain strict 7 文件)、lint-imports 4 契约、pytest 185 passed
(基线 169 + 新增 16)。

偏差说明:
1) SDK 实际无 qcloud_cos.method 模块,Method 按官方文档传字符串 "PUT";
   get_presigned_url 参数名为 Expired(非任务文档写的 Expire);配置类
   导出名为 CosConfig(非 CosS3Config)。按文档 §3 "以实际 SDK 签名为准"
   处理,已用假密钥端到端冒烟验证(纯本地签名计算,零网络)。
2) build_presign 增加可选 keyword-only 参数 now(默认 None=当前 UTC),
   供单测注入固定时钟(对齐 DailyBudgetGuard 的 clock 注入惯例),
   不改变文档签名的调用兼容性。
3) 两处防御分支文档未规定,均配了单测:场景 bucket 别名非法报
   CONFIG_ERROR 而非 40010(配置错误不应让客户端背 4xx);COS 密钥为空
   在 SDK 构造前报 CONFIG_ERROR,避免未处理的裸 500。
2026-08-14 16:17:49 +08:00
xsl 64175a1ea1 feat(analytics): POST /events/batch with fail-close validation
ob_event 落库通道:形状校验 + props 字节上限 + client_ts 合理性 +
PII 键剔除,整批原子(FR-SYS-06 服务端部分)。

实现:packages/analytics/schemas.py(EventIn/EventBatchRequest/
EventBatchResponse + 事件名形状正则常量);service.py 拆出纯函数
validate_batch(批量/props 字节/事件名形状/client_ts 窗口/PII 剔除,
任一不过整批 40020),AnalyticsService.record 组装 ObEvent 逐条
add + 一次 flush;routers/events.py 提供 POST /events/batch
(/api/v1 与 /v1 别名,认证必需)。

测试:单测 tests/unit/test_events_validation.py 8 例(合法批、超批量、
props 超字节/不可序列化、事件名形状含尾随换行、client_ts 双边界、
naive 时区按 UTC、PII 键含大小写变体剔除);集成
tests/integration/test_events.py 3 例(两前缀可用+落库断言、
非法批 40020 且库无半条、无 token 40100)。门禁全绿:ruff
check/format、mypy(T2 列表 + domain strict)、lint-imports 4 契约、
单测 177 通过、集成 6 通过。

偏差说明:event_name 正则未写成 Field(pattern=...),而是作为
EVENT_NAME_PATTERN 常量放 schemas.py、由服务层 validate_batch 强制——
若挂在请求体校验上,非法事件名会先被 RequestValidationError 拦截、
返回通用 40001/422 信封,无法满足任务契约 §4「大写 event_name →
40020」的集成行为。正则本体与文档一致。
2026-08-14 16:16:26 +08:00
xsl 9e7a886863 chore(prep): shared groundwork for the parallel task batch
多 agent 并行批次的协调提交(消除全部共享文件冲突面):
- 迁移 0004:id_session + ob_event 两表(T3/T4 用,任务不再写迁移)
- ORM:UserSession / ObEvent;错误码:九个新码(40xxx/60006,含 meta)
- 配置:四档 tiers / refresh TTL / 任务奖励注册表 / 埋点上限 / COS 直传全套
- 骨架:packages/analytics + packages/storage + events/uploads 空路由(已注册)
- deps:AnalyticsServiceDep / StorageServiceDep;DailyBudgetGuard 接收 tiers
- uv add cos-python-sdk-v5;import-linter/CI/Makefile 纳入两个新包
2026-08-14 16:02:54 +08:00
xsl 2009aa938c fix(billing): make quota seed and column names match the product SSOT
The product plan is the only source for quota numbers, but the 0001 seed
used a second set (agent_chat 20/50, tryon hard 10) and omitted hairstyle,
hd_export and report regen. Migration 0003 replaces the seed and renames
points_reserved → quota_reserved and bonus_value → bonus_granted so the
docs and the schema stop contradicting each other.

Effective limit now clips at hard_cap. CI runs the integration slice
against the Postgres/Redis services it already starts.
2026-08-14 11:28:35 +08:00
xsl f46ecc5eb0 fix(llm): price by call time and stop paying for thinking tokens
Two defects that only surface against the real DeepSeek API, both of which
break the cost model rather than the code:

deepseek-v4-flash enables thinking mode by default. The same single-turn
question took 13.5s with it on and 1.8s with it off, and the thinking tokens
are billed at the output rate. That misses NFR-PERF-02b (15s per turn) and
inflates every conversational call. SceneConfig now carries an explicit
`thinking` flag, all four V1 scenes set it to false, and the provider always
sends the parameter rather than relying on the upstream default. Reasoning
tokens are recorded separately in Usage.tokens_reasoning so the cost of ever
turning it back on is visible.

DeepSeek moves to peak/off-peak pricing on 2026-08-17, with peak (Beijing
09:00-12:00 and 14:00-18:00) at twice the off-peak rate. Reports are generated
mostly during peak hours, so a single blended rate would leave ob_ai_call --
the only input to V2 pricing -- off by nearly a factor of two.
PeakAwareRateTable resolves the rate at call time; cost is computed and stored
then, never recomputed later at whatever the current rate happens to be.

Also here:
- Replace the live API key in the redaction test with a synthetic one. The
  test that proves we redact secrets was itself a secret leak waiting for its
  first commit.
- Correct LLM_COST_RATES_JSON, which held placeholder rates.
- Make ids.SnowflakeGenerator take an injectable clock so rollover and
  clock-rollback are testable without patching a slots class.
- Rename exceptions to the -Error suffix (N818) and clear the remaining ruff,
  mypy and import-linter findings, including two dead lines: a duplicate
  pytestmark assignment and a no-op assert on a None return.

Gates: ruff, mypy (96 files incl. tests), 4/4 import contracts,
detect-secrets, 166 unit + 3 integration tests, migration up/down round-trip.
2026-08-14 00:54:40 +08:00
xsl 746b66da01 feat(api): vertical slice POST /v1/agent/chat and the Celery ping path
One request exercises every layer: JWT auth, quota reservation, a versioned
prompt through the LLM gateway, a cost row, quota commit or refund, and the
unified envelope. The trivial ping task proves the queue routing and the
tk_task transitions before any real long-running task is written.

Routes are mounted at the /api/v1 base path of 架构文档 §9.1; /v1/* is
rewritten to it at the ASGI layer so both spellings work from one route table.
2026-08-14 00:26:39 +08:00