178 lines
9.9 KiB
Markdown
178 lines
9.9 KiB
Markdown
# 项目历史召回插件设计方案
|
||
|
||
> **状态:** 设计方案,非实施代码。本文件定义插件的能力边界与集成方式;代码实现需另行授权。
|
||
|
||
**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:<id> 链接 + 原文片段`。
|
||
|
||
**模式:**
|
||
- `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 实测,不预先承诺。
|