小手机(猫爪聊天)· 完整交付文档
一份文档看懂整个项目:源码结构 + 功能介绍 + 开发进程 + 测试证据 + 交接信息
生成时间: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) |
本文档由本地只读检查 + 实际运行测试生成;未修改任何源码、未提交、未部署、未读取任何真实用户数据。