README · 交接索引
README · 小手机(猫爪聊天)交接索引
生成时间:2026-10-27 00:45(北京时间) 生成方式:只读整理。未修改任何代码、未部署、未写入/迁移/删除任何生产数据。 适用对象:接手本项目的新 AI / 开发者。
1. 这份档案是什么
把 2026-10-05 ~ 10-27 的全部可访问历史(对话摘要、任务书、源码、提交、测试、部署、数据面)整理成 10 个文件,让接手者尽量不需要再问用户「以前做过什么」。
它不是:项目宣传稿、完整源码包、或对未来的承诺。它是「现状 + 历史 + 坑 + 待办」的底账。
2. 阅读顺序
| 顺序 | 文件 | 什么时候读 |
|---|---|---|
| 1 | README_交接索引.md(本文) |
先读,知道资料边界 |
| 2 | 01_项目总览与长期目标.md |
建立全貌:是什么、要变成什么 |
| 3 | 04_技术架构与当前代码基线.md |
知道代码怎么跑、数据在哪 |
| 4 | 07_问题风险与失败经验.md |
动手前必读:哪里会炸、哪些事别重做 |
| 5 | 03_任务清单与真实进度.md |
知道做到哪、哪些只是雏形 |
| 6 | 05_长期记忆与收藏系统历史档案.md |
要动记忆/删除/检索时必读 |
| 7 | 06_决策记录与版本变化.md |
想知道「为什么是这样」时查 |
| 8 | 02_历史需求与设计构想.md |
要还原原始需求、找「提过但没做」的东西 |
| 9 | 09_缺失资料与待确认事项.md |
准备问用户之前先看,避免重复问 |
| 10 | 08_下一阶段执行路线.md |
决定要开工时按它排期 |
3. 文件清单与作用
| 文件 | 内容 | 关键结论 |
|---|---|---|
01_项目总览与长期目标.md |
项目是什么、最初需求、12 项长期目标、三端入口、状态快照 | 三端跑同一份修复版;数据四份副本 |
02_历史需求与设计构想.md |
按主题整理的全部需求(UI/安全/任务书/记忆/托管)+ 禁止事项 + 提过没做的 | 阶段 5–9 全未开始 |
03_任务清单与真实进度.md |
30+ 任务的状态、证据、缺口 | 只有 Mock 测试,无生产 E2E |
04_技术架构与当前代码基线.md |
仓库/文件/构建链/路由/键位/三端配置/数据量/测试/成本 | v1 = 历史主库;v2 = 全新空库 |
05_长期记忆与收藏系统历史档案.md |
记忆五代演进、当前实现、设计差距、必须保留的约定 | 双写 mem:+msg: 是最大冗余 |
06_决策记录与版本变化.md |
20 条决策(区分用户确认/AI 建议)+ 版本时间线 + 冲突 | v2 空库是设计,不是故障 |
07_问题风险与失败经验.md |
14 个事故档案(含根因与教训)+ P0–P3 风险 + 别重做的清单 | P0 三项:寻址分裂、删不彻底、令牌公开 |
08_下一阶段执行路线.md |
阶段 0/A–F 的依赖排序、验收、回滚、审批 | 先止血,再修数据,最后做阶段 5–9 |
09_缺失资料与待确认事项.md |
资料三分类、冲突、要用户补的材料、7 个待确认决定 | 先定「主数据源」与「279 条删不删」 |
4. 资料来源与核实等级
4.1 实际读取过
- 本会话全部对话 + 30 个阶段压缩摘要(覆盖 10-05 ~ 10-25)
/root/uploads/的任务书 v1.0(27KB)与原始前端v6.html(56KB)worker.js全文、index.html全部逻辑区、deploy-web/*全文、build.js、两份 wrangler 配置tests/10 个文件(说明与断言数)、tools/关键行小手机-完整交付文档.md、任务交接说明.md、模块索引.md、安全说明.md、修复记录-P0-P1.md、国内版落库迁移方案.md、小手机-全新后端环境说明.md- 国内版 SQLite(只读查询)、v1/v2 KV(抽样读)、Cloudflare API(worker/secret/bundle)、国内版
/与/health
4.2 知道存在但未读全
猫爪聊天-完整项目.md(304KB)、handoff-public.md / 小手机-交接-公开版.md(40KB×2)、替换指南.md、给其他AI-代码包/、小手机-审计草案/01–04、17 张截图、小手机交接/index.html。
4.3 无法访问
GitHub 远端实时状态、Cloudflare 线上日志、KV 精确键列表(配额耗尽)、secret 明文、用户浏览器数据、平台控制台、10-22 之前的逐字对话(仅存摘要)、部分已过期的截图链接。
4.4 证据标注
【源码】读代码可得【测试】本地跑过(Mock,不等于线上)【部署】线上版本已确认【实测】我亲自执行并取得结果【线上核验】通过 Cloudflare API / 线上页面核对【推断】有间接证据但未闭环【未验证】无证据【待确认】需用户拍板
5. 三句话结论
- 代码是好的,架构有三处硬伤:前后端两套寻址规则(聊天走设置、记忆走同源)、消息存两份但只删一份、页面公开下发令牌。
- 数据是散的:v1 KV(历史主库,含 279 条软删)、v2 KV(全新空库)、国内版 SQLite(307 条导入+13 条新聊)、每台浏览器 IndexedDB(图只在这),四份互不同步。
- 长期目标只完成了前四阶段:阶段 5 人设版本、6 成长分析、7 后台、8 沈屿迁移、9 清理,全部未开始。
6. 交接时的最小动作清单
接手者第一小时建议只做这些(全部只读):
cd /workspace/chat
git log --oneline -20
git status
ls -la
wc -l worker.js index.html build.js deploy-web/server.js deploy-web/kv-sqlite.js
然后:
- 读 01 → 04 → 07;
- 用 Cloudflare API 核对 v1/v2 线上版本(不要
wrangler deploy); - 用只读方式查国内版
GET /health; - 把
09里的三个问题问用户; - 得到答复后再看
08,不要直接开工。
7. 红线(写在这里,防止新 AI 一上来就踩)
- 不改用户没点名的区域;不重构、不改 UI 风格。
- 不手工编辑
cat-chat-all-in-one.js;改完worker.js/index.html必须node build.js。 - 不部署、不清库、不改密钥,除非用户明确说「可以」。
- 不批量删除;任何清理 ≤200 条/批、先导出、跨天做。
- 不把「Mock 测试通过」说成「线上可用」。
- 不把 AI 的建议写成「用户已同意」。
- 不在文档里写任何真实密钥/令牌/密码。
01 · 项目总览与长期目标
01 · 项目总览与长期目标
档案版本:2026-10-27 | 证据等级见 README | 本文不含任何密钥、令牌或密码
1. 一句话
「小手机」= 猫爪聊天:一个单页、仿微信、手机优先的 AI 伴侣聊天 Web App。前端是唯一真源 index.html,后端是唯一真源 worker.js,构建时内联成单文件 cat-chat-all-in-one.js 部署到 Cloudflare Workers;另一套 Node 垫片(deploy-web/)把同一个 worker.js 跑在国内托管平台上,用 SQLite 代替 KV。数据按「角色(profile)」隔离,记忆分「长期记忆碎片 / 结构化档案 / 高光原话 / 经历 / 关联关系」五类,模型 Key 由用户在前端自填。
2. 项目起因与使用场景
- 起因:用户已有一个自写的聊天前端(上传文件
0643203b2611b966_v6.html,56,757 字节,仍在/root/uploads/),要求在不重构、不改布局的前提下,把它升级成「有真实后端、有长期记忆、能主动搭话」的 AI 伴侣。 - 使用场景:手机浏览器 / 添加到主屏幕(PWA),日常一对一聊天;用户是唯一使用者,默认角色
owner。 - 真实角色:长期目标是接入角色「沈屿」的本体(历史聊天 + 人设 + 记忆迁移),当前阶段属于测试角色验证期(任务书原文要求:测试稳定后再删测试后端与测试数据,再迁移沈屿本体)。
3. 最初的需求(2026-10-05,原文要点)
用户第一条任务书写明「绝对不准重构我的代码结构,必须保留现有布局」,并要求一次性完成五组功能:
| 组 | 要求 |
|---|---|
| 一、前端 UI | ①果冻气泡(backdrop-filter: blur(8px)、半透明白底、微高光边缘、圆角 16px、最大宽 75%)②圆形头像、必须可自定义上传,不写死默认图 ③猫爪印面板:点开先弹「颜文字 / 表情包」二级选项,颜文字点击填入输入框,表情包支持本地上传转 Base64 与粘贴 URL,右侧麦克风占位 ④长按菜单:复制 / 引用回复 / 删除单条 / 收藏,引用时输入框上方显示原文 ⑤消息状态:发送中 / 成功 / 失败可重试 ⑥动态状态胶囊:按后端 ai_status 渲染,没有就不显示(不留空白框) |
| 二、数据与请求 | ①localStorage 换 IndexedDB ②删掉 setTimeout 假回复,改真实 fetch,报错显示在气泡里 |
| 三、后端与记忆(核心) | ①外接 Cloudflare D1 或 KV,用户输入与 AI 回复原封不动入库并带时间戳 ②每次发消息检索最相关历史原话 + 人设打包给模型,预留 Worker 地址 ③请求体预留 tools: [] |
| 四、高级功能 | ①时间感知:前端带系统时间,后端注入提示词 ②主动消息:按时间间隔触发,每天 2–3 次 |
| 五、绝对禁止 | ①不准一次性给几百行代码,要拆成几步教用户在手机上替换 ②不准重构结构、不准改 UI 风格 |
当时通过提问确认的三个方案(用户已确认): 1. 后端 Worker 统一转发(Key 不进前端直连); 2. 检索先用关键词匹配(不上向量库); 3. 主动消息由页面打开时检查(不用 Cron)。
4. 长期目标(任务书 v1.0,12 项能力)
来源:
/root/uploads/1792388616892580314-文本.txt(《小手机项目|完整系统升级与安全改造任务书 v1.0》,27KB)
- 完整保存所有聊天原文
- 可靠的本地与云端同步机制
- 独立的高光原话库
- 结构化的经历记忆
- 可追溯来源的记忆时间线与关联关系
- 可搜索、可导出、可恢复的完整聊天档案
- 分层人设系统与人设版本管理
- 定期批量运行的人设成长分析
- 带证据、反例、审核、回滚的成长建议流程
- 统一的后台管理系统
- 沈屿本体的历史聊天 / 人设 / 记忆迁移
- 控制 Token 与 Cloudflare 成本
核心原则(原文):保留现有可用能力,优先修复真问题,再逐步加功能;禁止为了新功能破坏原有项目。
5. 当前三个线上入口
| 入口 | 地址 | 运行时 | 存储 | 定位 |
|---|---|---|---|---|
| v1 | https://zhai.beibbloveme.workers.dev |
Cloudflare Worker zhai |
KV chat_memory_v2 |
历史主库:真实聊天归档与记忆都在这里(用户长期使用的入口) |
| v2 | https://zhai-v2.beibbloveme.workers.dev |
Cloudflare Worker zhai-v2 |
KV chat_memory_v3 |
按用户要求全新重建的空环境(2026-10-24 深夜),不是搬迁失败 |
| 国内版 | https://a0cd1755f00a22a37.app.workbuddy.host |
Node 22 + server.js 垫片 |
/tmp/cat-chat-data/chat.db(SQLite) |
国内直连可用、无 KV 配额限制;数据是 v1+v2 合并导入 + 新聊 |
命名坑(务必记住):
chat_memory_v2是 v1 的库;chat_memory_v3才是 v2(zhai-v2)的库。用户口中的「v3 后端 = 全新的那个 = 配置里叫 v2」,指的是zhai-v2+chat_memory_v3。
6. 当前状态快照(2026-10-27 00:45)
| 维度 | 状态 |
|---|---|
| 代码 | 分支 main,HEAD 5666a72(2026-10-25 20:36),工作区无已修改文件(仅有未跟踪的文档/截图) |
| v1 | 已部署 a3ef2a33(10-25 22:54),含「KV 配额烧穿」修复;线上 bundle 与本地逐字节一致【线上核验】 |
| v2 | 已部署 8c6fe98a(10-25 20:38),同一份代码(etag 与 v1 相同)【线上核验】 |
| 国内版 | 进程 22:26 重启,已加载新代码;页面注入 cat-web-token 与 __CAT_WEB_MODE__【实测】 |
| 聊天可用性 | 国内版完全可用;v1/v2 在 10-26 08:00(北京时间)KV 配额重置前处于降级模式(能聊、提示「云端检索今日配额已用完」) |
| 数据规模 | 国内版 SQLite:652 键(消息 320 / 记忆 312 / 高光 2 / 经历 1 / 元数据 2 / 限流 14 / 主动计数 1 / 墓碑 0)【实测】 |
| 测试 | tests/ 共 10 个脚本、约 206 项断言,全部为「内存/SQLite Mock + Mock 模型」的契约测试,无生产 E2E【源码】 |
| 最大风险 | 见 07:P0 三项(前后端寻址分裂、彻底删除不清 mem:、页面公开下发令牌) |
7. 与原设计的差距(一句话版)
- 已完成:聊天、记忆检索、档案、高光/经历/关系、删除、同步/恢复、三端部署、配额防护。
- 只做了雏形:人设(仅一段自定义文本,无版本管理)、成长分析(完全没有)、后台管理(没有独立后台,只有设置面板)。
- 未开始:阶段 5 人设版本、阶段 6 成长队列、阶段 7 后台强认证、阶段 8 沈屿迁移、阶段 9 测试环境清理。
- 设计过但未采用:向量检索、R2 存图、Mem0 式四态记忆 + history 版本链(只出了调研与建议)。
详细依据见 03_任务清单与真实进度.md、05_长期记忆与收藏系统历史档案.md。
02 · 历史需求与设计构想
02 · 历史需求与设计构想
按「主题 + 时间」整理。每条尽量保留原始措辞的关键含义。 标注:✅ 已实现 | 🟡 部分实现/雏形 | ⬜ 已提出未实施 | ❌ 已撤销/被取代 证据:对话摘要(30 个历史阶段摘要)、任务书原文、Git 提交、当前代码。
一、初始需求(2026-10-05)
见 01_项目总览与长期目标.md 第 3 节(五组要求逐条列出,不再重复)。三条关键约束至今有效:
- 不准重构代码结构、不准改 UI 风格;
- 不准一次性甩几百行代码,要拆成可复制粘贴的步骤(手机操作);
- 用户输入与 AI 回复必须原封不动入库。
已确认的技术选型:Worker 统一转发 / 关键词检索 / 页面打开时检查主动消息。
二、UI 与交互:历次明确要求
2.1 第一批(10-05 ~ 10-06,✅)
| 要求 | 结果 |
|---|---|
| 果冻/液态玻璃气泡(blur 8px、半透明白、微高光、圆角 16、宽 75%) | ✅ |
| 头像可自定义上传,不写死任何默认图 | ✅(内置无头像时用文字占位,不写死图片) |
| 猫爪面板:颜文字 / 表情包(本地上传转 Base64 + 粘贴 URL)/ 右侧麦克风占位 | ✅ |
| 长按菜单:复制 / 引用 / 删除单条 / 收藏;引用时上方显示原文 | ✅ |
| 消息状态:发送中 / 成功 / 失败可重试 | ✅(后续加了秒级时间,再后又改回居中时间条) |
动态状态胶囊:按 ai_status 渲染,无则不显示空白框 |
✅ |
| IndexedDB 替代 localStorage 存聊天记录 | ✅ |
| 真实 fetch 替代假回复,错误显示在气泡 | ✅ |
2.2 外观细化(10-14 ~ 10-16,✅ / 一处回滚)
| 日期 | 要求 | 状态 |
|---|---|---|
| 10-14 | 手绘气泡框(猫/云/星星/蝴蝶结装饰,参照截图) | ❌ 上线后中文乱码(atob 字节问题)→ 回滚 |
| 10-14 | 液态玻璃 + 「我的气泡粉白少女心」 | ✅ |
| 10-14 | 阴影范围缩小、颜色加深 | ✅ |
| 10-14 | 状态文字:头像正上方、白字、明显深阴影 | ✅(后修正居中、过长贴边不裁切) |
| 10-14 | 表情包点击直接发送 + 发送前自动压缩 | ✅ |
| 10-14 | 文案「自定义标题 → 备注喵」「自定义状态 → 状态喵」 | ✅ |
| 10-14 | 设置分「头像与聊天背景」「后端与模型」两组折叠 | ✅ |
| 10-14 | 猫爪面板重排 + 照片三入口(上传 / 拍照 / 文件) | ✅ |
| 10-14 | 新增收藏(微信式 UI、双向收藏、显示收藏者) | ✅ |
| 10-14 | 发送状态移到气泡下方 + 成功后秒级时间;行距调大 | ✅(10-17 改为居中时间条) |
| 10-16 | 顶部全屏按钮(Fullscreen API,图标随状态切换) | ✅ |
| 10-16 | PWA:添加到主屏幕无地址栏启动(manifest + 爪印图标 + apple meta) | ✅ |
| 10-16 | 图片全屏灯箱(点击查看、点击关闭) | ✅ |
| 10-16 | showToast 轻提示组件 |
✅ |
2.3 10-17:功能与观感修正(✅)
- 修发送键(点击事件被当成文本,出现
[object PointerEvent]); - 修头像阴影被重复定义覆盖;
- 新状态只跟新消息,不全量改写历史状态;
- 取消气泡下方秒级时间 → 微信式居中时间条
2026.10.15 10:20; - 顶部栏 / 底部输入栏改悬浮圆角玻璃卡片;
- AI 认知纠正:知道自己是 AI;「状态」= 心情/情绪,不是任务或地点;
- 建立《模块索引.md》,以后按区域点单改代码。
2.4 10-17 第二轮(✅)
照片三入口面板 / iOS 高透液态玻璃 / 自定义气泡皮肤(PNG 九宫格 border-image-slice:33%,分「我的/对方」)/ 顶栏、底栏、全局底色可单独调 / 气泡颜色+透明度+磨砂分双方 / 表情包按微信规范压缩(长边 ≤240,透明图存 PNG,显示 ≤150px)/ 状态文字居中贴边 / 一键恢复默认外观(含深色模式)。
2.5 10-22:iOS 全屏与键盘(✅,过程极曲折)
用户原话演进(保留原意):
- 「你格式不是全屏的 是不是?这是新开的初始化」
- 「我每次点进去,然后我下拉的那个状态他不会保存」(滚动位置)
- 「根本不是这个问题,还是你的那个整体的叫什么布局格式有问题,你重新校准……把我底部的那个也给改回去 如图所示」
- 「你设置自适应高度啊行不行咋别人前端不这样 改了几次了烦死了 你搜别人的怎么做啊 自己想什么啊」
- 「我改了那么多次跟你反馈问题你自己不会思考怎么做或者搜索吗??……一个搜索能解决的你给我改了6版」
- 「之前有成功的版本的 这版时好时坏的」
- 「它直接不跟着那个键盘移动了,它直接钉死在那了,你继续搜啊,还有之前不是有成功的版本吗,你参考一下」
- 「不打字的那个状态下面,有的时候它会跳出来一大片那个空格」
- 「取消或者发送完以后,它这个键盘还是会抽搐,你不要再继续改那个代码了,我之前说没问题的那一版,你把这个那部分的那个代码直接套在这里」
- 「我真服了,终于行了。」
最终方案(✅,提交 f02cc07 / 线上 51ac31d3):
100vh → 100dvh渐进增强;.wrapper普通流height:100%(不再整页 fixed);- 键盘高度
kb = innerHeight - visualViewport.height - offsetTop,只写 CSS 变量--kb-inset,不在回调里改布局高度; - iOS 26 已知 bug(键盘收起后
offsetTop残留)用「未聚焦即归零」规避; - 去掉
transition:bottom与 focusout 延迟补测(抽搐根因); - 去掉
--safe-bar双重补偿(iOSinnerHeight已排除工具栏); ResizeObserver实测输入栏高度写--bar-h,面板展开/引用条出现不遮挡消息;- 输入框字号 16px(iOS <16px 聚焦自动缩放整页的根治方案)。
2.6 10-24:输入框与删除(✅,9ed08a6 / 线上 0114d69f)
- 「删除功能出问题了 气泡框也是又出问题了 就修复这两个其他别动 上网搜怎么做」——经确认「气泡框」指输入框。
- 修法:单条删除走服务端(
deleteOneOnServer,两条删除路径统一);输入框 15px→16px;颜文字输入框 13px→16px;--bar-h动态高度。
2.7 10-25:删除语义(✅)
- 「我发现这个云端删除它不能真的云端删除」→ 选 A 根治:物理删除 + 墓碑。
- 「这个云端删除是后端删除的意思吗?」→ 是(云端 KV 记录),并明确 UI 文案「本机和云端都删,不可恢复」。
- 用户随后要求「先做 V2 的,万一跑不通怎么办」→ v2 先行,v1 后行(staged rollout)。
三、安全改造(10-17 第三轮,✅)
用户 7 条要求 → 实现与验证:
| # | 要求 | 实现 |
|---|---|---|
| 1 | 接口身份验证(防未授权/绕过) | X-Cat-Token + 同源校验;所有 POST、/proactive、/backup、/init 必须带令牌 |
| 2 | API Key 保护 | 出站白名单、https+443、禁内网 IP、禁重定向、错误脱敏 |
| 3 | 频率/体积/模型额度 | IP 分钟 20、IP 日 400、全局 1500;body 1MB(导入 8MB);消息 4000 字;图片 800KB;max_tokens 500 |
| 4 | KV/记忆越权 | 全部接口置于鉴权之后;后续再加 profile 隔离 |
| 5 | 提示词注入/输入校验 | <<< >>> 包裹数据 + 最高优先级安全段 + stripMarkers + 控制字符清洗 |
| 6 | SSRF/开放代理/重定向 | 白名单 + 禁 IP 字面量 + redirect:'manual' + 3xx 拒绝 |
| 7 | 日志/轮换/备份/回滚 | rid 追踪、wrangler secret put AUTH_TOKEN 轮换、/backup 导出、wrangler rollback |
测试:后端 46 项断言 + Playwright 前端回归(当时全过)。
遗留:令牌随页面下发(知道地址即可拿令牌),当时已如实记录为「未解决风险」,任务书阶段 7 才计划做独立强认证。
四、《完整系统升级与安全改造任务书 v1.0》阶段 0–9
原文:
/root/uploads/1792388616892580314-文本.txt
| 阶段 | 目标 | 状态 | 说明 |
|---|---|---|---|
| 0 | 最新源码审计(只读,输出 A–E 五份清单) | ✅ | 审计报告 + 修复清单,10-19 完成 |
| 1 | 备份与基础可靠性(只修确认存在的问题) | ✅ | 修 B1 误清空、备份恢复、保存失败反馈、幂等 |
| 2 | 统一数据结构(角色/会话/消息/来源/同步状态) | ✅ | profile_id / message_id / conversation_id / sync_status、msg:<profile>:<mid> |
| 3 | 完整档案与搜索(查询/追溯/导出/恢复/重试) | ✅ | /archive(含 keyword/from/to/speaker/分页)、/export/full、/import/full |
| 4 | 高光与经历记忆(时间线/关联/来源/不确定标注) | ✅ | hl/exp/rel + 记忆库面板 + 候选经历单列 |
| 5 | 人设版本管理(独立保存、版本、对比、回滚) | ⬜ 未开始 | 当前人设只是一段 localStorage 文本 |
| 6 | 批量成长分析(任务队列、证据/反例、待审核) | ⬜ 未开始 | 无任何成长分析代码 |
| 7 | 后台管理(档案/记忆/人设/任务/备份) | 🟡 雏形 | 只有设置面板里的记忆库三标签;无独立后台、无强认证 |
| 8 | 沈屿本体迁移(历史/人设/记忆) | ⬜ 未开始 | 当前是测试角色 owner |
| 9 | 测试环境清理 | ⬜ 未开始 | 测试数据仍在(v1 归档 279 条软删、国内版 307 条导入) |
任务书还规定:每阶段必须交付 11 项内容(目标/文件清单/改动说明/API 变更/迁移风险/测试结果/未解决问题/回归/部署验证/回滚/下阶段建议),未实际运行的测试不得声称通过。
五、记忆系统构想(详见 05)
按代际:
| 代 | 构想 | 状态 |
|---|---|---|
| v0 | 所有消息原封不动存 KV(mem_<ts>_<rand>),关键词检索最近片段 |
✅ 已实现(旧键保留只读) |
| v1 | 按 profile 隔离(mem:<profile>:*),修跨角色泄漏 |
✅ |
| v2 | 结构化档案(msg:<profile>:<mid>)+ 稳定 mid + 幂等归档 |
✅ |
| v3 | 高光原话(必须引用真实消息)+ 经历(active/candidate + source_mids + 确定性)+ 关系(5 种类型) | ✅ |
| v4 | 参考 Mem0/Graphiti/Letta/LangGraph 的四态记忆、双时间戳、版本链、token 预算 | ⬜ 仅调研 + 5 条建议,未实施 |
六、托管与部署构想
| 时间 | 要求 | 状态 |
|---|---|---|
| 10-24 | 「重新建一个后端……不是一个网址」「全部重新部署吧一个全新的」 | ✅ 新建 zhai-v2 + chat_memory_v3(空库),旧环境保留 |
| 10-24 | 「暂时先部署到 WorkBuddy 上面,后端那个记录的先不用」 | ✅ 国内直连测试版(先无 KV,后加 SQLite) |
| 10-25 | 「理论上来说你是不是可以把后端相同的迁移到那个阿里云的云服务器?」→ 给四方案 → 用户选 C(先验证 WorkBuddy 存储持久性) | ✅ 四批完成(SqliteKV / 垫片 / 回归 / 双快照 / 搬迁) |
| 10-25 | 「情况一(v2 搬迁)情况二(导出导入恢复)都考虑,做一个版本」 | ✅ 双快照三层防护 + 手动导出导入 |
| 10-25 | 「你直接做完吧,不用问我了」 | ✅ 第 3、4 批自主推进 |
国内版的硬约束(平台规则,实测):
- 同源请求必须带 ?sharecode=…,否则网关 403「链接不完整」;
- 部署目录会被本地代码同步覆盖 → 数据必须放 /tmp;
- 反向代理改写 Host → 同源基准只能用固定的可信公网地址。
七、明确「不要做什么」(全部仍然有效)
摘自初始需求、任务书与历次纠正:
- 不准重构代码结构、不准改 UI 风格;只改被点名的区域(《模块索引.md》就是为了这个)。
- 不准一次性给几百行;要拆成可复制粘贴的步骤。
- 不准写死默认头像/默认人设;人设只用用户自定义的。
- 不准用假回复(
setTimeout);必须是真实请求,错误要显示在气泡里。 - 不准把界面已显示消息当成云端保存成功(任务书原话)。
- 不准在没备份的情况下覆盖唯一可用版本;不准直接清空 KV/IndexedDB。
- 不准把推测当确定事实;不确定的记忆要标 candidate/unknown。
- 不准把管理密钥/模型 Key 写进公开 HTML。
- 不准只靠隐藏按钮/前端路由保护管理功能。
- 不准跳过阶段验收;未实际运行的测试不得声称通过。
- 不准未经批准扩大修改范围;发现问题先停下说明风险。
- 不准删测试后端/测试数据(阶段 9 之前);删除必须指定 profile 与清单,不能用模糊条件。
八、已提出但未实施(保留,不要丢)
| 构想 | 出处 | 状态 |
|---|---|---|
| 向量检索(Vectorize + embedding) | 初始方案选项 | ⬜ 用户选了关键词检索;任务书也说「不得视为第一阶段必要」 |
| 图片存 R2(云端保留原图) | 交付文档「已知限制 2」 | ⬜ 涉及付费,未授权 |
| 全量导出/导入的前端 UI | 交付文档「待办」 | 🟡 API 已有,前端仅国内版可用 |
| 档案搜索面板 + 跳转原消息 | 交付文档「待办」 | ⬜ 后端 /archive?keyword= 已有 |
| 人设四层 + 版本管理(阶段 5) | 任务书 | ⬜ |
| 成长队列 + 证据/反例/审核(阶段 6) | 任务书 | ⬜ |
| 独立后台 + 强认证(阶段 7) | 任务书 | ⬜ |
| 沈屿本体迁移(阶段 8) | 任务书 | ⬜ |
| 测试环境清理(阶段 9) | 任务书 | ⬜ |
| 流式回复(SSE) | 10-23 基线审查标注【未实现】 | ⬜ 未提出明确需求 |
| 消息全文搜索 UI | 10-23 基线审查标注【未实现】 | ⬜ |
| 多角色切换 UI | 10-23 基线审查标注【未实现】 | 🟡 后端 profile 已支持,前端固定 owner |
| 深色模式 | 10-17 用户要求删除 | ❌ 已移除入口(CSS 仍有变量残留) |
| 手绘气泡装饰 | 10-14 | ❌ 因乱码回滚,未再尝试 |
九、需求冲突与待核实
| 冲突 | 处理 |
|---|---|
| 「云端删除」早期=软删(记录仍在)vs 10-25 用户要求「真的删」 | 10-25 起改为硬删+墓碑;历史 279 条软删记录仍物理存在(见 07),需一次性清理 |
| 「备份要包含记忆」vs 早期导出只含 messages/favorites/settings | 已补 /export/full(含 mem/hl/exp/rel);但 v1/v2 的前端导出仍走本机数据(见 07 P1-4) |
| 「v2 空=搬迁失败」(我的错误结论)vs 用户「v2 是全新部署」 | 以用户为准:v2 空是设计;606 条搬进的是国内版(见 06 D-08) |
03 · 任务清单与真实进度
03 · 任务清单与真实进度
状态定义:✅ 已完成(有代码+证据)|🟡 部分完成(链路未闭环)|⬜ 未开始|⏸ 待验证|❌ 已取消/被取代 证据等级:
【代码】源码可见 |【测试】本地测试跑过 |【部署】线上版本已确认 |【实测】我亲自执行并得到结果 |【未验证】无证据 重要:tests/里全部是「Mock KV / Mock 模型」的契约测试,能证明逻辑分支,不能证明生产端到端可用。凡是只有【测试】没有【实测/部署】的,都不算「线上已验证」。
一、任务总表(按主题)
A. 项目奠基与前端 UI
| # | 任务 | 状态 | 证据 | 未完成/说明 |
|---|---|---|---|---|
| A1 | 基于用户源码(v6.html)产出改造版 index.html(7 个片段) |
✅ | 提交 afa0aa4 基线;文件在 /workspace/chat/index.html【代码】 |
— |
| A2 | 果冻/液态玻璃气泡、头像自定义、猫爪面板、长按菜单、消息状态、动态状态胶囊 | ✅ | 同上【代码】 | 手绘装饰版曾上线后乱码 → 回滚 |
| A3 | IndexedDB 替换 localStorage + 真实 fetch | ✅ | readMessages/saveMessages/idbSet【代码】 |
— |
| A4 | 收藏(本地 fav_list + AI [[fav:]] 回传) |
✅ | parseFavorites、addFavorite【代码】 |
收藏历史只在本机(后加云端高光) |
| A5 | 全屏按钮 / PWA / 图片灯箱 / showToast | ✅ | 提交 08815a9 等【代码】 |
国内版 PWA 图标受 sharecode 影响(见 07 P2-10) |
| A6 | 外观定制(气泡颜色/透明度/磨砂、自定义皮肤九宫格、顶底栏调色、恢复默认) | ✅ | applyAppearance、applySkinClass【代码】 |
深色模式入口已按用户要求删除 |
| A7 | iOS 全屏与键盘适配(6 轮返工) | ✅ | 提交 43ec8d7…f02cc07;线上 51ac31d3【部署】 |
过程见 07 事件 03 |
| A8 | 输入框 16px 防聚焦缩放 + --bar-h 动态高度 |
✅ | 提交 9ed08a6;线上 0114d69f【部署】 |
— |
| A9 | 「仅本机删除」与「彻底删除(含云端)」两条路径拆分 | ✅ | 提交 d7ada29、6a09007;test_ui_delete_scope.mjs【测试】 |
历史消息无 mid 时只能本地删(旧数据限制) |
B. 后端与安全
| # | 任务 | 状态 | 证据 | 未完成/说明 |
|---|---|---|---|---|
| B1 | Worker 后端:KV 记忆、关键词检索、时间注入、主动消息、tools 预留 | ✅ | worker.js【代码】 |
主动消息最多 3 次/天、间隔 ≥3h |
| B2 | 安全 v2:令牌/同源/限流/SSRF/体积/脱敏/提示词边界 | ✅ | 提交链 + 46 项断言【测试】 | 令牌随页面下发未解决(P0-3) |
| B3 | 多服务商代理(OpenAI/DeepSeek/通义/自定义) | ✅ | callModel + validateApiUrl【代码】 |
v1/v2 ALLOW_CUSTOM_API=0 → 自定义域名会被 403(P2-4) |
| B4 | 页面令牌按部署注入(修 v1 401) | ✅ | 提交 32aa8a5;v1 a3ef2a33 线上拉取验证【线上核验】 |
v1 无 AUTH_TOKEN → 用代码默认值(弱) |
| B5 | 模型 Key 服务端兜底 | ❌ | v1 secret 叫 DEEPSEEK_API_KEY,代码只读 env.API_KEY【线上核验】 |
死配置;目前全靠前端自填 Key |
C. 任务书阶段 0–4
| # | 任务 | 状态 | 证据 | 未完成/说明 |
|---|---|---|---|---|
| C1 | 阶段 0 只读审计 | ✅ | 审计报告-阶段0.md、小手机-审计草案/【代码/文档】 |
— |
| C2 | 阶段 1 备份与基础可靠性(含 B1 误清空修复) | ✅ | 提交 08815a9【代码】 |
— |
| C3 | 阶段 2 统一标识(profile / mid / conversation / sync_status) | ✅ | 提交 e6c9661【代码】 |
— |
| C4 | 阶段 3 完整档案与同步(云端 msg: + 同步/恢复按钮) |
✅ | 提交 e6c9661【代码】 |
同步为全量上传,O(n) 写(P1-7) |
| C5 | 阶段 4 高光 / 经历 / 关系 + 记忆库面板 | ✅ | 提交 ca44bb8【代码】 |
关系不进模型上下文(设计如此) |
| C6 | P0-1 记忆按 profile 隔离 | ✅ | 提交 9505597;test_p0_memory_isolation 15/15【测试】 |
旧 mem_* 仅 owner 只读 |
| C7 | P0-2 稳定 mid 与引用完整性 | ✅ | 提交 513f24b;21/21【测试】 |
— |
| C8 | P0-3 删除接口与防复活 | 🟡 | 提交 d4aa34e(软删)→ 83bc573(硬删+墓碑);36/36【测试】 |
硬删不清 mem:(P0-2) |
| C9 | P0-4 全量导出/导入 | ✅ | 提交 2460f9a;27/27【测试】 |
前端 UI 只在「国内版」放开(P1-4) |
| C10 | P1 记忆闭环 + 档案搜索 | ✅ | 提交 5bfe8c8;13/13 + 14/14【测试】 |
搜索无前端面板 |
D. 托管、迁移与部署
| # | 任务 | 状态 | 证据 | 未完成/说明 |
|---|---|---|---|---|
| D1 | Git 仓库接入(GitHub 私有) | ✅ | github.com/beibbloveme-crypto/yuai;git remote -v【代码】 |
远端实时状态【未验证】(沙箱 403) |
| D2 | Pages 静态托管适配 | 🟡 | 提交 1ad73ae;Pages 项目 chat-ui-beib 实为 10-03 的另一原型【实测】 |
当前三端都不用 Pages |
| D3 | 全新后端重建(zhai-v2 + chat_memory_v3) |
✅ | 提交 867faee;小手机-全新后端环境说明.md【文档】 |
空库是设计,不是失败 |
| D4 | 国内直连测试版(WorkBuddy) | ✅ | 提交 0b01ef6;线上可访问【实测】 |
初版无落库,后改 SQLite |
| D5 | C 方案第 1 批:server.js 垫片化 + SqliteKV |
✅ | 提交 4a8bdb1、e20c937【代码】 |
— |
| D6 | C 方案第 2 批:测试套件换真 SQLite | ✅ | 提交 ef6f725;188 项断言【测试】 |
仍是契约测试,非 E2E |
| D7 | C 方案第 3 批:双快照三层防护 + 导出补全 | ✅ | 提交 a32fe2e;13/13【测试】 |
快照与库同在 /tmp(P1-3) |
| D8 | C 方案第 4 批:v2 → 国内版全量搬迁 606 条 | ✅ | 提交 5f61f81;国内版 counts 与逐条导入报告【实测】 |
实际构成:307 消息 + 299 记忆;其中 217 条 mid 为 local_(来自本机历史同步) |
| D9 | 国内版三连修复(WORKER_URL 污染 / sharecode 403 / 人设丢失) | ✅ | 提交 c007087、be64807、bd6a5af【代码】 |
— |
| D10 | 国内版 403 根治(可信公网基准) | ✅ | 提交 0f1007c【代码】 |
域名一变仍会 403(P1-3) |
| D11 | 三端部署同一份修复版 | ✅ | v1 a3ef2a33、v2 8c6fe98a、国内版进程 22:26【线上核验/实测】 |
v1/v2 受 KV 配额限制,10-26 08:00 前降级 |
E. 删除语义与配额
| # | 任务 | 状态 | 证据 | 未完成/说明 |
|---|---|---|---|---|
| E1 | 「彻底删除」改物理删除 + 墓碑(v2 先行) | ✅ | 提交 83bc573;v2 65bf6ef0【部署】 |
历史 279 条软删未清理 |
| E2 | 墓碑防复活(导入四类记录前查墓碑) | ✅ | test_p0_delete_sync 第 7/8 节【测试】 |
墓碑不参与导出 → 换库恢复会丢墓碑 |
| E3 | 修复 KV 每日 list 配额烧穿(缓存+降级+干净 503) | ✅ | 提交 5666a72;test_p0_kv_quota 17/17【测试】;v1/v2 部署【部署】 |
写配额仍紧(P2-3);检索 1000 键截断(P1-2) |
F. 记忆系统升级(任务书 5–9)
| # | 任务 | 状态 | 证据 | 说明 |
|---|---|---|---|---|
| F1 | 阶段 5 人设版本管理 | ⬜ | 无代码 | 当前人设 = localStorage 一段文本 |
| F2 | 阶段 6 批量成长分析 | ⬜ | 无代码 | 无任务队列、无证据/反例、无审核 |
| F3 | 阶段 7 后台管理 + 强认证 | ⬜ | 无代码(只有设置面板) | 任务书要求「不得只靠隐藏按钮保护」 |
| F4 | 阶段 8 沈屿本体迁移 | ⬜ | 无代码 | 当前测试角色 owner |
| F5 | 阶段 9 测试环境清理 | ⬜ | 无代码 | 测试数据仍在(见 04 数据面) |
| F6 | 记忆框架升级(Mem0 四态 / Graphiti 双时间戳 / Letta 预算) | ⬜ | /workspace/小手机项目-全项目代码基线审查报告.md 第七部分(调研+5 条建议)【文档】 |
只出建议,未实施 |
G. 文档与交接
| # | 任务 | 状态 | 证据 | 说明 |
|---|---|---|---|---|
| G1 | 完整交付文档(源码/功能/进程/测试/交接) | ✅ | 小手机-完整交付文档.md(10-22,605 行)【文档】 |
已过期(不含 10-23 之后) |
| G2 | 模块索引(按区域改代码) | ✅ | 模块索引.md【文档】 |
行号已漂移,选择器名仍有效 |
| G3 | 安全说明 / 审计报告 / 修复记录 / 迁移方案 | ✅ | 4 份 md【文档】 | 均截至各自日期 |
| G4 | 交接档案(本次 10 个文件) | ✅ | 本目录【文档】 | 截至 2026-10-27 |
| G5 | 交接文档更新到当前基线 | 🟡 | 本档案即更新版 | 旧文档未删改(保留历史) |
二、用户明确下达但尚未完成的事
| # | 事 | 现状 | 建议 |
|---|---|---|---|
| 1 | 历史 279 条软删记录「真的删掉」 | 记录仍物理存在(v1 KV + 国内版 SQLite),仅带 deleted_at |
分批硬删+墓碑(≤200 条/批,跨天,注意写配额) |
| 2 | v1 与国内版数据统一 | 两份独立副本,互不同步 | 先定「哪个是主」,再做单向同步 |
| 3 | 图片备份 | 图只在本机 IndexedDB | 决策:接受 / 上 R2 / 只备份缩略图 |
| 4 | 沈屿本体迁移(阶段 8) | 未开始 | 先补人设版本(阶段 5),否则迁移无载体 |
| 5 | 后台管理(阶段 7) | 未开始 | 与 P0-3 访问控制一起做最省事 |
三、被取消 / 被取代的任务
| 任务 | 原因 |
|---|---|
| 手绘气泡装饰 | 上线后乱码,回滚 |
| 深色模式 | 用户 10-17 要求删除入口 |
| 默认人设「小喵」 | 用户 10-19 要求取消,只用自定义人设 |
deploy.sh 手工改 cat-chat-all-in-one.js 后端 |
被 build.js 整体重生成取代 |
| 软的「云端删除」 | 被硬删+墓碑取代(10-25) |
| 单一 Worker 架构 | 被 v1 / v2 / 国内版三端取代 |
| 阿里云 ECS 方案 | 用户选 C(WorkBuddy + SQLite) |
四、如何自行复核本表
cd /workspace/chat
git log --oneline --all # 全部提交
git status # 工作区状态
node build.js # 重建产物(会重写 cat-chat-all-in-one.js)
for f in tests/*.mjs; do node "$f"; done # 契约测试
线上版本核对(只读):
- Cloudflare API:/accounts/{acc}/workers/scripts/{name} 拉取线上 bundle;
- 国内版:curl http://127.0.0.1:3000/health(沙箱内)。
提醒:
node build.js会重写cat-chat-all-in-one.js,属于「修改文件」。接手后若要跑,请先git status确认无未提交改动,或先建分支。
04 · 技术架构与代码基线
04 · 技术架构与当前代码基线
基线时间:2026-10-27 00:45 | 只读核实,未改任何代码 金额/配额口径:Cloudflare Workers 免费版 KV = 每天 1000 次 list、1000 次写、10 万次读(账户级,北京时间 08:00 重置)
1. 仓库与工作区
| 项 | 值 | 证据 |
|---|---|---|
| 本地目录 | /workspace/chat |
【实测】 |
| 远程 | github.com/beibbloveme-crypto/yuai |
git remote -v【代码】 |
| 分支 | main(当前)、fix/p0-memory-consistency(历史分支) |
git branch -a【实测】 |
| HEAD | 5666a72(2026-10-25 20:36)fix(kv): 修复每日 list 配额烧穿导致聊天 500 |
git log【实测】 |
| 工作区 | 干净(无已修改文件);未跟踪:handoff-public.md、小手机-完整交付文档.md、2 张 PNG |
git status【实测】 |
| 提交总数 | 60(10-05 起) | git rev-list --count HEAD【实测】 |
| 远端同步 | 本地 main 已包含 10-25 的提交;远端实时状态未验证(沙箱访问 GitHub 受限) |
【未验证】 |
2. 文件与职责
| 文件 | 行数 / 大小 | 角色 | 纪律 |
|---|---|---|---|
index.html |
12,509 行 / 230,966 B | 前端唯一真源 | 改完必须 node build.js |
worker.js |
2,801 行 / 152,446 B | 后端唯一真源 | 同上;serveFrontend 由构建期注入,源码里未定义 |
build.js |
58 行 | 构建脚本 | worker.js + base64(index.html) + serveFrontend() → 产物 |
cat-chat-all-in-one.js |
2,822 行 / 460,954 B | 部署产物 | 禁止手工编辑 |
wrangler.toml |
— | v2 配置(zhai-v2 + KV 0a4c…) |
含真实账号/KV ID,勿外传 |
wrangler.v1.toml |
— | v1 配置(zhai + KV ec1a…) |
同上 |
deploy-web/server.js |
484 行 | 国内版平台垫片 + 快照管理 | 只做适配,不做业务 |
deploy-web/kv-sqlite.js |
155 行 | env.CHAT 的 SQLite 实现 |
对齐 KV 六方法 |
deploy-web/worker.mjs |
2,822 行 / 460,954 B | 后端部署副本 | 与 bundle 逐字节相同(手工同步) |
deploy-web/start-domestic.sh |
— | 看门狗(退出 3 秒重启 + pidfile) | — |
tests/*.mjs |
10 个文件 | 契约测试 | 全部 Mock,非 E2E |
tools/*.mjs |
3 个 | 搬迁/审计脚本 | 一次性工具 |
3. 构建链
index.html ─┐
├─ node build.js ─► cat-chat-all-in-one.js ─┐
worker.js ──┘ ├─ wrangler deploy (v2) → zhai-v2
└─ wrangler deploy --config wrangler.v1.toml → zhai
index.html ──► deploy-web/server.js 运行时读取(注入令牌/WEB_MODE/sharecode 垫片)
worker.js ──► cp 成 deploy-web/worker.mjs ──► Node 动态 import
已知脆弱点:
- wrangler deploy --config wrangler.v1.toml 不会自动构建 → 忘记 node build.js 就部署旧产物;
- deploy-web/worker.mjs 没有自动同步步骤(当前一致,靠人工);
- worker.js 单独 import 会因 serveFrontend 未定义在 GET / 时报错。
4. 运行时拓扑
┌──────────────── 浏览器(手机) ────────────────┐
│ index.html(单页,内联 CSS/JS) │
│ localStorage:设置/外观/模型 Key/worker_url │
│ IndexedDB(cat_chat_db/kv):聊天记录、设备快照 │
└───────┬─────────────────────────────┬──────────┘
│ 聊天:CONFIG.WORKER_URL │ 记忆/档案/删除:相对路径(同源)
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Cloudflare Worker │ │ Node 垫片(server.js) │
│ v1: zhai │ │ + SQLite(/tmp/...db) │
│ v2: zhai-v2 │ │ 复用同一 worker.js │
└────────┬──────────┘ └──────────┬───────────┘
▼ ▼
Cloudflare KV SQLite(WAL)
v1 ec1a / v2 0a4c 652 键
│
▼
大模型 API(OpenAI 兼容,Key 由前端提供 → Worker 代理)
5. 路由表(worker.js,三端共用)
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
/、未匹配 GET |
GET | 否 | 返回页面(注入令牌) |
| 任意路径 | POST | 是 | 聊天主接口(handleChat) |
/manifest.webmanifest、/icon-192.png、/icon-512.png、/apple-touch-icon.png |
GET | 否 | PWA 资源(base64 内嵌) |
/health |
GET | 否 | {ok, service, version:"2.0"}(版本号写死) |
/init |
POST | 是 | 空实现(兼容保留) |
/backup |
GET | 是 | 旧版导出(只含 mem) |
/archive |
GET | 是 | 档案导出;支持 keyword/from/to/speaker/limit/offset |
/archive |
POST | 是 | 档案幂等导入(按 mid) |
/export/full |
GET | 是 | 全量导出(msg/hl/exp/rel/mem) |
/import/full |
POST | 是 | 全量导入(dry_run、幂等、逐条 errors/warnings、墓碑跳过) |
/highlights |
GET/POST/DELETE | 是 | 高光;写入强制引用真实 msg: |
/experiences |
GET/POST/DELETE | 是 | 经历;source_mids 必须存在 |
/relations |
GET/POST/DELETE | 是 | 关系;两端必须存在 |
/proactive |
POST | 是 | 主动消息(3 次/天、间隔 ≥3h、按 profile) |
/messages/delete |
POST | 是 | mode=soft\|hard、force;有引用 → 409 |
/snapshots、/snapshot/now、/snapshot/rollback、/sysinfo |
GET/POST | 令牌 | 仅国内版(server.js 自处理) |
6. KV / SQLite 键位图
| 键模式 | 内容 | 说明 |
|---|---|---|
mem:<profile>:<ts>_<rand> |
长期记忆(role/content/ts/day_key/hour/period) | 聊天检索的数据源 |
mem_<ts>_<rand> |
旧格式(全局) | 仅 owner 只读兼容 |
msg:<profile>:<mid> |
结构化档案(含 deleted_at) |
导出/恢复/删除的对象 |
hl:<profile>:<hid> |
高光原话(配 mid、content_hash) |
记忆库 UI |
exp:<profile>:<eid> |
经历(active/candidate、source_mids、确定性) |
记忆库 UI |
rel:<profile>:<rid> |
关系(5 种类型) | 记忆库 UI;不进提示词 |
tomb:<profile>:<id> |
墓碑(400 天 TTL,只存编号) | 硬删防复活 |
meta_lastTs:<profile> / meta_lastTs |
最后聊天时间 | 后者为旧键 |
proactive:<profile>:<day> / proactive_<day> |
主动消息每日计数 | 26h TTL |
rl_<hash> / rl_global |
限流计数 | 2 天 TTL |
<profile> 默认 owner,校验 ^[a-z0-9_]{1,32}$。
7. 三端配置现状(值已脱敏)
| 项 | v1 zhai |
v2 zhai-v2 |
国内版 |
|---|---|---|---|
| KV 绑定 | CHAT = ec1a…7accc |
CHAT = 0a4c…ce8ad |
CHAT = SqliteKV 实例 |
| Secret | DEEPSEEK_API_KEY(代码不读) |
AUTH_TOKEN |
无(AUTH_TOKEN 由 makeEnv 注入) |
| 令牌 | 代码默认值(源码内) | secret(页面注入) | 代码内 cat-web-token |
| vars | API_URL/ALLOWED_API_HOSTS/ALLOW_CUSTOM_API=0/ALLOWED_ORIGINS |
同上 + MODEL |
ALLOW_CUSTOM_API=1、STRICT_MID=1、MEM_READ_LEGACY=1 |
| 部署版本 | a3ef2a33(10-25 22:54) |
8c6fe98a(10-25 20:38) |
进程 22:26 启动 |
| 代码一致性 | 线上 = 本地(含修复)【线上核验】 | 同左(etag 相同) | worker.mjs = bundle 逐字节【实测】 |
8. 数据面(截至 2026-10-27 00:45)
8.1 国内版 SQLite(/tmp/cat-chat-data/chat.db,652 键)
| 家族 | 数量 | 备注 |
|---|---|---|
msg: |
320 | 307 条迁移导入 + 13 条 10-25 新聊 |
mem: |
312 | 迁移带入 |
hl: |
2 | — |
exp: |
1 | — |
rel: |
0 | — |
tomb: |
0 | 硬删功能尚未在此库使用过 |
meta |
2 | meta_lastTs 与 meta_lastTs:owner |
proactive |
1 | — |
rl |
14 | 限流计数残留 |
被删状态:320 条中 279 条带 deleted_at(10-23 22 点 68 条、10-24 13 点 211 条),有效 41 条。
迁移来源:307 条的 mid 前缀 = local_ 217 / c_ 44 / a_ 39 / u_ 7 → 大多数来自本机历史的「云端同步」上传,不是服务端实时聊天记录。
时间跨度:2026-10-14 02:40 → 2026-10-25 22:37。
8.2 Cloudflare KV
| 库 | 内容 | 证据 |
|---|---|---|
v1 ec1a… |
抽样 4 个 msg:owner:*(local_/c_ 各 2)全部 200 命中;meta_lastTs:owner 读出 10-25 22:31 的时间戳 |
【实测】 |
v2 0a4c… |
同 4 个键全部 404;meta_lastTs 停在 10-25 18:56 |
【实测】 |
精确键数无法枚举:免费版 list 配额已耗尽(
code 10048),RESTkeys同样被拒。以上为抽样结论,不是全量统计。
8.3 浏览器
每一台设备各有一份 IndexedDB(cat_chat_db/kv,键 messages),含图片 base64;云端不存图。
9. 测试覆盖
| 文件 | 断言数 | 覆盖 |
|---|---|---|
test_p0_memory_isolation.mjs |
15 | profile 隔离(含图片/主动/关键词重叠/旧数据) |
test_p0_mid_refs.mjs |
21 | mid 回写、重试幂等、409、引用校验 |
test_p0_delete_sync.mjs |
36 | 软/硬删、409、级联、墓碑、防复活 |
test_p0_export_import.mjs |
27 | 全量导出/导入、dry_run、幂等、错误报告 |
test_p0_kv_quota.mjs |
17 | list 缓存、降级、503、写去重 |
test_p1_memory_context.mjs |
13 | 高光/经历进 prompt、候选分离 |
test_p1_archive_search.mjs |
14 | 搜索/时间/说话方/分页/软删 |
test_phase4_v2.mjs |
32 | 阶段 4 回归(源码 + 产物各 16) |
test_kv_sqlite.mjs |
23 | SqliteKV 六方法 |
test_ui_delete_scope.mjs |
~8 | 删除范围 UI 契约(静态断言) |
| 合计 | 约 206 | 全部为 Mock / 静态测试,非生产 E2E |
未覆盖:CORS 跨源、令牌注入、WORKER_URL 覆盖、三端联调、真实上游、浏览器 E2E、配额恢复后的行为。
10. 依赖与成本
| 项 | 说明 |
|---|---|
| 运行时依赖 | 无 npm 依赖(Workers 原生 + Node 内置 node:sqlite) |
| 外部服务 | Cloudflare Workers/KV;国内托管平台(WorkBuddy);大模型 API(用户自选) |
| 成本结构 | KV 免费额度(list/写各 1000/天);模型费用由用户 Key 承担 |
| 实测约束 | 一次聊天约 5–6 次 KV 写、约 300 次读、1–2 次 list(热缓存 0–1 次) |
11. 与原设计的偏差
| 设计 | 现状 | 偏差原因 |
|---|---|---|
| D1 或 KV | 选 KV | 用户选型 |
| 单一 Worker | v1 + v2 + 国内版三端 | 重建 + 国内可用性需求 |
| 「外置记忆库,原文入库」 | mem: 与 msg: 双写 |
分阶段演进叠加,未合并 |
| 「云端完整档案」 | 图片只存占位 | 未接 R2 |
| 「管理后台 + 强认证」 | 无 | 阶段 7 未开始 |
| 「人设版本管理」 | 一段文本 | 阶段 5 未开始 |
05 · 记忆与收藏系统
05 · 长期记忆与收藏系统历史档案
本文件把「设计过的」「做出来的」「还没做的」分开记录,不合并、不互相覆盖。 现状基线:
5666a72/ v1a3ef2a33/ v28c6fe98a/ 国内版 22:26 进程。
一、五代演进(设计层)
v0 · 原始记忆库(2026-10-05,✅ 已实现)
用户原始要求(原文):
「外置记忆库:后端必须接入 Cloudflare D1 或 KV。每次用户输入和 AI 回复原封不动存进数据库,带上时间戳。」 「检索逻辑:每次发消息时,去数据库检索最相关的历史原话,配合人设打包给大模型。」
实现:
- 键 mem_<ts>_<rand>,值 {role, content, ts, day_key, hour, period};
- 检索:list(prefix:'mem_') → 取最近 300 条 → 中文双字滑窗 + 英文整词 tokenize → 命中数排序 → top5;
- 时间感知:前端带 time.iso,后端 resolveTime() 归一,提示词注入「现在几点/什么时段」。
局限(后来被 P0-1 修掉):所有角色共用同一前缀,测试角色会读到正式角色的记忆。
v1 · 按 profile 隔离(2026-10-19,✅)
- 新键
mem:<profile>:<ts>_<rand>;旧mem_*仅 owner 只读(MEM_READ_LEGACY开关); meta_lastTs/ 主动消息计数也按 profile;- 测试:
test_p0_memory_isolation15/15(含图片消息、主动消息、关键词重叠、旧数据仅 owner)。
v2 · 结构化档案(2026-10-19,✅)
msg:<profile>:<mid>:profile_id / conversation_id / speaker / message_type / created_at / source_id / sync_status / deleted_at / attachment;- 稳定 mid:前端生成
c_<16hex>随请求带上;AI 消息a_<ts>_<shortId>; - 幂等:同 mid 只写一次;同 mid 不同内容 → 409
mid_conflict; - 这是「可追溯来源」的基础:高光/经历/关系都必须引用真实
msg:。
v3 · 高光 / 经历 / 关系(2026-10-19,✅)
| 类型 | 键 | 必填 | 进模型上下文? |
|---|---|---|---|
| 高光原话 | hl:<profile>:<hid> |
mid(真实消息)、content 必须能在原文中找到 |
✅ top3 |
| 经历 | exp:<profile>:<eid> |
title/content/source_mids、status=active\|candidate、time_certainty |
✅ active top3 + candidate top2(候选明确标注「不得当作事实」) |
| 关系 | rel:<profile>:<rid> |
from_id/to_id(高光或经历)、5 种类型 |
❌(仅 UI 查看) |
删除级联:删高光/经历会软删引用它的关系;删消息时若有引用 → 409,确认后级联。
v4 · 框架升级构想(2026-10-23,⬜ 只调研,未实施)
用户下发《基线审查任务书》,要求研究 4 个开源项目并说明「可借鉴什么/对应源码位置/兼容性/成本/风险」,明确不得直接照搬、不得只看 README。
| 项目 | 机制要点 | 当时建议 |
|---|---|---|
| Mem0 | ADD/UPDATE/DELETE/NONE 四态决策 + history 表(每次变更留旧值)+ BM25 加性打分 |
建议 1:确定性规则 + history 版本链(每条记忆保留变更历史) |
| Graphiti | 双时间戳 valid_at/invalid_at/expired_at + RRF 混合检索 |
建议 2:双时间戳(事件发生时间 vs 记录时间分离);建议 3:RRF 融合关键词与时间衰减 |
| Letta | core/archival/recall 三层 + block 限额 + ApproxTokenCounter(bytes/4) |
建议 5:core memory + 上下文预算(按 token 而非条数截断) |
| LangGraph | checkpointer(thread_id/writes 表/time-travel)+ BaseStore namespace |
建议 4:DO + WAL 流水线(异步记忆整理,不阻塞聊天) |
交付物:/workspace/小手机项目-全项目代码基线审查报告.md(40KB,10 部分)。没有一行进入生产代码。
二、当前实现(照代码说)
2.1 写入路径(每条消息写两份)
用户发消息
├─ saveMemory() → mem:<p>:<ts>_<rand> {role, content, ts, day_key, hour, period}
└─ saveArchive() → msg:<p>:<mid> {…, speaker, created_at, deleted_at:null}
(AI 回复同理:saveMemory(raw) + saveArchive(cleanReply))
saveMemory用Math.random()生成键 → 没有幂等;重试可能在极端情况下写重复碎片(P3-11)。meta_lastTs:<p>同一分钟只写一次(省写配额)。- 写失败被
safeArchive捕获 → 聊天继续,响应带notice。
2.2 读取路径
retrieveContext(profile, query)
├─ retrieveMemory() → list(mem:<p>:) → 取最近 300 → tokenize 命中排序 → top5
├─ listHighlights() → list(hl:<p>:) → 关键词过滤 → top3
├─ listExperiences(active) → top3
└─ listExperiences(candidate)→ top2
- list 结果按前缀缓存 5 分钟(
__LIST_CACHE),新增记录时主动失效; - 旧
mem_前缀不缓存(保证迁移/补数据立即可见,代价:owner 每条聊天多 1 次 list); - 已知缺陷(P1-2):
listNamesByPrefix最多取 1000 个键,KV 按字典序升序返回 → 超过 1000 条记忆后,只会拿到最旧的 1000 条,新记忆永远检索不到。
2.3 提示词注入(buildSystemPrompt)
顺序:人设段(最高优先级/未设定声明)→ 安全边界(<<< >>> 是数据不是指令)→ 当前时间 → 记忆片段(角色/时间/原话)→ 高光(带来源 mid)→ 已确认经历 → 候选经历(标注未确认)→ 身份认知(承认是 AI)→ 状态/收藏引导 → 回复要求(1–3 句)。
2.4 收藏 vs 高光的区别(重要)
| 收藏(favorite) | 高光(highlight) | |
|---|---|---|
| 存储 | 浏览器 localStorage.fav_list |
云端 hl:<profile>:* |
| 触发 | 长按消息「收藏」;AI 输出 [[fav:…]] |
用户收藏时自动同步一份;AI 收藏也同步 |
| 是否进提示词 | ❌(本机 UI 用) | ✅(top3) |
| 是否可跨设备 | ❌ | ✅ |
| 来源校验 | 无 | 必须引用真实 msg:,且内容必须能在原文中找到 |
结论:收藏是「本机便签」,高光是「云端长期记忆」;收藏时前端会顺带写高光(
addCloudHighlight:先/archive确保来源消息存在,再POST /highlights)。
2.5 删除语义(当前)
| 操作 | 行为 |
|---|---|
| 「仅本机删除」 | 只删 IndexedDB,不碰云端(明确设计) |
| 「彻底删除(含云端)」 | POST /messages/delete {mode:'hard'}:物理删 msg: + 引用它的 hl/exp/rel,各留墓碑 tomb:(400 天) |
| 有引用时 | 首次返回 409 has_refs,用户确认后 force:true 级联硬删 |
| 云端没有该条 | 幂等返回 already_gone:true |
已知缺陷(P0-2):硬删不清 mem: 碎片,而聊天检索读的正是 mem: → 被删内容仍会被检索进提示词。
历史遗留:10-23/10-24 两次「云端删除」是软删,279 条记录仍物理存在(只在导出/检索时被过滤)。
2.6 导入 / 导出
| 接口 | 内容 | 去重 | 墓碑 |
|---|---|---|---|
/export/full |
msg + hl + exp + rel + mem(+ owner 的旧 mem_) |
— | ❌ 不导出墓碑 |
/import/full |
同上,dry_run 可预览 |
mid/hid/eid/rid + mem 的 content|ts 指纹 |
✅ 命中即跳过 |
/archive GET/POST |
只含 msg(+ legacy),支持搜索分页 | mid | ✅ |
墓碑不导出 → 把备份恢复到另一个空库时,防复活保护会丢失(P2)。
2.7 profile 隔离
- 后端全部按 profile 分前缀;
profileOf()从profile_id或profile取,非法值回退owner; - 前端固定写死
owner(profile_id:'owner'出现在所有调用里); - 因此「多角色」在数据层已就绪,UI 层没有入口。
三、设计 vs 实现的差异(不合并,逐条列)
| 设计(任务书/构想) | 实现现状 | 差距 |
|---|---|---|
| 原始消息与 AI 记忆整理相互独立,整理失败不能丢原文 | ✅ safeArchive 分离;记忆检索失败降级 |
已满足 |
| 高光必须是原话,不能 AI 改写 | ✅ content 必须被原文包含(STRICT_MID) |
已满足 |
| 删除高光不删原始消息 | ✅ 只删 hl + 级联关系 | 已满足 |
| 派生记忆必须有来源 | ✅ source_mids 必须全部存在 |
已满足 |
| 不确定信息不得当事实 | 🟡 candidate 单列 + 提示词标注;但没有审核流转 |
无「确认/驳回」动作 |
| 记忆可更新/合并 | ❌ 只能新增与删除;没有 update/merge | 与 Mem0 四态差距最大 |
| 记忆变更历史 | ❌ 无 history 表 | 建议 1 未实施 |
| 双时间戳(事件时间/记录时间) | 🟡 经历有 event_time + time_certainty,消息只有 created_at |
不完整 |
| Token 预算 | ❌ 按条数截断(5/3/3/2),无 token 计数 | 建议 5 未实施 |
| 记忆异步整理(不阻塞聊天) | ❌ 同步写两份 | 建议 4 未实施 |
| 向量检索 | ❌ 关键词子串匹配 | 明确非第一阶段必要 |
| 跨设备收藏 | ❌ 收藏只在本机 | 高光可跨设备 |
四、成本与配额影响(记忆系统的真实代价)
| 操作 | KV 读 | KV 写 | list |
|---|---|---|---|
| 一条聊天(冷缓存) | 约 300(mem 最近 300 条)+ 若干 | 5–6 | 4–5 |
| 一条聊天(5 分钟内热缓存) | 约 300 | 5–6 | 0–1 |
| 一次「彻底删除」 | 与 hl/exp/rel 数量成正比 | 2 + 引用数 | 2(hl/exp 原始 list) |
| 一次全量导出 | 与总记录数成正比 | 0 | 每 200 键 1 次 |
| 一次全量导入 | 与总记录数成正比(含指纹预扫) | 与新增数成正比 | 2–3 |
免费版上限(账户级/天):list 1000、写 1000、读 10 万。 → 约 160 条聊天/天 会打满写配额;333 条/天 会打满读配额;密集同步/搬迁是 list 的主要杀手。 2026-10-25 的 500 故障就是 list 1000 次被烧穿(见 07 事件 06)。
五、必须保留的东西(接手时不要动)
mem:/msg:/hl:/exp:/rel:/tomb:六类键的命名与前缀语义(有存量数据)。STRICT_MID与MEM_READ_LEGACY两个开关的默认值(1/1)与含义。content_hash = strHash(profile + '|' + content)的去重算法(改了会重复导入)。- 墓碑 400 天 TTL 与「导入前查墓碑」的检查顺序。
- 高光「内容必须被原文包含」的校验(这是防「AI 改写冒充原话」的核心)。
profile默认owner与校验正则。
六、最需要先决定的三件事
- 检索数据源统一:继续双写
mem:+msg:,还是让检索改读msg:(顺手解决 P0-2)? - 历史 279 条软删怎么处理:硬删+墓碑(要分批、耗写配额)还是保留但标记?
- 记忆要不要升级到 v4(更新/合并/history/双时间戳):这决定阶段 6「成长分析」有没有地基。
06 · 决策记录与版本变化
06 · 决策记录与版本变化
分类严格区分: 【用户确认】 = 用户明确说「可以/同意/就这么做」或直接下达; 【用户提出】 = 用户提出方向但未最终拍板; 【AI 建议】 = 我提出、用户没有明确确认; 【已取代】 = 后来的要求覆盖了旧决定(旧记录保留,不静默删除)。
一、决策表
| ID | 时间 | 问题 | 用户原话/要求 | 最终决定 | 依据/原因 | 状态 |
|---|---|---|---|---|---|---|
| D-01 | 10-05 | 模型 Key 放哪 | (AskUserQuestion 选择) | Worker 统一转发,Key 不进前端直连 | 安全 | 【用户确认】仍有效 |
| D-02 | 10-05 | 检索方式 | (选择) | 关键词匹配,不上向量库 | 零成本、够用 | 【用户确认】仍有效 |
| D-03 | 10-05 | 主动消息触发 | (选择) | 页面打开/回前台时检查,不用 Cron | 无需定时任务 | 【用户确认】仍有效 |
| D-04 | 10-05 | 是否重构 | 「绝对不准重构我的代码结构,必须保留现有布局」 | 只做增量补丁,按区域改 | 用户在手机上操作,怕失控 | 【用户确认】仍有效,最高优先级 |
| D-05 | 10-19 | 默认人设 | 要求取消「小喵」默认人设 | 只用自定义人设;无则明确告知模型「未设定」 | 用户不想被内置角色绑定 | 【用户确认】仍有效 |
| D-06 | 10-17 | 深色模式 | 要求删掉入口 | 移除 UI 入口(CSS 变量残留) | 简化 | 【用户确认】仍有效 |
| D-07 | 10-14 | 手绘气泡 | 参照截图做装饰气泡 | 上线后中文乱码 → 回滚 | atob 字节处理错误 |
【已取代】不再尝试 |
| D-08 | 10-24 02:55 | 后端总出错 | 「聊天记录没有就没有了……全部重新部署吧一个全新的」「我的意思是前后端全部都是新的」 | 新建 zhai-v2 + 新 KV chat_memory_v3(空库),旧 zhai/chat_memory_v2 保留不删 |
用户要干净环境;旧库留作退路 | 【用户确认】仍有效 |
| D-09 | 10-24 18:00 | 国内能不能用 | 「暂时先部署到 WorkBuddy 上面,后端那个记录的先不用」 | 国内直连版先不落库,只做代理 | 先验证可用性 | 【已取代】→ D-10 |
| D-10 | 10-25 04:40 | 后端迁哪 | 「理论上……迁移到阿里云?」→ 四方案 → 「你先试一下 C 吧」 | C 方案:WorkBuddy + Node 垫片 + SQLite;先验证持久性 | 成本最低、改动最小 | 【用户确认】仍有效 |
| D-11 | 10-25 04:38 | 极端场景 | 「情况一跟情况二的话,你这两个你都考虑一下,然后做一个版本」 | 双快照三层防护:服务端每日快照(7 份)+ 设备每日快照(3 份)+ 手动导出导入 | 防平台清盘/手滑/换设备 | 【用户确认】已实现 |
| D-12 | 10-25 19:00 | 云端删除是假的 | 「我发现这个云端删除它不能真的云端删除」→ 选 A 根治 | 物理删除 + 墓碑防复活 | 兑现「本机和云端都删」的 UI 承诺 | 【用户确认】已实现 |
| D-13 | 10-25 18:32 | 灰度顺序 | 「这个改进的你先做那个 V2 的……万一跑不通怎么办?」 | v2 先行验收,再 v1,再国内版 | 怕一次全改跑不通 | 【用户确认】已执行(v2→v1;国内版随代码自动生效) |
| D-14 | 10-25 22:32 | 500 是不是额度问题 | 「意思是…额度没了?你确定么」 | 复核证据:两库 list 同报 10048、读正常 → 确认是账户级 list 配额;修复=缓存+降级+干净 503 |
用户要求对质证据 | 【用户质疑→证据确认】已实现并部署 |
| D-15 | 10-25 23:23 | 审查范围 | 「立刻停止所有修改动作!进入只读审查模式」 | 只读审查三端,出报告,不改码 | 用户要决策依据 | 【用户确认】已执行 |
| D-16 | 10-26/27 | 交接 | 要求整理全部历史,给新 AI | 生成 10 份交接档案 | 换 AI 接手 | 【用户确认】本次执行 |
二、AI 提出但用户没有确认的建议(不要当成已定)
| ID | 建议 | 状态 | 备注 |
|---|---|---|---|
| S-01 | 图片存 R2(云端保留原图) | 未确认 | 涉及付费;当前图片只在本机 |
| S-02 | 用 Cloudflare Access / 登录态替换「令牌随页面下发」 | 未确认 | P0-3 的推荐解 |
| S-03 | 记忆框架升级(Mem0 四态 + history / Graphiti 双时间戳 / Letta 预算) | 未确认 | 只有调研报告 |
| S-04 | 检索改读 msg:、停写 mem:(解决删除不彻底) |
未确认 | P0-2 的方案 A |
| S-05 | 历史 279 条软删分批硬删 + 墓碑 | 未确认 | 需写配额预算 |
| S-06 | 轮换 v2 的 AUTH_TOKEN(因明文写在 index.html) |
未确认 | 换令牌后页面自动注入,无需改前端 |
| S-07 | 统一备份口径(v1/v2 也支持 schema:2 导入) | 未确认 | P1-4 |
| S-08 | 删除设置里的「Worker 地址」输入(永远同源) | 未确认 | P0-1 的方案 B |
| S-09 | 把 deploy-web/worker.mjs 改成构建时自动复制 |
未确认 | P2-2 |
| S-10 | 阿里云 ECS / 国内 Serverless | 未采用 | 用户选 C |
三、版本与线上变化(可核对)
| 时间 | 版本/提交 | 变化 | 影响 |
|---|---|---|---|
| 10-05 | 基线 afa0aa4 |
初版全栈 | — |
| 10-14 | 手绘气泡 → 回滚 | 乱码事故 | 用户看到「乱码」 |
| 10-16~17 | 08815a9、7702738 等 |
UI v2、PWA、安全 v2 | 46 项安全断言 |
| 10-19 | 9505597…b03a612 |
P0-1~P0-4 + P1 | 145 项断言 |
| 10-22 | f02cc07 / 线上 51ac31d3 |
iOS 键盘最终版 | 用户确认「终于行了」 |
| 10-23 | 9ed08a6 / 线上 0114d69f |
删除+输入框修复 | — |
| 10-24 02:59 | 867faee |
新建 v2(zhai-v2 + chat_memory_v3) |
空库 |
| 10-24 18:32~19:11 | c007087、be64807、bd6a5af |
国内版三连修 | sharecode/默认地址/人设 |
| 10-25 04:33~05:37 | aa3998b…a32fe2e |
C 方案 1–3 批 | SqliteKV、垫片、快照 |
| 10-25 09:01 | 5f61f81 |
第 4 批搬迁 606 条 | 国内版有数据 |
| 10-25 13:33~15:31 | b885e6e、0f1007c、dfeff1d、32aa8a5 |
同源默认、403 修复、v1 令牌注入 | v1 401 修复 |
| 10-25 18:48 | 83bc573 |
硬删+墓碑(先 v2 65bf6ef0) |
删除语义变更 |
| 10-25 20:36 | 5666a72 |
配额修复 | 缓存+降级 |
| 10-25 20:38 | v2 8c6fe98a |
部署配额修复 | — |
| 10-25 22:54 | v1 a3ef2a33 |
部署配额修复 | 用户主入口恢复 |
四、被取代/撤销的旧决定(保留记录)
| 旧决定 | 取代者 | 说明 |
|---|---|---|
| 手绘气泡装饰 | 无 | 乱码后回滚,未再提 |
| 深色模式 | 无 | 用户要求删除入口 |
| 默认人设「小喵」 | 只用自定义人设 | 10-19 |
| 软删即「云端删除」 | 硬删+墓碑 | 10-25 |
| 国内版「不落库」 | C 方案 SQLite | 10-25 |
| 单一 Worker | v1 + v2 + 国内版 | 10-24 起 |
| Pages 静态托管 | 三端自托管页面 | Pages 项目 chat-ui-beib 实为 10-03 的另一原型 |
deploy.sh 手工改产物 |
build.js 整体重生成 |
10-19 |
| 迁移只搬 v2 | 两库合并(v1+v2) | 用户 10-25 指出「搬错了有是 v1 的」 |
五、记录冲突(保留双方,标记待核实)
| 冲突 | A 说法 | B 说法 | 处理 |
|---|---|---|---|
| v1 库在 10-24 是否有数据 | 10-24 02:35 审计:msg:owner 仅 1 条、mem:owner 0 条、11 条旧 mem_* |
10-27 实测:4 个 msg:owner:* 抽样全部命中,含 217 条 local_ 前缀 |
结论:v1 在 10-24 之后被本机历史「云端同步」填充;具体是哪台设备/哪个入口触发待确认 |
| 搬迁数量 | 提交信息「606 条」 | 国内版实际:307 消息 + 299 记忆(312 mem 含后续新增) | 606 = 307+299,口径是「消息+记忆」,不是 606 条消息 |
| 国内版「搬错了」 | 用户:搬的是 v1 的数据 | 迁移工具设计:两库合并 | 事实成立:工具第 45 行同时读 0a4c 与 ec1a,v1 数据被一起带入;是否「错」取决于用户想要哪份 |
| v2 空库 | 我早期判断:搬迁失败 | 用户:全新部署,本来就空 | 以用户为准 |
DEEPSEEK_API_KEY |
wrangler.v1.toml 注释:secret 不随部署变动 | 代码只读 env.API_KEY |
死配置,见 07 P1-1 |
| 「前后端是一套」 | 我早期表述 | 用户:前后端分开且不一致 | 以用户为准:两份真源 + 两套寻址规则 |
07 · 问题风险与失败经验
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;下次复现立即抓 |
08 · 下一阶段执行路线
08 · 下一阶段执行路线
排序原则:先止血 → 再修数据正确性 → 再补部署韧性 → 最后做任务书阶段 5–9。 每个阶段都标注:前置条件 / 要动的文件 / 验收 / 回滚 / 是否需要用户批准。 ⚠️ 本文件只是路线,不代表已经开工。
阶段 0 · 只读调查(无需批准,随时可做)
| # | 事项 | 方法 | 产出 |
|---|---|---|---|
| 0.1 | 确认三端是否仍跑同一份代码 | CF API 拉 zhai/zhai-v2 bundle,与本地 cat-chat-all-in-one.js 比对;国内版 GET / 看 data.notice |
一致性结论 |
| 0.2 | 等 KV list 配额恢复(北京时间 08:00 后),枚举 v1/v2 精确键数 | REST keys 分页(注意:每次调用都消耗配额,分批) |
数据量基线 |
| 0.3 | 核对 v1 的 307 条与国内版 320 条的差集 | 按 mid 对比(导出两份 id 列表) | 哪些只在一边 |
| 0.4 | 查清 v1 数据是何时/哪个入口同步进去的 | msg 的 created_at 分布 + source_id + meta_lastTs 历史 |
数据来源结论 |
| 0.5 | 复核 279 条软删的内容与时间分布 | 只读 SQL / KV 抽样 | 清理清单 |
不做:不写库、不删键、不部署。
阶段 A · 止血(1 次部署,约 30 分钟)
| 项 | 内容 |
|---|---|
| 目标 | 修掉「一改就全坏」的低级陷阱,不改业务语义 |
| 前置 | 无 |
| 文件 | worker.js(json() 增加 request/env 参数并合并 corsHeaders;validateApiUrl 保留 query;env.API_KEY \|\| env.DEEPSEEK_API_KEY;serveFrontend 桩)、index.html(无需改) |
| 具体 | ① P1-5 CORS ② P1-1 Key 兼容 ③ P2-5 query ④ P2-13 兜底函数 |
| 验收 | 跨源 OPTIONS + POST 都有 ACAO;?a=1 的 API URL 不被截断;先 node build.js 再部署;/health 200;本地 10 个测试全过 |
| 回滚 | wrangler rollback <上一版本>;v1 a3ef2a33、v2 8c6fe98a |
| 批准 | 需要(涉及部署) |
阶段 B · 数据正确性(本阶段最关键,必须先做决策)
B0 先决策(用户拍板,二选一)
| 选项 | 含义 | 代价 |
|---|---|---|
| B-方案 1(推荐) | 检索数据源统一到 msg:,mem: 停写、只读兼容 |
改动中等;需一次性对齐存量;删除天然彻底 |
| B-方案 2(保守) | 保留双写,硬删时扫 mem: 找 mid 删除 |
改动小;每次删除多 1 次 list;库越大越慢 |
B1 修 P0-2(彻底删除)
- 前置:B0 决策;KV 配额充足(方案 2 需要批量写)
- 文件:
worker.js(saveMemory增加mid/retrieveMemory改数据源 /hardDeleteRefs增补) - 验收:删一条 →
mem与msg都查不到该内容 → 再聊相关话题不出现;/export/full的memories不含;测试补 3 条断言 - 回滚:保留旧
mem:不删(只改读路径),可一键切回 - 批准:需要
B2 修 P0-1(前后端寻址统一)
- 前置:确认「多后端」是否真实需求
- 文件:
index.html(apiBase()+ 8 处调用点)、worker.js(无需) - 验收:把设置里的地址改成另一个域 → Network 面板可见所有 API 都跟着走;或按方案 B 直接禁用输入
- 回滚:纯前端改动,回滚页面即可
- 批准:需要(会影响用户习惯)
B3 修 P1-2(检索 1000 键截断)
- 前置:B0 决策(若改读
msg:,本条随之解决一半) - 文件:
worker.js(listMemoryKeys/retrieveMemory) - 方案:按月分片
mem:<p>:<YYYYMM>:或维护mem_recent索引键 - 验收:造 1200 条记忆,确认最新 300 条可被检索
- 回滚:双读(新分片 + 旧前缀)一个版本,观察后再切
- 批准:需要
阶段 C · 部署韧性(国内版优先)
| # | 事项 | 前置 | 文件 | 验收 |
|---|---|---|---|---|
| C1 | P1-3 数据落持久盘 + 快照异地 | 平台是否有持久卷 | server.js、start-domestic.sh |
重启容器后 kv.count() 不变;快照可下载 |
| C2 | P1-3 同源基准兜底 | 无 | server.js(PUBLIC_ORIGIN 读取顺序 + 403 中文页) |
换域名访问不再「发送失败」,而是明确提示 |
| C3 | P1-4 备份口径统一 | 无 | index.html(去掉 isWebMode 限制)、worker.js(无需) |
v1/v2 也能导出/导入 schema:2 |
| C4 | P1-7 分批同步 | 无 | index.html(cloudSyncToServer 分批 + lastSyncTs) |
1000 条同步不一次打满写配额 |
| C5 | P1-6 图片策略(先决策) | 用户决定:接受 / R2 / 缩略图 | index.html 文案;R2 需 worker.js + 绑定 |
用户明确知道「图不同步」或图可恢复 |
阶段 D · 访问控制(P0-3)
| 选项 | 工作量 | 说明 |
|---|---|---|
| D-A Cloudflare Access(推荐) | 小 | 在 CF 面板给 zhai/zhai-v2 加策略,只允许指定邮箱;国内版视平台能力 |
| D-B 自建口令登录 | 中 | POST /login → 签名 HttpOnly cookie;页面不再注入 token;三端同款 |
| D-C 最低成本 | 小 | 页面不再明文注入 token,改「首次输入访问码」存 localStorage;GET / 也只返回壳页 |
- 验收:未登录访问 API 一律 401/302;登录后功能不变
- 回滚:Access 可在面板一键关闭;自建登录保留旧 token 分支一个版本
- 批准:需要(会影响所有设备)
阶段 E · 历史数据清理(279 条软删)
| 项 | 内容 |
|---|---|
| 前置 | 阶段 A 完成;配额充足;先导出全量备份 |
| 步骤 | ① 导出 /export/full 存档 ② 生成待删清单(mid)③ 每批 ≤200 条:POST /messages/delete {mode:'hard', force:true} ④ 每批后抽查墓碑与残留 ⑤ 记录每批结果 |
| 配额 | 每条硬删 ≈ 2 写 + 1–2 list;200 条约 400–600 写 → 一天最多 1 批 |
| 验收 | tomb: 数量 = 279;msg: 剩 41 条;/export/full 的 counts 与预期一致 |
| 回滚 | 备份可 /import/full 恢复(但墓碑会阻止恢复被删项——这正是设计目的;如需恢复须先删对应墓碑) |
| 批准 | 需要(不可逆) |
阶段 F · 任务书阶段 5–9
| 阶段 | 先决条件 | 主要产出 | 验收要点 |
|---|---|---|---|
| 5 人设版本 | 决定人设存储结构(KV persona:<p>:<ver> + 当前指针) |
版本列表/对比/回滚;聊天只用「当前版本」 | 核心人设不被单次聊天覆盖;改动留版本 |
| 6 成长分析 | 阶段 5 完成;记忆可更新(v4 能力) | 定时任务(CF Cron 或国内 cron)+ 候选建议 + 审核队列 | 不在每轮聊天跑;失败可重试;建议带证据/反例;未审核不生效 |
| 7 后台管理 | 阶段 D 完成(强认证) | 档案/记忆/人设/任务/备份的移动端管理页 | 有鉴权、有操作日志、关键操作二次确认 |
| 8 沈屿迁移 | 阶段 5 + 7;旧数据已整理 | 导入真实历史/人设/记忆;角色隔离 | 数量与内容核验;记忆可追溯来源;备份恢复实测 |
| 9 测试清理 | 阶段 8 验收通过 | 删测试 profile 数据与测试专用资源 | 正式数据完整;测试数据不参与检索;通用代码保留 |
给新 AI 的开工建议(按顺序)
- 先读:
README_交接索引.md→ 01 → 04 → 07(先知道哪里有坑)→ 03 → 05。 - 先只读核对:阶段 0 的 5 项,不要一上来改代码。
- 第一批改代码只做阶段 A(4 个小修),一次部署,观察 1 天。
- 等用户对 B0 拍板再做 P0-2/P0-1/P1-2;这三项会动检索与前端寻址,必须一起设计。
- 不要在没得到批准时:部署、清理数据、改密钥、动 v1 历史库。
- 任何批量操作都按「≤200 条/批、跨天、先备份」执行。
明确不建议现在做的事
| 事 | 原因 |
|---|---|
| 大规模重构 / 换框架 | 违反用户第一约束;现有三端在跑 |
直接把 mem: 全删 |
会丢历史记忆;先改读路径,再决定是否清 |
| 立刻上向量检索 / Mem0 | 成本与复杂度高;先把删除与检索正确性修好 |
| 把 v1 数据整体覆盖到 v2 | 用户没要求;v2 是「全新环境」的定位 |
| 用 R2 存图 | 涉及付费,未获授权 |
09 · 缺失资料与待确认
09 · 缺失资料与待确认事项
真实性规则:只写实际访问过的;「知道存在但没读」和「无法访问」分开列;不假装拥有完整历史。
一、我实际读过 / 核对过的资料
| 类别 | 资料 | 读取程度 |
|---|---|---|
| 对话历史 | 本会话全部消息 + 30 个阶段压缩摘要(2026-10-05 ~ 10-25) | 摘要逐段读过;更早的逐字对话只存在于摘要中 |
| 原始需求 | /root/uploads/1792388616892580314-文本.txt(任务书 v1.0,27KB) |
读过背景、12 项目标、阶段 0–9、约束条款 |
| 原始源码 | /root/uploads/1791183407093837939-0643203b2611b966_v6.html(56KB) |
只读了摘要中记录的改造方式,未逐行重读 |
| 代码 | worker.js(2801 行)全文 |
逐段读完 |
| 代码 | index.html(12509 行) |
读了配置区、请求层、发送、删除、同步、记忆库、启动等全部逻辑区;CSS 未逐行 |
| 代码 | deploy-web/server.js、kv-sqlite.js、build.js、两份 wrangler 配置、start-domestic.sh |
全文 |
| 代码 | deploy-web/worker.mjs |
与 bundle 逐字节比对(相同) |
| 文档 | 小手机-完整交付文档.md(605 行) |
全文 |
| 文档 | 任务交接说明.md(254 行) |
全文 |
| 文档 | 模块索引.md、安全说明.md、修复记录-P0-P1.md、国内版落库迁移方案.md |
读了章节结构与关键节 |
| 文档 | 小手机-全新后端环境说明.md |
全文 |
| 文档 | /workspace/小手机项目-全项目代码基线审查报告.md(40KB) |
读了摘要与目录级信息 |
| 数据 | 国内版 SQLite(只读) | 键分布、时间分布、mid 前缀、软删统计 |
| 数据 | v1/v2 KV(抽样读) | 4 个消息键 + meta_lastTs;无法枚举 |
| 线上 | Cloudflare API:worker 列表、secret 名单、线上 bundle | 已核对 v1/v2 版本与代码内容 |
| 线上 | 国内版 GET /、/health |
已核对注入令牌与页面版本 |
二、知道存在、但没有读全的资料
| 资料 | 大小 | 为什么没读 |
|---|---|---|
/workspace/chat/猫爪聊天-完整项目.md |
304KB | 历史融合版全代码(占位符版),与当前代码重复 |
/workspace/chat/handoff-public.md |
40KB | 旧交接公开版,内容被 10-22 交付文档覆盖 |
/workspace/小手机-交接-公开版.md |
40KB | 同上 |
/workspace/chat/替换指南.md、给其他AI-代码包/、给其他AI-完整代码包.zip |
— | 10-14 时期的历史交接材料 |
/workspace/小手机-审计草案/01–04*.md |
37KB | 只读了目录;内容已并入 P0/P1 修复记录 |
| 17 张界面截图 PNG | — | 视觉参考,不影响状态判断 |
/workspace/小手机交接/index.html(62KB)+ build_site.py |
— | 10-22 的「交接网页」构建产物 |
tools/migrate_v2_to_domestic.mjs 全文 |
— | 只读了关键行(双命名空间、/import/full) |
tests/ 10 个文件全文 |
— | 读了每个文件的头部说明与断言数量 |
三、当前无法访问的资料
| 资料 | 原因 | 影响 |
|---|---|---|
| GitHub 远端实时状态 | 沙箱访问 GitHub 受限 | 无法确认远端是否与本地 5666a72 一致 |
Cloudflare 线上日志(wrangler tail / Logpush) |
网络无法建 WebSocket | 无法取 500 的原始异常文本 |
| v1/v2 KV 的精确键列表 | 免费版 list 配额耗尽(10048) |
只能用抽样与迁移结果推断总量 |
| Secret 明文 | CF API 不返回 | 只能确认名称,无法核对令牌值 |
| 用户浏览器 IndexedDB / localStorage | 在用户设备上 | 无法确认本机历史是否比云端更全 |
| 平台(WorkBuddy)控制台与持久化策略 | 无访问权限 | /tmp 是否跨容器重建持久,只能靠实测推断 |
| 早期逐字对话(10-05 ~ 10-22) | 已被压缩成摘要 | 个别措辞与细节可能丢失;本档案以摘要为准并标注 |
| 聊天中出现过的截图(COS 链接) | 部分链接已过期/需鉴权 | 无法复核当时的视觉问题 |
四、需要用户补充的材料
- 沈屿本体的资料(阶段 8 用):历史聊天导出、人设原文、重要经历清单——目前一份都没有。
- 你手机上当前的本机聊天记录是否比云端多?(决定「云端 vs 本机」谁是主副本)
- 10-24 之后你是否在 v1 页面点过「云端同步」?(决定 v1 那 307 条的来源解释)
- 国内版平台的 持久化承诺(是否会重建容器/清
/tmp)与可用存储路径。 - 你是否还需要 Pages 那个入口(当前三端都不用)。
- 图片:接受「只在本机」,还是愿意为 R2 付费?
五、需要你确认的决定(按影响排序)
| # | 问题 | 选项 | 我的建议 |
|---|---|---|---|
| Q1 | 历史 279 条软删怎么办 | A 分批硬删+墓碑 / B 保留但标记 / C 先导出再删 | A(先导出) |
| Q2 | 检索数据源 | A 统一到 msg:(推荐)/ B 保留双写+删除时扫 mem: |
A |
| Q3 | 「Worker 地址」设置 | A 保留但全接口跟随 / B 删掉,永远同源 | B(最少踩坑) |
| Q4 | 访问控制 | A Cloudflare Access / B 自建登录 / C 暂不处理 | A |
| Q5 | 主数据源 | A v1 / B 国内版 / C 双主(需同步) | 先定一个主,另一个只做备份 |
| Q6 | 图片 | A 接受本机 / B R2 / C 只存缩略图 | 先 A,明确告知 |
| Q7 | 是否继续任务书阶段 5–9 | 继续 / 暂停 | 先修 P0,再谈阶段 5 |
六、安全事项(不含任何密钥值)
| 事项 | 现状 | 建议 |
|---|---|---|
| Cloudflare API Token 曾写在文档里 | 任务交接说明.md 第 133 行有明文 token(早期文档) |
轮换该 token;文档外发前删除该行 |
v2 AUTH_TOKEN 明文在 index.html 第 10 行 |
已提交进 git 历史 | 轮换 v2 令牌;页面改占位符;必要时清 git 历史 |
| 用户在对话中贴过模型 API Key | 用户已表示会吊销 | 确认已吊销;不要写进任何文档 |
平台 sharecode 写死在 server.js |
属平台访问凭据 | 视作敏感;换域名时同步更换 |
| 页面公开下发令牌 | 三端一致 | 见 Q4 |
wrangler.toml 含账号 ID / KV ID |
仅本机 | 外发用占位符版 |
本档案及同目录其余 9 个文件均不含上述任何真实值。
七、资料之间的冲突(保留双方,待核实)
| 冲突点 | 说法 A | 说法 B | 状态 |
|---|---|---|---|
| v2 为何是空库 | 我 10-25 判断「搬迁失败」 | 用户:「我让你重新部署的一个完全全新的」 | 以 B 为准;A 是误判 |
| 606 条是什么 | 提交信息「606 条」 | 307 消息 + 299 记忆 | 口径不同,非冲突 |
| 国内版数据来源 | 用户:「搬错了有是 v1 的」 | 工具设计:两库合并 | 事实是合并;是否算「错」取决于目标 |
| 前端后端是否一套 | 我早期表述「一体」 | 用户:「单独分开的,也是不一致的」 | 以 B 为准 |
| v1 库 10-24 是否有数据 | 审计:几乎为空 | 10-27 实测:307 条在其中 | 中间发生了本机同步;触发者待确认 |
DEEPSEEK_API_KEY 是否生效 |
配置注释暗示生效 | 代码只读 env.API_KEY |
以代码为准:不生效 |
八、如果只能问三个问题
- 主数据放哪:v1 还是国内版?(决定后面所有同步/清理的方向)
- 历史那 279 条删不删?(不可逆,必须先定)
- 要不要现在做访问控制?(P0-3,越晚风险越大)
其余都可以边做边定。
三端代码与风险审查(2026-10-25)
猫爪聊天 · 项目状态与风险审查报告
审查性质:只读审查(未修改任何一行代码、未改任何配置、未部署) 审查时间:2026-10-25 23:30(北京时间) 审查范围:v1(worker
zhai)、v2(workerzhai-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(图片不入云) | 本机主副本 |
关键事实(只读查询得出):
- 国内版 320 条消息中,279 条是软删状态(删除时间集中在 10-23 22 点 68 条、10-24 13 点 211 条),真正有效的只有 41 条;这些软删来自 v1 的历史操作,导入时原样保留。
- 国内版库里 没有任何墓碑键(tomb:)= 0:说明今天上线的「彻底删除 + 墓碑」还没在这个库跑过。
- 图片只存
[图片]占位 + 字节数,真实图片只在本机浏览器里,换设备/清缓存即永久丢失。
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 地址」只对聊天生效,记忆/归档/删除永远打当前页面后端 → 一个设置就能把数据劈成两半
- 位置:
index.html:5206-5225(CONFIG.WORKER_URL)、7183(聊天用它);8079 / 8537 / 8903 / 9023 / 9549 / 9563 / 9577 / 9128(其余接口全用相对路径) - 现象:设置页允许填「Worker 地址」。一旦填写(哪怕是从 v2 页面填 v1 的地址),会出现:
- 聊天 → 发到填的地址(比如 v1)
- 记忆库 / 云端存档 / 恢复 / 彻底删除 / 收藏入库 → 仍打到页面所在后端(比如 v2)
- 结果:AI 的上下文记忆在 A 库,用户在记忆库里看到的和删掉的是 B 库;「删除了但 AI 还记得」正是这种分裂的典型表现
- 影响:数据一致性彻底破坏,且用户无法从 UI 看出发生了分裂
- 补充证据:v1 页面的
WORKER_URL默认取location.origin(同源),本身没问题;问题在可覆盖且只覆盖一半接口
P0-2 「彻底删除」不彻底:只删 msg:/hl:/exp:/rel:,不删 mem:<profile>:* —— 而聊天检索读的正是 mem:
- 位置:
worker.js:2409-2416(硬删只 deletemsgKey+ 墓碑)、128-164(saveMemory 写mem:<p>:<ts>_<rand>)、775-826(retrieveMemory 读 mem 键) - 现象:每条消息在云端存两份(
mem:记忆碎片 +msg:结构化归档)。彻底删除只清msg:与引用它的高光/经历/关系;mem:那份原封不动,而 AI 检索上下文时读的就是mem:。所以:用户删掉的话,下一轮聊天仍会被检索进提示词。 - 影响:功能承诺(「本机和云端都删,不可恢复」)不成立;隐私风险
- 验证方式:删一条刚发的消息,再聊一句相关话题,看是否仍被回忆(或直接查 KV 里该条
mem:是否还在)
P0-3 任何人拿到三端任一 URL,即获得全部数据权限(页面公开 + 令牌随页面下发)
- 位置:
worker.js:1501-1531(pageResponse 无鉴权,向任何 GET / 返回带令牌的页面)、2487-2489(GET 一律返页面)、deploy-web/server.js:382-384(同样公开) - 现象:访问
/不需要任何凭据,返回的 HTML 里就写着window.__CAT_TOKEN__ = "…";拿到它即可调用/archive、/export/full、/messages/delete,读走或删光全部记忆 - 影响:目前只靠「URL 没人知道」这一层遮蔽;一旦链接被转发/截图/出现在浏览器同步记录里,等于全库公开
- 说明:这不是本次改动引入的,是「个人单用户应用 + 无登录」的固有设计;但既然现在数据里有真实聊天,必须正视
P1(7 项)
P1-1 v1 的模型 Key secret 名字不对,代码永远读不到;v2 完全没有模型 Key
- 位置:
worker.js:985(opts.apiKey || env.API_KEY);线上 secret 名单:v1=DEEPSEEK_API_KEY,v2=AUTH_TOKEN - 现象:v1 里那个
DEEPSEEK_API_KEY是死配置(代码只认env.API_KEY)。目前聊天能通,纯粹因为前端每次都自带 Key;一旦前端没填(换设备、清缓存、新装),后端没有兜底 → 400「未配置 API Key」 - 影响:换设备首聊必失败;服务端兜底能力名存实亡
P1-2 记忆检索只扫「最旧的 1000 个」key —— 超过 1000 条后新记忆永远检索不到
- 位置:
worker.js:313-336(listNamesByPrefix的out.length < max截断)、725-747(listMemoryKeys(max=1000))、775-781(排序后取最近 300) - 现象:KV
list按 key 字典序升序返回,mem:owner:<13 位时间戳>_xxx的字典序≈时间序,所以先拿到的 1000 个是最旧的 1000 个;再按时间倒排取 300,得到的是「最旧 1000 条里最新的 300 条」。当前 v1 约 300 条记忆还没触发,长到 1000 条后,新说的话会静默不再被检索 - 影响:长期使用后 AI 会「失忆但自己不知道」;无任何报错
P1-3 国内版数据全在 /tmp,且同源基准写死 —— 容器重建即全丢;域名一变全部 POST 403
- 位置:
server.js:27-33(PUBLIC_ORIGIN写死、DATA_DIR=/tmp/cat-chat-data)、start-domestic.sh(同款默认值)、bridge()342-368 - 现象 a:
chat.db与 7 份快照都在/tmp。平台重建容器/清盘 → 库和快照一起消失(前端本机快照是最后一道,但它依赖浏览器 IndexedDB 没被清) - 现象 b:
isSameOrigin用PUBLIC_ORIGIN与浏览器Origin比对。平台换了访问域名(或你从别的域名打开),所有 POST 一律 403forbidden_origin——日志里已出现过{"ev":"forbidden_origin","path":"/chat"}(也可能来自此前的跨源测试,但机制成立) - 影响:一次平台变更 = 全库丢失 + 页面「发送失败」且原因不可见
P1-4 备份/恢复能力三端不一致:v1/v2 只能「导出本机」,云端全量档案只有国内版能导入
- 位置:
index.html:8531(if(isWebMode())才导云端)、8741-8745(restoreCloudFullFile非 web 模式直接拒绝) - 现象:同一个「备份」按钮,在国内版导的是云端全量(消息+高光+经历+关系+记忆),在 v1/v2 导的是浏览器本机数据;而
schema:2的云端档案在 v1/v2 无法导入 - 影响:换设备/换端时,v1/v2 用户拿不到完整备份;同一份文件在不同端行为不同
P1-5 跨源响应缺少 Access-Control-Allow-Origin → 任何「页面在 A、后端在 B」的组合都会发送失败
- 位置:
worker.js:66-74(json()只带Vary: Origin)、1287-1300(corsHeaders只在 OPTIONS 用) - 现象:预检 OPTIONS 返回了 ACAO,真正响应却没有 → 浏览器把响应拦掉,前端只看到「网络错误/发送失败」
- 影响:直接堵死「国内版页面 + CF 后端」「Pages/自定义域名 + worker」这类组合;也和 P0-1 的「切后端」互相引爆
P1-6 图片不入云:只有 [图片] 占位,图只存在发送它的那台浏览器
- 位置:
worker.js:2596-2609(归档只写message_type:'image'+attachment.bytes)、index.html:6696-6723(渲染读本机 base64) - 现象:换设备、清缓存、重装 → 历史里的图片全部变成空白/丢失;备份 JSON 里也没有图
- 影响:用户会以为「云端有备份」,实际图片没有任何备份
P1-7 首次「云端同步 / 恢复」是 O(n) 写入,1000 条就能一次打满免费版日配额
- 位置:
index.html:8874-8933(一次 POST 全部本地消息)、worker.js:1785-1815(逐条 get+put) - 现象:首次同步 1000 条 = 1000 次 get + 1000 次 put;免费版写配额 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 收敛访问权限(三选一,按你的使用场景定)
- 方案 A(最省事,推荐):给 v1/v2 套 Cloudflare Access(Zero Trust,免费档够用),只允许你的邮箱/设备;国内版在平台侧加访问控制(若有)。
- 方案 B(自建):加口令登录:
POST /login校验口令 → 下发HttpOnly; Secure; SameSite=Lax的签名 cookie;页面不再注入 token,改为所有 API 校验 cookie。工作量中等,但三端都能用(国内版同款逻辑)。 - 方案 C(最低成本缓解):页面不再明文注入 token,改为「首次访问输入访问码 → 存 localStorage」;同时把 worker 的 GET / 也要求带码(否则返回一个只有输入框的壳页)。能挡「随手点开链接的人」,挡不住有心人。
P1-1 修 Key
- v1:
wrangler secret put API_KEY --config wrangler.v1.toml(值 = 你的 DeepSeek Key),或把代码改成opts.apiKey || env.API_KEY || env.DEEPSEEK_API_KEY(更稳,兼容现状)。 - v2:同样加
API_KEY。 - 前端:当
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 国内版持久化与同源
- 数据目录改到平台持久卷(若有);若无,增加「每日把快照 base64 POST 到 v2 的
/import/full」做异地副本(注意配额,可每周一次)。 PUBLIC_ORIGIN改为:优先环境变量 → 其次读平台注入的公开域名 → 兜底现值;isSameOrigin增加「同 host 的任意路径/端口」放行。- 403 时返回中文可读页:「当前访问域名与后端配置不一致,请用 xxx 访问」,而不是让前端显示「发送失败」。
P1-4 统一备份口径
- 去掉
exportBackup的isWebMode()限制:v1/v2 也调/export/full(失败再退回本机导出)。 - 去掉
restoreCloudFullFile的isWebMode()限制(/import/full三端都支持)。 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)。这三件事不解决,后面每一次「修好了」都会在换端/换设备/换域名时重新裂开。
全项目代码基线审查(2026-10-23)
小手机项目 · 全项目代码基线审查报告(只读版)
审查对象:
beibbloveme-crypto/yuai(私有仓库,未认证 GitHub API 返回 404 证实非公开) 审查基线:分支fix/p0-memory-consistency,HEAD =f02cc07(2026-10-22 19:19:35 +0800) 生产版本:51ac31d3-98db-4740-8b8e-f5de09fd7d43(2026-10-22 19:19:43 +0800,经 wrangler API 只读核验,与 HEAD 内容吻合) 审查性质:只读。未修改任何代码、未提交、未部署、未触碰线上数据。所有测试均在本地内存 Mock KV + Mock 模型 + 虚构令牌下运行。 状态标注体系:【已验证完成】【部分完成】【未实现】【存在缺陷】【无法确认】
一、项目基线总览
1.1 仓库与分支状态
| 项 | 值 | 证据 |
|---|---|---|
| 远端 | github.com/beibbloveme-crypto/yuai(私有) |
git remote -v;未认证 api.github.com 返回 404 |
| 当前分支 | fix/p0-memory-consistency |
git branch |
| HEAD | f02cc07「fix(ui): 去掉自加的抽搐源」 |
git log -1 |
| main 分支 HEAD | 75657e0(阶段4 文档提交) |
git log main -1 |
| 本地/远端一致性 | 一致(fetch 后无差异) | 前期 git fetch 核验 |
【存在缺陷】分支割裂:P0/P1 六个修复提交(9505597 记忆隔离、513f24b 稳定 mid、d4aa34e 删除接口、2460f9a 全量导出导入、5bfe8c8 上下文组装、5b01441 构建修复)只存在于 fix 分支,main 分支不包含(git merge-base --is-ancestor 9505597 main 判定为否)。生产环境实际运行 fix 分支代码,main 已落后生产 15 个以上提交。任何从 main 发起的部署都会丢失全部记忆一致性修复。
1.2 文件清单与职责
| 文件 | 大小 | 职责 | 性质 |
|---|---|---|---|
index.html |
208 KB(约 11560 行) | 前端唯一真源 | 源文件 |
worker.js |
138 KB(2510 行) | 后端唯一真源 | 源文件 |
cat-chat-all-in-one.js |
417 KB | 部署产物 = worker.js + base64 内联前端 + serveFrontend() |
构建产物 |
build.js |
2 KB | 由两个真源重新生成部署产物 | 构建脚本 |
deploy.sh |
808 B | node build.js → node --check → npx wrangler deploy |
部署脚本 |
wrangler.toml |
1.4 KB | Worker 配置 | 配置 |
tests/(7 个 .mjs) |
— | P0/P1/阶段4 回归测试 | 测试 |
| 9 个 .md 文档 + 12 张截图 | — | 交接/安全/审计文档 | 文档 |
构建漂移核验【已验证完成】:在 /tmp/buildcheck 用仓库内的 build.js 对当前 worker.js + index.html 重新构建,产物与仓库中的 cat-chat-all-in-one.js 逐字节一致(diff 无输出)。不存在"只换前端、后端漂移"问题。
1.3 部署配置(wrangler.toml)
name = "zhai"
account_id = "d285d56ff8e27fbe825c552b7a86cc65"
compatibility_date = "2024-09-23"
KV binding: CHAT → ec1a5feca1f446a28c6bb12b5ed7accc
vars: API_URL=https://api.openai.com/v1/chat/completions
MODEL=gpt-4o-mini
ALLOWED_API_HOSTS=""
ALLOW_CUSTOM_API="0"
ALLOWED_ORIGINS=""
线上 secrets(wrangler secret list 只读查询):仅 DEEPSEEK_API_KEY 一项。
【存在缺陷】死配置:callModel(worker.js L851)读取的是 env.API_KEY,全仓库无任何代码引用 DEEPSEEK_API_KEY(grep 证实)。即:线上配置的模型密钥永远不会被使用,实际调用模型完全依赖前端请求体里传来的 apiKey;未传时返回 missing_api_key 400。文档"API_KEY 不强制:前端自选填入即可"(L29 注释)与代码行为一致,但 secret 命名与代码读取名不一致属于配置事故。
无 AUTH_TOKEN secret:getToken(L1193-1195)回退到源码内硬编码的 DEFAULT_AUTH_TOKEN = '518effb141664b32940bf691cf65e506'(L1100)。详见第七部分安全分析。
1.4 文档与代码差异比对
| 文档 | 声明 | 实际 | 判定 |
|---|---|---|---|
安全说明.md |
生产版本 93c1d1d8-a503-47db-9625-8a0333374034 |
实际 51ac31d3-98db-4740-8b8e-f5de09fd7d43 |
【文档过期】 |
任务交接说明.md |
当前生产版本 93c1d1d8(2026-10-17 安全加固 v2) |
同上,已被 10-22 共 9 个 UI 提交超越 | 【文档过期】 |
修复记录-P0-P1.md |
"未部署生产;线上仍为 f490d9d2" |
P0/P1 修复已在生产(fix 分支即生产内容) | 【文档过期】 |
安全说明.md |
"源码里只有占位符 __CAT_TOKEN_VALUE__,不落盘" |
源码 L1100 有硬编码 DEFAULT_AUTH_TOKEN,占位符仅在页面注入时被替换(L1361) |
【文档与代码不符】 |
安全说明.md |
各项限额(1MB/4000 字/800KB 图/10 条历史/500 tokens/25s 超时/20·400·1500 限流) | 与 SEC 常量(L1080-1094)逐条一致且均有强制点 | 【已验证完成】 |
安全说明.md |
出站白名单、禁 IP、禁重定向、错误脱敏 | L1262-1327、L881-903 逐条证实 | 【已验证完成】 |
二、前端审查(index.html)
2.1 结构总览
- CSS:约 1200 行,含 72 处
.dark-mode选择器定义(L80、L414、L595、L739、L816 等)。 - JS 模块:
storage键表(L5053-5083)、CONFIG(L5125,WORKER_URL默认取location.origin)、syncViewportMetrics()视口/键盘适配(L10909-11035)、escapeHtml(L8667)、记忆库前端(L8644-8740)、设置持久化(L10546-10723)。 - 令牌传递:
window.__CAT_TOKEN__(L7 占位符,Worker 返回页面时注入实际值),7 处请求点携带X-Cat-Token头(L7058、L7994、L8495、L8532、L8677 等)。 - 本地存储键(13 个):
chat_history、avatar_ai、avatar_me、chat_bg、user_status、worker_url、persona_prompt、model_provider、model_api_key、model_api_url、model_name、sticker_list、kaomoji_list。API Key 以明文存于 localStorage(model_api_key),属已知取舍(文档声明"前端自选,存本机")。
2.2 功能完成度
| 功能 | 状态 | 证据 |
|---|---|---|
| 聊天收发(文本 + 图片 inline base64 + 附件元数据) | 【已验证完成】 | 消息渲染链路 + P0-2 测试 7.1-7.3 通过 |
| 表情包 / kaomoji 面板、点击直发 + 发送前压缩 | 【已验证完成】 | stickerWrap/kaomojiGrid 渲染点 L9531/L9064 |
| 收藏(微信式 UI、双向、显示收藏者) | 【已验证完成】 | favWrap L9333;favoriteMessage → addCloudHighlight |
| 记忆库面板(高光/经历/关联三 tab + 删除 + 高光关键词搜索) | 【已验证完成】 | memoryModal L8646+;/highlights /experiences /relations 调用点 |
| 云同步(导出到云端 / 从云端恢复,幂等) | 【已验证完成】 | cloudRestoreBtn L8640;/archive 调用 |
| 删除消息(先服务端后本地,防复活) | 【已验证完成】 | P0-3 测试 23/23 通过 |
| 设置持久化(人设/Key/地址/模型/头像/背景) | 【已验证完成】 | L10546-10723 storage 读写 |
| 全屏(Fullscreen API + PWA manifest + 添加主屏幕引导) | 【已验证完成】 | 23fd149 提交;manifest/icon 路由 L1431-1442 |
| 图片灯箱 / showToast | 【已验证完成】 | 任务交接说明批次 + 代码存在 |
| iOS Safari 全屏初始化 + 键盘跟随(本轮 9 个 UI 提交) | 【已验证完成】 | HEAD 即最终版;线上已部署 |
| 流式回复(SSE/stream) | 【未实现】 | 全文 0 处 stream:true/text/event-stream/ReadableStream |
| 全局消息搜索 | 【未实现】 | 仅记忆库内高光搜索(L4253 placeholder"搜索高光原话…") |
| 多角色 / profile 切换 UI | 【未实现】 | 前端无任何 profile 切换控件;后端 profile 机制存在但前端只用默认 owner |
| 深色模式 | 【死功能】 | 72 处 .dark-mode CSS,0 处 JS 添加/切换该类(grep classList/toggle/className 无命中) |
| 顶部栏颜色设置 | 【存在缺陷】 | 设置项存在但无生效代码路径(审查确认无效) |
| 相对路径 / WORKER_URL 割裂 | 【存在缺陷】 | file:// 直接打开 index.html 时 WORKER_URL 落占位符 https://YOUR-WORKER-SUBDOMAIN.workers.dev,全部调用失败;必须经 Worker 托管或手动在设置里填地址 |
2.3 XSS 面
escapeHtml(L8667)完整转义& < > " '。- 全部 9 处
innerHTML:7 处为清空(= '')或静态字符串;2 处为记忆库列表渲染(L8689/8692),其中所有用户可控字段(hid/content/mid/category/title/eid/rid/from_id/to_id/reason/rel_type)均经escapeHtml包裹(L8705-8734)。 - 消息气泡渲染不走 innerHTML 拼装用户文本。
- 结论:当前无可利用 XSS 向量(前提是后续新增渲染继续沿用 escapeHtml;无 CSP 头兜底,见第七部分)。
三、后端 API 审查(worker.js)
3.1 路由全表(fetch handler L1415-2238)
| # | 路由 | 方法 | 鉴权 | 限流 | 说明 |
|---|---|---|---|---|---|
| 1 | /manifest.webmanifest、/apple-touch-icon.png、/icon-192.png、/icon-512.png |
GET | 公开 | 无 | 静态资源,不含数据 |
| 2 | /health |
GET | 公开 | 无 | 仅 ok/service/version/time |
| 3 | /init |
POST | 令牌(L1455) | 无 | ensureSchema |
| 4 | /backup |
GET | 令牌(L1458) | 无 | 备份导出 |
| 5 | /archive |
GET/POST | 令牌(L1492 专属 checkAuth) | 无 | GET=档案导出(过滤软删);POST=幂等导入 |
| 6 | /export/full |
GET | 令牌 | 无 | 全量导出 schema 2 |
| 7 | /import/full |
POST | 令牌 | 无 | 全量导入,8MB 上限(L1726),dry_run |
| 8 | /proactive |
POST/GET | 令牌(L1457) | 有(L2096) | 主动消息 |
| 9 | /messages/delete |
POST | 令牌(POST 即保护) | 无 | 软删/硬删 + 引用检查 + 级联 |
| 10 | /highlights /experiences /relations |
GET/POST/DELETE | 令牌(L2017) | 无 | 记忆库三实体 |
| 11 | 任意其他路径 | POST | 令牌(L1455) | 有(L2179) | 聊天主接口 handleChat |
| 12 | 任意其他路径 | GET | 公开 | 无 | 返回前端页面(运行时注入令牌 L1361) |
鉴权覆盖面结论【已验证完成】:所有数据读写路径均有令牌保护;不存在未授权读数据的 GET 路由。鉴权链:isSameOrigin(L1168,浏览器带异源 Origin 直接 403)→ protectedRoute 判定 → checkAuth → safeEqual 定长比较(L1198,防时序侧信道)。
3.2 出站安全(callModel L847-923)
| 机制 | 状态 | 证据 |
|---|---|---|
| https 强制 / 禁账号密码 / 禁非 443 端口 | 【已验证完成】 | L1300-1308 |
| 主机白名单(8 家内置 + ALLOWED_API_HOSTS 追加) | 【已验证完成】 | L1103-1112、L1316-1324 |
| 禁 IP 字面量(点分十进制 + IPv6 括号/冒号) | 【部分完成】 | L1284-1285 正则仅挡 ^\d{1,3}(\.\d{1,3}){3}$ 与含 :/[ 形式 |
| 禁 localhost/.local/.internal/metadata.google.internal | 【已验证完成】 | L1275-1281 |
redirect:'manual' + 3xx 一律拒绝 |
【已验证完成】 | L881、L891-893 |
| 上游错误脱敏后入日志,不回传前端 | 【已验证完成】 | L896-903 sanitizeErrorText |
| 模型输出 max_tokens=500、超时 25s AbortController | 【已验证完成】 | L862-863、L878、L1090 |
【存在缺陷】SSRF 整数 IP 穿透:L1284 只挡点分十进制。以下形式可绕过:十进制整数 IP(https://2852039166/ = 169.254.169.254)、十六进制 IP(https://0xa9fea9fe/)、解析到内网的公网域名(如 localtest.me、nip.io 类)。Cloudflare 出口本身不完全等价于目标网络位置,且白名单机制(默认 ALLOW_CUSTOM_API=0)已挡住绝大多数场景,故定为中危而非高危——但若运维设置 ALLOW_CUSTOM_API=1,该缺陷升级为高危。
3.3 限流(checkRate L1249 / bumpCounter L1226)
- 参数:单 IP 20 次/分、400 次/天;全局 1500 次/天(SEC L1091-1093),超限 429 +
Retry-After。 - 【存在缺陷】fail-open:
bumpCounter的 KV 读(L1228-1230)与写(L1242-1244)失败均被catch吞掉,读失败时计数器从 0 起算——KV 故障或限流键竞态期间限制形同虚设。 - 【存在缺陷】KV 最终一致性:计数器存 KV,跨节点读写可能少计,限流是尽力而为而非硬保证。
- 【无法确认】实际拦截效果:需要线上构造超限流量验证,本阶段未做(只读约束 + 不影响线上数据)。
3.4 检索与上下文(tokenize L620 / retrieveMemory L641 / retrieveContext L695 / buildSystemPrompt L716)
tokenize:中文双字滑窗 + 英文整词(≥2 字符),token 上限 12 个(L638)。retrieveMemory:listMemoryKeys(默认 max 1000)→ 按 ts 排序 → 取最近SCAN_LIMIT=300(L44)→Promise.all并行 KV get → 纯String.includes命中计数排序 → 无命中则退化为最近 N 条。retrieveContext:memories(limit 8)+ highlights(slice 3)+ experiences active(slice 3)+ candidate(slice 2)四路并行。- 【存在缺陷】记忆容量硬上限:超过 300 条的旧记忆永不参与检索( silent amnesia ),且无任何提示。
- 【存在缺陷】KV 读取放大:单次
/chat仅 retrieveContext 就最多产生 300(mem)+ hl/exp 各自 list+N get 次 KV 读,轻松超过 300 次读/请求。Cloudflare KV 免费额度 10 万读/天 → 理论约 300+ 次聊天即触顶,付费额度下也直接推高成本。测试日志可见单次 chat 仅 0-1ms(Mock KV),真实 KV 延迟下这会成为主要耗时。 - 检索质量:无语义理解、无权重衰减、无时效加权,纯子串命中数。
3.5 写入路径
saveMemory(L129):cleanText 4000 字、剥离控制字符与模型标记。- 【存在缺陷】KV 无事务竞态:
meta_lastTs双写(新键meta_lastTs:<profile>L155 + 旧键meta_lastTsL158,owner 时),读-改-写无原子性,并发聊天互相覆盖(last-write-wins),影响"距上次聊天太近"判定精度。 /messages/delete(L2121-2169):软删打deleted_at;硬删(mode:'hard' && force)env.CHAT.delete(key)只删消息本体。- 【存在缺陷】硬删留记忆碎片:
cascadeDeleteRefs(L499)只级联清理hl:/exp:/rel:引用,mem:前缀的记忆碎片不会被清理——硬删消息后,由该消息抽取的记忆永久残留,且仍参与检索。 /archivePOST 导入(L1613-1672):按 mid 幂等去重(L1639 先 get 后写)、import_batch_id溯源、逐条 try/catch。- 【存在缺陷】
/archive导入无体积上限:L1615 直接request.text()全量读入后 JSON.parse,既无 content-length 预检也无大小截断。对比/import/full有 8MB 检查(L1726)与聊天接口 1MB 检查(L2174/2202)。超大 body 可导致 Worker 内存超限或 CPU 超时(需令牌,定中危)。
3.6 主动消息(handleProactive L2444-2510)
- 配额:
PROACTIVE_MAX_PER_DAY = 3(L39),按day_key计数(L1009)。 - 【存在缺陷】客户端时间伪造(高危):
resolveTime(L84-105)完全信任请求体的t.iso——ts、day_key、period、week全部来自客户端。持有令牌者只需每次传入不同的(如未来的)iso,即可让day_key永不落码,从而绕过每日 3 次配额无限刷主动消息;同时污染消息时间线(任意时间戳入库)。该接口有令牌 + 限流保护,跨站攻击者无法直接利用,故综合定高危(对持令牌者)/中危(对陌生攻击者)。 - 主动消息同样走
saveMemory入库(L2499)与bumpProactive(L2501)。
四、数据存储与同步审查(Cloudflare KV)
4.1 键空间设计
| 前缀 | 内容 | 隔离方式 |
|---|---|---|
msg:<profile>:<mid> |
消息(含 deleted_at、message_type、附件元数据、sync_status) |
profile 前缀 |
mem:<profile>:<id> |
原话记忆 | profile 前缀 |
hl:<profile>:<hid> |
高光原话 | profile 前缀 |
exp:<profile>:<eid> |
经历记忆(active/candidate) | profile 前缀 |
rel:<profile>:<rid> |
关联关系 | profile 前缀 |
proactive:<profile>:* |
主动消息计数 | profile 前缀 |
rl_<hash> / rl_global |
限流计数器(TTL 172800) | IP 哈希 |
meta_lastTs:<profile> + 遗留 meta_lastTs |
最后聊天时间 | profile |
mem_*(遗留) |
旧版记忆键 | 仅 owner 只读兼容(L595-602,MEM_READ_LEGACY=1 默认开) |
- profile 校验:
VALID_PROFILE = /^[a-z0-9_]{1,32}$/(L172),非法回落DEFAULT_PROFILE='owner'(L171)。profile 隔离机制本身已实现且经测试验证(P0-1:15/15 通过,含 owner/test 双向隔离、旧键兼容、无mem:undefined:*残留)。 - 注意:前端不传 profile,实际全部数据落
owner单空间——多角色隔离目前是"后端有能力、前端未使用"。
4.2 同步机制
| 机制 | 状态 | 证据 |
|---|---|---|
/archive GET 导出(过滤软删、支持关键词/时间/说话方/分页) |
【已验证完成】 | P1-7 测试 14/14 |
/archive POST 导入(幂等按 mid 去重) |
【已验证完成】 | P0-3 测试 6.1/6.2(重复同步 imported=0 skipped=2) |
/export/full + /import/full(schema 2、dry_run、保留 deleted_at、图片附件元数据、8MB 上限、逐条错误报告) |
【已验证完成】 | P0-4 测试 27/27 |
| 删除同步(软删不复活、引用检查、级联软删) | 【已验证完成】 | P0-3 23/23 |
| 跨 profile 隔离(导出/导入/删除/记忆) | 【已验证完成】 | P0-1 15/15、P0-4 7.1/7.2 |
| 事务性 / 原子性 | 【未实现】 | KV 无多键事务;导入部分失败留半成品(逐条 catch 不整体回滚) |
| 版本管理 / 冲突解决 | 【未实现】 | 无版本号、无冲突检测,last-write-wins |
| 断点续传 / 增量同步 | 【未实现】 | 仅全量 + 幂等跳过 |
4.3 测试结果汇总(本阶段实际运行)
| 测试文件 | 结果 |
|---|---|
test_p0_memory_isolation.mjs |
15/15 通过 |
test_p0_mid_refs.mjs |
21/21 通过 |
test_p0_delete_sync.mjs |
23/23 通过 |
test_p0_export_import.mjs |
27/27 通过 |
test_p1_archive_search.mjs |
14/14 通过 |
test_p1_memory_context.mjs |
13/13 通过 |
test_phase4_v2.mjs |
32/32 通过 |
| 合计 | 145 断言通过,0 失败 |
测试均为内存 Mock KV + Mock 模型 + 虚构数据/令牌,未触碰线上。
五、记忆系统专项
5.1 现状机制
用户消息 ──► handleChat ──► saveMemory(L129) ──► mem:<profile>:<id>(原话,cleanText 4000)
│
├─► retrieveContext(L695) ──► memories(≤8) + highlights(≤3) + experiences(≤3) + candidates(≤2)
│ │
│ ▼
└──────────────────────────► buildSystemPrompt(L716) ──► callModel
- 三层记忆实体:
mem(原话记忆,自动)、hl高光(收藏/模型[[fav:]]触发,带来源 mid)、exp经历(active/candidate 两态,带time_certainty确定性、source_mids来源)、rel关联(from_id → to_id,带 reason/confidence)。 - 检索进上下文已验证:P1-6 测试 13/13(含"纯图片时记忆条数受限 ≤8")。
- 引用完整性已验证:P0-2 测试(mid 一致性、真实来源、missing_refs 400 拦截)。
- 候选经历单列:candidate 状态不混入 active(P1-6)。
5.2 记忆系统的能力缺口(相对工业级记忆系统)
| 能力 | 现状 | 状态 |
|---|---|---|
| 记忆变更决策(新增/更新/删除/无操作) | 只有"追加写",无对旧记忆的 UPDATE/DELETE 决策,矛盾记忆并存 | 【未实现】 |
| 记忆版本链 / 变更历史 | 无 history 表,记忆被覆盖即丢失前值 | 【未实现】 |
| 双时间戳(生效/失效/过期) | 仅单时间戳 created_at;经历有 time_certainty 字段但非真正的时间推理 |
【部分完成】 |
| 混合检索(关键词 + 语义) | 纯关键词子串 | 【未实现】 |
| 上下文 token 预算控制 | 按条数硬截(8/3/3/2),无 token 计量 | 【部分完成】 |
| 人工审核队列 | 无;candidate 仅是状态字段,无审核界面/流程 | 【未实现】 |
| 记忆容量管理 | 300 条扫描上限即事实容量上限,无归档/分层/淘汰 | 【存在缺陷】 |
| 任务恢复 / 持久状态 | 无 | 【未实现】 |
六、原有功能保留清单
以下为对"已有功能逐项核对"的结果(用于后续重构时的保留基准):
| # | 功能 | 位置 | 状态 |
|---|---|---|---|
| 1 | 聊天收发(文本/图片/重试复用 client_mid 幂等) | 前端消息链路 + worker handleChat | 【已验证完成】P0-2 |
| 2 | 图片消息(inline base64 + kind/mime/bytes 元数据保留) | L2271 MAX_IMAGE 校验 + 测试 7.1-7.3 | 【已验证完成】 |
| 3 | 表情包 / kaomoji(点击直发 + 压缩) | stickerWrap/kaomojiGrid | 【已验证完成】 |
| 4 | 收藏(双向 + 云端高光联动) | favWrap + /highlights | 【已验证完成】 |
| 5 | 记忆库面板(高光/经历/关联 + 删除 + 高光搜索) | L8644-8740 | 【已验证完成】 |
| 6 | 云同步(/archive 导出导入 + /export/full + /import/full) | L1491-2012 | 【已验证完成】P0-3/P0-4 |
| 7 | 删除(软删/硬删/引用检查/级联/防复活) | L2121-2169 | 【已验证完成】P0-3 |
| 8 | 主动消息(每日 3 次 + 人设 + 记忆找话题) | L2444-2510 | 【部分完成】功能可用,但时间可伪造 |
| 9 | 多 profile 隔离(后端) | L171-172、L591-613 | 【已验证完成】P0-1;前端无切换 UI |
| 10 | 旧数据兼容(mem_* / meta_lastTs 遗留键 owner 只读) | L595-602、L996-1000 | 【已验证完成】P0-1 3.1-3.3 |
| 11 | 限流(IP 分/日 + 全局日) | L1249-1260 | 【部分完成】fail-open + KV 一致性 |
| 12 | 出站白名单/SSRF 防护 | L1262-1327 | 【部分完成】整数 IP 穿透 |
| 13 | 令牌鉴权 + Origin 同源校验 | L1168-1200 | 【部分完成】默认令牌硬编码(见七) |
| 14 | 全屏/PWA/灯箱/toast/添加主屏幕 | 23fd149 批次 | 【已验证完成】 |
| 15 | iOS 键盘/视口适配 | syncViewportMetrics L10909-11035 | 【已验证完成】(本轮 9 提交,线上即此版) |
| 16 | 深色模式 | 72 处 CSS | 【死功能】无切换代码 |
| 17 | 流式回复 | — | 【未实现】 |
| 18 | 全局消息搜索 | — | 【未实现】(仅记忆库内高光搜索) |
| 19 | 顶部栏颜色设置 | 设置项 | 【存在缺陷】无效 |
七、安全、稳定性与部署
7.1 生产与开发环境区别
- 无独立 staging:
deploy.sh直接把构建产物推到生产 Worker(zhai.beibbloveme.workers.dev);开发用wrangler dev本地 Mock。生产与开发运行同一套代码、同一份 KV(dev 默认本地模拟,但wrangler dev --remote会连生产 KV——存在误操作风险)。 - 凭据:
/root/.cloudflare_token或环境变量(deploy.sh)。 - KV 无环境隔离:单一
CHATnamespace,测试与生产数据若共用即互相污染(当前测试用 Mock,无此问题)。
7.2 高危问题(3 项)
| # | 问题 | 证据 | 缓解与残余风险 |
|---|---|---|---|
| H1 | 硬编码默认访问令牌:DEFAULT_AUTH_TOKEN='518effb…'(L1100)随源码与构建产物分发;线上未配置 AUTH_TOKEN secret(secret list 仅 DEEPSEEK_API_KEY)。isSameOrigin 对无 Origin 的直连请求放行(L1170"非浏览器调用没有 Origin,由令牌把关")——持令牌者可用 curl 直接调所有接口 |
L1100、L1193-1195、L1168-1170、wrangler secret list | 缓解:仓库私有(未认证 API 404)。残余:任何获得仓库/构建产物读取权限的人即持令牌;令牌无轮换机制(注释说轮换需 wrangler secret put AUTH_TOKEN,但从未执行过) |
| H2 | 主动消息客户端时间伪造:resolveTime 信任 t.iso,day_key 客户端可控 → 绕过 3 次/日配额、污染时间线 |
L84-105、L2447、L1009 | 缓解:需令牌 + 限流。残余:配额与时间线完整性对持令牌者不设防 |
| H3 | 限流 fail-open + KV 最终一致性:计数器读写失败均吞异常,读失败从 0 起算 | L1226-1247 | 缓解:全局配额仍有大部分时间有效。残余:KV 抖动期间可无限刷 |
7.3 中危问题(6 项)
| # | 问题 | 证据 |
|---|---|---|
| M1 | /archive POST 导入无体积上限,大 body 可 OOM/超时 |
L1613-1616(对比 L1726 的 8MB 检查缺失) |
| M2 | SSRF 整数/十六进制 IP 与解析到内网的域名可绕过 IP 字面量检查 | L1284-1285 |
| M3 | KV 读取放大:单次 /chat 约 300+ 次 KV 读,直接推高成本与延迟 | L641-651、L695-702 |
| M4 | 硬删消息不清 mem: 记忆碎片,永久残留且参与检索 |
L2157-2158、cascadeDeleteRefs L499 |
| M5 | meta_lastTs 双写 + 读-改-写竞态 |
L155-158 |
| M6 | 线上 secret DEEPSEEK_API_KEY 与代码读取的 env.API_KEY 不匹配(死配置,实际模型 Key 全靠前端传入,服务端无兜底 Key) |
L851、secret list |
7.4 低危与其他(5 项)
| # | 问题 | 证据 |
|---|---|---|
| L1 | 无 CSP / HSTS 等安全响应头(仅 referrer-policy) | L1370 |
| L2 | API Key 明文存前端 localStorage(model_api_key) |
L5073 |
| L3 | main 分支落后生产 15+ 提交,从 main 部署将丢失全部 P0/P1 修复 | git merge-base --is-ancestor 核验 |
| L4 | 三份文档的生产版本号过期(93c1d1d8 / f490d9d2) | 第一·1.4 |
| L5 | /proactive 与 /messages/delete 等写路径无限流(仅 /chat 与 /proactive 有) |
L2096 vs L2121 |
7.5 测试覆盖缺口
已覆盖:记忆隔离、mid 一致性、删除同步、导出导入、档案搜索、上下文组装、阶段4 回归(145 断言)。 未覆盖:限流触发与 fail-open、SSRF 各绕过向量、时间伪造、并发竞态、体积上限、令牌轮换、前端渲染回归(无前端测试框架)。
八、开源项目对照研究(Mem0 / Graphiti / Letta / LangGraph)
研究方法:基于四个项目公开仓库的源码结构(非 README 宣传)分析其记忆/时间线/关系/持久状态/任务恢复/人工审核机制。结论:不照搬任何框架,仅提炼可借鉴机制与 KV 架构下的低成本落地方式。
8.1 Mem0——记忆变更决策与版本链
- 核心机制:对每条新记忆先做 ADD / UPDATE / DELETE / NONE 四态决策(由 LLM 判定与既有记忆的关系),所有变更写入独立 history 表,保留旧值快照;检索用 BM25 加性打分(多词命中线性叠加)。
- 源码位置:
mem0/memory/main.py(Memory 类的 add/search 流程与 history 落库)、mem0/memory/下的 graph 与向量存储实现。 - 可借鉴:①"先决策再写入"——当前项目只有追加写,矛盾记忆并存无感知;②history 版本链——记忆被更新/删除后仍可追溯。
- KV 落地成本:中。可在现有
mem:<profile>:<id>旁加memhist:<profile>:<id>键存变更链;决策逻辑需一次额外 LLM 调用(成本 +1 次/记忆写入)。 - 风险:决策调用引入延迟与费用;决策错误会静默污染记忆库(需人工审核兜底,见 8.5)。
8.2 Graphiti——双时间戳与时序知识图谱
- 核心机制:每条事实带
valid_at/invalid_at/expired_at三个时间戳,显式建模"何时生效、何时失效、何时过期";检索用 RRF(Reciprocal Rank Fusion)融合多路结果。 - 源码位置:
graphiti-core/graphiti_core/(edges/nodes 的时间字段定义与 search 融合逻辑)。 - 可借鉴:①时间语义——当前经历记忆只有单
created_at和一个声明式的time_certainty字段,没有"这句话描述的事何时成立/失效"的概念;②RRF 融合——当前纯关键词单路检索,未来接入语义检索时需要融合策略。 - KV 落地成本:低-中。时间字段只是给现有
exp:/rel:记录加字段;RRF 需要多路检索结果,当前架构只有一路关键词,暂用不上(接入语义检索后才需要)。 - 风险:时间推理依赖 LLM 抽取质量;低。
8.3 Letta——分层记忆与上下文预算
- 核心机制:core memory(有限容量的核心块,Block 有 limit)/ archival memory(无限归档)/ recall memory(对话历史)三层;用
ApproxTokenCounter(约 bytes/4)在组 prompt 前做 token 预算控制,超预算按策略裁剪。 - 源码位置:
letta/(letta/schemas/block.py的 Block limit、letta/下的 token 计数与 context 组装)。 - 可借鉴:①上下文预算——当前
buildSystemPrompt按条数硬截(8/3/3/2),中英文长度差异大,条数 ≠ token 量;②分层——把"人设/长期事实"(core,必进 prompt)与"历史原话"(archival,按需检索)分开。 - KV 落地成本:低。token 估算就是
Buffer.byteLength/4量级的函数;分层只是调整buildSystemPrompt的取数逻辑。 - 风险:低。纯计算逻辑,无外部依赖。
8.4 LangGraph——持久状态、任务恢复与命名空间
- 核心机制:checkpointer 把每步状态持久化(thread_id + writes 表),支持 time-travel(回到任意历史步重放);BaseStore 用 namespace 前缀做跨线程共享存储。
- 源码位置:
libs/langgraph/(langgraph/checkpoint/的 BaseCheckpointSaver 实现与 writes 表结构、langgraph/store/的 BaseStore namespace)。 - 可借鉴:①thread_id + 分步 writes——当前对话就是扁平的 msg 列表,没有"一次任务多步执行、中断可恢复"的概念(对照第 5.2 节"任务恢复未实现");②namespace 前缀——与当前
mem:<profile>:*前缀设计同构,说明现有键设计方向正确。 - KV 落地成本:中。需要引入"任务"实体与状态机;KV 无事务,多步写入的原子性需自己补偿(见 8.5 建议4)。
- 风险:中。任务状态机是较重的抽象,个人项目应按最小集引入(例如只做"主动消息任务的断点恢复")。
8.5 横向对比与 KV 架构设计建议
| 维度 | Mem0 | Graphiti | Letta | LangGraph | 当前项目 |
|---|---|---|---|---|---|
| 记忆变更 | 四态决策 + history | 边失效 | Block 覆盖 | 状态覆盖 | 仅追加 |
| 时间建模 | created_at | valid/invalid/expired 三戳 | 无 | 时间戳 | 单 created_at |
| 关系 | 隐式 | 显式图谱 + RRF | 无 | 无 | rel 实体(轻量) |
| 检索 | BM25 加性 | RRF 混合 | 分层预算 | — | 关键词子串 |
| 上下文控制 | — | — | token 预算 | — | 条数硬截 |
| 任务恢复 | — | — | — | checkpointer | 无 |
| 人工审核 | 依赖外部 | 依赖外部 | 依赖外部 | 依赖外部 | 无 |
给后续记忆系统重新设计的 5 条建议(均基于现有 KV 架构,零新基础设施):
- 确定性规则 + history 版本链(借鉴 Mem0):写入前用确定性规则(同实体高相似度 → UPDATE 并写
memhist:链)替代无条件追加;LLM 四态决策作为可选增强,默认关闭以控成本。 - 双时间戳(借鉴 Graphiti):给
exp:/rel:记录加valid_at/invalid_at字段,检索时过滤已失效项——这是修复"矛盾记忆并存"的最低成本手段。 - RRF 融合预留(借鉴 Graphiti):
retrieveMemory的排序层抽象成"多路候选 → 融合"接口,当前只有关键词一路;未来接入任何语义检索时直接挂载。 - DO + WAL 流水线(借鉴 LangGraph checkpointer 思想,规避 KV 无事务):把"一次聊天的多次 KV 写"先写 Durable Object 的 WAL 再异步落 KV,解决 7.2-H3 之外的写入原子性与读取放大(M3)。
- core memory + 上下文预算(借鉴 Letta):prompt 组装从"条数硬截"改为"core 必进 + archival 按 token 预算填充",token 估算用 bytes/4 近似。
引入成本与风险总评:以上 5 条均可在现有 Worker + KV 上实现,不引入新服务;主要成本是 LLM 决策调用(可选)与一次检索层重构。明确不建议直接引入 Mem0/Graphiti/Letta/LangGraph 框架本体——它们分别自带向量库/图数据库/服务端运行时,与当前"单 Worker + KV"的极简架构和零外部依赖原则冲突。
九、升级前置条件(重新设计记忆系统之前必须解决)
按优先级排序,每条对应前文证据:
- 分支合并:将
fix/p0-memory-consistency合回main并重新确立基线,否则任何"从 main 重构"都会丢失 P0/P1 全部修复(7.4-L3)。 - 令牌治理:部署
AUTH_TOKENsecret 替换硬编码默认值;建立轮换流程;评估"共享静态令牌"是否足够(当前页面注入方案决定了令牌必然随页面下发,跨站已被 Origin 校验挡住,主要风险是源码/产物扩散)(7.2-H1)。 - 时间服务端化:
resolveTime忽略客户端iso或取max(server_now, client);主动消息配额改按服务端日期(7.2-H2)。 - 限流加固:计数器失败时 fail-closed 或至少告警;评估 Durable Object 计数器的成本(7.2-H3)。
/archive导入加体积上限(对齐/import/full的 8MB 或更小)(7.3-M1)。- SSRF 规范化:IP 字面量检查前做规范化(解析十进制/十六进制/短形式为点分十进制再判)(7.3-M2)。
- KV 成本治理:单次 chat 300+ 读的放大必须先行解决(DO 缓存 / 批量读 / 降低 SCAN_LIMIT 并配归档),否则新记忆系统只会更重(7.3-M3)。
- 硬删补清理:硬删消息时级联清理
mem:碎片,或在检索层按来源 mid 过滤已删消息(7.3-M4)。 - secret 命名对齐:把线上
DEEPSEEK_API_KEY改为代码读取的API_KEY,或改代码读 secret 现名;确定服务端是否保留兜底 Key(7.3-M6)。 - 文档版本机制:三份文档的生产版本号与实际部署脱节,建立"部署后回写版本号"的流程(7.4-L4)。
- 测试补齐:为限流、SSRF、时间伪造、并发、体积上限补测试;当前 145 断言不覆盖上述任何一项(7.5)。
- 记忆容量方案:300 条扫描上限是事实容量天花板,重构前需确定归档/分层/淘汰策略(5.2)。
十、证据索引
10.1 代码位置索引(worker.js,共 2510 行)
| 机制 | 行号 |
|---|---|
| SEC 常量(限额/限流/超时) | L1078-1096 |
| DEFAULT_AUTH_TOKEN(硬编码令牌) | L1100 |
| DEFAULT_API_HOSTS(出站白名单) | L1103-1112 |
| PROACTIVE_MAX_PER_DAY=3 / DEFAULT_PROFILE / VALID_PROFILE | L39 / L171 / L172 |
| SCAN_LIMIT=300 | L44 |
| saveMemory | L129 |
| meta_lastTs 双写 | L155-158 |
| cascadeDeleteRefs | L499 |
| listMemoryKeys(含旧键兼容) | L591-613 |
| tokenize | L620-639 |
| retrieveMemory | L641-692 |
| retrieveContext | L695-709 |
| buildSystemPrompt | L716-840 |
| callModel(超时/禁重定向/脱敏) | L847-923 |
| resolveTime(客户端时间信任点) | L84-105 |
| proactiveState / bumpProactive | L980 / L1014 |
| logEvent | L1146-1150 |
| corsHeaders | L1153-1165 |
| isSameOrigin | L1168-1190 |
| getToken / checkAuth / safeEqual | L1193-1195 / L1198+ |
| hashShort / bumpCounter / checkRate | L1215-1260 |
| isForbiddenHost / validateApiUrl | L1271-1327 |
| 页面响应 + 令牌注入(TOKEN_MARK 替换) | L1361 |
| fetch handler(路由/鉴权/兜底) | L1415-2238 |
| handleProactive(时间伪造入口) | L2444-2510 |
| /archive POST 导入(无体积限制) | L1613-1672 |
| /import/full(8MB 上限) | L1679-2012 |
| /messages/delete | L2121-2169 |
| 聊天主接口(限流 + 1MB + 兜底) | L2171-2230 |
10.2 代码位置索引(index.html,约 11560 行)
| 机制 | 行号 |
|---|---|
| 令牌占位符注入点 | L7 |
| .dark-mode CSS(72 处,无 JS 切换) | L80 起 |
| storage 键表(13 个 localStorage 键) | L5053-5083 |
| CONFIG / WORKER_URL 默认值 | L5125-5140 |
| 消息输入框 | L4027-4032 |
| 记忆库面板(含 escapeHtml 渲染) | L8644-8740 |
| escapeHtml | L8667 |
| X-Cat-Token 请求头(7 处) | L7058、L7994、L8495、L8532、L8677 等 |
| syncViewportMetrics(视口/键盘适配终版) | L10909-11035 |
10.3 配置与部署证据
wrangler.toml:name=zhai、KV CHAT id、vars(API_URL/MODEL/ALLOWED_API_HOSTS=""/ALLOW_CUSTOM_API="0"/ALLOWED_ORIGINS="")wrangler secret list:仅DEEPSEEK_API_KEY(无 AUTH_TOKEN、无 API_KEY)wrangler deployments list/status:最新生产51ac31d3-98db-4740-8b8e-f5de09fd7d43(2026-10-22 19:19:43 +0800)git log:HEADf02cc07(19:19:35 +0800,较部署早 8 秒,内容吻合)- 构建核验:
/tmp/buildcheck重新构建产物与仓库cat-chat-all-in-one.jsdiff 为空 - 分支核验:
git merge-base --is-ancestor证实 6 个 P0/P1 提交不在 main
10.4 测试证据(本阶段实际运行,全部通过)
test_p0_memory_isolation.mjs 15/15 profile 隔离 + 旧键兼容
test_p0_mid_refs.mjs 21/21 mid 一致性 + 引用校验 + 图片元数据
test_p0_delete_sync.mjs 23/23 软删/硬删/级联/防复活/隔离
test_p0_export_import.mjs 27/27 全量导出导入/幂等/dry_run/软删保留
test_p1_archive_search.mjs 14/14 关键词/时间/说话方/分页/软删过滤
test_p1_memory_context.mjs 13/13 记忆进 prompt/候选分离/条数上限
test_phase4_v2.mjs 32/32 阶段4 原功能回归
合计 145/145 通过,0 失败(Mock KV + Mock 模型 + 虚构令牌)
10.5 开源项目参考
- Mem0:
github.com/mem0ai/mem0(mem0/memory/main.py四态决策与 history;BM25 加性打分) - Graphiti:
github.com/getzep/graphiti(graphiti-core/graphiti_core/双时间戳与 RRF) - Letta:
github.com/letta-ai/letta(Block limit、ApproxTokenCounter、三层记忆) - LangGraph:
github.com/langchain-ai/langgraph(langgraph/checkpoint/、langgraph/store/namespace)
10.6 未能核验事项(【无法确认】)
- 线上
/health实际响应:沙箱网络无法访问zhai.beibbloveme.workers.dev(curl 超时),版本一致性结论基于 wrangler API 元数据而非线上抓取。 - 限流在真实 KV 上的实际拦截效果(需构造超限流量,未执行)。
- KV 中现存数据量(未做线上 list 全量扫描,避免无谓的读放大与费用)。
- 仓库确切的协作者/权限名单(GitHub API 未认证仅确认私有)。
附:审查结论摘要
- 代码基线本身是健康的:双真源 + 构建产物零漂移、145 项回归测试全过、P0/P1 记忆一致性修复已在生产。
- 主要风险集中在三处:硬编码默认令牌(H1)、主动消息时间伪造(H2)、限流 fail-open(H3),另有 KV 读取放大带来的成本风险(M3)。
- 分支与文档管理落后于部署节奏:main 不含生产修复、三份文档版本号过期——这是重构启动前最优先要平整的工程债。
- 记忆系统的能力缺口清晰:无变更决策、无版本链、无双时间戳、无 token 预算、无人工审核、300 条事实容量上限;第八部分给出了 5 条不引入外部框架的 KV 原生演进路径。
本阶段任务至此结束。按任务书要求,交付本报告后停止,不开始重构,不开发新记忆系统,不修改任何既有代码与线上配置。
完整交付文档(2026-10-22)
小手机(猫爪聊天)· 完整交付文档
一份文档看懂整个项目:源码结构 + 功能介绍 + 开发进程 + 测试证据 + 交接信息
生成时间:2026-10-22 | 代码基线:分支
fix/p0-memory-consistency@b03a612(main@75657e0) 线上版本:f490d9d2-8ee5-4d0c-9274-b082b5d34990(2026-10-19T07:14:23Z 起 100% 在线,早于本轮修复,未包含 P0–P1 改动)证据约定:本文所有结论分三级——【源码】= 直接读代码得出;【实测】= 本地跑测试得出;【未验证】= 沙箱无法证实(如线上字节一致性、GitHub 远端实时状态)。凡未标注者均为【源码】级。
目录
- 这份文档怎么用
- 项目概况
- 源码介绍(逐文件 · 逐模块 · 逐接口)
- 功能介绍(完整清单)
- 架构与数据流
- 开发进程记录(从 0 到当前)
- 测试与证据
- 交接信息(仓库 / 备份 / 部署 / 敏感项)
- 已知限制与技术债
- 待办与后续阶段
- 常用操作手册 附录 A · 证据索引 | 附录 B · 术语表
0. 这份文档怎么用
| 你是谁 | 建议读法 |
|---|---|
| 接手审查的 AI / 开发者 | 第 1、2、4 章建立全局 → 第 5 章理解演进 → 第 6、7 章核对证据 → 第 8、9 章看风险 |
| 只想改 UI | 第 2.5 节前端地图 + 仓库内《模块索引.md》(按区域点单,改完 node build.js 重新内联) |
| 只想改后端 | 第 2.2 节后端地图 + 第 2.3 节路由表 + 第 2.4 节 KV 键位图 |
| 只关心安不安全 | 第 3.6 节安全层 + 第 8 节已知限制 + 仓库内《安全说明.md》 |
| 只想复现测试 | 第 6 章测试矩阵 + 第 10 章命令 |
本文不包含:任何真实密钥、令牌、wrangler.toml 中的账号 ID 与 KV ID(这些只在本机工作区)。文中出现的 ID 均为仓库文件内已有的非机密标识。
1. 项目概况
1.1 一句话
一个单文件 Cloudflare Worker 全栈应用:前端是一个仿微信的高定制聊天界面(唯一真源 index.html,11241 行),后端是同一仓库的 worker.js(2510 行),构建时内联成单一部署文件 cat-chat-all-in-one.js,数据全部存 Cloudflare KV,大模型 Key 由用户在前端自行填写、经 Worker 代理调用。
1.2 基本信息
| 项 | 值 | 证据 |
|---|---|---|
| 产品名 | 猫爪聊天(交接代号:小手机) | 源码注释 |
| 线上地址 | https://zhai.beibbloveme.workers.dev | 【未验证】沙箱访问 workers.dev 返回 000,无法确认当前在线字节 |
| 部署平台 | Cloudflare Workers(name = "zhai") |
【源码】wrangler.toml |
| 数据存储 | Cloudflare KV(binding CHAT)+ 浏览器 localStorage/IndexedDB |
【源码】 |
| 前端 | 原生 HTML/CSS/JS 单页,无框架、无构建依赖 | 【源码】 |
| 后端 | Worker fetch 处理器,无框架 |
【源码】 |
| 模型 | OpenAI 兼容接口,多服务商可选(OpenAI / DeepSeek / 通义 / 自定义) | 【源码】 |
| Git 仓库 | https://github.com/beibbloveme-crypto/yuai.git | 【源码】git remote -v |
| 当前分支 | fix/p0-memory-consistency(领先 main 7 个提交,未推送) |
【实测】git status |
| 提交总数 | 20(基线 1 + UI/人设 5 + 阶段 0–4 五轮 + P0–P1 修复 7 + 文档 2) | 【实测】git log |
1.3 目录结构
/workspace/chat/
├── index.html # 前端唯一真源(11241 行 / 196KB)
├── worker.js # 后端唯一真源(2510 行 / 138KB)
├── build.js # 构建脚本(58 行):worker.js + index.html → 部署文件
├── cat-chat-all-in-one.js # 部署产物(2531 行 / 401KB)= 后端 + FRONTEND_B64 + serveFrontend()
├── wrangler.toml # Worker 配置(含真实账号/KV ID,勿外传)
├── deploy.sh # 备用部署脚本(CF_API_TOKEN 方式)
├── icons/ # PWA 图标(192/512/apple-touch)+ gen_icon.py 生成脚本
├── tests/ # 7 个测试脚本(Node 直调 + 内存 Mock KV)
├── 任务交接说明.md # 交接总览
├── 安全说明.md # 安全层设计
├── 审计报告-阶段0.md # 阶段 0 只读审计
├── 修复记录-P0-P1.md # 本轮修复的提交级记录
├── 模块索引.md # 改代码前的区域地图(按区点单)
├── 猫爪聊天-完整项目.md # 历史融合版全代码(占位符版,可外传)
├── 替换指南.md / 给其他AI-代码包/ # 历史交接材料
├── 效果图-*.png / 新界面-*.png # 17 张界面截图(其中 2 张未提交)
└── .wrangler/ # 本地 wrangler 状态(已 gitignore,可能含登录凭证)
2. 源码介绍
2.1 三个核心文件的分工(先理解这个,再谈任何改动)
index.html ──┐
├──► build.js ──► cat-chat-all-in-one.js ──► wrangler deploy ──► 线上
worker.js ───┘ (部署文件)
| 文件 | 角色 | 纪律 |
|---|---|---|
index.html |
前端唯一真源。改 UI 只改这里 | 改完必须 node build.js 重新内联,否则线上不生效 |
worker.js |
后端唯一真源。改后端只改这里 | 同上;新函数放在 export default { 之前(顶层) |
cat-chat-all-in-one.js |
构建产物,不要手工编辑 | 由 build.js 整体重写;部署前校验与 worker.js 前缀逐字节一致 |
历史上踩过的坑(构建漂移):早期
build.js只替换前端 base64、不同步后端,导致部署文件的后端长期停在旧版——线上跑的后端和仓库里的worker.js不是同一份代码。本轮已重写build.js为整体生成,并以 SHA256 校验前缀一致(c137a6ca…)。【实测】
build.js 核心逻辑(全文 58 行,摘录):
const b64 = Buffer.from(html, 'utf8').toString('base64');
const backend = fs.readFileSync(workerPath, 'utf8');
const serveBlock = `... const FRONTEND_B64 = "${b64}"; function serveFrontend(){...} ...`;
fs.writeFileSync(outPath, backend.replace(/\s+$/, '') + '\n' + serveBlock);
2.2 后端源码地图(worker.js,行号为当前 HEAD)
区域总表
| 区域 | 职责 | 关键函数(行号) |
|---|---|---|
| 工具层 | JSON 响应、时间键、时段解析 | json() 66 · dayKey() 77 · resolveTime() 84 · periodOf() 114 |
| P 记忆系统 | 统一 profile / 结构化档案 / 高光 / 经历 / 关系 | saveMemory() 129 · profileOf() 176 · msgKey() 182 · saveArchive() 187 · listArchiveKeys() 211 · listLegacyMemKeys() 230 · hlKey/expKey/relKey() 269-271 · listByPrefix() 273 · collectAllByPrefix() 289 |
| 高光 | 原话高光库 CRUD | listHighlights() 299 · addHighlight() 319 · deleteHighlight() 359 |
| 经历 | 经历记忆 CRUD(active/candidate) | listExperiences() 369 · addExperience() 389 · deleteExperience() 430 |
| 关系 | 高光↔经历关联 CRUD | listRelations() 440 · addRelation() 452 · deleteRelation() 490 · cascadeDeleteRefs() 499 |
| 引用完整性 | 删除时的引用检查与级联(P0-3) | findMessageRefs() 515 · softDeleteRefs() 535 |
| 记忆检索 | 旧记忆 + 高光 + 经历的上下文组装(P0-1/P1) | scanMemoryPrefix() 566 · listMemoryKeys() 591 · tokenize() 620 · retrieveMemory() 641 · retrieveContext() 695 · buildSystemPrompt() 716 |
| K 模型调用 | 多服务商代理 | callModel() 847 |
| L 解析层 | 回复标记解析 | parseFavorites() 931([[fav:…]])· parseStatus() 952([[status:…]]) |
| N 主动消息 | AI 主动搭话(按 profile 限额) | proactiveState() 980 · bumpProactive() 1014 |
| 静态资源 | PWA | pngResponse() 1036 · manifestResponse() 1050 |
| O 安全层 | 令牌/同源/限流/SSRF/脱敏 | toAppError() 1124 · shortId() 1129 · clientIp() 1137 · logEvent() 1146 · corsHeaders() 1153 · isSameOrigin() 1168 · getToken() 1193 · safeEqual() 1198 · checkAuth() 1209 · hashShort() 1214 · bumpCounter() 1226 · checkRate() 1249 · allowedHosts() 1263 · isForbiddenHost() 1271 · validateApiUrl() 1290 · cleanText() 1330 · stripMarkers() 1337 · sanitizeErrorText() 1344 · pageResponse() 1354 · handleBackup() 1377 |
| 主处理 | 聊天 / 主动消息入口 | handleChat() 2247 · handleProactive() 2444 |
关键源码摘录(理解系统行为的六个点)
① 记忆按 profile 隔离(P0-1,修掉跨角色泄漏)
async function saveMemory(env, profile, role, content, time) {
const p = VALID_PROFILE.test(String(profile || '')) ? String(profile) : DEFAULT_PROFILE;
const key = `mem:${p}:${ts}_${rand}`; // 旧格式为 mem_<ts>_<rand>(全局)
await env.CHAT.put(key, val);
await env.CHAT.put('meta_lastTs:' + p, String(ts)); // 最后聊天时间也按 profile
if (p === DEFAULT_PROFILE) await env.CHAT.put('meta_lastTs', String(ts)); // owner 兼容旧键
}
② 消息归档幂等(P0-2,修掉重试重复入档)
async function saveArchive(env, profile, rec) {
if (!rec || !rec.mid || !rec.content) return { skipped: true };
if (await env.CHAT.get(key)) return { skipped: true }; // 同 mid 只写一次
await env.CHAT.put(key, val);
}
③ 鉴权闸门(所有 POST 在进入任何业务逻辑前拦截)
const protectedRoute =
request.method === 'POST' ||
url.pathname === '/proactive' ||
url.pathname === '/backup' ||
url.pathname === '/init';
if (protectedRoute && !checkAuth(request, env)) {
return json({ error: 'unauthorized', rid }, 401);
}
getToken(env) = env.AUTH_TOKEN || DEFAULT_AUTH_TOKEN(1193 行)。令牌同时通过pageResponse()以window.__CAT_TOKEN__注入页面——这意味着知道线上地址的人就能从页面里拿到访问令牌(已知限制第 1 条,见第 8 节)。
④ 提示词组装(P1,记忆闭环)
function buildSystemPrompt(persona, time, ctx) {
// ctx 来自 retrieveContext():旧记忆 top5 + 高光 top3 + active 经历 top3 + 候选经历 top2
// 三段新增:长期高光原话 / 已确认经历 / 候选经历(明确标注"不得当作事实")
}
⑤ 模型上下文检索(P1)
async function retrieveContext(env, profile, query, time, limit = 5) {
// 同时取四路,全部按 profile:mem:<profile>:* / hl:<profile>:* / exp:<profile>:*(active|candidate)
}
⑥ 聊天主入口行为(handleChat,2247 行起)
- 解析
body.client_mid(前端生成的c_<ts>_<rand>),用户消息 mid 优先复用它 → 重试不产生重复档案; - AI 回复使用独立
ai_mid(a_<ts>_<shortId>),响应回传user_mid/ai_mid; - 同 mid 内容一致 → 跳过写入(
user_mid_reused=true);内容不一致 →409 mid_conflict; - 检索上下文 → 组提示词 →
callModel→ 解析[[status:]]/[[fav:]]→ 入档 → 返回。
2.3 路由表(全部接口)
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
/ 及其他未匹配路径 |
GET | 否 | 返回前端页面(运行时注入 __CAT_TOKEN__) |
/ 及其他未匹配路径 |
POST | 是 | 聊天主接口(任意 POST 路径都走这里) |
/manifest.webmanifest |
GET | 否 | PWA manifest |
/apple-touch-icon.png /icon-192.png /icon-512.png |
GET | 否 | PWA 图标 |
/health |
GET | 否 | 健康检查 {ok, service, version:"2.0"},不含配置密钥 |
/init |
POST | 是 | 初始化 KV schema(兼容保留) |
/backup |
GET | 是 | 导出最近 N 条记忆(?limit=200) |
/archive |
GET | 是 | 档案导出;支持 keyword/from/to/speaker/limit/offset 搜索分页(P1),过滤软删 |
/archive |
POST | 是 | 档案幂等导入 |
/export/full |
GET | 是 | 全量导出(schema 2:消息+高光+经历+关系+计数)(P0-4) |
/import/full |
POST | 是 | 全量导入:dry_run 预览、幂等、逐条 errors/warnings(P0-4) |
/highlights |
GET/POST/DELETE | 是 | 高光库;写入强制引用真实 msg:<profile>:<mid>(P0-2) |
/experiences |
GET/POST/DELETE | 是 | 经历记忆;source_mids 必须全部存在 |
/relations |
GET/POST/DELETE | 是 | 关联关系;两端必须存在 |
/proactive |
POST | 是 | 主动消息轮询(每日上限 + 最小间隔,按 profile) |
/messages/delete |
POST | 是 | 删除消息:mode=soft\|hard、force;有引用且无 force → 409 has_refs;force → 级联软删(P0-3) |
请求体上限 SEC.MAX_BODY = 8MB(P0-4 为全量导入从 1MB 提升);单条消息 4000 字;图片 800KB。
2.4 KV 键位图(CHAT 命名空间)
| 键模式 | 内容 | 备注 |
|---|---|---|
mem:<profile>:<ts>_<rand> |
长期记忆(role/content/ts/day_key/hour/period) | P0-1 新格式 |
mem_<ts>_<rand> |
旧格式记忆 | 仅 owner 只读兼容,开关 MEM_READ_LEGACY |
meta_lastTs:<profile> / meta_lastTs |
最后聊天时间(主动消息间隔判断) | 后者为 owner 兼容旧键 |
msg:<profile>:<mid> |
结构化消息档案(原文/角色/时间/附件/deleted_at) | 阶段 2/3 引入 |
hl:<profile>:<id> |
高光(原话 + 来源 mid + 分类) | 阶段 4 引入 |
exp:<profile>:<id> |
经历(active/candidate + certainty + source_mids) | 阶段 4 引入 |
rel:<profile>:<id> |
关系(高光↔经历,deleted_at 软删) |
阶段 4 引入 |
proactive:<profile>:<day> / proactive_<day> |
主动消息每日计数 | 后者为旧键 |
rl_<hash> / rl_global |
限流计数(分钟/日/全局三级,TTL 172800 秒) | 安全层 |
<profile> 默认 owner,校验 ^[a-z0-9_]{1,32}$;测试角色必须用不同 profile_id 与正式数据隔离。
2.5 前端源码地图(index.html,改 UI 前先看《模块索引.md》)
| 区域 | 名称 | 关键选择器 / ID / 函数 |
|---|---|---|
| A | 顶部栏 | .header #fullscreenBtn #openSetting #multiHeader #multiDel;updateFsBtn() |
| B | 消息区 | #chatBox .msg-row .bubble .bubble-img .msg-status-text .time-divider;appendMessage() updateMessage() refreshTimeDividers() |
| C | 底部输入栏 | #messageInput #sendBtn #pawBtn #imgPreviewArea #replyBar;sendMessage() requestChat() compressImage() |
| D | 猫爪面板 | 照片/表情包/颜文字/收藏四页签;#pickPhotoBtn #photoSheet #pawPanel;openPawPanel() switchTab() renderFavorites() |
| E | 设置面板 | #settingModal:状态喵/头像/外观气泡/后端与模型/数据备份;#exportBackupBtn #importBackupBtn #cloudSyncBtn #cloudRestoreBtn #memoryBtn #memoryModal |
| F | 通用弹层 | 灯箱 .img-viewer、showToast()、确认弹窗 #confirmModal、长按菜单 #longMenu |
| G | 数据层 | localStorage 设置 + IndexedDB 消息历史;readMessages() saveMessages() normalizeMessages();保护键 messages_corrupt messages_pre_restore |
| H | 接口层 | requestChat(text, img, clientMid)、checkProactive()、worker_url 取值 |
| I | 外观 | CSS 变量 --bubble-me-* --bubble-ai-* --topbar-* --bottombar-*;applyAppearance() applySkinClass() |
P0-2 起前端新增的关键字段/行为:每条消息对象带 mid(c_<ts>_<rand>);requestChat 携带 client_mid;重试复用同一 mid(幂等);收藏/云同步/恢复携带真实 mid;多选删除先 POST /messages/delete 成功后再删本地;AI 的 [[fav:]] 也进入云端高光。
2.6 六条主数据流
- 发消息:
sendMessage()→ 生成mid→ 本地渲染 →requestChat(text, img, clientMid)POST → Worker 鉴权/限流 → 按 profile 检索上下文 → 组提示词 →callModel→ 解析标记 →saveArchive入档 → 返回{reply, user_mid, ai_mid}→ 前端回写 mid 与状态。 - 重试:复用同一
mid→ Worker 发现档案已存在且内容一致 → 跳过写入(不重复归档),但仍调用模型生成新回复(新ai_mid)。 - 多选删除:前端收集 mid →
POST /messages/delete(默认 soft)→ 有引用返回 409 列表 → 用户确认 force → 级联软删 → 成功后本地再删 → 失败保留本地并提示(防“恢复复活”)。 - 云同步:
cloudSyncToServer()→POST /archive(带m.mid)→ 幂等写入msg:<profile>:<mid>。 - 云恢复:
cloudRestoreFromServer()→GET /archive→ 合并本地;恢复前本地另存messages_pre_restore。 - 全量导出/导入(API,暂无 UI):
GET /export/full→ JSON(schema 2)→ 异机POST /import/full?dry_run=1预览 → 正式导入逐条报告。
3. 功能介绍(完整清单)
3.1 聊天核心
- 文字消息(4000 字上限)、图片消息(相册/相机/文件三入口,前端压缩 ≤800KB)
- 表情包(微信式压缩,长边 240px,透明图存 PNG)、颜文字、收藏夹
- 消息状态:发送中/失败/已送达;失败可重试(幂等)
- 头像上方状态文字(
[[status:…]]由 AI 生成并解析展示) - 居中时间条、按时间分组刷新
- 引用回复(
#replyBar) - 图片灯箱查看
- 多选操作(全选/批量删除)
3.2 AI 行为
- 主动消息:AI 可主动搭话,每日上限
PROACTIVE_MAX_PER_DAY、最小间隔PROACTIVE_MIN_GAP,按 profile 计数 - 人设:仅自定义人设(设置里填写),无则完全不放人设段;提示词内含身份认知、时间感知(
resolveTime/periodOf)、状态/收藏引导 - 记忆闭环(P1):每次请求自动携带——长期记忆 top5、高光原话 top3、已确认经历 top3、候选经历 top2(候选明确标注“不得当作事实”)
3.3 多服务商模型
前端可选 OpenAI / DeepSeek / 通义 / 自定义,Key 存浏览器本地,经 Worker 代理;validateApiUrl() 白名单 + https + 443 + 禁内网 IP + 禁重定向。
3.4 外观定制(区域 I)
全局底色、顶栏色、底栏色、双方气泡颜色/透明度/磨砂模糊、气泡文字色、自定义气泡皮肤(PNG 九宫格 border-image-slice:33%)、全屏模式、状态文字贴头像定位。
3.5 备份与恢复(三层)
| 层 | 入口 | 内容 |
|---|---|---|
| 本地导出/导入 | 设置 → 数据备份 | localStorage 设置 + IndexedDB 消息全量;导入前自动另存 messages_pre_restore,加载异常时保护为 messages_corrupt |
| 云端同步/恢复 | 设置 → 云同步/恢复 | 消息档案 msg:<profile>:<mid> 幂等同步、按 profile 恢复 |
| 全量导出/导入(API) | GET /export/full / POST /import/full |
schema 2:消息+高光+经历+关系+计数;dry_run 预览;幂等;逐条 errors/warnings;软删记录保留 |
3.6 记忆系统(阶段 2–4 + P0–P1)
| 能力 | 存储 | 进入模型上下文 |
|---|---|---|
| 长期记忆(旧) | mem:<profile>:*,关键词检索 |
是(top5) |
| 高光原话库 | hl:<profile>:*,必须引用真实消息 |
是(top3) |
| 经历记忆 | exp:<profile>:*,active/candidate + 确信度 |
是(active top3 + candidate top2) |
| 关联关系 | rel:<profile>:*,高光↔经历 |
否(界面时间线/关联查看) |
| 记忆库 UI | 设置 → 记忆库(三标签面板) | — |
3.7 安全层(安全说明.md 有完整设计)
访问令牌认证(任意 POST / /proactive /backup /init 必须带头)、同源校验、三级限流(IP 分钟/IP 日/全局,KV 计数)、请求体与消息体积限制、出站地址白名单与 SSRF 防护、提示词注入边界(<<< >>> + 最高优先级安全段)、错误脱敏(对外只给错误码 + rid)、日志 rid 追踪。
3.8 PWA
manifest + 192/512/apple-touch 三图标(icons/gen_icon.py 生成),可“添加到主屏幕”。
4. 架构与数据流
4.1 总体架构
┌─────────────────────────── 浏览器 ───────────────────────────┐
│ index.html(单页) │
│ UI 层(区域 A-F) 数据层 G(localStorage + IndexedDB) │
│ 接口层 H(requestChat / checkProactive / 云同步 / 全量导入) │
└───────────────┬──────────────────────────────────────────────┘
│ HTTPS(带令牌 X-Cat-Token / body.token)
┌───────────────▼──────────────────────────────────────────────┐
│ Cloudflare Worker(cat-chat-all-in-one.js) │
│ 安全闸门:同源 → 令牌 → 限流 → 体积 → 路由分发 │
│ /chat(任意POST) /archive /export|import /highlights... │
│ /proactive /messages/delete /backup /init /health │
│ 检索(retrieveContext)→ 提示词(buildSystemPrompt) │
│ → callModel(出站白名单)→ 解析(status/fav)→ 入档 │
└───────────────┬──────────────────────────────────────────────┘
│
┌───────────────▼──────────────────────────────────────────────┐
│ Cloudflare KV(CHAT):mem:* / msg:* / hl:* / exp:* / rel:* │
│ meta_lastTs:* / proactive:* / rl_* │
└───────────────────────────────────────────────────────────────┘
│ 出站
┌───────────────▼──────────────────────────────────────────────┐
│ 大模型 API(OpenAI 兼容,多服务商,Key 由前端提供) │
└──────────────────────────────────────────────────────────────┘
4.2 存储分层
| 层 | 存什么 | 丢失后果 |
|---|---|---|
| localStorage | 设置、外观、模型配置、worker_url、令牌覆盖 |
可重设 |
IndexedDB(messages) |
本地聊天历史 | 可从云端恢复 |
| KV(云端) | 档案、记忆、高光、经历、关系 | 仅剩本地 |
4.3 一次聊笑的完整链路(按代码顺序)
请求 → CORS/同源 → 令牌闸门(④) → 路由匹配 → 限流 checkRate → 体积检查 → handleChat → profileOf 定 profile → 解析 client_mid → saveArchive(幂等)→ retrieveContext(四路检索)→ buildSystemPrompt → callModel(validateApiUrl → fetch → 错误脱敏)→ parseStatus/parseFavorites → saveMemory + 高光/收藏入库 → 返回 {reply, user_mid, ai_mid, should_*} → 前端回写 mid/状态。
5. 开发进程记录
5.1 Git 全历史(20 个提交,git log --oneline)
| # | 提交 | 阶段 | 内容 |
|---|---|---|---|
| 1 | afa0aa4 |
基线 | 猫爪聊天 v2 完整版(功能 + UI 定制 + 安全加固) |
| 2 | 352bc65 |
基线 | 取消默认人设:只用自定义人设,无则完全不放人设段 |
| 3 | 790e7a8 |
基线 | 文档:模块索引区域 J 去除 DEFAULT_PERSONA 引用 |
| 4 | 9f3a385 |
基线 | 部署取消默认人设版本 3c0cd4a7,token 恢复存入本地 |
| 5 | 1b18312 |
基线 | 修复自定义人设不生效:人设优先级声明 + 历史角色隔离 |
| 6 | d0bd491 |
UI | 输入栏贴底去描边、顶栏去胶囊、删备注与深色模式、时间条白字深影 |
| 7 | f2c6dbd |
阶段 0 | 只读审计:顶栏白条修复部署 + 提交源码审计报告(不改正式代码) |
| 8 | 08815a9 |
阶段 1 | UI v2(去标题/设置贴右上/底部悬浮胶囊)+ 修 B1 误清空 + 完整备份恢复 |
| 9 | 7702738 |
UI | 顶栏改零占位悬浮层(去透明方块)+ 输入栏贴近 Home 指示条 |
| 10 | b3cba15 |
文档 | 补录续十二 |
| 11 | e6c9661 |
阶段 2/3 | 统一 profile_id 标识 + 完整云端档案与同步(本地↔云端) |
| 12 | ca44bb8 |
阶段 4 | 记忆系统:高光原话库 + 经历记忆 + 时间线/关联(后端 + 前端记忆库面板) |
| 13 | 75657e0 |
文档 | 续十三(阶段 2/3)+ 续十四(阶段 4)+ 模块索引 P 区 + 记忆库效果图(= main 顶点) |
| 14 | 9505597 |
P0-1 | 记忆按 profile 隔离(mem:<profile>:*),旧 mem_* 仅 owner 只读兼容 |
| 15 | 513f24b |
P0-2 | 稳定 client_mid 幂等归档 + AI 独立 mid + 高光/经历/关系引用真实消息 |
| 16 | d4aa34e |
P0-3 | 新增 /messages/delete(软删/硬删 + 引用检查 + 级联软删),archive 过滤软删,前端先服务端后本地 |
| 17 | 2460f9a |
P0-4 | 全量导出/导入(schema 2、dry_run 预览、幂等、错误/警告报告、图片附件元数据、软删保留) |
| 18 | 5bfe8c8 |
P1 | 高光/经历/候选进入模型上下文(带来源、候选单列)+ 档案搜索 |
| 19 | 5b01441 |
构建修复 | build.js 改为由 worker.js 重新生成部署文件(消除后端漂移)+ 阶段 4 v2 回归 |
| 20 | b03a612 |
文档 | P0-P1 修复记录(当前 HEAD) |
分支:fix/p0-memory-consistency(14–20,7 个提交,未推送)|main = 75657e0(2026-10-19 15:49)。
5.2 阶段 0–4 简述
| 阶段 | 目标 | 落点 |
|---|---|---|
| 0 | 只读审计,定位顶栏白条等显示问题 | 提交 f2c6dbd,只出报告不改码 |
| 1 | 修 B1 误清空 + 完整备份恢复 + UI v2 | 08815a9 |
| 2/3 | 统一 profile 标识 + 云端结构化档案与同步 | e6c9661 |
| 4 | 记忆系统(高光/经历/关系 + 记忆库面板) | ca44bb8 |
5.3 审计进程(修复前发生了什么)
- 交接只读检查 ×3:仓库地址/私有性/备份时间/未提交项;两个未跟踪截图文件(
新界面-v3-贴底无方块.png、新界面-顶部无壳.png,UI 效果图,无功能作用);部署版本证据(Cloudflare API 只读确认f490d9d2在线,早于本轮修复)。 - 完整只读审计:以任务书为验收基准,输出功能清单、架构与数据流、逐条需求验收表、风险清单、证据索引。发现高危项:
mem_*跨 profile 泄漏、高光/经历不进模型上下文、mid 断链、重试重复入档、本地删除不落云端导致恢复复活、生产 KV 无全量备份、令牌随页面公开下发。 - 第二轮只读复核:逐项追踪泄漏条件、ID 不一致点、重复入档与云端复活是必然还是条件性,标注证据等级与置信度。
- 夜间自主推进(只读 + 本地虚构数据):交叉复核、整理可重复测试(
/workspace/小手机-审计草案/,含 21/21 通过的隔离测试)、分阶段修复方案、独立草案补丁、交接文档。未改正式源码、未提交、未部署、未读真实数据。
5.4 修复阶段(P0-1 → P0-4 + P1,每个都先测试后提交)
| 项 | 问题(审计实证) | 修法 | 测试 |
|---|---|---|---|
| P0-1 | mem_* 全局键:图片消息/主动消息无条件跨 profile 泄漏,文本关键词重叠时条件泄漏 |
新键 mem:<profile>:*;旧键仅 owner 只读(MEM_READ_LEGACY);meta_lastTs/主动计数全部按 profile |
15/15(含图片、主动、关键词重叠、旧数据仅 owner) |
| P0-2 | 前端本地数字 id 与档案 u_/a_ 断链;重试重复入档;高光可引用不存在的消息 |
client_mid 随请求;saveArchive 幂等;同 mid 不同内容 409;高光/经历/关系强制引用真实 msg: 键(STRICT_MID) |
21/21 |
| P0-3 | 本地删除不落云端,云恢复后“复活” | 新增 /messages/delete(soft/hard + 引用检查 409 + 级联软删);/archive 过滤 deleted_at;前端先服务端后本地 |
23/23 |
| P0-4 | 生产 KV 无全量备份手段 | /export/full + /import/full(schema 2、dry_run、幂等、逐条报告);MAX_BODY 1MB→8MB |
27/27 |
| P1 | 高光/经历/关系完全不进模型请求,记忆系统对 AI 不可见 | retrieveContext 四路检索;buildSystemPrompt 三段注入(候选单列并标注“不得当作事实”);/archive 搜索分页 |
13/13 + 14/14 |
| 构建 | 部署文件后端停在旧版(漂移) | build.js 整体重生成;SHA256 前缀一致校验 |
阶段4 v2 32/32(worker.js + 部署文件双模块) |
历史
test_phase4.mjs有 12 项失败,原因是旧测试用假 mid(u_111)被新的严格引用校验拒绝——属测试数据过期,非功能回归,已由test_phase4_v2.mjs(真实来源)替代。【实测】
5.5 当前 Git 状态(2026-10-22)
- 分支:
fix/p0-memory-consistency@b03a612;main@75657e0;远端origin/main存在(沙箱内 GitHub 返回 403/404,远端实时状态未验证)。 - 与
main差异:12 个文件,+3229 / -193(worker.js +853 行为主)。 - 工作区未跟踪文件 2 个:
新界面-v3-贴底无方块.png、新界面-顶部无壳.png(均为 UI 效果图)。 - 未 push、未部署。线上仍是修复前的
f490d9d2。
6. 测试与证据
6.1 测试矩阵(全部本地运行,内存 Mock KV + Mock 模型 + 虚构数据 + 虚构令牌)
| 测试文件 | 结果 | 覆盖 |
|---|---|---|
tests/test_p0_memory_isolation.mjs |
15/15 | 记忆跨 profile 隔离(正/反/图片/主动/关键词重叠/旧数据仅 owner) |
tests/test_p0_mid_refs.mjs |
21/21 | mid 回写、重试幂等、409 冲突、引用校验、图片元数据 |
tests/test_p0_delete_sync.mjs |
23/23 | 软删/硬删、引用 409、级联、防复活、profile 隔离删除 |
tests/test_p0_export_import.mjs |
27/27 | 全量导出/导入、dry_run、幂等、错误/警告、软删保留 |
tests/test_p1_memory_context.mjs |
13/13 | 高光/经历进 prompt、候选分离、跨 profile、条数上限 |
tests/test_p1_archive_search.mjs |
14/14 | 关键词/时间/说话方/分页/软删过滤 |
tests/test_phase4_v2.mjs |
32/32 | 阶段 4 原功能回归(worker.js + 部署文件各 16 项) |
历史回归(/tmp,修复期跑过) |
phase1 10/10 · phase23 14/14 · ui_v2 32/32 · ui_v3 12/12 | UI 与原功能回归 |
6.2 构建一致性【实测】
cat-chat-all-in-one.js的 worker 前缀与worker.js逐字节一致(SHA256 同为c137a6ca…);- 部署文件内联前端与
index.html一致。
6.3 复跑方式
cd /workspace/chat
for f in tests/test_p0_memory_isolation.mjs tests/test_p0_mid_refs.mjs \
tests/test_p0_delete_sync.mjs tests/test_p0_export_import.mjs \
tests/test_p1_memory_context.mjs tests/test_p1_archive_search.mjs \
tests/test_phase4_v2.mjs; do node "$f"; done
语法校验(对部署文件):cp cat-chat-all-in-one.js /tmp/w.mjs && node --check /tmp/w.mjs。
6.4 尚未覆盖(诚实声明)
- 未做线上验证:沙箱无法访问 workers.dev(返回 000),线上字节与 KV 实际内容未核对。
- 未做浏览器端 E2E:Playwright 方案就绪(本地
wrangler dev --local),本轮未重跑全链路 UI。 - 图片本体、R2 迁移:未开始(见第 9 节)。
7. 交接信息
7.1 仓库与备份
| 项 | 状态 |
|---|---|
| GitHub 仓库 | https://github.com/beibbloveme-crypto/yuai.git(git remote -v;公开/私有属性与远端实时状态【未验证】) |
| 最近一次进入远端的提交 | 75657e0(2026-10-19 15:49,= main);此后 7 个修复提交只在本地 |
| 未提交修改 | 2 个未跟踪 PNG(UI 效果图);无已修改未暂存文件 |
| 本地即最新 | 是——所有代码在 /workspace/chat/ 工作区,丢失本地 = 丢失 P0–P1 修复 |
7.2 部署
| 项 | 值 |
|---|---|
| 线上 Worker | zhai,部署文件 cat-chat-all-in-one.js |
| 当前线上版本 | f490d9d2-8ee5-4d0c-9274-b082b5d34990,2026-10-19T07:14:23Z 起 100% 在线【Cloudflare API 只读查询】 |
| 与本地关系 | 线上早于 P0–P1 修复,跑的是修复前代码 |
| 回滚锚点 | 若部署出问题:npx wrangler rollback f490d9d2-8ee5-4d0c-9274-b082b5d34990 |
| 查询版本 | npx wrangler deployments list --json(只读) |
7.3 敏感信息处理
wrangler.toml含真实 account_id 与 KV id,仅存本机工作区,勿外传;对外分享用《猫爪聊天-完整项目.md》(占位符版)。.wrangler/可能含登录凭证,已 gitignore,不要提交、不要打包外发。- Worker 源码内
DEFAULT_AUTH_TOKEN为内置默认令牌,且会随页面下发给浏览器——知道线上地址即可获得访问令牌(已知限制第 1 条)。轮换方式:wrangler secret put AUTH_TOKEN(未执行,待授权)。 - 本文档及所有报告不含任何真实密钥。
7.4 最小交接文件清单
| 类别 | 文件 |
|---|---|
| 必需 | index.html worker.js build.js wrangler.toml icons/(3 png + gen_icon.py)tests/(7 个) |
| 可选 | 各 .md 文档、deploy.sh、新界面-*.png 效果图 |
| 敏感(勿外传) | wrangler.toml 真实值、.wrangler/、任何含真实 KV 数据的导出文件 |
| 可重新生成 | cat-chat-all-in-one.js(node build.js) |
8. 已知限制与技术债
- 令牌随页面公开下发:
pageResponse把__CAT_TOKEN__注入页面,任何能打开线上地址的人都能拿到访问令牌(可读写该 Worker 的全部数据)。缓解:轮换AUTH_TOKENsecret、后续阶段改独立强认证(阶段 7)。这是当前最大安全面。 - 图片本体不入云档:云端只存
message_type:'image'与附件元数据;图片本体恢复需 R2(未启用、涉及付费、未授权)。 - 旧本地消息无 mid:更早的本地消息只能本地删除;若曾被归档,云端记录可能仍在。
- 重试会生成新的 AI 回复:用户消息幂等,但每次重试仍调模型、写新
ai_mid。 - 搜索为扫描式:最多扫 20,000 key,无索引;数据量大时 KV 读成本上升。
- 全量导入上限 8MB:更大备份需分批。
STRICT_MID默认开启:外部脚本用假 mid 建高光/经历/关系会得 400(刻意的完整性约束)。- 旧
mem_*不迁移不删除:仅 owner 只读;其他角色不可读。 - 全量导出/导入、档案搜索暂无前端 UI:目前是受令牌保护的 API;现有“云端同步/恢复”按钮继续可用。
- 未部署:线上仍为
f490d9d2,本地修复未上线。 - index.html 存在同名选择器重复定义(
.avatar.header.bubble等 2 份以上,以最后一份为准)——改样式前必须grep -n "选择器{"数一遍(《模块索引.md》第五节有清单)。 cat-chat-all-in-one.js是构建产物但被 Git 跟踪:历史上它才是“真正部署的文件”,保留跟踪便于回滚;修改必须经build.js。
9. 待办与后续阶段
9.1 立即可做(需你授权)
| 事项 | 前提 |
|---|---|
git push 分支 fix/p0-memory-consistency 到 origin |
你批准推送 |
部署到线上(node build.js && npx wrangler deploy) |
你批准部署;建议先轮换 AUTH_TOKEN |
| 前端补全量导出/导入 UI(接口已就绪) | 你批准改前端 |
| 前端补档案搜索面板 + “跳转原消息/上下文”(接口已就绪) | 同上 |
| 图片本体入 R2 | 涉及付费(开通 R2 需绑定支付方式),需你明确授权和预算 |
9.2 原任务书后续阶段(未开始)
| 阶段 | 内容 |
|---|---|
| 5 | 人设四层 + 版本管理 |
| 6 | 成长队列 |
| 7 | 后台独立强认证(解决第 8.1 条令牌问题) |
| 8 | 数据迁移 |
| 9 | 清理 |
10. 常用操作手册
# 改前端后重新生成部署文件(必须!)
cd /workspace/chat && node build.js
# 语法校验部署文件
cp cat-chat-all-in-one.js /tmp/w.mjs && node --check /tmp/w.mjs
# 跑全部测试
for f in tests/test_*.mjs; do node "$f"; done
# 本地起前端(file:// 下 localStorage 被禁用,必须走 http)
python3 -m http.server 18924
# 查看与 main 的差异(可审查)
git diff main..HEAD --stat
git log main..HEAD --oneline
# 只读查询线上部署版本
npx wrangler deployments list --json
# 回滚线上(紧急)
npx wrangler rollback f490d9d2-8ee5-4d0c-9274-b082b5d34990
# 代码回滚(放弃本地修复)
git checkout main
# 或逐个 revert
git revert 5b01441 5bfe8c8 2460f9a d4aa34e 513f24b 9505597
附录 A · 证据索引
| 结论 | 证据位置 |
|---|---|
| 修复内容与提交级记录 | 修复记录-P0-P1.md(仓库内,已提交) |
| 改代码前的区域地图 | 模块索引.md(仓库内) |
| 安全层设计 | 安全说明.md(仓库内) |
| 阶段 0 审计 | 审计报告-阶段0.md(仓库内) |
| 审计草案(复核报告/修复方案/测试方案/交接补全) | /workspace/小手机-审计草案/01–04*.md + test_cross_profile_and_ids.mjs(21/21) |
| 隔离测试(跨 profile + ID) | /workspace/小手机-审计草案/test_cross_profile_and_ids.mjs |
| 线上版本证据 | Cloudflare API 只读查询(2026-10-19/20 多次,f490d9d2 在线) |
| 测试原始输出 | 本文第 6 章 + 修复记录-P0-P1.md 第四节 |
附录 B · 术语表
| 术语 | 含义 |
|---|---|
| profile | 角色标识(默认 owner),所有 KV 数据按它隔离;测试角色必须用不同值 |
mid / client_mid |
消息唯一 ID;client_mid 由前端生成(c_<ts>_<rand>),AI 消息为 a_<ts>_<shortId> |
| 高光 | 值得记住的原话,必须引用真实消息 |
| 经历 | 从对话沉淀的事件记忆,分 active/candidate |
| 关系 | 高光↔经历的关联 |
| 软删 | 记录打 deleted_at 不物理删除,导出/检索时过滤,可恢复 |
| 漂移 | 部署文件与源码不一致(已修复) |
| STRICT_MID | 严格引用校验开关(默认开) |
| MEM_READ_LEGACY | 旧 mem_* 兼容读取开关(默认开,仅 owner) |
本文档由本地只读检查 + 实际运行测试生成;未修改任何源码、未提交、未部署、未读取任何真实用户数据。