first
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
# 当前知识库自动检索增强设计
|
||||
|
||||
> 状态:**已完成** · 实施于 2026-05-28 · 对应 ADR-19 · 阶段 3.5 收尾补强
|
||||
>
|
||||
> 本文档是 codex 实施手册。先读 §0 决策摘要,再按 §9 顺序动手。
|
||||
> 任何与 [PRODUCT.md](../PRODUCT.md) 冲突的描述以 PRODUCT.md 为准;本文档需要破例的地方在 §12 明确写出并对应到 ADR-19。
|
||||
|
||||
## 0. 决策摘要(codex 起手必读)
|
||||
|
||||
| 决策 | 选择 | 理由速记 |
|
||||
|---|---|---|
|
||||
| 架构路线 | **双轨**:新工具 `query_knowledge_base` + `/api/prompt` 预调用同一份代码 | 工具路径符合 ADR-7;预调用兜底弱模型不调工具的情况 |
|
||||
| 触发策略 | **黑名单**:默认触发,特定场景跳过 | 漏触发的代价是当前 bug;误触发的代价只是浪费 token |
|
||||
| 多轮检索 | **每轮 user turn 都判断 + 检索** | 用户切话题时能立刻拿到新页面,本地 grep 成本可接受 |
|
||||
| 失败降级 | **静默降级裸 prompt + SSE error 事件 + 日志** | 用户体验不中断,问题可溯源 |
|
||||
| 检索 cache | **复用 [pages.ts](../server/src/pages.ts) 的 `getCachedPages`** | 避免两套 cache 失效时机错位 |
|
||||
| `@` 格式契约 | **解析消息中的 `[[wiki/...]]`**(前端 [ChatPanel.tsx:218-230](../web/src/components/ChatPanel.tsx) 已插入此格式) | 不发明新语法 |
|
||||
| 上下文上限 | **按 main 模型 contextWindow 的 20% 动态算**;模型未知时 fallback 4000 字符 | 兼顾 glm(4k 级)与 Claude(200k 级) |
|
||||
| 验收硬标志 | **日志 + 字符串命中**双重客观可查 | 防止"看起来对实际没注入"的 hallucination 蒙混过关 |
|
||||
|
||||
## 1. 问题
|
||||
|
||||
阶段 3.5 已实现非 wiki 目录初始化 + 批量消化,但用户进入新知识库直接问"这是我自媒体创作的文章,总结一下",主模型反问用户提供文章内容。
|
||||
|
||||
已确认事实:
|
||||
|
||||
- 批量消化结果已写入 `wiki/synthesis/sessions/`
|
||||
- `/api/refs` 能列出这些新页面([pages.ts](../server/src/pages.ts) PAGE_DIRS 含 `synthesis`)
|
||||
- 失败对话的会话日志显示模型未调用任何 KB 工具
|
||||
- `/api/prompt`([index.ts:745-832](../server/src/index.ts))当前是裸 `session.prompt(message)`,无任何检索 / 注入
|
||||
- 现有 Extension 工具 `current_knowledge_base` / `list_knowledge_base_pages`([extensions/knowledge-base.ts](../server/src/extensions/knowledge-base.ts))描述写得够强,但 agent 实际不调
|
||||
|
||||
**根因**:主对话缺少一层"系统兜底检索"。ADR-7 设计的"靠 Extension 工具让 agent 自觉调用"在弱模型场景下不可靠。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
- 用户在当前库提问,系统默认把当前库作为上下文来源
|
||||
- `@` 显式引用优先级高于自动检索
|
||||
- 回答末尾列参考页面;空库 / 无命中时明确说明
|
||||
- 不把整个库塞进每次对话;不引入新依赖
|
||||
- 不改变 `@` 引用 / `/` 命令 / 子代理批量消化的现有心智
|
||||
|
||||
## 3. 核心思路:双轨
|
||||
|
||||
```text
|
||||
用户提问
|
||||
↓
|
||||
后端解析消息中的 [[wiki/...]] → 显式引用列表 R_explicit
|
||||
↓
|
||||
shouldUseKnowledgeBase(message) 判断是否触发自动检索(黑名单)
|
||||
↓
|
||||
若触发 → 调用同一份 searchKnowledgeBase(kbPath, query, R_explicit)
|
||||
↓
|
||||
拼成隐藏上下文 + 原始消息 → session.prompt(wrapped)
|
||||
↓
|
||||
模型输出(包含末尾"参考页面")
|
||||
```
|
||||
|
||||
同一份 `searchKnowledgeBase` 同时暴露为:
|
||||
|
||||
- **新工具** `query_knowledge_base(query)`:让强模型 / 未来强 agent 显式调用,符合 ADR-7
|
||||
- **后端预调用**:`/api/prompt` 在判断需要时主动调,兜底弱模型不调工具的失败模式
|
||||
|
||||
这条路径只覆盖**主对话**。子代理批量消化([digest/batch.ts](../server/src/digest/))走的是裸 prompt + only read tool,本设计不涉及它。
|
||||
|
||||
## 4. 不采用的方案
|
||||
|
||||
### 4.1 纯改 prompt / 加强 system prompt
|
||||
|
||||
不稳定。同一模型不同问法、不同 provider 表现差异巨大。
|
||||
|
||||
### 4.2 每次塞整库
|
||||
|
||||
KB 大了就慢、贵、乱,污染普通聊天。
|
||||
|
||||
### 4.3 把批量消化结果写进聊天历史
|
||||
|
||||
消化结果属于知识库,不属于某一次对话。塞进历史会让切会话 / 切库 / 长期使用语义错乱。
|
||||
|
||||
### 4.4 立即引入向量库
|
||||
|
||||
本地文本检索足够覆盖当前阶段。未来 KB 规模 > 100 篇再升级。
|
||||
|
||||
### 4.5 纯工具路径(无后端预调用)
|
||||
|
||||
ADR-7 的精神路线,但已被你这次"反问要文章"的对话验证为不稳。本设计保留工具(双轨的一条腿),但不依赖它。
|
||||
|
||||
## 5. 解法方案
|
||||
|
||||
### 5.1 检索函数 `searchKnowledgeBase`
|
||||
|
||||
签名(伪代码,仅说明契约):
|
||||
|
||||
```text
|
||||
searchKnowledgeBase(kbPath, query, options) → SearchResult[]
|
||||
options:
|
||||
explicitRefs: string[] // 来自 parseExplicitPageRefs,必含
|
||||
maxPages: number // 默认 6
|
||||
snippetMaxChars: number // 默认 600
|
||||
totalBudgetChars: number // 由 /api/prompt 按模型 contextWindow 算后传入
|
||||
SearchResult:
|
||||
path: string // 相对 KB 根,如 wiki/synthesis/sessions/xxx.md
|
||||
title: string
|
||||
snippet: string
|
||||
mtime: number
|
||||
hitReason: 'explicit' | 'title' | 'filename' | 'body' | 'recent_synthesis' | 'kb_meta'
|
||||
score: number
|
||||
```
|
||||
|
||||
**实现要点**:
|
||||
|
||||
1. **复用 [pages.ts](../server/src/pages.ts)**:调 `getCachedPages(kbPath)` 拿候选,不另建 cache。score 函数在 pages.ts 现有 title 2 / name 1 / path 0.5 基础上扩展(见下)。
|
||||
2. **检索范围**:
|
||||
- `wiki/` 下所有 markdown(已被 pages.ts 覆盖)
|
||||
- 额外加 KB 元信息文件:`purpose.md` / `index.md` / `overview.md`(如果存在)—— 这些是"总结这个库"类问题的最强答案源
|
||||
- 现阶段**不读** `raw/` 下的原始素材(批量消化后用户应基于消化页问答)
|
||||
3. **排序规则**(高到低):
|
||||
- hitReason='explicit'(来自 `[[wiki/...]]` 显式引用,固定排首位)
|
||||
- hitReason='title'(标题命中)
|
||||
- hitReason='kb_meta'(purpose/index/overview 在泛化问题下加权)
|
||||
- hitReason='recent_synthesis'(`wiki/synthesis/sessions/` 在"这些文章 / 刚刚消化 / 总结"类问题下加权,按 mtime 降序)
|
||||
- hitReason='filename'
|
||||
- hitReason='body'
|
||||
4. **片段提取**:命中 body 时,从命中位置前后各取 ~300 字符;标题/文件名命中时取页面开头 ~600 字符
|
||||
5. **总预算控制**:累加每个 snippet 字符数,达到 `totalBudgetChars` 立即停止;至少保证 explicitRefs 全部入选(哪怕超 budget 也强保留显式引用的截断版本)
|
||||
6. **空 KB 返回 `[]`,不抛错**
|
||||
7. **cache 隔离**:pages.ts 的 cache 按 kbPath 隔离,retrieval 直接复用,无需自己维护
|
||||
|
||||
### 5.2 `parseExplicitPageRefs`
|
||||
|
||||
签名:
|
||||
|
||||
```text
|
||||
parseExplicitPageRefs(message) → string[] // 相对路径数组,如 ['wiki/synthesis/sessions/x.md']
|
||||
```
|
||||
|
||||
**实现要点**:
|
||||
|
||||
- 正则匹配 `\[\[(wiki\/[^\]\n]+\.md)\]\]`
|
||||
- 去重,保持出现顺序
|
||||
- 不验证文件是否存在(验证交给 searchKnowledgeBase)
|
||||
|
||||
### 5.3 `shouldUseKnowledgeBase`(黑名单策略)
|
||||
|
||||
签名:
|
||||
|
||||
```text
|
||||
shouldUseKnowledgeBase(message, kbSelected) → boolean
|
||||
```
|
||||
|
||||
**实现要点**(按顺序判断,命中即返回):
|
||||
|
||||
1. 没选中 KB → `false`
|
||||
2. 消息 trim 后以 `/` 开头(命令)→ `false`
|
||||
3. 消息已含 `[[wiki/...]]`(显式引用)→ `true`(短路,必须检索)
|
||||
4. 消息 trim 后字符数 < 3 → `false`
|
||||
5. 命中**寒暄白名单**(精确匹配,忽略标点)→ `false`
|
||||
- 集合:`你好` / `hi` / `hello` / `在吗` / `谢谢` / `thanks` / `thx` / `ok` / `好的`
|
||||
6. 命中**元问询白名单**(含其一即跳过)→ `false`
|
||||
- 关键词:`当前模型` / `模型是什么` / `怎么设置` / `怎么用` / `界面` / `快捷键` / `登录` / `API key`
|
||||
7. 其余 → `true`(默认触发)
|
||||
|
||||
**关键**:本策略激进地默认触发。误触发的代价只是 token,漏触发的代价是当前观察到的 bug。
|
||||
|
||||
### 5.4 `buildKnowledgeContextPrompt`
|
||||
|
||||
签名:
|
||||
|
||||
```text
|
||||
buildKnowledgeContextPrompt(originalMessage, kb, results) → wrappedMessage
|
||||
```
|
||||
|
||||
**输出格式**(results 非空时):
|
||||
|
||||
```text
|
||||
{用户原始消息原样保留在最前}
|
||||
|
||||
---
|
||||
[系统检索上下文 / 用户不可见]
|
||||
|
||||
当前知识库:
|
||||
- 名称: {kb.name}
|
||||
- 路径: {kb.path}
|
||||
|
||||
系统已从当前知识库检索到以下页面(按相关度排序):
|
||||
|
||||
[1] {results[0].title}
|
||||
路径: {results[0].path}
|
||||
{results[0].snippet}
|
||||
|
||||
[2] ...
|
||||
|
||||
回答约束:
|
||||
- 必须基于以上页面内容回答;不得编造未在页面中出现的事实
|
||||
- 如果以上页面不足以回答,明确说明"当前知识库未找到相关内容",禁止反问用户提供文章
|
||||
- 回答末尾必须列出"参考页面",格式为 `- 《标题》:路径`
|
||||
- 参考页面只能从上述检索结果中选取,禁止编造路径
|
||||
```
|
||||
|
||||
**results 为空时**:仍包装一段简短上下文:
|
||||
|
||||
```text
|
||||
{用户原始消息}
|
||||
|
||||
---
|
||||
[系统检索上下文]
|
||||
|
||||
当前知识库:{kb.name}
|
||||
系统已尝试检索,但未在当前知识库找到与该问题相关的页面。
|
||||
请明确告诉用户"当前知识库未找到相关内容",禁止反问用户提供文章,也禁止编造来源。
|
||||
```
|
||||
|
||||
**关键**:空结果也包装,因为弱模型一旦完全没拿到上下文就会回到"反问要文章"的失败模式。强制告诉它"找过了,没有"。
|
||||
|
||||
### 5.5 主对话 `/api/prompt` 改造
|
||||
|
||||
[index.ts:745-832](../server/src/index.ts) 当前逻辑:
|
||||
|
||||
```text
|
||||
session.prompt(message)
|
||||
```
|
||||
|
||||
改造后:
|
||||
|
||||
```text
|
||||
1. 解析: explicitRefs = parseExplicitPageRefs(message)
|
||||
2. 判断: shouldUse = shouldUseKnowledgeBase(message, kbSelected)
|
||||
3. 若 !shouldUse → 直接 session.prompt(message)(与今日行为一致)
|
||||
4. 若 shouldUse:
|
||||
a. 算 totalBudgetChars = floor(activeModel.contextWindow * 0.2) || 4000
|
||||
b. SSE 推送 knowledge_search_start
|
||||
c. try {
|
||||
results = searchKnowledgeBase(kbPath, message, { explicitRefs, totalBudgetChars })
|
||||
wrapped = buildKnowledgeContextPrompt(message, kb, results)
|
||||
SSE 推送 knowledge_search_done { count: results.length, paths: results.map(r => r.path) }
|
||||
写日志(见 §5.7)
|
||||
session.prompt(wrapped)
|
||||
} catch (err) {
|
||||
SSE 推送 knowledge_search_error { message: err.message }
|
||||
写日志
|
||||
session.prompt(message) // 静默降级
|
||||
}
|
||||
```
|
||||
|
||||
**每轮 user turn 独立判断 + 检索**。不缓存判断结果。
|
||||
|
||||
### 5.6 失败降级
|
||||
|
||||
任何一环抛异常(IO 失败、parseExplicitPageRefs throw、buildKnowledgeContextPrompt 字符数爆掉):
|
||||
|
||||
- 不中断 prompt
|
||||
- 降级为裸 `session.prompt(message)`
|
||||
- SSE 推 `knowledge_search_error` 事件,前端在状态栏轻量显示"知识库检索失败,已按普通对话处理"
|
||||
- 日志(见 §5.7)写完整 error stack
|
||||
|
||||
### 5.7 检索日志
|
||||
|
||||
写入 `~/.llm-wiki-agent/logs/retrieval/<YYYY-MM-DD>.jsonl`,每行:
|
||||
|
||||
```json
|
||||
{
|
||||
"ts": 1735000000000,
|
||||
"sessionId": "abc12345",
|
||||
"kbPath": "~/my-kb",
|
||||
"messagePreview": "这是我自媒体创作的文章...",
|
||||
"triggered": true,
|
||||
"explicitRefs": [],
|
||||
"results": [{"path": "wiki/synthesis/sessions/x.md", "hitReason": "recent_synthesis", "score": 3.2}],
|
||||
"wrappedCharCount": 4823,
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
**作用**:验收脚本、用户反馈复现、长期效果分析都依赖这份日志。
|
||||
|
||||
### 5.8 新工具 `query_knowledge_base`
|
||||
|
||||
在 [extensions/knowledge-base.ts](../server/src/extensions/knowledge-base.ts) 注册第三个工具:
|
||||
|
||||
```text
|
||||
pi.registerTool({
|
||||
name: 'query_knowledge_base',
|
||||
parameters: { query: string },
|
||||
execute: ({ query }) => {
|
||||
results = searchKnowledgeBase(currentKbPath, query, { explicitRefs: [], totalBudgetChars: 4000 })
|
||||
return formatted text for agent
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**作用**:让强模型在主对话流中也能继续追问 KB;未来若改为"工具优先 + 注入降级"也只是改 `/api/prompt` 的判断条件,retrieval 函数不动。
|
||||
|
||||
工具描述要点:"Search the user's current knowledge base for pages relevant to a query. Use this when the user asks a follow-up question that may require pulling additional context from the knowledge base beyond what's already in the conversation."
|
||||
|
||||
### 5.9 前端反馈
|
||||
|
||||
复用现有 tool 状态显示样式。新增三个 SSE 事件渲染:
|
||||
|
||||
- `knowledge_search_start` → 显示"正在检索当前知识库…"
|
||||
- `knowledge_search_done { count, paths }` → 显示"已检索到 N 个相关页面"
|
||||
- `knowledge_search_empty`(count=0 时)→ 显示"当前知识库未找到相关内容"
|
||||
- `knowledge_search_error` → 显示"知识库检索失败,已按普通对话处理"
|
||||
|
||||
不新增大面板。
|
||||
|
||||
## 6. API 与模块边界
|
||||
|
||||
### 6.1 新增文件
|
||||
|
||||
- `server/src/retrieval.ts`:导出 `searchKnowledgeBase` / `parseExplicitPageRefs` / `shouldUseKnowledgeBase` / `buildKnowledgeContextPrompt`
|
||||
- 单测 `server/src/retrieval.test.ts`:触发策略、`[[...]]` 解析、空 KB、cache 隔离、budget 控制
|
||||
|
||||
### 6.2 复用与改动
|
||||
|
||||
- **复用** [pages.ts](../server/src/pages.ts) 的 `getCachedPages` —— retrieval 在其上加片段提取与扩展 score
|
||||
- **改动** [index.ts](../server/src/index.ts) 的 `/api/prompt`:按 §5.5 改造,新增 SSE 事件类型
|
||||
- **改动** [extensions/knowledge-base.ts](../server/src/extensions/knowledge-base.ts):新增 `query_knowledge_base` 工具
|
||||
- **不动**:digest/ 目录全部(子代理批量消化不受影响)
|
||||
|
||||
### 6.3 不新增对外 HTTP API
|
||||
|
||||
第一版只在 `/api/prompt` 内部使用。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
### 7.1 客观可查标志(**所有验收的硬底线**)
|
||||
|
||||
每条主对话验收必须同时满足:
|
||||
|
||||
1. **日志可查**:`~/.llm-wiki-agent/logs/retrieval/*.jsonl` 中存在对应记录,`triggered` / `results` / `wrappedCharCount` 字段符合预期
|
||||
2. **字符串命中**:模型回答中至少出现一次检索结果的页面标题或独特短语(grep 可验)
|
||||
|
||||
无法同时通过这两条的验收一律不算过。
|
||||
|
||||
### 7.2 检索函数 `searchKnowledgeBase`
|
||||
|
||||
1. 对"总结这些文章"类问题,能命中 `wiki/synthesis/sessions/` 下最新页面
|
||||
2. 对具体关键词(如 `OpenClaw`),能命中标题或正文相关页面
|
||||
3. 返回结果含 path / title / snippet / mtime / hitReason / score
|
||||
4. 默认 maxPages=6,可配置
|
||||
5. 单页 snippet 不超过 snippetMaxChars
|
||||
6. 总字符数不超过 totalBudgetChars
|
||||
7. explicitRefs 即使超 budget 也强保留(可截断片段)
|
||||
8. 空 KB 返回 `[]`,不抛错
|
||||
9. 同一 query 在 cache 未失效时多次调用,结果一致
|
||||
10. 批量消化新文件后,调用 pages.ts cache 失效路径,下一次检索能拿到新页
|
||||
|
||||
### 7.3 `parseExplicitPageRefs`
|
||||
|
||||
1. `这是什么 [[wiki/synthesis/sessions/x.md]]` → `['wiki/synthesis/sessions/x.md']`
|
||||
2. `[[wiki/a.md]] 和 [[wiki/b.md]]` → `['wiki/a.md', 'wiki/b.md']`
|
||||
3. `[[wiki/a.md]] [[wiki/a.md]]` → `['wiki/a.md']`(去重)
|
||||
4. `普通文本无引用` → `[]`
|
||||
5. `[[notwiki.md]]` → `[]`(必须以 wiki/ 开头)
|
||||
|
||||
### 7.4 `shouldUseKnowledgeBase`(黑名单)
|
||||
|
||||
1. 未选 KB → false
|
||||
2. `/sediment` / `/pdf` → false
|
||||
3. `你好` / `谢谢` / `ok` → false
|
||||
4. `当前模型是什么` → false
|
||||
5. `[[wiki/x.md]] 说了啥` → true(含显式引用,短路)
|
||||
6. `总结一下` → true
|
||||
7. `这是我自媒体创作的文章,总结一下` → true(本 bug 的原始 case)
|
||||
8. `OpenClaw 相关页面讲了什么` → true
|
||||
9. 长度 < 3 → false(如 `嗯`)
|
||||
|
||||
### 7.5 `/api/prompt` 端到端
|
||||
|
||||
1. 触发路径:SSE 顺序为 `knowledge_search_start → knowledge_search_done → text_delta* → done`
|
||||
2. 不触发路径:SSE 不出现任何 knowledge_search_* 事件
|
||||
3. 触发但检索抛错:SSE 出现 `knowledge_search_error`,但 `text_delta` 仍正常流出(裸 prompt 降级)
|
||||
4. 注入字符数符合模型 contextWindow * 20% 上限
|
||||
5. 模型回答末尾出现"参考页面"列表,所有路径都在 `results.paths` 中(禁止幻觉路径)
|
||||
6. 多轮:第 1 轮触发后第 2 轮再问新话题,第 2 轮独立判断 + 独立检索
|
||||
|
||||
### 7.6 `@` 显式引用
|
||||
|
||||
1. `@A` 选中后输入框变成 `[[wiki/.../A.md]]`,发送后 A 在 results 首位(hitReason='explicit')
|
||||
2. `[[wiki/.../A.md]] [[wiki/.../B.md]] 对比` → results 前两个是 A 和 B
|
||||
3. `[[wiki/.../A.md]] 再结合全库看看` → results[0]=A,后续是自动检索补充
|
||||
4. `[[wiki/不存在.md]] 说了啥` → results 中无该路径,buildKnowledgeContextPrompt 在末尾加一行"用户引用了 wiki/不存在.md,但该页面不存在"
|
||||
|
||||
### 7.7 切库隔离
|
||||
|
||||
1. A 库批量消化后,问"总结" → results 全部属于 A 库
|
||||
2. 切到 B 库(B 库为空)→ 问"总结" → results 为空 / 模型说"未找到"
|
||||
3. 切回 A 库 → 问"总结" → 仍能命中 A 库的页面
|
||||
|
||||
### 7.8 失败降级
|
||||
|
||||
1. mock fs read 失败 → SSE 推 error 事件、回答仍能流式输出、日志写 error stack
|
||||
2. 注入字符数超 contextWindow → 自动截断到 budget 内,不抛错
|
||||
3. shouldUseKnowledgeBase 抛错(不应发生,但兜底)→ 走裸 prompt
|
||||
|
||||
## 8. 端到端验收剧本
|
||||
|
||||
### 场景 1:批量消化后总结(本 bug 原始 case)
|
||||
|
||||
1. 选择普通目录 → 初始化为 KB → 批量消化 7 篇文章
|
||||
2. 进入该 KB
|
||||
3. 问:"这是我自媒体创作的文章,总结一下。"
|
||||
|
||||
**必须结果**:
|
||||
- 不反问用户要文章
|
||||
- 回答覆盖多篇文章主题
|
||||
- 末尾"参考页面"列出 ≥ 3 篇 sessions/ 下的页面
|
||||
- 日志 jsonl 中 `triggered=true`、`results.length ≥ 3`
|
||||
- 模型回答中 grep 到至少 1 个 results.paths 中页面的标题
|
||||
|
||||
### 场景 2:具体主题检索
|
||||
|
||||
1. 同一库问:"OpenClaw 相关的文章讲了什么?"
|
||||
|
||||
**必须结果**:
|
||||
- results 中至少 1 篇含 `OpenClaw`
|
||||
- 回答基于这些页面,无幻觉
|
||||
|
||||
### 场景 3:切库隔离
|
||||
|
||||
1. 切到另一个 KB(已知不含相关内容)
|
||||
2. 问:"这些文章总结一下。"
|
||||
|
||||
**必须结果**:
|
||||
- results 为空 or 全部属于新 KB
|
||||
- 模型明确说"当前知识库未找到相关内容"
|
||||
- 不出现上一 KB 的页面路径
|
||||
|
||||
### 场景 4:`@` 显式引用
|
||||
|
||||
1. 输入 `@某篇文章 总结它的传播亮点`(前端会替换为 `[[wiki/.../某篇.md]] 总结它的传播亮点`)
|
||||
|
||||
**必须结果**:
|
||||
- results[0] 是该指定页面
|
||||
- 自动检索补充 ≤ 3 篇
|
||||
- 回答主要基于指定页面
|
||||
|
||||
### 场景 5:普通聊天不检索
|
||||
|
||||
1. 问:"你好。"
|
||||
2. 问:"当前模型是什么?"
|
||||
3. 输入 `/sediment`
|
||||
|
||||
**必须结果**:
|
||||
- 三次都不触发 knowledge_search_* 事件
|
||||
- 日志 jsonl 中 `triggered=false`
|
||||
|
||||
### 场景 6:多轮切话题
|
||||
|
||||
1. 第 1 轮:"总结一下这批文章"
|
||||
2. 第 2 轮(在同一对话):"其中讲 AI 的那篇具体说了什么?"
|
||||
|
||||
**必须结果**:
|
||||
- 两轮独立检索,各自有日志记录
|
||||
- 第 2 轮 results 与第 1 轮不必相同(query 变了)
|
||||
|
||||
### 场景 7:失败降级
|
||||
|
||||
1. 用 `chmod 000` 临时让 KB 某文件不可读
|
||||
2. 问:"总结一下"
|
||||
|
||||
**必须结果**:
|
||||
- SSE 出现 `knowledge_search_error`
|
||||
- 但 `text_delta` 仍正常输出
|
||||
- 日志 jsonl 中 `error` 字段非空
|
||||
- 前端状态栏短暂显示"知识库检索失败,已按普通对话处理"
|
||||
|
||||
## 9. 实施顺序(codex 起手必读)
|
||||
|
||||
| 步骤 | 任务 | 验收 |
|
||||
|---|---|---|
|
||||
| 1 | 新建 `server/src/retrieval.ts` + 单测;实现 `parseExplicitPageRefs` / `shouldUseKnowledgeBase` | §7.3 §7.4 全过 |
|
||||
| 2 | 在 retrieval.ts 实现 `searchKnowledgeBase`(复用 pages.ts `getCachedPages`) | §7.2 全过 |
|
||||
| 3 | 实现 `buildKnowledgeContextPrompt` + 日志写入 | 单测覆盖空 results / 含 explicit / 含 error 三种形态 |
|
||||
| 4 | 改造 [index.ts](../server/src/index.ts) `/api/prompt`:增加判断 + 调用 + SSE 新事件 + 失败降级 | §7.5 §7.8 全过 |
|
||||
| 5 | 在 [extensions/knowledge-base.ts](../server/src/extensions/knowledge-base.ts) 注册 `query_knowledge_base` 工具 | 单测:工具签名正确、execute 返回格式符合 §5.8 |
|
||||
| 6 | 前端新增 3 个 SSE 事件渲染(knowledge_search_start/done/error) | 手动验:剧本 1 能看到检索状态 |
|
||||
| 7 | 跑完 §8 全部 7 个端到端剧本 | 所有剧本通过 §7.1 双重客观标志 |
|
||||
|
||||
每步动手前先在对话里说"准备改 X,影响 Y",作者确认后再动。
|
||||
|
||||
## 10. 风险与边界
|
||||
|
||||
### 10.1 模型不读注入内容
|
||||
|
||||
弱模型(glm 等)可能拿到注入仍反问。
|
||||
- 缓解:§5.4 wrappedMessage 末尾加强约束句("如未基于以下页面回答而再次反问用户提供文章,视为错误")
|
||||
- 监控:日志可统计"注入了但模型反问"的发生率
|
||||
|
||||
### 10.2 cache 失效窗口
|
||||
|
||||
批量消化刚跑完、pages.ts cache 还指向旧 fingerprint。
|
||||
- 缓解:pages.ts 已用 mtime+size fingerprint,每次 retrieval 都会触发重新扫描
|
||||
- 验收:场景 1 必须在批量消化结束后立即问(不等手动刷新)
|
||||
|
||||
### 10.3 切库 cache 串库
|
||||
|
||||
pages.ts cache 按 kbPath 隔离,retrieval 复用,不会串库。
|
||||
- 验收:场景 3 必须通过
|
||||
|
||||
### 10.4 检索误触发
|
||||
|
||||
黑名单策略激进。
|
||||
- 缓解:寒暄白名单 + 元问询白名单 + `/` 命令短路 + 长度 < 3 短路
|
||||
- 监控:日志可统计触发率,过高时回头收紧
|
||||
|
||||
### 10.5 来源不真实
|
||||
|
||||
模型可能编造路径作为来源。
|
||||
- 缓解:§5.4 prompt 中强约束"参考页面只能从上述检索结果中选取"
|
||||
- 验收:§7.5.5 grep 检验回答中所有路径都在 results.paths 中
|
||||
|
||||
### 10.6 与 `@` 心智冲突
|
||||
|
||||
用户老心智:"想引用必须 @"。新行为:"不 @ 系统也可能引用"。
|
||||
- 缓解:§5.9 前端轻量状态显示让用户知道何时检索了
|
||||
- 长期:如果用户反馈混乱,提供"关闭自动检索"开关(本轮不做)
|
||||
|
||||
### 10.7 中文路径 / emoji
|
||||
|
||||
PRODUCT.md §6.6 UTF-8 铁律。
|
||||
- retrieval 所有字符串处理用 raw UTF-8,不 normalize
|
||||
- cache key 用原始 absolute path
|
||||
|
||||
### 10.8 多轮成本
|
||||
|
||||
每轮都检索 = 每轮多 100-300ms IO。
|
||||
- 本地 grep + cache 命中下可接受
|
||||
- 监控:日志记 retrieval 耗时,> 1s 时考虑优化
|
||||
|
||||
## 11. 本轮不做
|
||||
|
||||
- 不引入向量数据库
|
||||
- 不引入新 npm 包
|
||||
- 不做复杂搜索 UI
|
||||
- 不把整个知识库塞进每次 prompt
|
||||
- 不把批量消化结果写进聊天历史
|
||||
- 不改变 `@` / `/` / 子代理批量消化的现有心智
|
||||
- 不动 digest/ 目录任何代码
|
||||
- 不做"关闭自动检索"开关(等用户反馈再说)
|
||||
|
||||
## 12. ADR 影响
|
||||
|
||||
本设计**正面破例** [PRODUCT.md ADR-7](../PRODUCT.md)("知识库上下文用 Extension 注入,不拼 prompt")。需要在 PRODUCT.md 第 7 节追加:
|
||||
|
||||
### ADR-19:主对话引入"系统检索 + 上下文注入"
|
||||
|
||||
**背景**:ADR-7 设计的"靠 Extension 工具让 agent 自觉调用"在阶段 3.5 批量消化后的真实场景下不稳——弱模型不调 `list_knowledge_base_pages` / `read`,直接反问用户提供文章。
|
||||
|
||||
**决策**:
|
||||
|
||||
1. 主对话 `/api/prompt` 路径破例采用"后端检索 + 拼隐藏上下文"模式
|
||||
2. ADR-7 的"应用状态用 Extension 注入"原则在 `currentKnowledgeBase` 等状态查询上仍然成立,本破例只针对"问答类知识库检索"
|
||||
3. 同一份 `searchKnowledgeBase` 同时暴露为新工具 `query_knowledge_base`,保留 ADR-7 路径供未来强模型使用
|
||||
4. 子代理批量消化(digest/ 目录)不受影响,继续走裸 prompt + only read tool
|
||||
5. 主对话流式输出过程中,每个 user turn 独立判断 + 独立检索,不跨轮缓存
|
||||
6. 失败降级为裸 prompt,SSE 推 error 事件 + 写日志,绝不中断对话
|
||||
|
||||
**与既有 ADR 的关系**:
|
||||
|
||||
- 破例 **ADR-7**:仅限主对话问答检索,状态查询工具保留
|
||||
- 兼容 **ADR-3**(SSE):新增 3 个轻量事件类型
|
||||
- 兼容 **ADR-16**(Skill 优先 / agent 元能力用 Extension):检索是 agent 工作台元能力,落 server/ 端合理
|
||||
- 兼容 **ADR-18**(子代理路径):不影响 digest/ 目录
|
||||
|
||||
**何时重新评估**:
|
||||
|
||||
- 主流模型工具调用稳定性显著提升 → 考虑改回纯工具路径
|
||||
- 用户大量反馈"参考页面被编造" → 强化 prompt 约束 + 引入后置校验
|
||||
- KB 规模 > 100 篇时检索耗时不可接受 → 引入向量检索
|
||||
|
||||
## 13. 完成情况
|
||||
|
||||
本设计已在阶段 3.5 收尾中落地。
|
||||
|
||||
- `/api/prompt` 已接入当前知识库自动检索;每轮用户消息独立判断,触发时推送 `knowledge_search_start` / `knowledge_search_done` / `knowledge_search_empty` / `knowledge_search_error`
|
||||
- `query_knowledge_base` 工具已注册,和主对话预检索共用同一套检索函数
|
||||
- 检索结果复用 `pages.ts` 的缓存与扫描路径,覆盖 `wiki/synthesis/sessions/`、`purpose.md`、`index.md`、`wiki/overview.md`
|
||||
- `[[wiki/...]]` 显式引用会优先进入结果;缺失页面会进入包装提示,不静默吞掉
|
||||
- 检索日志写入 `~/.llm-wiki-agent/logs/retrieval/<YYYY-MM-DD>.jsonl`
|
||||
- 普通寒暄、`/` 命令、模型/设置类问题、导出产物指令不会触发知识库检索
|
||||
|
||||
**验收实况**:
|
||||
|
||||
- `node --import tsx --test server/src/retrieval.test.ts server/src/digest/concurrency.test.ts` 通过
|
||||
- `npm run --silent typecheck` 通过
|
||||
- 真实接口验证通过:`这是我自媒体创作的文章,总结一下` 触发检索并返回参考页面;`你好` 不触发检索;导出 PDF 指令不触发检索
|
||||
@@ -0,0 +1,210 @@
|
||||
# 阶段 4.6 设计文档:图谱演进第一批(看清关系 + 层层聚焦 + 控制工具条)
|
||||
|
||||
> 状态:**已实施并通过验收**
|
||||
> 日期:2026-06-14
|
||||
> 来源:[PRODUCT.md](../PRODUCT.md)「图谱演进候选池」(2026-06-13 沉淀)的首次落地;经 2026-06-14 spark 脑暴收敛。
|
||||
> 与 [stage-4.5-design.md](stage-4.5-design.md) 的关系:4.5 是「图谱可用性收尾」(已合入);本批是其后第一个增量批次,复用 4.5 的画布导航 / 点击即阅读 / 右抽屉基建。G1-2、G1-3 **修订** 4.5 的 D4.5-6(社区交互、左上角浮层图例),冲突处以本文档为准。
|
||||
> 阶段编号:**4.6**(作者 2026-06-14 拍板,已登记 PRODUCT.md §4)。实施完成后已同步 PRODUCT.md 与 ADR-23;决策号沿用 `G1-*`。
|
||||
|
||||
---
|
||||
|
||||
## §0 背景:方向与边界
|
||||
|
||||
「图谱演进候选池」作为备忘清单合格,但作为推进依据有三处会误导,spark 脑暴已逐一纠正:
|
||||
|
||||
1. **它把一条主线藏成了清单里的普通一项**。候选池自己写了尸检教训"局部图才是工具",却把「局部图模式」与「导出美图」并列。真相:局部聚焦是主线,关系边 / 过滤器是挂在它上面的增强。
|
||||
2. **它混进了一个根本不是图谱功能的东西**。「图谱增强检索(后端暗改)」用户看不见图谱,是 RAG 检索质量功能(属 ADR-19 检索演进线),不该占图谱候选池的决策位。本批不收。
|
||||
3. **「高价值低成本」是会骗人的排序轴**。7 项全标"高价值低成本"等于没排序,还诱导"先挑便宜的"。正确的尺子是**就绪度**——候选池里多数项不是"要新建的新功能",而是阶段四「双宿主统一」时被打散、就绪度不同的半成品。
|
||||
|
||||
**本批方向(作者拍板)**:让**在用的人更顺手**(日常可用性)——看得清关系、能层层聚焦、控制件不挡图。美观 / 惊艳 demo 后移。"功能完善"指**用户真正能用到的**功能,花里胡哨靠边站。
|
||||
|
||||
候选池逐项处置:
|
||||
|
||||
| 候选池项 | 本批处置 | 理由 |
|
||||
|---|---|---|
|
||||
| 关系类型上边 | ✅ 做(G1-1) | 关系词汇表与置信度体系已存在;当前边 `type` 实际是置信度,本批先补齐边数据契约,再渲染 |
|
||||
| 局部图模式 | ✅ 做(G1-2) | 主线;引擎已有 focus/neighbors/密度,差"聚焦视图"组装 |
|
||||
| 类型/时间过滤器 | ◐ 类型做、时间二期(G1-2) | 类型数据现成;时间需给节点补 mtime |
|
||||
| 左上角浮层(社区面板) | ✅ 重构为控制工具条(G1-3/4) | 现状不透明挡图、低频霸屏 |
|
||||
| 路径查找 + agent 讲解 | ⏸ 后移 | 惊艳 demo 非日常顺手;旧 HTML 做过、引擎留残骸 |
|
||||
| lint 健康上图 | ⏸ 下一批 | "知识库体检"与"浏览聚焦"不同类、代码不共享 |
|
||||
| 导出美图 | ⏸ 后移 | 美观后移;导出走外挂 Skill 是未来路线(见 §5 备注) |
|
||||
| 图谱增强检索 | ✗ 不收 | 非图谱功能,归 ADR-19 检索线 |
|
||||
| 远期池(嵌入布局 / LLM 推断边 / AI 摘要 / 社区摘要 hover) | ⏸ 远期 | 依赖消化管线升级 |
|
||||
|
||||
---
|
||||
|
||||
## §1 核心决策(G1-1 ~ G1-5)
|
||||
|
||||
### G1-1 关系类型上边(看清关系)
|
||||
|
||||
把边"是什么关系""有多确定"同时画出来,二者都是本批 G1-1 的完整交付,不做"只有颜色、没有置信度虚实"的降级版。
|
||||
|
||||
**数据契约 · 关系类型与置信度必须分开**:
|
||||
- `relation_type`(字段名可在实现时按本地命名定,但语义必须独立)承载关系词:实现 / 依赖 / 对比 / 矛盾 / 衍生
|
||||
- `confidence` 承载置信度:`EXTRACTED` / `INFERRED` / `AMBIGUOUS`
|
||||
- 当前代码里 `GraphEdge.type` 是 `Confidence`,`build-graph-data.sh` 输出的 `type` 也是置信度;执行时不可直接把现有 `type` 当关系类型上色
|
||||
- 如果 Phase 0.2 核验发现任一维缺失,先补 `build-graph-data.sh` 与类型/测试,再接 UI;后端服务仍零改动
|
||||
|
||||
**必做 · 颜色 = 关系类型**(克制编码,避免与节点色抢视觉——节点已用左色条编码 ENTITY/SOURCE 等类型):
|
||||
- 对立关系给警示色:**矛盾**、**对比**(琥珀)——❗ 矛盾色须**避开 ENTITY 节点已用的红**,取品红 / 橙红一类,防"红节点 vs 红边"语义混淆
|
||||
- 顺承关系(实现 / 依赖 / 衍生)= 统一中性色(蓝灰,跟随主题)
|
||||
- hover 边时浮出关系词中文,让中性色边也能查到具体类型
|
||||
|
||||
**必做 · 虚实 = 置信度**(`EXTRACTED` 实线 / `INFERRED` 虚线 / `AMBIGUOUS` 点划或弱虚线):置信度不再借用关系色表达,必须与关系类型并存。
|
||||
|
||||
**全局低权重、聚焦才完整呈现(与 G1-2 协同,关键)**:截图实测 88 节点 / 217 边,全局图上给每条边都强着色只会更糊。所以全局视图边保持**低视觉权重**(细、低饱和);进入 G1-2 聚焦视图(边数骤减)后,关系色 / 虚实才完整显现。关系边的价值在"看清局部",不在"全局花式"。
|
||||
|
||||
- **不做方向箭头**(有向信息 `from/to` 已在数据,箭头增噪,需要时再加)
|
||||
- 配套**边图例**(颜色 / 虚实含义)进控制工具条弹出层(G1-3)
|
||||
|
||||
**现状证据**:
|
||||
- 当前边数据带 `type`,但含义是置信度:[build-graph-data.sh:160](../../scripts/build-graph-data.sh#L160) 读取 `<!-- confidence: ... -->`,[build-graph-data.sh:261](../../scripts/build-graph-data.sh#L261) 输出 `{id, from, to, type}`
|
||||
- 当前类型定义也把边 `type` 定义成 `Confidence`:[types.ts:56](../../packages/graph-engine/src/types.ts#L56)
|
||||
- 当前渲染已有置信度 class / 虚线基础:[static-renderer.ts:940](../../packages/graph-engine/src/render/static-renderer.ts#L940)、[static-renderer.ts:1682](../../packages/graph-engine/src/render/static-renderer.ts#L1682)
|
||||
- 关系词汇表:`.wiki-schema.md`([schema-template.md:178](../../templates/schema-template.md#L178),实现 / 依赖 / 对比 / 矛盾 / 衍生)
|
||||
- 置信度体系:[SKILL.md:384](../../SKILL.md#L384) 起 `EXTRACTED` / `INFERRED`
|
||||
- ❗ Phase 0.2 必须产出样本,证明每条边同时有关系类型与置信度;缺哪个补哪个,不裁掉 UI 维度
|
||||
|
||||
**归属**:引擎层渲染(`packages/graph-engine/render`),两端同享。
|
||||
|
||||
### G1-2 递进式聚焦(层层钻进,主线)
|
||||
|
||||
用户心智:"先用社区筛出想看的范围,再在范围里点出重点。"两层递进:
|
||||
|
||||
```
|
||||
第一层 点社区行/团块 → 只显示该社区节点,隐藏其余社区(非淡化);进入【社区聚焦视图】
|
||||
第二层 视图内点节点 → 节点高亮 + 右抽屉打开阅读态(沿用 4.5 点击即阅读,二者合一)
|
||||
类型筛选 实体/主题/来源 → 与社区聚焦同机制(visibility 过滤),可叠加
|
||||
```
|
||||
|
||||
**手势契约**(关键,消歧义):
|
||||
|
||||
| 操作 | 行为 |
|
||||
|---|---|
|
||||
| 单击空白(聚焦视图内) | 退一层:节点高亮 → 当前社区视图(**不回全图**——用户明确要的"误操作不打回原形") |
|
||||
| 单击空白(全局视图) | 清空当前选区 / 高亮,不切换视图 |
|
||||
| 双击空白 | 一步回全图(沿用 4.5 D4.5-1 已有手势) |
|
||||
| Esc | 一步回全图并清空(**不做逐级**——逐级靠单击空白,分工清晰、不让用户迷糊在第几层) |
|
||||
|
||||
**弹出层与空白点击优先级**:如果控制工具条弹出层已打开,第一次单击画布空白只关闭弹出层,不触发聚焦退层 / 清选区;弹出层已关闭时,才执行上表的画布语义。
|
||||
|
||||
**与 4.5 的关系**:4.5 D4.5-6 的社区点击是"选中整簇高亮 + 其余淡化 + 视口飞至"(选区态)。本批**升级**为"隐藏其余、进入聚焦视图",同时抽屉仍呈现该簇选区态动作(聚焦与选中合一,一个动作两个收益)。
|
||||
|
||||
**现状证据**(多为"半成品收尾"而非新建):
|
||||
- `focusNode` 引擎已有且工作台已接:[GraphPanel.tsx:361](../web/src/components/GraphPanel.tsx#L361)
|
||||
- `neighbors` 选区全链路已通(App / GraphSelection / GraphPanel / RightDrawer)
|
||||
- 密度模式 `point-plus-focus`、`model/visibility.ts::applyFocusMode`、`Community` 全套类型均在
|
||||
- ❗ 区分:现有 `focusNode` 是"镜头对准某点",`neighbors` 是"全局图上叠加高亮";本批新增的是"**隐藏非聚焦集、只渲染聚焦子集**"的视图过滤层
|
||||
|
||||
**类型筛选**:节点 `type`(entities/topics/sources)建库即分好([build-graph-data.sh:89](../../scripts/build-graph-data.sh#L89)),纯前端开关。**时间筛选(最近 N 天)本批不做**——节点数据无 mtime,需轻度改管线,归二期。
|
||||
|
||||
**归属**:引擎层过滤逻辑,两端同享。
|
||||
|
||||
### G1-3 顶部控制工具条(取代左上角浮层)
|
||||
|
||||
**问题**:现左上角"社区"面板不透明、从顶到底霸屏、直接盖住画布节点(作者截图实证),且把"图例(被动看)"与"聚焦筛选(主动点)"两种相反性质揉在一处 → 又大又挡。
|
||||
|
||||
**设计**:把常驻浮层换成"画布是主角、控制件平时收边、叫了才上前"。
|
||||
|
||||
- **位置**:图谱视图顶部标签栏下方的横条(工作台截图红框区,现为空白);离线 HTML 取页面顶部。固定、不挡图、横向可扩展
|
||||
- **平时**:只露少量入口(筛选/社区、图例、回全图),其余收"更多"
|
||||
- **点"筛选/社区"** → 弹出一张紧凑面板:社区列表(点行 = 进 G1-2 聚焦视图)+ 类型开关 + 边图例(颜色/虚实含义)。点画布空白优先收起弹出层
|
||||
- **取代**现常驻浮层"社区"白面板(删除该浮层)
|
||||
|
||||
> **取舍**:本方案比"给旧面板加个折叠箭头"工程量大,但本批的类型筛选(G1-2)和边图例(G1-1)都要进场,塞进旧大列表只会更挤;一次把工具条立起来避免二次返工。这是主动选择,不是过度设计。
|
||||
|
||||
**防失控原则**(写死,避免工具条沦为图标垃圾堆):
|
||||
|
||||
> 工具条只放「对整张图」的操作(筛选 / 回全图 / 图例 / 未来导出);「对单个节点」的操作(阅读 / 提问 / 建链)留在点节点后的右抽屉。常用露出、长尾收「更多」。
|
||||
|
||||
- **成长位(本批不做,仅预留布局)**:导出美图、切换布局、健康体检——来了都挂这条工具条
|
||||
|
||||
### G1-4 默认收起 + 半透明("折叠"需求的升级)
|
||||
|
||||
用户原始诉求是"面板可折叠(小屏不友好)"。升级为:
|
||||
|
||||
- **不是"默认展开 + 可折叠",而是"默认收起、用时展开"**(弹出层用完即收,画布常态 100% 可见)
|
||||
- 控制面板 / 弹出层**半透明(毛玻璃)**,即便展开也不死挡图
|
||||
- 收起 / 展开状态**记本机**(沿用 4.5 D9 / ADR-22:"图例折叠属于浏览状态,留本机,不入库文件")
|
||||
|
||||
### G1-5 双宿主分工
|
||||
|
||||
沿用 ADR-21「一个引擎、两个宿主」+ capabilities 注入:
|
||||
|
||||
| 能力 | 工作台 | 离线 HTML(Skill) |
|
||||
|---|---|---|
|
||||
| 关系边上色/虚实 + 边图例(G1-1) | ✅ | ✅ |
|
||||
| 递进聚焦 + 类型筛选(G1-2) | ✅ | ✅ |
|
||||
| 控制工具条 + 默认收起 + 半透明(G1-3/4) | ✅ 顶部标签栏下方 | ✅ 页面顶部 |
|
||||
| 节点提问 / 建链(onAsk) | ✅ | ✗(离线无 agent,点节点 = 阅读/高亮,不提问) |
|
||||
|
||||
差异仅靠现有 `GraphEngineCapabilities`(`onAsk` 等回调)表达,引擎核心零分叉。
|
||||
|
||||
---
|
||||
|
||||
## §2 实施面
|
||||
|
||||
**实施顺序(先止血)**:G1-3 / G1-4(解决挡图)→ G1-2(聚焦 + 类型筛选)→ G1-1(关系边)。关系边排最后,因它要靠聚焦视图才看得清(见 G1-1 全局低权重)。
|
||||
|
||||
```
|
||||
packages/graph-engine/
|
||||
├── src/types.ts 边契约区分 relation_type 与 confidence,保留旧 type=confidence 兼容入口直到脚本与测试迁完
|
||||
├── render/ 关系边渲染(全局低权重 / 聚焦完整呈现)+ hover 关系词;边图例;控制工具条 + 弹出面板(半透明 / 默认收起);
|
||||
│ 离线 HTML 已有 `.offline-header`(标题 + 统计 badges,build-graph-html.sh:239),工具条挂入该 header,非新增结构
|
||||
├── model/ 聚焦视图过滤层(隐藏非聚焦集,区别于现有 focus 镜头/neighbors 叠加);类型筛选过滤
|
||||
├── select/ 社区点击语义:从"选中高亮"升级为"进入聚焦视图 + 选区态"(修订 D4.5-6)
|
||||
└── index.ts 必要的视图状态 API(进/出聚焦视图、当前筛选集)
|
||||
|
||||
workbench/web/
|
||||
├── 删除左上角常驻"社区"浮层,改接顶部工具条
|
||||
├── 顶部标签栏下方挂工具条容器;弹出面板复用 cmdk/shadcn 既有组件
|
||||
└── 单击/双击空白手势接线(G1-2 手势契约);节点聚焦 → 右抽屉阅读态(复用 4.5 GraphReader)
|
||||
|
||||
tests/
|
||||
├── 引擎单测:关系类型→颜色、置信度→虚实;聚焦视图过滤(隐藏集正确);类型筛选;手势状态机
|
||||
└── 回归:关系边 DOM 断言;社区点击新语义;离线 HTML 工具条存在 + 无 onAsk 入口
|
||||
```
|
||||
|
||||
脚本:`scripts/build-graph-data.sh` 在本批范围内,用来补齐关系类型 + 置信度边契约。后端服务:**零改动**。离线 HTML:引擎升级自动获得关系边 / 聚焦 / 工具条(两端红利)。
|
||||
|
||||
## §3 验收剧本(给实施 plan 引用)
|
||||
|
||||
1. **关系边**:边按关系类型着色——矛盾色(**非 ENTITY 红**)、对比琥珀、顺承中性色;置信度控制虚实——`INFERRED` 虚线 / `EXTRACTED` 实线 / `AMBIGUOUS` 弱虚线;全局视图边低权重、聚焦视图内完整呈现;hover 边浮关系词;山水 / 墨夜两主题下均与节点色可区分
|
||||
2. **边图例**:工具条弹出面板含边图例,颜色/虚实说明与实际渲染一致
|
||||
3. **社区聚焦(第一层)**:点社区 → 仅该社区节点可见、其余隐藏;**单击空白回到该社区视图而非全图**;双击空白回全图
|
||||
4. **节点聚焦(第二层)**:社区视图内点节点 → 高亮 + 右抽屉阅读态同时出现;Esc 一步回全图并清空
|
||||
5. **类型筛选**:实体/主题/来源开关即时增减可见节点,可与社区聚焦叠加;时间筛选**不存在**(二期)
|
||||
6. **控制工具条**:左上角不再有常驻浮层;顶部工具条平时只露少量入口、半透明不挡图;点筛选弹出紧凑面板(社区+类型+边图例),弹出层打开时点空白只先收回弹出层;收起状态重启后保持(本机)
|
||||
7. **离线 HTML**:关系边 / 聚焦 / 类型筛选 / 工具条全部可用;点节点 = 阅读/高亮,**无提问入口**
|
||||
8. **自动化**:主仓库 JS 测试 / regression / typecheck / 引擎测试 / 双产物构建全绿
|
||||
|
||||
## §4 风险
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 边密集(217 条)+ 多色 → 全局噪音 | G1-1:全局低权重、聚焦才完整呈现;仅对立关系独立色(矛盾避 ENTITY 红)、顺承中性;双主题调试取证 |
|
||||
| 聚焦视图过滤与现有 focus/neighbors 概念混淆 | §1 已区分"镜头/叠加/过滤"三者;新增的是过滤层,不动现有两者;单测断言隐藏集 |
|
||||
| 社区点击语义变更破坏 4.5 选区回归 | 明确为"升级 D4.5-6";保留选区态动作,只加"隐藏其余";4.5 selection 测试须保持绿 |
|
||||
| 边 `type` 当前是置信度,不是关系类型;关系类型 / 置信度任一维可能缺失 | Phase 0.2 先定完整边契约;缺字段就在 build-graph-data.sh 与类型/测试补齐,再接 UI;不交付"仅颜色维"半成品 |
|
||||
| 工具条 / 弹出层在离线 HTML 与工作台样式漂移 | 引擎层统一实现,宿主只注入位置与 capabilities |
|
||||
|
||||
## §5 实施结果:PRODUCT.md / ADR 同步
|
||||
|
||||
阶段 4.6 已按本文档落地,实施后文档状态如下:
|
||||
|
||||
1. **阶段编号**:已定 **4.6**(2026-06-14),PRODUCT.md §4 / §10 已登记为已完成
|
||||
2. **ADR 同步**:
|
||||
- 已修订 **ADR-21 / D4.5-6**:社区交互从"选中高亮"升级为"聚焦视图(隐藏其余)";左上角浮层图例 → 顶部控制工具条
|
||||
- 已新增 **ADR-23**:关系边可视化采用"关系类型控制颜色、置信度控制虚实"
|
||||
- 已在 **ADR-19** 注记"图谱增强检索"移交检索质量演进线,不占本批图谱候选池决策位
|
||||
3. **候选池更新**:PRODUCT.md「图谱演进候选池」已标注本批已落地项,并记录"图谱增强检索移交 ADR-19 线"
|
||||
|
||||
> 备注(导出美图走外挂 Skill 的未来路线):作者倾向美观/导出类用 pi-agent 可插拔 Skill 实现。需厘清——**工作台里图本身的实时观感属渲染引擎,Skill 改不了**;Skill 能做的是"吃图数据吐精致产物(PNG/SVG/独立 HTML)"。且 Skill 是独立进程,拿不到"工作台当前视角"(缩放/聚焦/钉位),"导出当前视角"要么退回导出全局,要么由工作台把视口状态喂给 Skill。本批不实现,记此以备远期。
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-06-14 v4(实施完成同步):阶段 4.6 已落地并通过验收;更新状态为已实施,§5 改为实施结果,记录 PRODUCT.md / ADR-19 / ADR-21 / ADR-23 / 候选池同步结果。
|
||||
- 2026-06-14 v3(计划审查修订):修正 G1-1 数据事实——当前 edge `type` 是置信度而非关系类型;明确本批必须补齐关系类型 + 置信度双字段,不做"仅颜色维"降级;把 build-graph-data.sh 纳入必要实施面;统一 Esc 为一步回全图;补工具条弹出层与画布空白点击优先级。
|
||||
- 2026-06-14 v2(自审修订):spark 后自我 review 补 6 处——G1-1 拆"颜色必做 / 虚实条件做"并补"全局低权重、聚焦才完整呈现"(找回脑暴洞察:关系边价值在局部不在全局)、矛盾色避开 ENTITY 红;G1-2 的 Esc 改"一步回全图"(去三级逐级)、补全局视图单击空白行为;G1-3 记工具条 vs 折叠取舍理由;§2 厘清离线 HTML 工具条挂入现有 offline-header、补实施顺序(G1-3/4 → G1-2 → G1-1);验收剧本 1 与风险表同步。
|
||||
- 2026-06-14 v1:首版。来源:2026-06-14 spark 脑暴(候选池三处纠偏 + 按就绪度而非成本重排)。含 G1-1(关系边双维度克制编码)、G1-2(递进聚焦 + 手势契约,升级 D4.5-6)、G1-3(顶部控制工具条取代浮层)、G1-4(默认收起 + 半透明)、G1-5(双宿主分工)、实施面 / 验收剧本 / 风险 / PRODUCT.md 前置动作。
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,504 @@
|
||||
# 阶段四设计文档:monorepo 合并 + 图谱活地图
|
||||
|
||||
> 状态:**已实施完成,主观验收项待验收人判断**
|
||||
> 日期:2026-06-12
|
||||
> 前置讨论:本文档由 2026-06-12 的四轮设计对话沉淀而成(战略定位 → 选区设计 → 钉扎持久化 → 生长事件链 → 引擎抽取)。
|
||||
|
||||
---
|
||||
|
||||
## §0 文档用法
|
||||
|
||||
- 本文档是阶段四的**唯一实施依据**。与 PRODUCT.md 冲突时,以 PRODUCT.md §7 的 ADR-20 / ADR-21 为准(两者由同一轮讨论产出,理论上不会冲突)。
|
||||
- 每个 Step 动手前先读对应小节;实施者每完成一个 Step,按协作约定 commit 并向作者列改动清单。
|
||||
- ❗ 标记 = 已知坑,实施时必须按写死的对策执行,不要自由发挥。
|
||||
|
||||
---
|
||||
|
||||
## §1 阶段四总览
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
两条线在本阶段汇合:
|
||||
|
||||
1. **战略线**:llm-wiki 的终局形态已定——"一个产品、两扇门"(产品 = 知识库格式 + 中文素材管线 + 方法论;门一 = Skill,门二 = 工作台)。ADR-16 规划的"agent 并回主仓库"需要一个启动时机。
|
||||
2. **产品线**:原阶段四"图谱集成"经设计讨论升级为"图谱活地图"——图谱不是嵌进来的 HTML,而是工作台的第二主屏,同时图谱引擎反哺 Skill 的离线 HTML。
|
||||
|
||||
两条线汇合的原因:图谱引擎是**两端共享的第一块代码**。共享代码出现的那一刻,"分居两仓库"开始产生真实摩擦(跨仓库依赖、双份维护)——这就是合并时机成熟的信号。
|
||||
|
||||
### 1.2 两大目标
|
||||
|
||||
1. **monorepo 合并(工程部分)**:本仓库(llm-wiki-agent)整体搬入主仓库(llm-wiki-skill 仓库)成为 workspace 成员。**不发版、不改 README、不对外宣布**——品牌动作(仓库改名、双形态叙事)留给后续阶段。
|
||||
2. **图谱活地图**:共享图谱引擎 `@llm-wiki/graph-engine` 落地,工作台获得活的图谱视图(活模拟、钉扎、选区提问、生长动画),Skill 离线 HTML 在最后一步切换到引擎产物。
|
||||
|
||||
### 1.3 范围
|
||||
|
||||
- Step 0:monorepo 搬家 + workspace 根配置 + 全链路冒烟
|
||||
- Step 1:引擎包骨架 + helpers 纯函数 TS 化 + 测试迁移
|
||||
- Step 2:工作台图谱视图(静态复现基线)+ 主题 token(山水 / 墨夜第一版)
|
||||
- Step 3:活模拟 + 钉扎 + 持久化
|
||||
- Step 4:选区系统 + 对话联动
|
||||
- Step 5:文件监听 + 重算链 + 生长动画
|
||||
- Step 6:Skill 离线 HTML 切换引擎产物
|
||||
- Step 7:总验收 + 墨夜主题打磨 + UX 体感收尾
|
||||
|
||||
### 1.4 不包含
|
||||
|
||||
- ❌ 仓库改名 / README 双形态叙事 / 对外发布(后续品牌阶段)
|
||||
- ❌ Tauri 打包(原阶段五,已决策推迟到工作台被真实外部用户使用之后)
|
||||
- ❌ 跨知识库图谱("联邦知识库"远期命题)
|
||||
- ❌ 自由套索圈选(见 D5,明确砍掉)
|
||||
- ❌ 全部"明确不做清单"见 §8
|
||||
|
||||
---
|
||||
|
||||
## §2 关键设计决策(D1–D14)
|
||||
|
||||
> 每条决策含结论与一句话理由。完整论证过程见设计对话,此处只存可执行结论。
|
||||
|
||||
### D1 仓库布局:丙方案(monorepo 一次成型)
|
||||
|
||||
agent 仓库整体搬入主仓库子目录 `workbench/`,图谱引擎作为第一个共享包 `packages/graph-engine/`。目标布局:
|
||||
|
||||
```
|
||||
llm-wiki-skill 仓库(未来品牌阶段改名 llm-wiki)
|
||||
├── SKILL.md / scripts/ / templates/ / platforms/ / deps/ / tests/ ← Skill 主线,不动
|
||||
├── packages/
|
||||
│ └── graph-engine/ ← 新:共享图谱引擎(TS)
|
||||
├── workbench/ ← 原 llm-wiki-agent 全部内容(server/ web/ docs/ PRODUCT.md ...)
|
||||
├── package.json ← 新:workspace 根
|
||||
├── .mise.toml / .nvmrc ← 从 agent 仓库上移
|
||||
└── ...
|
||||
```
|
||||
|
||||
- 搬运方式:`git subtree add --prefix=workbench <agent 仓库 URL> main`(保留全部 57 个 commit 历史,路径自动重写到 workbench/ 下)。
|
||||
- 旧 agent 仓库处置:**保留不动**(不 archive、不加迁移说明),处置推迟到品牌阶段。开发主场即日起切到 monorepo。
|
||||
- 理由:砖要砌在最终的房子上;同屋檐下 workspace 联调体验最好;主仓库 git 活跃恢复是战略副产物。
|
||||
|
||||
### D2 图谱地位:主区域第二视图
|
||||
|
||||
- 侧栏新增"图谱"入口;主区域在"对话 ⇄ 图谱"间切换;图谱**绑定当前知识库**(与 ADR-12 会话绑库同构:切库 = 换地图)。
|
||||
- 对话仍是第一主屏(ADR-2 不动摇)。图谱只接"结构可见"的问题;非结构问题(消化、闲聊)主入口仍是对话。
|
||||
|
||||
### D3 一个引擎、两个宿主
|
||||
|
||||
- 引擎包 `@llm-wiki/graph-engine`:数据进、画布出,构建双产物(ESM 给工作台 React;IIFE 单文件给离线 HTML 打包)。
|
||||
- 宿主差异通过 **capabilities 能力注入**表达(见 §4.5),引擎核心零分叉,没有任何"如果是离线 HTML 就…"的判断。
|
||||
- React 侧是 ~30 行薄壳组件(useRef 容器 + useEffect 生命周期),React 管壳、引擎管画布内部。
|
||||
|
||||
### D4 主题:一对官方主题,跟随工作台
|
||||
|
||||
- 浅色「数字山水」(品牌签名,离线 HTML 默认)+ 深色「墨夜」(同一国风语法的夜间变体:黑底、白墨、朱砂点睛)。
|
||||
- 图谱主题**跟随工作台浅/深切换**,不提供独立的图谱主题选择。明确不做主题商店。
|
||||
- 主题实现为 CSS token 层(颜色、纹理、字体变量),留扩展位但只维护两个官方主题。
|
||||
|
||||
### D5 选区:结构化四式,砍掉自由套索
|
||||
|
||||
空间邻近 ≠ 语义相关(力导向布局位置是算法产物),自由画圈必然圈出无语义保证的集合,**不做**。只提供四种自带语义保证的选择方式:
|
||||
|
||||
| 方式 | 操作 | 语义保证 |
|
||||
|---|---|---|
|
||||
| 选一页 | 点节点 | 就是这一页 |
|
||||
| 选一簇 | 点社区色块/标签 | Louvain 聚类,内部链接密集 |
|
||||
| 选一页+语境 | 选中后点"+邻居" | 沿真实 wikilink 扩一跳 |
|
||||
| 手动挑几页 | Shift+逐个点 | 用户明确自选 |
|
||||
|
||||
### D6 选区动作:结构事实先行,动作随性质变,本质 = 已有工作流的空间入口
|
||||
|
||||
- 选区面板先显示**结构事实**(N 页、M 条内部链接、跨几个社区、孤立数),再按选区性质给 2–4 个动作按钮 + 自由输入框:
|
||||
- 单社区 → 总结这一簇(digest)/ 找知识缺口(lint 局部)/ 生成主题页
|
||||
- 两社区 → 为什么没联系 / 找潜在桥梁 / 对比这两块(comparisons)
|
||||
- 互不相连的多选 → 探索潜在联系 / 对比异同(**不**提供"总结这一簇"——那是伪问题)
|
||||
- 孤岛页 → 把它链入知识库
|
||||
- **选区 = 批量 `@`**:发送时选区展开为结构化文本(页面清单 + 链接关系 + 社区归属),沿用现有 `/api/prompt` 文本通道,**不加新参数**;页面正文由 agent 按需 `read`(与阶段二 `@` 语义、ADR-19 检索路线一致)。
|
||||
- UI 上选区在输入框呈现为一个胶囊 chip:`@[选区:Agent工程 · 12页]`,可点击回图谱重看。
|
||||
- 发送目标:默认**当前活跃对话**;面板留"在新对话中打开"次按钮。
|
||||
|
||||
### D7 跨库图谱:不做
|
||||
|
||||
图谱能跨库而对话不能(ADR-12)会撕裂圈选提问的心智;跨库联系频繁出现说明分库方式该调。留给远期"联邦知识库"命题。
|
||||
|
||||
### D8 钉扎交互:活模拟 + 松手即钉 + 双击解钉
|
||||
|
||||
- 工作台图谱运行实时力模拟(d3-force:斥力 + 弹簧力 + 向心力)。
|
||||
- 拖动时**低温运行**(alphaTarget ≈ 0.1–0.2):直接邻居被弹簧温和带动让位,隔层微颤,远处不动;松手后冷却归零,全图入睡(❗ 确保 alphaMin 配置正确,图绝不能永远蠕动)。
|
||||
- 松手即钉(设 fx/fy),亮朱砂图钉角标;双击解钉,节点飘回算法位置。
|
||||
- 钉住的节点是后续自动布局的锚点:新节点在锚点间自动找位置——手动与自动不抢方向盘。
|
||||
- "重置布局"按钮:解开全部钉 + 清文件 + 重跑模拟;交互为**重置后 toast 撤销**,不做事前确认弹窗。
|
||||
- 被拖动节点的**邻居**因物理被带动的新位置**不写入持久化**(只存用户的主观决策)。
|
||||
|
||||
### D9 钉扎持久化:库文件,只存钉的,路径为 key
|
||||
|
||||
- 存储位置:知识库根下 `.wiki-graph-layout.json`(与 `.wiki-cache.json` 同级同风格)。
|
||||
- 原则:**"对知识的主观组织进库文件;浏览状态(缩放/平移/折叠)留本机 localStorage"**。此原则适用于今后所有同类问题。
|
||||
- 格式见 §4.1。只存钉住的节点;key 用**库内相对路径**;坐标用**模型坐标**(❗ 不是屏幕坐标,否则缩放平移后错位)。
|
||||
- 路径 key 天然解决 PR #44 的"指纹作废"病根:图谱重建后路径未变的钉扎自动存活;被删页面在下次图谱数据重建时惰性清理;改名 = 钉扎丢失(可接受退化,不做改名跟踪)。
|
||||
- 写入:前端松手时调后端 API,整文件覆写,防抖(拖动过程不写盘)。文件损坏 = 当作不存在,从零开始,不崩溃。
|
||||
- 并发:工作台单实例(PRODUCT.md §6.7);Skill 侧只读不写此文件。
|
||||
- Obsidian 共存忽略清单(PRODUCT.md §6.4)与 init 生成的 `.gitignore` 模板**不需要**改动逻辑,但 agent 读写文件清单(§6.4"agent 读写的文件")需补 `.wiki-graph-layout.json`。
|
||||
|
||||
### D10 位置层 / 结构层分权
|
||||
|
||||
| 层 | 内容 | 谁说了算 | 拖动改变它吗 |
|
||||
|---|---|---|---|
|
||||
| 位置层 | 节点摆哪、钉不钉 | 用户 | 改变 |
|
||||
| 结构层 | 颜色、社区归属、连线 | 知识库里的真实 wikilink | 完全不变 |
|
||||
|
||||
- 拖 A 进别的社区团块,A 颜色不变——视觉语言自行消解"拖动改分类"的误解。
|
||||
- 想真正改变社区归属 = 改知识:通过选区提问让 agent 建立真实链接写回 wiki,下次重算颜色才变。**想改图的样子,动手拖;想改知识的结构,开口问。**
|
||||
- 水墨团块按"成员聚集主体"晕染,**不追离群钉点**(否则团块拉成变形虫);拖动中团块淡化,松手定格后再重新晕染。
|
||||
|
||||
### D11 混合布局:预计算起点 + 低温入睡 + 活会话坐标策略
|
||||
|
||||
- **冷启动**:打开图谱时初始位置 = 构建期预计算坐标(build-graph-data 现有能力)+ 钉扎文件钉位;活模拟在此起点低温微调数秒后入睡。保证:每次打开长相稳定、与离线 HTML 初始视图一致、肌肉记忆不失效。
|
||||
- **活会话**(❗ 防"布局漂移"):图谱开着时收到重算结果,**既有未钉节点保持当前画面位置不动**(不采用新一轮预计算坐标),新增节点由活模拟在现有布局的缝隙中安置。预计算坐标**只用于冷启动**——否则每次重算老节点集体跳位,画面失控。
|
||||
- 推论:跨会话重新打开时布局可能与上次会话末态不同(新一轮预计算)——这是力导向图常态(Obsidian 同),钉扎就是给用户的稳定工具。
|
||||
|
||||
### D12 重算链:监听文件系统,不监听"消化"
|
||||
|
||||
- 变化源至少五个:批量/单篇消化、结晶、agent 补链(选区提问闭环的终点)、lint 修复、Obsidian 手改。只盯"消化"会让地图说谎。
|
||||
- 底座:后端监听当前知识库目录(`.md` 与 `.wiki-schema.md`),**防抖 ~5s** 合并零星变化;**自家批量流程(批量消化)开始时挂起监听、done 后立即触发一次**。
|
||||
- ❗ **监听排除清单**:`.wiki-tmp/`(Skill 运行时临时目录,消化过程狂写,不排除会被自家流程刷爆)、`.git/`、`.obsidian/`、`node_modules/`、`.DS_Store`,以及自家生成物 `.wiki-graph-layout.json`、`wiki/graph-data.json`、`wiki/knowledge-graph.html`。与 PRODUCT.md §6.4 忽略清单对齐。
|
||||
- 监听器生命周期跟随当前 KB:切库时停掉旧库监听、启动目标库监听(与会话切换同一时机)。
|
||||
- 重算 = 子进程跑现有 `build-graph-data.sh` 全量管线(❗ 不重写、不做增量图计算)。重算期间旧图谱照常显示与交互;新数据就绪才换场,绝不白屏。
|
||||
- 重算进行中又有新触发:排队合并(单飞行任务 + 最多一个 pending,跑完再跑一次)。
|
||||
- 重算完成后与旧版数据对比产出**差异清单(diff)**——diff 即动画剧本。
|
||||
|
||||
### D13 生长动画:diff 队列,图谱可见时消费
|
||||
|
||||
- SSE 推 `graph_updated` 事件(含 diff 与统计)。前端三分支:
|
||||
- 图谱视图可见 → 播放生长动画
|
||||
- 图谱不可见 → 侧栏图谱入口亮安静徽标;diff 入挂起队列,**打开图谱时从旧布局开场补播**(最高价值一幕:批量消化完才打开图谱看成果)
|
||||
- 用户正在拖节点 → 挂起,松手后播
|
||||
- 动画剧本(按 diff 四类):新节点从**语义锚点**(其邻居位置)发芽、由活模拟安置(呼应 D11:既有未钉节点不动),孤岛从空白处淡入;新边墨线描画;换社区节点颜色渐变(以 §4.3 社区对齐后的真实变化为准);删除节点淡出;全新社区诞生 → 团块晕染浮现。
|
||||
- 节奏:错峰发芽(间隔几十 ms),总时长**压在 2–3 秒内**;点击画布立即定格;尊重系统 `prefers-reduced-motion`(开了直接定格,零配置)。
|
||||
- 工作台重启后不补播(动画是体验不是持久化资产),直接显示最新状态。
|
||||
|
||||
### D14 引擎抽取:新骨架、旧器官
|
||||
|
||||
现有 graph-wash 代码按四级资产处理。事实依据:已验证 `graph-wash.js` / `graph-wash-helpers.js` 两个运行时文件 **0 处使用 d3 / rough**(纯手写几何引擎);Step 2 已核查 `header.html` 内联脚本,同样 **0 处使用 d3 / rough**,仅命中 `graph-data` JSON script。Step 6 因此移除 `d3.min.js` / `rough.min.js` 与旧 `graph-wash*.js`,marked / purify 保留:
|
||||
|
||||
| 级 | 内容 | 处理 |
|
||||
|---|---|---|
|
||||
| A 直接搬 | graph-wash-helpers.js ~1325 行纯函数 + tests/js 配套测试 | TS 化,**逻辑一行不改**,测试迁移保绿 |
|
||||
| B 拆开搬 | graph-wash.js ~1008 行中的"画法"(节点视觉分层、团块晕染、阅读态) | 画法保留;骨架(全局单例、直接操作 document)换成可实例化引擎 |
|
||||
| C 留给宿主 | header.html ~2001 行壳与样式 | 壳归宿主自理;视觉样式抽成主题 token 共享 |
|
||||
| D 新写 | 力模拟+钉扎、选区、diff 动画、React 壳、capabilities | 本阶段新增 |
|
||||
|
||||
- ❗ A 级 TS 化纪律:先 1:1 翻译保测试绿,禁止"顺手优化"逻辑。
|
||||
- ❗ 手绘感"沸腾效应":图形路径生成一次即缓存,动画帧只改 transform,**绝不每帧重算路径**。
|
||||
- 力模拟依赖 `d3-force` 单模块(几 KB),不引入完整 d3;离线 HTML 打包清单里的完整版 `d3.min.js` 与 `rough.min.js` 已在 Step 6 移除(marked / purify 保留,阅读态在用)。
|
||||
- 不做 SVG/Canvas 双渲染后端(几百节点 SVG 足够;canvas 是将来性能实测不足时的事)。
|
||||
- 不把引擎做成对外发布的通用图谱库。
|
||||
|
||||
---
|
||||
|
||||
## §3 新增依赖
|
||||
|
||||
| 依赖 | 位置 | 用途 | 备注 |
|
||||
|---|---|---|---|
|
||||
| `d3-force` | packages/graph-engine | 力模拟(斥力/弹簧/向心) | 单模块几 KB;唯一确定新增 |
|
||||
| `concurrently` | monorepo 根 | 原 agent 根 devDependency 上移 | 非新增,位置迁移 |
|
||||
|
||||
**候补(默认不装,触发条件见 §7)**:`chokidar`(若 Node 22 原生 `fs.watch` recursive 在 macOS 实测不可靠才引入)。
|
||||
|
||||
构建工具:引擎包双产物(ESM + IIFE)优先用 workbench/web 已有的 Vite 体系(`vite build --lib` 两种 format),**不新增打包器**。
|
||||
|
||||
---
|
||||
|
||||
## §4 数据与接口契约
|
||||
|
||||
> 路径与字段为**建议命名**,实施时如需调整,在 commit message 里说明并回填本文档。
|
||||
|
||||
### 4.1 `.wiki-graph-layout.json`(新,知识库根目录)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"pins": {
|
||||
"wiki/topics/agent-engineering.md": { "x": 412.5, "y": -88.2 },
|
||||
"wiki/entities/karpathy.md": { "x": 130.0, "y": 245.7 }
|
||||
},
|
||||
"updatedAt": "2026-06-12T10:23:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
- key = 库内相对路径;坐标 = 模型坐标;只含钉住的节点。
|
||||
- 读不出 / 格式不符 → 当作空文件处理。
|
||||
|
||||
### 4.2 `graph-data.json`(现有,格式不变)
|
||||
|
||||
由 `build-graph-data.sh` 产出,工作台后端子进程调用。引擎包内为其补 TS 类型定义(`types.ts`),类型是契约的单一事实源。
|
||||
|
||||
### 4.3 diff 结构(引擎包内定义并实现)
|
||||
|
||||
```ts
|
||||
interface GraphDiff {
|
||||
addedNodes: NodeId[]; // 新增节点(生长动画主角)
|
||||
removedNodes: NodeId[]; // 删除节点(淡出)
|
||||
recoloredNodes: Array<{ id: NodeId; from: CommunityId; to: CommunityId }>;
|
||||
addedEdges: EdgeId[]; // 新边(墨线描画)
|
||||
removedEdges: EdgeId[];
|
||||
newCommunities: CommunityId[]; // 对齐后仍无法匹配的全新社区(团块浮现动画)
|
||||
stats: { nodeCount: number; edgeCount: number; communityCount: number };
|
||||
}
|
||||
```
|
||||
|
||||
diff 计算工具放引擎包(跟着数据类型走),后端调用。
|
||||
|
||||
❗ **diff 必须先做社区对齐再判变色**:Louvain 重跑后社区编号可能整体洗牌("3 号社区"变"1 号"),直接比对编号会满屏误报变色。算法:新旧社区按**成员重叠率**(Jaccard)贪心配对,配对成功的沿用旧编号语义,配对后成员真正换了归属的才进 `recoloredNodes`;无法配对的新社区进 `newCommunities`。
|
||||
|
||||
### 4.4 后端 API(workbench/server 新增)
|
||||
|
||||
| 方法 | 路径 | 行为 |
|
||||
|---|---|---|
|
||||
| GET | `/api/graph` | **只读**:返回当前 KB 的 graph-data;无数据时返回 `{ needsBuild: true }`,**不**在 GET 里触发构建 |
|
||||
| POST | `/api/graph/rebuild` | 异步触发重算(与监听触发走同一单飞行队列),立即返回;完成经 SSE `graph_updated` 通知 |
|
||||
| GET | `/api/graph/layout` | 返回钉扎文件内容(无文件返回空 pins) |
|
||||
| PUT | `/api/graph/layout` | 整文件覆写钉扎 |
|
||||
|
||||
前端首开流程:GET 得 `needsBuild` → 显示"构建中" → 自动 POST rebuild → 收到 `graph_updated` 后 GET 取数渲染。构建可能秒级到几十秒(大库),全程不阻塞请求。
|
||||
|
||||
SSE 新事件(沿用现有 `/api/events` 通道):
|
||||
|
||||
```
|
||||
event: graph_updated
|
||||
data: { "diff": GraphDiff | null, "rebuiltAt": "..." }
|
||||
```
|
||||
|
||||
引入节奏:Step 2 先上**最简版**(`diff: null`,仅作"构建完成"通知);Step 5 扩展携带真实 diff。
|
||||
|
||||
### 4.5 引擎 API(`@llm-wiki/graph-engine` 门面)
|
||||
|
||||
```ts
|
||||
const engine = createGraphEngine(container: HTMLElement, {
|
||||
data: GraphData,
|
||||
pins: PinMap,
|
||||
theme: "shan-shui" | "mo-ye",
|
||||
capabilities: {
|
||||
persistPins?: (pins: PinMap) => Promise<void>; // 工作台→PUT 后端;离线→localStorage 适配器
|
||||
onAsk?: (selection: Selection) => void; // 不传 → 选区面板不显示提问动作
|
||||
onOpenPage?: (path: string) => void; // 工作台→右抽屉;离线→内置阅读态
|
||||
}
|
||||
});
|
||||
|
||||
engine.applyDiff(diff: GraphDiff): Promise<void>; // 生长动画,resolve 于动画结束/被跳过
|
||||
engine.focusNode(path: string): void; // 对话联动高亮
|
||||
engine.select(selector: SelectionInput): void;
|
||||
engine.setTheme(theme: ThemeId): void;
|
||||
engine.destroy(): void;
|
||||
```
|
||||
|
||||
### 4.6 引擎包目录结构
|
||||
|
||||
```
|
||||
packages/graph-engine/
|
||||
├── src/
|
||||
│ ├── model/ ← A级:helpers 纯函数 TS 化(atlas 模型/视口/小地图/密度)
|
||||
│ ├── render/ ← B级:绘制重组(节点视觉分层、团块晕染、阅读态部件)
|
||||
│ ├── sim/ ← D级:d3-force 封装 + 钉扎
|
||||
│ ├── select/ ← D级:选区系统(四式选择 + 结构事实计算)
|
||||
│ ├── anim/ ← D级:diff 生长动画
|
||||
│ ├── themes/ ← C级:shan-shui / mo-ye token
|
||||
│ ├── diff.ts ← diff 计算
|
||||
│ ├── types.ts ← graph-data / pins / diff 类型(契约单一事实源)
|
||||
│ └── index.ts ← createGraphEngine 门面
|
||||
├── test/ ← 迁移自主仓库 tests/js/*.test.js
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §5 八个 Step 详细设计
|
||||
|
||||
### Step 0:monorepo 搬家
|
||||
|
||||
**前置**(❗ 流程顺序):先把 agent 仓库的 `stage-4-design` 分支合入其 `main`(设计文档必须随 subtree 进主仓库,否则搬过去的是没有阶段四设计的旧 main)。
|
||||
|
||||
**范围**:
|
||||
1. 主仓库新建分支 `stage-4`,执行 `git subtree add --prefix=workbench <agent 仓库 GitHub URL> main`(保留全历史)
|
||||
2. 主仓库根新建 `package.json`:
|
||||
- workspaces: `["workbench/server", "workbench/web", "packages/*"]`
|
||||
- scripts:`dev` / `dev:server` / `dev:web` / `typecheck`(从原 agent 根 package.json 上移,workspace 路径改写)
|
||||
- devDependencies:`concurrently`
|
||||
- ❗ **不设 `"type": "module"`**——主仓库 `tests/js/*.test.js` 是 CommonJS(`require` + 双出口),根上声明 ESM 会让它们全炸
|
||||
3. ❗ **检查子包 module 类型**(原 agent 根 package.json 声明了 `"type": "module"`,删除它之前必须确认):workbench/server、workbench/web 各自 package.json 是否自带 `"type": "module"`,**缺则补在子包内**(不上提到根);以 `npm run dev` + `typecheck` 实跑为准
|
||||
4. `workbench/package.json` 删除(其内容已上移);`.mise.toml` / `.nvmrc` 上移到根
|
||||
5. 检查 `.gitignore` 合并(根新增 node_modules 等通用项;workbench 原有忽略项保留)
|
||||
6. ❗ 隐私检查:subtree 进来的内容 grep 本地用户目录绝对路径,有则清理后再 commit
|
||||
|
||||
**验收**:
|
||||
1. 主仓库根 `npm install && npm run dev` 一行拉起工作台前后端,浏览器 5180 正常对话
|
||||
2. 主仓库原有测试照常绿:`bash tests/regression.sh`(或至少 `node --test tests/js/*.test.js` 全过)
|
||||
3. `npm run typecheck` 通过
|
||||
4. `git log --oneline workbench/ | head` 能看到原 agent 仓库历史
|
||||
|
||||
### Step 1:引擎包骨架 + helpers TS 化(M0)
|
||||
|
||||
**范围**:
|
||||
1. 建 `packages/graph-engine`(目录结构见 §4.6),Vite lib 模式配置双产物
|
||||
2. `templates/graph-styles/wash/graph-wash-helpers.js` → `src/model/` 按职责拆成若干 TS 模块(atlas 模型 / 视口数学 / 小地图映射 / 密度策略 / 标签文案),**1:1 翻译,逻辑零改动**
|
||||
3. `tests/js/graph-wash-helpers.test.js` 等迁移为引擎包测试(断言不改)
|
||||
4. `types.ts`:graph-data / pins / diff 类型定义
|
||||
5. ❗ 此 Step 不动 `templates/` 原文件——旧模板继续服役到 Step 6
|
||||
|
||||
**验收**:引擎包 `npm test` 全绿(迁移断言数量 ≥ 原测试);`npm run build` 产出 ESM + IIFE 两份产物。
|
||||
|
||||
### Step 2:工作台图谱视图——静态复现(M1,安全网)
|
||||
|
||||
**范围**:
|
||||
1. `src/render/`:从 graph-wash.js 搬运绘制逻辑,重组为可实例化结构(构造时挂容器、destroy 可清理);先实现**静态渲染**(预计算布局,无模拟)
|
||||
2. `themes/`:从 header.html 抽视觉 token,山水主题完整、墨夜主题第一版(先保证深色工作台下不突兀,精修在 Step 7);❗ 顺手核查 header.html 内联脚本是否使用 d3 / rough(结论记入本文档 D14,决定 Step 6 打包清单)
|
||||
3. workbench/web:侧栏"图谱"入口 + 主区域"对话 ⇄ 图谱"视图切换 + React 薄壳组件
|
||||
4. workbench/server:`GET /api/graph`(只读)+ `POST /api/graph/rebuild`(异步构建 + SSE 通知,行为见 §4.4 首开流程)
|
||||
5. 阅读态:图谱内点节点 → 通过 `onOpenPage` 调工作台右抽屉(不用引擎内置阅读态)
|
||||
|
||||
**验收**:
|
||||
1. 工作台打开真实知识库图谱,与旧版离线 HTML 同屏对照,布局/节点分层/团块/小地图视觉一致(人工对照 + 截图存档)
|
||||
2. 切换知识库 → 图谱跟随切换;无图谱数据的库显示"构建中"再出图
|
||||
3. 浅/深主题切换,图谱跟随山水/墨夜
|
||||
|
||||
### Step 3:活模拟 + 钉扎 + 持久化(M2)
|
||||
|
||||
**范围**:
|
||||
1. `sim/`:d3-force 接入;混合布局(D11:预计算起点 + 低温入睡);拖动低温让位(D8 参数);❗ rough/手绘路径缓存(D14 沸腾对策——若 B 级搬运中存在随机抖动绘制,路径一次生成挂节点,帧只更新 transform)
|
||||
2. 钉扎:松手即钉 + 朱砂图钉角标 + 双击解钉 + 重置布局(toast 撤销)
|
||||
3. 持久化:`GET/PUT /api/graph/layout`;前端松手防抖写入;§4.1 文件格式
|
||||
4. 团块行为:拖动中淡化、定格后重晕染、不追离群点(D10)
|
||||
5. 顺手查证:`init-wiki.sh` 生成的知识库 `.gitignore` 对 `.wiki-cache.json` 的处理方式,`.wiki-graph-layout.json` 与之对齐(倾向**不排除**——钉扎是用户资产,应可随知识库进版本管理;查证结果回填本条)
|
||||
|
||||
**实施定稿**:
|
||||
- 拖动手感参数:`coldStartAlpha=0.08`、`lowHeatAlphaTarget=0.15`、`alphaMin=0.003`、`alphaDecay=0.14`、`velocityDecay=0.58`;斥力 `strength=-34` / `distanceMax=220`;X/Y 回中强度 `0.052`;碰撞 `strength=0.64` / `iterations=2`。拖动时直接邻居参与让位,远处节点冻结,松手后 `alphaTarget=0`。
|
||||
- `.wiki-graph-layout.json` 与 `.wiki-cache.json` 对齐为知识库资产:`init-wiki.sh` 生成的知识库 `.gitignore` 仅忽略 `.wiki-tmp/`,不忽略 layout 文件。
|
||||
|
||||
**验收**:
|
||||
1. 拖节点:邻居让位流畅、远处不动、松手 1–2s 后全图静止(不蠕动)
|
||||
2. 钉住节点 → 刷新页面 / 重启工作台 → 钉位还原;双击解钉 → 文件中该条删除
|
||||
3. 拖 A 进异色团块:A 颜色不变、团块不追
|
||||
4. 重置布局 → toast 撤销可恢复
|
||||
5. 手动损坏 `.wiki-graph-layout.json` → 图谱正常打开(当作无钉)
|
||||
|
||||
### Step 4:选区系统 + 对话联动(M3)
|
||||
|
||||
**范围**:
|
||||
1. `select/`:四式选择(D5)+ 选区结构事实计算(页数/内部链接数/社区数/孤立数)
|
||||
2. 选区面板:结构事实 + 按性质生成的动作按钮(D6 映射表)+ 自由输入 + "发送/新对话中打开"
|
||||
3. 工作台联动:`onAsk` → 切到对话视图,输入框出现选区胶囊;发送时胶囊展开为结构化文本(页面清单 + 链接关系 + 社区归属)走现有 `/api/prompt`
|
||||
4. 反向联动:对话中 agent 引用 wiki 页面 → 若图谱视图打开,`focusNode` 高亮("引用"的判定**复用阶段二既有的 wiki 链接识别**,不另发明检测逻辑)
|
||||
5. 离线 HTML 不传 `onAsk` → 引擎自动隐藏提问动作(capabilities 验证点)
|
||||
|
||||
**实施定稿**:选区在输入区显示胶囊 `@[选区:<标题> · <页数>页]`;发送时展开为结构化文本,包含选区摘要(页数、内部链接数、社区数、孤立数)、页面清单(类型 / 社区 / 路径)、内部链接列表、动作与自由输入。对话历史保留胶囊态,展开内容作为发送 payload,不把几十行结构化文本直接刷屏。
|
||||
|
||||
**验收**:
|
||||
1. 点社区 → 面板显示"N 页 · M 条内部链接" → 点"总结这一簇" → 对话收到选区上下文,agent 输出的总结明确引用选区内页面
|
||||
2. Shift 多选 3 个互不相连节点 → 面板显示"无相互链接",动作为"探索潜在联系",**没有**"总结这一簇"
|
||||
3. 选两个社区 → 动作含"为什么没联系";agent 回答后引导建链 → agent 写回 wikilink → 触发重算(Step 5 完成后回归验证颜色变化闭环)
|
||||
4. 发送默认进当前对话;"新对话中打开"开新线程
|
||||
|
||||
### Step 5:文件监听 + 重算链 + 生长动画(M4)
|
||||
|
||||
**范围**:
|
||||
1. workbench/server:当前 KB 目录监听(Node 原生 `fs.watch` recursive,封装一层适配器;❗ 实测不可靠再换 chokidar,见 §7);防抖 ~5s;批量消化 start 挂起 / done 立即触发
|
||||
2. 重算任务:单飞行 + 排队合并;子进程跑 `build-graph-data.sh`;完成后 diff(引擎包 `diff.ts`)
|
||||
3. `graph_updated` 扩展携带真实 diff(Step 2 的最简版升级);前端 diff 队列(可见消费 / 不可见徽标 + 补播 / 拖动中挂起)
|
||||
4. `anim/`:生长动画(语义锚点发芽、错峰、墨线描边、变色渐变、淡出;≤3s;点击定格;`prefers-reduced-motion`)
|
||||
|
||||
**验收**:
|
||||
1. 批量消化 10 篇(图谱视图关着)→ 完成后侧栏徽标亮 → 打开图谱 → 从旧布局开场播放生长(新节点从邻居处发芽)→ 3s 内定格
|
||||
2. 图谱开着时在 Obsidian 手动新建一篇含 wikilink 的页面 → ~5s 后图谱自动长出新节点
|
||||
3. 批量消化进行中不触发逐篇重算(日志确认挂起);done 后恰好一次重算
|
||||
4. 动画播放中点击画布 → 立即定格;系统开启"减少动态效果"→ 直接定格
|
||||
5. 正在拖动节点时收到 `graph_updated` → 松手后才播
|
||||
|
||||
**实施定稿**:macOS + Node 22 下原生 `fs.watch` recursive 两轮实测可收到嵌套目录创建、嵌套 Markdown 创建 / 修改 / 删除事件;部分修改可能以 `rename` 形式到达,因此实现不依赖事件类型,只要命中非排除路径就触发防抖重算。未引入 chokidar。
|
||||
|
||||
### Step 6:Skill 离线 HTML 切换引擎产物(M5)
|
||||
|
||||
**范围**:
|
||||
1. `build-graph-html.sh` 改造:拼接 `engine.iife.js` + 主题 CSS + graph-data + 启动脚本(创建 engine;capabilities:persistPins → localStorage 适配器【沿用现有 per-wiki 命名空间机制】、onOpenPage 不传 → 用引擎内置阅读态)
|
||||
2. 生成时读 `.wiki-graph-layout.json` 把钉位烤进初始布局
|
||||
3. 打包清单:移除 `d3.min.js`(完整版,运行时从未使用)与旧 `graph-wash*.js`;保留 marked / purify;新增 engine.iife.js
|
||||
4. 旧模板 `templates/graph-styles/wash/` 标记 deprecated(保留一个版本周期再删)
|
||||
5. 主仓库回归测试与东方设计合同测试对新产物跑通(断言按新 DOM 结构最小修订)
|
||||
|
||||
**验收**:
|
||||
1. 真实知识库跑 `build-graph-html.sh` → 双击产物 HTML 离线打开,视觉与工作台一致(山水主题)
|
||||
2. 工作台里钉好的布局,在导出 HTML 的初始视图中生效
|
||||
3. 离线 HTML 内可拖动(localStorage 持久,刷新仍在),**无**提问按钮(capabilities 生效)
|
||||
4. `bash tests/regression.sh` 全绿
|
||||
|
||||
### Step 7:总验收 + 墨夜打磨 + 收尾
|
||||
|
||||
**范围**:墨夜主题精修(黑底/白墨/朱砂的完整视觉走查)、动画节奏与拖动手感参数调优、§6 总验收剧本完整跑一遍、PRODUCT.md 进度回填、(可选)旧 agent 仓库内加一条不对外的开发笔记。
|
||||
|
||||
---
|
||||
|
||||
## §6 总验收标准
|
||||
|
||||
1. **monorepo**:主仓库根一行 `npm run dev` 起工作台;Skill 主线测试全绿;两边互不破坏
|
||||
2. **静态基线**:工作台图谱与旧版离线 HTML 视觉一致(Step 2 截图存档为证)
|
||||
3. **钉扎**:拖动让位流畅、松手即钉、重启还原、Obsidian 旁路修改不破坏钉扎文件
|
||||
4. **选区**:四式选择可用;动作随选区性质变化;"两簇为何没联系 → agent 建链 → 重算后颜色真变"全闭环跑通
|
||||
5. **生长**:批量消化后打开图谱可见补播动画;Obsidian 手改 ~5s 内自动反映;批量期间不抖动重算
|
||||
6. **离线 HTML**:新产物双击可用、钉位生效、无提问按钮、回归测试绿
|
||||
7. **主题**:工作台浅/深切换图谱跟随山水/墨夜,深色模式下无违和
|
||||
|
||||
---
|
||||
|
||||
## §7 风险与 TBD
|
||||
|
||||
| 编号 | 风险/待定 | 对策/何时定 |
|
||||
|---|---|---|
|
||||
| R1 | 根 package.json 的 module 类型破坏主仓库 CommonJS 测试 | 已写死:根不设 type,ESM 声明留在子包(Step 0 ❗) |
|
||||
| R2 | 手绘路径每帧重算导致"沸腾"+性能塌方 | 已写死:路径一次生成缓存,帧只改 transform(Step 3 ❗) |
|
||||
| R3 | `fs.watch` recursive 在 macOS 的可靠性 | 已定稿:Node 22 原生 recursive 实测可用;事件类型不稳定但事件可达,按非排除路径统一触发重算;未引入 chokidar |
|
||||
| R4 | subtree 合并带入本地绝对路径 / 隐私内容 | 已定稿:Step 0 隐私清理通过;后续每 commit 执行 staged 内容本地路径扫描 |
|
||||
| R5 | 选区注入大社区(如 30 页)时上下文过大 | 首版只注入清单+结构(正文 agent 按需 read);实测 token 仍超 → 注入端做清单截断 + 提示 agent 分批读 |
|
||||
| R6 | 力模拟在 500+ 节点的帧率 | 首版接受(个人库几百页内);实测掉帧 → 布局计算挪 Web Worker;canvas 后端为更远期备选 |
|
||||
| R7 | 旧测试断言绑死旧 DOM 结构,Step 6 迁移成本 | 允许按新 DOM 最小修订断言,但"东方设计合同"语义级断言(分层/签条/批注存在性)必须保留 |
|
||||
| R8 | Louvain 重跑社区编号洗牌 → recolored 满屏误报 | 已写死:diff 先按成员重叠率(Jaccard)做新旧社区贪心配对,再判真实变色;无法配对的进 `newCommunities`(§4.3 ❗) |
|
||||
| TBD-4.1 | 拖动手感参数(alphaTarget / 衰减时长 / 让位半径) | 已定稿:参数见 Step 3 实施定稿;主观手感交验收人判断 |
|
||||
| TBD-4.2 | 选区胶囊展开文本的具体模板 + 对话历史显示策略 | 已定稿:模板见 Step 4 实施定稿;历史保留胶囊态,发送 payload 展开结构化文本 |
|
||||
|
||||
---
|
||||
|
||||
## §8 明确不做清单(汇总)
|
||||
|
||||
| 不做 | 原因 |
|
||||
|---|---|
|
||||
| 自由套索圈选 | 空间邻近无语义保证(D5) |
|
||||
| 跨库图谱 | 撕裂会话绑库心智(D7) |
|
||||
| 多套布局方案 / 主题商店 | 维护负债、稀释签名(D4/D9) |
|
||||
| 页面改名钉扎跟踪 | 丢了重钉,成本不值(D9) |
|
||||
| 修饰键临时拖动模式 | 双击解钉已覆盖(D8) |
|
||||
| 图谱指纹作废机制 | 被路径 key 取代(D9) |
|
||||
| 增量图计算 | 全量+diff 简单一个量级(D12) |
|
||||
| 动画速度/开关设置项 | 默认调好是产品责任;系统 reduced-motion 除外(D13) |
|
||||
| 知识库成长历史回放 | 远期传播玩具,不进阶段四(D13) |
|
||||
| SVG/Canvas 双渲染后端 | 规模未到;过早抽象(D14) |
|
||||
| 引擎通用库化对外发布 | 它是器官不是轮子(D14) |
|
||||
| 重写 build-graph-data.sh | Skill 能力优先(ADR-16/D12) |
|
||||
| Tauri 打包 | 推迟到工作台有真实外部用户后 |
|
||||
|
||||
---
|
||||
|
||||
## 附:与 PRODUCT.md 的对应
|
||||
|
||||
- 本文档对应 PRODUCT.md §4"阶段四"与 §7 ADR-20(monorepo 合并)、ADR-21(图谱引擎与活地图)。
|
||||
- 实施期间的决策变更:先改本文档相应小节并在 changelog 留痕,再改代码。
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-06-12 v3(实施回填):阶段四落地后补齐定稿事实
|
||||
- D12 监听排除清单补自家生成物三项:`.wiki-graph-layout.json`、`wiki/graph-data.json`、`wiki/knowledge-graph.html`
|
||||
- D14 回填 header.html 内联脚本 d3/rough 审计结论;Step 6 已移除 d3/rough 与旧 graph-wash 打包项
|
||||
- Step 3 回填拖动手感参数与 `.wiki-graph-layout.json` 的 gitignore 对齐结论;Step 4 回填选区胶囊/展开 payload 模板;Step 5 回填 fs.watch 实测结论
|
||||
- §7 将 R3/R4 与 TBD-4.1/TBD-4.2 更新为已定稿状态
|
||||
- 2026-06-12 v2(自审修订):修复 5 个实质缺口 + 收紧 2 处事实表述
|
||||
- D11 补活会话坐标策略:既有未钉节点不随重算跳位,新节点活模拟安置,预计算坐标仅用于冷启动(防布局漂移)
|
||||
- §4.3 补 diff 社区对齐(Jaccard 配对,防 Louvain 编号洗牌导致满屏误报变色)+ `newCommunities` 字段;新增 R8
|
||||
- §4.4 统一 GET 只读 / POST 异步构建;`graph_updated` 分两步引入(Step 2 最简版、Step 5 带 diff)
|
||||
- D12 补监听排除清单(`.wiki-tmp/` 等,防 Skill 消化刷爆监听)+ 切库监听生命周期
|
||||
- Step 0 补前置(stage-4-design 先合入 agent main,否则设计文档不随 subtree 进主仓库)+ 子包 module 类型检查列为明确动作
|
||||
- D14 / Step 2 / Step 6 收紧"0 处 d3/rough"表述:已验证两个运行时 JS;header.html 内联待 Step 2 核实,d3.min.js 移除以核实为前提
|
||||
- Step 3 补 init `.gitignore` 对 layout 文件的对齐查证;Step 4 注明引用识别复用阶段二既有逻辑;TBD-4.2 扩展对话历史胶囊显示策略
|
||||
- 2026-06-12 v1:首版。由四轮设计对话(战略 / 选区 / 钉扎 / 生长 / 引擎抽取)沉淀,含 D1–D14 决策、8 Step、API 契约、验收剧本、风险清单。
|
||||
@@ -0,0 +1,195 @@
|
||||
# 阶段 4.5 设计文档:图谱可用性收尾
|
||||
|
||||
> 状态:**设计完成,待作者审定后实施**
|
||||
> 日期:2026-06-13
|
||||
> 来源:阶段四交付后作者实测反馈 + 验收发现(离线 HTML 功能减配裁决),经 2026-06-13 三轮设计讨论沉淀。
|
||||
> 与 [stage-4-design.md](stage-4-design.md) 的关系:本文档修订其 D6(选区动作映射)并补全其遗漏(画布导航);冲突处以本文档为准。
|
||||
|
||||
---
|
||||
|
||||
## §0 背景:五个实测问题 + 一个验收裁决
|
||||
|
||||
阶段四验收通过后,作者实际使用暴露以下问题(按根因分类):
|
||||
|
||||
| # | 问题 | 根因 |
|
||||
|---|---|---|
|
||||
| 1 | 画布不能缩放、不能平移 | **plan 盲区**:视口数学(`zoomAtlasViewport` 等)Phase 1 已搬进引擎,但渲染器从未接交互——有发动机没装油门 |
|
||||
| 2 | 点击节点弹出选区悬浮窗,看不懂、有废话信息(单节点显示"1页/0链接/1社区/0孤立")、动作错位(单节点出现"探索潜在联系")、悬浮窗还遮住所选节点 | **设计盲区**:stage-4 D6 动作映射表没有"单节点"行,单节点内部链接恒为 0 被误判进"无链接多选"剧本;且点击语义被"提问"独占,**阅读路径消失** |
|
||||
| 3 | Shift 多选按了没反应 | 代码存在(`static-renderer.ts:581` 监听 shiftKey),疑似失效待排查;且**零可发现性**——界面无任何提示 |
|
||||
| 4 | 无图谱内搜索、社区颜色无图例、无聚焦入口 | 阶段四验收裁决项:离线 HTML 功能减配,作者选定"补核心、缓其余" |
|
||||
| 5 | 节点默认卡片三行(类型文字/标题/权重数字)过胖 | 密度策略已接线(大库自动降级),但"卡片档"本身信息冗余:类型已由左边条颜色编码,文字行重复;权重数字对用户无含义 |
|
||||
|
||||
**验收裁决**(作者拍板):搜索 + 社区聚焦补回;**学习系统三件套(学习队列、"从这里开始"学习路径、札记笔记)不做**,等真实使用后在工作台语境重新设计,不照搬旧版。
|
||||
|
||||
---
|
||||
|
||||
## §1 核心决策(D4.5-1 ~ D4.5-9)
|
||||
|
||||
### D4.5-1 画布导航(P0,最高优先级)——丝滑规格
|
||||
|
||||
> 作者用旧版(发布版 Skill)生成真实库图谱实测"缩放不够丝滑"。诊断:旧版方向正确(指数缩放/指针锚点/单层 transform 都有),病根在四个工程细节——无帧合并(触控板每秒上百事件逐个触发完整渲染)、高频路径混入重活(SVG 边层属性重算+小地图重绘)、不区分鼠标/触控板 delta、内容层未提升 GPU 合成。本节规格以"丝滑"为验收目标,不止"有"。
|
||||
|
||||
**交互行为**:
|
||||
- 滚轮缩放,**以鼠标指针位置为缩放中心**;ctrl+wheel(浏览器捏合约定)同路支持
|
||||
- 空白处拖拽 = 平移画布(节点上拖拽 = 移动节点,按事件目标区分,互不冲突)
|
||||
- 双击空白处 = 回到全图(fit);小地图与视口实时联动(接视口矩形)
|
||||
- 缩放范围夹紧:fit 的 0.5× ~ 4×
|
||||
|
||||
**丝滑六条(实现规格,❗ 逐条进验收)**:
|
||||
1. **单层合成变换**:节点/边/团块挂同一内容层,缩放平移只改该层一条 CSS transform(translate+scale),节点 DOM 与样式零触碰;层声明 `will-change: transform` 提升 GPU 合成层——帧成本与节点数无关
|
||||
2. **rAF 帧合并**:wheel/pointermove 只更新目标视口数值,写 DOM 仅发生在 requestAnimationFrame 回调,每帧至多一次
|
||||
3. **设备归一化**:按 `deltaMode` 区分鼠标(行)与触控板(像素),各自缩放系数;指数缩放 `s' = s·exp(-Δ·k)` 保持手感线性
|
||||
4. **跟手零动画,跳转才有动画**:滚轮/拖拽逐帧直接应用,**禁止加过渡**(wheel 上加缓动 = 橡皮筋延迟感的来源);双击回全图 / 图例飞行 / 搜索命中飞行用 ~200ms ease-out
|
||||
5. **语义缩放**:密度档位与缩放联动——节点屏幕投影小于阈值自动降为紧凑标签/圆点,放大长回卡片,档位切换 ~150ms 交叉淡入;现有密度策略从"按节点总数定档"升级为"按屏幕有效密度定档"
|
||||
6. **高频期降载**:缩放/平移进行中暂停团块重晕染与小地图精绘(沿用"拖动中团块淡化"惯例),交互停止 ~150ms 后恢复精绘
|
||||
|
||||
**其余约束**:
|
||||
- **引擎层实现**,工作台与离线 HTML 同享;作者手中旧版生成的 HTML 不修,4.5 完成后重新生成即得新版
|
||||
- ❗ 与钉扎无冲突:钉扎存**模型坐标**,视口只是观察矩阵,两者正交——实现时不得把视口偏移混进节点坐标
|
||||
- 不做:惯性滚动(基础做对优先)
|
||||
|
||||
### D4.5-2 点击语义重构:"点击即阅读,选区即升级"
|
||||
|
||||
**选区悬浮窗取消。** 右抽屉统一承载,一个容器两种状态:
|
||||
|
||||
```
|
||||
单击节点 → 右抽屉【阅读态】(页面正文),节点在图上高亮,无浮窗
|
||||
Shift+点第二节点 → 抽屉切【选区态】(结构事实 + 动作)
|
||||
点社区团块/图例 → 直接进入【选区态】(整簇)
|
||||
阅读态点"+邻居" → 升级为【选区态】(该节点+一跳邻居)
|
||||
Esc / 关闭抽屉 → 清空选区,回到纯画布
|
||||
```
|
||||
|
||||
理由:用户点一个节点,心里的问题是"这是什么"(阅读),不是"你想执行什么操作"(提问)。提问是阅读后的升级动作,不是点击的默认答案。抽屉不遮画布(解决悬浮窗盖住所选节点)、复用工作台既有抽屉基建与用户心智(引用预览/产物预览同款容器)、宽度可拖与移动端全屏适配都是现成的。
|
||||
|
||||
### D4.5-3 抽屉【阅读态】蓝图(瘦身定稿)
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Pi Agent SDK │ ← 标题
|
||||
│ 来源 · 2026-06-10 · 查看原文 │ ← 元信息一行:类型中文 · 更新日期 ·(仅 source 页)原文链接
|
||||
│ [ 问这一页 ] [ + 邻居 ] │ ← 动作条,仅此两个;孤岛节点额外显示 [ 帮它链入知识库 ]
|
||||
├──────────────────────────────┤
|
||||
│ 正文 markdown │ ← wikilink 可点:抽屉内换页 + 图谱 focusNode 联动高亮
|
||||
│ (置信度标注随正文自然呈现) │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
**去除清单**(对照旧版札记栏,逐项有理由):
|
||||
|
||||
| 旧版元素 | 处置 | 理由 |
|
||||
|---|---|---|
|
||||
| 学习队列 / "从这里开始" / 札记笔记 | 去(作者拍板) | 学习系统三件套整体不做 |
|
||||
| "摘要"区块 | **迁移至 hover 预览卡**(见 D4.5-7) | 摘要的价值在"读前决策"(图上扫视时判断要不要点开),不在"阅读中"(正文第一段就在眼前)。旧版的别扭是住错了房间,不是摘要无价值——抽屉内不设摘要区,摘要搬到悬停预览 |
|
||||
| 置信度独立列表 | 去区块 | 置信度标注本就写在页面正文里,渲染时自然呈现;单独列表是又一个派生重复区。**不是砍特色,是去重复** |
|
||||
| 相邻节点列表 | 去 | 关系归图谱管:正文 wikilink 可点 + "+邻居"一键扩选区 + 左边整张图就是关系视图 |
|
||||
| "查看来源"大按钮 | 收纳 | 溯源保留(数据可信的根基),收成元信息行小链接 |
|
||||
| `kind · sourcePath` 技术化 meta | 改写 | 类型改中文(实体/主题/来源),完整文件路径藏进"查看原文",补更新日期 |
|
||||
|
||||
设计原则一句话:**抽屉负责内容,图谱负责关系,凡从正文派生的信息不重复展示。**
|
||||
|
||||
- 不做抽屉内浏览历史栈(wikilink 点击直接换内容,避免抽屉变浏览器)。
|
||||
- 离线 HTML 内置 reader 同步微调 meta 行(中文类型 + 日期),保持两端一致;离线 reader 其余结构已是瘦身版,不动。
|
||||
|
||||
### D4.5-4 选区态修订 + 动作映射表完整版(修订 stage-4 D6)
|
||||
|
||||
| 选区类型 | 统计卡显示 | 动作 |
|
||||
|---|---|---|
|
||||
| **单节点(新增行)** | **不显示统计卡** | 总结这一页 / 它和谁有关 / 在对话中引用 |
|
||||
| 单节点且为孤岛 | 不显示 | 上述 + 帮它链入知识库 |
|
||||
| 单社区 | N 页 · M 条内链 | 总结这一簇 / 找知识缺口 / 生成主题页 |
|
||||
| 两社区 | 两簇间 K 条链接 | 为什么(没)联系 / 找潜在桥梁 / 对比这两块 |
|
||||
| ≥2 节点多选,互有内链 | N 页 · M 条内链 | 总结这一组 / 探索它们的关系 |
|
||||
| ≥2 节点多选,零内链 | N 页 · 无相互链接 | 探索潜在联系 / 对比异同 |
|
||||
|
||||
修订要点:
|
||||
|
||||
1. ❗ **修复 unlinked 误触发**:单节点选区不得落入"零内链多选"剧本(这就是"探索潜在联系"出现在单节点上的 bug 根源)
|
||||
2. 统计卡仅在 ≥2 节点时显示;"孤立"计数仅在 >0 时出现(不显示恒零项)
|
||||
3. 自由输入框降权:占位文案改"补充说明(可选)",视觉弱化,动作按钮是主路径
|
||||
4. **可发现性**:选区态首行常驻浅字提示 "Shift+点击 增删节点"
|
||||
5. Shift 失效排查:实施第一步先在真实浏览器复现作者"按了没反应"的场景,修复后补回归断言(具体原因实施时查明并记录)
|
||||
|
||||
### D4.5-5 图谱搜索(两端)
|
||||
|
||||
- `Cmd/Ctrl + F`(图谱视图聚焦时)浮出搜索框;输入即时:命中节点高亮、其余淡化;`Enter` 视口飞向下一个命中(依赖 D4.5-1 先行);`Esc` 退出恢复
|
||||
- 复用引擎既有 helper(`buildSearchIndex` / `applySearchToNodeIds` / `buildSearchHaystack`——6.2 的测试至今保护着它们)
|
||||
- ❗ 测试反转说明:`tests/graph-html-search.regression-1.sh` 在阶段四被改写为断言"旧搜索 UI 不存在",本阶段需再次改写为断言**新搜索 UI 存在**——这是预期内的轮回,不是反复
|
||||
|
||||
### D4.5-6 社区聚焦列表(兼图例,两端)
|
||||
|
||||
- 画布左上角小图例:每社区一行 = 色块 + 社区名 + 页数
|
||||
- 一物三用:解释团块颜色含义(当前用户全靠猜)/ hover 高亮整簇 / 点击 = 选中该社区(接 D4.5-4 选区态)+ 视口飞至该簇
|
||||
- 折叠为小图标可收起,状态存本机(浏览状态不入库文件,沿用 stage-4 D9 原则)
|
||||
|
||||
### D4.5-7 节点默认态瘦身 + hover 预览卡(配套设计)
|
||||
|
||||
- 默认态:**一行标题 + 类型色条**(左边条颜色已编码类型——ENTITY 红、SOURCE 绿等——文字行直接删)
|
||||
- 权重数字(`priority/weight`)从默认态移除,hover/选中展开完整卡片时显示
|
||||
- **hover 预览卡**(瘦身的另一半:瘦下去的信息从悬停里长回来):悬停节点浮出小卡——标题 + 类型中文 + **摘要两三行**。摘要来源:❗ graph-data 节点**无 summary 字段**(已查证,字段仅 content/label/type/community/source_path),从 `content` 现场提取首个有效正文段落截断,复用引擎已有正文清洗函数(`stripAtlasMarkdown`,A 级搬运已在 model 模块);content 为空则只显示标题+类型。预览卡是只读浮层,不可交互、跟随移开即逝、有 ~300ms 延迟防误触
|
||||
- hover/选中态(节点本体):展开为现有完整卡片
|
||||
- 密度策略保持不动(大库自动降级到紧凑标签/圆点的逻辑已在),本条只是把"卡片档"瘦一号;**点状/紧凑档的节点同样有 hover 预览卡**(大库下预览卡价值更高)
|
||||
- 验收用 `graph-interactive-dense` fixture 确认密度降级路径不受影响
|
||||
- 两端同享(离线 HTML 白拿预览卡);AI 生成真摘要(消化时写入页面元数据、管线优先读取)是内容质量升级,**本阶段不做**,记入 D4.5-9
|
||||
|
||||
### D4.5-8 工作台抽屉阅读态的数据来源
|
||||
|
||||
复用阶段二引用预览的页面读取 API(`/api/refs` 体系),不新增后端端点。GraphPanel 因 d5a83f8 后体量偏大,允许拆分子组件文件(GraphReader / GraphSelection),属于本阶段直接产生的合理拆分,不算顺手重构。
|
||||
|
||||
### D4.5-9 明确不做
|
||||
|
||||
| 不做 | 原因 |
|
||||
|---|---|
|
||||
| 学习队列 / 学习路径 / 札记笔记 | 作者拍板:等真实使用后按工作台语境重新设计,不照搬旧版 |
|
||||
| 抽屉内浏览历史栈(前进/后退) | 抽屉不是浏览器;wikilink 直接换页够用 |
|
||||
| 缩放惯性 / 动画曲线调优 | 先把基础导航做对 |
|
||||
| AI 生成真摘要(消化时写入页面元数据) | 内容管线改动,不混进 UI 收尾;hover 预览卡先用正文首段,管线升级后自动变好 |
|
||||
| 移动端触控全套手势 | 桌面优先;wheel 事件天然覆盖触控板,触屏完整支持留 Tauri 阶段 |
|
||||
|
||||
---
|
||||
|
||||
## §2 实施面
|
||||
|
||||
```
|
||||
packages/graph-engine/
|
||||
├── render/ 视口交互接线(wheel/drag/dblclick + 小地图联动)、节点默认态瘦身、
|
||||
│ 搜索 UI、社区图例、离线 reader meta 微调
|
||||
├── select/ 动作映射表修订(单节点行 + unlinked 修复 + 统计卡条件)
|
||||
└── index.ts 视口控制 API 暴露(fitView / focusNode 带飞行)
|
||||
|
||||
workbench/web/
|
||||
├── GraphPanel 拆分:GraphReader(阅读态)+ GraphSelection(选区态),悬浮窗删除
|
||||
└── 右抽屉接线:阅读态复用引用预览的 markdown 渲染与页面读取
|
||||
|
||||
tests/
|
||||
├── 引擎单测:映射表六行剧本、视口数学接线、瘦身 DOM、搜索高亮
|
||||
└── 回归:graph-html-search 反转为"新搜索 UI 存在";图例/导航钩子断言
|
||||
```
|
||||
|
||||
后端:**零改动**。离线 HTML:引擎升级自动获得导航/搜索/图例(两端红利)。
|
||||
|
||||
## §3 验收剧本(给实施 plan 引用)
|
||||
|
||||
1. **导航**:滚轮以指针为中心缩放;空白拖拽平移;双击空白回全图(带缓动);小地图框联动;**离线 HTML 双击打开后同样全部可用**。**丝滑判据**:①缩放平移期间 DOM 断言节点元素的内联样式不变化(只有内容层 transform 变);②对 dense fixture(200+ 节点)连续滚轮 3 秒,帧率不低于 50fps(Playwright trace 或性能面板取证,环境不支持则标 manual);③触控板与鼠标滚轮各自体感正常(manual);④zoom out 节点自动降档、zoom in 长回卡片,切换无闪跳
|
||||
2. **点击**:单击节点 → 右抽屉阅读态(标题/元信息/双动作/正文),无悬浮窗;抽屉内**不存在**队列/摘要/置信度列表/邻居区块;正文 wikilink 点击换页且图上高亮跟随
|
||||
3. **选区**:Shift+点第二节点 → 抽屉切选区态且首行有 Shift 提示;单节点无统计卡、动作为单节点剧本、**"探索潜在联系"不出现**;点社区 → 统计与动作正确;孤岛单节点出现"帮它链入知识库"
|
||||
4. **搜索**:Cmd+F → 输入 → 命中高亮其余淡化 → Enter 飞向命中 → Esc 恢复;离线 HTML 同样可用
|
||||
5. **图例**:左上角社区列表与团块颜色一致;点击 → 选中整簇 + 视口飞至
|
||||
6. **节点**:默认一行 + 色条;hover 节点本体展开完整卡,同时浮出预览卡(标题/类型中文/正文首段摘要 2-3 行);content 为空的节点预览卡无摘要不报错;dense fixture 下密度降级正常且点状档同样有预览卡
|
||||
7. **主题**:以上全部在山水 / 墨夜两主题下正常
|
||||
8. **自动化**:主仓库 JS 测试 / regression / typecheck / 引擎测试 / 双产物构建全绿
|
||||
|
||||
## §4 风险
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 视口变换混进节点坐标,破坏钉扎 | D4.5-1 已写死"模型坐标与观察矩阵正交";引擎单测断言:缩放平移后钉位模型坐标不变 |
|
||||
| Shift 失效根因未知 | 实施第一步真实浏览器复现,根因记录进 progress decision_log |
|
||||
| graph-html-search 测试二次反转引起困惑 | D4.5-5 已注明轮回原因,commit message 同步说明 |
|
||||
| GraphPanel 拆分引入回归 | 拆分是纯移动 + 接线,现有 web 测试(selection prompt / api / wiki-link)必须保持绿 |
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-06-13 v3(导航丝滑规格):作者用旧版生成真实库图谱实测"缩放不丝滑"——D4.5-1 从"有缩放"升级为"丝滑规格":诊断旧版四病根(无 rAF 帧合并 / 高频路径混重活 / 设备 delta 不区分 / 无 GPU 合成层),定丝滑六条(单层合成变换、帧合并、设备归一化、跟手零动画+跳转缓动、语义缩放、高频降载),验收剧本第 1 条补丝滑判据(含 dense fixture 50fps 基准)。
|
||||
- 2026-06-13 v2(摘要处置修订):作者反馈摘要应保留——修正 v1 把"摘要"与"摘要区"混为一谈的判断。摘要的价值在读前决策(图上扫视),不在阅读中(正文在眼前):抽屉内仍无摘要区,摘要迁移至 **hover 预览卡**(D4.5-7,与节点瘦身配套:瘦下去的信息从悬停长回来)。已查证 graph-data 无 summary 字段,预览卡从 content 现场提取首段(复用 stripAtlasMarkdown),零数据层改动;AI 真摘要管线列入明确不做(留内容质量升级阶段)。验收剧本第 6 条同步。
|
||||
- 2026-06-13 v1:首版。来源:作者实测五问题 + 阶段四验收裁决;含 D4.5-1~9、动作映射表完整版(修订 stage-4 D6)、抽屉瘦身去留表、实施面与验收剧本。
|
||||
@@ -0,0 +1,165 @@
|
||||
# 可拖动预览区与侧栏折叠设计
|
||||
|
||||
状态:已实现并验证
|
||||
|
||||
创建日期:2026-05-28
|
||||
|
||||
实现日期:2026-05-28
|
||||
|
||||
## 背景
|
||||
|
||||
当前界面是左侧知识库导航、中间对话、右侧预览的三段式布局。左侧栏固定 270px,右侧预览打开后固定 420px。这个设计稳定,但在直接预览 HTML/PDF/页面产物时,右侧区域偏窄,用户常常需要全屏才能看清内容;全屏又会完全遮住对话上下文。
|
||||
|
||||
本设计补强现有产品方向:对话仍是主屏,右侧预览仍是辅助面板,但允许用户在“边聊边看”和“重点看预览”之间快速调整空间。
|
||||
|
||||
## 目标
|
||||
|
||||
- 右侧预览区可以通过鼠标拖动左边缘变宽或变窄。
|
||||
- 左侧知识库栏可以折叠成窄图标栏,释放更多横向空间。
|
||||
- 窄图标栏的每个按钮都有悬停文字提示。
|
||||
- 用户调整后的侧栏状态和预览宽度会被记住。
|
||||
- 保留现有全屏预览按钮,作为只看产物的极端模式。
|
||||
|
||||
## 不做什么
|
||||
|
||||
- 不把三栏全部做成自由拖动。左侧栏是导航区,折叠已经能解决占空间问题。
|
||||
- 不新增 npm 依赖。
|
||||
- 不改变知识库、对话、产物的数据结构。
|
||||
- 不改变右侧预览的内容渲染方式。
|
||||
|
||||
## 方案选择
|
||||
|
||||
采用“右侧预览可拖动 + 左侧栏折叠为窄图标栏”。
|
||||
|
||||
对比过三个方向:
|
||||
|
||||
- 只让右侧预览可拖动:最稳,改动集中,但左侧仍占固定空间。
|
||||
- 三栏都可拖动:自由度最高,但容易拖乱界面,状态和边界复杂度更高。
|
||||
- 预设宽度 + 全屏:简单,但不如拖动自然。
|
||||
|
||||
最终方案在第一个方向上补上左侧栏折叠,既保留清晰心智,又能明显释放预览空间。
|
||||
|
||||
## 交互设计
|
||||
|
||||
### 左侧栏
|
||||
|
||||
完整侧栏保持当前结构:顶部品牌和操作按钮,下面是知识库与对话列表,底部是新建和添加入口。
|
||||
|
||||
新增一个“切换侧栏”按钮,放在侧栏顶部,与刷新、设置同级。点击后侧栏在两种状态间切换:
|
||||
|
||||
- 完整侧栏:显示知识库、对话、按钮文字。
|
||||
- 窄图标栏:只显示核心图标,不显示长文字。
|
||||
|
||||
窄图标栏至少包含:
|
||||
|
||||
- 展开侧栏
|
||||
- 当前知识库入口
|
||||
- 新建知识库
|
||||
- 添加现有库
|
||||
- 设置
|
||||
|
||||
所有图标按钮都需要悬停提示,例如“展开侧栏”“当前知识库”“新建知识库”“添加现有库”“设置”。
|
||||
|
||||
折叠状态不应该让用户迷路:窄栏始终保留展开按钮,且当前知识库入口要能表达“当前仍在某个库里”。
|
||||
|
||||
### 右侧预览
|
||||
|
||||
右侧预览打开时,在它的左边缘显示一条细拖动把手。
|
||||
|
||||
交互规则:
|
||||
|
||||
- 按住把手向左拖,预览区变宽。
|
||||
- 按住把手向右拖,预览区变窄。
|
||||
- 双击把手,恢复默认宽度。
|
||||
- 关闭预览区后释放空间。
|
||||
- 再次打开预览区时使用上次宽度。
|
||||
- 全屏按钮保留,进入全屏后不显示拖动把手。
|
||||
|
||||
### 宽度边界
|
||||
|
||||
右侧预览宽度需要限制在合理范围内:
|
||||
|
||||
- 默认宽度:420px。
|
||||
- 最小宽度:360px,保证预览和标签仍可用。
|
||||
- 最大宽度:视口宽度的 70%,但必须给中间对话保留至少 420px。
|
||||
|
||||
当窗口太窄时,优先保证对话区可用;移动端沿用现有逻辑,右侧预览占满屏幕,不启用拖动。
|
||||
|
||||
### 状态记忆
|
||||
|
||||
使用本地浏览器存储记住:
|
||||
|
||||
- 侧栏是否折叠。
|
||||
- 右侧预览宽度。
|
||||
|
||||
刷新页面后恢复上次状态。异常值需要自动回到默认值,避免用户因为窗口变化进入不可用布局。
|
||||
|
||||
## 页面结构
|
||||
|
||||
整体仍是三段式:
|
||||
|
||||
- 左:导航区,完整侧栏或窄图标栏。
|
||||
- 中:对话区,占用剩余空间。
|
||||
- 右:预览区,打开时显示,宽度可调整。
|
||||
|
||||
中间对话区不设置固定宽度,而是随左右两侧变化自动伸缩。这样右侧拉宽时,对话区自然变窄;右侧收窄或关闭时,对话区自然变宽。
|
||||
|
||||
## 实现边界
|
||||
|
||||
预计改动集中在:
|
||||
|
||||
- `web/src/App.tsx`:保存侧栏状态、预览宽度,并传给组件。
|
||||
- `web/src/components/Sidebar.tsx`:支持完整侧栏和窄图标栏两种形态。
|
||||
- `web/src/components/RightDrawer.tsx`:增加拖动把手和宽度回调。
|
||||
- `web/src/index.css`:增加折叠侧栏、拖动把手、宽度状态的样式。
|
||||
|
||||
不需要改后端。
|
||||
|
||||
## 可访问性与细节
|
||||
|
||||
- 侧栏切换按钮需要有明确的 `aria-label`。
|
||||
- 拖动把手需要可聚焦,并提供键盘兜底:左右方向键调整宽度,Home 或双击恢复默认宽度。
|
||||
- 拖动中避免选中文本。
|
||||
- 拖动中给把手明确的 hover/active 状态。
|
||||
- 窄图标栏的提示文案使用现有 tooltip 样式。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 点击侧栏切换按钮,侧栏能在完整侧栏和窄图标栏之间切换。
|
||||
2. 窄图标栏按钮悬停时都有清楚提示。
|
||||
3. 打开右侧预览后,拖动左边缘能改变宽度。
|
||||
4. 双击拖动把手能恢复默认预览宽度。
|
||||
5. 预览区拉宽时,中间对话不会被挤到无法使用。
|
||||
6. 刷新页面后,侧栏折叠状态和预览宽度仍保持。
|
||||
7. 移动端或窄屏下不启用拖动,预览继续按现有方式占满屏幕。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 运行前端检查。
|
||||
- 启动应用。
|
||||
- 打开已有 HTML 产物预览。
|
||||
- 手动切换侧栏折叠状态。
|
||||
- 手动拖动右侧预览宽度并双击恢复。
|
||||
- 刷新页面确认状态保持。
|
||||
- 缩小窗口确认不会出现不可用布局。
|
||||
|
||||
## 实施结果
|
||||
|
||||
已按本文档方案实现:
|
||||
|
||||
- 左侧栏支持完整侧栏与窄图标栏切换。
|
||||
- 窄图标栏保留展开、当前知识库、刷新、新建、添加、设置入口。
|
||||
- 窄图标栏按钮均有悬停提示。
|
||||
- 右侧预览支持拖动左边缘调整宽度。
|
||||
- 双击拖动边缘恢复默认宽度。
|
||||
- 侧栏折叠状态和预览宽度会在本地保留。
|
||||
- 移动端沿用全屏预览,不显示拖动边缘。
|
||||
|
||||
验证结果:
|
||||
|
||||
- `npm run typecheck` 通过。
|
||||
- 受控浏览器验证默认预览宽度为 420px。
|
||||
- 受控浏览器验证拖动后预览区可变宽,并保持对话区可用。
|
||||
- 受控浏览器验证双击拖动边缘可恢复默认宽度。
|
||||
- 受控浏览器验证侧栏可折叠为 52px 窄图标栏,刷新后仍保持。
|
||||
- 受控浏览器验证移动端预览占满屏幕,拖动边缘隐藏。
|
||||
@@ -0,0 +1,50 @@
|
||||
# 设置面板滚动修复
|
||||
|
||||
状态:已实现并验证
|
||||
|
||||
创建日期:2026-05-28
|
||||
|
||||
实现日期:2026-05-28
|
||||
|
||||
## 背景
|
||||
|
||||
设置面板已经承载认证状态、API key、环境变量、模型角色和 Skill 加载。内容高度超过部分屏幕时,弹窗本身没有限制高度,内容区也没有独立滚动,导致底部设置不可见。
|
||||
|
||||
## 目标
|
||||
|
||||
- 设置面板在普通桌面窗口和较矮窗口里都能看完整内容。
|
||||
- 标题和关闭按钮始终保留在弹窗顶部。
|
||||
- 设置项在弹窗内部滚动,不影响背后的工作台布局。
|
||||
- 不改变设置项的数据逻辑,不新增依赖。
|
||||
|
||||
## 方案
|
||||
|
||||
采用“弹窗外框限高 + 内容区内部滚动”:
|
||||
|
||||
- 弹窗最高不超过屏幕高度,并保留上下边距。
|
||||
- 弹窗分成顶部标题区和内容区。
|
||||
- 内容区独立滚动,确保底部 Skill 加载设置可达。
|
||||
|
||||
暂不改成独立设置页,也暂不拆分设置标签。当前设置仍是辅助操作,用弹窗更符合工作台心智;等设置项继续增多后,再考虑拆成“认证 / 模型 / Skill”等页签。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 打开设置后,底部 Skill 加载区可见或可通过滚动到达。
|
||||
2. 设置内容滚动时,标题和关闭按钮仍在顶部。
|
||||
3. 不影响 API key 输入、模型角色选择和 Skill 开关。
|
||||
4. 前端检查和构建通过。
|
||||
|
||||
## 实施结果
|
||||
|
||||
已按本文方案实现:
|
||||
|
||||
- 设置弹窗增加屏幕高度上限。
|
||||
- 弹窗内部采用标题区 + 可滚动内容区。
|
||||
- 内容区增加最小高度保护,避免矮窗口下滚动区域被撑开。
|
||||
|
||||
验证结果:
|
||||
|
||||
- `npm run typecheck` 通过。
|
||||
- `npm run build --workspace=@llm-wiki-agent/web` 通过。
|
||||
- `npm run lint --workspace=@llm-wiki-agent/web` 通过。
|
||||
- Chrome 中打开设置面板,确认底部 Skill 加载区可见,窗口不再遮挡底部内容。
|
||||
Reference in New Issue
Block a user