Vai al contenuto principale
G

dsh-plugin-sage-subagent

gezi-wen/dsh-plugin-sage-subagent

Named, file-defined subagent roles for DeepSeek Harness: persona, subagent-only skills, tool permissions, an ordered model chain with failover, and derived groups.

Installazione

dsh plugin --profile web add github:gezi-wen/dsh-plugin-sage-subagent

README

dsh-plugin-sage-subagent

给「子代理(subagent)」定义有名有姓的角色:每个角色自带人设、专属技能、工具权限、 模型链(含失败顺序回退)、启停状态与递归深度,不影响主代理与其他子代理

开箱自带 4 组 32 个默认角色分组能从角色派生,也能自己建、自己改名。与官方 subagent 工具并存

安装

# 装进某个 profile(推荐)
dsh plugin --profile web add dsh-plugin-sage-subagent

# 或者直接在 profile 目录里用 npm / pnpm 装
cd <DSH_HOME>/profiles/web
pnpm add dsh-plugin-sage-subagent

装完重启那个 profile 的实例,就能在「设置 → 子代理角色」看到面板。

第一次打开时,如果你的 <DSH_HOME>/.agent-roles/ 是空的,插件会自动铺开这 32 个默认角色; 里面已经有角色就完全不动 —— 不覆盖你的文件,也不会把你删掉的内置角色塞回来。

为什么是「role」这个词

DSH 核心已经占用了三个近义词:profile(部署 profile,--profile web)、 persona(部署人格字段)、preset(会话预设)。role 未被占用,且语义准确。

快速了解

  • 角色存文件<DSH_HOME>/.agent-roles/<id>.md,可 git、可 diff、手改最顺手
  • 默认角色:插件包内置 defaults/roles/首次加载时若角色目录为空则自动铺开; 目录里已经有角色就完全不动(不覆盖、也不把用户删掉的角色塞回来)
  • 分组:从角色派生 —— 写一个 group 值就多一个组,没有组定义文件。一个角色只属于一个组
  • 启停:每个角色一个开关,列表里直接拨;也可以整组开/关
  • 模型链:有序候选,第一个不可用时按错误码自动往下切
  • 子代理永远不能再往下派(硬规则,见下)

分组

分组是从角色派生出来的,不是一张写死的清单。

写一个角色带 group: 我的项目,就多出一个叫「我的项目」的组;该组最后一个角色被删掉或改走, 这个组就消失。所以没有「组定义文件」要维护,也没有「先建组、再放角色」这种顺序要求。

  • 组名就是显示名,中文也行 —— 它既是标识也是名字,不需要 id ↔ 显示名的映射
  • 编辑角色时,分组下拉列出「有角色的组 ∪ 内置建议组」,最后一项是 + 新建分组…
  • 选中某个组时,列表头上有 重命名 —— 它会把这一组所有角色的 group 字段一起改写
  • chip 只显示有角色的组,空组不占位置

内置建议分组(4 组)

装什么角色数默认
工程实践动手做东西6
审查把关检查做得对不对7
调研与写作想清楚、写出来(不直接改代码)7
语言与框架特定语言的专家12

前三个是按「你要它干什么」分的 —— 动手、检查、思考表达,任何人做任何事都落在这三类里。 第四个单独拎出来,是因为语言专家有 12 个、而且默认关着,需要一个「整组开关」一次放出来。

这四个只是内置默认角色用的初始值,你完全可以不用,去建自己的组。

三条状态规则(改代码前务必理解)

  1. enabled 是启停的唯一真值。
  2. 分组开关不存状态 —— 它只是「把这组每个角色的 enabled 改一遍」的批量动作。 这样组开关和角色开关永远不可能打架
  3. 一个角色只属于一个组group 是单值)。「同一角色出现在两个组」在单组模型下物理不可能, 从根上消灭了「A 组关掉牵连 B 组」的问题。想要同样的角色出现在另一个组, 复制一份、换个标识 —— 两者从此各有独立开关,互不牵连(界面里报错提示会直接给这个动作)。

组名允许任意像个名字的值(非空、不含控制字符、≤ 32 字符、首尾空白自动 trim)。 只有这几条形状约束就够了 —— 但也不能一条都没有:否则「工程实践」和「工程实践 」 会变成两个看起来一模一样的组。

默认角色集(4 组 32 个)

人设一律 3–5 行,只写「职责边界 + 交付标准」,不写步骤 —— 步骤该由主代理按当下情况决定,写死在角色里等于替它做决定、把它的手脚捆住。

工程实践 explorer 代码探索者 · builder 实现者 · debugger 调试员 · tester 测试员 · designer 界面设计 · ops 运维员

审查把关 review-code 代码审查 · review-security 安全审查 · review-perf 性能审查 · review-arch 架构审查 · review-test 测试审查 · review-api 接口审查 · review-project 项目审查

审查是重灾区:审查不到位直接养出屎山。所以这一组有统一的硬要求 —— 只在真有问题时才提、没问题就说没问题、不要凑数;按影响排序,风格问题最多提一次; 每条写清「什么条件下出问题 + 影响 + 怎么改」。 review-test 单独说一句:测试本身没人审查,它是假绿和「测试写得像实现注释」的源头。

调研与写作 architect 架构师 · scout 检索员 · researcher 调研员 · writer 技术写作 · slides 演示文稿 · analyst 数据分析(默认关) · paper 学术写作(默认关)

这一组里 analyst / paper 默认关(给做科研的人用),所以它的 chip 显示 5/7组内混着开关状态是正常的 —— 启停一律由每个角色自己的 enabled 说了算,组不决定默认值。

语言与框架(默认关)python-pro · typescript-pro · javascript-pro · golang-pro · rust-engineer · java-architect · cpp-pro · csharp-developer · sql-pro · react-specialist · vue-expert · powershell-7-expert

默认角色由 tools/generate-default-roles.mjs 生成:内容集中一处,改完重跑,不会漏改某一个。

角色文件格式

位置:<DSH_HOME>/.agent-roles/<id>.md(可用环境变量 SAGE_AGENT_ROLES_DIR 覆盖)。 frontmatter 是元数据,markdown 正文就是人设 / 系统提示词。

---
name: 代码探索者
description: 要摸清一块陌生代码的结构、依赖与入口时用它;只读,不改任何文件
group: 工程实践
enabled: true
skills:
  - repo-survey
tools:
  allow:
    - read
    - glob
    - grep
    - skill
model:
  chain:
    - provider: deepseek-official
      model: deepseek-flash
    - provider: deepseek-official
      model: deepseek-v4-pro
  switchOn:
    - RATE_LIMIT
    - TIMEOUT
maxDepth: 1
backgroundMode: one-shot
---
你是「代码探索者」。把一块陌生代码摸清楚,交回能直接拿去做决定的结论。你不修改任何文件。
字段必填说明
name显示名
description何时用它(写触发场景,不写职责)—— 进工具描述,是主代理选角色的唯一依据
(正文)人设 / 系统提示词
group任意「像个名字」的值(中文也行)。组是派生的:写一个新值就多一个组。省略则归入 工程实践 并给提示;首尾空白自动 trim
enabled默认 true停用只看这个字段
skills子代理专属技能池里的技能名(只做加法,见下)
toolsallow / deny精确工具名(不支持通配符)
model.chain有序候选;省略则继承父代理
model.switchOn命中哪些错误码才往下切(默认覆盖「路由服务不了我们」那一组,不含 INVALID_REQUEST
maxDepth默认 1。写大于 1 没有实际作用(见「硬规则」)。0 是「这个角色完全派不出去」—— 但停用请用 enabled: false
backgroundModeone-shot(默认)或 continuable

未识别的 frontmatter 键会响亮失败(连同键名),不静默丢弃。

标识唯一与防覆盖

文件名即标识,文件系统保证 <id>.md 只有一个。所以真正要防的不是「重复注册」,而是「静默覆盖」—— 新建一个已存在的标识会把那个角色的文件整个抹掉。

时机行为
新建时标识撞了报错,并写明它已经在哪个组里,同时给一个「复制它到本组」的动作
编辑时改了标识撞了别人同样报错
宿主侧写文件那一步再挡一道saveRole 默认 mode: 'create'

最后一条不能省:前端校验是给用户看的,宿主侧那道是给「.md 被手改」或「RPC 被直接调用」兜底的。 只信前端的结果,就是某天有人的角色被静默抹掉。

硬规则:子代理永远不能再往下派

这不是默认值,是结构性事实。层层分包会让一个任务在无人监督下指数扩散,成本与可控性一起失控。 所以 allowRedelegation: true 会被判为非法配置(写 false 或直接删掉这个字段都可以), 没有任何开关能打开它

实现是调用时刻的动态 guard,注册在子代理自己的 agent.ctx 上:

scoped.tools.guard((execution) =>
  execution.name === 我们的工具名 || execution.name.startsWith('subagent')
    ? '本角色不允许再往下派子代理(allowRedelegation: false)'
    : undefined)

为什么必须这么做(三条都是实测踩出来的,别改回去):

  1. maxDepth 挡不住。它是「我这一次创建的这个孩子」的深度上限,管不到那个孩子以后自己派谁—— 实测:maxDepth=1 的子代理照样成功派出了孙代理,因为孙代理那一层用的是 官方委派工具实例自己的配置。
  2. tools.restrict() 删不掉官方委派工具。它注册在 agent 本层,而官方明确 「restriction 只过滤继承来的工具,永不过滤本层自己的注册」。
  3. 黑名单不能预先算。官方委派工具是「逐 agent、稍后注册」的,agent/created 那一刻的快照里 可能只有 subagent_fork 而没有 subagent;按快照算名单会漏掉主入口,子代理照样派得出去。 所以 guard 里必须是调用时刻的前缀判断,不能查表。

子代理专属技能(只做加法)

技能正文放 <DSH_HOME>/.agent-roles/skills/<名>/SKILL.md只注册进子代理自己的作用域agent.ctx + ctx.skills.register),因此只有该子代理看得见。

⚠️ 视图是「全局层 ∪ 本作用域链」。所以全局技能目录对子代理仍然可见—— 本插件只做加法,不做收窄。收窄(同名遮蔽)已实测可行,留作后续可选项。

明确不做

  • 不做「按子代理指定 MCP」。MCP 工具由 host 平面的 dsh-mcp-client 全局注册, 没有官方的按 agent 挂载入口。需要时在派发 prompt 里写清让子代理去用即可。
  • 不做语言专家导入桥。曾经有过一个「从 <DSH_HOME>/skills/language-specialists/ 导入」的功能, 2026-09-15 删除:那是某个特定环境专有的技能库,别的开发者装上根本没有这个目录,按钮点了只会是空的。 语言专家改为内置的默认角色
  • 用户自定义分组?做。 组从角色派生,想建就建、想改名就改名(见上「分组」)。 这里曾经写的是「不做」,理由是「六个固定组够用、自定义会把唯一指向搞复杂」—— 后半句是概念混淆:「组可以自定义」和「一个角色只能属于一个组」是两件事, 后者才是唯一指向的关键,前者完全无害。2026-09-15 纠正。
  • 不新增自定义 session 事件类型,不新增自定义 source.kind(会让会话在后续 DSH 版本读不出来)。

模块规则(防腐化,改代码前先读)

lib/
  index.js            host 半:装配与生命周期(薄)
  client.js           client 半:槽注册 + 面板(**单文件**,见下)
  shared/*.js         纯逻辑:无文件系统、无 Cordis、无副作用 ← 单测覆盖
  host/*.js           运行时接线:读写、注册、事件(薄)
defaults/roles/*.md   内置默认角色(由 tools/ 生成)
docs/                 设计定稿、样式真源、官方视觉契约
tools/                生成脚本
  1. 跨层调用只允许 lib/* → shared/* → 无shared/ 不许 import host/lib/
  2. shared/host/role-store.js 的业务判断必须可单测;host/ 其余模块只做接线。
  3. 新增一个职责就新增一个模块,不要把逻辑塞进 index.js

为什么 client.js 是单文件

浏览器半边的模块加载器(dsh-client-modules)只解析模块图里已注册的 specifier (包名,以及去掉尾部 /client 的规范化形式)。相对路径不在图里,require('./x.js') 会直接抛错。 官方和所有第三方插件的 client.js 都是单文件,原因就在这里。

CSS 从哪来

lib/client.js 里的 const CSS 那一段是生成的,不要手改。样式真源是 docs/panel.css

node tools/sync-panel-css.mjs

它做四件事:剥掉布局稿专用段(审阅工具条、视图互斥)、剥掉官方外壳段(.overlay/.panel/.nav/.options ——那些由 DSH 提供)、给类选择器加 sr- 前缀(避免与其它插件撞车)、把全局元素选择器收进 .sr-root 作用域。

⚠️ 顺序不能换:先前缀化,再收作用域。反过来的话 .sr-root 会被前缀化二次改名成 .sr-sr-root。 ⚠️ docs/panel.css注释里不要出现带前导点的类名 —— 抽取脚本会把注释里的 .xxx 一起前缀化。

开发与验证

下面这些是给从源码仓库开发的人的。用 npm 装的话,包里只有 lib/ + defaults/ + 文档, 没有 tests/tools/files 白名单刻意如此)。

按技能 dsh-plugin-development 的隔离流程:fork/开发 → 挂 web-test profile(link:) → 三级验证(--dump-config 验组合 / 模块解析 / 真启动打标记)→ 确认后才动生产。

# 纯逻辑单测(pwsh 不展开 glob,显式列文件最稳)
$files = Get-ChildItem tests -Filter *.spec.js | ForEach-Object { $_.FullName }
node --test @files

node tools/generate-default-roles.mjs   # 重新生成默认角色
node tools/sync-panel-css.mjs           # 样式注入 client.js

测写操作时记得隔离角色目录,别拿真实数据试:

$env:SAGE_AGENT_ROLES_DIR = "$env:TEMP\roles-test"   # 临时目录就行,别拿真实数据试
node $env:DSH_HOME\..\npm\node_modules\@deepseek-ai\dsh\lib\bin.js --profile web-test --port 0 --no-open

空目录会触发默认角色铺开;启动日志里那行 [subagent-roles] loaded reason=startup roles=N skills=M dir=... 就是三级验证的标记。

已知的坑(都踩过,别改回去)

  1. ctx.logger 的输出不进 stdout(只上 Web 日志面)—— 打标记必须用 console.log
  2. 静态 ToolDefinition.parameters 是原始 JSON Schema,不是动态工具 DSL 的逐字段 required: true
  3. 技能白名单要自动补 skill —— 不然官方技能目录整体消失(子代理看到的目录 0 条)。
  4. resolveCallConfig 挡不住模型名写错 —— 额外查一次 llm.listModels()listed 提示。
  5. 回退链必须每次请求显式写完整配置 —— 失败请求的配置会写进持久请求头,增量改写下一次 attempt 会拿到旧值。
  6. CSS [hidden] 会被 display: flex 盖掉 —— .sr-roleCard[hidden]{display:none} 必须显式写。
  7. 别把对象塞进 JSX children —— 思考强度项是 { id, name, description } 对象(LlmReasoningEffortInfo), 直接当 <option> 的 children 会触发 React error #31,整棵子树卸载、面板变空白。 凡是渲染来自 RPC 的值,先确认它是字符串还是对象。
  8. projectRole(host/rpc.js)是手写字段列表 —— schema 加了新字段必须追到这个投影点, 否则前端拿到的 role 上根本没有那个字段(表现是分组计数全 0/0)。
  9. RPC 错误响应的 error.details 是必填 —— DSH 的 connection 层校验 code/message/details 三者齐全,少一个就抛 connection: invalid server-response failure, 真正的错误信息被丢掉、界面上只剩那句英文。
  10. roles.list 的投影不含 persona(人设正文大,列表页用不到),所以切开关要走 roles.setEnabled,不能拿列表里的对象回传给 roles.save —— 那会崩在 persona.trim()
  11. 作用域化时不要加 color: inherit —— .sr-root button(0,1,1)的特异性高于 .sr-btnPrimary(0,1,0),会把主按钮文字色盖成继承色,深色主题下白底白字、按钮变成一块空白。

模块解析(改依赖前先读)

本插件在 $DSH_HOME 树之外,Node 向上查找永远够不到 DSH 的依赖闭包。所以:

# 直接在插件目录做 junction 指向闭包
New-Item -ItemType Junction -Path "node_modules\<包名>" -Target "$env:DSH_HOME\profiles\node_modules\<包名>"

peer 一律写 *,DSH 升级后闭包自动变新,本文件不用动。

License

Apache-2.0 —— 见 LICENSE,署名与第三方说明见 NOTICE

面板 UI 遵循 DeepSeek Harness 官方设计语言(MIT)。没有复制 DSH 源码: 尺寸、颜色、间距与交互模式都是对着官方公布的 CSS 变量(--dsw-*)和产物里量出来后 重新实现的 —— 这一点在 NOTICE 里也写明了。

Plugin correlati