@leonhardthust/dsh-decision-engine 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ main model acts.
30
30
  **Modes**: off · observe · enforce
31
31
 
32
32
  **Providers**: system one · bamboonode · 硅基流动 SiliconFlow (CN / Global) ·
33
- Perplexity · OpenRouter · custom base URL
33
+ Alibaba Bailian · TokenHub · Perplexity · OpenRouter · custom base URL
34
34
 
35
35
  **Safety level (how often to ask)**: Don't ask · Ask less · Default · Ask more
36
36
  (none of the four changes permission-mandated approval or denial)
package/README.zh.md CHANGED
@@ -29,7 +29,7 @@
29
29
  **三态**:关闭 · 观察 · 生效
30
30
 
31
31
  **决策服务**:system one · bamboonode · 硅基流动 SiliconFlow(国内 / 国际)·
32
- Perplexity · OpenRouter · 自定义 base URL
32
+ 阿里云百炼 · TokenHub · Perplexity · OpenRouter · 自定义 base URL
33
33
 
34
34
  **安全档位(询问频率)**:不问 · 少问 · 默认 · 多问(四档都不改变权限强制的人工批准与拒绝)
35
35
 
@@ -42,13 +42,20 @@ mode: shadow
42
42
  # ============================================================================
43
43
  # 服务提供方(provider)
44
44
  # ============================================================================
45
- # 七选一:
45
+ # 九选一:
46
46
  # bamboonode 内置。StartLux 官方端点 POST /v1/decisions,本插件的原有默认值。
47
47
  # systemone 内置。任意 System One 协议服务 POST /v1/systemone,
48
48
  # 默认指向 TypeSafe(https://api.typesafe.ai/v1)。
49
49
  # siliconflow 内置。硅基流动(国内站)https://api.siliconflow.cn/v1/systemone
50
50
  # siliconflow-intl 内置。Silicon Flow 国际站 https://api.siliconflow.com/v1/systemone
51
51
  # (与国内站是两套账号,key 互不通用)
52
+ # bailian 内置。阿里云百炼决策模型(POST /compatible-mode/v1/systemone)。
53
+ # 需要 Workspace ID + 地域,端点域名由二者拼出;model 必填
54
+ # decision-model-preview;key 用 DASHSCOPE_API_KEY。
55
+ # tokenhub 内置。TokenHub(tokenhub.com)的 System One 端点
56
+ # https://us-api.tokenhub.com/v1/systemone。
57
+ # 注意:腾讯云 TokenHub 只有 OpenAI/Anthropic 兼容的文本/图像/
58
+ # 视频/3D 接口,**没有决策端点**;两者同名,不是同一个产品。
52
59
  # perplexity 内置。Perplexity Decider(api.perplexity.ai);
53
60
  # 地址与默认模型待实测,端点风格 auto(404 自动换路径重试)
54
61
  # openrouter 内置。OpenRouter(https://openrouter.ai/api/v1),
@@ -96,6 +103,25 @@ systemoneModel: ""
96
103
  siliconflowModel: ""
97
104
  siliconflowIntlModel: ""
98
105
 
106
+ # ---- 阿里云百炼(provider=bailian)------------------------------------------
107
+ # 决策模型:POST /compatible-mode/v1/systemone,model 必填 decision-model-preview。
108
+ # 文档:https://help.aliyun.com/zh/model-studio/decision-model-api
109
+ # 端点域名包含 Workspace ID,因此必须填 workspace 与地域:
110
+ # https://{workspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone
111
+ # 地域只支持 cn-beijing(默认)与 ap-southeast-1;API Key 必须与地域匹配。
112
+ # 鉴权用 DASHSCOPE_API_KEY(或下方的 bailianApiKeyEnv 指定的变量)。
113
+ # bailianWorkspaceId: ws_xxx
114
+ # bailianRegion: cn-beijing
115
+ # bailianModel: decision-model-preview
116
+
117
+ # ---- TokenHub(provider=tokenhub)-------------------------------------------
118
+ # tokenhub.com(TypeSafe)的 System One 决策端点:
119
+ # https://us-api.tokenhub.com/v1/systemone
120
+ # 模型可选 jev-latest / jev-1-13;鉴权用 TOKENHUB_API_KEY。
121
+ # 再次提醒:腾讯云 TokenHub(tokenhub.tencentmaas.com)没有决策端点,
122
+ # 不要拿腾讯云的 key 打这里(会 401)。
123
+ # tokenhubModel: jev-latest
124
+
99
125
  # ---- custom 预设(provider=custom 时才生效)--------------------------------
100
126
  # base URL 必填,且必须是 http/https 地址;不带 scheme 会被拒绝(内置预设
101
127
  # 允许省略 scheme,custom 不允许——自定义地址必须显式,避免猜错协议)。
package/docs/releasing.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # npm 发布
2
2
 
3
- 发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.11.1`,公开包。
3
+ 发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.12.0`,公开包。
4
4
 
5
5
  本分支基于 `zuosus/dsh-decision-engine` 的 MIT 代码,保留原作者与许可证;npm scope、源码仓库、bundle 引用和浏览器主模块 / charts chunk 标识已同步为本分支身份。内部 API、样式标识和用户数据目录保留旧名称以兼容已有配置。不要同时启用原包与本 scoped fork,它们提供相同的服务与页面。
6
6
 
@@ -9,7 +9,7 @@
9
9
  - `.github/workflows/ci.yml`:main push、PR、手动运行或发布工作流复用;Ubuntu 24.04 + Node.js 24 + Vite+ 1.6.0。执行 frozen install、完整测试与接线、隔离情景、实际打包和 tarball 检查。失败不会被忽略。
10
10
  - `.github/workflows/publish.yml`:每次 `main` push 自动检测版本。先比较整个 push 的 `before` 与当前 manifest,拒绝错误包名、非法或倒退版本;再只读查询 npmjs。当前版本已上传则跳过;当前版本高于 npmjs latest 且未上传则进入完整验证及发布。多提交 push 比较整个 push 前后,不仅比较 HEAD 的父提交。
11
11
  - 后续发版只需修改 `package.json` 版本并提交到 main;无需创建 tag 或手动点运行。发布依赖检测和本次提交的完整 CI 均成功,不跳过测试。稳定版本发布到 `latest`,预发布版本自动进入 `next`。
12
- - 当前 `1.11.1` 包含 DEC-070:空闲但仍可接续的同一棵树的子代理不再被误判为「接收方权限未核验」,普通队友消息不再升级成人审/拒绝。`1.11.0` 的第二轮审计修复见 DEC-067 ~ DEC-069,`1.10.0` 的五项改动见 DEC-062 ~ DEC-066。
12
+ - 当前 `1.12.0` 包含 DEC-072 / DEC-073:新增阿里云百炼(workspace + 地域拼端点)与 TokenHub(tokenhub.com;腾讯云 TokenHub 无决策端点)预设,设置页暴露百炼的 Workspace ID / 地域;「免费模型」从独立开关合并为 provider 下拉第一项。`1.11.2` 的 never 回退、`1.11.1` 的 DEC-070、`1.11.0` 的审计修复、`1.10.0` 的五项改动依次见 DEC-062 ~ DEC-071。
13
13
  - checkout 与 setup-vp Action 固定到已核对的 commit SHA;不通过 `pull_request_target` 运行不可信 PR。
14
14
  - 验证任务只有 `contents: read`;只有发布 job 有 `id-token: write`。发布 step 只使用 OIDC(trusted publishing),不再读取任何 npm token secret。代码 checkout 不保留 Git 凭据。此工作流不会自动修改版本、提交源码或推送 Git。
15
15
 
@@ -58,7 +58,7 @@ Windows 全量测试最近仍有 3 个既有 POSIX 文件权限断言失败;
58
58
  授权及完整验证均满足后:
59
59
 
60
60
  ```powershell
61
- npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.11.1.tgz --registry "https://registry.npmjs.org/" --access public --tag latest
61
+ npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.12.0.tgz --registry "https://registry.npmjs.org/" --access public --tag latest
62
62
  npm view @leonhardthust/dsh-decision-engine version --registry "https://registry.npmjs.org/" --json
63
63
  ```
64
64
 
@@ -66,4 +66,4 @@ npm view @leonhardthust/dsh-decision-engine version --registry "https://registry
66
66
 
67
67
  ## 安装
68
68
 
69
- DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.11.1`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
69
+ DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.12.0`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
package/lib/client.js CHANGED
@@ -102,6 +102,11 @@ window.__ModuleLoader__.load({
102
102
  "服务端未返回 provider 列表;当前选择仍会生效": "The server returned no provider list; your current choice still applies",
103
103
  "自定义决策服务:自建 llama.cpp / vLLM、企业网关或第三方中转": "Custom decision service: self-hosted llama.cpp / vLLM, corporate gateway, or relay",
104
104
  "服务提供方": "Provider",
105
+ "免费模型(服务端下发)": "Free model (server-provided)",
106
+ "Workspace ID": "Workspace ID",
107
+ "地域": "Region",
108
+ "百炼决策端点的域名是 {template},Workspace ID 是域名的一部分,必填。填写业务空间 ID(形如 ws_xxx)": "Bailian's decision endpoint host is {template}; the Workspace ID is part of the host and is required. Enter the business workspace ID (for example ws_xxx)",
109
+ "百炼只提供 cn-beijing 与 ap-southeast-1 两个决策地域;API Key 必须与地域匹配": "Bailian offers only the cn-beijing and ap-southeast-1 decision regions; the API key must match the region",
105
110
  "生效端点": "Effective endpoint",
106
111
  "由上方 base URL + 端点风格拼接;已带 /decisions 或 /systemone 时原样使用": "Built from the base URL above plus the endpoint style; kept as-is when it already contains /decisions or /systemone",
107
112
  "内置预设的默认地址(可用 config.yml 的 baseUrl 覆盖 bamboonode)": "Built-in preset default address (config.yml's baseUrl can override bamboonode)",
@@ -3117,6 +3122,12 @@ window.__ModuleLoader__.load({
3117
3122
  // 分开提交会让中间状态被当成最终配置。
3118
3123
  customHeaders: (provider.customHeaders ?? "").trim(),
3119
3124
  customApiKeyEnv: (provider.customApiKeyEnv ?? "").trim(),
3125
+ // 百炼的两格:workspace 空串也提交(用户可能清空,服务端会判 provider
3126
+ // 不可用并给出「需要 Workspace ID」),地域只在拿到字符串时提交。
3127
+ ...(typeof draft.bailianWorkspaceId === "string"
3128
+ ? { bailianWorkspaceId: draft.bailianWorkspaceId.trim() }
3129
+ : {}),
3130
+ ...(typeof draft.bailianRegion === "string" ? { bailianRegion: draft.bailianRegion } : {}),
3120
3131
  };
3121
3132
  // customBaseUrl 只在**合法**时提交。空串对内置 provider 是合法的
3122
3133
  //(等于不覆盖),对 custom 则是必填缺失——那种情况整笔延后,见 flush()。
@@ -3175,6 +3186,10 @@ window.__ModuleLoader__.load({
3175
3186
  ["customEndpointStyle", view.customEndpointStyle ?? "auto"],
3176
3187
  ["customHeaders", view.customHeaders ?? ""],
3177
3188
  ["customApiKeyEnv", view.customApiKeyEnv ?? ""],
3189
+ // 百炼的两格:与服务端回显比,否则「改了 workspace 但不触发保存」
3190
+ // ——输入框看起来能编辑,实际永远写不回 config。
3191
+ ["bailianWorkspaceId", view.bailianWorkspaceId ?? ""],
3192
+ ["bailianRegion", view.bailianRegion ?? "cn-beijing"],
3178
3193
  ["keyStorage", view.keyStorage ?? "credentials"],
3179
3194
  // 决策历史(DEC-022):与服务端的生效值比。服务端回传的是
3180
3195
  // `cfg.decisionsTabEnabled !== false` 这种**生效值**(不是原值),
@@ -6626,7 +6641,7 @@ window.__ModuleLoader__.load({
6626
6641
  }, []);
6627
6642
  // 语言变化时重绘本页。放在所有 useState 之后:测试是按「第 n 个
6628
6643
  // useState」注入状态的,插在中间会把后面的槽位全部挪位。
6629
- useLang();
6644
+ const uiLang = useLang();
6630
6645
  /**
6631
6646
  * 待落盘值的唯一真源。
6632
6647
  *
@@ -6680,6 +6695,10 @@ window.__ModuleLoader__.load({
6680
6695
  // 跟随服务端下发(DEC-030):默认关;与服务端保持一致,
6681
6696
  // 否则保存后开关会回弹。
6682
6697
  serverProvider: next.serverProvider?.enabled === true,
6698
+ // 百炼的 workspace / 地域:服务端状态里有就必须回填,否则切回
6699
+ // 百炼时输入框是空的(用户会以为自己没填过)。
6700
+ bailianWorkspaceId: next.bailianWorkspaceId ?? "",
6701
+ bailianRegion: next.bailianRegion ?? "cn-beijing",
6683
6702
  // 安全档位(DEC-035):同样从服务端派生,否则点滑块后回弹。
6684
6703
  tolerance: next.tolerance?.level ?? "default",
6685
6704
  // 强制安全核心 / 工具授权三态 / 自动审查回退:整组从服务端派生
@@ -6850,6 +6869,9 @@ window.__ModuleLoader__.load({
6850
6869
  providerError: "",
6851
6870
  providerCatalog: [],
6852
6871
  baseUrl: "",
6872
+ // 百炼的两格:status 缺失时给安全默认,界面不会读到 undefined。
6873
+ bailianWorkspaceId: "",
6874
+ bailianRegion: "cn-beijing",
6853
6875
  // 探测出的可用路径:status 失败时留空(界面不显示那一行)。
6854
6876
  detectedEndpoint: "",
6855
6877
  model: "",
@@ -6915,6 +6937,9 @@ window.__ModuleLoader__.load({
6915
6937
  approvalDetailReadable: view.approvalDetailReadable !== false,
6916
6938
  // 跟随服务端下发(DEC-030):默认关(与服务端 DEFAULTS 一致)。
6917
6939
  serverProvider: view.serverProvider?.enabled === true,
6940
+ // 百炼的两格:首屏缺了它们,输入框会先空一帧再跳值(看起来像没保存)。
6941
+ bailianWorkspaceId: view.bailianWorkspaceId ?? "",
6942
+ bailianRegion: view.bailianRegion ?? "cn-beijing",
6918
6943
  // 强制安全核心 / 工具授权三态 / 自动审查回退:缺任何一格,首屏就会
6919
6944
  // 在服务端说「开」的同时把控件画成关/空——用户顺手拨一下就把
6920
6945
  // 错误的值存下去了(与上面两个开关同一类事故)。
@@ -7026,6 +7051,9 @@ window.__ModuleLoader__.load({
7026
7051
  approvalDetailReadable: next.approvalDetailReadable !== false,
7027
7052
  // 跟随服务端下发(DEC-030):同样是「生效值」,必须从服务端派生。
7028
7053
  serverProvider: next.serverProvider?.enabled === true,
7054
+ // 百炼两格同样是生效值:保存成功后必须对齐,否则输入框会回弹。
7055
+ bailianWorkspaceId: next.bailianWorkspaceId ?? "",
7056
+ bailianRegion: next.bailianRegion ?? "cn-beijing",
7029
7057
  // 安全档位(DEC-035):同样必须从服务端派生,否则保存后回弹。
7030
7058
  tolerance: next.tolerance?.level ?? "default",
7031
7059
  // 强制安全核心 / 工具授权三态 / 自动审查回退:保存成功后必须整组
@@ -7616,84 +7644,147 @@ window.__ModuleLoader__.load({
7616
7644
  //
7617
7645
  // 页脚(保存状态 / 测试端点 / 状态信息)**不进任何分组**:它是所有
7618
7646
  // 分组的共同结论(刚才存没存、端点通不通),收进某一组反而会被误读。
7647
+ const serverSelected = draft.serverProvider === true;
7648
+ /**
7649
+ * provider 下拉的选中值:免费模式选中的是虚拟项 `server`。
7650
+ *
7651
+ * 服务端没有 `provider: "server"` 这种配置(`serverProvider` 仍是独立
7652
+ * 的布尔键),所以这里只做**展示层合并**:选中 server ≠ 改写 provider,
7653
+ * 取消免费模式时还能回到上次选的 provider。
7654
+ */
7655
+ const providerSelectValue = serverSelected ? "server" : providerDraft.provider;
7619
7656
  const connectionItems = [
7620
7657
  // ① 连接方式:地址从哪来、发给谁、用谁的 key、怎么存。
7621
7658
  // 第一行的「使用免费模型」是这一组的总开关:它决定下面这些项
7622
7659
  // 是服务端下发还是用户自己填,所以必须排在最前。
7623
- // 1) 使用免费模型(DEC-030,原名「跟随服务端下发」;DEC-037 改名并置顶):
7624
- // 地址 / 模型 / key 都改由服务端下发,不必自己填。
7660
+ // 1) 服务提供方(含「免费模型」选项)。
7625
7661
  //
7626
- // 放在整页的最上面:它是**地址来源**的决定——「用免费的,还是自己配」。
7627
- // 下面每一组的每一项都取决于它(发给谁、用谁的 key、守卫跑在哪家模型上),
7628
- // 所以它必须先被读到。
7662
+ // 免费模型以前是上面的一条独立开关:它和 provider 下拉是两个控件,
7663
+ // 却表达同一件事——「地址/模型/key 从哪来」。两个入口意味着两种
7664
+ // 不一致的状态(开关开着而 provider 指向别处、或反之),用户看不出
7665
+ // 最终到底用哪一套。现在合并成一个下拉:`server` 是第一项,选中它
7666
+ // 等于打开下发;选任何真实 provider 等于关闭下发。
7629
7667
  //
7630
- // 说明下面那行小字「可能会失效」是这件事的代价,不是附注:免费通道随时
7631
- // 可能停用,用户得知道本地手动配置仍是可靠后路。改名也是为了让用户先看到
7632
- // 「免费」这个能感知的事实,而不是「服务端下发」这个实现细节。
7633
- //
7634
- // 默认关:不打开时本插件的行为与之前逐字一致(全部走本地手动配置)。
7635
- row(
7636
- "使用免费模型",
7637
- serverHint,
7638
- h(
7639
- "div",
7640
- { style: { display: "flex", gap: "8px", alignItems: "center", flex: "none" } },
7641
- // 「重新获取」只在开着时出现:关着的时候它什么也不会变,摆在那里
7642
- // 只会让人以为这个功能坏了。
7643
- view.serverProvider?.enabled === true
7644
- ? h(
7645
- "button",
7646
- {
7647
- type: "button",
7648
- key: "server-refetch",
7649
- onClick: reloadServerProvider,
7650
- disabled: busy !== "",
7651
- style: { ...S.saveBtn, flexShrink: 0, padding: "6px 10px" },
7652
- },
7653
- busy === "serverProvider" ? "…" : t("重新获取"),
7654
- )
7655
- : null,
7656
- toggle(draft.serverProvider === true, () => {
7657
- const next = draft.serverProvider !== true;
7658
- const fresh = { ...draft, serverProvider: next };
7659
- setDraft(fresh);
7660
- // 开关是离散控件:拨一下就落盘。服务端据它决定是否去取下发值。
7661
- scheduleSave({ draft: fresh }, true);
7662
- }),
7663
- ),
7664
- "serverProvider",
7665
- "可能会失效",
7666
- ),
7667
- // 2a) 服务提供方:最高层的选择,决定地址与鉴权从哪来
7668
+ // 配置键仍是 `serverProvider`(已落盘契约),行为不变,只是入口从
7669
+ // 开关变成选项。
7668
7670
  row(
7669
7671
  "服务提供方",
7670
- providerHint,
7672
+ serverSelected
7673
+ ? h(
7674
+ "div",
7675
+ { style: { display: "flex", flexDirection: "column", gap: "4px" } },
7676
+ h("span", null, serverHint),
7677
+ view.serverProvider?.enabled === true
7678
+ ? h(
7679
+ "button",
7680
+ {
7681
+ type: "button",
7682
+ key: "server-refetch",
7683
+ onClick: reloadServerProvider,
7684
+ disabled: busy !== "",
7685
+ style: { ...S.saveBtn, alignSelf: "flex-start", padding: "4px 8px", marginTop: "2px" },
7686
+ },
7687
+ busy === "serverProvider" ? "…" : t("重新获取"),
7688
+ )
7689
+ : null,
7690
+ )
7691
+ : providerHint,
7671
7692
  h(
7672
7693
  "select",
7673
7694
  {
7674
- style: { ...S.input, minWidth: "180px", flex: "none" },
7675
- value: providerDraft.provider,
7695
+ style: { ...S.input, minWidth: "200px", flex: "none" },
7696
+ value: providerSelectValue,
7676
7697
  disabled: busy !== "",
7677
7698
  onChange: (event) => {
7678
7699
  const next = event.target.value;
7679
- setProviderDraft({ ...providerDraft, provider: next });
7700
+ if (next === "server") {
7701
+ // 保留 provider 原值:用户取消免费模式时回到上次选的 provider。
7702
+ const fresh = { ...draft, serverProvider: true };
7703
+ setDraft(fresh);
7704
+ scheduleSave({ draft: fresh }, true);
7705
+ return;
7706
+ }
7707
+ const fresh = { ...draft, serverProvider: false };
7708
+ const nextProviderDraft = { ...providerDraft, provider: next };
7709
+ setDraft(fresh);
7710
+ setProviderDraft(nextProviderDraft);
7680
7711
  // 切换 provider 只改「当前用哪个」,不动其它 provider 已保存的值:
7681
7712
  // custom 的字段留在 providerDraft 里,切回来还在。
7682
7713
  setProviderError("");
7683
7714
  // 下拉是离散控件:选完即存。切到 custom 但地址还没填时,
7684
7715
  // flush 会整笔延后并提示(见 flush 里的 urlProblem),
7685
7716
  // 地址补全后自动落盘,不需要再点一次。
7686
- scheduleSave({ providerDraft: { ...providerDraft, provider: next } }, true);
7717
+ scheduleSave({ draft: fresh, providerDraft: nextProviderDraft }, true);
7687
7718
  },
7688
7719
  },
7720
+ h("option", { value: "server", key: "server" }, t("免费模型(服务端下发)")),
7689
7721
  ...(catalog.length === 0
7690
7722
  ? [h("option", { value: providerDraft.provider, key: providerDraft.provider }, providerDraft.provider)]
7691
7723
  : catalog.map((item) =>
7692
- h("option", { value: item.id, key: item.id }, item.label),
7724
+ h(
7725
+ "option",
7726
+ { value: item.id, key: item.id },
7727
+ // 预设显示名由服务端下发:英文界面取 labelEn,中文取 label。
7728
+ // 只取 label 会让「阿里云百炼」「硅基流动」这类中文名在
7729
+ // 英文界面里原样出现。
7730
+ uiLang === "en" && typeof item.labelEn === "string" && item.labelEn.length > 0
7731
+ ? item.labelEn
7732
+ : item.label,
7733
+ ),
7693
7734
  )),
7694
7735
  ),
7695
7736
  "provider",
7737
+ serverSelected ? t("可能会失效") : null,
7696
7738
  ),
7739
+ // 1b) 百炼:workspace 参与域名拼接,不能塞进「自定义 provider」区
7740
+ // (那不是用户自建服务,只是地址需要两个参数)。两格都属于普通
7741
+ // 配置字段,跟 custom 的五个字段分开存。
7742
+ activeProvider?.dynamicBaseUrl === true && !serverSelected
7743
+ ? row(
7744
+ "Workspace ID",
7745
+ t(
7746
+ "百炼决策端点的域名是 {template},Workspace ID 是域名的一部分,必填。填写业务空间 ID(形如 ws_xxx)",
7747
+ { template: activeProvider.baseUrlTemplate ?? "" },
7748
+ ),
7749
+ h("input", {
7750
+ type: "text",
7751
+ style: S.input,
7752
+ placeholder: "ws_…",
7753
+ value: typeof draft.bailianWorkspaceId === "string" ? draft.bailianWorkspaceId : "",
7754
+ onChange: (event) => {
7755
+ const next = { ...draft, bailianWorkspaceId: event.target.value };
7756
+ setDraft(next);
7757
+ scheduleSave({ draft: next });
7758
+ },
7759
+ onBlur: () => scheduleSave(null, true),
7760
+ }),
7761
+ "bailianWorkspaceId",
7762
+ )
7763
+ : null,
7764
+ activeProvider?.dynamicBaseUrl === true && !serverSelected
7765
+ ? row(
7766
+ "地域",
7767
+ t("百炼只提供 cn-beijing 与 ap-southeast-1 两个决策地域;API Key 必须与地域匹配"),
7768
+ h(
7769
+ "select",
7770
+ {
7771
+ style: { ...S.input, minWidth: "190px", flex: "none" },
7772
+ value: typeof draft.bailianRegion === "string" ? draft.bailianRegion : "cn-beijing",
7773
+ disabled: busy !== "",
7774
+ onChange: (event) => {
7775
+ const next = { ...draft, bailianRegion: event.target.value };
7776
+ setDraft(next);
7777
+ scheduleSave({ draft: next }, true);
7778
+ },
7779
+ },
7780
+ ...(Array.isArray(activeProvider.regions) && activeProvider.regions.length > 0
7781
+ ? activeProvider.regions
7782
+ : ["cn-beijing", "ap-southeast-1"]
7783
+ ).map((region) => h("option", { key: region, value: region }, region)),
7784
+ ),
7785
+ "bailianRegion",
7786
+ )
7787
+ : null,
7697
7788
  // 2b) 模型行:custom 时移入下方「自定义 provider」区(见 modelRow 的说明)。
7698
7789
  // 每个 provider 用自己那一格(modelField 由服务端下发),分开存才能做到
7699
7790
  // 「切 provider 不覆盖其它 provider 已保存的模型」。
@@ -22,7 +22,18 @@ export function approvalOutcomeForCall(session, callId, toolName, afterSeq) {
22
22
  const numberOrNull = (value) => Number.isFinite(value) ? value : null;
23
23
  const identityOrNull = (value) => typeof value === "string" && value ? redactGuardData(value).replace(/[\r\n\0]/g, " ").slice(0, 256) : null;
24
24
  const idleEvaluation = () => ({ status: "not_requested", provider: null, model: null, latencyMs: null, reason: null });
25
- const automaticReview = (context, config) => context?.permissionPreset === "auto" || config?.tolerance === "autonomous";
25
+ /**
26
+ * 是否走「自动审查」:把软性询问交给回退审查者,而不是直接拒绝。
27
+ *
28
+ * 这里刻意**不**把 `approvalPolicy === "never"` 也算进来:这个开关同时
29
+ * 决定「模型失效 / 答案畸形 / 审查异常」是否走回退,而那条链路必须保持
30
+ * fail-closed(失败不能变成一次放行)。never 下真正需要改的只是「模型给出
31
+ * 了**有效**意见、结论是 ask」这一种情况——它在下面用 `automaticReview`
32
+ * 之外的分支单独交给 `guardFallback` 处理。
33
+ */
34
+ const automaticReview = (context, config) =>
35
+ context?.permissionPreset === "auto"
36
+ || config?.tolerance === "autonomous";
26
37
 
27
38
  export function mountGuard(ctx, deps) {
28
39
  const { getConfig, reloadOwnConfig, effectivePointModes, client, toleranceCalibrator, digestRecent, previewArguments, logDecision, recordGuardDetail } = deps;
@@ -224,7 +235,31 @@ export function mountGuard(ctx, deps) {
224
235
  } else if (primary.status === "failed") {
225
236
  action = { kind: context.approvalAvailable === true ? "ask" : "deny", reason: "advisory-failed-unknown-effect" };
226
237
  } else if (action.kind === "ask" && context.approvalPolicy === "never") {
227
- action = { ...action, kind: "deny" };
238
+ /**
239
+ * 审批通道不存在(DSH 的 `never` = 每个 ask 确定性 rejected)。
240
+ *
241
+ * 只有「模型给出了有效意见,且这次询问属于**软性**升级」才交给
242
+ * `guardFallback`——那是用户配置好的「没人可问时谁来裁决」。
243
+ *
244
+ * 刻意排除三类,它们必须保持 fail-closed,不能被回退模型洗成放行:
245
+ * - `mandatory.status === "unknown"`:工具副作用没判定,这不是
246
+ * 「软性询问」,而是一条未知边界(现有情景测试专门钉住
247
+ * `manual unknown + never` 不得伪造批准);
248
+ * - `risk.choice === "high"` / 注入达线:高危与注入嫌疑不接受
249
+ * 「再问一个模型」的软化;
250
+ * - 模型失效 / 答案畸形:没有可复核的有效意见(走上面的失败分支)。
251
+ */
252
+ const injectionThreshold = Number.isFinite(config.guardInjectionAskThreshold)
253
+ ? config.guardInjectionAskThreshold
254
+ : 0.8;
255
+ const softAsk = advisory?.status === "valid"
256
+ && mandatory.status !== "unknown"
257
+ && advisory.risk.choice !== "high"
258
+ && !(Number.isFinite(advisory.injectionP) && advisory.injectionP >= injectionThreshold)
259
+ && (advisory.risk.choice === "medium" || tolerated?.changed === true);
260
+ action = softAsk
261
+ ? await runFallback()
262
+ : { ...action, kind: "deny" };
228
263
  }
229
264
  // 档位把「默认放行」升级成「询问」时,`action.reason` 是档位 note(含
230
265
  // `severity=` 与阈值小数)——那是审计口径,不能当审批卡标题。这里换成
package/lib/index.js CHANGED
@@ -82,6 +82,8 @@ import {
82
82
  // key 只活在 ServerConfigSource 实例里,本文件只负责把它接进各解析点。
83
83
  import { SERVER_CONFIG_ENDPOINT, ServerConfigSource } from "./server-config.js";
84
84
  import {
85
+ BAILIAN_REGIONS,
86
+ DEFAULT_BAILIAN_REGION,
85
87
  DEFAULT_PROVIDER,
86
88
  DEFAULT_THINK_MODE,
87
89
  ENDPOINT_STYLES,
@@ -90,6 +92,7 @@ import {
90
92
  SERVER_PROVIDER_ID,
91
93
  SERVER_PROVIDER_LABEL,
92
94
  THINK_MODES,
95
+ bailianWorkspaceOf,
93
96
  buildEndpoint,
94
97
  buildHeaders,
95
98
  buildRequestBody,
@@ -153,6 +156,18 @@ const DEFAULTS = {
153
156
  */
154
157
  perplexityModel: "",
155
158
  openrouterModel: "",
159
+ /**
160
+ * 阿里云百炼决策模型:workspace + 地域拼出专用 endpoint。
161
+ *
162
+ * workspace 是域名的一部分(`https://{WorkspaceId}.{region}.maas.aliyuncs.com`),
163
+ * 因此它是**必填**,不像其它预设那样有公共默认地址;留空时 provider 直接
164
+ * 判为不可用并给出「需要 Workspace ID」。
165
+ */
166
+ bailianWorkspaceId: "",
167
+ bailianRegion: DEFAULT_BAILIAN_REGION,
168
+ bailianModel: "",
169
+ /** TokenHub(tokenhub.com)的决策模型;留空用预设默认 jev-latest。 */
170
+ tokenhubModel: "",
156
171
  /** 自定义 provider:地址、端点风格、鉴权、请求头、模型。 */
157
172
  customBaseUrl: "",
158
173
  customEndpointStyle: "auto",
@@ -165,6 +180,8 @@ const DEFAULTS = {
165
180
  siliconflowIntlApiKey: "",
166
181
  perplexityApiKey: "",
167
182
  openrouterApiKey: "",
183
+ bailianApiKey: "",
184
+ tokenhubApiKey: "",
168
185
  customModel: "",
169
186
  customHeaders: "",
170
187
  /** 各预设的密钥来源变量名,留空即用预设内置值。 */
@@ -174,6 +191,8 @@ const DEFAULTS = {
174
191
  siliconflowIntlApiKeyEnv: "",
175
192
  perplexityApiKeyEnv: "",
176
193
  openrouterApiKeyEnv: "",
194
+ bailianApiKeyEnv: "",
195
+ tokenhubApiKeyEnv: "",
177
196
  /**
178
197
  * 密钥保存位置:
179
198
  * credentials = ~/.dsh/.credentials.yaml 的 refs 段(默认,与其他服务统一托管)
@@ -448,6 +467,8 @@ export const Config = z.object({
448
467
  + "bamboonode = StartLux 官方端点(POST /v1/decisions,原有默认);"
449
468
  + "siliconflow = 硅基流动(api.siliconflow.cn,POST /v1/systemone);"
450
469
  + "siliconflow-intl = Silicon Flow 国际站(api.siliconflow.com);"
470
+ + "bailian = 阿里云百炼决策模型(workspace 专用域名,POST /compatible-mode/v1/systemone);"
471
+ + "tokenhub = TokenHub(tokenhub.com)的 System One 决策端点;腾讯云 TokenHub 没有决策端点;"
451
472
  + "perplexity = Perplexity Decider(api.perplexity.ai,待实测);"
452
473
  + "openrouter = OpenRouter(openrouter.ai/api/v1,待实测);"
453
474
  + "custom = 自己填地址(任意提供决策端点的服务,路径可自动回退)",
@@ -466,6 +487,16 @@ export const Config = z.object({
466
487
  .description("Perplexity Decider 预设的模型 id;留空用预设默认 sonar(端点 model 必填)"),
467
488
  openrouterModel: z.string().default(DEFAULTS.openrouterModel)
468
489
  .description("OpenRouter 预设的模型 slug;留空用预设默认 openai/gpt-4o-mini(请求必带 model)"),
490
+ bailianWorkspaceId: z.string().default(DEFAULTS.bailianWorkspaceId)
491
+ .description(
492
+ "阿里云百炼的 Workspace ID(如 ws_xxx):决策端点的域名就是 https://{workspaceId}.{region}.maas.aliyuncs.com,必填",
493
+ ),
494
+ bailianRegion: z.union([...BAILIAN_REGIONS]).default(DEFAULTS.bailianRegion)
495
+ .description("阿里云百炼地域:cn-beijing(默认)或 ap-southeast-1"),
496
+ bailianModel: z.string().default(DEFAULTS.bailianModel)
497
+ .description("阿里云百炼的模型 id;留空用预设默认 decision-model-preview(该端点 model 必填)"),
498
+ tokenhubModel: z.string().default(DEFAULTS.tokenhubModel)
499
+ .description("TokenHub 的模型 id;留空用预设默认 jev-latest(该端点 model 必填)"),
469
500
  customBaseUrl: z.string().default(DEFAULTS.customBaseUrl)
470
501
  .description("provider=custom 时的 base URL,必须是 http/https 地址"),
471
502
  customEndpointStyle: z.union([...ENDPOINT_STYLES]).default(DEFAULTS.customEndpointStyle)
@@ -484,6 +515,10 @@ export const Config = z.object({
484
515
  .description("Perplexity Decider 的 API key;留空则读 PERPLEXITY_API_KEY"),
485
516
  openrouterApiKey: z.string().role("secret").default(DEFAULTS.openrouterApiKey)
486
517
  .description("OpenRouter 的 API key;留空则读 OPENROUTER_API_KEY"),
518
+ bailianApiKey: z.string().role("secret").default(DEFAULTS.bailianApiKey)
519
+ .description("阿里云百炼的 API key;留空则读 DASHSCOPE_API_KEY"),
520
+ tokenhubApiKey: z.string().role("secret").default(DEFAULTS.tokenhubApiKey)
521
+ .description("TokenHub 的 API key;留空则读 TOKENHUB_API_KEY"),
487
522
  customApiKeyEnv: z.string().default(DEFAULTS.customApiKeyEnv)
488
523
  .description("custom 的密钥来源变量名(环境变量或 .credentials.yaml 的 refs 键)"),
489
524
  customModel: z.string().default(DEFAULTS.customModel)
@@ -502,6 +537,10 @@ export const Config = z.object({
502
537
  .description("perplexity 预设的密钥变量名覆盖;留空用 PERPLEXITY_API_KEY"),
503
538
  openrouterApiKeyEnv: z.string().default(DEFAULTS.openrouterApiKeyEnv)
504
539
  .description("openrouter 预设的密钥变量名覆盖;留空用 OPENROUTER_API_KEY"),
540
+ bailianApiKeyEnv: z.string().default(DEFAULTS.bailianApiKeyEnv)
541
+ .description("bailian 预设的密钥变量名覆盖;留空用 DASHSCOPE_API_KEY"),
542
+ tokenhubApiKeyEnv: z.string().default(DEFAULTS.tokenhubApiKeyEnv)
543
+ .description("tokenhub 预设的密钥变量名覆盖;留空用 TOKENHUB_API_KEY"),
505
544
  apiKey: z.string().role("secret")
506
545
  .description("当前 provider 的 API key;留空则读 apiKeyEnv 指定的环境变量"),
507
546
  apiKeyEnv: z.string().default(DEFAULTS.apiKeyEnv)
@@ -783,6 +822,25 @@ export function validateProviderPatch(patch, current) {
783
822
  if (!checked.ok) return { ok: false, error: `base URL 不合法:${checked.error}` };
784
823
  }
785
824
 
825
+ // 百炼:workspace 会拼进 hostname,非法字符必须在这里拦下(否则会拼出
826
+ // 另一个域名,而不是得到一条用户能懂的报错)。允许留空——用户往往先选
827
+ // provider、再填 workspace;此时 provider 会显示「需要 Workspace ID」。
828
+ if (Object.hasOwn(patch, "bailianRegion") && !BAILIAN_REGIONS.includes(patch.bailianRegion)) {
829
+ return {
830
+ ok: false,
831
+ error: `未知百炼地域: ${String(patch.bailianRegion)}(可选 ${BAILIAN_REGIONS.join(" / ")})`,
832
+ };
833
+ }
834
+ if (Object.hasOwn(patch, "bailianWorkspaceId")) {
835
+ if (typeof patch.bailianWorkspaceId !== "string" || /[\r\n\0]/.test(patch.bailianWorkspaceId)) {
836
+ return { ok: false, error: "百炼 Workspace ID 必须是单行字符串" };
837
+ }
838
+ const workspace = patch.bailianWorkspaceId.trim();
839
+ if (workspace.length > 0 && !/^[A-Za-z0-9_-]+$/.test(workspace)) {
840
+ return { ok: false, error: "百炼 Workspace ID 只能包含字母、数字、下划线和短横线(形如 ws_xxx)" };
841
+ }
842
+ }
843
+
786
844
  // 请求头格式:只要写了内容就必须至少解析出一项,否则用户会以为头已生效。
787
845
  if (Object.hasOwn(patch, "customHeaders") && typeof patch.customHeaders === "string") {
788
846
  const raw = patch.customHeaders.trim();
@@ -3301,6 +3359,10 @@ export function apply(ctx, config) {
3301
3359
  // custom 的字段原样回显(除 key 明文):用户保存后再打开必须看到自己填的值。
3302
3360
  customBaseUrl: cfg.customBaseUrl ?? "",
3303
3361
  customEndpointStyle: cfg.customEndpointStyle ?? "auto",
3362
+ // 百炼的两个「非密钥/模型」配置:workspace 参与域名拼接,必须回显,
3363
+ // 否则用户切回百炼时看不到自己填过的值。
3364
+ bailianWorkspaceId: cfg.bailianWorkspaceId ?? "",
3365
+ bailianRegion: cfg.bailianRegion ?? DEFAULT_BAILIAN_REGION,
3304
3366
  // 实际用得通的那条路径(探测出来的)。路径后缀由服务商决定,
3305
3367
  // 实测 SiliconFlow 只通 /systemone;界面必须显示「实际打哪」,
3306
3368
  // 否则用户看到 404 完全猜不到是路径选错了。
@@ -3504,6 +3566,10 @@ export function apply(ctx, config) {
3504
3566
  "customBaseUrl",
3505
3567
  "customEndpointStyle",
3506
3568
  "customHeaders",
3569
+ // 百炼:workspace 与地域不是预设表里的 model/key/keyEnv 三格,
3570
+ // 必须显式列进白名单,否则「下拉能选、输入框能填,刷新后没了」。
3571
+ "bailianWorkspaceId",
3572
+ "bailianRegion",
3507
3573
  "keyStorage",
3508
3574
  ...Object.values(PROVIDER_PRESETS).flatMap((preset) => [
3509
3575
  preset.modelField,
package/lib/providers.js CHANGED
@@ -43,6 +43,8 @@ export const PROVIDER_IDS = [
43
43
  "bamboonode",
44
44
  "siliconflow",
45
45
  "siliconflow-intl",
46
+ "bailian",
47
+ "tokenhub",
46
48
  "perplexity",
47
49
  "openrouter",
48
50
  "custom",
@@ -124,6 +126,44 @@ export function normalizeThinkMode(raw) {
124
126
  return THINK_MODES.includes(value) ? value : DEFAULT_THINK_MODE;
125
127
  }
126
128
 
129
+ /**
130
+ * 阿里云百炼决策模型支持的地域。
131
+ *
132
+ * 文档核对(2026-11-15):决策模型只提供 cn-beijing 与 ap-southeast-1 两个
133
+ * 地域,workspace id 直接出现在域名里。这里导出给配置校验和界面下拉共用,
134
+ * 避免三处各写一份枚举。
135
+ */
136
+ export const BAILIAN_REGIONS = ["cn-beijing", "ap-southeast-1"];
137
+
138
+ /** 百炼默认地域(国内站)。 */
139
+ export const DEFAULT_BAILIAN_REGION = "cn-beijing";
140
+
141
+ /**
142
+ * 取百炼 Workspace ID(只接受能安全放进域名的字符)。
143
+ *
144
+ * 为什么校验字符:workspace 会拼进 hostname。带空格、斜杠、点或 `@` 的值
145
+ * 会拼出另一个域名(甚至把请求发到别处),必须在进入 URL 之前就拒掉,
146
+ * 而不是等 `new URL()` 报一个用户看不懂的错。
147
+ *
148
+ * @param cfg - 配置对象。
149
+ * @returns 合法 workspace;缺失或非法返回空串。
150
+ */
151
+ export function bailianWorkspaceOf(cfg = {}) {
152
+ const raw = typeof cfg?.bailianWorkspaceId === "string" ? cfg.bailianWorkspaceId.trim() : "";
153
+ return /^[A-Za-z0-9_-]+$/.test(raw) ? raw : "";
154
+ }
155
+
156
+ /**
157
+ * 取百炼地域;非法值回落默认地域。
158
+ *
159
+ * @param cfg - 配置对象。
160
+ * @returns `cn-beijing` 或 `ap-southeast-1`。
161
+ */
162
+ export function bailianRegionOf(cfg = {}) {
163
+ const raw = typeof cfg?.bailianRegion === "string" ? cfg.bailianRegion.trim() : "";
164
+ return BAILIAN_REGIONS.includes(raw) ? raw : DEFAULT_BAILIAN_REGION;
165
+ }
166
+
127
167
  /**
128
168
  * 内置预设。
129
169
  *
@@ -299,6 +339,80 @@ export const PROVIDER_PRESETS = {
299
339
  hint: "OpenRouter 端点(openrouter.ai/api/v1);模型填 OpenRouter 的 slug,地址与默认模型待实测",
300
340
  hintEn: "OpenRouter endpoint (openrouter.ai/api/v1); model takes an OpenRouter slug — base URL and default model are placeholders until verified",
301
341
  },
342
+ /**
343
+ * 阿里云百炼决策模型(System One 协议)。
344
+ *
345
+ * 本预设有两格**其它 provider 没有**的配置:Workspace ID 与地域。原因是
346
+ * 百炼的决策端点不是固定的公共域名,而是
347
+ * `https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1/systemone`
348
+ * ——workspace 是域名的一部分,所以 base URL 必须**现拼**(`baseUrlFrom`),
349
+ * 不能像其它预设那样写死。`baseUrl` 因此留空;界面通过 `baseUrlTemplate`
350
+ * 展示形状。
351
+ *
352
+ * 文档核对(2026-11-15):POST /compatible-mode/v1/systemone,model 必填且
353
+ * 当前取 `decision-model-preview`,鉴权 `Bearer $DASHSCOPE_API_KEY`,支持
354
+ * cn-beijing 与 ap-southeast-1。请求/响应形状与本插件的 systemone 协议一致。
355
+ */
356
+ bailian: {
357
+ id: "bailian",
358
+ label: "阿里云百炼",
359
+ labelEn: "Alibaba Bailian",
360
+ // 动态地址:workspace 或地域缺一不可,因此不能给静态 baseUrl。
361
+ baseUrl: "",
362
+ baseUrlTemplate: "https://{workspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1",
363
+ baseUrlFrom(cfg) {
364
+ const workspace = bailianWorkspaceOf(cfg);
365
+ if (workspace.length === 0) return "";
366
+ return `https://${workspace}.${bailianRegionOf(cfg)}.maas.aliyuncs.com/compatible-mode/v1`;
367
+ },
368
+ endpointStyle: "systemone",
369
+ // 百炼把 model 列为必填;留空必须回落到默认值,否则是注定 400 的请求。
370
+ requiresModel: true,
371
+ defaultModel: "decision-model-preview",
372
+ knownModels: ["decision-model-preview"],
373
+ apiKeyEnv: "DASHSCOPE_API_KEY",
374
+ legacyApiKeyEnvs: [],
375
+ keyField: "bailianApiKey",
376
+ modelField: "bailianModel",
377
+ keyEnvField: "bailianApiKeyEnv",
378
+ // 这两格不是「密钥/模型」,但界面必须知道字段名才能写回配置。
379
+ workspaceField: "bailianWorkspaceId",
380
+ regionField: "bailianRegion",
381
+ regions: BAILIAN_REGIONS,
382
+ // 决策模型本身不生成文本;think_mode 是 startlux 家族的字段,百炼文档
383
+ // 没有提到它,因此按「未实测」处理,不在界面上承诺效果。
384
+ thinkModeSupport: "unknown",
385
+ hint: "阿里云百炼决策模型(POST /compatible-mode/v1/systemone,model=decision-model-preview)。需要 Workspace ID 与地域;鉴权用 DASHSCOPE_API_KEY",
386
+ hintEn: "Alibaba Bailian decision model (POST /compatible-mode/v1/systemone, model=decision-model-preview). Needs a Workspace ID and region; authenticate with DASHSCOPE_API_KEY",
387
+ },
388
+ /**
389
+ * TokenHub 的 System One 决策端点。
390
+ *
391
+ * 先澄清一个容易混的事实:**腾讯云 TokenHub**(tokenhub.tencentmaas.com)
392
+ * 官方文档只提供 OpenAI / Anthropic 兼容的文本、图像、视频、3D 接口,
393
+ * 没有决策 / System One 端点。带 Jev 决策模型的是 **tokenhub.com**
394
+ * (TypeSafe)的 TokenHub,文档给出的地址是 `https://us-api.tokenhub.com/v1/systemone`。
395
+ * 因此这个预设指向后者,并在 hint 里把区别写清——否则用户会拿腾讯云的
396
+ * key 打到 tokenhub.com,得到 401 却不知道为什么。
397
+ */
398
+ tokenhub: {
399
+ id: "tokenhub",
400
+ label: "TokenHub",
401
+ labelEn: "TokenHub",
402
+ baseUrl: "https://us-api.tokenhub.com/v1",
403
+ endpointStyle: "systemone",
404
+ requiresModel: true,
405
+ defaultModel: "jev-latest",
406
+ knownModels: ["jev-latest", "jev-1-13"],
407
+ apiKeyEnv: "TOKENHUB_API_KEY",
408
+ legacyApiKeyEnvs: [],
409
+ keyField: "tokenhubApiKey",
410
+ modelField: "tokenhubModel",
411
+ keyEnvField: "tokenhubApiKeyEnv",
412
+ thinkModeSupport: "unknown",
413
+ hint: "TokenHub(tokenhub.com,TypeSafe)的 System One 决策端点(/v1/systemone,模型 jev-latest)。注意:腾讯云 TokenHub 没有决策端点,两者同名但不是同一个产品",
414
+ hintEn: "TokenHub (tokenhub.com, TypeSafe) System One decision endpoint (/v1/systemone, model jev-latest). Note: Tencent Cloud TokenHub has no decision endpoint — same name, different product",
415
+ },
302
416
  custom: {
303
417
  id: "custom",
304
418
  label: "custom",
@@ -704,7 +818,12 @@ export function resolveProvider(cfg = {}, options = {}) {
704
818
  }
705
819
 
706
820
  // 内置预设:预设默认打底,配置覆盖。
707
- const presetBase = preset.baseUrl;
821
+ //
822
+ // 动态预设(目前只有百炼)的地址由配置拼出(workspace + region),拼不出
823
+ // 来时 `baseUrlFrom` 返回空串——这不是「配置没填好」的普通无效,界面要给出
824
+ // 「需要 Workspace ID」而不是一句 URL 不合法。
825
+ const presetBase = typeof preset.baseUrlFrom === "function" ? preset.baseUrlFrom(cfg) : preset.baseUrl;
826
+ const dynamicMissing = typeof preset.baseUrlFrom === "function" && presetBase.trim().length === 0;
708
827
  const legacyBase =
709
828
  id === "bamboonode" && typeof cfg.baseUrl === "string" ? normalizeBaseUrl(cfg.baseUrl) : "";
710
829
  let baseUrl = legacyBase.length > 0 ? legacyBase : presetBase;
@@ -765,8 +884,12 @@ export function resolveProvider(cfg = {}, options = {}) {
765
884
  id === "bamboonode" ? cfg.apiKey : undefined,
766
885
  ),
767
886
  headers: {},
768
- valid: checked.ok,
769
- error: checked.ok ? undefined : `provider ${id} 的 base URL 不可用:${checked.error}`,
887
+ valid: checked.ok && !dynamicMissing,
888
+ error: dynamicMissing
889
+ ? `provider ${id} 需要 ${preset.workspaceField ? "Workspace ID" : "必填地址"}`
890
+ : checked.ok
891
+ ? undefined
892
+ : `provider ${id} 的 base URL 不可用:${checked.error}`,
770
893
  };
771
894
  }
772
895
 
@@ -947,6 +1070,13 @@ export function providerCatalog() {
947
1070
  label: preset.label,
948
1071
  labelEn: preset.labelEn,
949
1072
  baseUrl: preset.baseUrl,
1073
+ // 动态地址的预设(百炼):静态 baseUrl 为空,形状由模板表达。界面用它
1074
+ // 渲染占位/端点示例;`dynamicBaseUrl` 决定要不要显示 Workspace/地域两格。
1075
+ baseUrlTemplate: preset.baseUrlTemplate,
1076
+ dynamicBaseUrl: typeof preset.baseUrlFrom === "function",
1077
+ workspaceField: preset.workspaceField,
1078
+ regionField: preset.regionField,
1079
+ regions: [...(preset.regions ?? [])],
950
1080
  endpointStyle: preset.endpointStyle,
951
1081
  defaultModel: preset.defaultModel,
952
1082
  apiKeyEnv: preset.apiKeyEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leonhardthust/dsh-decision-engine",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "dsh-decision-engine: a cheap judgement layer for DeepSeek Harness — a small decision model answers structured questions before the main model acts, so calls can be allowed, escalated, rerouted to a cheaper model, or skipped. All eight decision points (tool authorization, model routing, delegation, compaction, turn continuation, retrieval, skill pick, COT translation) are implemented and individually switchable. Works against any service exposing a decision endpoint: built-in presets (system one, bamboonode, SiliconFlow, Silicon Flow) or a custom base URL.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",