交接档案
07 · 问题风险与失败经验
原因分级:【已证实】 有日志/复现/文档证据 |【高度可疑】 证据链完整但缺最后一环 |【未查明】 无法复现或无线索。 原则:不把偶发问题直接定性成「代码故障/数据丢失/安全事件」。
一、事故与故障档案
事件 01 · 手绘气泡上线后中文乱码(10-14)【已证实】
- 表现:整页中文变乱码。
- 原因:
serveFrontend()用new Response(atob(B64)),atob返回「每字节一个 latin1 字符」的字符串,UTF-8 多字节被拆开。 - 修法:
atob→Uint8Array逐字节 →Response(bytes)。 - 结果:修复;手绘气泡功能回滚,未再尝试。
- 教训:base64 → 页面必须走字节数组,不能直接当字符串。
事件 02 · 发送键把点击事件当消息(10-17)【已证实】
- 表现:发送后出现
[object PointerEvent]文本。 - 原因:事件处理函数把
event当第一个参数传给sendMessage(text)。 - 修法:显式取
messageInput.value。 - 教训:
addEventListener('click', sendMessage)会把 Event 传进去。
事件 03 · iPhone 底部布局/键盘连续 6 轮返工(10-22)【已证实】
- 表现:新开页面不滿屏 / 输入栏悬空 230pt / 键盘弹出时不跟随 / 键盘收起后「钉死」 / 取消或发送后「抽搐」 / 不打字时下方空一大片。
- 根因(逐条):
1.
100vh在 iOS 不随工具栏收缩 → 用100dvh+visualViewport; 2. 整页position:fixed包裹 + 在回调里改 layout 高度 → 与键盘动画打架; 3.transition: bottom与 iOS 键盘自带动画叠加 + focusout 多次延迟补测 → 抽搐; 4. iOS 自身已从innerHeight排除工具栏,再按screen.height - vv.height补偿 → 双重补偿空白; 5. iOS 26 bug:键盘收起后visualViewport.offsetTop不归 0 → 输入栏钉死。 - 修法:只写 CSS 变量
--kb-inset;未聚焦即归零;删 transition;删双重补偿;ResizeObserver测输入栏高度。 - 用户反馈:「一个搜索能解决的你给我改了6版」「你不要再继续改那个代码了」。
- 教训:先搜业界标准方案再动手;有成功版本时优先回滚对照,而不是继续叠加补丁。
事件 04 · 部署产物与源码漂移(10-19 前)【已证实】
- 表现:线上后端与仓库
worker.js不是同一份代码(build.js只更新前端 base64,不同步后端)。 - 修法:
build.js改为「由worker.js整体重生成」,SHA256 前缀校验。 - 教训:产物必须可重建;不要手工改产物。
事件 05 · 国内版请求穿墙打 v2(10-25 凌晨)【已证实,我方引入】
- 表现:国内页面秒开,但聊天全部失败。
- 原因:我此前为 Pages 适配,把
CONFIG.WORKER_URL默认值写死成https://zhai-v2.beibbloveme.workers.dev;国内版页面沿用了这个默认值。 - 修法:默认值改
location.origin + location.search;server.js 注入时替换写死值;旧值迁移清理。 - 用户反馈:「吗的老子知道为什么了。你后端填v2的干嘛啊」。
- 教训:任何「默认地址」都必须同源,跨端复用同一份前端时尤其危险。
事件 06 · 国内版记录类接口全 403(10-25)【已证实】
- 表现:删除/归档/高光/记忆库全失败;提示「链接不完整」。
- 原因:平台网关要求所有同源请求带
?sharecode=…;相对路径fetch('/messages/delete')不带;WORKER_URL + '/proactive'把 query 污染成?sharecode=X/proactive。 - 修法:注入
FETCH_PATCH在 fetch 层统一补/修 sharecode;manifest href 补 sharecode;默认地址带location.search。 - 教训:托管平台的网关规则要当「运行时契约」对待。
事件 07 · 国内版人设丢失、AI 自认 DeepSeek(10-25)【已证实】
- 原因:早期
server.js自己实现了一份handleChat,只转发 message/key,system prompt 一行没有。 - 修法:改为「垫片」架构——
worker.js整体搬迁,零改动复用;此后人设/记忆/安全全部随主代码走。 - 教训:不要维护第二份业务逻辑。
事件 08 · v1 页面 401(令牌不匹配)(10-25)【已证实】
- 原因:
index.html第 10 行写死 v2 的令牌(给 Pages 用);worker.js设计了__CAT_TOKEN_VALUE__占位符替换,但页面里没有占位符 →split().join()是空操作 → v1 页面带着 v2 令牌打 v1。 - 修法:
pageResponse增加正则改写window.__CAT_TOKEN__ = "…",并校验令牌字符集。 - 教训:「静默失效的兼容分支」比直接报错更危险。
事件 09 · 删除后云端「复活」(10-24 前)【已证实】
- 原因:长按「删除单条」只删本地 IndexedDB,不调服务端;云同步/换设备后消息回来。
- 修法:
deleteOneOnServer()统一两条删除路径;先服务端成功再删本地。 - 教训:本地删除必须与服务端语义对齐。
事件 10 · 「云端删除」其实只是软删(10-25)【已证实】
- 原因:前端写死
mode:'soft',后端的 hard 分支从未被调用。 - 修法:前端改
mode:'hard';后端加墓碑与级联硬删。 - 遗留:历史 279 条软删记录仍在库里。
事件 11 · v1/v2 聊天全部 500(10-25 14:30–22:30)【已证实(主因)】
- 表现:两个入口都返回「发送失败:服务端处理失败,请稍后再试」。
- 排查过程:
1. 本地用同款产物 + 真实数据副本复现 → 均 200,排除代码路径;
2. 沙箱无法直连
workers.dev,wrangler tail也超时; 3. 改用 Cloudflare REST API 实测:两个 KV 库keys端点同时报code 10048(今日免费配额已用完),values读正常; 4. 官方文档确认:免费版 1000 list/天,账户级,超额后该操作直接报错; 5. 时间线吻合:10-24 14:42 v1 已有失败 → 10-25 08:00 配额重置 → 14:30 v2 正常 → 之后密集测试+用户重试烧穿。 - 结论:
list()抛未捕获异常 →toAppError兜底成 500。已证实(配额耗尽本身有 API 直接证据;「第一次 500 的确切异常文本」未取到,属证据链最后一环缺失)。 - 修法:list 前缀缓存(5 分钟)+ 增写失效 +
kv_list_failed503 人话 + 聊天检索/归档双降级 +notice前端提示 +meta_lastTs写去重。 - 教训:免费额度是架构约束,不是运维细节;任何按前缀全量 list 的设计都要先算配额。
事件 12 · 国内版「搬迁后不能用了」(10-25 12:58)【部分查明】
- 用户报告:迁移后国内版出现与 v1/v2 相同的症状(用不了)。
- 当时证据:日志有
chat_error missing_api_key(400)与一次forbidden_origin(403)。 - 结论:至少两条独立原因(未配 Key;同源基准不匹配),与后来的 500 配额问题不是同一个原因。用户当时的 Key 已吊销,也会造成 401。未完全闭环(没有当时的完整日志)。
- 教训:不要把所有「用不了」归成一个原因;先看错误码。
事件 13 · pgrep -f 误杀自己(10-19)【已证实】
- 原因:
pgrep -f "server\.js"匹配到执行命令的 shell 自身(命令行里含node server.js)。 - 修法:按显式 PID kill;改看门狗脚本 + pidfile。
- 教训:不要用模糊匹配杀进程。
事件 14 · 沙箱访问不了 workers.dev(长期)【环境限制】
- 表现:
curl返回 000、wrangler tailWebSocket 连不上、WebFetch失败。 - 结论:网络限制,不是部署失败。线上验证只能靠 Cloudflare API 拉 bundle / 看版本。
- 教训:不要把「沙箱看不到」当成「线上坏了」。
二、当前风险登记(按严重度)
完整版与修复方案见
/workspace/审查报告-三端代码与风险-20261025.md;此处只列结论。
P0(致命,必须马上处理)
| ID | 风险 | 后果 |
|---|---|---|
| P0-1 | 前端「聊天」走 CONFIG.WORKER_URL,「记忆/档案/删除」走相对路径 |
一个设置就能让读写分裂到两个后端;「删了 AI 还记得」 |
| P0-2 | 硬删只删 msg:,不删 mem:(检索读的正是 mem:) |
「彻底删除」名不副实,被删内容仍进提示词 |
| P0-3 | 页面公开 + 令牌随页面下发 | 拿到 URL 即可读走/删光全部记忆 |
P1(严重)
| ID | 风险 |
|---|---|
| P1-1 | v1 的 DEEPSEEK_API_KEY 代码不读;v2 无模型 Key → 换设备首聊必失败 |
| P1-2 | 检索只扫「最旧 1000 个」mem 键,超 1000 后新记忆静默失效 |
| P1-3 | 国内版数据与快照全在 /tmp;同源基准写死,换域名全 403 |
| P1-4 | 备份口径三端不一致(v1/v2 只能导本机,云端档案只有国内版能导) |
| P1-5 | 跨源响应缺 Access-Control-Allow-Origin(json() 没带) |
| P1-6 | 图片不入云,换设备即永久丢失,备份也不含 |
| P1-7 | 首次云同步 O(n) 写,1000 条一次打满日配额 |
P2/P3(摘要)
- 产物双轨与手工同步副本(P2-1/P2-2)
- 限流每 POST 两次写、失败静默失效(P2-3)
ALLOW_CUSTOM_API=0与前端「自定义」冲突(P2-4)validateApiUrl丢 query(P2-5)- 主动消息不写
msg:(P2-6) - 三套导入导出并存(P2-7)
/init空实现、/health版本号写死(P2-8)- 回滚先
clearAll再导入(P2-9) - 平台 sharecode 写死、PWA 图标不带 sharecode(P2-10)
- v2 令牌明文在
index.html(P2-11) - 重导出接口无限流(P2-12)
worker.js依赖构建期注入的serveFrontend(P2-13)- 双写冗余、旧键多一次 list、无 CI、16px/键盘常量散落(P3)
三、不要重复的操作(血泪清单)
- 不要手工编辑
cat-chat-all-in-one.js;一律改worker.js/index.html后node build.js。 - 不要跳过 build 直接
wrangler deploy --config(会部署旧产物)。 - 不要在没备份时
clearAll()/ 清 KV / 清 IndexedDB。 - 不要用
pkill -f/pgrep -f匹配server.js(会杀自己)。 - 不要一次硬删超过 200 条(写配额 1000/天;还要留余量给聊天)。
- 不要在配额耗尽时反复重试(每次重试都在烧 list/写;新代码已降级,但旧页面会继续打)。
- 不要用「沙箱打不开 workers.dev」判断线上故障。
- 不要把「v2 空库」当成故障(那是按用户要求新建的)。
- 不要在页面里写死后端地址或令牌(v1 401 与国内版穿墙都源于此)。
- 不要维护第二份业务逻辑(国内版早期
handleChat副本导致人设丢失)。 - 不要相信「UI 有按钮 = 功能通」(云端删除、全量导入都曾如此)。
- 不要只跑 Mock 测试就宣称线上可用。
四、仍然未闭环的三个问题
| # | 问题 | 缺什么 | 怎么补 |
|---|---|---|---|
| 1 | 10-25 第一次 500 的确切异常文本 | 线上日志(wrangler tail 不可用) |
下次故障时让用户截图 rid,或接 CF Logpush |
| 2 | v1 库在 10-24 之后被谁/何时填充 | 无部署/同步日志 | 看 v1 meta_lastTs 时间线 + 用户回忆;或等配额恢复后按 created_at 抽样 |
| 3 | 10-25 12:58 国内版「用不了」的完整原因 | 当时的服务日志已滚动 | 保留 /tmp/cat-chat-server.log;下次复现立即抓 |
本页由交接包内的 Markdown 原样转换;内容未改动。源码与完整包请回目录页下载。