dsh-decision-engine 0.4.0 → 0.5.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 +32 -36
- package/{startlux.example.yml → config.example.yml} +16 -11
- package/cordis.patch.yml +1 -1
- package/lib/client.js +53 -4
- package/lib/config-file.js +1 -1
- package/lib/index.js +102 -26
- package/lib/providers.js +39 -5
- package/package.json +2 -2
- package/scripts/migrate-profile.mjs +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
(风险、可逆性、任务相关性、注入嫌疑),据此放行、询问或拦截。默认 `shadow`:
|
|
5
5
|
只记录判断,绝不打断你干活。
|
|
6
6
|
|
|
7
|
-
**它支持所有 system-one 风格 API
|
|
7
|
+
**它支持所有 system-one 风格 API**:内置 `system one` 与
|
|
8
8
|
`bamboonode` 两个预设,也可以填任意 `custom` base URL(自建 llama.cpp / vLLM、
|
|
9
9
|
企业网关、第三方中转)。请求路径、鉴权方式与响应结构与 system-one 协议保持一致,
|
|
10
10
|
换 provider 不影响各决策点的解析逻辑。
|
|
11
11
|
|
|
12
12
|
> **本项目原名 `dsh-startlux-decision`**(更早叫 `dsh-startlux-guard`)。
|
|
13
13
|
> 0.4.0 起更名为 `dsh-decision-engine` 并加入 provider 抽象。
|
|
14
|
-
>
|
|
14
|
+
>
|
|
15
|
+
> **0.5.0 起配置文件名从 `startlux.yml` 改为 `config.yml`**——本插件已不是
|
|
16
|
+
> 只服务 startlux 一家,名字里再挂某个服务商只会误导。旧文件会被**自动改名
|
|
17
|
+
> 迁移**,升级不需要重新填 key;迁移失败时仍会回退读取旧文件名。
|
|
15
18
|
|
|
16
19
|
## 安装
|
|
17
20
|
|
|
@@ -39,22 +42,6 @@ cd dsh-decision-engine
|
|
|
39
42
|
# 复制到 profile 的 node_modules,或 pnpm link
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
### 从旧名字升级
|
|
43
|
-
|
|
44
|
-
包名变了,profile 里的两条记录都要改(否则 DSH 找不到插件):
|
|
45
|
-
|
|
46
|
-
```jsonc
|
|
47
|
-
// ~/.dsh/profiles/desktop/package.json
|
|
48
|
-
"dsh.profile.bundles": [
|
|
49
|
-
// "dsh-startlux-decision" ← 删掉这一项
|
|
50
|
-
"dsh-decision-engine" // ← 换成这个
|
|
51
|
-
],
|
|
52
|
-
"dependencies": {
|
|
53
|
-
// "dsh-startlux-decision": "file:/Users/you/Code/dsh-startlux-decision" ← 删掉
|
|
54
|
-
"dsh-decision-engine": "file:/Users/you/Code/dsh-startlux-decision" // key 必须与包名一致
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
45
|
仓库里带了迁移脚本,不用手改(默认只预览,`--write` 才写入并自动备份):
|
|
59
46
|
|
|
60
47
|
```sh
|
|
@@ -68,18 +55,16 @@ cd ~/.dsh/profiles/desktop && pnpm install
|
|
|
68
55
|
|
|
69
56
|
| | 路径 |
|
|
70
57
|
|---|---|
|
|
71
|
-
|
|
|
72
|
-
| 旧数据目录(迁移来源,保留不动) | `~/.dsh/dsh-startlux-decision/` |
|
|
73
|
-
| 更早的位置(仅回退) | `~/.dsh/plugins/dsh-startlux-decision/`、`~/.dsh/plugins/dsh-startlux-guard/` |
|
|
58
|
+
| 数据目录(迁移目标) | `~/.dsh/dsh-decision-engine/` |
|
|
74
59
|
|
|
75
60
|
迁移规则:
|
|
76
61
|
|
|
77
|
-
- **只搬一次**:新目录已有 `
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
-
|
|
62
|
+
- **只搬一次**:新目录已有 `config.yml` 就不再动,绝不覆盖你后来改的值。
|
|
63
|
+
- **配置改名**:新目录里的旧名 `startlux.yml` 会被 `rename` 成 `config.yml`
|
|
64
|
+
(rename 失败则拷贝);跨目录迁移是**拷贝**,源文件保留,出问题可对照。
|
|
65
|
+
- **只搬两个文件**:配置文件与 `telemetry.jsonl`。目录里其它东西
|
|
66
|
+
(笔记、备份)一概不碰。
|
|
67
|
+
- **权限 600**:配置里可能有明文 key,迁移后仍保持只有你可读。
|
|
83
68
|
|
|
84
69
|
## 配置
|
|
85
70
|
|
|
@@ -90,6 +75,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
|
|
|
90
75
|
|
|
91
76
|
- **服务提供方**下拉:`system one` / `bamboonode` / `custom`,下面一行显示当前生效端点
|
|
92
77
|
- 两个开关:`启用守卫`、`真正拦截`
|
|
78
|
+
- **思考模式**下拉:`never` / `auto` / `always`(见下文「省 token 的手段」里的取舍)
|
|
93
79
|
- **模型**下拉(来自当前 provider 的 `/v1/models`,含「自动(服务端默认)」)
|
|
94
80
|
- 一个 **API Key 输入框** + `保存` 按钮
|
|
95
81
|
- 选 `custom` 时展开四个字段:`Base URL`(必填)、`端点风格`、`附加请求头`、`密钥变量名`
|
|
@@ -97,7 +83,7 @@ cd ~/.dsh/profiles/desktop && pnpm install
|
|
|
97
83
|
- **决策模型参与点**列表:每个决策点一行,右侧是 `关闭 / 观察 / 生效` 三态
|
|
98
84
|
- `自身 token 预算` 输入框,以及实时的缓存命中率与每个点的 token / 延迟统计
|
|
99
85
|
|
|
100
|
-
改完点`保存`即写入 `
|
|
86
|
+
改完点`保存`即写入 `config.yml`,**不需要重启**:监听器常驻注册,provider 与
|
|
101
87
|
模式在每次调用时现读;预算与缓存参数同样是取值函数而非启动期快照。
|
|
102
88
|
|
|
103
89
|
## Provider:接任意 system-one 风格服务
|
|
@@ -163,7 +149,7 @@ content-type: application/json
|
|
|
163
149
|
|
|
164
150
|
### 持久化与切换行为
|
|
165
151
|
|
|
166
|
-
- 所有 provider 的配置**并列**存在 `
|
|
152
|
+
- 所有 provider 的配置**并列**存在 `config.yml` 里,互不覆盖:
|
|
167
153
|
`provider` 记录当前选择,`baseUrl`/`model` 属于内置预设(bamboonode 的
|
|
168
154
|
`baseUrl` 保留老的覆盖语义),`customBaseUrl`/`customModel`/`customApiKey`/
|
|
169
155
|
`customApiKeyEnv`/`customHeaders`/`customEndpointStyle` 属于 custom。
|
|
@@ -182,7 +168,7 @@ content-type: application/json
|
|
|
182
168
|
| `keyStorage` | 写到哪里 | 适合 |
|
|
183
169
|
|---|---|---|
|
|
184
170
|
| `credentials`(默认) | `~/.dsh/.credentials.yaml` 的 `refs:` 段 | 与其他服务(DeepSeek / ModelScope …)统一托管,插件目录不留明文 |
|
|
185
|
-
| `config` | 插件配置目录下的 `
|
|
171
|
+
| `config` | 插件配置目录下的 `config.yml` | 希望配置自包含、能随目录整体备份或迁移;注意是**明文** |
|
|
186
172
|
|
|
187
173
|
在设置界面「密钥保存位置」下拉切换,保存后立即按新位置写入;已有的 key 不会被
|
|
188
174
|
自动搬家(避免"看不到的地方被改动"),需要时重新粘贴一次即可。
|
|
@@ -283,7 +269,7 @@ content-type: application/json
|
|
|
283
269
|
|
|
284
270
|
配置页的「模型」是**下拉框**,由服务商实时提供,另有「自动(服务端默认)」选项。
|
|
285
271
|
|
|
286
|
-
**默认就是「自动」**(`
|
|
272
|
+
**默认就是「自动」**(`config.yml` 里 `model:` 留空):插件**不发送该字段**,
|
|
287
273
|
服务端用它当前的默认决策模型。实测省略 `model` 返回 200,响应里回填实际使用的模型名:
|
|
288
274
|
|
|
289
275
|
```json
|
|
@@ -292,7 +278,7 @@ content-type: application/json
|
|
|
292
278
|
|
|
293
279
|
这样即便服务商将来更换模型名,留空配置也不会因此 400。
|
|
294
280
|
|
|
295
|
-
> 注意:`
|
|
281
|
+
> 注意:`config.yml` 里的 `model:` **必须写出来但留空**(`model:` 后面不跟值)。
|
|
296
282
|
> 解析器把显式空值保留为 `""`;若整行删掉,键会缺失并回落到内置的硬编码模型名,
|
|
297
283
|
> 「自动」就不生效了。
|
|
298
284
|
|
|
@@ -370,8 +356,18 @@ AbortSignal.timeout(budget) → 超时 → degrade("timeout") → 钩子 next()
|
|
|
370
356
|
于是**实测 309 次调用命中 0 次**——等于缓存完全没工作。
|
|
371
357
|
现在 4 条同类 `node -e '...'` 变体只需 1 次真实请求(命中率 60%),
|
|
372
358
|
而 `rm -rf` 不会复用任何缓存裁决。
|
|
373
|
-
- **`thinkMode
|
|
374
|
-
(该端点 `output_tokens` 恒为 0
|
|
359
|
+
- **`thinkMode` 默认 `never`**:实测主要省**延迟**(约 1.9s vs 7.2s);输入 token 基本不变
|
|
360
|
+
(该端点 `output_tokens` 恒为 0,思考开销未计入计量)。三档可在配置页「思考模式」
|
|
361
|
+
下拉切换,或手改 `config.yml`:
|
|
362
|
+
|
|
363
|
+
| 值 | 行为 | 适用 |
|
|
364
|
+
|---|---|---|
|
|
365
|
+
| `never` | 不思考。默认值 | 热路径首选,不想拖慢每次工具调用 |
|
|
366
|
+
| `auto` | 仅在模型自己判断需要时思考 | 折中 |
|
|
367
|
+
| `always` | 总是思考 | 高风险场景、排查误判时临时使用;每次约多等 5s |
|
|
368
|
+
|
|
369
|
+
非法值(如 `thinkMode: sometimes`)会被归一成 `never`,不会被原样发给服务端:
|
|
370
|
+
配置来自自研 YAML 子集解析器,绕过了 Schemastery 校验,因此取值必须在这里兜。
|
|
375
371
|
- **只读工具直接放行**:`read`/`grep`/`glob`/`todo_write` 等不发请求。
|
|
376
372
|
- **state 截断**:`args` 走头尾摘要,`recent` 只带最近 4 条、每条 240 字符。
|
|
377
373
|
- **失败冷却**:连续 3 次失败进入 60s 冷却,避免对着故障端点烧钱。
|
|
@@ -396,7 +392,7 @@ pnpm verify # 前两条一起跑
|
|
|
396
392
|
| `test/spec.test.js` | 每个决策点的问法 / 解析 / 处置与阈值边界 |
|
|
397
393
|
| `test/engine.test.js` | 适配器接线、off/observe/enforce 语义、全部降级分支 |
|
|
398
394
|
| `test/providers.test.js` | provider 预设、地址拼接、URL 校验、鉴权与请求构造 |
|
|
399
|
-
| `test/config-file.test.js` | `
|
|
395
|
+
| `test/config-file.test.js` | `config.yml` 读写往返:保存后再打开的回显、切换不互相覆盖 |
|
|
400
396
|
| `test/client-ui.test.js` | 设置界面渲染:provider 下拉、custom 字段、错误提示 |
|
|
401
397
|
|
|
402
398
|
`test/providers.test.js` 是 provider 改造的验收用例(不依赖宿主依赖,
|
|
@@ -459,7 +455,7 @@ system-one 服务),它逐点校验:
|
|
|
459
455
|
|
|
460
456
|
### 其余八个决策点
|
|
461
457
|
|
|
462
|
-
全部阈值都在 `
|
|
458
|
+
全部阈值都在 `config.yml` 里可覆盖,默认值按实测校准(见上文「阈值是实测
|
|
463
459
|
校准的」)。下表列出「改动它会怎样」:
|
|
464
460
|
|
|
465
461
|
| 键 | 默认 | 调高会怎样 | 调低会怎样 |
|
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
#
|
|
3
3
|
# 用法:复制到数据目录,按需修改。
|
|
4
4
|
# mkdir -p ~/.dsh/dsh-decision-engine
|
|
5
|
-
# cp
|
|
5
|
+
# cp config.example.yml ~/.dsh/dsh-decision-engine/config.yml
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# 文件名是 config.yml:本插件早已不是「只服务 startlux 一家」的插件,
|
|
8
|
+
# 而是任意 system-one 风格服务(system one / bamboonode / custom)的决策层,
|
|
9
|
+
# 名字里再挂某个服务商只会误导。旧名 startlux.yml 仍会被自动读取并迁移,
|
|
10
|
+
# 升级不需要重新填 key。
|
|
11
|
+
#
|
|
12
|
+
# 插件按以下顺序找配置,取第一个存在的:
|
|
13
|
+
# ~/.dsh/dsh-decision-engine/config.yml (首选位置)
|
|
14
|
+
# ~/.dsh/dsh-decision-engine/startlux.yml (旧名,首次启动自动改名)
|
|
15
|
+
# ~/.dsh/dsh-startlux-decision/startlux.yml (上一代数据目录;自动拷到新目录)
|
|
11
16
|
# ~/.dsh/plugins/dsh-startlux-decision/startlux.yml
|
|
12
17
|
# ~/.dsh/plugins/dsh-startlux-guard/startlux.yml
|
|
13
18
|
#
|
|
14
19
|
# 数据目录(插件自动读取):
|
|
15
|
-
# <上述目录>/
|
|
20
|
+
# <上述目录>/config.yml 配置
|
|
16
21
|
# <上述目录>/telemetry.jsonl 决策记录
|
|
17
22
|
#
|
|
18
23
|
# 改完立即生效:每次工具调用 / 每一步前都会检查 mtime,无需重启 DSH。
|
|
@@ -110,10 +115,6 @@ keyStorage: credentials
|
|
|
110
115
|
# bamboonodeApiKey: ""
|
|
111
116
|
# customApiKey: ""
|
|
112
117
|
|
|
113
|
-
# 历史字段:老版本的单一 key 槽位,仅对 bamboonode 生效(保留兼容,不建议再用)。
|
|
114
|
-
apiKey: ""
|
|
115
|
-
apiKeyEnv: BAMBOO_API_KEY
|
|
116
|
-
|
|
117
118
|
# .credentials.yaml 里的形态就是 refs 段:
|
|
118
119
|
# refs:
|
|
119
120
|
# BAMBOO_API_KEY: sk-你的key
|
|
@@ -122,7 +123,11 @@ apiKeyEnv: BAMBOO_API_KEY
|
|
|
122
123
|
# 超时预算(毫秒)。超时自动降级放行,连续失败进入冷却。
|
|
123
124
|
timeoutMs: 8000
|
|
124
125
|
|
|
125
|
-
#
|
|
126
|
+
# 思考模式(startlux 的 think_mode),三档:
|
|
127
|
+
# never 不思考——最省延迟(实测约 1.9s vs 7.2s),默认值,热路径首选
|
|
128
|
+
# auto 仅在模型自己判断需要时思考——折中
|
|
129
|
+
# always 总是思考——判断更充分,但每次工具调用约多等 5 秒
|
|
130
|
+
# 该端点 output_tokens 恒为 0,思考开销不计入计量,所以省的主要是延迟。
|
|
126
131
|
thinkMode: never
|
|
127
132
|
|
|
128
133
|
# ============================================================================
|
package/cordis.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -222,7 +222,7 @@ window.__ModuleLoader__.load({
|
|
|
222
222
|
* 每行左侧是「标题 + 灰色说明」,右侧是开关;底部一个「保存」按钮。
|
|
223
223
|
*
|
|
224
224
|
* 数据全部走本插件自己的 `/api/decision-engine/*`,**不用** settings 服务
|
|
225
|
-
*(旧插件死在 `settings.installSection` 上)。保存即改写
|
|
225
|
+
*(旧插件死在 `settings.installSection` 上)。保存即改写 config.yml。
|
|
226
226
|
*
|
|
227
227
|
* @param props - 槽位属性;`view === "summary"` 时只渲染一行摘要文本。
|
|
228
228
|
*/
|
|
@@ -271,6 +271,7 @@ window.__ModuleLoader__.load({
|
|
|
271
271
|
setDraft({
|
|
272
272
|
enabled: next.enabled === true,
|
|
273
273
|
mode: next.mode ?? "shadow",
|
|
274
|
+
thinkMode: next.thinkMode ?? "never",
|
|
274
275
|
model: next.rawModel ?? "",
|
|
275
276
|
systemoneModel: next.rawSystemoneModel ?? "",
|
|
276
277
|
});
|
|
@@ -352,6 +353,9 @@ window.__ModuleLoader__.load({
|
|
|
352
353
|
keyHint: "",
|
|
353
354
|
keyStorage: "credentials",
|
|
354
355
|
keyStatus: {},
|
|
356
|
+
// 兜底也要给出思考模式取值:status 请求失败时下拉框仍要能显示。
|
|
357
|
+
thinkMode: "never",
|
|
358
|
+
thinkModes: ["never", "auto", "always"],
|
|
355
359
|
stats: { requests: 0, cached: 0 },
|
|
356
360
|
};
|
|
357
361
|
|
|
@@ -359,6 +363,8 @@ window.__ModuleLoader__.load({
|
|
|
359
363
|
const draft = draftState ?? {
|
|
360
364
|
enabled: view.enabled === true,
|
|
361
365
|
mode: view.mode ?? "shadow",
|
|
366
|
+
// 思考模式也走 draft:改完点保存才落盘,与开关语义一致。
|
|
367
|
+
thinkMode: view.thinkMode ?? "never",
|
|
362
368
|
model: view.rawModel ?? "",
|
|
363
369
|
systemoneModel: view.rawSystemoneModel ?? "",
|
|
364
370
|
};
|
|
@@ -382,6 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
382
388
|
setDraft({
|
|
383
389
|
enabled: next.enabled === true,
|
|
384
390
|
mode: next.mode ?? "shadow",
|
|
391
|
+
thinkMode: next.thinkMode ?? "never",
|
|
385
392
|
model: next.rawModel ?? "",
|
|
386
393
|
systemoneModel: next.rawSystemoneModel ?? "",
|
|
387
394
|
});
|
|
@@ -488,6 +495,9 @@ window.__ModuleLoader__.load({
|
|
|
488
495
|
const patch = {
|
|
489
496
|
enabled: draft.enabled === true,
|
|
490
497
|
mode: draft.mode,
|
|
498
|
+
// 思考模式总是提交:它是全局项(与 provider 无关),
|
|
499
|
+
// 服务端还会再校验一次合法取值。
|
|
500
|
+
thinkMode: draft.thinkMode ?? "never",
|
|
491
501
|
// 模型字段只写当前 provider 自己那一格(model / systemoneModel /
|
|
492
502
|
// customModel)。混写会让「切过去再切回来」丢掉原值,也会把
|
|
493
503
|
// bamboonode 的模型名发给别的服务。
|
|
@@ -705,7 +715,7 @@ window.__ModuleLoader__.load({
|
|
|
705
715
|
"生效端点",
|
|
706
716
|
isCustom
|
|
707
717
|
? "由上方 base URL + 端点风格拼接;已带 /decisions 或 /systemone 时原样使用"
|
|
708
|
-
: "内置预设的默认地址(可用
|
|
718
|
+
: "内置预设的默认地址(可用 config.yml 的 baseUrl 覆盖 bamboonode)",
|
|
709
719
|
h(
|
|
710
720
|
"span",
|
|
711
721
|
{ style: { ...S.v, fontSize: "11px", opacity: 0.75, maxWidth: "300px" } },
|
|
@@ -739,6 +749,45 @@ window.__ModuleLoader__.load({
|
|
|
739
749
|
}),
|
|
740
750
|
"mode",
|
|
741
751
|
),
|
|
752
|
+
// 2.1) 思考模式:never 最省延迟,always 判断更充分但每次多等约 5s。
|
|
753
|
+
// 这一项以前只能手改配置文件,界面没有入口(后端一直支持三档)。
|
|
754
|
+
row(
|
|
755
|
+
"思考模式",
|
|
756
|
+
draft.thinkMode === "always"
|
|
757
|
+
? "总是思考:判断更充分,但每次工具调用约多等 5 秒"
|
|
758
|
+
: draft.thinkMode === "auto"
|
|
759
|
+
? "仅在模型自己判断需要时思考(折中)"
|
|
760
|
+
: "不思考:最快(约 1.9s),热路径首选",
|
|
761
|
+
h(
|
|
762
|
+
"select",
|
|
763
|
+
{
|
|
764
|
+
style: { ...S.input, minWidth: "190px", flex: "none" },
|
|
765
|
+
value: draft.thinkMode ?? "never",
|
|
766
|
+
disabled: busy !== "",
|
|
767
|
+
onChange: (event) => {
|
|
768
|
+
setDraft({ ...draft, thinkMode: event.target.value });
|
|
769
|
+
setDirty(true);
|
|
770
|
+
dirtyRef.current = true;
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
// 选项由服务端下发(thinkModes),前端不硬编码取值,避免漂移。
|
|
774
|
+
...(Array.isArray(view.thinkModes) && view.thinkModes.length > 0
|
|
775
|
+
? view.thinkModes
|
|
776
|
+
: ["never", "auto", "always"]
|
|
777
|
+
).map((value) =>
|
|
778
|
+
h(
|
|
779
|
+
"option",
|
|
780
|
+
{ key: value, value },
|
|
781
|
+
value === "never"
|
|
782
|
+
? "never(不思考)"
|
|
783
|
+
: value === "auto"
|
|
784
|
+
? "auto(按需思考)"
|
|
785
|
+
: "always(总是思考)",
|
|
786
|
+
),
|
|
787
|
+
),
|
|
788
|
+
),
|
|
789
|
+
"thinkMode",
|
|
790
|
+
),
|
|
742
791
|
// 3) 模型:下拉选择,含「自动(服务端默认)」
|
|
743
792
|
// custom 用自己那一格(customModel),内置预设共用 model——
|
|
744
793
|
// 分开存才能做到「切 provider 不覆盖其它 provider 已保存的模型」。
|
|
@@ -805,7 +854,7 @@ window.__ModuleLoader__.load({
|
|
|
805
854
|
row(
|
|
806
855
|
"密钥保存位置",
|
|
807
856
|
storageValue === "config"
|
|
808
|
-
? "存进插件配置
|
|
857
|
+
? "存进插件配置 config.yml(自包含,便于随目录备份;注意是明文)"
|
|
809
858
|
: "存进 ~/.dsh/.credentials.yaml(默认,与其他服务统一托管)",
|
|
810
859
|
h(
|
|
811
860
|
"select",
|
|
@@ -820,7 +869,7 @@ window.__ModuleLoader__.load({
|
|
|
820
869
|
},
|
|
821
870
|
},
|
|
822
871
|
h("option", { value: "credentials" }, ".credentials.yaml(默认)"),
|
|
823
|
-
h("option", { value: "config" }, "插件配置
|
|
872
|
+
h("option", { value: "config" }, "插件配置 config.yml"),
|
|
824
873
|
),
|
|
825
874
|
"keyStorage",
|
|
826
875
|
),
|
package/lib/config-file.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
25
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
25
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { join } from "node:path";
|
|
28
28
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -40,13 +40,16 @@ import { applyConfigUpdates, parseConfigText } from "./config-file.js";
|
|
|
40
40
|
import { applyRefUpdates, listRefNames, pickRefValue } from "./credentials.js";
|
|
41
41
|
import {
|
|
42
42
|
DEFAULT_PROVIDER,
|
|
43
|
+
DEFAULT_THINK_MODE,
|
|
43
44
|
ENDPOINT_STYLES,
|
|
44
45
|
PROVIDER_IDS,
|
|
45
46
|
PROVIDER_PRESETS,
|
|
47
|
+
THINK_MODES,
|
|
46
48
|
buildEndpoint,
|
|
47
49
|
buildHeaders,
|
|
48
50
|
buildRequestBody,
|
|
49
51
|
normalizeBaseUrl,
|
|
52
|
+
normalizeThinkMode,
|
|
50
53
|
parseCustomHeaders,
|
|
51
54
|
providerCatalog,
|
|
52
55
|
resolveKeyFor,
|
|
@@ -99,12 +102,13 @@ const DEFAULTS = {
|
|
|
99
102
|
/**
|
|
100
103
|
* 密钥保存位置:
|
|
101
104
|
* credentials = ~/.dsh/.credentials.yaml 的 refs 段(默认,与其他服务统一托管)
|
|
102
|
-
* config = 插件自己的
|
|
105
|
+
* config = 插件自己的 config.yml(自包含,便于随目录备份/迁移)
|
|
103
106
|
*/
|
|
104
107
|
keyStorage: "credentials",
|
|
105
108
|
apiKeyEnv: "BAMBOO_API_KEY",
|
|
106
109
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
107
|
-
|
|
110
|
+
// 思考模式:取值与默认值都来自 providers.js,避免两处各写一份而漂移。
|
|
111
|
+
thinkMode: DEFAULT_THINK_MODE,
|
|
108
112
|
cooldownMs: 60000,
|
|
109
113
|
failureThreshold: 3,
|
|
110
114
|
cacheSize: 256,
|
|
@@ -237,7 +241,7 @@ export const Config = z.object({
|
|
|
237
241
|
keyStorage: z.union(["credentials", "config"]).default(DEFAULTS.keyStorage)
|
|
238
242
|
.description(
|
|
239
243
|
"密钥保存位置:credentials = ~/.dsh/.credentials.yaml(默认,与其他服务统一托管);"
|
|
240
|
-
+ "config = 插件配置目录下的
|
|
244
|
+
+ "config = 插件配置目录下的 config.yml",
|
|
241
245
|
),
|
|
242
246
|
timeoutMs: z.number().step(1).min(1000).default(DEFAULTS.timeoutMs),
|
|
243
247
|
thinkMode: z.union(["never", "auto", "always"]).default(DEFAULTS.thinkMode)
|
|
@@ -438,6 +442,15 @@ export function validateProviderPatch(patch, current) {
|
|
|
438
442
|
};
|
|
439
443
|
}
|
|
440
444
|
|
|
445
|
+
// 思考模式同样在落盘前拦下:写进 yml 再报错等于让用户带着坏配置重启,
|
|
446
|
+
// 而坏值只会得到一次静默降级。
|
|
447
|
+
if (Object.hasOwn(patch, "thinkMode") && !THINK_MODES.includes(patch.thinkMode)) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
error: `未知思考模式: ${String(patch.thinkMode)}(可选 ${THINK_MODES.join(" / ")})`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
441
454
|
// 变量名必须能当环境变量用,否则凭据永远读不到。
|
|
442
455
|
for (const key of ["customApiKeyEnv", "bamboonodeApiKeyEnv", "systemoneApiKeyEnv"]) {
|
|
443
456
|
if (!Object.hasOwn(patch, key)) continue;
|
|
@@ -579,6 +592,26 @@ export function listCredentialRefs() {
|
|
|
579
592
|
/** 插件更名后的数据目录名(`~/.dsh/<name>/`)。 */
|
|
580
593
|
const DATA_DIR_NAME = "dsh-decision-engine";
|
|
581
594
|
|
|
595
|
+
/**
|
|
596
|
+
* 当前配置文件名。
|
|
597
|
+
*
|
|
598
|
+
* 早期叫 `startlux.yml`——那时本插件只服务 startlux 一家。现在它是
|
|
599
|
+
* 「对任意 system-one 风格服务的决策层」,名字里再挂某个服务商就会误导
|
|
600
|
+
* (用户会以为换 provider 要换文件),所以改叫 `config.yml`。
|
|
601
|
+
*
|
|
602
|
+
* **旧名字必须继续读**(见 `LEGACY_CONFIG_FILE_NAMES`):改文件名本身
|
|
603
|
+
* 不影响任何配置语义,一旦处理不当,用户唯一感知到的就是「我的 key 没了」。
|
|
604
|
+
*/
|
|
605
|
+
const CONFIG_FILE_NAME = "config.yml";
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* 历史配置文件名,按新→旧优先级排列。
|
|
609
|
+
*
|
|
610
|
+
* 只做**一次性迁移 + 回退读取**,绝不删除旧文件:配置里可能有明文 key,
|
|
611
|
+
* 删错一次就要用户重填。留在原地的成本只有 2KB。
|
|
612
|
+
*/
|
|
613
|
+
const LEGACY_CONFIG_FILE_NAMES = ["startlux.yml"];
|
|
614
|
+
|
|
582
615
|
/**
|
|
583
616
|
* 插件数据目录:`~/.dsh/dsh-decision-engine/`。
|
|
584
617
|
* 与插件源码分离——源码可以 clone 到任意位置,运行期数据只在 DSH 家目录下。
|
|
@@ -627,21 +660,42 @@ function legacyConfigPaths() {
|
|
|
627
660
|
* 2. 旧文件留着没有代价(配置只有 2KB),搬丢了却要用户重填 key;
|
|
628
661
|
* 3. 迁移失败必须能安静降级——所有异常都吞掉,由调用方回退。
|
|
629
662
|
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
663
|
+
* 搬两类东西:配置(含本次 `startlux.yml` → `config.yml` 的改名)与
|
|
664
|
+
* `telemetry.jsonl`。只搬这些已知文件、不递归整个目录:里面可能有用户
|
|
665
|
+
* 自己放的笔记或备份,插件无权擅自处理。
|
|
632
666
|
*
|
|
633
667
|
* @returns 是否真的搬了东西(供启动日志说明)。
|
|
634
668
|
*/
|
|
635
669
|
function migrateLegacyData() {
|
|
636
670
|
const targetDir = defaultTelemetryDir();
|
|
637
|
-
const targetConfig = join(targetDir,
|
|
671
|
+
const targetConfig = join(targetDir, CONFIG_FILE_NAME);
|
|
638
672
|
try {
|
|
639
673
|
// 新目录已有配置 → 之前迁移过,或用户主动建过。一律不再动它。
|
|
640
674
|
if (existsSync(targetConfig)) return false;
|
|
641
|
-
|
|
675
|
+
|
|
676
|
+
// 候选来源按「越新越靠前」排列:
|
|
677
|
+
// 1) 新目录里的旧文件名(本次改名的迁移来源,最常见)
|
|
678
|
+
// 2) 历史数据目录里的旧文件名
|
|
679
|
+
const candidates = [
|
|
680
|
+
...LEGACY_CONFIG_FILE_NAMES.map((name) => join(targetDir, name)),
|
|
681
|
+
...legacyConfigPaths(),
|
|
682
|
+
];
|
|
683
|
+
const legacyConfig = candidates.find((path) => existsSync(path));
|
|
642
684
|
if (legacyConfig === undefined) return false;
|
|
685
|
+
|
|
686
|
+
// 同目录改名时直接 rename:保留 mtime 之外的一切,且不会因为
|
|
687
|
+
// 「拷贝 + 旧文件仍在」造成两份配置并存(并存会导致读哪份说不清)。
|
|
688
|
+
// 跨目录/跨卷时 rename 会抛错,回退到拷贝。
|
|
643
689
|
mkdirSync(targetDir, { recursive: true });
|
|
644
|
-
|
|
690
|
+
if (join(legacyConfig, "..") === targetDir) {
|
|
691
|
+
try {
|
|
692
|
+
renameSync(legacyConfig, targetConfig);
|
|
693
|
+
} catch {
|
|
694
|
+
copyFileSync(legacyConfig, targetConfig);
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
copyFileSync(legacyConfig, targetConfig);
|
|
698
|
+
}
|
|
645
699
|
try {
|
|
646
700
|
// 配置里可能有明文 key,权限保持 600。
|
|
647
701
|
chmodSync(targetConfig, 0o600);
|
|
@@ -668,14 +722,22 @@ function migrateLegacyData() {
|
|
|
668
722
|
/**
|
|
669
723
|
* 在多个候选位置中挑出第一个存在的配置文件。
|
|
670
724
|
*
|
|
671
|
-
*
|
|
725
|
+
* 顺序:新文件名 → 新目录里的旧文件名 → 历史目录里的旧文件名。
|
|
726
|
+
* 后两者只在迁移没成功(目录只读等)时才可能命中,此时继续读写旧文件,
|
|
727
|
+
* 好过「因为搬不动就彻底不用配置」。
|
|
672
728
|
*
|
|
673
729
|
* @returns 配置文件绝对路径(都不存在时返回新路径,供首次写入使用)。
|
|
674
730
|
*/
|
|
675
731
|
function resolveConfigPath() {
|
|
676
|
-
const
|
|
732
|
+
const targetDir = defaultTelemetryDir();
|
|
733
|
+
const preferred = join(targetDir, CONFIG_FILE_NAME);
|
|
677
734
|
try {
|
|
678
735
|
if (existsSync(preferred)) return preferred;
|
|
736
|
+
// 迁移失败但旧文件名还在新目录里:继续用它,避免配置凭空消失。
|
|
737
|
+
for (const name of LEGACY_CONFIG_FILE_NAMES) {
|
|
738
|
+
const stale = join(targetDir, name);
|
|
739
|
+
if (existsSync(stale)) return stale;
|
|
740
|
+
}
|
|
679
741
|
for (const legacy of legacyConfigPaths()) {
|
|
680
742
|
if (existsSync(legacy)) return legacy;
|
|
681
743
|
}
|
|
@@ -697,7 +759,11 @@ function resolveTelemetryDir() {
|
|
|
697
759
|
if (existsSync(join(preferred, "telemetry.jsonl"))) return preferred;
|
|
698
760
|
// 新目录里还没有记录:只有「迁移失败」才继续用旧目录,
|
|
699
761
|
// 否则应当写进新目录(迁移成功但还没有新记录时属于这种情形)。
|
|
700
|
-
|
|
762
|
+
// 新旧两个文件名都要认:迁移失败时配置仍留在旧文件名下。
|
|
763
|
+
if (existsSync(join(preferred, CONFIG_FILE_NAME))) return preferred;
|
|
764
|
+
for (const name of LEGACY_CONFIG_FILE_NAMES) {
|
|
765
|
+
if (existsSync(join(preferred, name))) return preferred;
|
|
766
|
+
}
|
|
701
767
|
for (const legacy of legacyConfigPaths()) {
|
|
702
768
|
const dir = join(legacy, "..");
|
|
703
769
|
if (existsSync(join(dir, "telemetry.jsonl"))) return dir;
|
|
@@ -1102,7 +1168,7 @@ class StartluxClient {
|
|
|
1102
1168
|
}
|
|
1103
1169
|
|
|
1104
1170
|
/**
|
|
1105
|
-
* 读取插件自己的配置文件 `<插件目录>/
|
|
1171
|
+
* 读取插件自己的配置文件 `<插件目录>/config.yml`。
|
|
1106
1172
|
*
|
|
1107
1173
|
* 配置**只放这里**,不进 DSH 的 Settings 面板,也不依赖 profile 的
|
|
1108
1174
|
* cordis.patch.yml 覆盖——只有一项 api key,单独一个文件最省心,
|
|
@@ -1130,7 +1196,7 @@ async function readOwnConfig(path) {
|
|
|
1130
1196
|
/**
|
|
1131
1197
|
* 挂载守卫。
|
|
1132
1198
|
* @param ctx - cordis 插件上下文。
|
|
1133
|
-
* @param config - profile 里可选的 config(留空即可,本插件主配置在
|
|
1199
|
+
* @param config - profile 里可选的 config(留空即可,本插件主配置在 config.yml)。
|
|
1134
1200
|
*/
|
|
1135
1201
|
export function apply(ctx, config) {
|
|
1136
1202
|
// 更名迁移必须先跑:它决定后面读写的到底是哪个目录。
|
|
@@ -1138,11 +1204,11 @@ export function apply(ctx, config) {
|
|
|
1138
1204
|
const migrated = migrateLegacyData();
|
|
1139
1205
|
const ownConfigPath = resolveConfigPath();
|
|
1140
1206
|
|
|
1141
|
-
// 优先级:
|
|
1207
|
+
// 优先级:config.yml > profile 的 config 行 > DEFAULTS。
|
|
1142
1208
|
// 文件是主配置,profile 里的 config 只作为可选覆盖(便于临时试验)。
|
|
1143
1209
|
// 注意:DSH 传给 apply 的 config 是 Schemastery 解析后的**完整默认值**
|
|
1144
1210
|
//(enabled:false / mode:shadow / timeoutMs:20000 ...),并非用户显式配置。
|
|
1145
|
-
// 若直接用它覆盖
|
|
1211
|
+
// 若直接用它覆盖 config.yml,文件里保存的开关就会被永久压回默认值——
|
|
1146
1212
|
// 这正是「保存后开关回弹」的根因。因此这里先用 DEFAULTS 打底,
|
|
1147
1213
|
// 由 reloadOwnConfig 以「yml 优先、config 仅补空缺」的顺序合成。
|
|
1148
1214
|
let cfg = { ...DEFAULTS };
|
|
@@ -1157,7 +1223,7 @@ export function apply(ctx, config) {
|
|
|
1157
1223
|
const isLive = () => cfg.enabled === true;
|
|
1158
1224
|
|
|
1159
1225
|
/**
|
|
1160
|
-
* 载入
|
|
1226
|
+
* 载入 config.yml 并与当前配置合并。每次工具调用前都会调一次,
|
|
1161
1227
|
* 因此**改 yml 立即生效,不用重启**(靠 mtime 判断是否需要重读)。
|
|
1162
1228
|
*/
|
|
1163
1229
|
let lastMtime = 0;
|
|
@@ -1172,11 +1238,11 @@ export function apply(ctx, config) {
|
|
|
1172
1238
|
// 窗口,会显示一份默认配置(provider/地址全错)。代价只是并发时多读一次文件。
|
|
1173
1239
|
const own = await readOwnConfig(ownConfigPath);
|
|
1174
1240
|
if (Object.keys(own).length === 0) return;
|
|
1175
|
-
// 优先级:DEFAULTS <
|
|
1241
|
+
// 优先级:DEFAULTS < config.yml < profile 的 config 行。
|
|
1176
1242
|
// profile 显式配置必须能覆盖文件——否则用户在 cordis.patch.yml 里改的
|
|
1177
1243
|
// 值会被文件静默吃掉,测试与运维都无法覆盖(DSH 的配置层级也是这个顺序)。
|
|
1178
|
-
// 合成顺序:DEFAULTS < DSH 传入的 config <
|
|
1179
|
-
//
|
|
1244
|
+
// 合成顺序:DEFAULTS < DSH 传入的 config < config.yml。
|
|
1245
|
+
// config.yml 放最后,因为它是用户显式保存的唯一入口;DSH 传入的
|
|
1180
1246
|
// config 多为 schema 默认值,不能让它盖掉用户的选择。
|
|
1181
1247
|
cfg = { ...DEFAULTS, ...stripUndefined(config ?? {}), ...own };
|
|
1182
1248
|
lastMtime = info.mtimeMs;
|
|
@@ -1195,7 +1261,7 @@ export function apply(ctx, config) {
|
|
|
1195
1261
|
};
|
|
1196
1262
|
|
|
1197
1263
|
/**
|
|
1198
|
-
* 把键值写回
|
|
1264
|
+
* 把键值写回 config.yml —— 保留原有注释与顺序,只替换/追加目标键。
|
|
1199
1265
|
*
|
|
1200
1266
|
* 面板「保存」的落盘实现。刻意不做整文件重写:用户的注释是说明书,
|
|
1201
1267
|
* 重写一次就全丢。做法是逐行扫描,命中 `key:` 就改值,没命中就在末尾追加。
|
|
@@ -1211,7 +1277,14 @@ export function apply(ctx, config) {
|
|
|
1211
1277
|
try {
|
|
1212
1278
|
lines = (await readFile(ownConfigPath, "utf8")).split("\n");
|
|
1213
1279
|
} catch {
|
|
1214
|
-
|
|
1280
|
+
// 首次写入:给一份最小骨架,并说明这个文件是什么(用户会直接编辑它)。
|
|
1281
|
+
lines = [
|
|
1282
|
+
"# dsh-decision-engine 配置(插件自有,唯一配置文件)",
|
|
1283
|
+
"#",
|
|
1284
|
+
"# 改这个文件即生效:每次工具调用前都会检查 mtime,改完不用重启 DSH。",
|
|
1285
|
+
"# 完整配置项说明见 config.example.yml。",
|
|
1286
|
+
"",
|
|
1287
|
+
];
|
|
1215
1288
|
}
|
|
1216
1289
|
// 逐行替换而不是整文件重写:用户的注释是说明书,重写一次就全丢。
|
|
1217
1290
|
// 具体实现与格式规则见 config-file.js(有往返测试)。
|
|
@@ -1229,7 +1302,7 @@ export function apply(ctx, config) {
|
|
|
1229
1302
|
* 1. `credentials`(默认):写进 `~/.dsh/.credentials.yaml` 的 `refs:` 段,
|
|
1230
1303
|
* 键名就是该 provider 自己的变量名(TYPESAFE_API_KEY / BAMBOO_API_KEY /
|
|
1231
1304
|
* custom 的变量名)。此时 yml 里**不保留**明文 key,也不会写显式 key 字段。
|
|
1232
|
-
* 2. `config`:写进插件自己的 `
|
|
1305
|
+
* 2. `config`:写进插件自己的 `config.yml`,字段是该 provider 的显式 key
|
|
1233
1306
|
* 字段(`systemoneApiKey` / `bamboonodeApiKey` / `customApiKey`)。
|
|
1234
1307
|
* key 存进凭据文件的旧值**不清除**——换回 credentials 模式时还能用。
|
|
1235
1308
|
* 3. 传空字符串:删除该位置的 key(用户清空即撤销)。
|
|
@@ -1293,7 +1366,7 @@ export function apply(ctx, config) {
|
|
|
1293
1366
|
* @returns cordis 决策:next() 放行、{kind:'ask'} 询问、{kind:'deny'} 拦截。
|
|
1294
1367
|
*/
|
|
1295
1368
|
const guardListener = async (exec, next) => {
|
|
1296
|
-
// 每次调用前检查
|
|
1369
|
+
// 每次调用前检查 config.yml 是否改过:改配置立即生效,无需重启。
|
|
1297
1370
|
await reloadOwnConfig();
|
|
1298
1371
|
if (!isLive()) return next();
|
|
1299
1372
|
if (!cfg.guardEnabled) return next();
|
|
@@ -1520,7 +1593,7 @@ export function apply(ctx, config) {
|
|
|
1520
1593
|
buildRequestBody(provider, {
|
|
1521
1594
|
state: { probe: true, cwd: process.cwd() },
|
|
1522
1595
|
questions: { alive: { type: "noul", instructions: "Is this request well formed?" } },
|
|
1523
|
-
thinkMode:
|
|
1596
|
+
thinkMode: cfg.thinkMode,
|
|
1524
1597
|
}),
|
|
1525
1598
|
),
|
|
1526
1599
|
signal: AbortSignal.timeout(Math.max(5000, cfg.timeoutMs)),
|
|
@@ -1636,7 +1709,10 @@ export function apply(ctx, config) {
|
|
|
1636
1709
|
// 各 provider 是否已配好 key(不泄露明文,只回报布尔与来源)。
|
|
1637
1710
|
// 三个预设各自独立,切换时界面能准确显示"这一项有没有 key"。
|
|
1638
1711
|
keyStatus: buildKeyStatus(),
|
|
1639
|
-
thinkMode:
|
|
1712
|
+
// 思考模式:回传归一化后的值,避免界面把 `thinkMode: sometimes`
|
|
1713
|
+
// 原样回显成「已生效」;可选清单由服务端下发,前端不硬编码。
|
|
1714
|
+
thinkMode: normalizeThinkMode(cfg.thinkMode),
|
|
1715
|
+
thinkModes: THINK_MODES,
|
|
1640
1716
|
timeoutMs: cfg.timeoutMs,
|
|
1641
1717
|
keyConfigured: key !== undefined,
|
|
1642
1718
|
keyHint: key === undefined ? "" : `…${key.slice(-4)}`,
|
package/lib/providers.js
CHANGED
|
@@ -53,6 +53,40 @@ export const ENDPOINT_STYLES = ["auto", "decisions", "systemone"];
|
|
|
53
53
|
/** 端点风格 → 追加的路径段。 */
|
|
54
54
|
const STYLE_SEGMENT = { decisions: "decisions", systemone: "systemone" };
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 合法的思考模式(与 startlux 服务的 `think_mode` 取值一致)。
|
|
58
|
+
*
|
|
59
|
+
* never 不思考——最省延迟(实测约 1.9s vs 7.2s),默认值
|
|
60
|
+
* auto 仅在模型自己判断需要时思考
|
|
61
|
+
* always 总是思考——判断更充分,但热路径上每次多等约 5s
|
|
62
|
+
*
|
|
63
|
+
* 单独导出是为了让校验、前端下拉、文档三处只有一份取值来源。
|
|
64
|
+
*/
|
|
65
|
+
export const THINK_MODES = ["never", "auto", "always"];
|
|
66
|
+
|
|
67
|
+
/** 思考模式的默认值:热路径上优先保证不拖慢每次工具调用。 */
|
|
68
|
+
export const DEFAULT_THINK_MODE = "never";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 把配置里的 thinkMode 归一成合法取值。
|
|
72
|
+
*
|
|
73
|
+
* 为什么需要它:本插件的配置来自**自研的极简 YAML 解析器**,完全绕过
|
|
74
|
+
* Schemastery 的 `z.union([...])` 校验(那是 profile 层的 config 才走的路径)。
|
|
75
|
+
* 于是 `thinkMode: sometimes` 这种手写错误会被原样发到服务端,得到 400 后
|
|
76
|
+
* 静默降级放行——正是本插件最忌讳的「配了没反应」。
|
|
77
|
+
*
|
|
78
|
+
* 特别注意**空字符串**:`parseConfigText` 刻意把 `key:` 解析成 `""`
|
|
79
|
+
*(那是 `model:` 表示「自动」的语义),但 `""` 对 think_mode 是非法的。
|
|
80
|
+
* 因此这里不能用 `??` 兜底(它只挡 null/undefined),必须显式判空。
|
|
81
|
+
*
|
|
82
|
+
* @param raw - 配置里的原始值(可能是任意类型)。
|
|
83
|
+
* @returns 合法取值;非法一律回退默认值。
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeThinkMode(raw) {
|
|
86
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
87
|
+
return THINK_MODES.includes(value) ? value : DEFAULT_THINK_MODE;
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
/**
|
|
57
91
|
* 内置预设。
|
|
58
92
|
*
|
|
@@ -230,7 +264,7 @@ export function buildEndpoint(baseUrl, style = "decisions") {
|
|
|
230
264
|
*
|
|
231
265
|
* 存储形态是 `Name: Value; Name2: Value2` 的单行字符串——本插件的配置读取器
|
|
232
266
|
* 是极简 YAML 子集(只认标量与 `[a, b]` 数组),嵌套映射读不出来;用分号
|
|
233
|
-
* 分隔既能在 `
|
|
267
|
+
* 分隔既能在 `config.yml` 里手写,也能被面板原样回显。
|
|
234
268
|
*
|
|
235
269
|
* 受保护的头(`authorization` / `content-type` / `host`)会被忽略:鉴权方式
|
|
236
270
|
* 必须与 system-one 风格保持一致,否则换 provider 就可能悄悄换掉认证协议。
|
|
@@ -265,7 +299,7 @@ export function parseCustomHeaders(raw) {
|
|
|
265
299
|
* 这是全插件唯一的「provider → 请求参数」解析点。`classify()`、探测、
|
|
266
300
|
* 模型列表、状态接口都调它,因此三处不可能再各写一套地址规则。
|
|
267
301
|
*
|
|
268
|
-
* 解析顺序(内置预设):预设默认值打底,`
|
|
302
|
+
* 解析顺序(内置预设):预设默认值打底,`config.yml` 里显式写的值覆盖。
|
|
269
303
|
* 对 bamboonode 额外保留 `baseUrl` 的覆盖语义——那是老版本唯一的地址字段,
|
|
270
304
|
* 用户可能已经指向自建的 bamboonode 兼容服务,升级不能把它改回官方地址。
|
|
271
305
|
*
|
|
@@ -293,7 +327,7 @@ export function resolveProvider(cfg = {}) {
|
|
|
293
327
|
endpointStyle: style,
|
|
294
328
|
model,
|
|
295
329
|
defaultModel: "",
|
|
296
|
-
thinkMode: cfg.thinkMode,
|
|
330
|
+
thinkMode: normalizeThinkMode(cfg.thinkMode),
|
|
297
331
|
apiKeyEnv: firstNonEmpty(cfg.customApiKeyEnv, preset.apiKeyEnv),
|
|
298
332
|
legacyApiKeyEnvs: [...(preset.legacyApiKeyEnvs ?? [])],
|
|
299
333
|
explicitKey: typeof cfg[preset.keyField] === "string" ? cfg[preset.keyField].trim() : "",
|
|
@@ -340,7 +374,7 @@ export function resolveProvider(cfg = {}) {
|
|
|
340
374
|
endpointStyle: preset.endpointStyle,
|
|
341
375
|
model,
|
|
342
376
|
defaultModel: preset.defaultModel,
|
|
343
|
-
thinkMode: cfg.thinkMode,
|
|
377
|
+
thinkMode: normalizeThinkMode(cfg.thinkMode),
|
|
344
378
|
apiKeyEnv: configuredKeyEnv,
|
|
345
379
|
legacyApiKeyEnvs: fallbackKeyEnvs,
|
|
346
380
|
// 显式 key 也按 provider 分开存:切到 system one 时不会拿 bamboonode 的
|
|
@@ -429,7 +463,7 @@ export function buildRequestBody(provider, payload) {
|
|
|
429
463
|
...(model.length > 0 ? { model } : {}),
|
|
430
464
|
state: payload.state,
|
|
431
465
|
questions: payload.questions,
|
|
432
|
-
think_mode: payload.thinkMode ?? provider.thinkMode
|
|
466
|
+
think_mode: normalizeThinkMode(payload.thinkMode ?? provider.thinkMode),
|
|
433
467
|
};
|
|
434
468
|
}
|
|
435
469
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "dsh-decision-engine: a cheap judgement layer for DeepSeek Harness — all nine decision points (tool authorization, model routing, delegation, compaction, turn continuation, retrieval, skill pick, session title, COT translation) are implemented and individually switchable, and it works against any system-one style API (system one, bamboonode, or a custom base URL).",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"lib",
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"README.md",
|
|
22
|
-
"
|
|
22
|
+
"config.example.yml",
|
|
23
23
|
"icon.svg",
|
|
24
24
|
"locale",
|
|
25
25
|
"scripts/migrate-profile.mjs"
|
|
@@ -99,4 +99,4 @@ await copyFile(profilePath, backup);
|
|
|
99
99
|
await writeFile(profilePath, next, "utf8");
|
|
100
100
|
console.log(`\n已写入。原文件备份在:${backup}`);
|
|
101
101
|
console.log("接着执行:cd " + join(profilePath, "..") + " && pnpm install");
|
|
102
|
-
console.log("然后重启 DSH。配置(startlux.yml
|
|
102
|
+
console.log("然后重启 DSH。配置(config.yml,旧名 startlux.yml 会被自动迁移)不需要重填。");
|