xsl
90349f735a
feat(admin): 用户行为分析看板——/admin/api/v1/users(列表/详情,identity 掩码后输出)、/events(多维过滤分页)、/stats/overview(按天×事件 PV 透视+活跃序列);X-Admin-Token 鉴权沿用现有 admin 装配
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
2026-09-15 00:02:55 +08:00
xsl
df460eeea8
feat(auth): 邮箱验证码登录通道(正式兜底,替代不可用的短信)——/auth/email/code + /auth/email/login,SmtpEmailSender(stdlib smtplib,无新依赖)+ FakeEmailSender(仅本地);频控对齐短信口径(60s 冷却/邮箱 1h≤5·24h≤10/IP≤20/设备≤10);新增 EMAIL_*/SMTP_* 配置与 40112/40113/42912/42913/61001 错误码
2026-09-15 00:02:55 +08:00
xsl
5c4f1fc75a
feat(report): 形象深度诊断全链路——POST /reports/{id}/full 派生 tier=full 报告,复用 basic 输入与 32 维结果;full 流水线(无视觉阶段)LLM 扩写四大块(十二季定位/规则色板/4场景穿搭/30天计划),三重校验+语义重试+规则兜底降级;删除终身一次硬校验(40920 下线,重复生成由配额兜底);validation 枚举一致性校验放宽(十二季词表);迁移 0003 SSOT 补 full_report 规则行
2026-09-15 00:02:44 +08:00
xsl
a1d0171fc5
feat(uploads): 本地磁盘直传(COS 降级路线)+ 报告照片存在性校验
...
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- POST /uploads/direct:原始字节 body + Content-Type 申报 + scene query(默认 report_photo),场景/类型/大小三重校验复用 storage 既有规则,落盘 var/uploads/<scene>/<user_id>/<uuid>.<ext>(config 新增 upload_dir),key 布局与 COS 同构;不加 python-multipart 依赖
- storage/local.py:save_direct_upload + require_local_upload(resolve 归并 + is_relative_to 归属收敛 + 存在性校验,越界/模拟/不存在一律 404)
- report/service.create_report:资格校验后、配额预扣前插入照片存在性校验(模拟 key 被拦且不消耗额度;regenerate 复用已验 photo_meta 不复核)
- 测试:test_upload_direct 14 单测 + test_upload_direct_flow 4 集成(真 key 全链路/假 key 404 零流水/401/40010)
偏差说明:test_report_flow._create_report 改走 /uploads/direct 拿真 key(存在性校验必然封死假 key),40920 与幂等重放两条断言语义未动。
2026-09-12 00:24:01 +08:00
xsl
76a0502ea3
feat(billing): 任务换额度定稿闭环——claim 凭据校验(40311)+ 定稿 4 任务目录
...
- claim_task_bonus 在幂等快捷路径前做完成凭据校验:report_feedback 查当日 rp_feedback、first_report 查 succeeded rp_report,无凭据 40311 且不打流水;sign_in/share_report 不校验(产品决策:签到行为即领取,分享内测期接受端侧自报,hard_cap 锁死刷量收益)
- task_bonus_rules 默认值与 .env.example 同步产品定稿(产品方案 §3.2,2026-09-11):sign_in 对话+10/day、report_feedback 发型+2/day、share_report 换装+3/day、first_report 重生成+1/lifetime;下架 invite_friend/profile_complete
- billing/repo 新增只读凭据探测 has_feedback_between/has_completed_report(import-linter 禁 billing→report,直查 shared.models 的 rp_* 表,永远只读)
- 测试:test_task_bonus_eligibility 5 用例(40311 两分支/凭据后 200/40910/40410/零流水)
偏差说明:test_task_bonus 与 test_task_bonus_rules 断言数值随定稿更新(发放/幂等/封顶语义未动);quota.py docstring「占位值」说法顺手更正。
2026-09-12 00:23:43 +08:00
xsl
7bcbb8d40b
feat(agent,report): 会话持久化与画像注入、报告反馈与预约留资——PM/开发双Agent迭代1-3
...
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- agent: ag_conversation/ag_message 表(迁移0007),上下文组装注入最近报告画像摘要
(report.profile.latest_profile_summary 经 apps 层闭包注入,同层不互调),历史带最近
AGENT_HISTORY_TURNS=10 轮;POST/GET /agent/conversations 与消息历史接口
- agent 修复:ChatRequest.conversation_id 改 str(雪花ID超2^53,JS Number丢精度);
_build_context 挪进 reserve commit 后同一 try,异常走 refund 兜底
- report: rp_feedback(维度反馈 upsert,认同率/修正率 SQL 可算,T-P3-21 最小版——
L1 记忆未建,不回写结论)+ rp_reservation(预约留资幂等,T-P3-22,预约率口径
行数/full_report.card_view 去重曝光,门槛≥8%),迁移0008
- 测试:全量 606 passed;ruff/mypy/lint-imports 全绿;E2E 真实 deepseek-v4-flash 验证
画像注入与历史携带
2026-09-11 21:50:58 +08:00
xsl
38dfaaf479
merge: T15 报告编排骨架与失败退款(T-P3-07/09)
2026-08-14 22:18:51 +08:00
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
fc3393887f
merge: T16 确定性视觉与 32 维诊断装配(T-P3-01/06)
2026-08-14 22:15:37 +08:00
xsl
e7076c9c01
feat(report): deterministic vision provider and 32-dimension diagnosis assembly
...
dimensions.py 逐行编码需求文档 §4(32 项四要素、缺失显式 skipped)+
VisionProvider 接口位与确定性实现 + 规则引擎联动 rule_hits(T-P3-01/06)。
实现:
- dimensions.py:DimensionSpec 恰 32 项(number 1~32 连续,33/34 永不复用),
basic_coverage 口径机器可读:full 10 / label 3 / partial 8 / none 11,
初级覆盖 21/32(BASIC_FULL_KEYS + BASIC_COVERED_KEYS)。
- vision.py:VisionProvider Protocol(派生特征形状写进 docstring,真实
RTMPose/BiRefNet 实现后续按此替换)+ DeterministicVisionProvider(问卷
BMI/自评驱动体型与肤色,sha256(object_key+canonical问卷) 驱动其余枚举,
同输入同输出)+ build_vision_provider(未知名 CONFIG_ERROR)。
- vision_stage.py:derived_features 已存在 → skipped/reused(T-P3-09);
否则分析 → 写中间态并 flush;photo_meta.quality_flags 透传进产物(T-P3-01)。
- diagnosis_stage.py:32 维恒全键,缺输入显式 skipped+原因(T-P3-06 红线);
每维四要素 value/confidence/source/evidence/rule_hits + status;构造
RuleContext → RuleService 载入 published 规则 → evaluate,body_shape/color
类命中回落 #5/#19/#21,全量命中聚合进 #28/#30 的 rule_hits 与 evidence;
色板 #19/#20/#21 从 kb_color 按季型读取,库空 → skipped「色板未录入」。
测试:单测 29 项(32 项完整性/编号连续/确定性/工厂拒绝未知名;四要素齐全
性——故意缺问卷+缺视觉断言 32 键全在且全部 skipped 带原因;规则命中进
rule_hits 用内置最小规则集不连库);集成 1 项(种 2 条 published 规则 →
VisionStage → DiagnosisStage:中间态落库与复用 skipped/reused、32 维全键、
rule_hits 出现、kb_color 空时色板 skipped、coverage basic_full=10/32)。
全绿:ruff check/format、mypy packages/report、lint-imports;单测 536 通过
(3 处既有 skip)、集成 46 通过。
偏差说明:
1. 任务文档写 RulesService,实际类名为 RuleService(packages/rules/service.py),按实际代码调用。
2. kb_color 联表键采用四季代码 spring/summer/autumn/winter(仓库无既定口径),已在 vision.py 注释声明。
3. 任务文档只给 basic_coverage 四枚举,需求表格中的「粗判/8色/3色」归入 partial、「Top1」归入 label。
2026-08-14 22:14:54 +08:00
xsl
e78ba3feef
feat(report): LLM copy generation with mandatory post-validation and fallback
...
report_copy/v1 prompt(系统段零变量)+ 结构化 ReportCopy + 素材组装
(skipped 不进 prompt/超长裁剪)+ 贬损词表/一致性/绝对化三重校验 +
重试一次后降级规则直出模板(T-P3-10/11)。
- copy.py:ReportCopy(schema 宽进严出取舍见注释)/compose_prompt_material
(ok 过滤+6000 字符裁剪,低优先级先丢、硬结论不丢,记录 dropped_keys)/
render_fallback_copy 零 LLM 降级模板
- validation.py:PEJORATIVE_TERMS 35 词(源自合规 §3,初版内嵌注释)+
一致性三规则(枚举同款含别名/色值在板内/建议句需规则支撑·宽松口径)+
绝对化用语兜底
- copy_stage.py / validation_stage.py:任何出口 passed=true 或抛错,
语义层重试一次(与网关解析层重试独立),仍违规降级模板并 fail-close 自证
- 单测 25 例(词表含变形/三规则正反/降级零违规)+ 集成 4 例
(FakeProvider 全栈:干净通过/脏一次修正通过/持续脏降级且最终文案
零贬损词——黄金断言);全量 529 单测 + 49 集成全绿
偏差说明:
1. prompts/report_copy/v1.txt 非新建——LLM 批次已放置初版(user 段变量
为 {payload}),按本任务契约改写为 {dimensions_json} 并补内容红线
(该文件任务文档明确归 T17,无与他人冲突)。
2. check_conclusion_consistency 按契约签名 (copy, dimensions) 之外增加
可选 rule_hits 关键字参数:规则③的支撑判定需兼容命中内嵌在维度/
顶层列表两种 T16 契约形态,缺省时不追责(宽松口径)。
3. 绝对化用语未按单字「最」拦截,取营销语境复合词(最好/最合适/第一/
百分百等 17 词):单字会误杀「最近/最终」等中性词,红线范围不变。
4. 集成文件未挂 MEIDA_RUN_INTEGRATION skipif:用例为 FakeProvider 内存
装配、不依赖真实 PG/Redis,按任务要求仍放 tests/integration/ 并带
integration marker。
2026-08-14 22:09:09 +08:00
xsl
fa3da930cc
merge: T12 极简运营后台(T-P2-09,FR-ADM-01~05)
2026-08-14 21:20:06 +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
a044ea2e47
feat(retrieval): TTL+LRU result cache with generation-based invalidation
...
TtlCache(全输入进键/惰性过期/容量逐出/命中零 DB)+ search 接线
(空结果不缓存,保住 gap 埋点)+ invalidate_cache 真实现(T-P2-10)。
实现:
- packages/retrieval/cache.py(新建):CacheKey 全输入进键(query/body_shape/
occasion/entity_types 排序归一/rule_boost 排序序列化哈希/top_k/失效代数);
TtlCache 惰性过期 + OrderedDict LRU(超限 popitem 最旧)+ hits/misses/
evictions 计数与 stats();clock 可注入(单测免 sleep,默认 monotonic);
asyncio 单循环不加锁的前提写在模块 docstring;正确性红线(只缓存不可变
值、命中零 DB)同处。纯逻辑,不 import 任何项目内模块(值类型由 service
以 PEP 695 泛型注入,避免循环依赖)。
- packages/retrieval/service.py:__init__ 加 keyword-only cache=None(默认
禁用,行为与现状一致,既有调用点不受影响);from_settings 按 Settings
装配进程级共享缓存(retrieval_cache_enabled=False → None;TTL/容量变化
时重建);search() 在 embed 之前查缓存——命中直接返回(零 DB、零 embed),
写入与命中都做防御性拷贝(results 是 list,调用方拿不到缓存内引用),
仅有结果时写缓存(空结果不缓存,record_gap 每次都跑);invalidate_cache()
签名不变:自增 repo.INVALIDATION_VERSION(代数失效,旧代键永不命中,
与 T12 的契约保持)+ 清空共享实例(仅为立即回收内存,失效正确性只
依赖代数)。repo.py 未做任何改动。
测试:
- 单测 tests/unit/test_retrieval_cache.py(21 例,fake repo/embedding +
注入时钟,不连库):同参二连查零 DB 且结果相等、键分量矩阵(query/
occasion/body_shape/entity_types/rule_boost/top_k 任一变化 → miss)、
TTL 过期边界(60.0 过期/59.9 命中)、LRU 逐出与命中率/计数、空结果
不缓存且 gap 埋点每次触发、invalidate_cache() 代数失效(含自增语义
保留断言)、cache=None 与现状一致、from_settings 装配开关与共享实例、
rule_boost 哈希确定性。全量单测 494 passed(基线 473 + 新增 21)。
- 集成 tests/integration/test_retrieval_cache.py(2 例,真实 PG + pgvector):
发布新实体 + invalidate_cache() 后同参检索立即可见(失效前缓存仍只吐
旧实体,证明缓存在工作——T-P2-10 验收语义);不失效、TTL=1s 到期
(asyncio.sleep 1.1s)后新实体自然可见(双路径验收)。集成全量 40 passed。
偏差说明:无(invalidate_cache() 在代数失效之外顺带清空共享缓存实例,
失效正确性只依赖代数,清空仅为立即释放内存)。
2026-08-14 21:16:51 +08:00
xsl
9c1802130d
chore(prep): shared groundwork for batch-3 (admin/golden/cache)
...
第三批(T12 后台/T13 黄金集/T14 检索缓存)协调提交:
- kb:KbAssetService(登记/审核,tryon_ready 只能随 approved 打开)+ Asset 模型与 repo
- retrieval:list_gaps 缺口看板查询 + invalidate_cache() 钩子(T14 背后实现真缓存)
- 配置:ADMIN_TOKEN(空=后台 404)+ 检索缓存三参数
- pytest golden marker(默认跳过,CI 强制跑)+ Makefile golden + CI 防线 5c
- 测试隔离修复:test_auth_sms 加 autouse 清 sms:* 键夹具(频控计数跨运行
累积会毒化后续运行——随机 IP 空间仅 254 且部分用例固定 device_id)
2026-08-14 20:57:52 +08:00
xsl
aa99db4c17
merge: T8 pgvector 混合检索与缺口埋点(T-P2-03/04/06,TD-08)
2026-08-14 20:13:00 +08:00
xsl
aa1287d83d
feat(retrieval): hybrid search over pgvector with hard filters and gap tracking
...
DeterministicEmbedder(可换 BGE-M3 的接口位)+ kb_chunk 双索引写入
+ 硬过滤→向量→关键词→RRF 重排管线 + kb_gap 缺口埋点(T-P2-03/04/06,TD-08)。
实现:
- embedding.py:EmbeddingProvider Protocol + DeterministicEmbedder
(sha256 种子→归一化 1024 维假向量,同文本同向量/异文本近似正交)
+ build_embedding_provider 工厂(未知模型名抛 CONFIG_ERROR)。
- repo.py:upsert_chunks(整实体先删后插,逐行带 embedding_model/version,
可重跑判据 T-P2-03);hybrid_search(§8.3 CTE:硬过滤 published/
entity_types/occasion/avoid_body_shapes → <=> 余弦 topN → ts_rank topN
→ RRF);record_gap(咨询锁串行化同 query+filter_ctx 的 hit_count 递增)。
- rerank.py:RRF/权重/新鲜度纯数学(独立于 SQL 以便单测逐项断言)。
- service.py:RetrievalService.search 编排 embed→检索→(空结果)gap 埋点,
SearchOutcome.degraded 标记降级。
测试:单测 +30(test_embedding 确定性/维度/归一化/工厂拒绝、test_rerank
公式逐项断言、test_retrieval_service 编排打桩);集成 +5
(test_hybrid_retrieval:禁忌实体同文最近向量仍零出现、draft 零出现、
关键词路径、换模型整组替换、kb_gap hit_count=2、partial 索引 EXPLAIN 验证)。
全绿:ruff / mypy(retrieval+shared+domain) / lint-imports / 268 单测 /
21 集成。
偏差说明:
1) 新增 rerank.py(任务文档只点名 embedding/repo/service,但重排数学必须
不连库可单测,独立模块是最低成本实现,仍属 packages/retrieval/**)。
2) upsert_chunks 的 upsert 粒度是实体(先删后插):kb_chunk 无业务自然键,
行级 ON CONFLICT 不可行;换模型名=新版本整组替换,不留旧版本残向量。
3) 禁忌过滤用 COALESCE(attrs->avoid_body_shapes, '[]')兜底:jsonb NULL
的 ? 运算结果是 NULL,直接 NOT 会把未声明禁忌的好实体一并排除。
4) 关键词路过滤用 tsv @@ tsquery 而非 ts_rank>0:ts_rank 对不匹配文本返回
~1e-20 正 epsilon,阈值过滤会把无关 chunk 全放进关键词路。
5) RRF 分量按 1/(k+1) 归一到 (0,1],使 0.45/0.2/0.25/0.1 权重量纲可比
(原始倒数 0.45×1/61 与规则项 0.25×1 完全不可比)。
2026-08-14 20:12:27 +08:00
xsl
f295cbc225
merge: T7 规则引擎·数据驱动决策表(T-P2-02,TD-10)
2026-08-14 20:11:23 +08:00
xsl
e50a9424a1
feat(rules): data-driven decision table engine with hard constraints
...
kb_rule DSL 解析 + 纯域求值(优先级分组/硬约束/用户禁忌压过专业推荐)
+ 400 条基准 + 黄金集雏形(T-P2-02,TD-10)。
实现:dsl.py 封闭词汇表 pydantic 校验(未识别键拒绝)+ match 纯逻辑
(等值/include/exclude/min-max,缺字段不命中);engine.py 纯域求值
(priority 降序、confidence 次序、is_hard avoid → hard_blocks、高优先级
avoid 剔除冲突 recommend);service.py+repo.py published 整表载入进程内
缓存 + CRUD + draft→published→archived 状态机(同 T6 语义)。
测试:DSL 解析与 match 矩阵、排序/硬约束/禁忌优先、状态机矩阵、
黄金集 11 组(含用户禁忌压过专业推荐与多规则叠加)、400 条 × 1000 次
基准(实测均值 0.26ms/次、p95 0.41ms,预算 50ms);集成 3 用例
(published 载入命中拦截、draft 不载入、DSL 写入拒绝与状态机守卫)。
单测 310 通过(+72),集成 19 通过(+3)。
偏差说明:
1. 冲突仲裁按通用优先级语义实现(高优先级 avoid 剔除"严格更低"优先级的
冲突 recommend),任务文档的"用户禁忌(≥95)永远压过专业推荐(≤90)"
是该规则的特例,黄金集已含该场景用例。
2. include 子句按字段类型取语义:标量字段=命中列表任一值;集合字段=
列表全部在场(子集,精确命中);文档未区分,此实现两类字段均可用。
3. 单测引擎文件命名为 tests/unit/test_rules_evaluation.py(而非
test_rules_engine.py):与契约指定的集成文件 tests/integration/
test_rules_engine.py 同名会导致 pytest 收集冲突(tests 目录非包)。
4. 集成测试每用例独立 engine 并 dispose:pytest-asyncio 每用例一个事件
循环,直接用全局 session 工厂会让连接跨 loop 污染其它集成用例。
5. 规则不提供物理删除(命中链需可追溯,下线走 archived);kb_rule 契约
无 reviewing 状态,状态机为 draft→published→archived(draft 可撤稿)。
2026-08-14 20:10:21 +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
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
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
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
93b88db2f5
test: unit suite for the gateway invariants plus an integration slice
...
Covers routing, failover, the circuit breaker, structured-output retries, the
cache-prefix validator, cost computation including cached-token pricing, the
daily budget breaker, the quota protocol, the task state machine and log
redaction. HTTP is mocked -- no unit test spends money. The integration test
runs the real slice against PostgreSQL and Redis with a faked provider.
2026-08-14 00:26:39 +08:00