# 项目历史召回插件设计方案 > **状态:** 设计方案,非实施代码。本文件定义插件的能力边界与集成方式;代码实现需另行授权。 **Goal:** 用一个插件同时解决两个需求:(1) 会话项目归属(project_id),(2) 项目内历史的按需关键词+语义检索。 **定位:** Footprint Ladder 第 4 级 —— 独立插件,不动核心文件,不落 `plugins/memory/`(该目录已关闭新 provider)。装在 `~/.hermes/plugins/project-history-recall/` 或独立仓库 pip 包。 --- ## 一、为什么用一个插件而不是两个 两个需求共享同一套底座:会话→项目的归属解析 + state.db 只读访问 + 检索结果格式化。拆成两个插件会重复这份底座并引入"哪个插件负责归属"的模糊边界。一个插件、两个工具,归属逻辑写一次,两个工具都吃它。 **与 V3/V4.1 的关系:** 本插件方案替代它们的"改核心 session_search"路径。V3/V4.1 的归属语义(project_id、移动只改归属、cwd 分离)和检索设计(项目过滤下推、混合检索、溯源)全部保留,但**落点从核心 session_search 改为插件的独立工具**,核心零改动。 ## 二、插件结构与发现 ``` ~/.hermes/plugins/project-history-recall/ ├── plugin.yaml # name/version/description,不动核心 ├── __init__.py # register(ctx):注册两个工具 ├── attribution.py # 归属解析(读 projects.db + sessions 行) ├── search.py # 关键词+语义混合检索 └── config_schema.py # embedding 配置项声明(如有) ``` 发现路径:`PluginManager`(`hermes_cli/plugins.py`)扫描 `~/.hermes/plugins/`,later-wins 发现 `register(ctx)`。 注册方式(对齐 `plugins/spotify/__init__.py:28-31` 的现有模式): ```python def register(ctx) -> None: ctx.register_tool(name="project_history_search", toolset="project", schema=SEARCH_SCHEMA, handler=handle_search, check_fn=check_available, emoji="📂") ctx.register_tool(name="project_session_move", toolset="project", schema=MOVE_SCHEMA, handler=handle_move, check_fn=check_available, emoji="📁") ``` 两个工具都在 `project` toolset(会话作用域,桌面端 GUI 平台自动折叠进可用集合,不依赖 `HERMES_DESKTOP` 环境变量)。 ## 三、需求一:项目归属(project_id 语义) **归属数据源:** 复用现有 `projects.db`(`hermes_cli/projects_db.py`)+ 会话行的 `cwd`/`git_repo_root`(`hermes_state_common.py:305-307`)。 **归属解析(attribution.py):** 纯函数,无 IO 写入。 1. 读 `projects.db` 的 `projects` + `project_folders` → 构建文件夹索引(最长祖先匹配,`_FolderIndex` 同款逻辑,插件内重新实现,不从 tui_gateway 复制——它是显示层,不是共享内核)。 2. 当前会话行 → `cwd`/`git_repo_root` → 匹配显式项目文件夹 → `Project.id`。 3. 无显式项目命中 → 用 `git_repo_root` 的 path_key 作为 auto 项目身份(Tier 2)。 4. 无 cwd 且无 git_repo_root → **Home 桶**,返回明确空结果,不检索。 **移动(project_session_move 工具):** 更新会话的归属记录。归属与 cwd 一致移动——会话移到哪个项目,工作目录就跟到哪个项目的目录。通过现有 `session.workspace.move` RPC 完成(它已有持久化 cwd/git_repo_root 的完整路径,见 `tui_gateway/methods_session.py:840-876`),插件在其上叠加归属语义(校验目标项目存在、写入 project_id)。核心零改动。 **归属语义(保留 V3 合同,cwd 跟随移动):** - 移动会话 = 归属与 cwd 一起切到目标项目目录(workspace.move 的现有行为,插件叠加归属校验)。 - 移动前开始的检索可以完成;移动后的新检索用新归属和新 cwd。 - Home 桶(无 cwd)不检索,返回空并说明原因。 - 跨 profile 归属不合并。 ## 四、需求二:项目内历史检索(关键词 + 语义) **工具:** `project_history_search`,两个模式由 `mode` 参数控制(默认 `hybrid`)。 ### 4.1 关键词路径 插件直接只读查询 state.db(`SessionDB` 的只读实例,或 `sqlite3.connect(..., uri=True)` 只读模式): ``` SELECT m.id, m.session_id, m.role, m.content, m.timestamp, s.title, s.started_at FROM messages m JOIN sessions s ON m.session_id = s.id WHERE s.<归属匹配条件> AND m.content LIKE / FTS5 MATCH ... ORDER BY rank / timestamp LIMIT ? ``` 归属匹配条件由 attribution.py 解析出的项目会话集合生成(`s.project_id = ?` 或 `s.cwd LIKE '...%'` 或 `s.git_repo_root = ?`,取决于归属是否已持久化)。 **只读约束是硬性的:** 插件对 state.db 只有 SELECT,绝不 INSERT/UPDATE/DELETE。state.db 是核心的权威存储,插件写入会破坏核心的一致性假设(FTS 索引、token 计数、lineage 状态)。 ### 4.2 语义路径 向量索引为插件自有派生数据,存在插件自己的 SQLite 文件(`~/.hermes/plugins/project-history-recall/embeddings.db`)——不碰 state.db schema。 **索引构建:** - 按归属解析出项目会话集合 → 读 messages 的 user/assistant 文本 → 按消息切片(长消息分段)→ 调用 embedding 模型 → 存向量。 - 首次索引按项目分批执行;后续增量(新消息)在每次检索前检查并补建。 - 索引记录 `session_id + message_id + chunk_offset + content_hash + model_fingerprint + vector`。 - 移动会话后向量不重算——归属解析在查询时重新执行,向量只关联 session_id。 **检索流程:** 1. 归属解析 → 项目会话集合。 2. 关键词:state.db FTS5 查询,限定在集合内。 3. 语义:embeddings.db 向量查询,限定在集合内(先 JOIN 过滤再算相似度,不全库 Top-K 后过滤)。 4. 混合:Reciprocal Rank Fusion 按消息锚点归并两路结果。 5. 展开:对命中消息用 state.db 读原文窗口(前后各 N 条),返回 `session_id + message_id + @session: 链接 + 原文片段`。 **模式:** - `hybrid`(默认):关键词 + 语义,语义不可用时退化为关键词并注明。 - `keyword`:仅关键词,用于精确符号/错误码查询。 - `semantic`:仅语义,用于不同措辞的相似问题追溯。 ### 4.3 结果格式 JSON 字符串返回(对齐现有工具约定): ```json { "success": true, "mode": "hybrid", "project": {"kind": "explicit", "id": "p_abc", "name": "Hermes"}, "results": [{ "session_id": "20260901_143022_a1b2", "link": "@session:20260901_143022_a1b2", "title": "session_search 项目过滤设计", "message_id": 47, "snippet": "……项目归属应该持久化在 sessions 表……", "score_source": "both", "when": "2026-09-01 14:30" }], "sessions_in_scope": 12, "semantic_status": "ready" } ``` `semantic_status`:`disabled / consent_required / indexing / ready / partial / unavailable`。 ## 五、Embedding 配置与隐私 embedding 模型配置通过插件自己的配置文件(`~/.hermes/plugins/project-history-recall/config.yaml`)或 `config.yaml` 的 `plugins.project-history-recall` 节,走 `hermes tools` 的插件配置 UX。密钥进 `.env`。 **隐私(保留 V4.1 的对话确认设计):** - 首次需要远程 embedding 且无授权记录时,工具返回 `consent_required` 状态,不发送任何内容。 - Agent 在对话中说明服务方、发送内容、项目范围,用户确认后写入配置。 - 授权持久化,同范围后续不重复询问;换服务/加项目重新确认。 - 未确认时关键词路径正常工作。 ## 六、与核心的交互边界 | 插件做的 | 插件不做的 | |---|---| | 读 projects.db、读 state.db(只读) | 写 state.db 的 sessions/messages 表(移动除外——workspace.move 有核心自己的写路径) | | 自己的 embeddings.db(向量存储) | 改 state.db schema | | 注册两个工具到 `project` toolset | 修改核心 session_search 或 inline executor | | 归属解析纯函数 | 修改 project_tree.py 或桌面端代码 | | embedding 调用(自有配置) | 新增核心 HERMES_* 环境变量 | **核心配合需求:无。** 移动会话归属与 cwd 一致移动,复用现有 `session.workspace.move` RPC 的完整路径(持久化 + git 元数据更新),插件只在其上叠加项目校验和归属语义。核心零改动。 ## 七、实施顺序 | 步骤 | 内容 | 验证 | |---|---|---| | B0 | 验证 state.db 只读访问 + projects.db 读取 + workspace.move 的归属叠加路径 | 真实临时 HERMES_HOME 下跑通 | | P1 | attribution.py 归属解析纯函数 + 单测 | 显式项目/自动 repo/Home 桶三态 | | P2 | project_history_search 关键词路径 + project toolset 注册 | P/Q 项目隔离、Home 桶空结果、溯源链接 | | P3 | project_session_move 移动 + 归属更新 | 移动后新检索用新归属 | | P4 | embeddings.db + 语义路径 + 混合检索 | 同义词命中、关键词保底、移动不重算向量 | | P5 | 对话确认链路 + 隐私控制 | 未确认不发送、确认后自主检索 | ## 八、不做的事 - 不改核心 session_search、inline executor、SessionDB schema。 - 不做项目摘要、会话摘要、自动上下文注入。 - 不做旧会话自动目录归类。 - 不做权限撤销协议、版本表、跨 profile 联合检索。 - 不动全局 MEMORY/USER 的项目隔离。 ## 九、评审自审 - **Footprint Ladder:** 第 4 级插件,核心零改动,符合"能力在边缘"。 - **一个插件两个工具:** 归属底座共享,不重复。 - **只读 state.db:** 插件绝不写核心存储,向量索引自有。 - **prompt cache:** 工具结果通道,不碰系统提示词。 - **诚实边界:** 移动时 cwd 跟随归属是已确认的行为(workspace.move 现有路径),无悬而未决项;语义检索的性能包络待 P4 实测,不预先承诺。