Files
writer-work-flow/memory/contracts.md
Yaojia Wang fb5caa3d89 docs: 登记 WFW-8 整章再沟通/重写——契约 C-Rewrite + PROGRESS 台账
memory/contracts.md 立 C-Rewrite(POST .../rewrite + RewriteStreamRequest,SSE,只读,
守 #3/#7/#9,无迁移);PROGRESS 加 WFW-8 行() + 门禁基线更新(前端 vitest 636/cov 95.36%,
后端 mypy 224/pytest 含 test_rewrite_node)。
2026-07-08 07:17:11 +02:00

96 KiB
Raw Blame History

契约登记(解耦缝)

跨 agent 的契约——多 agent 靠它解耦并行。契约先行:定义方先在此登记并标 稳定,依赖方才动工;改契约必须在此记一笔 + 在 PROGRESS.md 通知依赖任务(前端要重生成 TS 客户端、依赖模块要重同步)。 状态:待定义草拟(@skill)稳定已变更(见日志)


C1 · LLM 网关接口 owner @llm 状态: 稳定T1.1, 2026-06-18

  • 来源:ARCHITECTURE.md §4.1LlmRequest / LlmResponse / Block / Usagesnake_case)。
  • 消费方:编排器、所有 AgentGateway.run / Gateway.stream)。
  • 关键不变量agent 只传 tierwriter/analyst/light不传具体 model。
  • 已实现(packages/llm_gateway/ww_llm_gateway
    • 类型 types.pyBlock(text,cache=False)Scope(user_id,project_id?)LlmRequest(tier,input:str|list[Block],system:list[Block],stream,output_schema?,thinking,max_tokens?,scope)Usage(provider,model,input_tokens,output_tokens,cache_read_tokens,cost_minor,currency)ServedBy(provider,model,fell_back)LlmResponse(text,parsed?,usage,served_by)Delta(text)Tier
    • Gateway(adapters:dict[str,ProviderAdapter], ledger:LedgerSink, resolver=resolve_route)async run(req)->LlmResponsestream(req)->AsyncIterator[Delta]。每次调用落 1 条 usage_ledger(经 LedgerSink,可注入内存替身)。
    • 适配器 ProviderAdapter(Protocol)providercapabilities()->Capabilitiesasync complete(req,model)->ProviderResultstream(req,model)->AsyncIterator[StreamChunk]。M1 实现 OpenAICompatAdapter(provider, client:AsyncOpenAI)DeepSeek注入 client 便于测试)。
    • 档位路由 resolve_route(tier)->Route(provider,model)config.tier_defaultsM1 仅全局默认;回退/熔断属 M5/T5.4未实现)。
    • 记账 SqlAlchemyLedgerSink(session)UsageLedgerowner_id=scope.user_id 单用户 stub列名 cache_read)。成本表 pricing.py(未知 provider/model→0

C1 扩展T5.4, 2026-06-19· 多 provider 韧性(回退链 + 熔断 + 能力协商/降级 owner @llm 状态: 稳定

  • 来源:ARCHITECTURE.md §4.4(能力协商/降级)/§4.5(回退/重试/熔断)仅加字段/形参,不破既有调用(旧 Gateway(adapters, ledger, resolver=resolve_route) 仍可用)。
  • ServedBy 加字段C1 形变,仅加可选 bool默认 False向后兼容ServedBy(provider, model, fell_back=False, degraded=False)degraded 标能力降级(所选 provider 不支持原生结构化输出,改走 instructor JSON-提示路径)。→ OpenAPI 表面ServedBy 不直接出 APIreview/draft SSE 不回 served_bysettings/providers 也不含它)→ 经核 本次无 OpenAPI 形变,前端无需 re-gen;若后续把 served_by 暴露到响应体,再触发 gen:api。
  • Gateway 构造扩(关键字,全可选):Gateway(adapters, ledger, *, chain_resolver: Callable[[Tier], list[Route]] | None=None, resolver: Callable[[Tier], Route] | None=None, max_retries=2, breaker: CircuitBreaker | None=None)chain_resolver(回退链)优先;resolver单路由M1 兼容)被自动包成单元素链;皆缺省回退到默认 resolve_chainrun/stream 行为:沿链逐 provider→瞬时失败退避重试 R 次→仍失败/熔断打开/无适配器则切下一个;首个成功者服务,非链首即 served_by.fell_back=True;链耗尽抛 AppError(LLM_UNAVAILABLE)。记账记实际服务方 provider/model回退后不记主模型。流式仅在首块产出前可切产出后中途失败上抛不静默重连§4.5)。
  • 回退链解析routing.pyresolve_chain(tier)->list[Route](默认仅全局 tier_defaults,单元素);chain_from_routing(tier, primary:str, fallback:list[str])->list[Route](据 DB tier_routing 行去重保序构链——供 apps/api 包成 ChainResolver 注入网关,apps/api 待接线build_gateway_for_tier 目前仍只注主 provider 适配器 + resolver=resolve_route,要启用回退须按 fallback 预备多个适配器 + 传 chain_resolver)。ChainResolver = Callable[[Tier], list[Route]] 类型别名导出。
  • 熔断器CircuitBreaker(*, threshold=5, reset_seconds=30.0, clock=time.monotonic)is_open(provider)/record_failure/record_success;连续失败超阈值→短时熔断(网关直接跳过该 provider 走回退);成功清零;冷却窗口过半开放行。默认每个 Gateway 实例自带一个 breaker(进程内、非跨实例共享)——跨请求持久熔断需调用方注入共享 breaker。
  • 瞬时错误契约:适配器把厂商 429/超时/5xx/连接错误翻译为 ww_llm_gateway.errors.TransientProviderErrorOpenAI 兼容/Anthropic/Gemini 适配器均已包装,按异常类名 + status_code 判定);网关另把 AppError(RATE_LIMITED) 也视作可重试/可回退。非瞬时错误(内容策略拒绝等)原样上抛、不重试。
  • 新适配器(均经注入的客户端 Protocol测试不联网、不硬 import 厂商 SDK
    • AnthropicAdapter(provider, client: AnthropicClient, *, structured_client?)capabilities()=(structured_output=True, prefix_cache=True, thinking=True);缓存断点经 system 块 cache_control:{type:ephemeral}§4.6);结构化经 instructor.from_anthropic(懒构建)。
    • GeminiAdapter(provider, client: GeminiClient)capabilities()=(structured_output=True, prefix_cache=False, thinking=True);结构化经 config.response_mime_type=application/json + response_schema,文本回 model_validate_json
    • OpenAICompatAdapter 不变(新增瞬时错误包装,参与回退链)。
  • 依赖packages/llm_gateway/pyproject.tomltenacity>=8.2(已 uv sync;原为 instructor 传递依赖)。anthropic/google-genai SDK 已由 @devops 加(已 uv sync——适配器仍懒导入/注入客户端,单测零依赖。

C1 扩 follow-up #22026-06-19· build_adapter provider→适配器工厂 owner @llm 状态: 稳定

  • 位置:packages/llm_gateway/ww_llm_gateway/factory.py,经包 __init__ 导出 build_adapter
  • 签名稳定apps/api build_gateway_for_tier 据此逐 provider 建适配器进 adapters dict def build_adapter(provider: str, *, api_key: str, base_url: str | None = None) -> ProviderAdapter
  • 分派provider=="anthropic"AnthropicAdapter(懒 import AsyncAnthropic(api_key=, base_url?)provider in {"gemini","google"}GeminiAdapter(懒 import genai.Client(api_key=)忽略 base_url——SDK 无该形参其余deepseek/kimi/qwen/glm/openai…OpenAICompatAdapter(provider, AsyncOpenAI(api_key=, base_url=))
  • 工厂从 api_key 构真实客户端(适配器内部仍是注入客户端 Protocol测试零联网故单测只断按 provider 名选对适配器类 + provider 字段透传tests/test_build_adapter_factory.py6 测),不联网。
  • @backend 行动build_gateway_for_tier 现可调 build_adapter(provider, api_key=…, base_url=…) 替换「一律 OpenAICompatAdapter」,使回退链中的 anthropic/gemini 走对应专用适配器。无 OpenAPI 形变served_by 不出 API前端无需 re-gen。

C1 扩K1.2, 2026-06-19· Kimi Code 订阅 plan 适配器(伪造头 + OAuth bearer owner @llm 状态: 稳定

  • 位置:packages/llm_gateway/ww_llm_gateway/adapters/kimi_code.py,经包 __init__ 导出 KimiCodeAdapter/build_kimi_code_client/kimi_code_headers/kimi_device_id/kimi_device_model/KIMI_CODE_BASE_URL/KIMI_CODE_USER_AGENT/KIMI_CLI_VERSION/KIMI_CODE_PLATFORM/KIMI_CODE_PROVIDER
  • provider 名kimi-code(与 API-key provider kimi 分离,便于档位切换)。build_adapter("kimi-code", api_key=<access_token>, base_url=None)K1.3 接线的缝——api_key 即 OAuth access tokenOpenAI SDK 自动发 Authorization: Bearer <token>base_url 缺省 → KIMI_CODE_BASE_URL
  • base URLhttps://api.kimi.com/coding/v1OpenAI 兼容;KimiCodeAdapter 子类化 OpenAICompatAdaptercapabilities 继承structured_output/prefix_cache=True
  • modelkimi-for-coding路由/档位关注点,经 .complete(req, model) 传入,在适配器硬编码。
  • 必需伪造头 = 完整 7 头opencode 规范)User-Agent + 6 个 X-Msh-*build_kimi_code_clientkimi_code_headers() 作为 AsyncOpenAI(default_headers=…),每次请求随客户端发出。
  • 头集真源校正2026-06-19= github.com/ooojustin/opencode-kimisrc/headers.ts kimiHeaders() + src/constants.ts1:1 镜像 kimi-cli v1.37.0。早先曾误信 picassio/pi-kimi-coder(仅 UA、不带 X-Msh-*)把头集裁成 UA-only——那是分歧/错误参考,已回退到 opencode 完整 7 头。coding API 校验全部 7 头偏差→Moonshot 后端 access_terminated_error: only available for Coding Agents403。7 头精确值:
    • User-Agent = KimiCLI/1.37.0f"KimiCLI/{KIMI_CLI_VERSION}"UA 前缀 = KimiCLI/<version>version 必须 == X-Msh-Version)。
    • X-Msh-Platform = kimi_cli(字面常量字符串, OS 名)。
    • X-Msh-Version = 1.37.0= UA 里的 CLI 版本)。
    • X-Msh-Device-Name = 主机名 ASCII 化(socket.gethostname()/platform.node(),裁非 ASCII
    • X-Msh-Device-Model = kimi_device_model()macOS f"macOS {platform.mac_ver()[0]} {platform.machine()}"(如 "macOS 14.5 arm64"Windows f"Windows {release} {machine}";其它 f"{system} {release} {machine}"platform.machine() 原样,返回 arm64/x86_64,不归一化)。
    • X-Msh-Os-Version = platform.version()OS 内核版本串,≈ Node os.version())。
    • X-Msh-Device-Id = 稳定 UUID4 hex 无连字符32 位小写)。kimi_device_id()env KIMI_DEVICE_ID 优先 → 否则读/建 ~/.kimi/device_id(首次写一次 uuid.uuid4().hex、之后复用;与 kimi-cli/opencode 共享路径)。跨调用/进程稳定,绝不每次随机
  • 单测(不联网,断构造出的客户端 base_url + default_headers全 7 头键 + 固定字面值 + device-id 32 位小写 hex 稳定 + host 派生值非空 ASCII + bearer + 工厂分派env KIMI_DEVICE_ID 注入保证 CI 确定):tests/test_kimi_code_adapter.py + tests/test_kimi_code_factory.py
  • @backend 行动K1.3OAuth device 服务刷新 token 后,_build_provider_adapterbuild_gateway_for_tier)对 provider kimi-codebuild_adapter("kimi-code", api_key=<当前 access_token>, base_url=…) 即得带伪造头的适配器token 刷新/获取归 K1.3,本适配器只收当前 access token。无 OpenAPI 形变,前端无需 re-gen。

C1 扩kimi-code-key, 2026-06-20· Kimi Code 订阅 plan 静态 Console KeyToS 合规变体 owner @llm 状态: 稳定

  • 位置:packages/llm_gateway/ww_llm_gateway/adapters/kimi_code_key.py,经包 __init__ 导出 KimiCodeKeyAdapter/build_kimi_code_key_client/KIMI_CODE_KEY_PROVIDER
  • provider 名kimi-code-key(与 OAuth 的 kimi-code、moonshot 的 kimi 均分离)。build_adapter("kimi-code-key", api_key=<console key>, base_url=None) 走此分支。
  • 与 OAuth kimi-code 的关键区别build_kimi_code_key_client 构造 AsyncOpenAI(api_key=, base_url=KIMI_CODE_BASE_URL)——不设 default_headers,即不伪造 UA、不带 X-Msh-*。Kimi Consolekimi.com/code/console)签发的 Key 走订阅额度、命中同一 coding 端点(https://api.kimi.com/coding/v1+ 同 model kimi-for-coding,但 plain Authorization: Bearer <key> 即可openclaw + models.dev 确认ToS 允许第三方 Key仅 UA 篡改违规)→ 故这是干净/合规的默认路径(无封号风险,对照 KimiCodeAdapter 的伪造头变体)。
  • 结构化输出coding 端点 kimi-for-coding thinking 开启,与强制 tool_choice 互斥live 400KimiCodeKeyAdapter 复用 kimi_code.build_kimi_code_structured_clientinstructor Mode.JSON)——同 OAuth 变体的 JSON-mode 修复。
  • 单测(不联网):tests/test_kimi_code_key_factory.py5 测:工厂分派 + coding base + bearer + 无 X-Msh-*/无 KimiCLI UA + JSON-mode 结构化 + 显式 base_url 覆盖)。
  • @backend 接线kimi-code-key普通 api_key providerauth_type="api_key",存 api_key_encFernet——经既有 build_gateway_for_tier/_build_provider_adapter/probe 路径无改动即可工作(只需 provider_deps._PROVIDER_BASE_URLS 注册 base_url触发 _build_provider_adapter 的 OAuth 刷新分支(其判据 auth_type==oauth or provider=="kimi-code" 不命中本 provider无 OpenAPI 形变(用既有 PUT /settings/providers),前端无需 re-gen。

C2 · DB schemaSQLAlchemy 模型 owner @db 状态: 待定义

  • 来源:ARCHITECTURE.md §3.1 DDL11 创作表 + chapter_reviews + 运营表无向量列users stub
  • 消费方:@backend(Repository/记忆服务/验收)、@llm(Agent reads/writes)。

C2 扩展K1.1, 2026-06-19· provider_credentials 加 OAuth 列 owner @db 状态: 稳定

  • 背景K1 Kimi Code OAuth订阅 plan device-flowprovider_credentials 表新增两列 + 改 api_key_enc 可空使一行可表「api_key 凭据」或「oauth 凭据」二选一。
  • 模型变更(packages/db/ww_db/models.py ProviderCredential
    • 新增 auth_type: Mapped[str] = Text, nullable=False, server_default="'api_key'"(值域 "api_key" | "oauth")。既有行迁移后默认 'api_key',向后兼容。
    • 新增 oauth_enc: Mapped[bytes | None] = LargeBinary, nullable=True持 Fernet 加密的 JSON 包 {access_token, refresh_token, expires_at}@backend K1.3 写/读:encrypt/decryptkey 取 settings.credential_enc_key,同 api_key_enc 的 Fernet
    • api_key_encnullable=Falsenullable=TrueMapped[bytes | None]——OAuth-only 行无 api_key既有 api_key 行迁移后仍保留其值。
    • UniqueConstraint("owner_id","project_id","provider") 不变OAuth provider 用独立 provider 名 kimi-code,与 api_key 的 kimi 分离,不撞约束)。
  • 迁移packages/db/migrations/versions/1f011c42bd4d_provider_credentials_oauth_columns.pydown_revision=220ca2e3d53f。up: add_column auth_type/oauth_enc + alter_column api_key_enc→nullabledown 逆向。alembic check 无漂移。
  • ⚠️ K1.3 @backend 必读api_key_enc 现为 bytes | Noneapps/api/ww_api/services/credentials.pyStoredCredential.api_key_enc: bytesdataclass 字段 line 28与 store 的 r.api_key_enc 赋值line 75/97mypy 报 arg-typebytes | Nonebytes)。K1.1 不动 @backend 文件(目录所有权)K1.3 接 OAuth store 时须把 StoredCredential.api_key_enc 改成 bytes | None(并加 auth_type/oauth_enc 字段/读写路径),届时 mypy 即绿。当前全仓 mypy 唯余此 2 处错(都在 credentials.py其余门禁绿ruff/format/alembic check/pytest 400 passed

C3 · API / OpenAPI 端点 owner @backend 状态: 稳定M1 端点全落T1.7 settings + T1.4 projects/chapters, 2026-06-18

  • 来源:ARCHITECTURE.md §7.2 端点清单(章节端点统一 /projects/:id/chapters/:no/...;含 PUT .../draft(自动保存)、/refine/jobs/:id/reviews)。
  • 消费方:@frontend(经 OpenAPI→TS 客户端)。改端点/字段 → 前端必须 pnpm gen:api 重生成客户端。
  • 已落T1.7, 2026-06-18snake_case响应仅脱敏 key
    • GET /settings/providersProvidersResponse{ providers:[ProviderView{provider, masked_key}], tier_routing:[TierRoutingView{tier, provider, model, fallback:list[str]}] }
    • PUT /settings/providersProvidersUpsertRequest{ credentials:[ProviderCredentialInput{provider, api_key}], tier_routing:[TierRoutingInput{tier, provider, model, fallback}] }ProvidersResponse(脱敏)
    • POST /settings/providers/testTestConnectionRequest{provider}TestConnectionResponse{provider, ok, capabilities:CapabilitiesView{structured_output, prefix_cache, thinking}}
    • 明文 key 永不过边界probe 经可注入 ProviderProbe(测试用 fake不联网
  • 已落T1.4, 2026-06-18snake_case
    • POST /projectsProjectCreateRequest{title, genre?, logline?, premise?, theme?, selling_points:list, structure?} → 201 ProjectResponse{id, title, genre?, logline?, premise?, theme?, selling_points, structure?}
    • GET /projectsProjectListResponse{projects:[ProjectResponse]}GET /projects/{id}ProjectResponse404 NOT_FOUND 信封)
    • POST /projects/{id}/chapters/{no}/draftSSE text/event-stream,帧 event:<token|done|error>\ndata:<json>\n\ntoken{text}/done{length}/error{code,message,request_id});无凭据 → 流前 LLM_UNAVAILABLE(503) JSON 信封(非帧)。
    • PUT /projects/{id}/chapters/{no}/draftDraftSaveRequest{text} → 200 DraftResponse{project_id, chapter_no, volume, status, version, length}幂等version 固定 1、status='draft')。
    • 网关注入缝 get_writer_gateway(从凭据构 OpenAICompatAdapter+SqlAlchemyLedgerSink);测试经 app.dependency_overrides 注 mock 网关(只需 .stream(req))。
    • @frontend 行动M1 端点已全在 OpenAPI → Wave D 前先 cd apps/web && pnpm gen:api 重生成 lib/api/schema.d.ts

C3 扩展(项目列表元数据, 2026-06-28· ProjectResponse 增加最近编辑与待审稿统计 owner @backend 状态: 稳定

  • ProjectResponse 追加字段:updated_at: datetime | nullpending_review_count: int >= 0。创建、列表、详情响应共用同一 schema前端已 pnpm gen:api
  • updated_at 口径:取项目自身 projects.updated_at 与该项目下章节 chapters.updated_at、审稿留痕 chapter_reviews.created_at 的最大值,用于作品库“最近编辑”排序。为支持草稿重复保存后的真实编辑时间,chapters 表新增 updated_at 列(迁移 8c1d2e3f4a5b,既有行以 created_at 回填)。
  • pending_review_count 口径:按章节草稿行计数;当草稿 updated_at 晚于该章最近 accepted 版本和最近审稿留痕时,视为待审稿。已验收且草稿未再更新的章节不计入。
  • @frontend 已消费:作品库增加“待审稿”筛选、“最近编辑”排序,作品卡显示最近编辑时间和待审稿徽标。
  • 已落T2.4+T2.5, 2026-06-18snake_case全部挂 projects.router、已在 OpenAPI— M2 审/裁/验收三端点
    • POST /projects/{id}/chapters/{no}/reviewReviewRequest{draft?}(空/缺→回退已存草稿无草稿→404 NOT_FOUND)→ SSE text/event-stream,帧:section{name,status:"done"|"incomplete"}(每审一条M2 仅 continuity) / conflict{type,where,refs:list,suggestion}(每冲突一条,形=C6 Conflict 五类) / done{length=审项数} / error{code,message,request_id}。无凭据→流前 LLM_UNAVAILABLE(503 JSON 信封)。续审网关 tier=analystget_review_gateway)。端点流耗尽后 session.commit()(网关 ledger + collect 均只 flush
    • GET /projects/{id}/chapters/{no}/reviewsReviewHistoryResponse{reviews:[ReviewHistoryItem{id,project_id,chapter_no,chapter_version?,conflicts:list,foreshadow_sug:list,style?,pace?,health_score?,decisions?}]}(新→旧)。
    • POST /projects/{id}/chapters/{no}/acceptAcceptRequest{final_text(min1), decisions:[ConflictDecision{conflict_index:int>=0, verdict:"accept"|"ignore"|"manual", note?}]}AcceptResponse{project_id,chapter_no,accepted_version:int,digest_added:bool,decisions_recorded:int,review_id?:uuid}冲突 gate:裁决的 conflict_index 集合须覆盖 range(len(最近一条 review.conflicts))缺判→409 CONFLICT_UNRESOLVED + details.missing_conflict_indices/conflict_count无留痕或零冲突→直通。digest 提炼 tier=lightget_digest_gateway在事务外R2单事务 promote(R4)+digest.append(#4)+set_decisions 末尾一次 commitR3
    • 注入缝:build_gateway_for_tier(session, store, tier)(原 build_writer_gateway 退化为 writer 特例) + get_review_gateway/get_digest_gateway/get_review_repo/get_digest_append_repo;测试经 app.dependency_overrides 注 mock。
    • @frontend 行动cd apps/web && pnpm gen:api 重生成客户端含此三端点T2.6)。
  • 已落T3.2, 2026-06-18snake_case— 伏笔登记/状态端点 + 验收后到期扫描(挂 foreshadow.routerprefix /projectstag foreshadow,已注册):
    • POST /projects/{id}/foreshadowForeshadowRegisterRequest{code(min1),title(min1),planted_at?,content?,expected_close_from?,expected_close_to?,importance?} → 201 ForeshadowView{code,title,status,planted_at?,content?,expected_close_from?,expected_close_to?,importance?,links:list,progress:list}(status=OPEN)。重复 code(DB 唯一 (project_id,code))→ 422 VALIDATION+details{field:"code",code,reason:"duplicate"}
    • PATCH /projects/{id}/foreshadow/{code}ForeshadowTransitionRequest{to_status?,progress_entry?} → 200 ForeshadowView。非法转移→422 VALIDATION+details{reason:"invalid_transition"}二者都缺→422 empty_updatecode 不存在→404 NOT_FOUND。先转移后追加进展,端点写后 commit()(repo 只 flush)。
    • 错误码取舍:重复/非法转移映射 VALIDATION(422) 非 409——现有唯一 409 码 CONFLICT_UNRESOLVED 语义专指未决冲突禁验收,不复用;未动 packages/shared/errors.py 契约。若 T3.5/前端需真 409 须先加专用码。
    • 验收后到期扫描POST .../accept 单事务 commit 成功后经 BackgroundTasks 登记 run_overdue_scan(session_factory, project_id, chapter_no=刚验收章号)——自建独立 session(请求 session 已关闭)、应用 current_ch>expected_close_to AND status≠CLOSED→OVERDUE、有变更才 commit日志 foreshadow_overdue_scan(带 request_id/overdue_count/codes)失败吞不冒泡。§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表。
    • T3.5 续接GET /projects/{id}/foreshadow?status= 看板加到本 foreshadow.router(注入 get_foreshadow_repolist_by_status,建议响应包 {foreshadow:[...]}POST .../outline独立 routers/outline.py(别混进 foreshadow.py),调 run_outline(C6 扩)+build_gateway_for_tier(...,"analyst"),逐章 upsert outline 表、端点末尾 commit。
    • @frontend 行动T3.6 前 pnpm gen:api 纳入伏笔登记/状态端点(+ T3.5 看板/outline
  • 已落T3.5, 2026-06-18snake_case— 伏笔看板 + 大纲生成端点
    • GET /projects/{id}/foreshadow?status=tag foreshadowquery status?OPEN|PARTIAL|CLOSED|OVERDUE(缺省=全部非法→422 VALIDATION+details{field:"status",reason:"invalid_status"}) → 200 ForeshadowBoardResponse{foreshadow:list[ForeshadowView]}(按 code 升序)。
    • POST /projects/{id}/outlinetag outline,独立 routers/outline.py)← OutlineGenerateRequest{volume:int=1,ge=1}M3 简单分卷:整批同卷)→ 200 OutlineResponse{chapters:list[OutlineChapterView{no,volume,beats:list[str],foreshadow_windows:list[ForeshadowWindowView{code,plant_chapter?,expected_close_from?,expected_close_to?}]}]}项目不存在→404 NOT_FOUND;无凭据/网关失败→503 LLM_UNAVAILABLE(流前 JSON 信封)。调 run_outline(analyst 网关 get_outline_gateway)→逐章 upsert outline 表(写侧 SqlOutlineWriteRepo,只 flush)→端点末尾 commit()(含 1 条 usage_ledger
    • beats 存储形DB outline.beats 是 JSONB dict→写侧包成 {"beats":[...]} 落库API 出参 OutlineChapterView.beats 解包成裸 list[str]
    • @frontend 行动T3.6 pnpm gen:api 纳入看板/大纲端点。

C3 扩展T4.3, 2026-06-19· 文风端点:学文风(jobs 异步) + 回炉 + 最新指纹(独立 routers/style.py,已注册)

  • snake_case改字段 → @frontend 必须 cd apps/web && pnpm gen:api 重生成 TS 客户端T4.4 前置)。
  • POST /projects/{id}/styleStyleLearnRequest{samples:list[str](min_length=1), mode:Literal["create","update"]="create"}202 StyleLearnResponse{job_id:uuid}。项目不存在→404 NOT_FOUND;无凭据→503 LLM_UNAVAILABLE(在 get_style_extract_gateway dep 解析阶段拦下,调度 job 之前,不凭空写注定失败的 job。流程job_repo.create(project_id,"style_learn")session.commit()job 行在 202 前持久化供轮询)→ background_tasks.add_task(run_job, session_factory, job.id, work)workrun_job 自建独立 session 上自造 analyst 网关(build_gateway_for_tier(session, SqlCredentialStore(session), "analyst")+ 写侧 repo → run_style_extraction(style_extract_spec, samples_text="\n\n".join(samples), ...) → 拆 dimensions_json={name:value}/evidence_json={name:[evidence]}SqlStyleFingerprintWriteRepo.appendversion+1→ 返回 {version, dims_count}(落 jobs.result)。run_job 拥有 session 生命周期/commit业务写 + job done 同一事务)。mode 不改落库逻辑(写侧始终 append 新版本),仅供前端区分首学/更新语义。前端经 GET /jobs/{job_id} 轮询。
  • GET /projects/{id}/style → 200 StyleFingerprintResponse{dimensions:dict, evidence:dict, version:int}最新版本完整指纹UX §6.9。项目不存在→404无指纹→404 NOT_FOUND新增端点ARCH §7.2 原缺读指纹端点,见 decisions 2026-06-19。读侧用写侧 repo 的 latest(含 evidence/version不动 C5 读侧 SqlStyleRepo.latest/StyleView(只 dimensionsassemble 用),故 assemble/C5 行为不变。
  • POST /projects/{id}/chapters/{no}/refineRefineRequest{segment:str(min_length=1), instruction:str|None=None} → 200 RefineResponse{original:str, refined:str}。同步回炉writer 网关(get_refine_gatewayrun(refiner_spec)output_schema=None → 取 resp.text 为 refined输入 = 【待重写段落】{segment} + 可选 【改写指令】{instruction}不写库(不变量 #3作者采纳经既有 draft 自动保存合入);末尾 session.commit()(网关 ledger add-only否则 usage_ledger 静默丢失,同 draft/review 纪律。项目不存在→404无凭据→503。
  • 注入缝(services/project_deps.pyget_style_write_repoSqlStyleFingerprintWriteRepo,服务 GET /style 读侧)、get_style_extract_gateway(analyst学文风的凭据探测缝)、get_refine_gateway(writer)。测试经 app.dependency_overrides override学文风后台路径 monkeypatch style.build_gateway_for_tier/style.SqlStyleFingerprintWriteRepo + job_runner.SqlJobReporun_job 自建 repo不走 dep
  • 写侧 repopackages/core/ww_core/domain/style_repo.py,经 ww_core.domain 导出):StyleFingerprintWriteRepo/SqlStyleFingerprintWriteRepo(加 Write 避撞 C5 读侧 SqlStyleRepo,仿 DigestAppendRepo 先例):append(project_id, *, dimensions_json, evidence_json)->intversion=当前 max+1首次 1只 flush 不 commit+ latest(project_id)->StyleFingerprintView|None(含 dimensions/evidence/versionGET /style)。
  • @frontend 行动T4.4 前 pnpm gen:api 纳入 style/refine/GET style + jobs 轮询类型。

C3 扩展T5.5, 2026-06-19· 规则端点 POST /projects/:id/rules(独立 routers/rules.py,已注册)

  • snake_case改字段 → @frontend 必须 cd apps/web && pnpm gen:apiT5.6 前置)。
  • POST /projects/{project_id}/rulesRuleCreateRequest{level:Literal["global","genre","style","project"], content:str(min_length=1)}201 RuleView{level:str, content:str}。非法 level / 空 contentFastAPI 422Pydantic Literal/min_length 校验,非 AppError 信封)。
  • 加规则是作者显式动作(不变量 #3不经 AI 静默写库)。写一行 rules(绑 project_id),喂给 assemble 的四级合并 merge_rulesglobal→genre→style→project
  • 提交边界:RuleWriteRepo.createflush(),端点写后 await session.commit()(仿 foreshadow/outline 写侧)。
  • 写侧 repopackages/core/ww_core/domain/rule_repo.py,经 ww_core.domain 导出):RuleWriteRepo(Protocol)/SqlRuleWriteRepo/RuleWriteView{project_id?,level,content}(加 Write 避撞 C5 读侧 domain.repositories.RuleView{level,content},仿 OutlineWriteRepo 先例)。
  • 注入缝(services/project_deps.pyget_rule_write_repoSqlRuleWriteRepo)。测试经 app.dependency_overrides override 注 fake repo + fake session断 commit 计数)。
  • @frontend 行动T5.6 前 pnpm gen:api 纳入 POST /rules(规则页用)。

C3 扩展T5.2, 2026-06-19· 生成/入库 + 读端点(独立 routers/generation.py已注册 owner @backend 状态: 稳定

  • snake_case改字段 → @frontend 必须 cd apps/web && pnpm gen:apiT5.6 前置)。生成走即时返回预览→作者确认→入库M5-d非 jobs
  • POST /projects/{project_id}/world/generateWorldGenerateRequest{brief:str(min1)}200 WorldGenPreviewResponse{entities:[WorldEntityCardView{type:str,name:str,rules:list[str]}]}预览不入库worldbuilder writer 网关 run_worldbuilder);末尾 commit() 仅落网关 ledger。项目不存在→404无凭据→503 LLM_UNAVAILABLEget_worldbuilder_gateway dep 解析阶段拦下)。
  • POST /projects/{project_id}/characters/generateCharacterGenerateRequest{brief:str(min1), count:int=1(ge1,le12), role:str|None, role_mix:dict[str,int]|None}200 CharacterGenPreviewResponse{cards:[CharacterCardView{name,role,traits:list[str],backstory,arc:str,speech_tics:list[str],tags:list[str],relations:[CharacterRelationView{name,kind,note?}]}]}预览不入库character-gen writer 网关注入已有角色防雷同generated_so_far=[]);末尾 commit 落 ledger。404/503 同上。⑦ 群像按定位配比role_mix{定位:数量})在场时 count 取各值之和(忽略客户端 count每项≥1、总和≤12model_validator 归一化);缺省退回单一 role+count
  • POST /projects/{project_id}/characters(入库)← CharacterIngestRequest{cards:[CharacterCardView](min1), acknowledge_conflicts:bool=false}201 CharacterIngestResponse{created:list[str], rejected_tables:list[str]}入库 gate:① precheck_generated_cards(continuity 预检, analyst 网关) 比对卡 vs 世界观/已有角色真相源——有冲突且 acknowledge_conflicts=false409 CONFLICT_UNRESOLVED + details{conflicts:[{type,where,refs,suggestion}], conflict_count}(仿 accept gate不静默入库不变量#3acknowledge_conflicts=true 即作者裁决放行)。② partition_writes(character_gen_spec, {"characters":cards}) 白名单过滤(越权表→rejected_tables 审计 log+丢弃character-gen 只声明 writes=characters 故正常为空)。③ 写 charactersschema list/str → DB JSONB dict 形变,见写侧 repo。404/503 同上。提交边界precheck 网关 ledger + 角色写侧均只 flush → 端点末尾一次 commit()(含冲突 409 路径也 commit 落 precheck usage
  • GET /projects/{project_id}/rules200 RuleListResponse{rules:[RuleView{level,content}]}(规则页;复用 C5 读侧 SqlRulesRepo.all_for_project,不动 assemble
  • GET /skills(独立 skills_routerprefix /skills)→ 200 SkillListResponse{skills:[SkillView{name,scope,tier,reads:list[str],writes:list[str],genre?}]}(技能库 UIget_skill_registry 读 registry按 name 升序)。
  • 写侧 repopackages/core/ww_core/domain/,经 ww_core.domain 导出):CharacterWriteRepo/SqlCharacterWriteRepo/CharacterWriteView{id,name,role}character_repo.py+ WorldEntityWriteRepo/SqlWorldEntityWriteRepo/WorldEntityWriteView{id,type,name}world_entity_repo.py,预留对称入库)。形变traits/speech_tics(list)→{"items":[...]}arc(str)→{"text":...}rules(list)→{"rules":[...]}tags/relations→直落 JSONB list仅 character 入库端点已落地world ingest 端点本期未建,仅 worldbuilder 走预览)。
  • 注入缝(services/project_deps.pyget_worldbuilder_gateway(writer)/get_character_gen_gateway(writer)/get_precheck_gateway(analyst)/get_character_write_repo/get_world_entity_write_repo/get_rules_read_repo。测试经 app.dependency_overrides 注 fake repo + schema-routing fake 网关(按 req.output_schema 返 WorldGenResult/CharacterGenResult/ContinuityReview+ fake session断 commit 计数)。
  • build_gateway_for_tier 多 provider 接线T5.4 follow-up已完成:据 DB tier_routing 取该 tier 的 provider:model + fallback,为每个可建适配器的 provider 预备适配器,注入 chain_resolver=chain_from_routing(...) 启用回退链。无 DB 路由行 → 退回全局 resolve_route(单 providerM1 兼容);无任何可用凭据 → 503。单 provider 配置 = 单元素链(行为不变)。
  • build_adapter per-provider 接线M5 R1 follow-up #22026-06-19已完成_build_provider_adapter(原 _build_openai_compat_adapter)现调 ww_llm_gateway.build_adapter(provider, api_key=…, base_url=…)C1 扩 follow-up #2 工厂)替代「一律 OpenAICompatAdapter」——OpenAI 兼容 providerdeepseek/kimi/qwen/glm/openai_PROVIDER_BASE_URLS 传 base_urlAnthropic/Gemini 传 base_url=None 走原生适配器。这样 tier_routing 链里配置的 Anthropic/Gemini provider 也能拿到真实适配器参与回退(不再被 base_url 缺失跳过)。未配凭据的 provider 返 None回退链跳过无 OpenAPI 形变served_by 不出 API。真实 Anthropic/Gemini 跑通仍需 @devops 加 anthropic/google-genai SDK 依赖(适配器懒导入/注入客户端)。
  • @frontend 行动T5.6 前 pnpm gen:api 纳入 world/characters generate + characters ingest + GET rules + GET skills。

C3 扩展M5 R1 follow-up, 2026-06-19· 设定库 Codex 读端点GET characters / world_entities owner @backend 状态: 稳定

  • 补 PROGRESS「Codex 读端点缺口」余项:设定库 Codex 现可展示跨会话全量已入库角色/世界观(前端先前只能「生成+本次入库会话内回显」。snake_case新增端点 → @frontend 必须 cd apps/web && pnpm gen:api 重生成 TS 客户端 + CodexPage 初始列表去掉「会话内」局限(见 gotcha 2026-06-19 @frontend Codex 缺口条)。
  • GET /projects/{project_id}/characters200 CharacterListResponse{characters:[CharacterCardView]}(已入库角色全量;复用 C5 读侧 SqlCharacterRepo,经 get_memory_reposmemory.character.list_for_project不动 assemble)。无行 → 空列表(非 404
  • GET /projects/{project_id}/world_entities200 WorldEntityListResponse{world_entities:[WorldEntityCardView]}(已入库世界观实体全量;复用 C5 读侧 SqlWorldEntityRepo)。
  • 形变DB JSONB→APIT5.2 ingest 形变的逆向)characters.traits/speech_tics dict {"items":[...]}→裸 list、arc dict {"text":...}→str、tags/relations→直落 list复用既有 _existing_characters helperworld_entities.rules dict {"rules":[...]}→裸 list。
  • 路由:挂既有 generation.routerprefix /projectsGET /{project_id}/characters 与 POST 同路径FastAPI 按 method 区分,无冲突。注入缝复用 get_memory_repos(无新 dep。测试经 app.dependency_overrides[get_memory_repos] 注 fake MemoryRepos
  • @frontend 行动pnpm gen:api 纳入 GET /projects/:id/characters + GET /projects/:id/world_entitieslib/api/serverfetchCharacters/fetchWorldEntities + CodexPage 初始列表拉全量。

C3 扩展(大纲读端点 follow-up, 2026-06-20· GET /projects/{id}/outline(挂既有 routers/outline.py已注册 owner @backend 状态: 稳定

  • 补缺口:大纲页先前只有 POST .../outline(生成+持久化),无读端点 → 重访页面已落库的大纲不回显,看似「未保存」。仿 Codex 读端点GET characters/world_entities补对称读侧。
  • GET /projects/{project_id}/outlinetag outline)→ 200 OutlineResponse{chapters:list[OutlineChapterView{no,volume,beats:list[str],foreshadow_windows:list[ForeshadowWindowView]}]}(与 POST .../outline 响应同形,前端类型对齐),按 chapter_no 升序。项目存在但无大纲 → 200 空列表(非 404项目不存在 → 404 NOT_FOUND(仿 POST 的项目存在性检查)。只读不写库
  • 复用 C5 读侧:扩 OutlineRepo(domain protocol) + SqlOutlineRepo(C5 assemble 读侧 repo) 加 list_for_project(project_id)->list[OutlineView](仿 CharacterRepo/WorldEntityRepo 命名order_by chapter_no既有 get(project_id, chapter_no) 不动assemble 行为不变。新注入缝 get_outline_read_repo(services/project_deps.py,仿 get_rules_read_repo)。
  • beats 形变DB outline.beats JSONB dict {"beats":[...]} → 出参 OutlineChapterView.beats 解包成裸 list[str](同 POST 路径 / OutlineWriteView._to_view)。
  • @frontend 行动pnpm gen:api 纳入 GET /projects/:id/outline;大纲页初次加载拉已持久化大纲(去掉「生成后才显示」局限)。

C3 扩展(草稿读端点 follow-up, 2026-06-20· GET /projects/{id}/chapters/{no}/draft(挂既有 routers/projects.py已注册 owner @backend 状态: 稳定

  • 补缺口:写作工作台先前只有 POST .../draft(SSE 流写) + PUT .../draft(自动保存) 无读端点 → 重访工作台时编辑器空白、看似「未保存」(其实 chapters 行在库)。仿 GET .../outline 补对称读侧。
  • GET /projects/{project_id}/chapters/{chapter_no}/draft200 DraftView{project_id, chapter_no, volume, status, version, content, length}比 PUT 的 DraftResponsecontent——读侧需正文重建编辑器;length=content 字符数派生)。无草稿行或正文空白404 NOT_FOUND(工作台据此呈现空编辑器,与其它「缺资源」端点一致)。只读不写库
  • 复用读 seam:直接调既有 chapter_repo.get_draft(project_id, chapter_no)->ChapterDraftView|None(同续审 _resolve_review_draft 的读缝);ChapterDraftView 已含全部字段,无需新 repo 方法/新 view。多版本时固定返草稿版次DRAFT_VERSION=1)那条可编辑工作副本,与 PUT 保存的同一行。
  • @frontend 行动pnpm gen:api 纳入 GET /projects/:id/chapters/:no/draft(新 schema DraftView);工作台初次加载拉已保存草稿回灌编辑器(去掉「重访看似未保存」局限)。

C3 扩展T4-b, 2026-06-20· POST .../draft 接收可选本章指令 directive(挂既有 routers/projects.py owner @backend 状态: 稳定

  • 仅加可选 body向后兼容POST /projects/:id/chapters/:no/draft 现接收可选 JSON body DraftStreamRequest{directive: str|None=None}(无 body 的旧调用方不变,仍按大纲生成)。directive=作者本章指令,临时输入、不持久化、无迁移
  • 直通 assemble(..., directive=…)_build_volatile 以「本章指令」段领衔 volatile断点后绝不入 stable_core(守缓存前缀不变量 #9draft_stream_start 日志加 directive_len(不记原文,脱敏)。
  • @frontend 行动pnpm gen:api 纳入 DraftStreamRequestuseDraftStream.start(projectId, chapterNo, directive?) 非空时 POST JSON body 传 directive。

C3 扩展K1.3, 2026-06-19· Kimi Code OAuth device-flow 端点(独立 routers/kimi_oauth.py已注册 owner @backend 状态: 稳定

  • snake_case新增 3 端点 + 3 schema → @frontend 必须 cd apps/web && pnpm gen:apiK1.4 前置)。响应/job 结果/日志绝不含 access/refresh token(只 user_code/connected/expires_at 等非密信息token 仅以 Fernet 密文存 provider_credentials.oauth_enc
  • POST /settings/providers/kimi-code/oauth/start202 OAuthStartResponse{job_id:uuid, user_code:str, verification_uri:str, verification_uri_complete:str|None, expires_in:int, interval:int}。流程:start_device_authorization(http)job_repo.create(None,"kimi_oauth") + session.commit()202 前持久化供轮询)→ background_tasks.add_task(run_job, session_factory, job.id, _make_poll_work(device))。后台 workrun_job 自建独立 session 上循环 poll_tokenauthorization_pending→续轮询、slow_down→增大 interval、expired/denied→抛 AppError 置 job failed成功 → upsert_oauth_credential(加密包)+ job done结果只 {connected:true, provider})。前端展示 user_code + 打开 verification_uri + 轮询 GET /jobs/{id}
  • POST /settings/providers/kimi-code/oauth/disconnect200 OAuthDisconnectResponse{disconnected:bool}delete_credential 清 OAuth 凭据行)。
  • GET /settings/providers/kimi-code/oauth/status200 OAuthStatusResponse{connected:bool, expires_at:str|None}(解密 oauth_enc 取过期时刻;无 token 本体;解密失败/无凭据→connected:false)。
  • store OAuth 读写services/credentials.pyC2 扩 K1.1 收口):StoredCredentialauth_type:str/oauth_enc:bytes|Noneapi_key_encbytes|NoneK1.1 的 2 处 mypy red 已消)。新方法:upsert_oauth_credential(owner,provider,oauth_enc)auth_type="oauth"+清 api_key_encread-modify-write 守可空 project_id 约束)、delete_credential(owner,provider)->boolupsert_credentialapi_key 路径)现显式 set auth_type="api_key"+清 oauth_enc。
  • _build_provider_adapter Kimi Code OAuth 接线services/project_deps.pyprovider kimi-codeauth_type="oauth"_resolve_kimi_code_token:解密 oauth_encneeds_refresh(剩余寿命 < 300s则经临时 httpx.AsyncClientkimi_oauth.refresh + upsert_oauth_credential 持久化新包 → 用刷新后的access token 调 build_adapter("kimi-code", api_key=<token>, base_url=…)K1.2 工厂构带伪造头 + coding base 的 KimiCodeAdapter)。无 oauth_encLLM_UNAVAILABLE_PROVIDER_BASE_URLS"kimi-code": "https://api.kimi.com/coding/v1"
  • 服务缝services/kimi_oauth.py,注入 AsyncHttpClient Protocol = httpx.AsyncClient.post 子集,测试零联网):start_device_authorization(http)->DeviceAuthpoll_token(http, device_code)->TokenSet(一次尝试,AuthorizationPending/SlowDown 异常供调用方循环)、refresh(http, refresh_token)->TokenSetencrypt_oauth_bundle/decrypt_oauth_bundleFernet 复用 security/credentialsencrypt_api_key/decrypt_api_key,工作在 JSON 串上);needs_refresh(token)。client_id env KIMI_CLIENT_ID 可覆盖默认 17e5f671-...device authorization scope=kimi-codecoding entitlement实测省略 scope→api.kimi.com/coding/v1 回 401故重新带上。token 交换/刷新不带 scope
  • @frontend 行动K1.4pnpm gen:api 纳入 OAuth start/disconnect/status + 3 schema连接流 = start 拿 user_code + 开 verification_uri → 轮询 GET /jobs/{job_id} 到 done已连接/failed过期/拒绝status 端点查连接态;档位路由到 kimi-code:kimi-for-coding

C3 扩展T6.2/T6.3, 2026-06-22· 创作工具箱通用端点(独立 routers/toolbox.py已注册 owner @backend 状态: 稳定

  • 通用生成器框架:声明驱动、一条执行路径——「加一个生成器」= 在 ww_skills.TOOLBOX 加一份 GeneratorTool 声明descriptor 在代码、无 DB 迁移无需新端点。snake_case新增 3 端点 + 新 schema → @frontend 必须 cd apps/web && pnpm gen:apiT6 前端前置)。
  • GET /skills/toolbox200 ToolboxListResponse{tools:[ToolDescriptorView{key,title,subtitle,genre?,is_legacy:bool,ingestable:bool,input_fields:[ToolInputFieldView{name,label,type,required,default?,help?}],legacy_route?:str}]}legacy 3 + 新 8按 key 升序。legacy 工具worldbuilding/character/outlineis_legacy=true)携 legacy_route/projects/{id}/codex?gen=world · ?gen=character · /projects/{id}/outline)供前端跳现有页面,不回归既测。
  • POST /projects/{project_id}/skills/{tool_key}/generateToolGenerateRequest{brief:str="", chapter_no:int|None(ge1), count:int|None(1..12), kind:str|None}200 ToolGeneratePreviewResponse{tool_key, output_kind:str(产物 schema 名), preview:dict(各工具 output_schema 的 model_dump)}预览不入库(按 spec.tierget_tier_gateway_builder 建网关 → run_generator);末尾 commit() 仅落 ledger。context 派发PURE services/toolbox_context.build_toolbox_contextbrief_only/with_project→build_brief_context、with_world→ + world_entities 硬规则卡、with_outline_chapter→ + 指定章 outline beats缺章/缺大纲→空节拍不报错)。未知 tool_key→404legacy 工具spec=None404(前端走 legacy_route项目不存在→404无凭据→503 LLM_UNAVAILABLE
  • POST /projects/{project_id}/skills/{tool_key}/ingestToolIngestRequest{world_entities:[WorldEntityCardView], chapter_no:int|None, scenes:[OutlineSceneIngestView{idx,beat,purpose,conflict,hook}], acknowledge_conflicts:bool=false}201 ToolIngestResponse{table:str, created:list[str], rejected_tables:list[str]}ingest!=None 的工具golden-finger/glossary→world_entitiesfine-outline→outline其余→422 VALIDATION。入库 gate仿 POST /charactersworld_entities 走 continuity 预检analyst 网关) 比对待入库实体 vs 既有世界观真相源——有冲突且 acknowledge_conflicts=false409 CONFLICT_UNRESOLVED + details{conflicts:[{type,where,refs,suggestion}],conflict_count}partition_writes(spec,{table:items}) 白名单过滤(越权→rejected_tables 审计schema→DB JSONB 形变写库rules→{"rules":[...]};细纲场景按 idx 升序拼成该章 beatsoutline_write_repo.upsert_chapter。outline细纲不做 continuity 预检(场景是粗节拍展开,非设定/角色比对),无 chapter_no→422。未知/legacy→404。提交边界ledger + 写侧只 flush → 端点末尾一次 commit()(含 409 路径也 commit 落 precheck usage
  • 注册表packages/skills/ww_skills/toolbox_registry.py,经 ww_skills 导出):TOOLBOX:dict[str,GeneratorTool]11 条)+ get_tool(key)->GeneratorTool|None。新工具的 key==spec.nameoutput_schema is spec.output_schema
  • 注入缝services/project_deps.pyget_tier_gateway_builderTierGatewayBuilder=Callable[[Tier],Awaitable[Gateway]](按工具 tier 动态建网关);测试经 app.dependency_overrides[get_tier_gateway_builder] 注返 mock 网关的 builder + get_world_entity_write_repo/get_outline_write_repo/get_memory_repos/get_project_repo 注 fakeschema-routing fake 网关按 req.output_schema 返 IdeaListResult/ContinuityReview/…)。
  • 守不变量#2spec 只声明 tier/#3预览不写库入库经 continuity gate + 白名单)/#9system_prompt 进缓存前块、注入材料进 input无新迁移descriptor 在代码,复用 world_entities/outline 表)。
  • @frontend 行动pnpm gen:api 纳入 GET /skills/toolbox + POST .../skills/{tool_key}/generate + POST .../skills/{tool_key}/ingest(新 schema ToolboxListResponse/ToolDescriptorView/ToolInputFieldView/ToolGenerateRequest/ToolGeneratePreviewResponse/ToolIngestRequest/ToolIngestResponse/OutlineSceneIngestView);工具箱落地页据 GET /skills/toolbox 渲染卡片栅格legacy 走 legacy_route 跳页,新工具据 input_fields 声明驱动表单 → generate 预览 → 可入库者 ingest + 复用 409 冲突裁决)。

C8 · Skill registry + 表权限沙箱 owner @backend 状态: 稳定T5.5, 2026-06-19M5 R3 迁入 ww_skills 包, 2026-06-19

  • 来源:ARCHITECTURE.md §5.6 / PRODUCT_SPEC §5.5(声明式 Skill = AgentSpec「声明式权限 + apply 层白名单」,非进程沙箱。消费方M5 编排/生成入库层、技能库 UIT5.6,经 C3
  • 实现位置M5 R3 已迁移):实现现落 packages/skills/ww_skills/{skill_registry,skill_permissions}.pypackages/skills 已是 uv workspace memberww_skills/__init__.py 直接导出(不再是再导出 ww_core 的门面)。消费方 from ww_skills import SkillRegistry/SqlSkillRepo/SkillRecord/SkillRepo/partition_writes/filter_reads/validate_declaration/KNOWN_TABLESww_core.domain 已删除这两个模块 + 其 __init__ 导出(不再经 ww_core.domain 暴露 skill 符号。历史T5.5 曾因 skills 非 workspace member 暂落 ww_core.domain见 decisions现已迁回。
  • registrySkillRecord{name,scope,tier:Tier,system_prompt,reads:list[str],writes:list[str],genre?}frozenskills 行的声明式快照input/output JSON Schema 暂不携带 Python 类型——纯声明执行)。SkillRepo(Protocol).list_all()->list[SkillRecord]SqlSkillRepo(session)skills 表(裸 tier 非法 → VALIDATIONSkillRegistry.load(repo)classmethodasync→ 每条跑 validate_declaration(越权声明 → AppError VALIDATION,加载中断)→ frozen dict[name,AgentSpec]get(name)->AgentSpec(缺 → NOT_FOUNDnames()->list[str]list_scope(scope)->list[AgentSpec]。不变量 #2只带 tier,不解析 modelmodel 解析在网关)。
  • 表权限沙箱skill_permissions.py,纯函数,不可变):KNOWN_TABLES10 创作表白名单projects/characters/world_entities/outline/chapter_digests/foreshadow/style_fingerprint/rules/chapters/chapter_reviews系统/运营表不在列)。filter_reads(spec, available)->dict(注入只喂声明 reads 的表);partition_writes(spec, produced)->(allowed:dict, rejected:list[str])(写库只应用声明 writes,越权表名进 rejected 供审计 log + 丢弃);validate_declaration(spec)reads/writes 越 KNOWN_TABLES → VALIDATION。不变量 #3allowed 仍须过验收 gate 才真入库,本层只裁剪白名单、不开写后门。
  • 注入缝:get_skill_registrySkillRegistry.load(SqlSkillRepo(session))async dep。T5.2 生成入库层应用 partition_writes 落自定义 skill 产出(仅声明 writes 表,越权审计),仍经验收。
  • @llm 注意:自定义 skill 经网关执行时复用 §5.1 机制registry 给 spec网关按 tier 路由apply 层(编排/入库)须调 filter_reads/partition_writes 守白名单。

C3.5 · jobs 长任务基建(写/进度/回收 + run_job owner @backend 状态: 稳定T4.1, 2026-06-18

  • 来源ARCH §7.4jobs 表 + GET /jobs/:id 轮询BackgroundTasks 无专用队列)。jobs 表 + Job 模型 + GET /jobs/{id} 读端点已 T0.3 建齐T4.1 补写/进度/回收层,供 T4.3「学文风走 jobs」 复用。
  • 位置:packages/core/ww_core/domain/job_repo.py(经 ww_core.domain 导出 JobRepo/JobView/SqlJobRepo+ apps/api/ww_api/services/job_runner.pyrun_job+ services/project_deps.get_job_repo + lifespan reaper。
  • JobView(frozen Pydantic, snake_case)id:uuidkind:strstatus:strprogress:int=0result:dict|None=Noneerror:str|None=None(镜像 GET /jobs/{id} 出参)。
  • JobRepo Protocol写方法只 flush 不 commit提交归 run_job/端点;唯 reap_zombies 自 commit
    • async create(project_id: uuid|None, kind: str) -> JobViewstatus=queued, progress=0
    • async set_running(job_id) -> JobViewstatus=running
    • async set_progress(job_id, pct: int) -> JobViewpct 夹取到 0..100
    • async complete(job_id, result: dict) -> JobViewstatus=done, progress=100, result=
    • async fail(job_id, error: str) -> JobViewstatus=failed, error=
    • async get(job_id) -> JobView | None
    • async reap_zombies() -> intbulk UPDATE runningfailed(+error 文案),返回被改条数,自 commit
    • 状态写方法对不存在的 job 抛 LookupError。状态常量导出:STATUS_QUEUED/RUNNING/DONE/FAILEDPROGRESS_COMPLETE=100job_repo 模块级)。
  • run_jobservices/job_runner.pyasync def run_job(session_factory: SessionFactory, job_id: uuid, work: JobWork, *, request_id: str|None=None, repo_factory=_default_repo_factory) -> None,其中 JobWork = Callable[[AsyncSession], Awaitable[dict]]。语义:自建独立 sessionsession_factory,仿 run_overdue_scan,请求 session 已关闭)→ set_runningawait work(session)(业务写与 job 状态写同一 sessioncomplete(job_id, work 返回值)commit;异常路在全新 session 里 fail(job_id, str(exc)) + commit异常被吞不冒泡(后台任务边界)。SessionFactory 复用 services/foreshadow_scan.SessionFactory= get_session_factory/get_sessionmaker())。repo_factory: Callable[[AsyncSession], JobLifecycleRepo](最小 Protocolset_running/complete/fail)是可注入缝,单测注 fake。
  • lifespan reaperM4-dmain.py _lifespanseed_stub_user 之后 SqlJobRepo(session).reap_zombies()(幂等、自 commit——进程重启丢 BackgroundTask 的残留 running 行标 failed,用户可见可重试。
  • T4.3 行动POST /stylejob_repo.create(project_id,"style_learn") + commit 返 202 {job_id}background_tasks.add_task(run_job, session_factory, job.id, work=<部分应用 run_style_extraction→StyleFingerprintWriteRepo.append 并返回 {version,dims_count} 摘要>)work 内自建 gatewayanalyst+ reporun_job 传入的独立 session。注入缝 get_job_repo/get_session_factory 测试经 app.dependency_overrides override。

C4 · 编排器接口LangGraph 写章图 owner @llm 状态: 稳定M1/T1.3, 2026-06-18M2 扩四审/验收)

  • 来源:ARCHITECTURE.md §5.2(图)/ §7.3SSEM1 仅单 write 节点;并行四审/collect/interrupt(accept) 属 M2。
  • 位置:packages/core/ww_core/orchestrator/
  • 图状态 ChapterState(TypedDict, snake_case){project_id:UUID, chapter_no:int, user_id:UUID, stable_core:str, volatile:str, draft:str}——仅控制流+组装上下文+累积草稿(不变量#5resume 从领域表重读。
  • 节点缝:
    • build_write_request(*, stable_core, volatile, user_id, project_id) -> LlmRequest(纯函数;tier="writer"system=[Block(stable_core,cache=True)]input=volatilestream=Truescope=Scope(user_id,project_id))。
    • async stream_chapter_draft(gateway, *, stable_core, volatile, user_id, project_id) -> AsyncIterator[Delta]——T1.4 拿来喂 normalize_deltas 的底层流缝
    • async write_node(state, *, gateway) -> {"draft":str}——可直接单测(注入 mock 网关)。GatewayStream Protocol = 节点对网关最小依赖(只需 .stream(req))。
  • SSE 归一缝T1.4 消费)async normalize_deltas(deltas, *, request_id=None) -> AsyncIterator[SseEvent]SseEvent{event:str, data:dict};事件 token{text}/done{length}/error{code,message,request_id}ARCH §7.3 子集M2 加 section/conflict。底层异常→发 error 事件后收尾(不上抛);AppError.code 透传,未知→INTERNAL。HTTP event-stream 编码归 T1.4。
  • 图工厂:build_write_graph(gateway, *, checkpointer=None) -> CompiledStateGraphSTART→write→END单测传 MemorySaver
  • checkpointer setup 入口:async setup_checkpointer(conn_string) -> None——只在 migrations/CI 调(内部 AsyncPostgresSaver.from_conn_string 跑一次 setup() DDL懒 import绝不在 app-runtime 跑)。
  • 不变量agent 只传 tierDB 唯一 agent 间通道;不可变更新;瞬时重试在网关不在节点。

C4 扩展T2.2, 2026-06-18· 审稿子图 + review SSE

  • 图工厂 build_review_graph(gateway, review_repo, *, review_specs=(continuity_spec,), checkpointer=None) -> CompiledStateGraphSTART →各 review spec 并行节点→ collect → END。M2 默认仅 continuityreview_specs 可扩M3/M4 加 foreshadow/style/pacebuild_write_graph 保留不动draft 端点仍用它)。
  • ChapterState(state.py) 新增 review_context:strreviews:Annotated[dict, merge_reviews](并行分支浅合并 reducer返回新 dictTypedDict 改 total=False(按节点逐步填充);仍仅控制流+组装上下文+产物句柄(不变量#5
  • 节点缝:run_review(spec, state, *, gateway)->{"reviews":{spec.name:{status,result}}}(裸函数可单测);make_review_node(spec, gateway)(绑 gateway 的公共缝,供 T2.5 跑单审);审稿用 gateway.run()(非 stream只读不写库(不变量#3任一审网关失败被隔离为 {status:"incomplete",result:None}§5.2),不上抛、不毁图。GatewayRun Protocol=节点对网关最小依赖(.run(req)->LlmResponse)。
  • collect 缝:collect_reviews(state, *, review_repo)->{}:抽 continuity 冲突 → review_repo.record(project_id, chapter_no, chapter_version=None, conflicts=[...])chapter_reviews 留痕;只 flush 不 commit(提交归端点/T2.4 事务)。ReviewRecorder Protocol 形对齐 domain.review_repo.ReviewRepo.record
  • SSE 新增事件:section{name,status}status ∈ started/done/incompleteconflict{type,where,refs,suggestion}(对齐 C6 Conflict);新归一缝 normalize_review(reviews, *, request_id=None)->AsyncIterator[SseEvent](每审一条 section + 每冲突一条 conflict + done{length=审项数};异常→error 不上抛,同 normalize_deltas 纪律。HTTP event-stream 编码归 T2.5。
  • 一键采纳改法方案1conflict SSE 事件 + ReviewConflictView(留痕)+ C6 Conflict schema 新增可选 original?:str|null/replacement?:str|null(一键采纳补丁对,最小句段;审稿可局部修复时提议,前端「采纳改法」把 original→replacement find-replace 进终稿,找不到则不动正文提示手改)。向后兼容:缺省 null审稿仍只读只提议不改稿不变量#3
  • accept 不在本图interrupt_before=["accept"]/accept 节点属 T2.4确定性事务代码从领域表重读R3/不变量#5
  • 消费方行动T2.5 跑 review 子图 + normalize_review + 端点流耗尽后 await session.commit()(网关 ledger + collect 均只 flush不提交则记账/留痕静默丢失,同 M1 draft 坑);GET .../reviewsreview_repo.list_for_chapter(新→旧)。T2.4 从 review_repo.list_for_chapter 读 collect 落的行(decisions=None)裁决。

C4 扩展T3.3, 2026-06-18· 三审齐foreshadow + pace 并入)

  • REVIEW_SPECS = (continuity_spec, foreshadow_spec, pace_spec)build_review_graph(..., review_specs=REVIEW_SPECS) 默认三审齐(文风第四审留 M4run_review/make_review_node 对 spec 泛型(按 req.output_schema 产 parsed
  • collect 列映射(一次 review_repo.record(...) 落齐continuity→conflicts(list)foreshadow→foreshadow_sug(listplanted/resolved 扁平成单 list、每条加 kind:"planted"|"resolved")pace→pace(dict{water,hook,beat_map} 入列)style 留 M4。任一审 incomplete→该列空/None不阻塞其余§5.2)。
  • normalize_review SSE向后兼容section/conflict 不变)新增:foreshadow{kind,code?,title,where?,note?}(每条建议一条)、pace{water:[{where,reason}],hook:bool,beat_map:[int]}一条。键按字典序确定性遍历continuity<foreshadow<pace。incomplete 审仅发 section 不发结果。
  • 消费方:前端 T3.6(冲突就地标注 / 伏笔看板联动 / 节拍图 ▁▃▅、E2E T3.7。

C4 扩展T4.2, 2026-06-19· 四审齐style 漂移并入)+ style SSE

  • REVIEW_SPECS = (continuity_spec, foreshadow_spec, pace_spec, style_drift_spec)build_review_graph(...) 默认四审齐。图工厂/run_review/make_review_node 对 spec 泛型,无改动(按 req.output_schema 产 parsedstyle 天然套循环)。
  • collect 列映射新增:extract_style(reviews)->dict|None(取 style 审 ok 结果整 dictcollect_reviews 末尾 style=extract_style(reviews) 传入 review_repo.record(...)chapter_reviews.style JSONB 列已存在。无指纹降级score=100/空段)仍是 ok 结果、照常落库(区别于 incomplete→None
  • normalize_review SSE向后兼容新增style{score:int, segments:[{idx:int,score:int,label:str|None}]}(一条)。键按字典序遍历 → continuity<foreshadow<pace<style确定性顺序。incomplete 审仅发 section{status:incomplete} 不发结果。
  • 常量/工厂导出(经 orchestrator __init__STYLE="style"extract_stylecollectEVENT_STYLE="style"style_event(*, score, segments)sse
  • 消费方:前端 T4.4StylePanel ◔ 相似度 + 漂移段一键回炉,section{name:"style"}/style{...} reduce、E2E T4.5(断言事件真发 + chapter_reviews.style 真填)。

C6 扩展T4.2, 2026-06-19· style-auditor 双轨 + refiner spec/schema

  • 位置:packages/agents/ww_agents/{schemas.py,specs.py}(经 ww_agents 导出);提取节点在 packages/core/ww_core/orchestrator/style_extract_node.py(经 orchestrator 导出)。
  • 提取轨 schemaStyleDimension{name:str, value:str, evidence:list[str]=[]}StyleFingerprintResult{dimensions:list[StyleDimension]=[]}16 维 = 9 通用 + 7 中文网文,每维带原文证据)。
  • 漂移轨 schemaStyleDriftSegment{idx:int, score:int, label:str|None=None}StyleDriftReview{score:int=100, segments:list[StyleDriftSegment]=[]}默认值即无指纹降级态score=100/空段)。
  • style_extract_spec(frozen AgentSpec)name="style_extract", tier="analyst", output_schema=StyleFingerprintResult, reads=["style_fingerprint"], writes=["style_fingerprint"](声明式,真写库经 T4.3)。独立生成(仿 outliner不进 review 图
  • style_drift_specname="style"(=section/列名), tier="light", output_schema=StyleDriftReview, reads=["style_fingerprint"], writes=[]只读第四审。system_prompt 含「材料无指纹→返回 score=100/空段」降级指令。
  • refiner_specname="refiner", tier="writer", output_schema=None(纯文本重写段), reads=[], writes=[](非持久;端点同步返回 {original,refined} 不写库,作者采纳经既有 draft 自动保存合入,不变量 #3
  • 提取节点缝(模块内自有 GatewayRun Protocol,不跨模块复用、不在 __init__ 重复导出,见 gotchabuild_style_extract_request(spec, *, samples_text, user_id, project_id)->LlmRequest(output_schema=StyleFingerprintResult,stream=False,system 缓存断点前块) + async run_style_extraction(spec, *, samples_text, gateway, user_id, project_id)->StyleFingerprintResult(gateway.run().parsed只读不写库;网关失败直接上抛不隔离parsed 违约抛 ValueError,仿 run_outline)。
  • 消费方T4.3 端点 POST /style 经 BackgroundTask 调 run_style_extractionStyleFingerprintResult → 拆 dimensions_json/evidence_jsonstyle_fingerprint 表;POST .../refinerefiner_spec(writer 网关) run() 重写段返回 {original,refined}@frontend 行动T4.4 前 pnpm gen:api 纳入 style/refine/jobs 端点。

C6 扩展T5.1, 2026-06-19· worldbuilder + character-gen spec/schema + 入库前 continuity 校验缝

  • 状态:稳定。位置:packages/agents/ww_agents/{schemas.py,specs.py}(经 ww_agents 导出);生成节点在 packages/core/ww_core/orchestrator/generation_node.py(经 orchestrator 导出)。T5.2 入库端点据此构建field 名已对齐 world_entities/characters DB 列T5.2 ingest 直接映射)。
  • worldbuilder schemaWorldEntityCard{type:str, name:str, rules:list[str]=[]}rules = 显式硬规则清单,供 continuity 引用)、WorldGenResult{entities:list[WorldEntityCard]=[]}。映射 world_entitiestype/name 列 + rules 入 JSONBDB 列 rules 是 JSONB入库可包成 {"rules":[...]} 或裸 list由 T5.2 定夺)。
  • character-gen schemaCharacterRelation{name:str, kind:str, note:str|None=None}CharacterCard{name:str, role:str, traits:list[str]=[], backstory:str, arc:str, speech_tics:list[str]=[], tags:list[str]=[], relations:list[CharacterRelation]=[]}必填name/role/backstory/arc其余集合默认空CharacterGenResult{cards:list[CharacterCard]=[]}。映射 charactersname/role/backstory 列 + traits/arc/speech_tics/tags/relations 入各自 JSONBDB traits/arc/speech_tics 是 JSONB dict 列、tags/relations 是 JSONB list——T5.2 ingest 须把 list[str] 的 traits/speech_tics 包进 dict 或调整,按 DB 列形落库;role = 角色定位 主角/CP/对手/导师/工具人)。
  • worldbuilder_spec(frozen AgentSpec)name="worldbuilder", tier="writer", output_schema=WorldGenResult, reads=["projects"], writes=["world_entities"](声明式,真写库经 T5.2)。独立生成(仿 outliner不进 review 图。system_prompt 强调硬规则显式可校验。
  • character_gen_specname="character-gen", tier="writer", output_schema=CharacterGenResult, reads=["world_entities","characters"], writes=["characters"]。system_prompt 注入「已生成卡 + 已有角色」差异化要求M5-a 防雷同)——含「已有角色」「已生成卡」「防雷同」字样。
  • 生成节点缝(模块内自有 GatewayRun Protocol,不跨模块复用、不在 __init__ 重复导出,见 gotcha导出公共函数 + 三个 context builder
    • async run_worldbuilder(spec, *, brief, project_context, gateway, user_id, project_id)->WorldGenResult+ build_worldbuilder_context(*, brief, project_context)->str)。
    • async run_character_gen(spec, *, brief, count:int, role:str|None, world_context:str, existing_chars:Sequence[CharacterCard], generated_so_far:Sequence[CharacterCard], gateway, user_id, project_id)->CharacterGenResult+ build_character_gen_context(*, brief, count, role, world_context, existing_chars, generated_so_far)->str——防雷同上下文:注入已有+已生成卡的 name(role)traits 简表,要求差异化;空时给「(暂无…)」占位不崩)。
    • 入库前 continuity 校验缝ARCH §6.5T5.2 调)async precheck_generated_cards(spec, *, cards:Sequence[CharacterCard], world_context:str, characters_context:str, gateway, user_id, project_id)->list[Conflict]+ build_precheck_context(*, cards, world_context, characters_context)->str)。复用 continuity_specanalyst 档,只读)跑生成卡 vs 世界观/已有角色真相源比对、返回 list[Conflict]C6 Conflict 五类)。编排器追加的检查,非 character-gen 直接互调(守 §5.4 数据流/不变量 #1。T5.2 入库端点用返回的 conflicts 做 gate有冲突→提示作者裁决/调整,不静默入库)。
    • 三函数均:生成用 run()stream()output_schema=spec.output_schemasystem 缓存断点前块cache=True#9网关失败直接上抛端点处理独立生成不做失败隔离parsed 违约抛 ValueError(仿 run_outline/run_style_extraction只读不写库#3
  • 测试替身:复用 packages/core/tests/fakes_orchestrator.pyFakeRunGateway/SchemaRoutingRunGateway/FailingRunGateway(按 req.output_schema 路由 parsed
  • 消费方:T5.2 端点 POST /world/generaterun_worldbuilderPOST /characters/generaterun_character_gen(批量循环时把已产卡传 generated_so_far 防雷同)、POST /characters(入库) 前调 precheck_generated_cards gateanalyst 网关复用 build_gateway_for_tier(session,store,"analyst")writer 用 "writer"。记账闭环同既有(端点末尾 commit

C5 · 记忆服务 assemble / select_relevant_entities owner @backend 状态: 稳定T1.2, 2026-06-18

  • 来源:ARCHITECTURE.md §3.4 / §5.3(确定性选择:显式+主角+近况;渲染卡片;缓存断点)。
  • 位置:packages/core/ww_core/memory/+ domain/repositories.py)。
  • 输出(决策:中性文本,非 LlmRequest,见 decisions 2026-06-18
    • AssembledContext{ stable_core:str, volatile:str, selection:SelectionTrace }
      • stable_core:断点前(作品蓝本(title/logline/premise/theme)+世界硬规则+定型主角(无 latest_state)+文风指纹+合并规则),已排序/无时间戳/无 UUID。「作品蓝本」是书级 spec、定型 → 入缓存前缀首段。
      • volatile:断点后(写作指令「请创作第 {chapter_no} 章的正文。」始终在场+注入卡片(含 latest_state)+伏笔窗口+近况摘要+本章 beats
    • 空-prompt 保证2026-06-19 修bugfix:哪怕世界观/角色/大纲全空,只要项目有 premise/titlestable_core(含作品蓝本) 与 volatile(含写作指令) 均非空 → writer 的 user message 永不为空(修 LLM 400 "message at position 0 ... must not be empty")。
    • SelectionTrace{ selected:list[SelectedEntity] }SelectedEntity{ kind:"character"|"world_entity", name:str, reasons:list[SelectionReason] }SelectionReason="explicit_beat"|"main_character"|"recent_digest"|"foreshadow_window"
  • 入口:async assemble(repos:MemoryRepos, project_id, chapter_no, recent_k=5)->AssembledContext;纯函数 select_relevant_entities(*, outline, characters, world_entities, recent_digests)->SelectionTracerender_cards(selection, characters, world_entities)->strmerge_rules(rules)->list[RuleView]global→genre→style→project
  • 依赖注入:MemoryRepos 捆绑 8 个 ProtocolOutline/Character/WorldEntity/Digest/Foreshadow/Style/Rules/Project);单测注入内存 fake运行时 sql_memory_repos(AsyncSession)T1.4 注入点)。ProjectSpecRepo.spec(project_id)->ProjectSpecView|NoneProjectSpecView{title, logline?, premise?, theme?},按 project_id 读 projects2026-06-19 新增,消费方须在 MemoryRepos(...)project= 字段)。
  • 不变量:确定性选择(无向量 #6);统一 project_id 过滤(§3.5)latest_state 严格归 volatile(#9)。
  • 消费方:T1.3 write 节点 → 构造 LlmRequest(system=[Block(stable_core, cache=True)], input=volatile)

C6 · Agent 声明AgentSpec+ 续审 I/O schema owner @llm 状态: 稳定T2.1, 2026-06-18

  • 来源:ARCHITECTURE.md §5.1 / §5.4 / §6.1。位置:packages/agents/ww_agents/specs.py / schemas.py,经 ww_agents 导出)。
  • AgentSpecfrozen Pydantic不可变name:strtier:Tier(writer/analyst/light复用 ww_llm_gateway.types.Tier——只声明档位不写 model不变量#2)、system_prompt:strinput_schema:type[BaseModel]|Noneoutput_schema:type[BaseModel]|None(writer 为 None=纯文本)、reads:list[str]writes:list[str]genre:str|None=Nonescope:str="builtin"
  • continuity_spectier="analyst"reads=["chapter_digests","characters","world_entities"]writes=[](只读,不变量#3)、input_schema=None(注入材料为序列化文本)、output_schema=ContinuityReview
  • ContinuityReview{ conflicts: list[Conflict] }仅冲突digest 不在审稿期产,不变量#4
  • Conflict{ type: ConflictType, where:str, refs:list[str]=[], suggestion:str }ConflictType = Literal["性格漂移","能力不符","设定违例","地理矛盾","时间线倒错"]ARCH §6.1 五类)。
  • 续审节点调法:req = LlmRequest(tier="analyst", system=[Block(system_prompt, cache=True)], input=审稿上下文文本, output_schema=ContinuityReview, scope=...)resp = await gateway.run(req)resp.parsedContinuityReview 实例(带 schema 时必非 None。续审用 run()stream();只读+产冲突,不写库(写在 T2.4 验收事务)。
  • 消费方:编排器(T2.2 续审/collect 节点)、技能运行时(M5)、前端审稿页(经 C3)。foreshadow/style/pace 三审 spec 待 M3-T3.3/M4 补入本契约。

C6 扩展T3.4, 2026-06-18· outliner Agent + 大纲/伏笔窗口 schema

  • 位置:packages/agents/ww_agents/{schemas.py,specs.py}(经 ww_agents 导出);节点在 packages/core/ww_core/orchestrator/outline_node.py(经 orchestrator 导出)。
  • outliner_spec(frozen AgentSpec)name="outliner"tier="analyst"reads=["projects","foreshadow","characters","world_entities"]writes=["outline"](声明式,真写库经 T3.5,不变量#3)、input_schema=Noneoutput_schema=OutlineResult
  • OutlineResult{chapters:list[OutlineChapter]}OutlineChapter{no:int, beats:list[str], foreshadow_windows:list[ForeshadowWindow]}ForeshadowWindow{code:str, plant_chapter?:int, expected_close_from?:int, expected_close_to?:int}§6.2 关联章与伏笔)。
  • 节点缝(沿用 review_node 的 GatewayRun Protocol .run(req)->LlmResponsebuild_outline_request(spec, *, context, user_id, project_id)->LlmRequest(output_schema=OutlineResult,stream=False) + async run_outline(spec, *, context, gateway, user_id, project_id)->OutlineResult(gateway.run().parsed只读不写库;大纲独立生成、非并行审,网关失败直接上抛不隔离parsed 违约抛 ValueError)。
  • 消费方T3.5 端点调 run_outlineOutlineResult → 逐章 upsert outline 表(beats/foreshadow_windows 入 JSONBvolume 由端点分卷逻辑提供schema 不含 volumeanalyst 网关复用 build_gateway_for_tier(session,store,"analyst")/get_review_gateway;记账闭环同 M2端点末尾 commit()。T3.6 前端大纲编辑器消费窗口。

C6 扩展T3.3, 2026-06-18· foreshadow-analyst + pace-checker spec

  • foreshadow_spec(name="foreshadow", tier="analyst", reads=["foreshadow"], writes=[]只读, output=ForeshadowReview{planted:[ForeshadowSuggestion],resolved:[ForeshadowSuggestion]}ForeshadowSuggestion{code?,title,where?,note?})。
  • pace_spec(name="pace", tier="light", reads=["rules"], writes=[]只读, output=PaceReview{water:[PaceIssue{where,reason}],hook:bool,beat_map:[int]})。genre 模板 DSL = pace system_prompt 内描述黄金三章/章末钩子/爽点密度genre 级模板经 review_context 规则合并(rules)注入,未新建表/repo
  • 三审只读,留痕经 collect→review_repo写库 commit 归端点)。详细图/SSE 见上「C4 扩展T3.3)」。

C7 · 前端 ↔ 后端类型契约 owner @backend(产出 OpenAPI) / @frontend(生成) 状态: 待定义

  • 机制FastAPI OpenAPI → apps/web/lib/api TS 类型openapi-typescript/orval
  • 规则:后端 schema 任何变更 → 跑 gen:api 重生成;不手写共享类型。

C-Chain · 多章工作流链端点 + 服务层 owner @backend / @llm(图) 状态: 稳定C2, 2026-06-23

设计真源:docs/design/chain-workflow.md。C1(@llm) 图签名 §3.3C2(@backend) 端点/schema/服务接线。

  • 3 端点routers/chain.py,挂 /projects 前缀):
    • POST /projects/{pid}/chains/{chain_key}/runChainRunRequest{start_chapter_no:int>=1, count:int 1..50} → 202 ChainRunAccepted{job_id, chain_key, start_chapter_no, count}。写一行 jobs(queued, kind="chain") 返 202链经 BackgroundTask run_chain_job 跑(自建独立 session。未知 chain_key→404draft_volumecount 越界→422schema Field项目不存在→404无凭据→503。
    • GET /jobs/{job_id}复用现有):轮询进度/awaiting。链 job result = {chain_key, written:[int], completed:bool, awaiting_chapter:int|null}interrupt 命中时 status="awaiting_input" + result.awaiting_chapter
    • POST /projects/{pid}/chains/runs/{job_id}/resumeChainResumeRequest{decisions:[ConflictDecision]} → 202 ChainRunAccepted。仅当 job=awaiting_input;非该态→409 CONFLICTjob 不存在→404。带裁决 Command(resume=...) 续跑。
  • schemaschemas/chain.pyChainRunRequest/ChainRunAccepted/ChainResumeRequestConflictDecision 复用 schemas/projects.pyconflict_index/verdict/note
  • 零迁移§7复用 jobskind/status/progress/result 全已存);新增 status"awaiting_input"jobs.status 自由 Text 列,无 DDLawaiting 章经 result.awaiting_chapter 表达。JobReposet_awaiting(job_id, result)唯一新增 DDL = langgraph 检查点表C3 迁移建,本任务未引入)。
  • 新增错误码ww_shared/errors.pyErrorCode.CONFLICT→409资源状态冲突如对非 awaiting 链 job 续跑;与 CONFLICT_UNRESOLVED 区分)。
  • 新网关缝project_deps.pyget_chain_gatewaybuild_chain_gateway:按请求 tier writer/analyst/light 分派、union 三档适配器+回退链——单档网关 chain_resolver 恒返该档会错路由 review/digest故链需多档分派get_digest_gateway_builderaccept 节点自建短事务建 light 档 digest 网关)。get_checkpointer_factoryservices/chain_deps.py:运行时 AsyncPostgresSaver.from_conn_string(database_url_sync) 上下文;测试注 MemorySaver 工厂)。
  • token 纪律job result/status/日志只记章号/计数/标志,绝不含 prompt/正文/token§5已断言
  • → 影响 @frontend链发起页/进度/裁决续跑面板,非目标本期不做,契约稳定后 follow-up pnpm gen:api@db/@devops C3检查点建表迁移 + 可选 jobs 注释)。

C6-ext · Prompt 外置 + SpecResolver owner @llm(SPECS/loader/catalog) + @backend(resolver/守卫) 状态: 稳定方案A2026-06-24

设计真源:docs/design/prompt-management.md。纯重构、零功能/schema 变更、零迁移(前端无需 pnpm gen:api)。

  • load_prompt(name) -> strpackages/agents/ww_agents/prompt_loader.py@llmprompts/<spec.name>.md import 期读盘 → 去 BOM(utf-8-sig)/CRLF·CR→LF/NFC/rstrip('\n') → 内存缓存 → 返回完整 UTF-8 文本;缺文件 PromptNotFoundErrorfail-fast不 fallback、不插值
  • SPECS: Final[dict[name, AgentSpec]]specs.py@llm21 内置 spec 集中名册,assert len(SPECS)==21system_prompt=load_prompt(name)output_schema=SCHEMA_CATALOG[name]不变量SPECS[name] is *_spec(兼容期同一实例,旧 from ww_agents import *_spec 不破);prompts/<name>.md/SPECS[name]/SCHEMA_CATALOG[name] 三集合按 name 恒等。
  • SCHEMA_CATALOG: Final[dict[name, type[BaseModel]|None]]schema_catalog.py@llmname→output type 唯一真相源refiner=Noneoutput_schema_for(name)。input 全 None本波不建 input 槽YAGNI
  • REVIEW_RESERVED_NAMES: Final[frozenset]={continuity,foreshadow,style,pace}@llm四审受信名显式白名单非派生集合安全边界锚此。
  • SpecResolverpackages/skills/ww_skills/spec_resolver.py@backendbuild(skills) 纯合并 dict(SPECS)+SkillRegistry(读路径无冲突校验);get(name) 先查内置 SPECS纯内存、零 DB)未命中才查 registry都无→NOT_FOUNDoutput_schema_for/names/list_scope。name 精确字符串相等(无大小写/连字符归一)。
  • 守卫前移:用户 skill 同名内置(REVIEW_RESERVED_NAMES set(SPECS))在 SkillRegistry 入库/加载校验期即拒(AppError(VALIDATION),与 validate_declaration 同处),不在 resolver 读路径——内置 get 确定性、零运行时分叉,守不变量 #3。
  • 打包契约@devops.gitattributespackages/agents/ww_agents/prompts/*.md text eol=lfpackages/agents/pyproject.toml hatchling artifacts 纳入 prompts/*.md 随 wheel/sdist 分发CI agents-wheel-smokebuild→裸装→import ww_agents; assert ww_agents.SPECS)防 .md 漏带(源码树 pytest 测不出)。packages/{llm_gateway,core} 补声明 structlog(原直接 import 未声明,靠 apps/api 传递)。
  • 消费方:toolbox_registry.GeneratorTool.specSPECS["<name>"](不再直接 import 生成器 *_spec)。本波不改编排器(REVIEW_SPECS/节点/§6.5 precheck/chain与 apps/api 3 路由(toolbox/outline/style)——兼容期续用 *_spec(因同一实例不变量无回归),切 resolver 列后续波次。

契约变更日志append-only

格式:- [date] @skill 改 Cx<改了什么> → 影响 <依赖方/任务>

  • [2026-07-06] @llm/@db/@backend/@frontend 改 C3+C4③ 人物塑造 advisory 审):新增第五审 人物塑造advisory 单维——仅建议、不阻断验收:不进 conflicts、不碰 assert_conflicts_resolved不入 REVIEW_RESERVED_NAMESD2DBchapter_reviews 加 1 列 characterizationJSONB nullable,迁移 f6a7b8c9d0e1,独立于恒 None 的 health_scorenullable 免 backfill契约变更ReviewHistoryItemschemas/projects.py+ ReviewViewdomain/review_repo.py,末位默认 None 优雅降级)各加 characterization: dict|Nonelist_reviews 路由透传。新 SPEC characterization_spectier="analyst" 不写 modelreads=("characters",) 以人物卡 motive/appearance 为客观锚点,writes=() 只读,prompts/characterization.md 显式忽略文本长度与辞藻华丽度、引用逐字片段+置信度)+ schema CharacterizationReview{issues:[CharacterizationIssue{character,aspect,where,quote,diagnosis,suggestion,confidence}]}(全字段默认值守解析韧性)+ 注册 SCHEMA_CATALOG["characterization"]计数三处 +122→23specs.py assert、test_prompt_loader.py len 断言、schema_catalog.py docstringregen prompt_hashes.json(仅加 characterization 一条)。接线链:graph.py REVIEW_SPECS 末位追加(并行扇出自动汇入 collectchain/nodes.py ReviewRecordRepo.record + collect.py {CHARACTERIZATION 常量, extract_characterization, ReviewRecorder Protocol, collect_reviews} + review_repo.py {Protocol.record, SqlReviewRepo.record, _to_view} 均加 characterization kwarg/字段;sse.py {EVENT_CHARACTERIZATION, characterization_event, _section_result_events elif}normalize 按 sorted 键自动纳入——字典序 characterization 排最前,不打乱既有四审断言)。前端 lib/review/sse.tsCharacterizationIssue/CharacterizationReport/CharacterizationEvent + KNOWN_EVENTS + asCharacterizationIssues 守卫 + reducer + state+ lib/review/history.ts normalizeCharacterization + useReviewStream ReviewSeed + 新 CharacterizationPanel.tsxadvisory 只展示、无裁决 UI、按置信度降序 + 低置信度折叠控注意力预算)+ ReviewReport.tsx 挂面板 + 双 seed 点。codegencharacterization = None → openapi-typescript 渲染为可选。测试test_characterization_does_not_block_accept(零 continuity 冲突 + 有 advisory 问题 → 验收直通)+ 计数/golden + collect 映射 + normalize 事件 + 前端 reducer/normalize/parse 纯函数。→ 影响 @frontendpnpm gen:api)。依赖 ⑧motive/appearance 已入 CharacterCard 契约,作客观锚点)。

  • [2026-07-06] @llm/@backend/@frontend 立 C3 新端点(⑤ AI 立项方案生成):POST /skills/project-plan/generate不带 project 前缀——立项前 project 未建,绕开 toolbox 的 project_repo.get→404)← ProjectPlanGenerateRequest{genre?,logline?,title?,premise?,theme?,structure?,tone?,ending_type?,narrative_pov?,selling_points[],brief?}(全字段 max_length 上界 CR-H9briefstr|None 令 codegen 渲染为可选)→ ProjectPlanView{title_candidates[str], setting, narrative_structure, story_core, ending_design, tone}(全字段默认值守解析韧性)。种子门控:缺 genre 或 loglinestrip 后空)→ 422 VALIDATION(空向导不出方案,防 slop无凭据→503。新 SPEC project_plan_spectier="analyst" 不写 modelreads=()/writes=() 纯预览,prompts/project-plan.md+ schema ProjectPlanResultww_agents.schemas+ 注册 SCHEMA_CATALOG["project-plan"]计数三处 +121→22specs.py assert、test_prompt_loader.py len 断言、schema_catalog.py docstringregen prompt_hashes.json(仅加 project-plan 一条)。端点复用 build_brief_context+run_generatorrun_generator/_build_requestproject_id 放宽为 uuid.UUID|None——向导阶段无 projectusage_ledger.project_id 本就 nullable新网关缝 get_project_plan_gatewayanalyst 档,project_deps.py)。零迁移(纯预览不写业务表,仅落 usage ledger。前端 wizard.tsmapPlanResultToWizardForm书名候选→title、叙事结构→structure、基调→tone时空背景/结局设计无独立向导字段→折进 premise 带标签不静默丢;结局取向/叙事视角预设枚举不臆造)+ applyPlanPatch仅回填空字段、保护已编辑+ canGeneratePlan/toPlanSeedRequest + useProjectPlan hookrenderHook 测试)+ ProjectWizard.tsx 第 2 步 PlanAssistant(生成→预览→按字段回填)。→ 影响 @frontendpnpm gen:api)。依赖 ④tone/ending_type/narrative_pov 已入 project 契约与向导)。

  • [2026-07-06] @db/@backend/@frontend 改 C2+C3④ 立项基调/结局/视角):projects 表加 3 列 tone/ending_type/narrative_povText nullable、无 CHECK,同 genre/structure迁移 e5f6a7b8c9d0nullable 免 backfillProjectCreateRequest+ProjectResponseschemas/projects.py)各加 3 字段 str|None无 LiteralLiteral 只留 Verdictdomain ProjectCreate/ProjectView/_to_view/SqlProjectRepo.create + FakeProjectRepo 同步透传。完整 stable_core 链路:ProjectSpecViewrepositories.pyfrozen加 3 字段 + SqlProjectSpecRepo.spec 查询构造带上 + _build_spec_sectionassemble.py)渲染进「作品蓝本」块(书级常量→缓存前缀,字节稳定守 #9,绝不入 volatile。前端 wizard.ts WizardForm/emptyWizardForm/toCreateRequest + 预设数组 TONES/ENDING_TYPES/NARRATIVE_POVS(仿 GENRES/STRUCTURES+ ProjectWizard.tsx 第 3 步控件tone→Selectpov/ending→SegmentedControl+ 确认页回显。codegen3 字段皆 = None 无默认工厂 → openapi-typescript 渲染为 ?: string | null 可选。→ 影响 @frontendpnpm gen:api);未来 ⑤ AI 立项方案回填这 3 字段。

  • [2026-07-06] @llm/@backend 改 C3⑧ appearance/motive 穿线):CharacterCardViewschemas/generation.py+ ww_agents.CharacterCard 各加 appearance:str="" + motive:str=""均给默认值守解析韧性DB 列 characters.appearance/motive 早已在初始迁移、此前一直 NULL——无新迁移)。写侧 CharacterWriteRepo.create/SqlCharacterWriteRepocreate+update backfill 分支/CharacterWriteFields 同步加两列;routers/generation.py _card_to_view/_view_to_card/_existing_characters 双向形变带上两字段。同批重切 motive/traits/arc 口径:动机唯一落 motivetraits[] 只留核心/表层/阴影三层,arc 引用 motivecharacter-gen.md → 已 regen prompt_hashes.json 金标准)。codegen 注意:因带 defaultopenapi-typescript 把两字段渲染为必填(响应恒有值),前端 CharacterCardView 字面量构造点须显式给两字段。→ 影响 @frontendpnpm gen:api + CharacterCardItem 展示/编辑两字段);未来 ③ 人物塑造审查以 motive/appearance 为客观锚点。

  • [2026-07-06] @backend/@llm/@frontend 改 C3⑦ 群像按定位配比·缩减版):CharacterGenerateRequestschemas/generation.py)加 role_mix:dict[str,int]|None——model_validator(mode="after") 在场时校验非空、每项≥1、总和≤MAX_GENERATE_COUNT=12并把 count 归一为各值之和(构造期归一化,忽略客户端 count缺省退回单一 role+count归属纠偏build_character_gen_context+run_character_genww_core/orchestrator/generation_node.py=@llm 域,加末位默认 role_mix=None 关键字参,在场时注入「角色定位配比」块(保插入序、确定性、无时间戳,守 #6/#7覆盖单一 role 行);routers/generation.py 穿 role_mix=body.role_mixcharacter-gen.md 加「按配比分配 role」纪律 → 已 regen prompt_hashes.json(仅 character-gen 哈希变,不进 SPECS/不 bump 计数)。前端 cards.tssanitizeRoleMix/roleMixTotal + buildCharacterGenerateRequest 第 4 参 roleMix在场时 count=Σ、发 role_mix、丢 roleuseCharacterGen CharacterGenInput.roleMix 穿参;新 RoleMixEditor.tsx + CharacterGenerator SegmentedControl 单一/配比双模。codegenrole_mix 渲染 ?: {[k:string]:number} | null 可选。→ 影响 @frontendpnpm gen:api)。无迁移

  • [2026-06-24] @llm/@backend 立 C6-extPrompt 外置方案A21 prompt 散文外置 prompts/<name>.md + load_prompt/SPECS/SCHEMA_CATALOG/REVIEW_RESERVED_NAMES@llm+ SpecResolver + SkillRegistry 入库守卫前移 + toolbox 桥接 SPECS@backend+ .gitattributes/wheel package-data/CI 冒烟(@devops。随后全量重写 21 prompt 内容任务对齐schema 契约/不变量保持,金标准 fixture 重生成。门禁绿ruff/format/mypy(209)/pytest(744)。→ 影响:后续波次可将编排器 + apps/api 路由切 SpecResolver 并删 *_spec 导出前端零影响GeneratorTool 描述符字段不变)。

  • [2026-06-23] @backend 立 C-ChainC23 端点 + schemas/chain.py + services/{chain_runner,chain_deps}.py + jobs 零迁移复用(加 status="awaiting_input" + JobRepo.set_awaiting+ 新 ErrorCode.CONFLICT(409) + project_depsbuild_chain_gateway/get_chain_gateway/get_digest_gateway_builder/get_checkpointer_factory。OpenAPI 含 2 新 POSTGET /jobs 复用。门禁绿ruff/format/mypy(193) 干净 + pytest 600 passed + alembic 无漂移。→ 影响 @frontendfollow-up pnpm gen:api)、@db/@devops C3检查点迁移

  • [2026-06-20] @backend 扩 C3新增 GET /projects/{project_id}/chapters/{chapter_no}/injection本章注入透明B0 读端点)→ InjectionResponse{project_id, chapter_no, selected:[InjectionEntity{kind,name,reasons[]}], recent_n}schemas/injection.pysnake_case。实现仅调既有 assemble() 回放确定性 SelectionTrace——无 LLM、无 commit、无 DDL项目不存在→404无大纲→selected:[]。reasons 取值同 SelectionReasonexplicit_beat/main_character/recent_digest/foreshadow_window。→ 影响 @frontendpnpm gen:api + ChapterAssistant 消费)。B0 可控版PUT override + select_relevant_entities 加 pinned/excluded/recent_n + draft 端点同读 override + 持久化)尚未实现,届时再扩本契约。

  • [2026-06-20] @backend 再扩 C3B0 可控版,接上条):新增 PUT /projects/{project_id}/chapters/{chapter_no}/injectionInjectionOverrideRequest{pinned:[{kind,name}], excluded:[{kind,name}], recent_n:int|null(1..20)} → 返回 InjectionResponse(同上,新增回显字段 pinned/excludedselected[].reasons 可含新值 author_pin。语义pin 强制纳入并加 author_pin 理由 / excluded 强制剔除(优先于 pin/ recent_n 覆盖近况回看章数。GET injection 与 draft 流式端点均先读同一覆盖再 assemble(override=...),故「看到的=写章用的」(不变量 #6 作者兜底)。持久化:新表 chapter_injection(迁移 ad2c4c663daf,唯一 (project_id,chapter_no),复用 outline 行已否决);select_relevant_entitiespinned/excluded frozenset 入参、assembleoverride 关键字参;新 domain/injection_repo.pyInjectionOverride/EntityRef/SqlInjectionOverrideRepoupsert 只 flush 端点 commit。→ 影响 @frontendpnpm gen:apiF1 加 pin/排除控件 + recent_n 步进器)。

  • [2026-06-18] @llm 改 C1Gateway.run() 现消费 LlmRequest.output_schema——OpenAICompatAdapter.complete 在 schema 非空时经 instructor(create_with_completion(response_model=...)) 取已校验 Pydantic 实例并填 LlmResponse.parsed;无 schema 时 parsed is None、纯文本路径不变;记账仍 1 条 usage_ledger/调用usage 从 raw completion 提取)。ProviderResult 新增 parsed 字段。结构化路径经可注入 StructuredClient Protocol 注 fake测试不联网。→ 影响 T2.2(续审节点可直接 gateway.run(req).parsed)、未来所有结构化输出 Agent。

  • [2026-07-06] @frontend 前端依赖新增(灵感⑥ 关系图谱缩减版 / D4无后端契约变更、无 gen:api、无迁移apps/web/package.json@xyflow/react@^12.11.2React FlowMITpeer react>=17 兼容 R19+ @dagrejs/dagre@^3.0.0(一次性初始布局,自带 TS 类型)。pnpm installpnpm-workspace.yaml 被 pnpm 自动追加 minimumReleaseAgeExclude@xyflow/react/@xyflow/system 供应链豁免)+ pnpm-lock.yaml 更新。新增 lib/characters/graphLayout.ts(纯逻辑:角色+relations→节点/边+dagre 布局,仅用现有 CharacterCardView.relations={name,kind,note}——边按 name-join、kind 作中文标签、节点按 role 上色,无 closeness/阵营,悬空边跳过并计数)+ components/characters/RelationshipGraph.tsx'use client',经 CodexPage dynamic(ssr:false) 加载,显式容器高度 + import RF CSS + prefers-reduced-motion 关 fitView 动画 + 纸感配色)。消费方:无(叶子特性,挂在设定库人物 tab

  • [2026-07-07] @backend/@llm/@frontend 立 C-RewriteWFW-8 整章再沟通/重写):新增 POST /projects/{project_id}/chapters/{chapter_no}/rewriteSSEtext/event-stream复用 token/done/error 帧契约)← RewriteStreamRequest{feedback:str(1..4000), prior_draft:str(1..200000)} → 流式重写整章一版。实现:prompts/rewrite.md 教条(非 SPEC仿 write_craft 经 load_prompt 读盘,test_prompt_loader doctrine 排除集加 rewrite+ orchestrator/rewrite_node.pybuild_rewrite_request 纯函数 system=[rewrite教条,stable_core] cache 前缀 / input=近况+当前草稿+作者意见 断点后,守不变量 #9stream_chapter_rewrite+ 端点复用 assemble() 记忆注入 + normalize_deltas只读不写库HITL新版停前端接受才落不变量 #3工具非写节点(不破坏 章=f(outline,state) 纯函数,不变量 #7、项目不存在触网关前 404。无 DDL/无迁移。门禁绿ruff/mypy(224)/pytest(+test_rewrite_node 5 例)/alembic 无漂移。→ 影响 @frontendpnpm gen:apiuseChapterRewrite SSE hook + ChapterRewritePanel 版本栈 + Workbench「整章重写」入口