笔记加入对话上下文(可被引用)设计
September 10, 2026 · View on GitHub
本文档设计「把笔记作为可引用的上下文注入 dsh 对话」——已实现(0.4.0 起)。 功能总览见 features.md,架构见 architecture.md。
0. 实现状态
最后更新 2026-09-11。✅ 已实现(0.4.0 起,0.7.0 补充 chip logo 与路径 fallback,0.1.5-rc 线补 chip 点击预览与相对路径保证);⏳ 少量待实测。
已确认(dsh 源码调研)
- ✅
@引用管线(ui-input-trigger:registerSource / candidates / onPick / ReferenceCodec) - ✅ fs 沙箱只拦写入、读放行 → 跨工作区读取无授权障碍
- ✅
tool-fs核心挂载,read工具默认对 agent 可用 - ✅ chip 的 DOM/尺寸为 dsh 硬编码(4em U+FFFC 单元格,插件不能改结构/尺寸);候选菜单 不可定制渲染,工作区信息走候选 label 文本(见 §3.3.1)
- ✅ 纯文本
@笔记名仅装饰(textRef 高亮),不产生 chip、不注入上下文;引用必须走菜单 chip - ✅ serialize 失败阻断发送(源码 "serialize failure blocks the send")——引用失效需用户移除
- ✅ textRef 装饰匹配
[\w-]+(lexicon 纯文本高亮仅对 ASCII 名称生效;不影响引用 语义与跨工作区触发——中文工作区名已支持,见 §3.2) - ✅ 候选 icon 实测结论:
MenuView把InputTriggerCandidate.icon作为纯文本渲染在 16px 槽位({item.icon}字符串子节点)——URL/SVG 无法渲染成图片,会显示字面 URL 文本。 因此候选 icon 用 📝 emoji 替代插件 SVG(见 §3.2)。
已实现(0.4.0 + 后续)
- ✅ 对话输入
@引用笔记(候选菜单 / chip / 跨工作区工作区行自动补全) - ✅ 序列化格式为标准 markdown 链接
[标题](路径)(见 §3.3) - ✅ host 端内容注入:
agent/pre-step把被引用笔记的内容折叠进模型请求(不依赖模型 自觉read),注入内容带「引用约定」一行(见 §3.7) - ✅ 纯文本
@笔记名装饰(lexicon热快照,仅 ASCII 标题生效) - ✅ chip 显示优化:标签前置截断(4em 单元,>4 字符 → 前 4 + …,显示开头而非中间一截,§3.3.1)
- ✅ chip 前置插件 logo(0.7.0):保留
appearance='notes'作用域 + 注入 scoped 样式绘制图标 - ✅ 引用路径 fallback(0.7.0):工作区改名后,serialize 用标题生成路径兜底, 引用不再指向不存在位置
- ✅ 候选行插件 logo(0.1.5-rc 线):
icon字段组件化,候选行行首为插件 logo - ✅ chip 点击预览(0.1.5-rc 线,dsh
openReference手势):点击已插入的 chip 在 右侧栏笔记查看器打开该笔记(ref 转文件地址;无 Sidebar 时点击无副作用) - ✅ 引用只含相对路径(隐私保证):pick 不再产生绝对路径 ref;serialize 对遗留 绝对 ref 改写为「工作区名/.dsh-notes/名」——引用行不会把本机目录带进消息/笔记/git
- 🚧 知识库式自动检索(超出 dsh 原生能力,需自定义,见 §4)
待实测(真实会话)
- ⏳ 注入生效验证:
@选笔记(含跨工作区)→ 发送 → 模型不依赖 read 也能引用内容 (已实测 ✅:模型正确回答;待测跨工作区注入) - ⏳ 引用失效路径:删除笔记后发送被阻断,提示「<笔记名> 无法找到,请删除引用」
- ✅ 已解决「模型不主动 read」风险:host 注入笔记内容,模型无需调用 read 也能引用
- ⏳ 4em chip 的截断观感在真实会话中的效果(后续可再评估是否值得做字体覆盖)
1. 目标
在 dsh 对话中输入框里,通过 @ 触发器引用笔记:选中一篇笔记后,其内容随该条
消息进入模型上下文,让模型在回答时能利用笔记内容。与既有「记入笔记」形成闭环:
对话可写入笔记(已实现),笔记也可引用进对话(已实现,本文档即实现蓝图)。
不追求"自动把笔记灌进每轮上下文"(那需要改动模型请求注入层,插件无法做到);
本设计采用 dsh 原生的用户主动精确引用模型——用户 @ 选择哪篇,哪篇进上下文。
2. dsh 提供的机制(已确认)
ui-input-trigger 是 dsh 官方的 slash / 引用触发管线,插件可直接接入:
| 机制 | 作用 | 笔记插件的用法 |
|---|---|---|
InputTriggerService.registerSource | 注册一个 @ 触发源 | trigger: '@',命名 notes(唯一;平台建议名) |
candidates(session, req) | 菜单候选列表 | 从 host list 拉当前工作区笔记 → { name, description, icon, hint } |
onPick(pick) | 选中回调 | 返回 ReferenceInsert { source, ref, label, clipboardText } |
ReferenceInsert | 插入 U+FFFC 占位符(UI 渲染为 chip) | source: 'notes'、ref: 会话工作区相对路径(绝不绝对路径,隐私保证)、label: 标题 |
openReference(可选,0.1.5-rc) | chip 点击手势 | ref 转文件地址 → sidebarRight.openResource 开查看器;无 Sidebar 返回 false |
ReferenceCodec | 提交时把引用序列化为模型文本 | serialize(ref) → 输出路径 + 标题(见 §3.3) |
warm(session) | 会话诞生时预取数据 | 预取笔记名列表(配合 lexicon) |
lexicon(session) | 纯文本 @笔记名 高亮装饰(同步热快照) | 返回笔记标题数组;仅装饰,不参与引用语义(见 §3.1) |
matchEnter | Enter 时解析整行 | 可选:支持 /引用 笔记名 等命令式 |
关键:ReferenceCodec.serialize 的输出就是进入模型上下文的内容——这是"注入"的
真正落点,UI 的 chip 只是表象。ReferenceInsert.ref 携带会话工作区相对路径(§3.3),
label 才是显示文本——serialize 阶段不再丢失工作区信息。
2.1 渲染定制能力(已确认,有限制)
- 引用 chip:dsh 硬编码渲染(
InputBar内css.chip+chipLabel,显示ReferenceInsert.label), 插件无法定制样式/结构(无 slot 注入点)。 @候选菜单:dsh 硬编码渲染(MenuView按 source 分组候选行),插件无法改面板结构; 但候选内容本身可携带任意 label/description,可在文本里体现工作区信息 (如「工作区名」笔记标题)。- 模型
read工具:tool-fs在 base/web bundle 核心挂载,read默认对 agent 可用(无需额外配置)。
3. 方案设计(B:路径引用 + host 内容注入)
核心决策:序列化输出可读的路径行(不把全文写进用户消息),并让 host 在模型请求前 读取笔记并把内容注入上下文(
agent/pre-step,§3.7)——引用可靠生效,不依赖模型自觉 调用read。dsh 的 fs 沙箱只拦截写入(read 全放行,源码:fs-sandbox "Reads pass through untouched"),跨工作区读取无授权障碍;read路径行作为引用失效/摘要模式下的 备用通道。注:注入后全文会进入上下文并持续到会话压缩——这是可靠性的代价,摘要方案 (TODO 1.1)可缓解。
3.1 交互流程(用户视角)
- 在对话输入框输入
@→ 弹出菜单,列出当前工作区的笔记(标题 + 文件名 hint,插件图标)。 - 上下键选择 / 点击选中 → 输入框出现一个笔记 chip(占位符),可多选(多篇笔记)。
- 跨工作区:输入部分工作区名(如
@dsh-pl或@中文)时,候选列出模糊匹配的工作区行 (dsh-plugin/、中文工作区/,🗂️ 前缀 + 「工作区」说明)+ 当前工作区过滤后的笔记; 点击工作区行自动补全@工作区名/并弹出该工作区的笔记列表,继续输入即过滤该工作区的 笔记(已输入工作区后只显示该工作区)。精确输入@工作区名也直接切换。 中文(无空格)工作区名已支持;带空格的工作区名无法文本触发(dsh 触发 token 遇空白截断)。 - 发送消息 → 每个 chip 经
codec.serialize序列化为路径 + 标题(§3.3)。 - 发送后 host 在模型请求前注入笔记内容(§3.7)——模型直接拿到内容并回答引用; 消息里的路径行保留(可读、可追溯,也是引用失效时的备用通道)。
纯文本
@笔记名仅为装饰(lexicon 高亮),不注入上下文——dsh 源码确认 textRef 只是显示层装饰,不产生 chip occurrence,发送时仍是字面@笔记名。真正引用必须通过 菜单选中(chip)。因此不依赖 lexicon 做引用语义(lexicon 仅可选地提供高亮)。 由于 chip 的ref是会话工作区相对路径(§3.3,同工作区.dsh-notes/…、跨工作区../<目录>/…),配合注入与读放行,跨工作区/同名笔记天然无歧义。
3.2 候选数据源(含跨工作区)
- 默认范围:当前会话工作区的笔记(
api('list', { sessionId }))——@默认只列当前工作区, 避免菜单杂乱。 - 跨工作区触发(方式 A):
- 部分名字(
@dsh-pl):候选 = 模糊匹配的工作区行(工作区名/,点击经{ text }自动补全为@工作区名/,重触发后弹出该工作区笔记)+ 当前工作区过滤后的笔记; - 已进入工作区(
@工作区名精确 或@工作区名/…):只显示并过滤该工作区的笔记。 - 工作区行名带尾斜杠(
dsh-plugin/),与笔记标题天然区分。 中文(无空格)工作区名已支持(精确/包含匹配);带空格的工作区名无法文本触发 (dsh 触发 token 遇空白截断,插件侧无法绕过)——改进方向:菜单内全工作区列选(TODO 1.2)。
- 部分名字(
- 候选:
{ name: 笔记标题, description: 文件名(去 .md), icon: '📝', hint: 无 }; 跨工作区候选description为工作区名 · 文件名。icon实测为纯文本渲染(16px 槽位),URL/SVG 无法显示为图片,故用 📝 emoji 替代 插件 SVG(§0 实测结论)。 - 无工作区:
warm/candidates返回空(@不到笔记)——不弹菜单,静默无候选。 - 实时性:
warm在会话诞生时预取;笔记增删后经subscribeLexicon通知刷新 (lexicon 仅用于可选的高亮装饰,不影响引用语义)。
3.3 序列化格式(进入模型的文本)
ReferenceCodec.serialize(ref) 返回路径引用(不包含全文),本地化、可读的一行文本
(随界面语言;中文用「」括标题,英文用引号)——在对话气泡里读起来自然,不再是 html 标签:
引用笔记 [README](../dsh-work/.dsh-notes/README.md)
- 标准 markdown 文件引用语法:
[标题](路径)把标题与路径绑定为结构化 token—— 模型提取路径可靠,任何 markdown 渲染器(包括未来的笔记跳转)都能识别为链接。 标题中的]/(已按 markdown 链接语法转义。 - 路径相对会话工作区根:
read工具解析相对路径的基准是会话 cwd = 当前工作区根 (tool-fssession-cwd.ts)。因此:- 同工作区引用 →
.dsh-notes/xxx.md; - 跨工作区引用 →
../<工作区目录名>/.dsh-notes/xxx.md(从当前工作区..到兄弟目录)。
- 同工作区引用 →
- 路径用目录名、不用工作区名(title):dsh 工作区的
title(显示名)可setTitle改、path(目录)创建后不变;模型read只能按目录名解析。因此引用路径永远取notesDir的真实目录,title 改名不影响已有引用;会话工作区未知(list 未就绪)时的兜底 也改用绝对路径(目录名)而非 title 前缀,避免 title 改名后路径指向不存在的位置。 - 为什么不用
<工作区名>/…前缀:会话 cwd 就是工作区本身,dsh-plugin/.dsh-notes/…会被解析成当前工作区/dsh-plugin/…——工作区不可能嵌套在自己里面,必然读不到 (实测:模型把它解析到 test-work 下,找不到)。目录名(如dsh-work)与工作区标题 (dsh-plugin)可能不同,路径只能用目录名才能被解析。 ref(chip 身份)即上述相对路径;title作可读标签。读放行(见 §3.6),跨工作区无碍。- 存在性校验的解析规则(serialize 提交时检查笔记是否仍存在,失败则阻断发送):
相对 ref 是相对会话工作区根生成的,因此「解析基准」与「目标工作区」可以不同——
匹配 = 找到拥有该文件名的笔记工作区 + 存在某工作区根能把 ref 解析到该笔记的绝对路径
(
..越界的无效路径直接判不匹配)。因此**跨工作区、任意深度(嵌套/祖先/子目录)**的 引用都能正确解析;host 注入端的路径正则也支持含空格的文件名。 - 格式演进:v1
<note ref="…">标题</note>(XML 标签,消息里显示为裸 html);v2 本地化 可读行(放弃标签语法);v3<工作区名>/.dsh-notes/…(实测模型解析不到——cwd 即工作区); v4 会话工作区相对路径(同工作区.dsh-notes/…,跨工作区../<目录>/…);v5 定稿 markdown 链接语法[标题](路径)(结构化、渲染器可识别,为笔记跳转打基础)。 - 引用失效处理:被引用的笔记若已删除/移动,
serialize失败会阻断发送(dsh 源码: "serialize failure blocks the send")。此时向用户提示 「<笔记名> 无法找到,请删除该引用」,保留 draft 与 chip 让用户移除后重发。 - 路径 fallback(0.7.0):工作区改名后其目录不变,但若目标笔记的路径解析不到
(如目录被移动/重建),serialize 用标题生成路径兜底(
<标题>.md),引用仍指向 可定位的笔记而非报「不存在」——见 [f5d744c修复]。 - 摘要(暂缓):未来可在序列化中附带"标题 + 前 N 字符摘要"(模型先看摘要、
需要全文再
read),减少不必要的读取。本期不实现,见 TODO。
3.3.1 输入框 chip 的显示宽度
- dsh 的 chip 单元格宽度 = 文本框里 U+FFFC 占位符的 advance,由
DshChipCell字体 (只映射 U+FFFC 为空白字形)硬编码为 4em(约 48px);标签在格内居中、overflow 裁剪——长标题会被前后同时截断(只露中间一截)。chip DOM/尺寸为 dsh 硬编码, 插件无法改结构。 - 曾尝试
@font-face覆盖DshChipCell(4em→6em/10em)放大单元格——按平台规范 (插件不应注入影响核心 composer 的全局样式)与「先看原生效果」的取舍,已移除, 退回原生 4em。 - chip 标签前置截断(>4 字符 → 前 4 字符 + …):即使标题超长,也总是显示开头而非 中间一截(4em 单元可见约 4 字符;短标题如「3333」完整显示)。
- 插件 logo(0.7.0):chip 前置插件图标——保留
appearance='notes'作用域, 注入 scoped 样式绘制图标(不污染 dsh 核心样式),让笔记 chip 一眼可辨。
3.4 实现位置
- client:新增
features/ContextSource/(ContextSource.tsx+ css),在apply里ctx.get('inputTriggers')?.registerSource(...)(挂ctx.effect,HMR 安全)。 - host:
listAPI 透出每个工作区的notesDir(WorkspaceEntry 已有该字段,http 层 补返回即可)——client 以会话工作区根为基准算相对路径(同工作区.dsh-notes/…、 跨工作区../<目录>/…),零新增 API。 跨工作区候选也由list(不带 sessionId)全量返回,各带notesDir。
3.5 与「记入笔记」的闭环
| 方向 | 现状 |
|---|---|
| 对话 → 笔记 | ✅ 已实现(appendConversation,回答下方 📝) |
| 笔记 → 对话 | ✅ 已实现(@ 引用 + 路径序列化 + host 内容注入) |
记入笔记的文本(提问 / 回答 / 会话标题)由 client 从浏览器会话快照提取
(features/note-text.ts,与复制按钮同源),host 的 appendConversation 只做格式化 +
写文件——不再调用 sessionQuery.readSession(该 API 会全量读会话日志 + 深拷贝 + replay
校验,长会话时同步阻塞事件循环,卡住面板的 list/read 请求)。
3.6 跨工作区引用的授权说明
- dsh 的 fs 沙箱只约束写入(
workspace-write拒绝在工作区外写文件),读取全放行。 - 因此引用任何工作区的笔记路径,对话的
read工具都能读——无需额外授权流程。 - 这与既有 git 同步的授权模型一致(仓库在插件管理目录、无授权),读方向天然开放。
3.7 模型可靠性:host 端内容注入(已实现)
风险:序列化只给路径,模型是否自觉调用 read 取决于模型判断,不保证。
方案(已实现,host/context-inject.ts):监听 dsh 原生 agent/pre-step 事件(agent-instructions
同款机制)——每次模型请求前扫描已认领消息里的笔记路径(正则提取 .dsh-notes/… 路径,相对
会话 cwd 解析),读取笔记内容并作为注入上下文消息折叠进模型请求
(source 用官方 plugin 变体:{ kind: 'plugin', plugin: 'md-notes', path }):
- 模型直接拿到笔记内容,无需调用
read;路径行仍在用户消息里(可读、可追溯)。 - 注入内容带中英双语引用约定(
[标题](路径)markdown 链接格式,与用户消息的序列化 语法一致)——结构化引用便于渲染器识别;这是对模型的尽力引导,非硬性保证。 - 注入消息随 step 持久化进会话日志(agent-loop 会把 decision.messages 全部 append),
界面上渲染为注入上下文行(DisclosureRow;ui-chat 的
contextProvenance对pluginsource 取plugin字段做标签,来源仍显示md-notes),跨步骤按 source 的path去重—— 一次引用只注入一次。 - 引用失效:文件已删除则跳过注入,用户消息里的路径行仍在(模型可尝试 read 或说明缺失)。
- 内容边界:只读取
.dsh-notes目录内的文件。
为什么必须是官方 plugin 变体而不是自定义 kind(选型依据,勿"优化"掉):dsh
0.1.5-alpha.1 把会话日志升到 V3,加载 V2 旧日志时迁移层对 user/message 的 source.kind
做白名单校验(SOURCE_KINDS,见 session-format-v2-to-v3 的 assertSource)——自定义
kind(插件早期版本用过的 {kind: 'md-notes'})会让整个日志被拒绝读取。官方 plugin
变体在白名单内且不限制额外键,path 字段保留作去重键;V2 时期由新插件版本写入的
plugin source 日志也能平滑过迁移。
为什么是 pre-step 折叠而不是 agent.inject()(选型依据,勿"优化"掉):dsh 还提供
agent.inject(message)——把注入上下文排入下一个 pre-step。它的文档明言「may miss a
request whose pre-step already claimed its batch」:引用发生在用户消息里,笔记内容必须与
该消息同批进入模型请求;inject() 排队语义下,正在 claim 的这批消息赶不上,引用
会失效一个 step。pre-step 是 waterfall 且允许替换进入 step 的 messages
(packages/core/agent/src/runtime-types.ts),折叠恰好满足「同批生效」。这两条都
依据 harness 源码,升级 dsh 时如事件契约变化需重新核对。
4. 知识库式自动检索(超出 dsh 原生,可选阶段)
- 现状:dsh 无知识库/RAG 机制;引用是用户主动选择,非自动检索。
- 可选增强 1(关键词检索候选):host 新增
contextSearch(q)API,按标题/内容 关键词过滤笔记,作为@候选的扩展(输入更多字符时过滤)。 - 可选增强 2(自动注入):把"当前工作区全部笔记"拼接进系统提示——插件无法直接改 模型请求,需 dsh 提供上下文片段注入接口(依赖 dsh 平台能力,本期不做)。
5. 验收标准
- 输入
@弹出笔记候选(默认当前工作区,候选带插件图标);选中后显示 chip;可多篇。 - 发送后序列化输出可读路径行;host 注入笔记内容,模型无需 read 也能在回答中引用。
- 跨工作区:输入部分工作区名(ASCII)出现工作区行,点击自动补全;序列化 ref 为
../<目录>/.dsh-notes/笔记名.md;注入内容正常进入模型。 - 引用失效:被引用笔记删除/移动后发送 → 提示「<笔记名> 无法找到,请删除引用」, draft 与 chip 保留,不静默丢弃。
- 纯文本
@笔记名仅装饰、不注入上下文(不依赖 lexicon 语义)。 - 无工作区时
@无候选(静默)。 - i18n:菜单空态/提示/序列化标签/失效提示中英双语。
- HMR 安全:
registerSource挂在ctx.effect,卸载自动清理。 - ✅ 已实测确认:
InputTriggerCandidate.icon在菜单中渲染为纯文本(16px 槽位), URL/SVG 不能显示为图片 → 候选图标用 📝 emoji(§0 实测结论)。
6. 实现步骤
- 依赖确认:
@deepseek-ai/dsh-client-ui-input-trigger加入 link-deps 与 tsdown external(client 侧类型 + 运行时服务)。✅ - host:
listAPI 透出每工作区notesDir(供 client 算会话工作区相对路径)。✅ - Client source:
features/ContextSource/实现InputTriggerSource(candidates/onPick/codec/warm/lexicon):ref= 会话工作区相对路径(同工作区.dsh-notes/…、跨工作区../<目录>/…),label= 标题;candidates解析query:斜杠前段精确匹配工作区 → 只显示/过滤该工作区;部分名字 → 工作区模糊行(自动补全)+ 当前工作区过滤;icon= 📝 emoji(实测 icon 为纯文本渲染,见 §0)。✅
- i18n:新增
context.*前缀 key(引用失效提示、校验失败提示)。✅ - 联调:真实会话中
@选笔记(含跨工作区)→ 发送 → 验证注入生效(模型不读文件也引用)。⏳ - 文档:features.md §2.7 + architecture.md(目录与 slot 说明)+ 本文状态。✅
7. 风险与取舍
- 上下文长度:注入后全文进入上下文并持续到会话压缩(§3.7)——长度风险由「模型自主读」 转为「注入占用」。缓解:摘要注入(TODO 1.1)只放标题 + 前 N 字,模型按需 read 全文。
- 多篇笔记:序列化多篇时按插入顺序拼接,各为一行「引用笔记」文本。
- 引用失效:笔记删除/移动后发送被阻断(dsh 源码行为)——按 §3.3 提示用户移除引用, 不静默降级。
- ✅ 模型主动读(已解决):依赖模型自觉不可靠,host 端
agent/pre-step内容注入 保证模型直接拿到笔记内容(§3.7);摘要功能(未来)可进一步控制上下文长度(见 TODO)。 - 渲染不可定制:chip 与候选菜单为 dsh 硬编码,插件无法改样式/结构——工作区信息只能 通过候选 label 文本体现(§2.1)。
- lexicon 全局合并:
@的 lexicon 是多个 source 合并的名称数组,纯文本装饰可能与其他 source 重名——但引用语义不依赖 lexicon(仅装饰),chip 的 ref 是会话工作区相对路径,无歧义。 - 空格限制:带空格的工作区名无法文本触发(dsh 触发 token 遇空白截断);中文(无空格) 已支持。改进方向:菜单内全工作区列选(TODO 1.2)。
- ✅ icon 结论:候选 icon 为纯文本渲染,插件 SVG 无法显示——用 📝 emoji 替代
(16px 槽位内可见),不再依赖
ICON_URL。