小手机项目 · 全项目代码基线审查报告(只读版)
审查对象: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_lastTs L158,owner 时),读-改-写无原子性,并发聊天互相覆盖(last-write-wins),影响"距上次聊天太近"判定精度。
/messages/delete(L2121-2169):软删打 deleted_at;硬删(mode:'hard' && force)env.CHAT.delete(key) 只删消息本体。
- 【存在缺陷】硬删留记忆碎片:
cascadeDeleteRefs(L499)只级联清理 hl:/exp:/rel: 引用,mem: 前缀的记忆碎片不会被清理——硬删消息后,由该消息抽取的记忆永久残留,且仍参与检索。
/archive POST 导入(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 无环境隔离:单一
CHAT namespace,测试与生产数据若共用即互相污染(当前测试用 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_TOKEN secret 替换硬编码默认值;建立轮换流程;评估"共享静态令牌"是否足够(当前页面注入方案决定了令牌必然随页面下发,跨站已被 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:HEAD f02cc07(19:19:35 +0800,较部署早 8 秒,内容吻合)
- 构建核验:
/tmp/buildcheck 重新构建产物与仓库 cat-chat-all-in-one.js diff 为空
- 分支核验:
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 原生演进路径。
本阶段任务至此结束。按任务书要求,交付本报告后停止,不开始重构,不开发新记忆系统,不修改任何既有代码与线上配置。