# DSH 插件开发笔记:对话大纲右侧面板 给后续开发 DSH Web 插件的 Agent 看。本插件在 better-sidebar 中注册“对话大纲” tab,显示当前会话的结构化大纲,并支持快速跳转到对应消息。 ## 项目产物 - 动态插件源码:`dynamic/src/host/plugin.host.js`、`dynamic/src/client/plugin.client.js` - 正式安装的静态包:`static/` - 安装目标:`C:\Users\ysw35\.dsh\profiles\web` ## 运行与安装事实 - 用户运行方式是 `npx @deepseek-ai/dsh web`。 - 动态插件只存在当前进程,重启即丢,不能作为正式交付。 - 正式安装命令(在 `static/` 目录内): ```powershell dsh plugin --profile web add . dsh --profile web --dump-config ``` - 需要先安装/链接 v0.12.0 的 `dsh-better-sidebar`(见 `static/README.md`);安装后重启 `npx @deepseek-ai/dsh web` 才让静态包和 Host 半生效。 - 当前 web profile 里已存在一个损坏的 `reasoning-effort-editor-static` 链接;本插件不修改该旧插件源码。若它阻塞安装,只绕开它,不删除旧插件目录。 ## 挂载点与交互 - 动态入口:`conversation.session.header.actions`,id `conversation-outline-toggle`,以 `priority: -1` 覆盖原生入口。 - 动态面板:`details` 右列,以 `priority: -1` 覆盖原生 DetailsPanel;仅用于动态验证,不用于正式交付。 - 正式静态包:按 [better-sidebar 外部插件指南](https://github.com/omdsh-dev/DSH-better-sidebar/blob/main/docs/external-plugin-guide.md) 接入,client half 通过 `ctx.betterSidebar.registerTab` 注册 `conversation-outline` tab。 - 外部插件 tab 只进入 better-sidebar 的 portal,面板宽度、分栏、tab 关闭和移动端抽屉均由 better-sidebar 原生管理。 - 不使用 `details`、`conversation.details.tool` 或 `shell.overlay`,因此不替换 DSH 原有右侧工具详情列。 ## WebUI 关键接口 - 动态客户端通过 `host.call('outline/snapshot', { sessionId })` 取时间线大纲。 - 动态 Host 通过 `ctx.sessionQuery.readSession(sessionId)` 读取完整日志,返回紧凑 JSON,不把原始 Session 对象跨 RPC。 - 动态 Host 通过 `host.call('outline/title', { sessionId, turn })` 为轮次生成一句话标题。 - 标题生成直接调用 `llm.stream`,`provider/model/maxTokens` 从主 Agent options 或 `agentDefaultModel.currentSelection()` 继承,不再创建 subagent。 - 动态 Host sandbox 没有 `AbortController`;优先用主 Agent `runMaintenance()` 的真实 signal,失败时回退到包内不可取消 signal shim。 - 静态 Host 通过 `ctx.webServer.register` 暴露 `/dsh-outline/snapshot` 与 `/dsh-outline/title` 同源 JSON 接口。 - 静态浏览器 bundle 用 `fetch('/dsh-outline/...')` 读取同一份时间线和生成标题,不依赖 `connection.api` 或动态 RPC。 - 会话历史加载:`sessions.binding(sessionId).session.loadOlder()`。 - 跳转锚点:聊天行有稳定 `data-chat-anchor-key`;从 `session.getSnapshot().chat.nodes.values()` 按 `anchorSeq` 找到节点 key,再在 DOM 中定位。 - 当前没有公开的“切换到 chat 视图”API;若用户停留在 trajectory 等视图,跳转会降级为提示,不破坏页面。 ## 打磨后的目录行为 - Host 返回 `{ timeline: [...] }`,按事件 `seq` 顺序排列。 - 动态面板注册到原生 `details` 列;打开时占用布局宽度,窄窗口由 layout 自动收起,不使用 fixed overlay。 - `command/run` 不再单独展示;轨迹页只显示对应的 `user/message` 行,因此大纲与轨迹保持一致,`/plan` 不会出现重复输入。 - 连续 `tool/call` 且次数 >= 2 时合并为 `tool-group`,默认折叠,组头显示“N 次调用”。 - 单次 `tool/call` 不折叠,直接显示为一条普通工具行。 - `assistant` 与 `error` 不参与工具组合并;工具出错会打断连续工具组。 - `source.kind === 'user'` 的普通用户消息进入对应轮次卡片;若在 `turn/start` 之前出现,会先进入 pending 队列,在下一个轮次创建时塞入卡片内部最前面。 - 如果该轮还没有助手/工具/错误等实质内容,用户输入位于轮次卡片内部最前面,标题下方。 - 轮次用卡片包裹,标题默认折叠,整组内容可手动展开/收起。 - 轮次标题使用 `position: sticky`,只有该组所有条目滚出后标题才会离开可视区。 - 工具组合并后默认折叠;展开时左侧 chevron 会旋转 90 度,与轮次标题一致。 - 标题由 `llm.stream` 生成;生成中显示“生成标题中...”,失败时如果卡片内有内容,回退显示卡片第一条内容的预览,否则回退为“第 N 轮”。 - 标题生成不会出现在对话界面的 sub agent 调用列表或侧边栏 sub agent 活动中。 - Host 用 `sessionId -> lastSeq` 缓存时间线;`outline/snapshot` 先读轻量 `listEvents`,只有会话增长后才重建。 - Client 用 `localStorage` 缓存已生成标题(key:`dsh-conversation-outline-titles:v1`),刷新或重启后不会为旧轮次再次调用 `llm.stream`。 - 标题缓存 key 使用轮次可见内容的 FNV-1a 指纹 `titleKey`,不是 `sessionId:turn`;分支新会话只要前缀轮次内容相同就会命中同一标题缓存。 - Client 刷新大纲时保留旧大纲可见,不再显示“刷新中...”文本;刷新按钮旋转并禁用,新大纲完成后恢复。 - 标题生成在 Client 端使用 `maxConcurrency`(默认 3)个 worker 并发;Host 端用 `titleKey` 去重,同一内容只启动一次 `llm.stream`。 - 跳转支持聊天页与轨迹页:聊天页继续用 `data-chat-anchor-key`;轨迹页通过 Host 返回的 `callId`、assistant `turn/step` 等元数据映射到 `data-trajectory-row-key`,必要时先加载更早历史并展开折叠摘要。 - 轨迹页里的 `/plan` 等命令没有独立轨迹行,会映射到紧随其后的同轮用户消息;轨迹内部把 `steering` 事件渲染为 `user` 行,因此跳转映射也按 `user` 行 key 计算;工具/错误行通过 `callId` 定位,压缩检查点通过 `compaction/start` 的 `startSeq` 定位。 ## 最容易踩的坑 - `user/message` 的正文在 `data.content`,不是 `data.message.content`;读错会导致所有普通用户输入被跳过。 - `command/run` 的正文/参数在 `data.args`,不是 message 包裹结构。 - 动态客户端不能用 `connection`;动态版必须走 Host RPC,静态版用同源 `fetch` 访问 `webServer` 注册的 JSON 路由。 - 不要对整个 Session、Slot props 或 Event payload 做 `JSON.stringify` / 递归复制;Host 端只提取标量字符串,客户端只消费紧凑 outline。 - 注册 `details` 单槽会替换现有 DetailsPanel 并带走 `conversation.details.tool` 的渲染;动态版有意用 `priority: -1` 覆盖它来获得原生右列,停止动态插件后原生详情会恢复。 - 动态入口与 `details` 都必须在正式静态版/原生面板的同 cell 上使用更低 priority,否则动态加载会报“already has a registration”。 - 动态客户端没有 `setTimeout` 等原生 timer 全局;自动刷新直接订阅 Session snapshot 的 `subscribe()`,不要在组件里建 timer。 - `session.loadOlder()` 每次只加载一页;跳转到很老的消息可能连续加载多页,必须设置上限并显示状态。 ## better-sidebar 外部插件设置 - 静态包通过 `ctx.betterSidebar.registerTab` 注册 `conversation-outline` tab,并通过 `settings.render` 提供原生齿轮弹窗设置。 - 设置持久化到 better-sidebar 的 `pluginSettings['conversation-outline']`,同时写浏览器 `localStorage` 作为旧版回退。 - 字段:标题生成模型、模型推理等级、标题生成并发数、标题超时(秒)、失败重试次数、是否不生成标题。 - 模型和推理等级都有“与会话模型相同”选项,值为空字符串时标题生成继续使用主会话模型配置。 - `/dsh-outline/config` 返回当前可用模型、推理等级;`/dsh-outline/title` 接受 query 中的 model / reasoningEffort 覆盖。 - `fetchTitle` 会把 `pluginSettings` 中的 model / reasoningEffort 传给 `/dsh-outline/title`;并发数和禁用逻辑在浏览器端生效。 - 静态 Host 的标题生成直接调用 `llm.stream`,不再创建 subagent,并支持 model / reasoningEffort 覆盖。 - `llm.stream` 失败会透出 `chunk.reason.failure` 的 code/status/message(UI 显示在“未生成 · ...”,Host `console.error` 也记录 provider/model/override)。 - Host 对**整个标题尝试**(会话读取、模型解析、凭证解析、`llm.stream` 全流程)施加超时(`timeoutSeconds`,默认 60s,可配置,`Promise.race` 强制),超时/失败按 `maxRetries`(默认 2,可配置)重试,退避 0.8s 起封顶 5s;全部失败后再缓存 60 秒。Client 另有总预算超时,避免“生成标题中...”卡死。 - 标题生成**完全并行**于会话:直接调用 `llm.stream`(自带 AbortController),**不使用 `agent.runMaintenance()`**——它只在 agent 空闲时能执行任务,忙碌时立即抛 `already has active work`,从不排队等待轮次结束;DSH 官方 `dsh-session-title-llm` 也是直接调 `ctx.llm.stream`(`purpose: 'session-title'` + deadline signal),不依赖 agent 状态。 - Client 只对**当前仍在进行中的轮次**(`endSeq === null` 且会话 `running === true`)延迟生成标题(显示“等待本轮结束...”);已结束轮次在会话运行中也会立即并行生成。`running` 翻转 false 后自动重新加载大纲,让刚结束的轮次开始生成——不再需要硬刷新。 - 之前“卡在生成中 / 超时不触发 / 硬刷新才出标题”的根因:旧代码把超时放在 `llm.stream` 循环内部逐 chunk 检查,若 provider 流一直不产生第一个 chunk(网络挂起),循环体永远不执行、超时检查永远不触发;而 pi-ai/DeepSeek 适配器的流空闲看门狗默认长达 300s,所以表现为“永远卡住”。整尝试超时(Promise.race + AbortController)从请求一开始就计时,与 chunk 是否到达无关,可确保每次尝试在 `timeoutSeconds` 内结束。 - 无文本/无 Agent 等确定性错误仍永久返回,不参与重试。 - npm 的 `dsh-better-sidebar` 当前是 `0.11.0`;`settings.render` 需要 v0.12.0,本地 profile 已链接 `Y:\github_proj\DSH-better-sidebar` 的构建产物。 ## 验证 - 动态:`cordis_define` 后 `cordis_run`,查看 Run 卡;若 Client 加载失败,用 `cordis_inspect_self` 读诊断。 - 静态:执行 `node --check static/client.js` 与 `node --check static/index.js`。 - 安装后:`dsh --profile web --dump-config` 应出现 `conversation-outline-sidebar` 与 `better-sidebar` 两行。 - 页面验证:better-sidebar + 菜单应出现“对话大纲”,设置页“侧边卡片”应有该 tab 卡片和齿轮设置。