Pular para o conteúdo principal
D

dsh-optimized-tools

dragonhorizon-xtihc/dsh-optimized-tools

Per-session control of which skills and MCP tools/servers the model can see (built-in tools are untouched): default shows everything, auto asks the model each turn what to keep, smart picks from your saved groups, or tick groups by hand, with a switcher under the composer and fail-open to full visibility on any error. The default state is auto, which adds one model call per turn (the session model unless judgeModel is set).

Instalar

dsh plugin --profile web add github:dragonhorizon-xtihc/dsh-optimized-tools

README

dsh-optimized-tools 2.0

按会话控制模型看得到什么:技能、MCP 工具、MCP server。每个会话一个状态, 状态在输入框下方随时可切,切换结果落在磁盘上,下次启动还在。

1.x 是「自动学习 + 判定 + 熔断」的启发式套装;2.0 保留其中确定性的部分,并把两条模型驱动模式 重新实现成有明确输入输出契约的判定:状态是显式选的,可见性是纯函数算出来的,失败一律往「看得见」倒。 与 1.x 的关键差别是判定只决定技能与 MCP 的可见集合(不碰基础工具),且失败方向由模式显式规定。

三种内置模式 + 分组多选

状态可见范围模型调用
default全部技能、全部 MCP 工具。没有任何裁剪,也不装任何限制层无
auto每个 turn 调一次模型,读「技能清单 + server 清单 + 本轮请求」,只保留本轮需要的技能与 server每 turn 1 次
smart每个 turn 调一次模型,从你已建的分组里选 1–3 个,按所选分组的并集裁剪每 turn 1 次
groups(勾选分组)只放行被勾选分组的并集:分组里列出的技能、分组里列出的 MCP server 下的全部工具。没勾任何分组时不允许提交(要全量请改用内置模式)无

smart 的前提是至少存在一个分组:没有分组时它既不出现也不可作为初值,宿主会拒绝并说明原因。

「并集」是逐项的:勾了 A、B 两个分组,A 放行 pdf 技能与 rea server,B 放行 xlsx 技能与 alt server,那么可见的是 pdf、xlsx、rea.*、alt.*——不是交集,也不是「A 和 B 都放行的」。 一个分组里两个列表都空是合法的:它放行零个成员,选中它等于看不见任何 MCP 工具。

模型驱动的两个模式:判定与失败方向

  • 判定单位是 turn:同一 turn 内的多个 step 复用同一次判定(step 是 turn 内部的工具循环, 用户意图没变)。跨 turn 重新判。
  • auto 失败时沿用上次成功判定;从未成功过则等同 default。理由是它的使命就是持续裁剪, 中途回落到全量会让上下文突然膨胀数万 token。
  • smart 失败时回落全量,不沿用上次:判定失败说明本轮意图没被理解,继续按上次的分组裁剪 会让模型看不到本轮真正需要的技能——用户看到的是「插件自己乱选」,而不是「插件退让了」。
  • 熔断:同会话连续失败 3 次、或 5 分钟内全局累计失败 5 次后停调模型,冷却 2 分钟后放行一次 探测,成功合闸、失败重新熔断。熔断期间按上面两条失败方向回落。
  • 上限:单会话最多判定 50 次;auto 单次最多保留 40 项;smart 单次最多选 3 个分组。 超出上限的判定结果整体作废(不截断——截断会静默改变模型的决定)。

判定输出必须是单行 JSON:auto 为 {"skills":[...],"servers":[...]},smart 为 {"groups":[...]}。 候选清单外的名字(幻觉)会被静默丢弃,两个键缺失或元素非字符串则整条判定判失败。

「立即生效」的确切含义

切状态之后,下一次请求构造时就生效。具体地:

  • 状态写入是同步落盘 + 异步 flush:界面上的勾选立刻反映,磁盘写入排队进行,插件卸载时排空。
  • 已经在跑的 turn 不会被打断。它的工具清单在下一个 step 边界重新计算——也就是模型下一次 决定调什么工具之前。所以「切了但这一轮还在用旧工具」是设计行为,不是缓存没清。
  • 没有正在跑的 turn 时,切换是即时的:下一次你发消息,模型看到的就是新清单。

一句话:立即生效 = 下一次请求构造时;在途 turn 在下个 step 生效。

安装

cd plugins/dsh-optimized-tools
npm run build          # 等价于 link-deps → tsc 宿主半 → tsc 客户端半 → tsdown
dsh plugin --profile web add link:$(pwd)

产物两份:

  • lib/types/** —— 宿主半(package.json 的 main 指向 lib/types/index.js)
  • lib/client.js —— 浏览器半,window.__ModuleLoader__.load({ id: "dsh-optimized-tools", factory }) 闭包工厂;只 require react 与 react/jsx-runtime,宿主包一律通过平台模块拿

挂载行由 cordis.patch.yml 提供,dsh plugin add 会把它接进 profile 的 bundles。

配置

键默认说明
defaultStateauto新会话的初始状态。只有 default / auto / smart 三值;groups 不是合法初值(新建会话时分组 id 还不存在)。smart 且没有分组时按 default 建档
inheritLastStatetrue新会话是否沿用上一次选择的会话状态。false 时 defaultState 每次都生效
stateDir''状态目录。空串 = $DSH_HOME/storages/dsh-optimized-tools;非空且非绝对路径时打 warn 并回退到默认目录
judgeModel{provider:'',model:'',maxTokens:512}判定用的模型。provider/model 留空 = 跟随会话当前模型;maxTokens 只需够一行 JSON
judgeTimeoutMs3000单次判定超时;超时按失败处理(计入熔断)
maxKeepItems40auto 单次最多保留的技能数 + server 数
maxSmartGroups3smart 单次最多选中的分组数
maxJudgmentsPerSession50单会话判定次数上限,达到后沿用当前结果
sessionCircuitThreshold3会话级熔断:连续失败多少次
globalCircuitThreshold5全局熔断:窗口内累计失败多少次
burstWindowMs300000全局失败计数的窗口
cooldownMs120000熔断冷却;冷却后放行一次半开探测

1.x 的其余键(enabled / tier0 / keep / hidden / roles / mcpLazy / stateFilePath …) 全部删除:它们驱动的学习与惰性断连逻辑在 2.0 里不存在,留着就是死配置项。 见到旧键时插件会打一条 warn 列出所有被忽略的键名,不做别名映射——映射会让「我改了配置」 与「配置根本没被读」这两件事在界面上长得一样。

从 1.x 升级

不迁移。v1 的 state.json(version: 1,里面是 hidden / mcp 之类的学习结果)不会被读取、 不会被解释、也不会被覆盖——插件把它当陌生文件,首次真正写盘前先原样复制成 state.json.v1.bak 留作证据,再写 v2。

所以升级后所有会话从 defaultState 重新开始,你手写的 hidden 名单不会带过来(那份名单的语义 在 2.0 里已经没有了:隐藏现在是「选中的分组之外的东西」,不是一张固定清单)。想留档就自己从 state.json.v1.bak 里抄。1.x 也不能回退读 2.0 的文件:两边同名不同构,只有 version 字段能区分。

两进程写同一份 state.json 不受支持:没有跨进程锁,后写的整份覆盖前写的。同一个 $DSH_HOME 下不要同时起两个 dsh。

安全姿态:有 connection 与没有 connection

本插件的写面是一条 HTTP 路由 POST /optimized-tools/rpc(能建组、删组、改会话可见性), 它用 webServer.register 挂在宿主上,完全绕过宿主 connection 服务的 /api 栅栏。 任何网页都能向 127.0.0.1 发跨源 fetch,绑定非 loopback 地址时局域网内任意主机亦可直连。 所以栅栏是分层的:

装配判定
有 connection 服务一律交给宿主:connection.requestRejection(req) 返回 401/403 就拒绝,undefined 放行,其它返回值一律 503。此时鉴权由宿主负责
没有 connection 服务内置回退:Host 主机名必须在 127.0.0.1 / localhost / [::1] / ::1 之内;Origin 缺失,或与请求自身完整同源(协议 + 主机 + 端口)才放行。这条分支没有任何认证,只有浏览器侧与网络侧两道约束

只比主机名是不够的:本机另一个端口上跑着的页面也是「同主机」,它同样能改这里的分组与会话可见性。 比较端口与协议之后,跨端口与 https → http 的调用都会被拒(返回 403)。

要远程访问就必须挂 connection,否则你只有上面那两道约束、没有鉴权。读 connection 抛错、 或服务形状不对时一律 503 失败关闭——装配异常时宁可让前端拿到一条可读的失败,也不让未鉴权的 写请求穿过去。

失败姿态:Fail Open

任何一步出问题,结果都是「工具还看得见」,不是「插件崩了」:

  • 宿主服务取不到(tools / skills / loader / llm)→ 该次裁剪或判定跳过,工具清单保持原样。 没有 llm 服务时 auto/smart 退回「不裁剪」,而不是拒绝加载
  • 技能影子注册失败 → 那个技能仍然可见,只有「隐藏」没生效;此时 applied 报 false, 可见计数按真实剩余项给(不虚报「已隐藏」)
  • tools.restrict 报错(例如名单里有 agent 自有层的工具)→ 解析出非法名、把它们从名单里剔掉后 只重试一次;仍失败就整层不装,工具全部可见
  • agent 创建时的装配异常 → 整体 try/catch 吞掉,agent 照常创建。这一条是必须的:agent/created 是 serial 监听器,抛错会让 agent 创建失败——那比「少隐藏了几个工具」严重得多
  • 判定链的任何异常(模型报错、超时、输出不合格)→ 只影响可见性,不打断 turn

开发

npm run typecheck      # 宿主半 + 客户端半 + 测试 三程序类型检查
npm test               # 单元与集成套件(27 文件 / 421 用例;不启动 DSH、不监听端口、不调真实模型)
npm run test:layering  # 分层边界检查(domain 不许 import state,capabilities 不许值导入 node:…)
npm run build          # 两份产物
npm run check:bundle   # 客户端产物形态(banner / intro / id==包名 / 可执行 / 导出)
npm run verify         # typecheck + layering + test + build + check:bundle 串联

分层是强制的,由 scripts/verify-layering.mjs 逐 import 检查:

shared   只有类型,零运行时            → 谁都能用
domain   纯函数,只许 import shared    → 不许碰 node:、不许碰宿主
state    持久化,node: 可用            → 只许 import shared / domain / 自己
capabilities  能力盘点与裁剪           → 宿主包只许 type-only 导入
transport     HTTP 与 RPC              → node: 可用,宿主包 type-only
client        浏览器半                 → 只许 react 与自身

真实进程的手工探针:

# 对着一个真的在跑的 dsh web,打印切换前后的可见计数
node scripts/verify-invariant.mjs --rpc http://127.0.0.1:3080/optimized-tools/rpc --session <sessionId>

这个脚本是 manual 专用,不出现在 package.json 的任何 script 里,也不进自动化门禁—— 它依赖一个活进程,任何一次「今天没连上」都会变成 CI 红,而那种红与代码对错无关。 (tests/scripts/layering.spec.ts 里有一条断言守着这条纪律。)

排查

现象原因处理
输入框下方没有模式下拉槽位注册被拒(宿主槽位改名),或客户端产物没加载看浏览器控制台是否 SlotAssemblyError;本插件的槽位注册是 try/catch 的,失败只降级不报错
设置页没有「分组」导航项同上,第二个槽位注册失败两个槽位各自独立 try/catch,一个失败不会带停另一个;若两个都没出现,查 lib/client.js 是否已构建
界面显示的是 key 字面量(如 trigger.groups)locale 字典没注册,或槽位的 locale 命名空间与字典不一致字典注册必须在槽位注册之前,两者用同一个 LOCALE_NAMESPACE
切了状态但工具没变在途 turn 还没走到下个 step 边界等这一轮结束;见上文「立即生效」
切了状态、界面也没报错,但下次启动变回去了写盘失败(目录不可写 / 被别的进程覆盖)看日志里有没有 状态写入失败;确认同一 $DSH_HOME 下只有一个 dsh 在跑
建分组时提示「至少要选择一个分组」勾空了这是有意的:空选择与「想要全量」在日志里必须可区分,要全量请选内置模式
提示「智能模式需要至少一个分组」smart 的合法性前提是至少存在一个分组先建一个分组,或改用 default / auto
模型驱动的模式「什么都没变」判定失败(模型不可用 / 超时 / 输出不合格 / 熔断打开)模型不可用时 auto/smart 一律不裁剪;设置页「效果驾驶舱」的「模型判定」事件会写明失败原因
每个 turn 都多一次模型请求auto/smart 的设计就是每 turn 判一次想省掉就切回 default 或勾选固定分组;也可以用 judgeModel 指一个小模型
升级后旧的隐藏名单没了v1 文件不迁移从 state.json.v1.bak 里抄;2.0 的隐藏语义是「选中分组之外」,不是固定名单
手动调 RPC 返回 403无 connection 时只放行 loopback Host + 完整同源(协议+主机+端口)从正常页面调,或给 profile 挂 connection
手动调 RPC 返回 503connection 服务形状不对或读它时抛错检查 profile 装配顺序,确认 connection 已正确挂载

已知限制(实测确认,非猜测)

  1. auto/smart 的判定输入只有「技能清单 + server 清单 + 本轮用户文本」。它看不到历史对话, 所以「接着上一轮继续」这类请求只能按最后一条用户消息判断。判错时的逃生口是切回 default 或显式勾选分组。
  2. skill 工具本身永远可见。本插件隐藏的是技能(在 agent 自己的层里注册同名影子技能, 把 invocation 置为双 false),不是 skill 工具。没有任何分支能产出 'skill' 的名字进 deny 名单——它不以 mcp__ 开头,这是结构性保证而非常量白名单。
  3. run_code 永远不进 deny。它是 PTC 保留名,restrict 显式拒绝;本插件在算名单时就把它排除。
  4. agent 自有层的 MCP 工具无法被 this 层限制。tools.restrict 只认继承层的名字,agent 自己注册的 mcp__* 在 schemas(scope) 里但不在可限制集里。这类名字会被剥离并记入 unenforceable, 结果是「它仍然可见」——Fail Open,不是静默失败。
  5. RPC 只暴露计数,不暴露工具名。session.get 给的是 visibleSkills/totalSkills 这类数字。 把工具名送到浏览器上等于给攻击面开一扇窗,代价是「I-1:非 mcp__ 工具指纹恒定」这条不变量 只能靠自动化套件(tests/e2e/invariant.spec.ts)兜,真机脚本测不了。
  6. 不做分组嵌套。分组是平的成员清单(技能名 + server 名),没有父子关系。
  7. 两进程写同一份状态文件不受支持,没有跨进程锁。

规格与计划文档

docs/superpowers/ 下:

plans/2026-10-23-phase0-skeleton.md   骨架(要做哪几块、边界在哪)
plans/2026-10-23-phase0-solution.md   方案(每个模块的导出面与逐条实现理由)
plans/2026-10-23-phase0-tasks.md      任务分解(T01–T18,逐条含失败先行断言与验收命令)
verify-plan-docs.mjs                  计划文档自检(7 类结构判据)
verify-plan-docs.mutation.mjs         门禁自检(向副本注入 47 个已知回归,要求全部被抓)
notes/                                取证记录与评审报告

PRD-CAPABILITY-MODES.md 是需求原文,archive/v1/ 是 1.x 的完整实现与文档(只读留档)。

License

MIT

Plugins relacionados