# 笔记加入对话上下文(可被引用)设计 > 本文档设计「把笔记作为可引用的上下文注入 dsh 对话」——TODO 第 2 项的实现蓝图。 > 功能总览见 [features.md](features.md),架构见 [architecture.md](architecture.md)。 ## 0. 实现状态 > 最后更新 2026-08-19。✅ 已实现(0.4.0 + NEXT_VERSION);⏳ 少量待实测。 ### 已确认(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/TODO 2.3) - ✅ **候选 icon 实测结论**:`MenuView` 把 `InputTriggerCandidate.icon` 作为**纯文本**渲染在 16px 槽位(`{item.icon}` 字符串子节点)——**URL/SVG 无法渲染成图片**,会显示字面 URL 文本。 因此候选 icon 用 📝 emoji 替代插件 SVG(见 §3.2)。 ### 已实现(0.4.0 + NEXT_VERSION) - ✅ 对话输入 `@` 引用笔记(候选菜单 / chip / 跨工作区工作区行自动补全) - ✅ 序列化格式为**标准 markdown 链接** `[标题](路径)`(见 §3.3) - ✅ **host 端内容注入**:`agent/pre-step` 把被引用笔记的内容折叠进模型请求(不依赖模型 自觉 `read`),注入内容带「引用约定」一行(见 §3.7) - ✅ 纯文本 `@笔记名` 装饰(`lexicon` 热快照,仅 ASCII 标题生效) - ✅ chip 显示优化:标签**前置截断**(4em 单元,>4 字符 → 前 4 + …,显示开头而非中间一截,§3.3.1) - 🚧 知识库式自动检索(超出 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: 标题` | | `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 2.1)可缓解。 ### 3.1 交互流程(用户视角) 1. 在对话输入框输入 `@` → 弹出菜单,列出**当前工作区**的笔记(标题 + 文件名 hint,插件图标)。 2. 上下键选择 / 点击选中 → 输入框出现一个笔记 chip(占位符),可多选(多篇笔记)。 3. **跨工作区**:输入部分工作区名(如 `@dsh-pl` 或 `@中文`)时,候选列出**模糊匹配的工作区行** (`dsh-plugin/`、`中文工作区/`,🗂️ 前缀 + 「工作区」说明)+ 当前工作区过滤后的笔记; **点击工作区行**自动补全 `@工作区名/` 并弹出该工作区的笔记列表,继续输入即过滤该工作区的 笔记(已输入工作区后只显示该工作区)。精确输入 `@工作区名` 也直接切换。 **中文(无空格)工作区名已支持**;**带空格**的工作区名无法文本触发(dsh 触发 token 遇空白截断,见 TODO 2.3)。 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 2.3(菜单内全工作区列选)。 - **候选**:`{ name: 笔记标题, description: 文件名(去 .md), icon: '📝', hint: 无 }`; 跨工作区候选 `description` 为 `工作区名 · 文件名`。 `icon` 实测为**纯文本渲染**(16px 槽位),URL/SVG 无法显示为图片,故用 📝 emoji 替代 插件 SVG(§0 实测结论)。 - **无工作区**:`warm`/`candidates` 返回空(`@` 不到笔记)——不弹菜单,静默无候选。 - **实时性**:`warm` 在会话诞生时预取;笔记增删后经 `subscribeLexicon` 通知刷新 (lexicon 仅用于可选的高亮装饰,不影响引用语义)。 ### 3.3 序列化格式(进入模型的文本) `ReferenceCodec.serialize(ref)` 返回**路径引用**(不包含全文),**本地化、可读的一行文本** (随界面语言;中文用「」括标题,英文用引号)——在对话气泡里读起来自然,不再是 html 标签: ```markdown 引用笔记 [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 `标题`(XML 标签,消息里显示为裸 html);v2 本地化 可读行(放弃标签语法);v3 `<工作区名>/.dsh-notes/…`(实测模型解析不到——cwd 即工作区); v4 会话工作区相对路径(同工作区 `.dsh-notes/…`,跨工作区 `../<目录>/…`);v5 定稿 **markdown 链接语法 `[标题](路径)`**(结构化、渲染器可识别,为笔记跳转打基础)。 - **引用失效处理**:被引用的笔记若已删除/移动,`serialize` 失败会**阻断发送**(dsh 源码: "serialize failure blocks the send")。此时向用户提示 **「<笔记名> 无法找到,请删除该引用」**,保留 draft 与 chip 让用户移除后重发。 - **摘要(暂缓)**:未来可在序列化中附带"标题 + 前 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」完整显示)。 ### 3.4 实现位置 - **client**:新增 `features/ContextSource/`(`ContextSource.tsx` + css),在 `apply` 里 `ctx.get('inputTriggers')?.registerSource(...)`(挂 `ctx.effect`,HMR 安全)。 - **host**:`list` 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.kind: 'md-notes'`): - 模型**直接拿到笔记内容**,无需调用 `read`;路径行仍在用户消息里(可读、可追溯)。 - 注入内容带**中英双语引用约定**(`[标题](路径)` markdown 链接格式,与用户消息的序列化 语法一致)——结构化引用便于渲染器识别;这是对模型的尽力引导,非硬性保证。 - 注入消息随 step 持久化进会话日志(agent-loop 会把 decision.messages 全部 append), 界面上渲染为**注入上下文行**(DisclosureRow,来源标 `md-notes`),跨步骤按 source 去重—— 一次引用只注入一次。 - 引用失效:文件已删除则跳过注入,用户消息里的路径行仍在(模型可尝试 read 或说明缺失)。 - 内容边界:只读取 `.dsh-notes` 目录内的文件。 ## 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. **host**:`list` API 透出每工作区 `notesDir`(供 client 算会话工作区相对路径)。✅ 3. **Client source**:`features/ContextSource/` 实现 `InputTriggerSource` (`candidates` / `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 2.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 2.3)。 - ✅ **icon 结论**:候选 icon 为纯文本渲染,插件 SVG 无法显示——用 📝 emoji 替代 (16px 槽位内可见),不再依赖 `ICON_URL`。