← Back to home@zuosus

dsh-decision-engine

Jev-like decision plugin for DeepSeek Harness

Stars
0
Language
JavaScript
Created
Oct 23, 2026
Updated
Oct 25, 2026

Introduction

dsh-decision-engine

给 DeepSeek Harness 的 System-1 决策守卫:在每次工具调用前问模型四个问题 (风险、可逆性、任务相关性、注入嫌疑),据此放行、询问或拦截。默认 shadow: 只记录判断,绝不打断你干活。

它支持所有 system-one 风格 API:内置 system one 与 bamboonode 两个预设,也可以填任意 custom base URL(自建 llama.cpp / vLLM、 企业网关、第三方中转)。请求路径、鉴权方式与响应结构与 system-one 协议保持一致, 换 provider 不影响各决策点的解析逻辑。

本项目原名 dsh-startlux-decision(更早叫 dsh-startlux-guard)。 0.4.0 起更名为 dsh-decision-engine 并加入 provider 抽象。

0.5.0 起配置文件名从 startlux.yml 改为 config.yml——本插件已不是 只服务 startlux 一家,名字里再挂某个服务商只会误导。旧文件会被自动改名 迁移,升级不需要重新填 key;迁移失败时仍会回退读取旧文件名。

安装

从 npm 安装(推荐)

dsh plugin add dsh-decision-engine

或手动装进 profile:

cd ~/.dsh/profiles/desktop
pnpm add dsh-decision-engine

然后在 profile 的 package.json → dsh.profile.bundles 里加入 "dsh-decision-engine",重启 DSH。

本地开发

git clone https://github.com/zuosus/dsh-decision-engine.git
cd dsh-decision-engine
# 复制到 profile 的 node_modules,或 pnpm link

仓库里带了迁移脚本,不用手改(默认只预览,--write 才写入并自动备份):

node scripts/migrate-profile.mjs           # 预览
node scripts/migrate-profile.mjs --write   # 写入 + 备份 .bak
cd ~/.dsh/profiles/desktop && pnpm install

然后重启 DSH。配置不用重填:插件在启动时会做一次自动迁移, 把旧目录里的配置与遥测拷到新目录——

路径
数据目录(迁移目标)~/.dsh/dsh-decision-engine/

迁移规则:

  • 只搬一次:新目录已有 config.yml 就不再动,绝不覆盖你后来改的值。
  • 配置改名:新目录里的旧名 startlux.yml 会被 rename 成 config.yml (rename 失败则拷贝);跨目录迁移是拷贝,源文件保留,出问题可对照。
  • 只搬两个文件:配置文件与 telemetry.jsonl。目录里其它东西 (笔记、备份)一概不碰。
  • 权限 600:配置里可能有明文 key,迁移后仍保持只有你可读。

数据目录解析规则(跨平台)

数据目录是 $DSH_HOME/dsh-decision-engine/,~/.dsh 只是 DSH_HOME 未设置时的默认值。解析顺序与宿主 @deepseek-ai/dsh-home-paths 完全一致:

优先级来源
1显式配置的路径(本插件暂无此入口)
2环境变量 $DSH_HOME
3操作系统家目录下的 ~/.dsh

细节(都由 lib/home-paths.js 统一处理):

  • 空白值等于未设置:DSH_HOME="" 或纯空白会回退 ~/.dsh,绝不会把数据 目录解析到当前工作目录。
  • 支持波浪号:DSH_HOME=~/myhome 会展开;~\(Windows 形式)同样支持。
  • 相对路径会被规范化成绝对路径,否则落盘位置会随进程工作目录漂移。

Windows 说明:~/.dsh 里的 .dsh 只是目录名,不是路径分隔符—— Windows 允许点开头的目录。真正与平台相关的是分隔符与家目录位置,二者分别由 node:path 与 node:os 处理,所以 Windows 上实际路径是 C:\Users\<你的用户名>\.dsh\dsh-decision-engine\config.yml。

配置

插件配置页

插件 → 已安装 → dsh-decision-engine → 点进该插件,配置区(「包含的组件」 上方)会出现 Decision Engine 卡片,布局与内置 dsh-rewind-plugin 一致:

  • 服务提供方下拉:system one / bamboonode / custom,下面一行显示当前生效端点
  • 两个开关:启用守卫、真正拦截
  • 思考模式下拉:never / auto / always(见下文「省 token 的手段」里的取舍)
  • 模型下拉(来自当前 provider 的 /v1/models,含「自动(服务端默认)」)
  • 一个 API Key 输入框
  • 选 custom 时展开四个字段:Base URL(必填)、端点风格、附加请求头、密钥变量名
  • 一个 测试端点 按钮(回显 provider、实际 POST 的完整地址、HTTP 码、请求 id 与响应体)
  • 决策模型参与点列表:每个决策点一行,右侧是 关闭 / 观察 / 生效 三态
  • 实时的缓存命中率与每个点的 token / 延迟统计

改完即保存,没有「保存」按钮:开关 / 下拉 / 分段控件点一下就落盘;文本框 (地址、请求头)停手约 0.7 秒或失焦后落盘;API Key 只在失焦或回车 时落盘(避免把打字途中的半截 key 写进凭据文件)。底部状态行实时显示 待保存… / 保存中… / 已保存 / 未保存:原因,失败时附「重试」。落盘即写入 config.yml,不需要重启:监听器常驻注册,provider 与模式在每次调用时现读; 缓存参数同样是取值函数而非启动期快照。

一个例外:选 custom 但 Base URL 还没填或不合法时,配置改动会整体延后 (状态行给出原因),地址补全后自动落盘;此时决策点改动不受影响,照常保存。

Provider:接任意 system-one 风格服务

请求形状对所有 provider 完全一致(这也是 system-one 协议本身):

POST <base>/decisions        或   POST <base>/systemone
authorization: Bearer <key>
content-type: application/json

{ "model": "...", "state": {...}, "questions": {...}, "think_mode": "never" }

响应只需含 answers(model / usage 可选)。因此 provider 只决定三件事: 往哪发、用谁的 key、默认哪个模型。

五个选项

provider默认地址端点密钥名默认模型
system onehttps://api.typesafe.ai/v1/v1/systemoneTYPESAFE_API_KEY服务端默认(jev-latest)
bamboonodehttps://api.bamboonode.dev/v1/v1/decisionsBAMBOO_API_KEY服务端默认(startlux-decision-27b-autothink)
siliconflow(硅基流动,国内站)https://api.siliconflow.cn/v1/v1/systemoneSILICONFLOW_API_KEYKev-4B(可改 SemIf)
siliconflow-intl(Silicon Flow,国际站)https://api.siliconflow.com/v1/v1/systemoneSILICONFLOW_INTL_API_KEYKev-4B(可改 SemIf)
custom用户填写(必填)按 端点风格 拼customApiKeyEnv(缺省 DECISION_ENGINE_CUSTOM_KEY)customModel

硅基流动两个站点的接入文档: 国内站 api-docs.siliconflow.cn · 国际站 docs.siliconflow.com

开箱即用:siliconflow / siliconflow-intl 只需要粘一个 API key——地址、 路径(/systemone)、默认模型(Kev-4B)都已预置。两个站点是两套账号, key 互不通用,所以各占一格。

每个 provider 有各自的 key 名,各自保存各自的 key。 五者互不覆盖, 可以同时配好五套凭据;切换 provider 不会串用别人的 key。

不写 provider 时等于 bamboonode,老配置行为完全不变。

模型字段按 provider 分开存,切换 provider 不会把 A 的模型名发给 B:

provider配置键默认
bamboonodemodel与历史一致(留空即服务端默认)
system onesystemoneModel留空 = 服务端默认(jev-latest)
siliconflowsiliconflowModel留空 = 预设默认 Kev-4B
siliconflow-intlsiliconflowIntlModel留空 = 预设默认 Kev-4B
customcustomModel留空 = 服务端默认

⚠️ 两个 SiliconFlow 端点把 model 列为必填(实测省略或空串都回 400 {"code":20013,"message":"Model field is required."}),所以它们的 「留空」不是「让服务端挑」,而是用预设默认 Kev-4B;想换模型填 SemIf。 官方目前只支持这两个决策模型,界面上的模型下拉也只列这两个—— /v1/models 会返回 98 个模型,但 chat 模型打到 /systemone 会 400 Model does not exist.,列出来就是「选了就失败」。

模型名大小写不敏感:kev-4b / Kev-4b / Kev-4B 实测都 200, 配置里三种写法等价。

custom:填写方式与限制

在设置界面选 custom 后需要填:

字段必填说明
Base URL是必须是 http:// 或 https:// 地址。不支持其它协议(ftp: 等会被拒绝),不带 scheme 也会被拒绝——自定义地址必须显式,避免猜错协议。不能含空格、换行或查询串。
端点风格否auto(默认)/ decisions / systemone。这是首选路径,不是唯一路径:首选返回 404 时插件会自动改用另一条再试一次(见下)。
附加请求头否格式 名称: 值,多项用分号分隔,如 X-Tenant: team-a; X-Trace: dsh。authorization / content-type / host 由插件固定,写了也会被忽略。
密钥变量名否填了就从该环境变量或 ~/.dsh/.credentials.yaml 的 refs: 段取 key;也可以直接在 API Key 输入框粘贴。
模型否留空 = 由服务端选默认;实测留空比发空串安全(发 "model": "" 会被 400)。

地址拼接规则(末尾斜杠一律先去掉,因此 .../v1/ 与 .../v1 等价):

你填的 base URL端点风格实际 POST
https://x.devdecisionshttps://x.dev/v1/decisions
https://x.dev/v1decisionshttps://x.dev/v1/decisions
https://x.dev/v1/decisionsdecisionshttps://x.dev/v1/decisions(不重复)
https://x.dev/api/v1decisionshttps://x.dev/api/v1/decisions
https://x.dev/v1systemonehttps://x.dev/v1/systemone
https://x.dev/v1/systemonedecisionshttps://x.dev/v1/systemone(已带端点即原样)

拼接是幂等的:对结果再拼一次不会变成 /v1/v1/decisions 或 //decisions。

端点路径由服务商决定,不由模型决定

这是本插件最容易踩空的一处,因此实测过(2026-10-25,真实 key 打真实端点):

服务商模型/v1/decisions/v1/systemone
SiliconFlowKev-4B404 Not Found200 ✅
bamboonodestartlux-decision-27b-autothink200 ✅200 ✅

也就是说,Kev-4B 并不是「只能用 /systemone 的模型」——路径由服务商决定。 同一个 Kev-4B 换一个服务商,路径就得跟着换。

因此 端点风格 只是「首选」:

  1. 先打首选路径;
  2. 收到 404(= 路径不存在)时,自动改用另一条再试一次;
  3. 试通的那条会被记住(按 base URL 区分),下次直接打它,不再多一次往返;
  4. 两条都 404 时,遥测与「测试端点」会列出试过的两个地址并提示改端点风格。

只有 404 会触发换路径:400(模型/内容不合法)、401(key 不对)、5xx 都与路径无关, 换路径只是多烧一次请求。配置页的「生效端点」下方会显示「已验证可用:…」。

旧版本没有回退:路径踩空就是 404,而守卫是 fail-open 的——静默失效。 现场证据:telemetry.jsonl 里 2026-10-25 13:16–13:41 有 8 次 http-404 降级。

持久化与切换行为

  • 所有 provider 的配置并列存在 config.yml 里,互不覆盖: provider 记录当前选择,baseUrl/model 属于内置预设(bamboonode 的 baseUrl 保留老的覆盖语义),customBaseUrl/customModel/customApiKey/ customApiKeyEnv/customHeaders/customEndpointStyle 属于 custom。
  • 切换 provider 只改 provider 一个键:其它 provider 已保存的地址、模型、密钥 原样留着,切回来即恢复。
  • 保存后再次打开配置页按服务端返回的状态回显(不缓存、不推断),custom 的字段 除密钥明文外全部回显。
  • 密钥优先级:该 provider 的显式 key(systemoneApiKey / bamboonodeApiKey / siliconflowApiKey / siliconflowIntlApiKey / customApiKey)> 密钥存储位置上对应的键 > 环境变量。
  • 旧的单一 apiKey 字段只对 bamboonode 生效(历史兼容)。

密钥保存位置

密钥可以放在两个地方,默认 credentials:

keyStorage写到哪里适合
credentials(默认)~/.dsh/.credentials.yaml 的 refs: 段与其他服务(DeepSeek / ModelScope …)统一托管,插件目录不留明文
config插件配置目录下的 config.yml希望配置自包含、能随目录整体备份或迁移;注意是明文

在设置界面「密钥保存位置」下拉切换,切换即按新位置写入;已有的 key 不会被 自动搬家(避免"看不到的地方被改动"),需要时重新粘贴一次即可。

写入 credentials 的行为约束:

  • 只改 refs: 段,records:(DSH 的授权记录,含嵌套结构)与注释逐字保留;
  • 键名就是该 provider 自己的名字,所以五套 key 能同时存在、互不覆盖;
  • 传空值等于删除该键(清空即撤销,不留空壳行);
  • 文件权限固定 600;
  • 写入失败会整体报错,不会出现"配置说存了、实际没存"。

密钥优先级:显式 key 字段 > 所选存储位置里的对应键 > 环境变量。

错误与降级

情形表现
base URL 非法 / 为空(custom)该次落盘直接被拒(HTTP 400 + 具体原因,如「必须以 http:// 或 https:// 开头」),不会写进 yml;自动保存会一直挂着「未保存」并允许重试
密钥缺失不发请求,no-key 降级(记录里带该 provider 的变量名)
鉴权失败(401/403)auth-failed 降级,提示检查该 provider 的 key 是否有效、是否与地址匹配
地址错(404/400)http-404 / http-400 降级,记录里带 provider、endpoint 与响应体
provider 配置不可用invalid-base-url 降级,不打到猜出来的地址上
网络失败 / 超时network-error / timeout 降级,随后按阈值进入冷却

降级一律 fail-open:放行原行为,并写一条 telemetry.jsonl。 「测试端点」按钮会把完整诊断(provider、实际端点、HTTP 码、请求 id、响应体)直接显示出来。

决策点(参与哪些环节)

这个插件的定位不是「工具调用风险闸门」,而是一个便宜的判断层:用一次 约 1.9k token 的小模型调用,去替代或阻止更贵的 LLM 开销。因此判断必须克制—— 只在「替代更贵的调用」或「阻止更大的开销」处介入,绝不为了「多一道检查」而介入。

八个决策点全部已实装,由你决定让决策模型进入哪些环节。

#决策点挂载位置触发闸(为什么不是每次都问)生效方式省 token 机制
1工具授权tools/pre-execute只读工具 / auto 预设 / 排除名单直接放行ask / deny替代 auto-review 的满上下文审查
2模型路由agent/request每轮仅前 N 步 + 路由未变不问 + 新用户输入不问替换 provider/model机械步骤下沉到便宜模型
3子任务分派agent/pre-step仅每轮第一步且带新用户输入注入一条提示少开一个 subagent = 省一整个上下文
4上下文压缩agent/pre-step → ctx.compaction仅实测 token 达触发线 ×70%先跑确定性裁剪,让摘要被跳过省掉一次摘要调用
5轮次延续agent/turn-stopping同一轮最多续一次agent.steer()避免空转一整轮
6检索必要性tools/pre-execute仅检索类工具;其余工具零开销deny省搜索与结果 token
7技能选择tools/pre-execute仅 skill 工具且候选 ≥2deny + 指路省误加载的技能正文
8思维链翻译门槛包住 translator.translate()本地预筛 + 同文本缓存跳过并返回原文按条省翻译调用

三态的含义

模式行为
关闭连请求都不发。省 token,也省那约 2 秒延迟
观察照常判断并记录「本该怎样」,但不改变任何行为
生效按判断执行该点的处置

新接入的决策点都应当先跑 观察,确认与人工判断一致后再切 生效。

阈值是实测校准的,不是拍脑袋的

每个决策点的阈值都用真实端点跑过对照样本(「该判 A」与「该判 B」成对输入), 只有两类输入真的分得开才留着。校准过程发现并修掉了三个问题,值得记下来:

  1. choice 的 confidence 是被选中项的概率。三选项平分时天然只有 0.33, 所以分派点的门槛不能设 0.6,否则永远判不出 advise。现在按「显著高于均分」 取 0.5。
  2. question 的问法决定一切。原来问「这个信息是否已知道」,两类输入给出 0.30 / 0.12 —— 完全没有区分力。改问「recent 里是否逐字覆盖了这个事实, 只依据那段文本回答」之后变成 0.92 / 0.01。
  3. 判据顺序会改变结果。轮次延续若先看 closed,「明确未完成」那组 因为 closed=0.28 不达线就会掉进默认 stop——而它恰恰该续轮。 现在先看 unfinished。

校准后的实测对照(scripts/verify-endpoint.mjs + 成对样本):

决策点输入 A判定输入 B判定
模型路由机械步骤switch新架构设计keep
子任务分派三个包并行补测试advise变量改名inline
上下文压缩结论已落地prune对话承载状态summarize
轮次延续任务已完成stop明确还有三步continue
检索必要性上下文已覆盖skip完全没提过allow
思维链翻译英文→中文translate中文→中文skip

为什么不是「所有 hook 都开」

决策模型每次约 1.9k token、p50 约 0.5 秒。agent/pre-step 与 agent/request 都是每步触发,两个都开且不加闸,等于每步多等 1 秒以上。所以热路径上的 决策点(注册表里标了 hotPath,配置页显示「热路径」角标)都带明确的触发闸, 不能无条件调用。

护栏是双层缓存(见下节):命中缓存就不发请求,把热路径上的重复调用挡掉。 本插件不设每小时的 token 预算——一次决策约 1.9k token,命中缓存后几乎为零, 不值得用「超限即停止判断」的方式省,那会让守卫在最需要它的时刻失效。

模型选择

配置页的「模型」是下拉框,由服务商实时提供,另有「自动(服务端默认)」选项。

默认就是「自动」(config.yml 里 model: 留空):插件不发送该字段, 服务端用它当前的默认决策模型。实测省略 model 返回 200,响应里回填实际使用的模型名:

{"answers":{...},"model":"startlux-decision-27b-autothink","usage":{...}}

这样即便服务商将来更换模型名,留空配置也不会因此 400。

注意:config.yml 里的 model: 必须写出来但留空(model: 后面不跟值)。 解析器把显式空值保留为 "";若整行删掉,键会缺失并回落到内置的硬编码模型名, 「自动」就不生效了。

列表为何要过滤:bamboonode 的 /v1/models 返回全部 20 个模型,其中多数是 chat 模型,拿去打 /v1/decisions 会 400 model ... is not served by this plugin。 因此对 bamboonode 按 supported_endpoint_types 只保留声明了 StartLux Decision / StartLux SystemOne 的模型。实测 typesafe/jev-1.14 虽声明 jev-decisions,仍被该端点拒绝,因此也排除。

对 system one / custom:自建服务的 /v1/models 往往不带 supported_endpoint_types,继续按类型过滤会得到空列表(用户会以为服务没有可用 模型)。因此这两种 provider 改为「有类型就按类型过滤,没类型就全收」,下拉里也 仍可手填。

对 siliconflow / siliconflow-intl:它们的 /v1/models 返回 98 个模型、 没有一条带 supported_endpoint_types,而决策端点只认 Kev-4B / SemIf (chat 模型打过去是 400 Model does not exist.)。类型过滤拿不到任何线索, 全收又等于「96 个选了就失败的选项」,因此按预设里的 knownModels 白名单筛, 下拉里只列这两个。

超时与自动回退

每次请求都带硬超时(默认 8000ms),超时不是错误,而是降级放行:

AbortSignal.timeout(timeoutMs) → 超时 → degrade("timeout") → 钩子 next() 放行

三层保护,保证守卫永远不会拖住工具执行:

  1. 单次超时:timeoutMs(默认 8000ms,可在配置页调整,最小 1000ms)。 实测该端点 p50≈0.7s、冷启动 p95≈6.4s,8s 覆盖冷启动又不至于让用户干等。
  2. 连续失败冷却:连续 failureThreshold(默认 3)次失败后进入 cooldownMs(默认 60s)冷却,冷却期内直接放行、0ms 返回, 不再对着故障端点反复消耗。
  3. 调用方取消:若上层传入 AbortSignal 且被中止,立即降级返回 caller-aborted。

实测(指向不可路由地址,timeoutMs: 1500):

第1次: 1504ms → 降级      第4次: 0ms → 降级   ← 冷却生效
第2次: 1502ms → 降级      第5次: 0ms → 降级
第3次: 1502ms → 降级(触发冷却)

没有配置 key 时根本不发请求,直接 no-key 降级,零延迟。

失败降级(fail-open)

插件的设计原则是任何不确定都退回原生行为,绝不因为守卫自身故障而卡住工具调用。 以下情形全部 next() 放行,并在 telemetry.jsonl 记一条 action=degraded:

情形detail
插件未启用(enabled: false)不进入判断,直接放行
没有 API keyno-key
命中失败冷却cooldown
端点 4xx / 5xxhttp-404 / http-500 等(附 endpoint、body)
网络不可达network-error
超时timeout(附 timeoutMs)
返回非 JSON / 非对象invalid-json / invalid-response
缺 answers 字段missing-answers
provider 配置不可用(如 custom 的 base URL 非法)invalid-base-url(附 provider 与原因)
鉴权被拒(401/403)auth-failed(附 provider、status、该 provider 的密钥变量名)
答案形状不符(类型错、缺字段、choice 非法、概率越界/NaN)answer-shape-mismatch
守卫内部抛异常捕获后放行,仅 logger.warn
只读工具 / auto 权限预设 / 被排除工具不判断,直接放行

默认 mode: shadow 时,即使判断出 deny 也只记录不执行。

省 token 的设计

  • 双层缓存:

    • 精确:完整 state + 问题集一致即复用,绝对安全。

    • 同类复用:键是「工具名 + 命令族 + 风险标记」的归一化签名, 只有低风险放行的裁决才进这一层;ask / deny 永不复用, 否则会把一次危险判断错误地扩散到同类操作。

      这一层是必要的:旧实现只用精确键,而键里含 args_preview, 于是实测 309 次调用命中 0 次——等于缓存完全没工作。 现在 4 条同类 node -e '...' 变体只需 1 次真实请求(命中率 60%), 而 rm -rf 不会复用任何缓存裁决。

  • thinkMode 默认 never:实测主要省延迟(约 1.9s vs 7.2s);输入 token 基本不变 (该端点 output_tokens 恒为 0,思考开销未计入计量)。三档可在配置页「思考模式」 下拉切换,或手改 config.yml:

    值行为适用
    never不思考。默认值热路径首选,不想拖慢每次工具调用
    auto仅在模型自己判断需要时思考折中
    always总是思考高风险场景、排查误判时临时使用;每次约多等 5s

    非法值(如 thinkMode: sometimes)会被归一成 never,不会被原样发给服务端: 配置来自自研 YAML 子集解析器,绕过了 Schemastery 校验,因此取值必须在这里兜。

  • 只读工具直接放行:read/grep/glob/todo_write 等不发请求。

  • state 截断:args 走头尾摘要,recent 只带最近 4 条、每条 240 字符。

  • 失败冷却:连续 3 次失败进入 60s 冷却,避免对着故障端点烧钱。

  • 双层缓存:精确命中与同类复用都不发请求,热路径上的重复判断几乎零成本。

  • 默认只观察:新决策点先跑 observe,确认与人工判断一致后再切 enforce。

验证链路

自动化验收(三条,全部可重复跑)

pnpm test              # 113 项单元测试:纯函数 + 适配器接线 + providers + 配置往返 + 设置界面渲染
pnpm verify:wiring     # 接线检查:用真实 schemastery 加载插件
pnpm verify:endpoint   # 8 个决策点逐个打真实端点,校验题型与答案形状
pnpm verify            # 前两条一起跑

四个测试文件各管一层,互不依赖宿主:

文件覆盖
test/spec.test.js每个决策点的问法 / 解析 / 处置与阈值边界
test/engine.test.js适配器接线、off/observe/enforce 语义、全部降级分支
test/providers.test.jsprovider 预设、地址拼接、URL 校验、鉴权与请求构造
test/config-file.test.jsconfig.yml 读写往返:保存后再打开的回显、切换不互相覆盖
test/client-ui.test.js设置界面渲染与交互:provider 下拉、custom 字段、错误提示、改动即保存(无保存按钮、改动自动 PUT /config、API Key 失焦提交)

test/providers.test.js 是 provider 改造的验收用例(不依赖宿主依赖, 裸仓库可直接跑),覆盖六项硬约束:

  1. 内置 system one 可用(默认地址 / /v1/systemone / 自己的密钥变量名)
  2. 内置 bamboonode 行为不变(默认端点、老 baseUrl 覆盖、老模型名)
  3. custom base URL 可用(含端点风格、请求头、密钥来源优先级)
  4. 非法 URL 被拒绝(缺 scheme、非 http(s)、含空格、带查询串、空值)
  5. 末尾斜杠与端点拼接正确、幂等,不出现 /v1/v1 或 //decisions
  6. 请求构造不再硬编码 bamboonode.dev(含源码级断言:lib/index.js 与 lib/client.js 里不得再出现该域名)

verify:wiring 需要 profile 的依赖可解析。仓库里没有 node_modules 时先建链接:

ln -s ~/.dsh/profiles/desktop/node_modules node_modules

它检查的是「宿主到底给不给这个服务」——包括缺 translator / sessionTitle 时 apply() 不抛、所有监听器都注册上、每个新点默认 off(升级不改变行为)。

verify:endpoint 要网络与对应 provider 的密钥(bamboonode 默认读 BAMBOO_API_KEY, 可用 DECISION_ENGINE_BASE_URL + DECISION_ENGINE_API_KEY 指向其它 system-one 服务),它逐点校验: 端点接受该点的问题集(choice 必须有 criteria、score 必须 2–10 档、 题型只认三种)、答案能被 parse() 解析、decide() 产出合法 kind。 这一步不能省:题型写错会在运行时 400 并静默降级,光看日志很难发现 「这个点其实一直没生效」。

手工验证

  1. 「测试端点」按钮 → 看到 HTTP 200 和 x-oneapi-request-id,说明配置与网络都对; 诊断文本第一行就是当前 provider,接着是实际 POST 的完整端点。
  2. 插件配置页 → 确认 服务提供方、生效端点、模型、密钥 已配置 ✅; 决策点列表里每个点都应有 关闭 / 观察 / 生效 三态(没有「即将支持」)。 若某个点显示 宿主缺服务,说明宿主没有它依赖的服务(如 translator), 该点无法开启——这不是 bug,是不适用的部署。
  3. 把某个点切到 观察,触发一次对应动作 → 看 ~/.dsh/dsh-decision-engine/telemetry.jsonl(首次升级时旧目录的历史记录 会被自动拷过来):
    • action=degraded, detail=no-key → key 没读到
    • detail=http-404 / http-400 → 地址或模型不对(记录里带 endpoint 和 body)
    • detail=answer-shape-mismatch → 端点返回的答案形状不对(该点的问题集要复核)
    • point=route, action=switch → 该点判出了「本该下沉到便宜模型」
    • 每条的 point 字段标明是哪个决策点,scores 里是原始判断分数
  4. 服务商后台 → 出现 type=2 · Action decision 记录。

阈值

工具授权

键默认含义
guardDenyConfidence0.85高风险 + 高置信 + 不可逆 → deny
guardIrreversibleDenyFloor0.5判定不可逆的门槛
guardIrreversibleCeiling0.2低风险但不可逆 → 升级询问
guardConfidenceMin0.6中风险置信下限,低于则放行
guardInjectionAskThreshold0.8注入嫌疑 → 升级询问

其余八个决策点

全部阈值都在 config.yml 里可覆盖,默认值按实测校准(见上文「阈值是实测 校准的」)。下表列出「改动它会怎样」:

键默认调高会怎样调低会怎样
routeCheapProvider / routeCheapModel空留空即不路由,点开着也只记录—
routeMaxStepsPerTurn3每轮判断更多步,更准但更慢更快但后续步骤沿用旧裁决
routeMaxScore0.4更少下沉(更保守)更多下沉(更省但可能降质)
routeMechanicalFloor0.6更难判为机械(更保守)更易下沉
routeRiskFloor0.3风险稍高就保留强模型(更保守)更敢下沉
delegateConfidenceFloor0.5更少建议分派更多建议分派
delegateBlockingFloor0.6更容易判「被阻塞」→ 更少分派更敢分派
delegateParallelFloor0.5组队需要更强并行证据更容易建议组队
compactContextWindow0—设成实际窗口才能按比例提前介入
compactThresholdRatio0.7更晚介入(贴近 compaction 自己的 0.8)更早介入
compactDistilledFloor0.6更少裁剪(更保守)更多裁剪
compactVerbatinFloor0.6更少因「需要原文」而改走摘要更保守(更容易摘要)
turnStopMaxSteers1一轮可续多次(注意空转风险)0 = 永不续轮
turnStopUnfinishedFloor0.6更少续轮更易续轮
turnStopValueFloor0.35更少续轮更易续轮
retrievalCoveredFloor0.5更少跳过检索(更安全)更敢跳过
retrievalWouldAddCeiling0.8更敢跳过(更省)更少跳过
skillPickConfidenceFloor0.5更少改指路更敢改指路
cotProseFloor0.5更少翻译(更省)更敢翻译
cotSameLanguageFloor0.8更少判「已是目标语言」→ 更多翻译更敢跳过

License

MIT