@leonhardthust/dsh-decision-engine 1.12.0 → 1.13.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/config.example.yml +9 -7
- package/docs/releasing.md +4 -4
- package/lib/client.js +128 -36
- package/lib/index.js +142 -5
- package/package.json +1 -1
package/config.example.yml
CHANGED
|
@@ -413,15 +413,17 @@ guardFallbackReasoningEffort: ""
|
|
|
413
413
|
# ============================================================================
|
|
414
414
|
# 匿名使用统计(可选)
|
|
415
415
|
# ============================================================================
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
# 架构、宿主版本、当日决策数、事件类型。
|
|
416
|
+
# 用途只有一个:让作者知道还有多少人在用、哪些版本/平台在用、每天判了多少
|
|
417
|
+
# 次,用来判断这个插件值不值得继续维护(以及优先修哪个平台的问题)。
|
|
419
418
|
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
419
|
+
# 上报字段(全部):随机安装 id(首次运行生成的 UUIDv4,与账号无关)、插件版本、
|
|
420
|
+
# 平台、架构、宿主版本、当天决策数、事件类型(install / daily heartbeat)。
|
|
422
421
|
#
|
|
423
|
-
#
|
|
424
|
-
#
|
|
422
|
+
# 绝不上报:API key、prompt / 工具参数、文件路径、provider 地址、模型名、会话
|
|
423
|
+
# 内容、用户名;服务端也不记录 IP。删掉 analytics.json 即换新身份。
|
|
424
|
+
#
|
|
425
|
+
# off 完全关闭:一个请求都不发
|
|
426
|
+
# install 仅首次运行上报一次「装了」,之后不再联网
|
|
425
427
|
# daily 每天上报一次,附带当日决策数(默认)
|
|
426
428
|
#
|
|
427
429
|
# telemetryLevel: daily
|
package/docs/releasing.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# npm 发布
|
|
2
2
|
|
|
3
|
-
发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.
|
|
3
|
+
发布目标:`@leonhardthust/dsh-decision-engine`,当前版本 `1.13.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.
|
|
12
|
+
- 当前 `1.13.0` 包含 DEC-074:匿名使用统计的用途/字段/边界重写;连通性测试从页脚前移到连接方式组,同时探测决策端点与回退语言模型,结果按链路分卡展示(摘要 + 可折叠详情)。`1.12.0` 的 DEC-072 / DEC-073、`1.11.2` 的 never 回退、`1.11.1` 的 DEC-070、`1.11.0` 的审计修复、`1.10.0` 的五项改动依次见 DEC-062 ~ DEC-073。
|
|
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.
|
|
61
|
+
npm publish ./release-artifacts/leonhardthust-dsh-decision-engine-1.13.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.
|
|
69
|
+
DSH 插件管理中使用 `@leonhardthust/dsh-decision-engine` 或指定 `@leonhardthust/dsh-decision-engine@1.13.0`。安装和启用是另外的用户动作,发布工作流不会修改本机 DSH profile。
|
package/lib/client.js
CHANGED
|
@@ -144,11 +144,15 @@ window.__ModuleLoader__.load({
|
|
|
144
144
|
"auto|由服务端决定(实测约 7.2s,比 never 慢)": "auto | decided by the server (measured ~7.2s, slower than never)",
|
|
145
145
|
"always|总是思考(约 +5s,质量收益未实测)": "always | always think (about +5s; quality gain not measured)",
|
|
146
146
|
// —— 匿名使用统计(DEC-020)——
|
|
147
|
-
"
|
|
147
|
+
"匿名使用统计": "Anonymous usage stats",
|
|
148
148
|
"每天上报一次:匿名安装 id、版本、平台与当日决策数": "Reports once a day: anonymous install id, version, platform, and today's decision count",
|
|
149
|
+
"每天一次,只用于让作者判断插件还有多少人在用、值不值得继续维护:随机安装 ID、插件版本、平台/架构、宿主版本、当天决策次数。不含会话内容、文件路径、密钥、模型名或身份信息。": "Once a day, used only so the author can tell how many people still use the plugin and whether it is worth maintaining: a random install ID, plugin version, platform/arch, host version, and today's decision count. No conversation content, file paths, keys, model names, or identity information.",
|
|
149
150
|
"已关闭:任何情况下都不联网": "Off: never touches the network",
|
|
151
|
+
"完全关闭:不发送任何请求。": "Fully off: no request is ever sent.",
|
|
150
152
|
"仅首次运行上报一次「安装了」,之后不再联网": "Reports \"installed\" once on first run, then never again",
|
|
153
|
+
"仅在第一次运行时上报一次「安装了」(字段同上),之后不再联网。": "Reports \"installed\" once on the first run (same fields as above), then never touches the network again.",
|
|
151
154
|
"off(不联网)": "off (no network)",
|
|
155
|
+
"off(完全关闭)": "off (fully disabled)",
|
|
152
156
|
"install(仅首次)": "install (first run only)",
|
|
153
157
|
"daily(每天一次,默认)": "daily (once a day, default)",
|
|
154
158
|
"auto(先 /decisions)": "auto (/decisions first)",
|
|
@@ -188,6 +192,10 @@ window.__ModuleLoader__.load({
|
|
|
188
192
|
"重试": "Retry",
|
|
189
193
|
"测试中…": "Testing…",
|
|
190
194
|
"测试端点": "Test endpoint",
|
|
195
|
+
"连通性测试": "Connectivity test",
|
|
196
|
+
"分别向决策端点与回退语言模型各发一次最小请求;仅在你点击时发生,会消耗少量 token": "Sends one minimal request to the decision endpoint and to the fallback language model. Only when you click; uses a few tokens.",
|
|
197
|
+
"已跳过": "Skipped",
|
|
198
|
+
"详情": "Details",
|
|
191
199
|
"未保存:密钥太短(至少 10 个字符)": "Not saved: the key is too short (at least 10 characters)",
|
|
192
200
|
"测试端点失败": "Endpoint test failed",
|
|
193
201
|
// —— 使用免费模型(DEC-030,原名「跟随服务端下发」,DEC-037 改名) ——
|
|
@@ -7273,10 +7281,11 @@ window.__ModuleLoader__.load({
|
|
|
7273
7281
|
setProviderError("");
|
|
7274
7282
|
try {
|
|
7275
7283
|
const result = await call("/test", { method: "POST" });
|
|
7276
|
-
//
|
|
7277
|
-
//
|
|
7284
|
+
// DEC-074:服务端现在返回分链路的 `results`(决策端点 / 回退模型),
|
|
7285
|
+
// 界面优先渲染成两张卡片;`text` 只作为旧服务端的兜底与纯文本记录。
|
|
7278
7286
|
setMsg({
|
|
7279
7287
|
ok: result.ok === true,
|
|
7288
|
+
results: Array.isArray(result.results) ? result.results : null,
|
|
7280
7289
|
text: result.ok === true
|
|
7281
7290
|
? t("端点连通 ✅\n{text}", { text: result.text ?? "" })
|
|
7282
7291
|
: tr(result.text ?? "") || t("端点返回失败"),
|
|
@@ -7865,6 +7874,109 @@ window.__ModuleLoader__.load({
|
|
|
7865
7874
|
),
|
|
7866
7875
|
"endpoint",
|
|
7867
7876
|
),
|
|
7877
|
+
// 4b2) 连通性测试:从页脚前移到它要验证的对象旁边(provider / 模型 /
|
|
7878
|
+
// 端点 / key 都在上面几行)。一次点击同时测两条链路:决策端点
|
|
7879
|
+
// (自拼 System One)与回退语言模型(宿主 llm.stream)。结果就
|
|
7880
|
+
// 显示在下面,不再需要滚到页脚找「测试」的结论。
|
|
7881
|
+
row(
|
|
7882
|
+
"连通性测试",
|
|
7883
|
+
t("分别向决策端点与回退语言模型各发一次最小请求;仅在你点击时发生,会消耗少量 token"),
|
|
7884
|
+
h(
|
|
7885
|
+
"button",
|
|
7886
|
+
{
|
|
7887
|
+
type: "button",
|
|
7888
|
+
onClick: runTest,
|
|
7889
|
+
disabled: busy !== "",
|
|
7890
|
+
style: { ...S.saveBtn, flexShrink: 0 },
|
|
7891
|
+
},
|
|
7892
|
+
busy === "test" ? "测试中…" : "测试端点",
|
|
7893
|
+
),
|
|
7894
|
+
"connectivity",
|
|
7895
|
+
),
|
|
7896
|
+
msg !== null
|
|
7897
|
+
? h(
|
|
7898
|
+
"div",
|
|
7899
|
+
{
|
|
7900
|
+
style: { display: "flex", flexDirection: "column", gap: "8px", marginTop: "10px" },
|
|
7901
|
+
key: "test-msg",
|
|
7902
|
+
},
|
|
7903
|
+
// 结构化结果(DEC-074):每条链路一张小卡片——先给结论(通/不通/
|
|
7904
|
+
// 跳过)+ 一行摘要,原始诊断折叠进「详情」。旧服务端只回 `text`
|
|
7905
|
+
// 时退回整段文本,不至于空白。
|
|
7906
|
+
...(Array.isArray(msg.results)
|
|
7907
|
+
? msg.results.map((item) => {
|
|
7908
|
+
const tone = item.skipped === true ? "#fbbf24" : item.ok === true ? "#4ade80" : "#f87171";
|
|
7909
|
+
const icon = item.skipped === true ? "–" : item.ok === true ? "✓" : "✕";
|
|
7910
|
+
return h(
|
|
7911
|
+
"div",
|
|
7912
|
+
{
|
|
7913
|
+
key: `test-${item.key ?? item.label}`,
|
|
7914
|
+
style: {
|
|
7915
|
+
border: `1px solid ${tone}55`,
|
|
7916
|
+
borderRadius: "8px",
|
|
7917
|
+
padding: "8px 10px",
|
|
7918
|
+
background: `${tone}0d`,
|
|
7919
|
+
},
|
|
7920
|
+
},
|
|
7921
|
+
h(
|
|
7922
|
+
"div",
|
|
7923
|
+
{ style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
7924
|
+
h("span", { style: { color: tone, fontWeight: 600 } }, icon),
|
|
7925
|
+
h("span", { style: { fontWeight: 600 } }, item.label),
|
|
7926
|
+
item.skipped === true
|
|
7927
|
+
? h("span", { style: { ...S.meta, marginTop: 0, color: tone } }, t("已跳过"))
|
|
7928
|
+
: null,
|
|
7929
|
+
),
|
|
7930
|
+
h(
|
|
7931
|
+
"div",
|
|
7932
|
+
{ style: { ...S.meta, marginTop: "4px", whiteSpace: "pre-wrap", wordBreak: "break-all" } },
|
|
7933
|
+
item.summary ?? "",
|
|
7934
|
+
),
|
|
7935
|
+
typeof item.detail === "string" && item.detail.length > 0 && item.detail !== item.summary
|
|
7936
|
+
? h(
|
|
7937
|
+
"details",
|
|
7938
|
+
{ style: { marginTop: "4px" } },
|
|
7939
|
+
h(
|
|
7940
|
+
"summary",
|
|
7941
|
+
{ style: { cursor: "pointer", ...S.meta, marginTop: 0, opacity: 0.7 } },
|
|
7942
|
+
t("详情"),
|
|
7943
|
+
),
|
|
7944
|
+
h(
|
|
7945
|
+
"pre",
|
|
7946
|
+
{
|
|
7947
|
+
style: {
|
|
7948
|
+
margin: "6px 0 0",
|
|
7949
|
+
whiteSpace: "pre-wrap",
|
|
7950
|
+
wordBreak: "break-all",
|
|
7951
|
+
fontSize: "12px",
|
|
7952
|
+
opacity: 0.8,
|
|
7953
|
+
fontFamily: "inherit",
|
|
7954
|
+
},
|
|
7955
|
+
},
|
|
7956
|
+
item.detail,
|
|
7957
|
+
),
|
|
7958
|
+
)
|
|
7959
|
+
: null,
|
|
7960
|
+
);
|
|
7961
|
+
})
|
|
7962
|
+
: [
|
|
7963
|
+
h(
|
|
7964
|
+
"div",
|
|
7965
|
+
{
|
|
7966
|
+
style: {
|
|
7967
|
+
...S.meta,
|
|
7968
|
+
marginTop: 0,
|
|
7969
|
+
color: msg.ok ? "#4ade80" : "#f87171",
|
|
7970
|
+
whiteSpace: "pre-wrap",
|
|
7971
|
+
fontFamily: "inherit",
|
|
7972
|
+
fontSize: "13px",
|
|
7973
|
+
},
|
|
7974
|
+
},
|
|
7975
|
+
msg.text,
|
|
7976
|
+
),
|
|
7977
|
+
]),
|
|
7978
|
+
)
|
|
7979
|
+
: null,
|
|
7868
7980
|
// 4c) 密钥保存位置:默认托管到 .credentials.yaml(DEC-037 从 API Key 紧邻处
|
|
7869
7981
|
// 下移到这里:它讲的是「这把 key 存到哪个文件」,不是「怎么连上服务」,
|
|
7870
7982
|
// 夹在连接组中间会打断 provider → 模型 → key → 端点这条阅读线;
|
|
@@ -8663,12 +8775,14 @@ window.__ModuleLoader__.load({
|
|
|
8663
8775
|
// 放在「思考模式」之后:它同样是「一次性选择、改完即存」的下拉,
|
|
8664
8776
|
// 与上面的开关/下拉一组,不与 provider 表格混在一起。
|
|
8665
8777
|
row(
|
|
8666
|
-
"
|
|
8667
|
-
|
|
8668
|
-
|
|
8778
|
+
"匿名使用统计",
|
|
8779
|
+
// 与下拉的兜底保持一致:undefined 时下拉显示 daily,说明也必须说 daily,
|
|
8780
|
+
// 否则首帧会出现「选中的是 daily、解释却写仅首次」的自相矛盾。
|
|
8781
|
+
(draft.telemetryLevel ?? "daily") === "daily"
|
|
8782
|
+
? t("每天一次,只用于让作者判断插件还有多少人在用、值不值得继续维护:随机安装 ID、插件版本、平台/架构、宿主版本、当天决策次数。不含会话内容、文件路径、密钥、模型名或身份信息。")
|
|
8669
8783
|
: draft.telemetryLevel === "off"
|
|
8670
|
-
? "
|
|
8671
|
-
: "
|
|
8784
|
+
? t("完全关闭:不发送任何请求。")
|
|
8785
|
+
: t("仅在第一次运行时上报一次「安装了」(字段同上),之后不再联网。"),
|
|
8672
8786
|
h(
|
|
8673
8787
|
"select",
|
|
8674
8788
|
{
|
|
@@ -8689,10 +8803,10 @@ window.__ModuleLoader__.load({
|
|
|
8689
8803
|
"option",
|
|
8690
8804
|
{ key: value, value },
|
|
8691
8805
|
value === "off"
|
|
8692
|
-
? "off
|
|
8806
|
+
? t("off(完全关闭)")
|
|
8693
8807
|
: value === "install"
|
|
8694
|
-
? "install(仅首次)"
|
|
8695
|
-
: "daily(每天一次,默认)",
|
|
8808
|
+
? t("install(仅首次)")
|
|
8809
|
+
: t("daily(每天一次,默认)"),
|
|
8696
8810
|
),
|
|
8697
8811
|
),
|
|
8698
8812
|
),
|
|
@@ -8701,8 +8815,10 @@ window.__ModuleLoader__.load({
|
|
|
8701
8815
|
];
|
|
8702
8816
|
|
|
8703
8817
|
const footerItems = [
|
|
8704
|
-
//
|
|
8818
|
+
// 页脚常驻在所有分组之下:保存状态与状态信息。
|
|
8705
8819
|
// 它回答的是「刚才那一下存没存」,与页面哪一组展开无关。
|
|
8820
|
+
// 连通性测试已前移到「连接方式」组(DEC-074)——它要验证的对象都在
|
|
8821
|
+
// 那里,结论也该贴着对象显示。
|
|
8706
8822
|
// 4g) 自动保存状态行:**没有「保存」按钮**,改动即落盘。
|
|
8707
8823
|
// 这一行回答的是「刚才那一下到底存没存」:进行中 / 已存 /
|
|
8708
8824
|
// 失败原因(附重试)。原来那个按钮的「保存中…」搬到这里。
|
|
@@ -8755,16 +8871,6 @@ window.__ModuleLoader__.load({
|
|
|
8755
8871
|
"重试",
|
|
8756
8872
|
)
|
|
8757
8873
|
: null,
|
|
8758
|
-
h(
|
|
8759
|
-
"button",
|
|
8760
|
-
{
|
|
8761
|
-
type: "button",
|
|
8762
|
-
disabled: busy !== "",
|
|
8763
|
-
onClick: runTest,
|
|
8764
|
-
style: { ...S.saveBtn, background: "transparent", color: "inherit" },
|
|
8765
|
-
},
|
|
8766
|
-
busy === "test" ? "测试中…" : "测试端点",
|
|
8767
|
-
),
|
|
8768
8874
|
),
|
|
8769
8875
|
// 4h) 状态信息(紧凑)
|
|
8770
8876
|
h(
|
|
@@ -8795,20 +8901,6 @@ window.__ModuleLoader__.load({
|
|
|
8795
8901
|
}) + (activeKeyStatus.configured === true ? "" : t("(未读到)"))
|
|
8796
8902
|
: null,
|
|
8797
8903
|
),
|
|
8798
|
-
msg !== null
|
|
8799
|
-
? h(
|
|
8800
|
-
"div",
|
|
8801
|
-
{
|
|
8802
|
-
style: {
|
|
8803
|
-
...S.meta,
|
|
8804
|
-
marginTop: "10px",
|
|
8805
|
-
color: msg.ok ? "#4ade80" : "#f87171",
|
|
8806
|
-
},
|
|
8807
|
-
key: "msg",
|
|
8808
|
-
},
|
|
8809
|
-
msg.text,
|
|
8810
|
-
)
|
|
8811
|
-
: null,
|
|
8812
8904
|
];
|
|
8813
8905
|
|
|
8814
8906
|
// 配置 tab 的全部内容。
|
package/lib/index.js
CHANGED
|
@@ -654,9 +654,10 @@ export const Config = z.object({
|
|
|
654
654
|
.description("缓存存活时间;改策略或配置后旧裁决不应继续被复用"),
|
|
655
655
|
telemetryLevel: z.union([...TELEMETRY_LEVELS]).default(DEFAULTS.telemetryLevel)
|
|
656
656
|
.description(
|
|
657
|
-
"匿名使用统计:off 完全不联网 / install
|
|
658
|
-
+ "
|
|
659
|
-
+ "
|
|
657
|
+
"匿名使用统计:off 完全不联网 / install 仅首次运行报一次 / daily 每天一次(默认,含当天决策数)。"
|
|
658
|
+
+ "用途只有「判断还有多少人在用、哪些版本与平台在用、值不值得继续维护」;"
|
|
659
|
+
+ "上报随机安装 ID、插件版本、平台/架构、宿主版本与决策计数,"
|
|
660
|
+
+ "永不上报 prompt、密钥、路径、模型名或会话内容",
|
|
660
661
|
),
|
|
661
662
|
telemetryEndpoint: z.string().default(DEFAULTS.telemetryEndpoint)
|
|
662
663
|
.description("统计上报地址;留空用内置端点(https://dsh.zuos.us/api/e)"),
|
|
@@ -3150,6 +3151,108 @@ export function apply(ctx, config) {
|
|
|
3150
3151
|
return { ok: false, text: `provider: ${provider.label}(${provider.id})\nPOST ${usedEndpoint}\n请求失败(${Date.now() - started}ms)` };
|
|
3151
3152
|
};
|
|
3152
3153
|
|
|
3154
|
+
/**
|
|
3155
|
+
* 探测回退语言模型(guardFallback = model 时)。
|
|
3156
|
+
*
|
|
3157
|
+
* 为什么要单独测:决策端点与回退模型是两条完全不同的链路——前者是自己拼的
|
|
3158
|
+
* System One 请求,后者走宿主的 `llm.stream`。决策端点通、回退模型 401 的
|
|
3159
|
+
* 情况完全可能发生;只测前者会让用户以为整套守卫都在工作,而真正需要兜底
|
|
3160
|
+
* 时才发现回退模型不可用。
|
|
3161
|
+
*
|
|
3162
|
+
* 刻意只测「显式指定 provider/model」这一种策略:
|
|
3163
|
+
* - session 用的是当前会话模型,设置页没有会话上下文,测不了(也不该猜);
|
|
3164
|
+
* - allow / deny 根本不调模型,没有可测的链路。
|
|
3165
|
+
*
|
|
3166
|
+
* 这是一次最小的真实模型调用(maxTokens 16),只在用户点「测试」时发生;
|
|
3167
|
+
* 失败/超时都折叠成一段可读诊断,绝不让测试按钮本身抛错。
|
|
3168
|
+
*
|
|
3169
|
+
* @returns `{ ok: boolean|null, skipped?: boolean, text }`;null = 本策略不适用。
|
|
3170
|
+
*/
|
|
3171
|
+
const probeFallbackModel = async () => {
|
|
3172
|
+
const policy = cfg.guardFallback;
|
|
3173
|
+
if (policy !== "model") {
|
|
3174
|
+
return {
|
|
3175
|
+
ok: null,
|
|
3176
|
+
skipped: true,
|
|
3177
|
+
text: `回退语言模型: 当前策略为 ${policy},不调用独立模型(${policy === "session" ? "使用会话当前模型,设置页无法离线测试" : "没有可测的模型链路"})`,
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
const provider = typeof cfg.guardFallbackProvider === "string" ? cfg.guardFallbackProvider.trim() : "";
|
|
3181
|
+
const model = typeof cfg.guardFallbackModel === "string" ? cfg.guardFallbackModel.trim() : "";
|
|
3182
|
+
if (provider.length === 0 || model.length === 0) {
|
|
3183
|
+
return { ok: false, text: "回退语言模型: 策略为 model,但 provider / model 未填全" };
|
|
3184
|
+
}
|
|
3185
|
+
const llm = ctx.get?.("llm");
|
|
3186
|
+
if (typeof llm?.stream !== "function") {
|
|
3187
|
+
return { ok: false, text: "回退语言模型: 宿主未提供 llm.stream,无法测试(请确认 DSH 版本支持该服务)" };
|
|
3188
|
+
}
|
|
3189
|
+
const configuredTimeout = Number.isFinite(cfg.timeoutMs) ? cfg.timeoutMs : 20000;
|
|
3190
|
+
const timeoutMs = Math.max(8000, Math.min(20000, configuredTimeout));
|
|
3191
|
+
const effort = typeof cfg.guardFallbackReasoningEffort === "string" ? cfg.guardFallbackReasoningEffort.trim() : "";
|
|
3192
|
+
const started = Date.now();
|
|
3193
|
+
try {
|
|
3194
|
+
const stream = llm.stream({
|
|
3195
|
+
provider,
|
|
3196
|
+
model,
|
|
3197
|
+
...(effort.length > 0 ? { reasoningEffort: effort } : {}),
|
|
3198
|
+
// 只要求一个词:这是连通性测试,不是质量评测;输出越短越省钱。
|
|
3199
|
+
messages: [{ role: "user", content: [{ type: "text", text: "Reply with the single word: pong" }] }],
|
|
3200
|
+
tools: [],
|
|
3201
|
+
maxTokens: 16,
|
|
3202
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3203
|
+
purpose: "decision-engine-connectivity",
|
|
3204
|
+
});
|
|
3205
|
+
let finish = null;
|
|
3206
|
+
let text = "";
|
|
3207
|
+
for await (const chunk of stream) {
|
|
3208
|
+
if (chunk?.type === "text-delta") text += chunk.text;
|
|
3209
|
+
if (chunk?.type === "finish") finish = chunk.reason?.kind ?? "unknown";
|
|
3210
|
+
}
|
|
3211
|
+
// length 也算连通:模型确实回应了,只是 16 token 被截断(例如它先输出了
|
|
3212
|
+
// 一段思考)。只有「没有 finish 或 finish 不是正常结束」才算失败。
|
|
3213
|
+
const ok = finish === "stop" || finish === "length";
|
|
3214
|
+
return {
|
|
3215
|
+
ok,
|
|
3216
|
+
text:
|
|
3217
|
+
`回退语言模型: ${provider} / ${model}${effort.length > 0 ? `(effort ${effort})` : ""}\n` +
|
|
3218
|
+
`耗时 ${Date.now() - started}ms finish: ${finish ?? "(无)"}\n` +
|
|
3219
|
+
`响应: ${text.trim().slice(0, 120) || "(空)"}`,
|
|
3220
|
+
};
|
|
3221
|
+
} catch (error) {
|
|
3222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3223
|
+
return {
|
|
3224
|
+
ok: false,
|
|
3225
|
+
text: `回退语言模型: ${provider} / ${model}\n请求失败(${Date.now() - started}ms): ${message}`,
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
/**
|
|
3231
|
+
* 把 probe() 的多行诊断压成一句「人先看这个」的结论。
|
|
3232
|
+
*
|
|
3233
|
+
* 原始输出是给排查用的(request id、尝试过的路径、响应体),但测试按钮的
|
|
3234
|
+
* 第一眼需要一个能判断「通没通、多快」的短句;详情仍然保留,折叠起来。
|
|
3235
|
+
*
|
|
3236
|
+
* @param text - probe() 返回的完整文本。
|
|
3237
|
+
* @returns 一行摘要(不抛错,解析不出来就取前两行)。
|
|
3238
|
+
*/
|
|
3239
|
+
const summarizeProbe = (text) => {
|
|
3240
|
+
const lines = String(text ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3241
|
+
const provider = lines.find((line) => line.startsWith("provider: "));
|
|
3242
|
+
const post = lines.find((line) => line.startsWith("POST "));
|
|
3243
|
+
const http = lines.find((line) => line.startsWith("HTTP "));
|
|
3244
|
+
const failed = lines.find((line) => line.startsWith("请求失败"));
|
|
3245
|
+
const reason = lines.find((line) => line.startsWith("失败原因: "));
|
|
3246
|
+
const parts = [];
|
|
3247
|
+
if (provider) parts.push(provider.slice("provider: ".length));
|
|
3248
|
+
if (post) parts.push(post);
|
|
3249
|
+
if (http) parts.push(http.replace(/\s+/g, " "));
|
|
3250
|
+
else if (failed) parts.push(failed);
|
|
3251
|
+
if (reason) parts.push(reason.slice("失败原因: ".length));
|
|
3252
|
+
if (parts.length === 0) return lines.slice(0, 2).join(" · ") || "(无结果)";
|
|
3253
|
+
return parts.join(" · ");
|
|
3254
|
+
};
|
|
3255
|
+
|
|
3153
3256
|
// ---- 面板 API:给前端 UI 用(不经过 settings 服务)-------------------------
|
|
3154
3257
|
ctx.inject?.(["connection"], () => {
|
|
3155
3258
|
const connection = ctx.get?.("connection");
|
|
@@ -3847,8 +3950,42 @@ export function apply(ctx, config) {
|
|
|
3847
3950
|
methods: ["POST"],
|
|
3848
3951
|
requestBody: "buffered",
|
|
3849
3952
|
fetch: async () => {
|
|
3850
|
-
|
|
3851
|
-
|
|
3953
|
+
// 两条链路各测一次:决策端点是自拼的 System One 请求,回退模型走宿主
|
|
3954
|
+
// 的 llm.stream。只测前者会让「决策端点通、回退模型不可用」被误报成
|
|
3955
|
+
// 整体健康——而回退模型恰好是审批通道不可用时的兜底裁判。
|
|
3956
|
+
const decision = await probe();
|
|
3957
|
+
await reloadOwnConfig();
|
|
3958
|
+
const fallback = await probeFallbackModel();
|
|
3959
|
+
// fallback.ok 为 null 表示该策略不适用(session / allow / deny),
|
|
3960
|
+
// 不能把「没有这条链路」算成失败;但明确失败(false)必须让整体变红。
|
|
3961
|
+
const ok = decision.ok === true && fallback.ok !== false;
|
|
3962
|
+
// 给界面一份结构化结果:每条链路一个短摘要 + 可折叠的原始诊断。
|
|
3963
|
+
// 摘要与详情分开,是因为用户第一眼只需要「通没通、多快」,排查时才
|
|
3964
|
+
// 展开 request id / 响应体。
|
|
3965
|
+
const results = [
|
|
3966
|
+
{
|
|
3967
|
+
key: "decision",
|
|
3968
|
+
label: "决策端点",
|
|
3969
|
+
ok: decision.ok === true,
|
|
3970
|
+
skipped: false,
|
|
3971
|
+
summary: summarizeProbe(decision.text),
|
|
3972
|
+
detail: decision.text,
|
|
3973
|
+
},
|
|
3974
|
+
{
|
|
3975
|
+
key: "fallback",
|
|
3976
|
+
label: "回退语言模型",
|
|
3977
|
+
ok: fallback.ok,
|
|
3978
|
+
skipped: fallback.skipped === true,
|
|
3979
|
+
summary: fallback.text,
|
|
3980
|
+
detail: fallback.detail ?? null,
|
|
3981
|
+
},
|
|
3982
|
+
];
|
|
3983
|
+
return json({
|
|
3984
|
+
ok,
|
|
3985
|
+
// `text` 保留给旧前端/日志:纯文本仍然可读。
|
|
3986
|
+
text: [decision.text, fallback.text].filter((line) => typeof line === "string" && line.length > 0).join("\n\n"),
|
|
3987
|
+
results,
|
|
3988
|
+
});
|
|
3852
3989
|
},
|
|
3853
3990
|
});
|
|
3854
3991
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leonhardthust/dsh-decision-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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",
|