# dsh-session-toolkit [English](README.md) | 中文 DeepSeek Harness 的整合插件工具箱。将先前 6 个独立的本地插件——会话身份、全局提示词、会话自动恢复、Web 重启服务、Session log 按钮平移、会话间消息——合并为单个可安装包(官方 bundle 形态,`dsh.bundle.patch`),通过 `dsh plugin add` 安装;另含提示词去重(Prompt Dedup)功能。 当前版本:**0.1.9**,已对照 **DeepSeek Harness `dsh-v0.1.6-alpha.2`** 验证(更早内核靠下文两处兜底继续可用)。 --- ## 功能 ### 会话身份(Session Identity) 每会话人设提示词注入该会话系统提示词(独立段 `session-identity`,order 40,每次组装按 agent 求值),支持默认身份与每会话覆盖。UI:身份浮层(启用开关、4000 字符软上限、保存/重置、编辑默认身份、继承默认身份)及双入口状态按钮:`conversation.session.header.actions`(id `session-identity`,order 40)与 `conversation.input.left`(id `session-identity-input`,order 40)。 ### 全局提示词(Global Prompt) 设置页(`settings.section`,id `global-prompt`,order 30),以 **Tabs(全局 / 按工作区)** 渲染。*全局* Tab 注入一段作用于所有会话系统提示词的文本(段 `global-prompt`,order 50);*按工作区* Tab 注入按工作区提示词(段 `workspace-prompt`,order 60)。两个段都以 **`interpolate: false`** 注册:提示词文本与引用文件里的 `{{...}}` 一律按字面保留,用户内容永不被改写,未注册的 `{{name}}` 也不可能让组装失败。0.1.6 之前的内核没有分段的 `interpolate` 开关,由 `lib/prompt-literal.js` 在组装结果上退化为 `{` 连续串空格化。 ### 工作区提示词(Workspace Prompt) 为 `cwd` 前缀匹配到已配置工作区目录(该目录及子目录)的会话注入按工作区提示词。工作区列表由**活跃会话的 `cwd`** 聚合而来(`ctx.agents.roots()`,去重并按会话数计数)。当多个已启用工作区前缀命中某会话的 `cwd` 时,取**最具体(路径最深/最长)**者。`removed` 记录用户已移除的路径,使活跃工作区同步不重新补回。工作区行的启用开关 **即时保存(live-save)**;「保存」按钮仅持久化提示词**内容 + 引用文件**。 ### 引用文件(Referenced Files) 全局提示词与工作区提示词均可引用**文件列表**。每次组装重新读取每个引用文件(UTF-8;按 `mtimeMs` + 大小缓存,未变化的文件不重复读盘),注入到提示词文本之后。有字节预算(`globalPrompt.maxFileBytes` / `maxTotalBytes`,默认 256 KiB / 1 MiB):超限文件**跳过**而不是阻塞组装。读取失败同样跳过,两种情况都在 UI 中显示具体原因。支持纯文本/markdown。每个文件的读取状态经 `prompt-file-status` 命名空间投影到 UI(`ok`:N 字符 / `fail`:原因 / 未读取);该投影**只在状态真正变化时写入**(每次 settings 写入都会持久化整份 `settings.yaml`)。 ### 会话自动恢复(Session Auto-Resume) 开启开关的会话在 GUI 重启后自动恢复,优先走**官方恢复链路**(`ctx.sessionController.resolveAgent`)——它除了 mount preset,还会通过 `installSelection` 恢复会话自己的模型选择,并做 subagent 归属校验与并发恢复去重;0.1.6 之前没有该服务的内核回落为 `ctx.agents.resume` + 手工 mount preset,并携带 `agentDefaultModel` 的默认模型。开启某会话即立即恢复(false→true 边沿)。过滤:开关开启、仅顶层(无 subagent origin、无 `delegationDepth > 0`、无 `parentSession`)、非空白(快照形状的 `eventCount !== 0`)。并发受限(`CONCURRENCY = 3`),逐项失败隔离 + 在途集合防重复恢复。 ### Web 重启(Web Restart) General 设置中的「重启服务」入口(`settings.general.item`,id `web-restart`,order 90),重启 GUI 服务器并显示全屏进度覆盖层(探针驱动进度、重载前填充动画、90 秒超时回退到手动刷新)。两条平台链路都**独立于将要退出的服务器进程**: - **Windows**(`windows-script`):`wscript.exe` 执行 launcher VBS(隐藏控制台),由它运行 `/autostart/dsh-web-restart.cmd`;spawn 继承服务器进程 token,提权分支(唯一 UAC 来源)不可达。 - **macOS / Linux**(`posix-relaunch`;配置了 `webRestart.scriptPath` 时为 `posix-script`):**无需任何配置即可自重启**——host 生成一次性 `/bin/sh` 脚本:SIGTERM 当前 PID → 最多等 10 秒(超时 SIGKILL)→ `cd` 回原工作目录 → 以原命令(`process.execPath` + `process.argv.slice(1)`)重新执行,输出追加到 `/autostart/dsh-web-restart.log`。若服务器由 supervisor 之类托管,可把 `webRestart.scriptPath` 指向自己的 `.sh` 接管。 client 挂载时探测 `GET /api/restart`,host 回报 `available: false`(不支持的平台)时直接隐藏入口,该平台 `POST` 返回 501。路由:`GET /api/restart`(健康探针,恒 200 + `available`/`mode`/`platform`)与 `POST /api/restart`(触发;重启在途 **409**,配置的脚本不存在或无法自重启 **500** 且带原因,可继续 **202** + 500ms 缓冲后 spawn)。client **只在拿到 202 时进入覆盖层**——其它状态就地显示错误,不再空转 90 秒。恢复检测采用**中断-恢复**:覆盖层仅在观察到探针连续失败 `restartFailThreshold` 次并再次返回 200 后重载;若探针全程可达则报告「未检测到重启」(`noRestart`)直到超时,提供手动刷新。 ### Session log 按钮平移(Session-Log Button Relocation) 遮蔽 `conversation.session.header.utilities` 中的官方条目(同 id `session-log-download`,priority −1,cell shadowing),并在 `conversation.session.header.actions` 注册副本(id `session-log-download-moved`,order 41),复用官方 `sessionLogDownload` controller(`ctx.get('sessionLogDownload')`),下载行为与官方一致。副本对齐 **0.1.6** 的官方形态——「⋯ 更多操作」菜单(单条「下载 Session 日志」)触发共享对话框(文案走本插件自己的 locale 命名空间);它是**冻结的复刻件**:官方改版必须人工同步,官方条目新增菜单项时也要重新核对遮蔽策略。 ### 会话间消息(Peer Messaging) host 平面注册 `send_to_session` / `list_sessions` 工具(按 id 或工作区路径寻址会话、wakeup 投递),并在 `conversation.session.header.actions`(id `copy-session-id`,order 30)与 `conversation.input.left`(id `copy-session-id-input`,order 30)各加「复制会话 ID」按钮。发出消息内容在投递前经 `toPlainText` 转为纯文本,接收方看到整洁文本而非原始 markdown。 ### 提示词去重(Prompt Dedup) 对 **身份 / 全局 / 工作区** 三段系统提示词(段 `session-identity`、`global-prompt`、`workspace-prompt`,order 40/50/60)做**跨段行级去重**。`promptDedup.enabled` 默认开启(仅显式设为 false 时禁用)。按 `\n` 切分,三段内出现过的**完全相同原行**只保留"先出现"那一份(全局 `seen` 贯穿三段,同段内部自重复也收敛),后出现段的重复行被去掉;任何段独有内容一律保留。**空行(含只有空白的行)不参与去重**——空行是 markdown 的段落/列表分隔,把它当成重复行会让第一段之后的每一段空行都被删掉。不解析 `{{name}}` 占位符(单行完整组,按行切分不会切断)、不破坏 markdown、不设 complete,绝不动 harness 自带段(`harness:identity` / `deployment:persona` / 工具段)。机制:在插件根 ctx 订阅 `system-prompt/assemble` waterfall,`await next()` 后对返回结果的 `sections` 做去重再返回。 --- ## 兼容性 插件已对照 **DeepSeek Harness `dsh-v0.1.6-alpha.2`** 验证,其用到的 host 与 client 集成点在该原生版本中均存在;其中两个 0.1.6 才有的能力(`interpolate: false`、`ctx.sessionController.resolveAgent`)各自带兜底,更早内核是降级而非断裂。 - **框架**:`@deepseek-ai/cordis` 4.0.2 与 `@deepseek-ai/schemastery` 3.18.2(即 `dsh-v0.1.6-alpha.2` vendored 的版本)。插件经 cordis harness 加载,并以 `dsh.bundle.patch` 注册为 bundle。 - **Host 服务**(已对照原生源码校验):`ctx.settings.register(ns, schema, { applies: 'live', base })` → scope `{ get / watch(next, prev) / update / replace }`,`get()` 返回 deep-freeze 值;`ctx.systemPrompt.section({ name, order, text, interpolate: false })`;`ctx.agents.{ get, resume({ resumeSessionId, agentOptions, setup }), roots, requireInitiator }`;`ctx.sessionController.resolveAgent(sessionId)`;`session.header` 字段(`cwd`、`origin`、`delegationDepth`、`parentSession`、`agentPreset`;没有 `seedLength`);用于等待晚到可选服务的 `ctx.inject(names, cb)`;`ctx.get('webServer').register({ kind: 'exact', path, handler })`;`@deepseek-ai/dsh-tools` 的 `defineTool` + `tools.register()`;以及 `ctx.get('agentDefaultModel')`、`sessionPersistence`、`sessionTitle`、`workspaceRegistry`、`sessionLogDownload`、`timer`、`on`、`effect`。 - **Client 服务**(已校验):`window.__ModuleLoader__.load({ id, factory })`;`ctx.get('slots')` → `slots.register(meta, render)` / `slots.inject(name, fn)`(**低 priority 遮蔽**);`ctx.get('settingsScope').bind({ namespace })` → `{ getSnapshot()/.value/.status, set(field, value), subscribe }`;`ctx.get('locale')` → `register(ns, { zh, en })` / `bind(ns)`;以及 `timer` client 服务(`ctx.timeout`)。bundle 的运行时 `require` 均解析自模块表种子词(`react`、`react/jsx-runtime`、`@deepseek-ai/dsh-client-store`、`@deepseek-ai/dsh-client-ui-primitives`、……)。 ### 配置校验 插件的 **host 侧** `Config` 在插件加载时即用 schemastery 校验整棵配置树。但 `config.client` **不会**由 cordis 送到浏览器:harness 以 `loader.create({ name })` 创建每个 client 条目,其 boot graph 行只有 `id/url/rev/inject/immediately/external`,而浏览器模块表里没有 `@deepseek-ai/schemastery`,client bundle 也就导不出自己的 `Config`。因此 host 把 `config.client` 作为组合 **base** 镜像进 `session-toolkit-ui` 命名空间,client 半通过 `ctx.get('settingsScope').bind({ namespace: 'session-toolkit-ui' })` 读取。解析顺序 = schema 默认 → `config.client`(base)→ 用户 `settings.yaml`;命名空间不可用时 client 保持冻结的兜底值。 --- ## 架构 - **Host 半** —— `lib/index.js` 组装七个功能模块(`identity.js`、`global-prompt.js`、`auto-resume.js`、`web-restart.js`、`peer-message.js`、`log-reposition.js`、`prompt-dedup.js`)。`inject` 为模块依赖去重并集;每个模块的 `apply` 在 `safe()` 守卫内运行,单个模块失败不影响整包。所有贡献均绑定生命周期(提示词段与 HTTP 路由用 `ctx.effect`,工具随插件 fiber 注册;定时器统一走 `timer` 服务)。`global-prompt.js` 拥有 `global-prompt`、`workspace-prompt`、`workspace-registry-active`、`prompt-file-status` 命名空间、`readPromptFiles` 辅助函数(实时 `fs.readFileSync` 读),以及工作区/活跃工作流投影(`agents.roots()` → 活跃工作区)。 - **Client 半** —— `client/client.js` 为单一 `window.__ModuleLoader__.load` bundle;五个 UI 模块内联在 IIFE 中,在一个 `apply` 里按序注册全部 slot(逐模块守卫)。所有 UI 用 `React.createElement`;样式以 `data-plugin` style 标签注入,使用主题 CSS 变量与深色覆盖;无全局 DOM 操作。global-prompt 模块渲染 **Tabs(全局 / 按工作区)** 页面,并含可复用 `FileRefsPanel`(添加/移除引用文件,经绑定的 `prompt-file-status` scope 显示每文件状态)。 ### 注册的 Slots | Slot | Id | Order / priority | 功能 | |---|---|---|---| | `settings.section` | `global-prompt` | order 30 | 全局 + 工作区提示词页(Tabs) | | `settings.general.item` | `web-restart` | order 90 | 重启入口 | | `conversation.session.header.actions` | `copy-session-id` | order 30 | 复制会话 ID | | `conversation.session.header.actions` | `session-identity` | order 40 | 身份按钮 | | `conversation.session.header.actions` | `session-log-download-moved` | order 41 | Session log 下载 | | `conversation.input.left` | `copy-session-id-input` | order 30 | 复制会话 ID(工具行) | | `conversation.input.left` | `session-identity-input` | order 40 | 身份按钮(工具行) | | `conversation.session.header.utilities` | `session-log-download` | priority −1(遮蔽) | 隐藏官方按钮 | --- ## 配置 ### 设置命名空间 Schema 校验、`applies: live`、持久化于 `settings.yaml`: | 命名空间 | Schema | 说明 | |---|---|---| | `session-identity` | `{ default: {enabled: boolean, text: string}, sessions: Record }` | 解析顺序:会话记录 → 默认 → 空。禁用或空文本不注入。身份文本上限 8000 字符(token 守卫)。 | | `session-auto-resume` | `{ sessions: Record }` | 每会话开关;缺省键视为关闭。 | | `global-prompt` | `{ enabled: boolean, content: string, files: string[] }` | 启用时注入所有会话。`files` 为引用文件列表,组装时读取并追加(按 mtime/大小缓存;读取失败或超限的文件跳过)。 | | `workspace-prompt` | `{ workspaces: Record, removed: string[] }` | 按工作区提示词。某会话会得到与其 `cwd` 目录前缀匹配、路径最深(最具体)且启用的工作区提示词。`removed` 记录用户已移除的路径,使活跃工作区同步不重新补回。 | | `workspace-registry-active` | `{ active: [{path, sessionCount}] }` | 活跃工作区只读投影,来自 **`ctx.agents.roots()`**(各 agent 的 `session.header.cwd` 去重计数)。**不**来自 `workspaceRegistry`(该服务在本插件作用域不可见)。 | | `prompt-file-status` | `{ byScope: Record }` | 各作用域引用文件的最近读取结果只读投影;UI 的 `FileRefsPanel` 读取显示 `ok: N 字符` / `fail: 原因`。**仅在状态变化时写入**(每次写入都会持久化整份 `settings.yaml`)。 | | `session-toolkit-ui` | `{ identityCharLimit, restartTimeoutMs, restartPollMs, restartFillMs, restartFailThreshold, restartSettleMs, copyFeedbackMs }` | host 注册、约定只读的 UI 旋钮命名空间,负责把配置送到浏览器半;`config.client` 是它的组合 base。这是 client bundle 唯一可用的 host→browser 配置通道。 | ### 插件 Config(cordis) 聚合包导出单一 `Config`(schemastery schema),按功能分键。默认值 = 现状;可在 `cordis.yml` / `cordis.patch.yml` 插件行的 `config` 字段覆盖,无需改代码。`config.client` 由 host 校验后作为 **`session-toolkit-ui` 命名空间的 base 层**,浏览器半由此读取(见[配置校验](#配置校验))。 ```yaml - id: session-toolkit name: 'dsh-session-toolkit' config: identity: maxText: 8000 sectionOrder: 40 globalPrompt: sectionOrder: 50 workspaceSectionOrder: 60 maxFileBytes: 262144 # 单个引用文件上限;超限文件跳过并在 UI 报 fail maxTotalBytes: 1048576 # 单个段的全部引用文件合计上限 autoResume: concurrency: 3 webRestart: scriptPath: '' # 可选;缺省推导为 /autostart/dsh-web-restart.cmd spawnDelayMs: 500 promptDedup: enabled: true # 三段(身份/全局/工作区)跨段行级去重开关;默认 true = 开启(仅显式设为 false 时禁用) client: identityCharLimit: 4000 restartTimeoutMs: 90000 restartPollMs: 1000 restartFillMs: 600 restartFailThreshold: 2 restartSettleMs: 8000 copyFeedbackMs: 1600 ``` | 键 | 默认值 | 含义 | |---|---|---| | `identity.maxText` | 8000 | 身份文本截断上限(字符,token 守卫)。UI 软上限为 `client.identityCharLimit`(4000,编辑区限制),**host 硬截断**为本值(8000)。 | | `identity.sectionOrder` | 40 | 身份段在系统提示词中的顺序。**迁移**:显式固定 `identity.sectionOrder: 55` 的用户需改为 40 以保持「身份 → 全局 → 工作区」顺序。 | | `globalPrompt.sectionOrder` | 50 | 全局提示词段的顺序。 | | `globalPrompt.workspaceSectionOrder` | 60 | 工作区提示词段的顺序(置于最后)。 | | `globalPrompt.maxFileBytes` | 262144 | 单个引用文件的字节上限;超限文件跳过(状态 `fail`)而不是阻塞组装。 | | `globalPrompt.maxTotalBytes` | 1048576 | 单个段全部引用文件的合计字节预算。 | | `autoResume.concurrency` | 3 | 启动恢复的最大在途 resume 数。 | | `webRestart.scriptPath` | 推导 | 重启脚本路径。为空(默认)= Windows 推导 `/autostart/dsh-web-restart.cmd`、macOS+Linux 推导 `…/dsh-web-restart.sh`,并在 POSIX 上额外启用**自重启**(无需脚本)。填路径=交给你自己的脚本:POSIX 下经 `/bin/sh` 执行,文件不存在时 `POST` 立即 500,不再让覆盖层空转。 | | `webRestart.spawnDelayMs` | 500 | 202 缓冲后 spawn 重启脚本的延迟。 | | `promptDedup.enabled` | true | 三段(身份/全局/工作区)系统提示词跨段行级去重开关(默认开启,仅显式设为 false 时禁用)。开启时,三段中出现过的**完全相同的非空原行**只保留"先出现"一份(全局 seen 贯穿三段),后出现段的重复行被去掉;**空行永远保留**(它是 markdown 的段落/列表分隔)。任何段独有内容一律保留。不解析 `{{name}}` 占位符、不破坏 markdown、不设 complete,绝不动 harness 自带段。 | 下列 `client.*` 键由 host 校验,并作为 **`session-toolkit-ui` 命名空间的 base 层**——浏览器半真正读取的就是该命名空间。用户也可以直接在 `settings.yaml` 的同名命名空间里覆盖它们。 | 键 | 默认值 | 含义 | |---|---|---| | `client.identityCharLimit` | 4000 | 身份编辑区字符上限(UI 软上限;全局提示词编辑区同用)。 | | `client.restartTimeoutMs` | 90000 | 重启覆盖层超时(之后提示手动刷新)。 | | `client.restartPollMs` | 1000 | 重启健康轮询间隔(也是进度 tick)。 | | `client.restartFillMs` | 600 | 检测到恢复后的进度填充动画时长。 | | `client.restartFailThreshold` | 2 | 判定中断前的连续健康轮询失败次数。 | | `client.restartSettleMs` | 8000 | 检测到恢复后、自动刷新前的稳定窗口(ms)。DSH 会话标题由 **LLM 异步生成**、无就绪信号,此值是"重启后首轮 reload 的等待窗口",用于改善标题 fallback(显示为工作区名)。若个别会话标题仍显示工作区名,可手动刷新或调大该键;根治需 DSH 提供"标题就绪"信号(建议向 DSH 反馈)。 | | `client.copyFeedbackMs` | 1600 | 复制反馈对勾时长。 | --- ## 部署 安装到任意 profile(bundle 层;单一来源,无副本): ```powershell # 来自 npm dsh plugin --profile web add dsh-session-toolkit # 来自 GitHub dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit # 来自本地 checkout / tarball dsh plugin --profile web add ./dsh-session-toolkit-.tgz ``` 包的 `dsh.bundle.patch`(`cordis.patch.yml`)将单一入口(`id: session-toolkit`,`name: 'dsh-session-toolkit'`)注册为 **bundle 层**——在 `dsh-base` / `dsh-web-app` 之后、profile patch 层之前应用(层序:bundles 依次 → profile patch → home patch → `--patch` 覆盖)。 卸载:`dsh plugin --profile web remove dsh-session-toolkit`。 ### 本地开发 迭代源码时可安装 checkout(`dsh plugin --profile web add <源码路径>`,使用 pnpm `link:` 依赖),或手工 junction 到 profile 的 `node_modules` 并在 profile 的 `cordis.patch.yml` 显式 `- insert:` 注册。推荐使用官方 `dsh plugin add` 流程。 验证门(仅限源码 checkout 内运行——`scripts/` 不随发布包分发;无需构建步骤,也无需安装依赖): ```powershell pnpm check # 语法门 —— 对全部随包 JS 跑 node --check pnpm verify # 另加打包契约 —— 入口可达、import 声明完整、双语 README 版本一致 ``` `pnpm verify` 断言「工作区内容 == 包内容」,因此一旦有人给 `package.json` 加上 `prepare`/`prepack`/`prepublishOnly` 脚本,它会**故意报错**。升级 harness 时使用的 DSH 集成点清单见 `docs/agents/integration-contracts.md`。 ### 分享与安装 已发布至 **npm**(`dsh-session-toolkit`,v0.1.8,MIT)并同步至 **GitHub**(`github.com/Han-Yao94/dsh-session-toolkit`)。纯 JS 包——**无构建步骤、无 prepare 脚本**。`files` 已白名单 `lib/`、`client/`、`cordis.patch.yml` 与 README。 - **npm**:消费者 `dsh plugin --profile web add dsh-session-toolkit` 安装;新版本通过 `npm publish`(或 `pnpm publish`)发布。 - **GitHub**:`dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit`。 - **tarball**:`pnpm pack` → `dsh plugin --profile web add ./dsh-session-toolkit-.tgz`。 运行时依赖(`@deepseek-ai/schemastery`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-home-paths`)声明在 `dependencies`,随安装自动拉取;平台模块(`react`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-store`、`@deepseek-ai/dsh-client-ui-primitives`)为 `peerDependencies`,由 DSH 宿主提供。harness 提供的包一律写成**前置版本并集**——`^0.1.2-alpha.5 || ^0.1.6-alpha.2`:caret 区间不跨 minor,`^0.1.2-alpha.1` 永远匹配不到 `0.1.6-alpha.2`,这正是 §F 偏斜的根因(插件拿到自己的旧副本、宿主在跑新版本)。`@deepseek-ai/dsh-client-ui-slots` 刻意不声明:`slots` 服务由 web shell 播种,npm peer 声明是死重。已验证:打包 tgz 的干净安装可完整解析所有 import(不依赖本地 junction)。另有两条**安装侧**工具(需要外部 checkout/profile,因此不挂 CI,见契约表 §D/§E/§F): ```bash node scripts/dependency-skew.measure.mjs --profile /profiles/web # §F:期望 SKEW_COUNT=0 node scripts/dsh-log-ui.drift.mjs --harness # §E:复刻件漂移 ``` --- ## 模型体验 ### 系统提示词贡献 #### 模型看到的内容 每次组装贡献三个段,顺序:`session-identity`(order 40)→ `global-prompt`(order 50)→ `workspace-prompt`(order 60),位于部署 persona 之后、工具引导(100–199)之前。身份段在组装时按 agent(`AssembleContext.agent`)从 `session-identity` 设置解析,subagent(`origin`/`delegationDepth`)跳过。工作区段为 `cwd` 前缀匹配到配置工作区(取路径最深/最具体且启用者)的会话注入该工作区提示词,否则为空。 全局段与工作区段都会在提示词文本后追加其**引用文件内容**:每次组装读取 `files`(UTF-8,按 mtime/大小缓存),按原文拼接(段声明 `interpolate: false`,内容不被改写)。无法读取或超出字节预算的文件会**跳过**(其内容不注入),但其读取状态被记录供 UI 显示。空段在渲染时删除。 #### Token 影响 启用时三个段的文本随每次请求重复。全局提示词作用于所有会话;身份文本仅作用于能解析到它的会话(自身记录或默认);工作区文本仅作用于 `cwd` 前缀匹配到已启用且已配置工作区(取最具体)的会话。引用文件的完整内容会加入实际提示词,因此消耗额外 token——大引用文件会显著增加每次请求的 token 成本。身份文本上限 8000 字符(token 守卫)。 #### KV Cache 影响 设置不变时各段渲染文本是请求前缀的固定部分;修改会话身份或全局/工作区提示词(或编辑/新增引用文件)可能从首个变化 token 起使提供方缓存复用失效(与官方 persona 段语义一致)。 ### 工具面 `send_to_session` 与 `list_sessions` 在 host 平面注册,所有会话可见(subagent 经常驻 preset 组装继承)。参数与返回均为 JSON 兼容。 --- ## 机制与红线 - **身份注入** 使用单一全局段、text 提供方按 agent 求值——无逐 agent 注册、无生命周期开销、设置变更实时生效。 - **frozen 设置铁律(红线)** —— DSH 的 `ctx.settings.register(...).scope.get()` 返回的 value 被 **`deepFreeze` 冻结(不可变)**。任何 host 写 scope 前,必须先将对象 **`{ ... }`(数组 `.slice()`)拷贝成可变对象,再用 `update()`**(register scope 只有 `get`/`watch`/`update`/`replace`,**无 `set`**);直接改冻结对象会抛 `object is not extensible`(正是此处修复的「工作区列表空」根因)。client 端用 `settingsScope.bind().set(field, value)`(client scope 支持 `set`)。同一 `{ ... }` 拷贝规则适用于 client 对 `workspace-prompt` 的写入(`onWsFilesChange` / `save` / `saveWsEnabled` / `removeWorkspace`)。 - **引用文件读取、失败跳过** —— `readPromptFiles` 在每次组装的 `text()` 内运行(stat 判定是否重读);读取失败或超限的文件不会中断组装,其状态被记录到 `prompt-file-status` 供 UI 显示,且只在状态变化时写回。 - **自动上线绝不调用 `dispose()`** —— `AgentHandle.dispose()` 会从存储移除会话;关闭开关只影响下次重启,绝不下线当前会话。 - **重启零 UAC 是构造性保证** —— spawn 继承服务器进程 token(SYSTEM 或用户),`taskkill` 目标是同权限进程,脚本提权分支(唯一 UAC 来源)不可达。若 3080 被其他程序占用,仍可能出现提权重试(重启脚本中有说明)。 - **遮蔽基于 cell shadowing** —— utilities 条目以更低 priority 重注册官方 `session-log-download` cell;遮蔽崩溃时官方条目优雅 abdicate 回退。 - **纯文本转换** —— `toPlainText`(10 条规则、代码围栏状态机、宽松匹配)仅在发送时执行;消息结构与 `source: { kind: 'user' }` 不变。 --- ## 已知限制与暂缓事项 - client 半为手工维护的单文件 IIFE 包;新增功能需同步维护 `lib/` 与 `client/client.js` 两处。 - 平移的 Session log 入口依赖官方 `sessionLogDownload` controller 接口,且复刻官方 0.1.6 的「⋯ 更多操作」菜单形态;**它是冻结的复刻件**:DSH 升级后跑一次 `node scripts/dsh-log-ui.drift.mjs --harness `——它按同一组锚点双向审计,漂移即非零退出(§E)。 - `toPlainText` 宽松斜体匹配可能误删非格式位置的成对 `*`(如 `a * b * c`);对 agent 生成消息可接受,边界收紧为可选优化。 - 聚合 `inject` 并集会等待所列全部服务;某 profile 缺一服务会拖慢整包 apply(web profile 当前齐备)。 - harness 提供的依赖区间是前置版本并集;改完区间必须重跑 `pnpm install`,并在装好的 profile 上跑 `node scripts/dependency-skew.measure.mjs --profile /profiles/web`(期望 `SKEW_COUNT=0`;`DE-INSTANCE` 表示同版本不同实例,§F 判定为可接受)。 - `ctx.get('agentDefaultModel')`、`sessionTitle`、`workspaceRegistry` 改为调用时惰性解析,缺失时降级为 cwd/路径寻址;`tools` 与 `webServer` 改用 `ctx.inject` 等待就绪——loader 并发创建条目,apply 时刻的 `ctx.get` 没有顺序保证,晚到会让功能永久静默消失。 - **重启探测窗口** — 仅在健康探测连续失败 `restartFailThreshold × restartPollMs`(默认 2 × 1000 ms = 2 s)后恢复时判定为重启。若 relaunch 在该窗口内完成,覆盖层可能误报「未检测到重启」(`noRestart`);调低 `restartFailThreshold` 到 1 虽更灵敏,也会让单次瞬时失败被误判为重启中断。 - **引用文件在组装路径预热** —— `readPromptFiles` 每次组装对每个引用文件做一次 `statSync`,仅在 mtime/大小变化时读盘;单文件与合计字节预算避免超大文件阻塞组装或撑爆提示词,状态投影也只在变化时写入。client 端 `files` 即时保存(`onWsFilesChange` / `save`)。 - **UI 旋钮来自 `session-toolkit-ui`** —— 因 harness 不给 client 条目任何 config 通道、也不提供 schemastery,浏览器半无法直接接收/校验 `config.client`;host 把它镜像进 settings 命名空间(见[配置校验](#配置校验)),命名空间不可用时 client 回落到冻结默认值。 --- ## 恢复方法 卸载 bundle:`dsh plugin --profile web remove dsh-session-toolkit`,然后重启 GUI。要回退到整合前的布局,请重新启用原插件而非安装本包。