feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复

M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2026-06-20 10:39:58 +02:00
parent 5fb7bfb1de
commit 765dbdfbd4
161 changed files with 17330 additions and 208 deletions

View File

@@ -14,6 +14,64 @@
---
## [2026-06-19] Kimi Code OAuthtoken 存储包 + 刷新启发式 + 省略 scope + 后台轮询走 jobs — @backend (K1.3)
- 背景Kimi 订阅 plan 走 device-flow OAuth需定 token 持久化形、何时刷新、device authorization 是否带 scope、后台轮询如何挂。
- 选择:
1. **存储包**`TokenSet{access_token, refresh_token, expires_at}` 序列化为 JSON 串经 Fernet复用 `encrypt_api_key`,工作在 str 上)加密入 `provider_credentials.oauth_enc`C2 扩 K1.1 列)。`expires_at` 存**服务端驱动的绝对 UTC 时刻**(从 `expires_in` 计算入库时刻)。明文 token 绝不进日志/响应/job 结果。
2. **刷新启发式**:建网关时若 access token 剩余寿命 < `MIN_REFRESH_BUFFER_SECONDS=300`300s 缓冲,对 ~15min access 足够),调 `refresh` 换新包并 `upsert_oauth_credential` 持久化(下次复用)。任务文档原提 `max(300, 0.5*expires_in)`——本实现采固定 300s 缓冲(不存原始 expires_inKISS对 15min access 等价于 0.5*expires_in=450s 略激进但安全)。
3. **省略 scope**device authorization 不带 `scope`(匹配研究确认的 kimi-cli v1.41.0+server 拒绝时再加 `scope=kimi-code` 兜底——本期先不带。client_id env `KIMI_CLIENT_ID` 可覆盖默认。
4. **后台轮询走 jobs**`POST .../start` 创建 `jobs(kind="kimi_oauth")` 返 202 + user_code复用 K1.1/M4 `run_job` 在独立 session 上**循环** `poll_token`(每 interval 秒,`authorization_pending` 续轮询、`slow_down` 增大 interval、过期/拒绝抛 AppError 置 job failed成功落加密 token + job done结果只 `{connected, provider}`)。前端轮询 `GET /jobs/{id}`
- 理由 / 取舍:复用既有 jobs/run_job 基建(无新长任务机制);固定 300s 缓冲省去存 expires_inYAGNI。代价refresh-on-build 每次建网关多一次解密 + 可能一次刷新网络调用(仅临近过期时)。
- 影响C3 扩OAuth 3 端点C2 扩 K1.1 的 `StoredCredential` 收口api_key_enc→nullable + auth_type/oauth_enc`_build_provider_adapter` 加 OAuth 分支。K1.4 前端连接流、K1.5 E2E真账号联网验证含封号风险用户自担
## [2026-06-19] Kimi Code OAuthdevice authorization **重新带 `scope=kimi-code`**(实测 401 后修正,超越上条第 3 点)— @backend (K1.3 fix)
- 背景K1.5 真账号联网实测——device-flow 登录成功拿到真 JWT但调 `api.kimi.com/coding/v1` 被拒 `401 Invalid Authentication`。根因device_authorization 省略了 `scope`,拿到的 token 缺 coding entitlement。
- 选择:`start_device_authorization` 的请求体在 `client_id` 之外加 `scope=kimi-code`(模块常量 `KIMI_CODE_SCOPE`)。**修正上条决策第 3 点的「省略 scope」**——`scope=kimi-code` 是 coding-agent 文档化 OAuth scope`ooojustin/opencode-kimi` `constants.ts` 发送之kimi-cli v1.41.0 已不发但服务端仍接受)。
- 范围scope **只**放 device_authorization 请求token 交换(`poll_token`/刷新(`refresh`**不带** scopeOAuth device flow 惯例,参考实现亦然)。
- 影响device-auth 请求体现为 `{"client_id": <id>, "scope": "kimi-code"}`K1.3 单测 `test_start_device_authorization_parses_response_and_sends_scope``data["scope"]=="kimi-code"`。无 OpenAPI 形变,前端无需 re-gen。
## [2026-06-19] Skill registry + 表权限沙箱迁回 `ww_skills` 包(@devops 已纳入 workspace— @backend (M5 R3)
- 背景T5.5 曾因 `packages/skills` 非 uv workspace member 把 `skill_registry`/`skill_permissions` 暂落 `ww_core.domain``ww_skills/__init__` 做再导出门面(见上 T5.5 决策)。@devops 现已把 `packages/skills` 做成真 workspace member`ww_skills` 可 import、有自己的 pyproject、`uv sync` 完成)——前提消除。
- 选择:把两个模块**物理迁入** `packages/skills/ww_skills/{skill_registry,skill_permissions}.py`(内部相对 import 从 `ww_core.domain.skill_permissions` 改为 `ww_skills.skill_permissions``ww_skills/__init__` 改为**直接导出**(不再 re-export ww_core删除 `ww_core.domain` 的两个模块文件 + `__init__` 里的 skill 导出。所有 importer`apps/api` `generation.py`/`project_deps.py` + `test_generation.py`)改 `from ww_skills import …`skill 单测从 `packages/core/tests/` 移到 `packages/skills/tests/`import 也改 `ww_skills`)。
- 理由 / 取舍恢复任务文档原定位skill 逻辑归 skills 包),物理位置与 §5.6 措辞一致ww_core 不再承载 skill 符号(关注点分离)。代价:`packages/skills/pyproject.toml` deps 需校准——`ww_skills` 直接 import `ww_db`/`ww_agents`/`ww_shared`/`ww_llm_gateway`(`Tier`),故去掉不再用的 `ww-core`、补 `ww-llm-gateway` + `sqlalchemy``SqlSkillRepo``select`/`AsyncSession`);改后 `uv sync` 重建 ww-skills。
- 影响C8 实现位置更新contracts.md C8 状态加「M5 R3 迁入 ww_skills 包」);消费方 import 路径从 `ww_core.domain`/`ww_core.domain.skill_registry``ww_skills``ww_core.domain.skill_*` 导入路径**已失效**(凡 grep 到旧路径须改)。`packages/skills/pyproject.toml` 改了 deps@backend 可编辑的 skills 目录内,但属打包元数据)——已 PROGRESS 通知 @devops 复核根锁。
## [2026-06-19] `build_gateway_for_tier` 改用 `build_adapter` 工厂per-provider 适配器)— @backend (M5 R1 #2)
- 背景T5.4 接线只为回退链里的每个 provider 一律建 `OpenAICompatAdapter`——Anthropic/Gemini 即便配进 `tier_routing` 链也因 `_PROVIDER_BASE_URLS` 无 base_url 被跳过、拿不到真实适配器。@llm 加了 `build_adapter(provider, *, api_key, base_url=None)` 工厂C1 扩 follow-up #2)按 provider 选适配器类。
- 选择:`_build_openai_compat_adapter` 改名 `_build_provider_adapter`去掉「base_url 缺失即返 None」分支改调 `build_adapter(provider, api_key=…, base_url=_PROVIDER_BASE_URLS.get(provider))`——OpenAI 兼容 provider 传 base_urlAnthropic/Gemini 传 None 走原生适配器。仅在凭据缺失时返 None回退链跳过。保留既有 503/凭据/单 provider 兼容行为。
- 理由 / 取舍:最小改动让回退链支持非 OpenAI-兼容 provideradapter 选择逻辑收敛到网关包的工厂单一真源apps/api 不再硬编码适配器类。代价:真实 Anthropic/Gemini 仍需 @devops 加 SDK 依赖(适配器懒导入)。
- 影响C3 扩build_gateway_for_tier 接线段更新);`apps/api` 去掉 `AsyncOpenAI`/`OpenAICompatAdapter` 直接 import。T5.7 切 provider E2E 可扩 Anthropic/Gemini 路径。
## [2026-06-19] 设定库 Codex 读端点复用 C5 读侧 + 反向 JSONB 解包 — @backend (M5 R1)
- 背景PROGRESS 余项①——Codex 设定库本应展示 characters/world_entities **读/管理**视图,但后端只有生成/入库写端点,前端只能「生成+本次入库会话内回显」。
- 选择:加 `GET /projects/:id/characters` + `GET /projects/:id/world_entities`**复用 C5 读侧** `SqlCharacterRepo`/`SqlWorldEntityRepo`(经既有 `get_memory_repos` dep不新增 repo、不动 assemble响应做 T5.2 ingest 形变的**逆向**解包(`{"items":[...]}`/`{"text":...}`/`{"rules":[...]}`→裸 list/str复用既有 `_existing_characters` helper。无行返空列表非 404列表语义
- 理由 / 取舍KISS——读端点是写入形变的镜像复用已有读侧 + helper 零新依赖GET 与 POST 同路径靠 method 区分无冲突。代价:读侧 CharacterView 仍是裸 dict端点层做解包与写侧 repo 的包裹对称)。
- 影响C3 扩(两读端点);@frontend `pnpm gen:api` + CodexPage 初始列表拉全量(去掉「会话内」局限,解 gotcha 2026-06-19 @frontend Codex 缺口条)。
## [2026-06-19] 角色入库 continuity gate有冲突→409 CONFLICT_UNRESOLVED作者 acknowledge 后放行 — @backend (T5.2)
- 背景ARCH §6.5 要求生成角色入库前过 continuity 校验(`precheck_generated_cards`但任务文档留了两条路线block / 返回冲突供作者裁决)。要选最简单正确、且守不变量 #3「无 AI 静默写库」。
- 选择:入库端点跑 precheck检出冲突且请求未带 `acknowledge_conflicts=true`**409 `CONFLICT_UNRESOLVED`** + `details{conflicts:[...], conflict_count}`(不写库)。作者在前端查看冲突、裁决后重发带 `acknowledge_conflicts=true` → 放行写库。零冲突 → 直接写库 201。
- 理由 / 取舍:复用既有 `CONFLICT_UNRESOLVED`(409) 码(语义=未决冲突禁写入,与 accept gate 一致),无需新增错误码;显式 `acknowledge` 标志=作者裁决的 HITL gate不静默入库守不变量 #3),比「自动返回冲突 + 另开裁决端点」更省(无新端点、无新状态表,原型 KISS。代价作者确认 = 重发一次请求(带 flag可接受。
- 影响C3 扩POST /characters`partition_writes` 在 gate 后再过写白名单character-gen 只声明 writes=characters。T5.6 前端入库流:预览→点入库→若 409 展示冲突→裁决勾选→带 acknowledge 重发。T5.7 E2E 覆盖两条路径。
## [2026-06-19] 角色卡 schema↔DB 形变落写侧 repolist→{"items"}、arc str→{"text"}、rules→{"rules"})— @backend (T5.2)
- 背景T5.1 gotcha 指出 `CharacterCard.traits`/`speech_tics`(list)/`arc`(str) 与 DB `characters` JSONB **dict** 列不同形;`WorldEntityCard.rules`(list) 与 `world_entities.rules` JSONB dict 列不同形。需定一个固定包裹形。
- 选择:写侧 repo 做转换层——`traits`/`speech_tics` 包成 `{"items":[...]}``arc` 包成 `{"text":...}``rules` 包成 `{"rules":[...]}``tags`/`relations` 是 JSONB list 直落。读回(喂防雷同/precheck反向解包同形`_existing_characters`/`_world_context`)。
- 理由 / 取舍:固定 wrapper key 让 round-trip 确定(写=读互逆),不改 @db 列类型(@db owned。代价assemble/C5 读侧 `CharacterView.traits` 仍是裸 dict本期 assemble 不消费 items 内层,无影响);若后续 assemble 要渲染 traits 文本,按同 wrapper 解包即可。
- 影响C3 扩(写侧 repo 形变契约ww_core.domain 新增 `character_repo`/`world_entity_repo`@db 若日后规整列类型为 list可去 wrapper迁移由届时 owner 执行)。
## [2026-06-19] 顺带补 `GET /projects/:id/style` 读指纹 + 写侧 repo 独立读视图 — @backend (T4.3)
- 背景UX §6.9 要展示完整 16 维指纹 + 证据,但 ARCH §7.2 端点清单**无**读指纹端点(只 `POST /style`。C5 读侧 `SqlStyleRepo.latest` 已存在,但其 `StyleView` **只含 `dimensions`**assemble 只需维度文本,不需证据/版本),扩它会牵动 C5 assemble 契约。
- 选择:(1) **加 `GET /projects/:id/style`** 返最新指纹 `{dimensions, evidence, version}`无指纹→404 `NOT_FOUND`)。(2) 不动 C5 读侧——在写侧 `SqlStyleFingerprintWriteRepo` 上加一个 `latest(project_id)->StyleFingerprintView`(含 evidence_json + version 的更全视图),`GET /style` 用它C5 `SqlStyleRepo.latest`/`StyleView`(只 dimensions供 assemble stable_core保持不变。
- 理由 / 取舍:读写分离 + 各自最小视图(同 `DigestAppendRepo` vs 读侧 `DigestRepo` 先例),不破坏 C5 assemble前端无须靠 job `result` 拼凑摘要、可直接拉完整指纹。
- 影响C3 扩(新增 `GET /style`**待回写规格**ARCH §7.2 端点清单 + PRODUCT_SPEC §6 应补 `GET /projects/:id/style`spec 一致性纪律,@docs 回写。T4.4 前端 FingerprintView 消费此端点。
## [2026-06-19] 学文风 `work` 自建网关+repo不走 FastAPI dep凭据探测前移到请求阶段 — @backend (T4.3)
- 背景:学文风走 jobsM4-c`POST /style` 立即返 202提取经 BackgroundTask `run_job` 在**独立 session** 上跑(请求 session 已关闭。但「无凭据→503」需在请求阶段就让用户知道不能 202 后再静默 fail job
- 选择:(1) `work(session)` 闭包内**自建** analyst 网关(`build_gateway_for_tier(session, SqlCredentialStore(session), "analyst")`+ 写侧 repo`run_job` 传入的独立 session不依赖 FastAPI depdep 绑的是请求 session。(2) 端点仍声明 `get_style_extract_gateway`(analyst) 依赖**仅作凭据探测**——无凭据时 dep 解析阶段抛 503`job_repo.create` 之前拦下;其返回的网关本身不被复用(请求 session 即将关闭)。
- 理由 / 取舍兼顾「BackgroundTask 必须自建 session」gotcha与「无凭据请求阶段即 503」友好、不写注定失败的 job。代价凭据被探测两次请求阶段 + 后台 work原型可接受。
- 影响C3 扩POST /style测试后台路径 monkeypatch `style.build_gateway_for_tier`/`style.SqlStyleFingerprintWriteRepo` + `job_runner.SqlJobRepo`(见 gotcha
## [2026-06-18] 网关结构化输出经可注入 `StructuredClient` 缝instructor— @llm
- 背景:`OpenAICompatAdapter` 需接 instructor 产结构化输出C1 类型预留 `output_schema`/`parsed` 但 M1 未接线),但测试绝不可联网/碰真实 LLM。
- 选择:定义 `StructuredClient` Protocol`create_with_completion(*, messages, response_model, **kw) -> (parsed, raw)`,即 `instructor.AsyncInstructor` 的形adapter 构造可选注入 `structured_client`,未注入时懒构建 `instructor.from_openai(self._client)``run()` 透传 `result.parsed → LlmResponse.parsed`
@@ -78,3 +136,9 @@
- 选择CLAUDE.md 只保留跨文档、易错的**规则**(含 9 条架构不变量);所有枚举型/具体值改为指向 ARCHITECTURE/PRODUCT_SPEC 对应 §(唯一真源)。新增"冲突裁决"段ARCHITECTURE > UX_SPEC/DEV_PLAN > PRODUCT_SPEC矛盾时回写上游。
- 理由 / 取舍:消除文档间漂移面、降低每 session 上下文成本;代价是查具体值需多跳一层到 ARCHITECTURE可接受
- 影响:编辑 CLAUDE.md 的任何 agent 遵循此纪律——契约/枚举值落在 spec 与 `memory/contracts.md`,不在 CLAUDE.md。
## [2026-06-19] T5.5 Skill registry + 表权限沙箱落 `ww_core.domain`,非 `packages/skills/ww_skills` — @backend
- 背景T5.5 任务文档把 registry/沙箱定位在 `packages/skills/ww_skills/`,但该包当前**不是 uv workspace member**(根 `pyproject.toml``[tool.uv.workspace].members` 不含它、mypy_path/ruff src 也不含),无 pyproject → 仅靠 sys.path hack 才能 importapp/测试/mypy 都无法干净消费它。根 pyproject 由 @devops 持有,自己接线会越权 + 阻塞。
- 选择:把实现落在已是 workspace member、且承载「编排/写库 apply 层」的 `ww_core.domain``skill_registry.py` + `skill_permissions.py`,经 `ww_core.domain` 导出)。`ARCHITECTURE §5.6` 明确「强制点在编排/写库层」=ww_core语义上也吻合。`packages/skills/ww_skills/__init__.py` 留**再导出门面**import 自 ww_core保留稳定「skills」导入名。
- 理由 / 取舍非阻塞、门禁可干净覆盖ruff/mypy/pytest 全跑)、不越权改根配置;代价是物理位置与任务文档措辞略偏,但功能/契约C8完整。待 @devops`packages/skills` 纳入 workspace + mypy_path 后,可平滑迁移实现到 `ww_skills` 而不动调用方(只改门面方向)。
- 影响:消费方 import `ww_core.domain``SkillRegistry`/`filter_reads`/`partition_writes`/`validate_declaration`(或经 `ww_skills` 门面)。若 @devops 后续纳入 workspace迁移由届时的 @backend/@llm 执行。