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:
@@ -14,6 +14,64 @@
|
||||
|
||||
---
|
||||
|
||||
## [2026-06-19] Kimi Code OAuth:token 存储包 + 刷新启发式 + 省略 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_in,KISS;对 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_in(YAGNI)。代价: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 OAuth:device 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`)**不带** scope(OAuth 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_url,Anthropic/Gemini 传 None 走原生适配器。仅在凭据缺失时返 None(回退链跳过)。保留既有 503/凭据/单 provider 兼容行为。
|
||||
- 理由 / 取舍:最小改动让回退链支持非 OpenAI-兼容 provider;adapter 选择逻辑收敛到网关包的工厂(单一真源),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 形变落写侧 repo(list→{"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)
|
||||
- 背景:学文风走 jobs(M4-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 dep(dep 绑的是请求 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 才能 import,app/测试/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 执行。
|
||||
|
||||
Reference in New Issue
Block a user