整合工具包:会话身份、全局/工作区提示词(含引用文件)、会话自动上线、免 UAC 网页重启、会话间消息。
安装
# npm 包(预构建)
dsh plugin --profile web add dsh-session-toolkit
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness 的整合插件工具箱。将先前 5 个独立的本地插件——会话身份、全局提示词、会话自动恢复、Session log 按钮平移、会话间消息——合并为单个可安装包(官方 bundle 形态,dsh.bundle.patch),通过 dsh plugin add 安装;另含提示词去重(Prompt Dedup)功能。
当前版本:1.0.0,已对照 DeepSeek Harness dsh-v0.2.0-rc.1 验证——它同时是最低支持版本(契约面 + 全套门,非完整功能回归):0.1.6 及更早会响亮失败而非静默降级(见兼容性)。
安装
安装到任意 profile(bundle 层;单一来源,无副本):
# 来自 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-<version>.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。
你安装的是什么
已发布至 npm(dsh-session-toolkit,最新已发布版本 v1.0.0,MIT)并同步至 GitHub(github.com/Han-Yao94/dsh-session-toolkit)。纯 JS 包——无构建步骤、无 prepare 脚本。files 已白名单 lib/、client/、cordis.patch.yml 与 README。
npm 上就是当前版本。
1.0.0已发布,因此dsh plugin --profile web add dsh-session-toolkit即可得到上文介绍的会话管理工具(create_session/rename_session)及其余全部功能。0.1.9与0.1.10打过 tag 但从未进入 npm;0.1.11是0.1.8之后第一个真正发布的版本。从本 checkout 或 GitHub 安装(dsh plugin --profile web add github:Han-Yao94/dsh-session-toolkit)等价。
- 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-<version>.tgz。
运行时依赖(@deepseek-ai/schemastery(下限 ^3.18.4——volatile() 自 3.18.3 起才有)、@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.2.0-rc.1:caret 区间不跨 minor,而 semver 还额外要求"比较器里必须有一个指明候选人自身 major.minor.patch 的前置版本",这正是当初要写成 ^0.1.2-alpha.5 || ^0.1.6-alpha.2 并集的原因,也正是解析偏斜的根因(插件拿到自己的旧副本、宿主在跑新版本)。既然 dsh-v0.2.0-rc.1 已是最低支持版本,旧的并集成员已删除:今后每次 harness 换代都必须同步抬高这些区间,而针对已装 profile 做一次依赖解析偏斜测量(期望 SKEW_COUNT=0)就是告诉你该抬了的那个检查。区间只在安装期生效——重新安装并重启 DSH 客户端之后再量。@deepseek-ai/dsh-client-ui-slots 刻意不声明:slots 服务由 web shell 播种,npm peer 声明是死重。已验证:打包 tgz 的干净安装可完整解析所有 import(不依赖本地 junction)。另有两条安装侧检查(需要外部 checkout/profile,因此不挂 CI):针对已装 profile 的依赖解析偏斜测量,与针对 harness checkout 的冻结复刻件漂移审计。两者都在维护者的工作副本里。
本地开发
迭代源码时可安装 checkout(dsh plugin --profile web add <源码路径>,使用 pnpm link: 依赖),或手工 junction 到 profile 的 node_modules 并在 profile 的 cordis.patch.yml 显式 - insert: 注册。推荐使用官方 dsh plugin add 流程。
维护者的验证门——对全部随包 JS 跑语法检查,另加打包契约(入口可达、import 声明完整、双语 README 版本一致)——针对源码 checkout 运行。它不在本仓库里,也不随发布包分发:本仓库跟踪的是插件源码与其文档——lib/、client/、cordis.patch.yml、两份 README 加 README.i18n.yaml、LICENSE、package.json 与 .gitignore;scripts/ 与 .github/ 有意不入库。该门断言「工作区内容 == 包内容」,因此一旦有人给 package.json 加上 prepare/prepack/prepublishOnly 脚本,它会故意报错。两条汇总命令(pnpm check、pnpm verify)只覆盖语法检查与这份打包契约——都不跑任何判据门;判据门一律逐门单跑,各自带自己的退出码约定。
功能
两族:提示词层(模型看到什么)与会话管道(驱动会话的工具与界面按钮)。
提示词层
会话身份(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 注入一段作用于所有会话系统提示词的文本;按工作区 Tab 注入按工作区提示词;组 Tab 注入按组提示词(见组提示词)。这三者、会话身份与入站 peer 消息纪律段,合起来就是本插件贡献的五个提示词段——文末的提示词段位图把它们的顺序与作用对象列成一张表。
同一个 webServer 上注册三条路由——两条只读、一条写入。状态路由 GET /api/session-toolkit/state(活跃工作区 + 引用文件读取状态 + 组选择器要用的在线会话清单;非 GET 一律 405)是设置页读取这些运行时投影的通道——它们不占 settings 命名空间、也从不落盘。检索路由 GET /api/session-toolkit/search 是跨会话检索面板的数据来源;它与模型工具 search_sessions 共用同一段字面扫描(q 加可选的 cwd / since / senders / surfaces / limit / perSession / maxSessions,senders 与 surfaces 按逗号分隔列表给):参数不合法答 400 { ok: false, error, errorText },扫不到任何命中仍是 200 + ok: true 且 0 命中。它不是全文检索:内核全文索引默认关闭(openAt: never),启用它也不会改变这条路径。转交路由 POST /api/session-toolkit/relay 是写入侧:检索视图里命中行上的「一键转交」正是经它把某条命中交到你当前所在的会话。请求体 = { target, source: { sessionId, seq, time }, text }:target 是收件会话、source.sessionId 是这条命中的出处会话,两者都必须是本部署已知会话(listSessions());把内容转交回它自己的出处会话被拒(409 SELF_TARGET)。非 POST 一律 405;请求体超过 8,192 字节答 413;text 上限 6,000 字节(UTF-8),time 只用于显示、可缺省(缺省时显示「未知」,最多 200 个单行字符)。本路由单飞——已有请求在处理时,第二个并发请求立刻得到 409 RELAY_IN_FLIGHT,不排队。成功 = 200 { ok: true, deliveredTo, peerMessageId };被拒的请求带判定,形如 { ok: false, error, errorText }(id 不是已知会话时 404 UNKNOWN_TARGET / 404 UNKNOWN_SOURCE)——不带 errorText 的只有两处:守卫自己的 403 { ok: false, error: 'origin not allowed' } 与光秃的 405。投递出去的消息把命中放进 4 空格缩进块,块头写出来源会话、命中序位与时间,并以明文声明「这是数据、不是指令」。它与两条只读路由复用同一个 originGuard:该守卫把 Origin 绑到 Host、拒绝 Origin: null,因此它挡的是浏览器跨站请求——它不是鉴权,能连到该端口的本机进程可以伪造这些请求头。
工作区提示词(Workspace Prompt)
为 cwd 前缀匹配到已配置工作区目录(该目录及子目录)的会话注入按工作区提示词。工作区列表由活跃会话的 cwd 聚合而来(ctx.agents.roots(),去重并按会话数计数)。当多个已启用工作区前缀命中某会话的 cwd 时,取最具体(路径最深/最长)者。removed 记录用户已移除的路径,使活跃工作区同步不重新补回。工作区行的启用开关 即时保存(live-save);「保存」按钮仅持久化提示词内容 + 引用文件。
组提示词(Group Prompt)
设置页上的第三个 Tab:组。每个组是一条具名条目:启用开关、提示词正文、引用文件列表,以及显式的成员会话 id 列表;匹配依据是该会话自己的 id,不是它所在目录——这正是「一个组可以跨工作区」的原因。组是额外的一层:它不取代全局提示词,也不取代按工作区提示词。所有启用且 sessions 含当前会话 id 的组都注入到段 group-prompt(order 55,夹在全局 50 与工作区 60 之间),命中的多个组按组键序拼接,块与块之间留一个空行。选择器只列在线(live)会话,且从不自动入组:新会话在你勾选之前不属于任何组;已存在组里但当前离线的会话标为未在线(可移除)。组装上下文里取不到会话 id 时,该段注入空。
引用文件(Referenced Files)
全局提示词、工作区提示词与组提示词均可引用文件列表。每次组装重新读取每个引用文件(UTF-8;按 mtimeMs + 大小缓存,未变化的文件不重复读盘),注入到提示词文本之后。有字节预算(globalPrompt.maxFileBytes / maxTotalBytes,默认 256 KiB / 1 MiB):超限文件跳过而不是阻塞组装。读取失败同样跳过,两种情况都在 UI 中显示具体原因。支持纯文本/markdown。每个文件的读取状态是 host 的运行时投影,经只读路由 GET /api/session-toolkit/state 送到 UI(ok:N 字符 / fail:原因 / 未读取),浏览器半只在设置页打开期间轮询它;状态不写入任何配置文件,内容未变时不重建投影。
提示词去重(Prompt Dedup)
对 身份 / 全局 / 组 / 工作区 四段系统提示词(段 session-identity、global-prompt、group-prompt、workspace-prompt,order 40/50/55/60)做跨段行级去重。promptDedup.enabled 默认开启(仅显式设为 false 时禁用)。按 \n 切分,四段内出现过的完全相同原行只保留"先出现"那一份(全局 seen 贯穿四段,同段内部自重复也收敛),后出现段的重复行被去掉;任何段独有内容一律保留。空行(含只有空白的行)不参与去重——空行是 markdown 的段落/列表分隔,把它当成重复行会让第一段之后的每一段空行都被删掉。不解析 {{name}} 占位符(单行完整组,按行切分不会切断)、不破坏 markdown、不设 complete,绝不动 harness 自带段(harness:identity / deployment:persona / 工具段)。机制:在插件根 ctx 订阅 system-prompt/assemble waterfall,await next() 后对返回结果的 sections 做去重再返回。
提示词段位图
| 段 id | 顺序 | 注入对象 | 说明 |
|---|---|---|---|
session-identity |
40 | 能解析到身份的会话(自身记录,否则默认身份) | 每次组装按 agent(AssembleContext.agent)求值;subagent(origin / delegationDepth)跳过 |
peer-inbox-discipline |
45 | 每个非 subagent 会话 | 固定的入站 peer 消息纪律提醒(见会话间消息);自身没有设置项 |
global-prompt |
50 | 所有会话 | |
group-prompt |
55 | 自身 id 列在某个已启用组里的会话 | 命中的多个组按组键序拼接,块间一个空行;无命中注入空 |
workspace-prompt |
60 | cwd 前缀匹配到已启用工作区的会话 |
最具体(匹配路径最长)者胜;否则为空 |
五个提示词段都以 interpolate: false 注册:提示词文本与引用文件里的 {{...}} 一律按字面保留,用户内容永不被改写,未注册的 {{name}} 也不可能让组装失败。0.1.6 之前的内核没有分段的 interpolate 开关,由 lib/prompt-literal.js 在组装结果上退化为 { 连续串空格化。
会话管道
会话自动恢复(Session Auto-Resume)
开启开关的会话在 DSH 客户端重启后自动恢复,优先走官方恢复链路(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)。并发受限(缺省 3,可由 autoResume.concurrency 配置),逐项失败隔离 + 在途集合防重复恢复。
会话管理(Session Admin)
host 平面另注册两个工具,与 send_to_session / list_sessions 同平面:
create_session—— 自主创建一个新的顶层会话(DSH 客户端左侧导航里的一个聊天窗口)。cwd与prompt均必填:cwd必须是绝对路径(无cwd的会话不会进宿主列表),prompt是新会话的首条消息。创建成功即产生一条真实用户消息(会真实跑一轮模型、消耗一次调用);按内核设计,产生过事件的会话会被持久化,因此本工具不提供「只登记、不说话」的临时会话。可选title会立即设定标题并 pin 住。返回体含sessionId、cwd、status、title与notes。rename_session—— 修改一个**在线(live)**会话的标题。改名会 pin 住标题,不再被自动标题生成覆盖。目标必须是顶层会话且当前在线:目标是子会话(origin=subagent或delegationDepth>0)时明确拒绝,不静默改写。
两者都以结构化结果返回(工具执行本身不抛未捕获异常):成功 { ok: true, … },失败 { ok: false, error: '<码>', errorText: '<原始原因>' }。错误码:MODEL_UNAVAILABLE / MODEL_SELECTION_FAILED / MODEL_SELECTION_INVALID / EMPTY_CWD / CWD_NOT_ABSOLUTE / EMPTY_PROMPT / PROMPT_TOO_LONG / PRESET_RESOLVE_FAILED / CREATE_FAILED / CREATE_UNAVAILABLE / CREATE_NO_AGENT / EMPTY_TARGET / EMPTY_TITLE / SESSION_UNAVAILABLE / TARGET_IS_SUBAGENT / TITLE_SERVICE_UNAVAILABLE / UNEXPECTED。
(prompt 缺失由内核工具参数校验在框架层拒绝——内核把它转成工具错误结果,该异常不经本插件代码;prompt 传了但纯空白才由本插件返回 EMPTY_PROMPT。两者都不创建会话。)
两处如实声明:
- 可见性未在工具内验证 —— 「带
prompt建出的会话会出现在左侧导航」取决于内核是否真正开跑一轮(导航按「空白会话」判据过滤,该状态只在turn/start时翻转;followup只是入队并唤醒驱动)。因此返回体不下「已出现在导航」的结论,notes会写明这一点。 - preset 降级 ——
agentPresets服务存在但默认 preset 解析失败时,工具返回PRESET_RESOLVE_FAILED,而不是交付一个没有 preset 的残缺会话;服务整体缺失属合法降级,会话照建并带说明性notes。
会话间消息(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。wakeup 有两条不同的投递路径:wakeup: true(走 steer)时,目标正在执行 ⇒ 消息进当前轮的下一个步边界(既不新开一轮,也不打断当前步);目标空闲 ⇒ 立刻开一轮;wakeup: false(走 inject)只投递、不唤醒任何人。两种情形仍会排队而不是插入:投递时目标空闲(没有步边界可插,消息在下一个轮次边界被收下),以及内核的 wakingAfterAbort 重分类(上一个活动正在被取消时,steer 被静默降级为 next-turn)。因此 steer 不是「消息永不等待」的承诺——判断一条消息是否真的插入,要看三件事同时成立:target 是 next-step、投递时有轮在跑、且投递到消费之间没有新的 turn/start。
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 命名空间);它是冻结的复刻件:官方改版必须人工同步,官方条目新增菜单项时也要重新核对遮蔽策略。
跨会话检索(Cross-Session Search)
search_sessions 是第六只模型工具:跨会话的字面内容检索(大小写不敏感、空白灵活),回答「这件事我们在哪聊过」,底座是宿主的 sessionQuery 服务。流程 = 列出会话 → 按 cwd / since 过滤 → 对每个会话提取出的语义文本做字面匹配 → 返回会话标题、事件坐标(sessionId + seq —— 宿主会话读取工具正是吃这两个值;本包自身不暴露读取工具)与文本片段。任何失败都以结构化 { ok: false, error, errorText } 返回(后者是人读的错误说明),绝不向调用方会话抛异常。
senders 把命中收窄到「指定的一组会话产生的那些」:数组元素 = 来源会话 id(harness 写出的两种形态 session-<uuid> 或裸 uuid 均可),或两个生产者 kind(agent-message / subagent-settled)之一(按规范化去重、上限 10;其余取值——未知 kind、空数组、非字符串元素——一律答 INVALID_SENDERS)。它是近似过滤,结果会自己说明这一点:只有 current 面的命中能判定其生产者,因此其它面的命中被保留并把 sender 记为 null、计入 sendersUnfiltered;current 面上没有生产者会话的命中(用户发言、注入的上下文)被丢弃、计入 senderlessExcluded;sendersSkippedSessions 与 sendersPartial 报告在扫描时间预算内未能索引的会话。这些键只在显式传 senders 时出现——不传时返回体与引入该参数之前逐字节相同。
事件面(surface)是这里最该知道的一点。 默认返回 current(仍在会话有效模型上下文里的文本)与 shadowed(被后续快照取代、或被上下文压缩挤出去的文本)两侧的命中 —— 你要「找回」的东西通常正躺在 shadowed 里;log-only(从未上过任何面的原始日志记录)默认排除,除非显式传 surfaces。扫描始终是字面的:它走宿主的 filterEvents,不经过 FTS 索引;因此是否启用索引对本工具没有影响。sessionQuery 服务缺失时本模块不注册任何东西,包内其余功能照常工作。
扫描预算:默认最多 50 个会话 / 每个会话 5 条命中 / 总共 20 条命中(maxSessions、perSession、limit 可提高,硬顶 200 / 20 / 100);被截断时 truncated.sessions / truncated.matches 为 true;空结果是 ok: true 且命中 0 条,不是失败。
配置
设置字段(条目 config 的 volatile 部分)
条目 id 固定为 session-toolkit;下面这些 volatile() 字段就是设置页读写的那份数据,schema 校验后落盘当前 profile 的 cordis.patch.yml。字段名即 config 路径(identity.sessions 等),浏览器半经 configForms.get('session-toolkit') 用同样的路径读写。
| 字段 | Schema | 说明 |
|---|---|---|
identity.default |
{enabled: boolean, text: string} |
默认身份。解析顺序:会话记录 → 默认 → 空。禁用或空文本不注入。 |
identity.sessions |
Record<sessionId, {enabled, text}> |
每会话身份。身份文本上限 identity.maxText(8000 字符,token 守卫)。 |
autoResume.sessions |
Record<sessionId, boolean> |
每会话「重启后自动上线」开关;缺省键视为关闭。 |
globalPrompt.{enabled,content,files} |
{enabled: boolean, content: string, files: string[]} |
启用时注入所有会话。files 为引用文件列表,组装时读取并追加(按 mtime/大小缓存;读取失败或超限的文件跳过)。 |
workspacePrompt.workspaces |
Record<path,{enabled, content, files: string[]}> |
按工作区提示词。某会话会得到与其 cwd 目录前缀匹配、路径最深(最具体)且启用的工作区提示词。 |
workspacePrompt.removed |
string[] |
用户已移除的路径,使活跃工作区同步不重新补回。 |
groupPrompt.groups |
Record<groupKey,{enabled, content, files: string[], sessions: string[]}> |
具名组(字典键即组名)。enabled 且其 sessions 含当前会话 id 时注入该组提示词(段 group-prompt,order 55)——成员资格按会话 id 判定,所以一个组可跨工作区;组是额外的一层,全局与按工作区提示词照常生效。v1 只列在线会话,且从不自动入组。 |
client.* |
2 个 UI 旋钮(见下表) | 浏览器半的运行时参数(字符上限、复制反馈)。 |
运行时投影(不落盘、不属于 config):活跃工作区 [{path, sessionCount}] 来自 ctx.agents.roots()(各 agent 的 session.header.cwd 去重计数;不来自本插件作用域不可见的 workspaceRegistry),组选择器要用的在线会话清单 [{id, cwd, title}](标题取自可选服务 sessionTitle,服务缺失或抛错时为 null——读标题绝不能让路由失败),引用文件读取状态为 Record<global\|path, [{filePath, status: 'ok'\|'fail', charCount?, reason?}]>;两者都经 GET /api/session-toolkit/state 提供给设置页。
插件 Config(cordis)
聚合包导出单一 Config(schemastery schema),按功能分键。默认值 = 现状;可在 cordis.yml / cordis.patch.yml 插件行的 config 字段覆盖,无需改代码。带 .volatile() 的分键(用户数据 + client.*)就是设置页读写的那份数据,浏览器半经 configForms.get('session-toolkit') 读到同一份解析结果(见配置与设置数据面)。
- id: session-toolkit
name: 'dsh-session-toolkit'
config:
identity:
maxText: 8000
sectionOrder: 40
default: # volatile:默认身份
enabled: false
text: ''
sessions: {} # volatile:Record<sessionId, {enabled, text}>
globalPrompt:
sectionOrder: 50
workspaceSectionOrder: 60
maxFileBytes: 262144 # 单个引用文件上限;超限文件跳过并在 UI 报 fail
maxTotalBytes: 1048576 # 单个段的全部引用文件合计上限
enabled: false # volatile:全局提示词开关
content: '' # volatile:全局提示词正文
files: [] # volatile:引用文件列表
workspacePrompt: # volatile:按工作区提示词
workspaces: {} # Record<path, {enabled, content, files}>
removed: [] # 用户已移除的路径
groupPrompt:
sectionOrder: 55
groups: {} # volatile:Record<groupKey, {enabled, content, files, sessions}>
autoResume:
concurrency: 3
sessions: {} # volatile:Record<sessionId, boolean>
promptDedup:
enabled: true # 四段(身份/全局/组/工作区)跨段行级去重开关;默认 true = 开启(仅显式设为 false 时禁用)
client:
identityCharLimit: 4000 # volatile:以下 2 键都由浏览器半读取
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 | 工作区提示词段的顺序(置于最后)。 |
groupPrompt.sectionOrder |
55 | 组提示词段的顺序(夹在全局 50 与工作区 60 之间;命中的多个组按组键序拼接,块间留一个空行)。 |
globalPrompt.maxFileBytes |
262144 | 单个引用文件的字节上限;超限文件跳过(状态 fail)而不是阻塞组装。 |
globalPrompt.maxTotalBytes |
1048576 | 单个段全部引用文件的合计字节预算。 |
autoResume.concurrency |
3 | 启动恢复的最大在途 resume 数。 |
promptDedup.enabled |
true | 四段(身份/全局/组/工作区)系统提示词跨段行级去重开关(默认开启,仅显式设为 false 时禁用)。开启时,四段中出现过的完全相同的非空原行只保留"先出现"一份(全局 seen 贯穿四段),后出现段的重复行被去掉;空行永远保留(它是 markdown 的段落/列表分隔)。任何段独有内容一律保留。不解析 {{name}} 占位符、不破坏 markdown、不设 complete,绝不动 harness 自带段。 |
identity.default / identity.sessions |
空 | 用户数据(volatile):默认身份与每会话身份。设置页「会话身份」写入;也可直接写 profile patch。 |
globalPrompt.enabled / .content / .files |
off / 空 | 用户数据(volatile):全局提示词开关、正文、引用文件列表。 |
workspacePrompt.workspaces / .removed |
空 | 用户数据(volatile):按工作区提示词与「已移除路径」。活跃工作区同步会把新出现的路径补进 workspaces(经 ctx.get('settings').update),removed 里的路径不会被补回。 |
autoResume.sessions |
空 | 用户数据(volatile):每会话「重启后自动上线」。false→true 立即恢复该会话。 |
下列 client.* 键由 host 校验;浏览器半经 configForms.get('session-toolkit') 读的就是这几个字段(表单不可用时回落 client/client.js 里的 UI_FALLBACK,值等于历史默认值)。设置页「插件」条目里可以直接改它们。
| 键 | 默认值 | 含义 |
|---|---|---|
client.identityCharLimit |
4000 | 身份编辑区字符上限(UI 软上限;全局提示词编辑区同用)。 |
client.copyFeedbackMs |
1600 | 复制反馈对勾时长。 |
从旧 settings.yaml 迁移(0.1.6 → 0.1.7)
0.1.6 及更早,本插件的用户数据放在 <DSH_HOME>/settings.yaml 的命名空间里;0.1.7 的 harness 启动时会把该文件改名为 settings.yaml.imported,并只把「节名 == 某个存活条目 id」的节导入该条目——本插件的旧命名空间(session-identity 等)不匹配任何条目 id,因此它们被拒收、原样留在 settings.yaml.imported 里。
迁移映射(旧节 → 新 config 路径),一次搬完即可:
旧 settings.yaml 节 |
新 config 路径 |
|---|---|
session-identity.default / .sessions |
identity.default / identity.sessions |
global-prompt.{enabled,content,files} |
globalPrompt.{enabled,content,files} |
workspace-prompt.{workspaces,removed} |
workspacePrompt.{workspaces,removed} |
session-auto-resume.sessions |
autoResume.sessions |
session-toolkit-ui.* |
client.*(默认值相同,通常无需搬) |
workspace-registry-active、prompt-file-status |
丢弃:它们是运行时投影,现在由 GET /api/session-toolkit/state 提供 |
两种落地方式,选一种:
设置页:打开「插件」里
dsh-session-toolkit条目的设置页,把旧值贴进对应字段(表单会写进 profile patch)。直接写 profile patch(适合批量搬运):在
<DSH_HOME>/profiles/<profile>/cordis.patch.yml追加一个- id: session-toolkit条目,把上表右侧的路径放进config:。写之前先用本插件自己的Config校验一遍即可避免形状错误:import plugin from 'dsh-session-toolkit' // 或直接 import 仓库的 lib/index.js plugin.Config(migratedConfig) // 抛错即形状不对
架构
- Host 半 ——
lib/index.js组装九个模块(identity.js、global-prompt.js、auto-resume.js、peer-message.js、session-admin.js、log-reposition.js、prompt-dedup.js、prompt-literal.js、session-search.js)。inject为模块依赖去重并集;每个模块的apply在safe()守卫内运行,单个模块失败不影响整包。所有贡献均绑定生命周期(提示词段与 HTTP 路由用ctx.effect,工具随插件 fiber 注册;定时器统一走timer服务)。global-prompt.js拥有globalPrompt/workspacePrompt/groupPrompt三组 volatile 字段的读取、三个提示词段(global-promptorder 50 /workspace-promptorder 60 /group-promptorder 55)、readPromptFiles辅助函数(实时fs.readFileSync读;缓存按段命名空间隔离,一段的剔除不会误删另一段的条目)、运行时投影聚合(活跃工作区 + 在线会话清单,经GET /api/session-toolkit/state送出),以及把新出现的工作区路径经ctx.get('settings').update('session-toolkit', …)补进条目 config。 - Client 半 ——
client/client.js为单一window.__ModuleLoader__.loadbundle;五个 UI 模块内联在 IIFE 中,在一个apply里按序注册全部 slot(逐模块守卫)。所有 UI 用React.createElement;样式以data-pluginstyle 标签注入,使用主题 CSS 变量与深色覆盖;无全局 DOM 操作。global-prompt 模块渲染 Tabs(全局 / 按工作区 / 组) 页面,并含可复用FileRefsPanel(添加/移除引用文件;每文件状态来自GET /api/session-toolkit/state的轮询投影)与组编辑器(组的新增/改名/删除、启用开关、正文、引用文件,以及会话选择器:在线会话按title ?? 短 id列出,已存但离线的成员标未在线)。
注册的 Slots
| Slot | Id | Order / priority | 功能 |
|---|---|---|---|
settings.section |
global-prompt |
order 30 | 全局 + 工作区 + 组提示词页(Tabs) |
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(遮蔽) | 隐藏官方按钮 |
sidebar.workspaces.session.menu.item |
dsh-session-toolkit.copy-session-id |
order 500 | 复制会话 ID(会话行 ⋯ 菜单) |
conversation.view |
dsh-session-toolkit.search-panel |
order 20 | 跨会话检索视图(会话内的 tab 页) |
模型体验
系统提示词贡献
模型看到的内容
每次组装贡献五个段,顺序:session-identity(order 40)→ peer-inbox-discipline(order 45)→ global-prompt(order 50)→ group-prompt(order 55)→ workspace-prompt(order 60),位于部署 persona 之后、工具引导(100–199)之前。身份段在组装时按 agent(AssembleContext.agent)从 session-identity 设置解析,subagent(origin/delegationDepth)跳过。入站 peer 消息纪律段输出一段固定的纪律提醒,同样对 subagent 跳过,且自身没有设置项。工作区段为 cwd 前缀匹配到配置工作区(取路径最深/最具体且启用者)的会话注入该工作区提示词,否则为空。组段在自身会话 id 命中某个已启用组的 sessions 列表时才注入(按会话 id 而非目录匹配,故一个组可跨工作区);多个命中组按组键序拼接、块间一个空行;无命中注入空。(这五个段另有一张表,见文末的提示词段位图。)
全局段与工作区段都会在提示词文本后追加其引用文件内容:每次组装读取 files(UTF-8,按 mtime/大小缓存),按原文拼接(段声明 interpolate: false,内容不被改写;{ 串的空格化只发生在 0.1.6 之前的旧内核兜底路径 lib/prompt-literal.js 里,当前路径不做任何改写)。无法读取或超出字节预算的文件会跳过(其内容不注入),但其读取状态被记录供 UI 显示。空段在渲染时删除。
Token 影响
启用时四个提示词段的文本随每次请求重复,而入站 peer 消息纪律段对每个非 subagent 会话也随每次请求重复。全局提示词作用于所有会话;身份文本仅作用于能解析到它的会话(自身记录或默认);工作区文本仅作用于 cwd 前缀匹配到已启用且已配置工作区(取最具体)的会话;组文本仅作用于自身会话 id 列在某个已启用组里的会话(多个命中组按组键序拼接);纪律文本作用于每个非 subagent 会话。引用文件的完整内容会加入实际提示词,因此消耗额外 token——大引用文件会显著增加每次请求的 token 成本。身份文本上限 8000 字符(token 守卫)。
KV Cache 影响
设置不变时各段渲染文本是请求前缀的固定部分;修改会话身份或全局/工作区提示词(或编辑/新增引用文件)可能从首个变化 token 起使提供方缓存复用失效(与官方 persona 段语义一致)。
工具面
send_to_session、list_sessions、inbox_check、create_session、rename_session 与 search_sessions 在 host 平面注册,所有会话可见(subagent 经常驻 preset 组装继承)。参数与返回均为 JSON 兼容。六个工具都会向模型暴露,因此 create_session 的语义后果(创建即产生一条真实用户消息并消耗一次模型调用)对模型是可见的。(inbox_check 只读、无副作用,目标恒为调用者自己的会话;search_sessions 同为只读——它只扫描会话数据并返回坐标,不写入任何会话日志。)
兼容性
插件已对照 DeepSeek Harness dsh-v0.2.0-rc.1 验证,并以它作为最低支持版本(契约面 + 全套门,非完整功能回归;声明的依赖区间——harness 包 ^0.2.0-rc.1、schemastery ^3.18.4):0.1.7 把 settings 从「插件注册命名空间 + ctx.settingsScope.bind」改为「条目 Config 的 volatile 字段 + ctx.configForms」,用户数据面因此整体迁移(见设置字段)。在 0.1.6 及更早内核上,客户端条目会停在 pending (waiting for service: configForms),web boot 报「Failed to load plugins」——这是响亮失败而非静默降级,处置是升级 harness 或卸载本插件。interpolate: false 与 ctx.sessionController.resolveAgent 的既有兜底不变。
- 框架:
@deepseek-ai/cordis4.0.4 与@deepseek-ai/schemastery3.18.4(即dsh-v0.2.0-rc.1vendored 的版本)。@deepseek-ai/schemastery的下限是^3.18.4:volatile()自 3.18.3 才存在,更早的版本会让Config构造直接抛错。插件经 cordis harness 加载,并以dsh.bundle.patch注册为 bundle。 - Host 服务(已对照原生源码校验):本条目
Config的volatile()字段 +.get()实时读取,提交后由ctx.on('loader/volatile-update', …)通知(不重挂插件);ctx.get('settings').update('session-toolkit', patch)作为 host 写回条目 config 的入口(工作区自动补回用);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('configForms').get('session-toolkit')→ 表单{ getSnapshot()/.value/.status, subscribe, set(field, value), unset(field), mutate(ops, expectedRevision) }(写入经 host 校验后落盘当前 profile 的cordis.patch.yml);ctx.get('locale')→register(ns, { zh, en })/bind(ns);只读状态路由GET /api/session-toolkit/state与只读检索路由GET /api/session-toolkit/search(后者与search_sessions工具共用同一段字面扫描);以及timerclient 服务(ctx.timeout)。bundle 的运行时require均解析自模块表种子词(react、react/jsx-runtime、@deepseek-ai/dsh-client-store、@deepseek-ai/dsh-client-ui-primitives、……)。
配置与设置数据面
插件的 host 侧 Config 在插件加载时即用 schemastery 校验整棵配置树。解析顺序 = schema 默认 → profile patch(用户层);两者都在 host 侧解析完再交给插件。
DSH 0.1.7 起,settings 只投影带 volatile() 的字段,并只用两个事实标识一份设置:被编辑条目的 id(= session-toolkit)与该条目的 Config。因此:
- 用户数据(身份文本、全局/工作区提示词、自动上线开关、UI 旋钮)声明为
volatile字段,浏览器半经ctx.get('configForms').get('session-toolkit')读写同一份条目 Config,写入由 host 校验后落盘当前 profile 的cordis.patch.yml(不再有settings.yaml命名空间,也没有settingsScope服务)。 - host 半每次用到时
.get()实时读取:volatile值提交后不重挂插件,identity/global-prompt段在下一次组装就生效,auto-resume经loader/volatile-update立刻恢复新开启的会话。 - 普通字段(顺序、上限、重试、并发等部署参数)保持非
volatile:它们同样能在设置页里看到,但修改走 cordis 配置的正常生命周期。 - 运行时的只读投影(活跃工作区、引用文件读取状态)不属于配置,经
GET /api/session-toolkit/state直接送给浏览器,既不落盘也不出现在表单里。
机制与红线
- 身份注入 使用单一全局段、text 提供方按 agent 求值——无逐 agent 注册、无生命周期开销、设置变更实时生效。
- frozen 配置铁律(红线) —— volatile 字段
ref.get()返回的是deepFreeze快照(不可变)。任何要改的地方必须先{ ... }(数组.slice()) 拷贝成可变对象再提交:host 半把整份新值交给ctx.get('settings').update(...),浏览器半把新值交给表单的set/mutate(它们按 config 路径提交,不用整份替换)。直接改冻结对象会抛object is not extensible(正是此处修复的「工作区列表空」根因)。同一{ ... }拷贝规则适用于 client 对workspacePrompt.workspaces的写入(onWsFilesChange/save/saveWsEnabled/removeWorkspace)。 - 引用文件读取、失败跳过 ——
readPromptFiles在每次组装的text()内运行(stat 判定是否重读);读取失败或超限的文件不会中断组装,其状态被记录进进程内投影供GET /api/session-toolkit/state与设置页显示,且只在内容变化时替换投影对象。 - 自动上线绝不调用
dispose()——AgentHandle.dispose()会从存储移除会话;关闭开关只影响下次重启,绝不下线当前会话。 - 遮蔽基于 cell shadowing —— utilities 条目以更低 priority 重注册官方
session-log-downloadcell;遮蔽崩溃时官方条目优雅 abdicate 回退。 - 纯文本转换 ——
toPlainText(10 条规则、代码围栏状态机、宽松匹配)仅在发送时执行;消息结构与source: { kind: 'user' }不变。
已知限制与暂缓事项
- client 半为手工维护的单文件 IIFE 包;新增功能需同步维护
lib/与client/client.js两处。 - client 半的「标识符作用域」现在有门覆盖。
scripts/scope-identifiers.assert.mjs:某个 IIFE 模块块用到某模块别名、而该块自己没有绑定它时即报红,并已挂 CI(带--selftest);覆盖边界照实写在它的文件头——只核require绑定过的别名、不做完整作用域分析,也不证「按钮真的在浏览器里渲染出来」。在这道门存在之前这一形态是静默的:调react.useState的块必须同时require('react'):react/jsx-runtime不提供它,而缺绑定时组件渲染即抛错;slot 渲染器会把那个抛错吞掉并丢掉整条 entry,于是症状是「按钮静默地不见了」,而不是任何人看得见的报错。这一形态从 2026-09-18(3e44642加了 hooks 调用却没加 require)活到 2026-09-22,穿过了全部的门。 - 收到的跨会话消息在界面上是「收起的一行」,不是可读的正文。
send_to_session按生产者归属记录投递——source: { kind: 'agent-message', form: 'relay', senderSessionId }——而客户端对所有非人类来源都走它对 turn trigger 的渲染,那一行默认收起,点开才见正文。写成kind: 'user'会像人类消息一样 inline 展开,但会把另一个 Agent 的话记成用户说的——而那正是 V4 唯一规定为「生产者拥有」的字段。归属优先;点那一行即可读到正文(正文首行仍自带发件人)。 - 图标名属于集成面。 DSH 0.1.7 把
@deepseek-ai/dsh-client-ui-primitives的图标从IconXxxOutline<尺寸>改名为IconXxxOutlineRegular/IconXxxOutlineMedium(1 px 与 1.3 px 笔画;artwork 保留旧默认size),因此 client 半必须使用目标 harness 的名字。不存在的名字求值为undefined,而React.createElement(undefined, …)会抛错,导致该组件子树整片空白、而它的导航行照常出现(注册与渲染是两件事)。这一形态对其余所有门都是静默的——语法门、打包门、锚门当时全绿。维护者的门守这一条:它把插件引用到的成员列出来,任何一个未被已装 harness 导出即报错。 - 平移的 Session log 入口依赖官方
sessionLogDownloadcontroller 接口,且复刻官方 0.1.6 的「⋯ 更多操作」菜单形态;它是冻结的复刻件:DSH 升级后由维护者对着 harness checkout 重跑一次漂移审计——按同一组锚点双向核对,有漂移就报出来,而不是静默通过。有意的分叉:官方 header 菜单此后多了第二项(feedback),本复刻件只保留 download;这是已裁定的状态、不是待决问题——门把它记成 note 而非失败,正因为"跟随上游新增能力"本身是一个决定,而该决定已于 2026-09-22 作出:不跟随。只有确实想要那个 feedback 入口时才需要重开。 toPlainText宽松斜体匹配可能误删非格式位置的成对*(如a * b * c);对 agent 生成消息可接受,边界收紧为可选优化。- 聚合
inject并集会等待所列全部服务;某 profile 缺一服务会拖慢整包 apply(web profile 当前齐备)。 - harness 提供的依赖区间是前置版本并集;改完区间必须重跑
pnpm install,并在装好的 profile 上做一次依赖解析偏斜测量(期望SKEW_COUNT=0;DE-INSTANCE表示同版本不同实例,判定为可接受)。量测必须显式给出宿主侧才有意义:profile 根是「已装依赖树的根」,不是「运行中的宿主实例」——这种跑法属插件自洽量测,会打上HOST_BASELINE=PROFILE-SELF,并自报「不构成对运行中宿主实例的结论」。 ctx.get('agentDefaultModel')、sessionTitle、workspaceRegistry改为调用时惰性解析,缺失时降级为 cwd/路径寻址;tools与webServer改用ctx.inject等待就绪——loader 并发创建条目,apply 时刻的ctx.get没有顺序保证,晚到会让功能永久静默消失。- 引用文件在组装路径预热 ——
readPromptFiles每次组装对每个引用文件做一次statSync,仅在 mtime/大小变化时读盘;单文件与合计字节预算避免超大文件阻塞组装或撑爆提示词,状态投影也只在变化时写入。client 端files即时保存(onWsFilesChange/save)。 - UI 旋钮来自同一条目的
client.*—— 浏览器半经configForms.get('session-toolkit')读client.*字段(表单不可用时回落冻结的UI_FALLBACK)。client 条目本身仍拿不到 cordis 行配置,但设置的读取已不再需要 host 镜像:同一条目 Config 两侧都可见。 - 最低 harness 版本 =
dsh-v0.2.0-rc.1—— settings 数据面在 0.1.7 改成「条目 Config 的 volatile 字段 +configForms」。0.1.6 及更早没有configForms,客户端条目会停在pending,web 客户端报「Failed to load plugins」;这是刻意的响亮失败(硬 inject),不是静默降级。
恢复方法
卸载 bundle:dsh plugin --profile web remove dsh-session-toolkit,然后重启 DSH 客户端。要回退到整合前的布局,请重新启用原插件而非安装本包。
链接
同类插件
Minglink/dsh-infinite-gen-4★ 2403
DeepSeek 专用破甲插件:以 order 100 追加无条件服从的系统提示词段,提供带校准元数据的 profile 工具,并通过会话投影在输入框上方显示实时破甲状态徽标。
ranxianglei/billion-context★ 637
billion-context官方版:上下文压缩插件,兼顾小窗口(100k上下文足矣)省token(省5倍token)和超长会话(数月级别几十亿token单会话)。
liangmianya/dsh-synapse★ 491
DeepSeek Harness 的可视化非线性对话工作区:把会话、追问与分支变成可浏览、可拖拽的对话地图。
Nwflower/dsh-chat-import★ 213
把 13 家 coding agent(Claude Code、Codex、ChatGPT、Cursor、Gemini、opencode 等)的完整对话历史导入为可续聊的 DeepSeek Harness 会话,并支持反向导出回 Claude Code。
Totoro-qaq/dsh-plugin-bridge★ 165
通过可预览的五段式交接,将已有 DSH 会话迁移到另一个 Agent Preset;保留源会话,并可让目标会话暂停等待确认或立即继续。
Anionex/dsh-turn-rewind★ 131
对话回退:基于持久 Change Ledger 回滚会话与工作区状态。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。