DeepSeek Harness 插件

Han-Yao94/dsh-session-toolkit

Star 数 ★ 4 下载量(近 30 天) 2,004 分类 会话与消息 收录于 2026-08-24 npm dsh-session-toolkit

整合工具包:会话身份、全局/工作区提示词(含引用文件)、会话自动上线、免 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 提供

两种落地方式,选一种:

  1. 设置页:打开「插件」里 dsh-session-toolkit 条目的设置页,把旧值贴进对应字段(表单会写进 profile patch)。

  2. 直接写 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-prompt order 50 / workspace-prompt order 60 / group-prompt order 55)、readPromptFiles 辅助函数(实时 fs.readFileSync 读;缓存按段命名空间隔离,一段的剔除不会误删另一段的条目)、运行时投影聚合(活跃工作区 + 在线会话清单,经 GET /api/session-toolkit/state 送出),以及把新出现的工作区路径经 ctx.get('settings').update('session-toolkit', …) 补进条目 config。
  • Client 半 —— client/client.js 为单一 window.__ModuleLoader__.load bundle;五个 UI 模块内联在 IIFE 中,在一个 apply 里按序注册全部 slot(逐模块守卫)。所有 UI 用 React.createElement;样式以 data-plugin style 标签注入,使用主题 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/cordis 4.0.4 与 @deepseek-ai/schemastery 3.18.4(即 dsh-v0.2.0-rc.1 vendored 的版本)。@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 工具共用同一段字面扫描);以及 timer client 服务(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-download cell;遮蔽崩溃时官方条目优雅 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 入口依赖官方 sessionLogDownload controller 接口,且复刻官方 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 客户端。要回退到整合前的布局,请重新启用原插件而非安装本包。

内容来自项目 README(GitHub)↗

链接

同类插件

查看整个分类 →

社区评论

评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。