f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
11 KiB
11 KiB
桌面伙伴记忆:共享/私有作用域 + 编辑 + 多入口 设计
状态:已确认(方案 A 整体交付)。日期:2026-06-29。
背景与问题
桌面伙伴的记忆有两套互不相干的系统:
| 系统 | 存储 | 类别 | 用途 |
|---|---|---|---|
| 伙伴记忆(本设计目标) | SQLite companion_memories(crates/backend/nomifun-companion) |
profile/preference/knowledge/episode/task/affective 六类 | 注入伙伴人格 prompt + 对话中 recall_memories |
| Agent 编码记忆 | 文件 MEMORY.md(crates/agent/nomi-memory) |
user/feedback/project/reference | Claude-Code 式长期记忆,不在本设计范围 |
排查发现"找不到记忆编辑入口"是三个问题叠加:
- 入口埋得深:记忆 UI(
MemoriesTab.tsx)藏在 nomi 页"共享(Shared)"域下,页面默认落"伙伴/概览"域,需先切换无"记忆"字样的域单选才看得到(nomi/index.tsx:53,167-171)。 - 悬浮伙伴到不了记忆:悬浮窗右键菜单只有 4 项(打开对话/打开设置/清除未读/隐藏),"打开设置"跳
tab=settings而非 memories(companionNativeMenu.ts:16-23、companion/index.tsx:1304-1321)。 - 没有编辑按钮:
MemoriesTab只能 增/置顶/归档/删除,内容是只读<div>(MemoriesTab.tsx:146)。但updateMemoryIPC 已支持content?,后端 PUT 已用 COALESCE 落库(store.rsupdate_memory)——所以"编辑内容"本是纯前端缺口。
附带发现的代码设计问题(本设计一并修复):
- 脱敏不对称:
insert_memory跑nomi_redact::redact_secrets,update_memory不跑。 - 更新无内容校验:
add_memorytrim/拒空,update_memory不会。 - 事件不对称:只有
companion.memory-created且只在对话保存路径发;HTTP 新增/编辑/删除都不发事件,前端只监听 created。 - 作用域列是死列:
companion_memories有scope_kind TEXT DEFAULT 'user'+scope_companion_id TEXT(v2→v3 迁移加),但 struct 不含、row_to_memory不读、所有写/读/注入/recall 都不用 → 记忆事实上全局共享。 companion_skills已把同一套作用域机制完整接通(scope_kind'user'/'companion'+scope_companion_id''=共享,查询WHERE scope_companion_id=? OR scope_kind='user',SkillScope{Shared,Companion(id)}枚举)——本设计照搬该蓝图到记忆。
目标
- 伙伴记忆区分共享(所有伙伴可见)与私有(仅归属伙伴可见),两者皆可编辑。
- 编辑可改 内容 + 共享/私有归属(不改 kind/tags/importance/strength)。
- 三个入口:悬浮伙伴右键菜单、侧边栏/nomi 页一级可见、对话窗口内。
- 顺带修复上述后端安全/事件隐患。
非目标:语义检索/embedding;手动编辑 strength/importance/kind/tags;回溯改写进行中对话已烘焙的 prompt。
概念模型
| 作用域 | scope_kind |
scope_companion_id |
可见范围 |
|---|---|---|---|
| 共享 | 'user' |
'' |
所有伙伴 |
| 私有 | 'companion' |
归属伙伴 id | 仅该伙伴 |
伙伴 C 可见记忆 = scope_kind='user' OR scope_companion_id = C(与 companion_skills 一致)。
默认归属:
- 对话保存(
CompanionStoreSink::save,source=chat)→ 私有给该伙伴(复用 owning-companion 解析)。 - 学习中枢(
learner,source=learn)→ 共享。 - 手动新增(route/UI)→ 请求携带,UI 选择,默认共享。
实现分层
A. 数据层 crates/backend/nomifun-companion/src/store.rs
- 新增
MemoryScope { Shared, Companion(String) }枚举 +scope_columns()/from_columns()辅助(镜像SkillScope/service.rs:scope_for)。 CompanionMemory加字段scope_kind: String、scope_companion_id: String。row_to_memory:读出两列,scope_companion_id用COALESCE(...,'')归一 NULL→''。- 迁移:新增幂等回填步骤
UPDATE companion_memories SET scope_companion_id='' WHERE scope_companion_id IS NULL(旧行scope_kind='user'即共享,符合现状)。 insert_memory:签名加scope: MemoryScope,INSERT 列含 scope_kind/scope_companion_id。insert_memory_raw(import):INSERT 列含两列(struct 已带,import 可往返)。memories_for_injection:签名加companion_id: &str,两个 SELECT 加AND (scope_kind='user' OR scope_companion_id = ?)。MemoryFilter:加scope_companion_id: Option<String>;list_memories据此加同款谓词。update_memory:除 content/pinned/status,接受可选scope: Option<MemoryScope>;对 content 重跑nomi_redact::redact_secrets;trim 拒空。
B. 服务/路由/事件 service.rs / routes.rs / events.rs / companion.rs / learner.rs / export.rs
service.add_memory:加scope参数并下传;保留 kind/content 校验。service.update_memory:下传 scope;status 枚举校验(已有)+ content trim;若scope_kind='companion',校验 companion_id 存在。routes.rs:AddMemoryRequest加scope_companion_id: Option<String>(''/缺省=共享)。UpdateMemoryRequest加scope_kind: Option<String>/scope_companion_id: Option<String>。
events.rs:新增emit_memory_updated(memory)→companion.memory-updated、emit_memory_deleted(id)→companion.memory-deleted。HTTP add 路径补发memory-created;PUT 发 updated;DELETE 发 deleted。- 默认归属接线:
CompanionStoreSink::save→Companion(owning_id);learnerinsert→Shared;route add→请求。 build_companion_system_prompt:加companion_id形参并下传memories_for_injection;更新全部调用点。- recall:把伙伴 id 传进
MemoryFilter.scope_companion_id,recall返回 共享+该伙伴私有。
C. IPC/TS ui/src/common/adapter/ipcBridge.ts
ICompanionMemory加scope_kind: 'user'|'companion'、scope_companion_id: string。addMemory/updateMemory/listMemoriespayload 加作用域字段。- 新增
onMemoryUpdated、onMemoryDeletedWS 监听。
D. 前端
- D1 编辑(
MemoriesTab.tsx):每行加"编辑"按钮 → 弹窗改 content + 归属选择器(共享/私有给某伙伴);工具栏加作用域筛选(全部/共享/仅当前伙伴);每行作用域徽标;新增弹窗也带归属选择器;订阅 updated/deleted 实时刷新;加"编辑只影响新对话+实时 recall"的说明文案。 - D2 入口① 悬浮伙伴右键菜单(
companionNativeMenu.ts/companion/index.tsx):CompanionMenuAction加'open-memories';菜单加"打开记忆"项 →openMainAt('/nomi?companion={id}&tab=memories');更新companionNativeMenu.test.ts。 - D3 入口② 侧边栏/nomi 可发现性(
nomi/index.tsx):把memories从"共享"域移到"伙伴"域,成为一级"记忆"标签;scope-aware:选中伙伴时默认显示 共享+该伙伴私有,作用域筛选保留"全部伙伴"。 - D4 入口③ 对话窗口内(
companion/index.tsx):对话中onMemoryCreated/updated 触发显示低调小条"记下了:<摘要>"+"编辑/管理"动作 →openMainAt('/nomi?companion={id}&tab=memories');聊天栏加"记忆"小入口同样跳主窗口。不在 240×214 小窗内行内编辑。 - i18n:新 key(
nomi.memories.edit/saved/scope/scopeShared/scopePrivate/scopeFilterAll|Shared|Private/editHint、nomi.menuOpenMemories等)补 en-US + zh-CNnomi.json,重新生成i18n-keys.d.ts。
数据流
- 新增(手动):UI 弹窗(content+scope) →
POST /api/companion/memories→service.add_memory(scope)→insert_memory(..,scope)→ emit created → 各面板刷新。 - 新增(对话):伙伴
save_memory工具 →CompanionStoreSink::save解析归属伙伴 →insert_memory(scope=Companion(id))→ emit created。 - 新增(学习):
learner→insert_memory(scope=Shared)。 - 编辑:UI 弹窗 →
PUT /:id {content?,scope_kind?,scope_companion_id?}→service.update_memory(redact+trim) →store.update_memory(COALESCE) → emit updated → 刷新。 - 注入:
build_companion_system_prompt(companion_id)→memories_for_injection(companion_id,..)→ 共享+该伙伴私有,烘焙进新对话。 - recall:
recall_memories→list_memories带 scope 过滤。
错误处理 & 安全
- update:trim 拒空(400);重跑脱敏;status 枚举校验;scope_kind='companion' 时校验 companion_id 存在(否则 400)。
- 迁移回填幂等。
- 持久化快照:编辑只影响新对话 + 实时 recall,不回溯改写在飞对话——UI 文案说明。
- DELETE 未知 id 返回 404(与 PUT 一致)——可选清理项。
insert_memory_raw(import)保持不脱敏(高保真导入),文档注明。
测试
Rust:
- store:带 scope 插入写两列;
row_to_memory读出;NULL→''归一;memories_for_injection过滤(伙伴见共享+自己私有,不见他人私有);list_memoriesscope 过滤;update_memory脱敏 + 拒空 + 改 scope。 - service:三种默认归属(对话→私有、学习→共享、手动→请求)。
- events:created/updated/deleted 在对应路径发射。
- 迁移幂等。
TS / 集成:
companionNativeMenu.test.ts:菜单含新"打开记忆"项与顺序。MemoriesTab:编辑流调用updateMemory带 content+scope;作用域筛选;徽标渲染(若有组件测试)。bun run check:typecheck + i18n 双语 key + theme contract。
涉及文件
后端:store.rs / service.rs / routes.rs / events.rs / companion.rs / learner.rs / export.rs(+ 迁移)。
IPC:ipcBridge.ts。
前端:MemoriesTab.tsx / nomi/index.tsx / companion/index.tsx / companionNativeMenu.ts(+test) /(必要时 Sider)。
i18n:locales/en-US/nomi.json、locales/zh-CN/nomi.json + 重新生成 i18n-keys.d.ts。
实现备注(落地时的取舍,与上文设计一致,细节微调)
- 无需新增迁移:旧行
scope_companion_id为 NULL,row_to_memory用try_get::<Option<String>>归一成'',配合scope_kind='user' OR scope_companion_id=?查询即正确;省去回填迁移、不动STORE_VERSION。 insert_memory保留为共享包装:新增insert_memory_scoped(..., scope),insert_memory(...)= 共享包装,最小化对学习器/既有测试的冲击。- 私有写入跳过去重:
add_memory/对话保存在私有作用域下跳过find_similar_active合并,避免把私有记忆误并进共享或他人记忆。 - recall 作用域:
CompanionMemorySink::recall增conversation_id,由会话归属伙伴解析作用域(与注入一致)。 - 对话内入口(5d):实现为悬浮窗「空闲时」低调气泡提示「📝 已记住:…」(避免覆盖进行中的回复气泡),编辑/管理经右键「打开记忆」直达 scope-aware 记忆页。
- 预存且无关的破损:集成测试
nomifun-ai-agent/tests/factory_provider_integration.rs因更早提交给BuildTaskOptions增了conversation_created_at字段而未同步,本就编译失败(已 stash 验证与本改动无关),不在本次范围内修复。