笔记加入对话上下文(可被引用)设计

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 实测结论MenuViewInputTriggerCandidate.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)
matchEnterEnter 时解析整行可选:支持 /引用 笔记名 等命令式

关键ReferenceCodec.serialize 的输出就是进入模型上下文的内容——这是"注入"的 真正落点,UI 的 chip 只是表象。ReferenceInsert.ref 携带会话工作区相对路径(§3.3), label 才是显示文本——serialize 阶段不再丢失工作区信息。

2.1 渲染定制能力(已确认,有限制)

  • 引用 chip:dsh 硬编码渲染(InputBarcss.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 交互流程(用户视角)

  1. 在对话输入框输入 @ → 弹出菜单,列出当前工作区的笔记(标题 + 文件名 hint,插件图标)。
  2. 上下键选择 / 点击选中 → 输入框出现一个笔记 chip(占位符),可多选(多篇笔记)。
  3. 跨工作区:输入部分工作区名(如 @dsh-pl@中文)时,候选列出模糊匹配的工作区行dsh-plugin/中文工作区/,🗂️ 前缀 + 「工作区」说明)+ 当前工作区过滤后的笔记; 点击工作区行自动补全 @工作区名/ 并弹出该工作区的笔记列表,继续输入即过滤该工作区的 笔记(已输入工作区后只显示该工作区)。精确输入 @工作区名 也直接切换。 中文(无空格)工作区名已支持带空格的工作区名无法文本触发(dsh 触发 token 遇空白截断)。
  4. 发送消息 → 每个 chip 经 codec.serialize 序列化为路径 + 标题(§3.3)。
  5. 发送后 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-fs session-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),在 applyctx.get('inputTriggers')?.registerSource(...)(挂 ctx.effect,HMR 安全)。
  • hostlist API 透出每个工作区的 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 的 contextProvenanceplugin source 取 plugin 字段做标签,来源仍显示 md-notes),跨步骤按 source 的 path 去重—— 一次引用只注入一次。
  • 引用失效:文件已删除则跳过注入,用户消息里的路径行仍在(模型可尝试 read 或说明缺失)。
  • 内容边界:只读取 .dsh-notes 目录内的文件。

为什么必须是官方 plugin 变体而不是自定义 kind(选型依据,勿"优化"掉):dsh 0.1.5-alpha.1 把会话日志升到 V3,加载 V2 旧日志时迁移层对 user/messagesource.kind 做白名单校验(SOURCE_KINDS,见 session-format-v2-to-v3assertSource)——自定义 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. 实现步骤

  1. 依赖确认@deepseek-ai/dsh-client-ui-input-trigger 加入 link-deps 与 tsdown external(client 侧类型 + 运行时服务)。✅
  2. hostlist API 透出每工作区 notesDir(供 client 算会话工作区相对路径)。✅
  3. Client sourcefeatures/ContextSource/ 实现 InputTriggerSourcecandidates / onPick / codec / warm / lexicon):
    • ref = 会话工作区相对路径(同工作区 .dsh-notes/…、跨工作区 ../<目录>/…),label = 标题;
    • candidates 解析 query:斜杠前段精确匹配工作区 → 只显示/过滤该工作区;部分名字 → 工作区模糊行(自动补全)+ 当前工作区过滤;
    • icon = 📝 emoji(实测 icon 为纯文本渲染,见 §0)。✅
  4. i18n:新增 context.* 前缀 key(引用失效提示、校验失败提示)。✅
  5. 联调:真实会话中 @ 选笔记(含跨工作区)→ 发送 → 验证注入生效(模型不读文件也引用)。⏳
  6. 文档: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