# 踩坑与约定(append-only) > 实现中发现的坑、易错点、约定俗成——让兄弟 agent 不重复踩。一条一项,最新在最上。 > 只记**非显而易见**的;规格/CLAUDE.md 已写的别重复。 格式:`- [date] @skill <坑/约定> — 缘由 + 怎么做` --- - [2026-06-20] @frontend **页面重访回显已存内容用 RSC 读 helper(`lib/api/server.ts`)作初值种入 client 组件,404/错误降级、绝不阻塞进页**(大纲/写作重载):先前大纲页 `initialChapters=[]`、工作台 `useState("")` → 重访空白虽然库里有数据。范式:① **大纲**:`fetchOutline(projectId)` 包 `getJson` 取 `chapters??[]`,**try/catch 整体降级为 `[]`**(项目存在无大纲后端返 200 空列表,但任何错误也不该让大纲页 500);`useOutline(initial)` 已据 `initial.length>0` 置 `status="ready"`,传非空即回显,生成流照常覆盖。② **草稿**:`fetchDraft` 直接复用既有 `getJsonOrNull`(**404→null**,后端无草稿行正是 404),页面 `draft?.content ?? ""` 作 `Workbench` 的 `initialText`。③ **不要把初值塞进 stream 的 reducer**——`Workbench` 用 `useState(initialText)` 起步即可:流式那条 `useEffect` 起始 `stream.state.text` 为空、仅当 `phase∈{streaming,done,aborted}` 且文本变化才 `setText`,所以初值不会被空流反扑,SSE+AbortController+PUT 自动保存全不用动。④ 测纯加载/降级逻辑:`lib/api/server.test.ts` 用 `vi.stubGlobal("fetch", …new Response(JSON, {status}))` + `afterEach(vi.unstubAllGlobals)`(node 环境够用,`server.ts` 只依赖 `process.env`/`fetch`),断 200 解包、空/错误→`[]`、404→null。 - [2026-06-19] @qa **Kimi OAuth E2E 要 token 真落 pg:override `get_session_factory`→`e2e_sm`、但**不能** monkeypatch `kimi_oauth.SqlCredentialStore`(K1.3 单测那样会让落库走内存 fake,验不到真 pg)**(K1.5):与 K1.3 单测(FakeSession/FakeStore/FakeJobRepo,验逻辑)不同,E2E 要验真持久化——让后台 `work` 自建的 `SqlCredentialStore(session)` + `run_job` 默认 `SqlJobRepo` **保持真**,只 override `get_session_factory`→`e2e_sm`(后台 work 用真 session 写真 pg)。仍须:monkeypatch **模块级** `routers.kimi_oauth._default_http_client`→同一 scripted fake(后台 work 自建 http 非 dep)+ `app.dependency_overrides[kimi_oauth._default_http_client]`→同一 fake(端点 device-auth 走 dep;call[0]=device-auth、call[1..]=token 轮询顺序共享)+ monkeypatch `routers.kimi_oauth.asyncio.sleep`→no-op。断「加密非明文」:`_ACCESS_TOKEN.encode() not in row.oauth_enc`(Fernet 密文里找不到明文字节)+ `decrypt_oauth_bundle` 回环。`Gateway` 无公开 adapters accessor → 用 `gateway._adapters[provider]`;`KimiCodeAdapter._client` 读 `.base_url`/`.default_headers`/`.api_key`(同 K1.2 单测)。refresh-on-build 直测 `_build_provider_adapter`(非经 HTTP,更清晰):字符串 target monkeypatch `"ww_api.services.project_deps.httpx.AsyncClient"` + `setattr(project_deps,"kimi_refresh",fake)`。清理:删 `provider_credentials`(kimi-code) + `jobs`(kind=kimi_oauth) + `tier_routing`(project_id 为 NULL 的全局行)。ruff E501 按**显示宽度**算(中文字符占 2 列)——含中文注释/docstring 的行别贴边 100。 - [2026-06-19] @frontend **Kimi Code OAuth 三个端点无 params/body → openapi-fetch 用 `api.POST(PATH, {})` / `api.GET(PATH)`;路径用 `as const` 字面量常量复用**(K1.4):`POST .../oauth/start`、`.../oauth/disconnect`、`GET .../oauth/status` 的生成 schema 全是 `path?: never; ...; requestBody?: never`——`api.POST(START, {})`(传空 init obj,typecheck 绿)、`api.GET("/settings/providers/kimi-code/oauth/status")`(无第二参)。把 `start`/`disconnect` 路径抽成模块级 `const START = "/settings/...start"` 给 `api.POST` 时,**字符串字面量类型仍被 openapi-fetch 推断为合法 path key**(无需 `as const`,但抽常量去重 DRY)。② 连接流复用 `useJobPoll`(M4)零改:`connect` 拿 `data.job_id` 调 `poll.poll(job_id)`;轮询终态用 `useEffect([poll.status])` + `startedRef` 守门(同 `useStyleLearn` 先例,`initialPollState.status` 默认 `"polling"` 不能进页即据此显进度)。③ kimi_oauth job 完成态 result 只 `{connected, provider}`(**无 token**)——`jobConnected(job)=job.result?.["connected"]===true`,**别**期待/解析任何 token 字段。④ user_code a11y:用 ``(可读、可全选复制、可聚焦),别用纯 ``。⑤ 档位路由原是只读展示,K1.4 改成可编辑(per-tier `