审查报告

猫爪聊天 · 项目状态与风险审查报告

审查性质:只读审查(未修改任何一行代码、未改任何配置、未部署) 审查时间:2026-10-25 23:30(北京时间) 审查范围:v1(worker zhai)、v2(worker zhai-v2)、国内版(Node + SQLite)三套运行的完整代码与线上状态 审查方法:全量通读 worker.js(2801 行)、index.html(12509 行,重点逻辑区)、deploy-web/server.js、deploy-web/kv-sqlite.js、build.js、两份 wrangler 配置、start-domestic.sh、10 个测试文件;用 Cloudflare API 拉取线上真实 bundle 逐字节比对;只读方式查询国内版 SQLite 库;回看聊天记录核对需求演变


0. 先更正我此前两个错误结论

你提醒得对,我此前有两处判断是错的,先把账认了:

我此前的说法 事实 说明
「v2 是空壳,昨天的搬迁没落到 v2 KV」 v2 空是设计如此。10-24 深夜你让我重新部署一个完全全新的环境(worker zhai-v2 + 新库 chat_memory_v3),它本来就该是空库。 我把「按你要求新建的空环境」误判成「搬迁失败」。606 条从来不是要搬进 v2 KV,而是搬进国内版(SQLite)。搬迁工具 tools/migrate_v2_to_domestic.mjs 第 45 行同时读了 0a4c…(v3) 和 ec1a…(v2/v1 数据) 两个库,所以国内版拿到的是两库合并——这也是你说「你聊天库搬错了有是 v1 的」的原因:v1 的数据被一起搬进了国内版。
「前后端是一套」 前端与后端确实是两份独立真源,且存在口径不一致(详见 P0-1、P1-4、P1-5)。 前端 index.html 是唯一前端真源,后端 worker.js 是唯一后端真源;cat-chat-all-in-one.js = 后端 + base64(前端),是构建产物。但前端里「聊天」走 CONFIG.WORKER_URL,而「记忆库/归档/删除」走相对路径(永远打当前页面所在后端)——两套寻址规则并存。

1. 整体概览

1.1 三个运行版本对照

项目 v1 v2 国内版
入口 https://zhai.beibbloveme.workers.dev https://zhai-v2.beibbloveme.workers.dev https://a0cd1755f00a22a37.app.workbuddy.host
运行时 Cloudflare Worker Cloudflare Worker Node 22(server.js)+ 内置 SQLite
部署标识 版本 a3ef2a33(22:54 部署)etag b22ea7c0… 版本 8c6fe98a(20:38 部署)etag b22ea7c0… 进程 22:26 重启,已加载新代码
存储绑定 KV ec1a…(chat_memory_v2) KV 0a4c…(chat_memory_v3,全新空库) /tmp/cat-chat-data/chat.db(SQLite,652 键)
访问令牌 无 AUTH_TOKEN secret → 用代码内默认值 518effb1… AUTH_TOKEN secret(页面自动注入) 代码内固定 cat-web-token
模型 Key secret 名是 DEEPSEEK_API_KEY,代码只读 env.API_KEY,永远读不到(P1-1) 无任何模型 Key secret 无(ALLOW_CUSTOM_API=1,靠前端自填)
代码版本 与 bundle 一致,含配额修复 同左(etag 与 v1 相同) worker.mjs 与 bundle 逐字节相同,含配额修复
数据状态 真实历史主库(约 307 条归档 + ~300 条记忆碎片) 基本为空(10-25 傍晚使用过少量) v1+v2 合并导入 307 条 + 今天新聊 13 条 + 312 条记忆
与前端关系 页面由 worker 自己吐出(内嵌) 同左 页面由 server.js 吐出,读仓库里的 index.html
当前可用性 可聊(KV 配额耗尽 → 降级模式 + 提示) 可聊(同左,但库空) 完全可用(SQLite 无配额)

1.2 数据到底存在哪里

四份互不同步的数据副本:

# 位置 内容 性质
① v1 KV ec1a… 归档消息 msg:owner:*(约 307 条,其中 279 条带软删标记)、记忆碎片 mem:owner:*(约 300 条)、高光/经历/关系少量 云端主档(历史)
② v2 KV 0a4c… 全新空库,仅 18:56 前后少量新数据 云端新档(按你要求重建)
③ 国内版 SQLite msg 320 / mem 312 / hl 2 / exp 1 / meta 2 / rl 14 / tomb 0 国内直连主档
④ 每台浏览器的 IndexedDB 全量聊天记录 + 图片 base64(图片不入云) 本机主副本

关键事实(只读查询得出):

1.3 代码构成与构建链

index.html  (前端唯一真源, 12509 行, 230,966 字节)
     │
     ├── build.js ──► cat-chat-all-in-one.js  (= worker.js + base64(index.html) + serveFrontend)
     │                        │
     │                        ├── wrangler.toml      → worker zhai-v2  (KV 0a4c)
     │                        └── wrangler.v1.toml   → worker zhai     (KV ec1a)
     │
     └── deploy-web/server.js 直接 fs.readFile('../index.html') 注入令牌后吐页面
                │
                └── deploy-web/worker.mjs(= cat-chat-all-in-one.js 的副本,逐字节相同)
                          └── kv-sqlite.js(KV 接口的 SQLite 实现)

三处「双份/多份」是本次审查的重点来源:清单文件双份(index.html 与 bundle)、后端双份(worker.js 与 worker.mjs)、三套部署三份数据。

1.4 进程与变更时间线(按你我的聊天记录 + git 提交核准)

时间 事件
10-05 ~ 10-14 初版:Pages 前端 + worker zhai + KV chat_memory_v2(即现在的「v1」数据)
10-19 ~ 10-22 UI 重构、记忆系统(高光/经历/关系)、P0/P1 安全加固
10-23 22 点 v1 上批量「云端删除」68 条(当时只有软删,记录仍在库)
10-24 13 点 再删 211 条(同上,仍物理存在)
10-24 13:30 改为单文件构建(build.js),前端/后端分离成两个真源
10-24 晚 国内版 C 方案上线(server.js + SQLite)
10-24 深夜 按你要求新建全新环境:worker zhai-v2 + KV chat_memory_v3(空库)
10-25 06:34 ~ 09:01 全量档案补 mem:*、搬迁工具、606 条导入国内版(两库合并)
10-25 13:33 前端默认后端改同源(旧默认写死 v2 地址导致国内版把请求打穿墙)
10-25 13:52 桥接基准改用可信公网地址,修国内版 403
10-25 15:31 worker 输出页面按本部署令牌改写,修 v1 401
10-25 14:30 ~ 18:00 v1/v2 全量 500 排查 → 定位 KV 免费版每日 1000 次 list 配额
10-25 18:48 「彻底删除」改物理删除 + 墓碑防复活(先 v2、后 v1)
10-25 20:36 配额修复提交:list 前缀缓存 + 降级 + 干净 503
10-25 20:38 v2 部署 8c6fe98a
10-25 22:26 国内版进程被重启(node server.js 启动于 22:26,晚于 20:29 的代码更新)→ 已加载新代码(我此前说「国内版未重启」已过时;线上页面已确认含 data.notice 逻辑、注入 cat-web-token)
10-25 22:54 v1 部署 a3ef2a33(修复到你正在用的入口)

2 + 3. 问题清单与风险分级

分级标准:P0 致命(会导致数据错乱/丢失/越权,必须马上改)、P1 严重(功能不符合承诺或会踩坑)、P2 一般(边界、体验、配置)、P3 优化建议。 「位置」均为当前工作区文件的行号(行号会随修改漂移,请以函数名为准)。

P0(3 项)

P0-1 「Worker 地址」只对聊天生效,记忆/归档/删除永远打当前页面后端 → 一个设置就能把数据劈成两半

P0-2 「彻底删除」不彻底:只删 msg:/hl:/exp:/rel:,不删 mem:<profile>:* —— 而聊天检索读的正是 mem:

P0-3 任何人拿到三端任一 URL,即获得全部数据权限(页面公开 + 令牌随页面下发)

P1(7 项)

P1-1 v1 的模型 Key secret 名字不对,代码永远读不到;v2 完全没有模型 Key

P1-2 记忆检索只扫「最旧的 1000 个」key —— 超过 1000 条后新记忆永远检索不到

P1-3 国内版数据全在 /tmp,且同源基准写死 —— 容器重建即全丢;域名一变全部 POST 403

P1-4 备份/恢复能力三端不一致:v1/v2 只能「导出本机」,云端全量档案只有国内版能导入

P1-5 跨源响应缺少 Access-Control-Allow-Origin → 任何「页面在 A、后端在 B」的组合都会发送失败

P1-6 图片不入云:只有 [图片] 占位,图只存在发送它的那台浏览器

P1-7 首次「云端同步 / 恢复」是 O(n) 写入,1000 条就能一次打满免费版日配额

P2(13 项)

ID 问题 位置 影响
P2-1 部署产物与真源双轨:cat-chat-all-in-one.js 是生成物却直接部署;wrangler deploy --config 不会自动 build,忘了 build 就部署旧代码 wrangler*.toml main、deploy.sh 线上/仓库漂移(今晚 v1 能上修复纯因为我先 build 过)
P2-2 deploy-web/worker.mjs 是手工同步的副本(当前与 bundle 逐字节相同),无任何机制保证下次还一致 deploy-web/worker.mjs 国内版可能悄悄跑旧后端
P2-3 写入放大:每个 POST 先做 2 次限流写(rl_<ip> + rl_global),一次聊天约 6 写;限流写失败被 catch 吞掉 → 配额耗尽时限流静默失效 worker.js:1365-1399 日上限约 160 条聊天;配额耗尽后失去防刷
P2-4 ALLOW_CUSTOM_API="0" + 前端有「自定义」服务商:用户填非白名单域名必 403,提示却让去改 toml 两份 wrangler [vars];worker.js:1455-1463 自选模型名不副实
P2-5 validateApiUrl 返回 u.origin + u.pathname,丢掉 query worker.js:1465 带查询参数的接口(部分 Azure/代理网关)不可用
P2-6 主动消息只写 mem:,不写 msg: 归档 worker.js:2789-2792 /export/full 的 messages 缺主动消息;换设备恢复后聊天记录少几条
P2-7 三套导入导出接口并存:/backup(旧) / /archive(中) / /export/full(新),语义重叠 worker.js:1534/1648/1839 维护成本、口径混乱
P2-8 /init 是空实现;/health 版本号写死 2.0 worker.js:51-54, 1602-1609 三端无法区分「跑的是哪个版本」
P2-9 国内版回滚先 kv.clearAll() 再导入,导入失败即空库(靠 pre_rollback 快照兜底,而快照也在 /tmp) server.js:290-315 一次失败的回滚 = 全库清零
P2-10 平台 sharecode 写死在代码里;manifest 的图标与 start_url 不带 sharecode server.js:46、104-106;worker.js:1184-1206 国内版「添加到主屏幕」可能 403/白屏
P2-11 v2 的 AUTH_TOKEN 明文写在 index.html 第 10 行,已进 git 历史 index.html:10 令牌事实上公开
P2-12 /export/full、/archive 无速率限制(只校验令牌) worker.js:1611-1621 令牌一旦泄露可被刷爆配额
P2-13 worker.js 依赖构建期注入的 serveFrontend(源码里未定义);直接 deploy worker.js 会在 GET / 时 ReferenceError worker.js:1505 绕开 build 就炸

P3(11 项)

ID 问题 说明
P3-1 mem: 与 msg: 双写冗余 同一句话存两份,KV 键数与写入次数翻倍;也是 P0-2 的根因。中长期应合并为一份
P3-2 旧 mem_ 兼容读不走缓存 owner 每条聊天多 1 次 list(已刻意如此保证迁移可见,代价可控)
P3-3 /backup、/init 可废弃 前端未使用,属历史包袱
P3-4 ALLOWED_ORIGINS 填的是 worker 自己的 origin 无意义(同源本来就放行),建议留空或填真实跨源前端
P3-5 deploy-web/package.json 描述过时 写着「无 KV 记录,仅聊天 API 代理」,实际已有完整 KV
P3-6 worker.mjs 内嵌 300KB FRONTEND_B64 Node 端用不到(页面由 server.js 读文件),属无效载荷
P3-7 无 CI / 无 lint;测试未覆盖 CORS、令牌注入、WORKER_URL 覆盖 10 个测试文件全是「内存 Mock KV + Mock 模型」的契约测试,覆盖后端逻辑,不覆盖三端联调
P3-8 限流常量与真实配额未联动 RATE_DAY=400 与 KV 1000 写/天没有换算关系
P3-9 重试路径不处理 notice retryMessage 未读 data.notice,降级提示只在首次发送出现
P3-10 resolveTime 信任客户端 time.local 并直接拼进提示词 理论可注入提示词(仅自伤),建议清洗
P3-11 重试可能写入重复 mem 碎片;AI 归档时间戳复用请求时间 幂等只覆盖 msg:(worker.js:2586-2611),mem: 随机键无幂等;ai 记录用 time.ts(2702-2710),长回复时排序会抖

4. 修复方案(只给方案,不动手)

P0-1 统一「后端基准地址」

方案 A(推荐,一次性根治) 1. 前端新增 function apiBase():返回 CONFIG.WORKER_URL(去掉尾部 /)。 2. 把所有相对路径调用改为 apiBase() + '/xxx':/messages/delete、/archive、/export/full、/import/full、/highlights、/experiences、/relations、/proactive。 3. 设置页在「Worker 地址」变更时弹确认:「切换后端后,聊天、记忆库、云端存档将全部指向新地址(不同地址的数据互不可见)」。 4. 若判定「多后端」不是真实需求,更简单:删掉设置里的 Worker 地址输入,永远同源(三端各自自洽),从根上消除误配。

方案 B(最小改动):保留相对路径,但把设置页的 Worker 地址输入只读化/隐藏,并在页面加载时校验 CONFIG.WORKER_URL 与 location.origin 不一致时强制回退同源 + 提示。

验收:在 v2 页面把地址改成 v1 → 记忆库请求路径变为 v1(Network 面板可见),或按方案 B 直接被拒绝。

P0-2 让「彻底删除」真的彻底

方案 A(推荐,中期重构):把检索数据源统一到 msg:* 1. retrieveMemory 改为读 msg: 归档(已有 deleted_at 过滤),不再读 mem:*。 2. saveMemory 停止写入(或仅保留一段时间双写做回滚)。 3. 删除路径自然覆盖(msg: 删了就检索不到),不再需要扫 mem。 4. 迁移:把现有 mem:* 的内容按 content+ts 与 msg:* 对齐后清理(需 list,等配额恢复后分批做)。

方案 B(短期止血): 1. saveMemory 记录里增加 mid 字段(用户消息传 userMid,AI 消息传 aiMid)。 2. 硬删时:从最新往旧扫 mem: 键(最多 N 条),找到 mid 命中即 delete + 留墓碑;扫不到就记录告警。 3. 代价:每次删除多 1 次 list + 若干读写,且随库增长变慢。

验收:删一条消息后,/export/full 的 memories 与 messages 都不再包含该内容;再聊天时提示词里不出现。

P0-3 收敛访问权限(三选一,按你的使用场景定)

P1-1 修 Key

  1. v1:wrangler secret put API_KEY --config wrangler.v1.toml(值 = 你的 DeepSeek Key),或把代码改成 opts.apiKey || env.API_KEY || env.DEEPSEEK_API_KEY(更稳,兼容现状)。
  2. v2:同样加 API_KEY。
  3. 前端:当 CONFIG.API_KEY 为空时,在输入框上方提示「未填 Key,将使用服务端默认 Key(若已配置)」。

P1-2 修检索「只看得到最旧 1000 条」

方案(推荐):按月分片 1. 写入键改为 mem:<profile>:<YYYYMM>:<ts>_<rand>。 2. 读取时从当前月开始往前取最多 3 个月(每月 ≤200 键,天然分页),合并后按时间倒排取最近 300。 3. 兼容:旧 mem:<p>:<ts>_... 仍按原前缀读一次(只在没有新分片数据时)。 4. 备选:维护一个 mem_recent:<p> 索引键(存最近 300 个键名,写入时更新,读时一次 get)——写放大 +1,但读取 O(1)。

P1-3 国内版持久化与同源

  1. 数据目录改到平台持久卷(若有);若无,增加「每日把快照 base64 POST 到 v2 的 /import/full」做异地副本(注意配额,可每周一次)。
  2. PUBLIC_ORIGIN 改为:优先环境变量 → 其次读平台注入的公开域名 → 兜底现值;isSameOrigin 增加「同 host 的任意路径/端口」放行。
  3. 403 时返回中文可读页:「当前访问域名与后端配置不一致,请用 xxx 访问」,而不是让前端显示「发送失败」。

P1-4 统一备份口径

  1. 去掉 exportBackup 的 isWebMode() 限制:v1/v2 也调 /export/full(失败再退回本机导出)。
  2. 去掉 restoreCloudFullFile 的 isWebMode() 限制(/import/full 三端都支持)。
  3. schema:2 文件在三端行为一致。

P1-5 修 CORS

json(data, status, request, env) 增加后两个参数,内部 Object.assign(headers, corsHeaders(request, env));或在路由出口统一包一层。注意:corsHeaders 只在 Origin 合法时回 ACAO,避免变成开放代理。

P1-6 图片

短期:UI 明确标注「图片只保存在本机,换设备不会同步」;备份说明里写清。 长期:接 R2(或对象存储),msg.attachment 存对象 key,渲染时按需取;KV 只存元数据。

P1-7 分批同步

cloudSyncToServer 改为:本地记录 lastSyncTs → 只上传之后的;每批 50 条,逐批 POST,失败可续传。

P2 / P3 速查

ID 修法
P2-1 deploy.sh 已含 build;给 v1 也加一个 deploy-v1.sh(build → deploy --config),并把「禁止直接 wrangler deploy」写进 README
P2-2 构建时顺手 cp cat-chat-all-in-one.js deploy-web/worker.mjs(build.js 加一行),消除手工同步
P2-3 限流改成「一个键同时记分钟/天」(现在已是 1 键 2 写→可合并为 1 写);或把限流计数放模块内存(isolate 级,允许超发)以减少 KV 写
P2-4 要么 ALLOW_CUSTOM_API=1,要么前端「自定义」时校验域名并在 UI 提示白名单
P2-5 return u.origin + u.pathname + (u.search || '')
P2-6 handleProactive 里补 saveArchive(aiMid, ...),与聊天同款
P2-7 保留 /export/full + /import/full,把 /backup、/archive 标 deprecated,前端统一
P2-8 /health 返回构建时间戳(build.js 注入 BUILD_ID)
P2-9 回滚改为「导入到临时库 → 校验 counts → 原子替换文件」,失败不 clearAll
P2-10 manifest 动态生成时带上当前 query(sharecode);start_url 同样处理
P2-11 轮换 v2 token,页面改成占位符;Pages 若已废弃则直接删掉硬编码
P2-12 给 /export/full、/archive 加同款 checkRate
P2-13 build.js 里给 worker.js 加一行 function serveFrontend(){...} 的桩,或直接禁止部署 worker.js
P3-1~11 见上表,随主修复顺带处理

5. 建议的执行顺序(等你决定,我不会擅自动手)

第一批(止血,1 次部署,30 分钟内) 1. P1-5 CORS(1 处函数签名) 2. P1-1 Key 读取兼容(1 行)+ 两个 worker 加 secret 3. P2-5 query 丢失(1 行) 4. P2-13 serveFrontend 桩(防误部署炸)

第二批(数据正确性,需要设计确认) 5. P0-1 统一后端基准(决定「保留多后端」还是「锁死同源」) 6. P0-2 删除彻底化(推荐方案 A:检索改读 msg:) 7. P1-2 检索分片(与 6 一起改,避免两次动检索)

第三批(部署与数据面) 8. P1-3 国内版持久化 + 同源兜底 9. P1-4 备份口径统一;P1-6 图片策略;P1-7 分批同步 10. 历史 279 条软删数据的清理(硬删 + 墓碑,必须分批:每批 ≤200 条,否则写配额当天就满)

第四批(安全) 11. P0-3 访问控制(建议 Cloudflare Access,最快)

配额相关注意:KV 免费版 1000 list / 1000 write / 10 万 read 每天(账户级,北京时间早 8 点重置)。任何「批量清理 / 全量搬迁 / 首次同步」都要按 ≤200 条/批、跨天做,否则第二天照样把配额打满。


6. 本次审查的边界(做不到的部分,如实说明)

项 原因
无法枚举 v1/v2 的精确键数与总量 KV 免费版 list 配额已耗尽(code 10048),REST keys 端点同样被拒;只能靠已知样本与国内版导入结果推断(307 条归档来自 v1)
无法查看线上运行时日志 沙箱到 workers.dev 与 CF 日志端点均超时,wrangler tail 不可用
无法读取 secret 明文 CF API 不返回 secret 值;只能确认名称
无法在沙箱内直连 v1/v2 做端到端冒烟 workers.dev 被网络阻断;已改用「CF API 拉取线上 bundle + 逐字节/语义比对」替代(结果:v1 前后端修复代码与本地完全一致)
未验证真实设备上的表现 只读审查,未触发任何写操作

7. 一句话总结

三端代码现在是同一份修复版、都能跑;真正的风险不在「能不能用」,而在架构层面的三处不一致:①前端一半接口认「设置的地址」、一半认「当前页面」(P0-1);②消息存两份、删除只删一份(P0-2);③三端各有一份互不同步的数据,图片只在本机(P1-3/4/6)。这三件事不解决,后面每一次「修好了」都会在换端/换设备/换域名时重新裂开。

本页由交接包内的 Markdown 原样转换;内容未改动。源码与完整包请回目录页下载。