dsh-model-reasoning-defaults 0.0.3 → 0.0.4
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 +40 -61
- package/cordis.patch.yml +6 -3
- package/lib/client.js +150 -58
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +40 -55
- package/lib/index.mjs +110 -237
- package/lib/index.mjs.map +1 -1
- package/lib/types/client/index.d.ts +2 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/route-table.d.ts +60 -0
- package/lib/types/client/route-table.d.ts.map +1 -0
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
- **仓库**:<https://github.com/xht-code/dsh-model-reasoning-defaults>
|
|
11
11
|
|
|
12
|
+
> [!IMPORTANT]
|
|
13
|
+
> 本版本(0.0.4+)面向 **DSH 0.1.7-alpha.2 及以上的设置体系**:设置项由 profile entry 的 `Config` schema 派生,可编辑字段必须声明为 `volatile`,写回落在 profile patch。旧版宿主(0.1.6 及以前)请使用 0.0.3。
|
|
14
|
+
|
|
12
15
|
## 安装 / 更新
|
|
13
16
|
|
|
14
17
|
安装插件到指定的 DSH profile:
|
|
@@ -34,10 +37,10 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
|
|
|
34
37
|
## 核心特性
|
|
35
38
|
|
|
36
39
|
- **多层级灵活路由**:支持从具体模型、Provider 通配到全局兜底的 6 级优先级匹配机制。
|
|
37
|
-
- **设置页可视化编辑**:在 Web
|
|
40
|
+
- **设置页可视化编辑**:在 Web 设置页「推理等级默认」中直接增删路由并选择等级。路由表存放在插件自身的配置项里并声明为 `volatile`,宿主原地更新引用而不重载插件,保存后对新请求立即生效(带修订号并发保护)。
|
|
38
41
|
- **全入口统一注入**:无缝覆盖 Agent 主循环(`agent/request`)、直接调用(`llm.stream`)以及预准备入口(`prepareCall` / `resolveCallConfig`)。
|
|
39
42
|
- **对象安全与不可变性保证**:采用非破坏性浅复制机制,安全处理 `Object.freeze` 深冻结请求,原请求对象与未命中的配置始终保持只读与纯净。
|
|
40
|
-
-
|
|
43
|
+
- **宿主标识直读**:loop 请求识别直接使用 DSH 导出的 `isAgentLoopRequest`(基于宿主 `markAgentLoopRequest` 进程标识),不维护自建登记表,跨模块副本与热重载天然一致。
|
|
41
44
|
- **无缝热重载(Hot-Reload Safe)**:基于 `RuntimePatchState` 管理多 Fiber 状态,配置变更即时生效,仅在最后一个插件 Fiber 卸载时还原原始方法,杜绝方法悬挂。
|
|
42
45
|
|
|
43
46
|
---
|
|
@@ -65,59 +68,32 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
|
|
|
65
68
|
|
|
66
69
|
### 方式 1:在 Web 设置页中直接编辑(推荐)
|
|
67
70
|
|
|
68
|
-
打开 Web 端 **设置
|
|
71
|
+
打开 Web 端 **设置 →「推理等级默认」**,即可增删路由行并从下拉框选择推理等级,保存后对新请求**即时生效**(无需重启或重载)。设置页经宿主统一 settings Remote 把改动写回插件自身的配置项(profile patch 中 id 为 `model-reasoning-defaults` 的 entry):
|
|
69
72
|
|
|
70
73
|
```yaml
|
|
71
|
-
#
|
|
72
|
-
model-reasoning-defaults
|
|
73
|
-
defaults
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
# ~/.dsh/profiles/<profile>/cordis.patch.yml(设置页保存后的落盘结果)
|
|
75
|
+
- id: model-reasoning-defaults
|
|
76
|
+
name: dsh-model-reasoning-defaults
|
|
77
|
+
config:
|
|
78
|
+
defaults:
|
|
79
|
+
"deepseek-official:*": high
|
|
80
|
+
"*": medium
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
> [!NOTE]
|
|
84
|
+
> - 写入按路径寻址(对 `defaults.<路由键>` 设值或删除),因此删除路由会真正落盘,也不会碰到同一 entry 里的结构化字段。
|
|
79
85
|
> - 路由键支持全部六级写法;等级下拉框为已知集合(off/minimal/low/medium/high/xhigh/max),也接受手工输入其它值。
|
|
80
|
-
> -
|
|
86
|
+
> - 编辑器带修订号保护:两个标签页同时编辑时,后保存的一方会被拒绝并刷新到最新版本,而不是静默覆盖。
|
|
81
87
|
> - 面板内嵌匹配语义说明(六级优先级次序与生效优先级),输入路由键时可参考已配置的 Provider/Model 建议。
|
|
88
|
+
> - 面板展示的是**解析后**的生效路由表(组合层 + 用户层)。如果某个键是由 **bundle 组合层**(而非 profile 用户层)提供的,宿主会把删除改写成"写回该层原值",删除这类键需要改动提供它的那一层;本插件自带 bundle 不提供 `defaults`,按方式 2 / 方式 3 在用户层声明的键不受影响。
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# settings.yaml
|
|
89
|
-
llm-pi-ai:
|
|
90
|
-
providers:
|
|
91
|
-
my-gateway:
|
|
92
|
-
apiKeyEnv: GATEWAY_API_KEY
|
|
93
|
-
baseURL: https://gateway.example/v1
|
|
94
|
-
# 1. Provider 级默认思考等级(llm-pi-ai 原生字段)
|
|
95
|
-
reasoning: medium
|
|
96
|
-
models:
|
|
97
|
-
# 2. 模型级默认思考等级(本插件扩展读取的字段,见下方说明)
|
|
98
|
-
- id: deepseek-reasoner
|
|
99
|
-
reasoning: high
|
|
100
|
-
- id: deepseek-chat
|
|
101
|
-
reasoning: low
|
|
102
|
-
|
|
103
|
-
# 3. 官方目录 Provider:在 modelOverrides 中为具体模型指定默认思考等级
|
|
104
|
-
# (同为扩展读取字段)
|
|
105
|
-
anthropic:
|
|
106
|
-
apiKeyEnv: ANTHROPIC_API_KEY
|
|
107
|
-
modelOverrides:
|
|
108
|
-
claude-3-7-sonnet:
|
|
109
|
-
reasoning: high
|
|
110
|
-
|
|
111
|
-
llm-deepseek:
|
|
112
|
-
# 4. DeepSeek 官方 Provider 默认思考等级(原生字段,映射到 deepseek-official 路由)
|
|
113
|
-
reasoningEffort: high
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
> [!IMPORTANT]
|
|
117
|
-
> - 上例第 2、3 处的模型级 `reasoning` 是**本插件扩展读取的约定字段**:`llm-pi-ai` 的 schema 在模型层声明的原生字段是 `reasoningEfforts`("可选等级集合 + 线格式映射"的能力声明,语义是模型**能选什么**),并非默认值选择(模型**默认选什么**);本插件读取的 `reasoning` 依赖其设置解析对未声明键的透传行为。若上游收紧解析策略,这部分提取会静默失效(届时请改用方式 3 / 方式 4 显式声明)。另注意:注入的默认等级若不在目标模型 `reasoningEfforts` 声明集合内,会被适配器拒绝。
|
|
118
|
-
> - 每个来源只认单一规范字段:`llm-pi-ai` 用 `reasoning`,`llm-deepseek` 用 `reasoningEffort`,两者不可混写。
|
|
90
|
+
> [!NOTE]
|
|
91
|
+
> **Provider 级默认值由 DSH 各 Provider 原生处理,本插件不再重复读取。**
|
|
92
|
+
> `llm-pi-ai` 的 `providers.<id>.reasoning` 与 `llm-deepseek` 的 `reasoningEffort`
|
|
93
|
+
> 在请求未显式携带 `reasoningEffort` 时由适配器自行应用,与本插件注入的结果等价;
|
|
94
|
+
> 需要**模型级**或**跨 Provider** 的默认值时,请用下面的方式 2 / 方式 3 写进本插件的路由表。
|
|
119
95
|
|
|
120
|
-
### 方式
|
|
96
|
+
### 方式 2:在插件配置中按 Provider/Model 结构声明
|
|
121
97
|
|
|
122
98
|
插件的配置通过 loader 补丁体系按 entry id 定位。在用户补丁层(如 `~/.dsh/profiles/<profile>/cordis.patch.yml`)按同一 id 覆盖 `config`:
|
|
123
99
|
|
|
@@ -142,7 +118,7 @@ llm-deepseek:
|
|
|
142
118
|
another-gateway: low
|
|
143
119
|
```
|
|
144
120
|
|
|
145
|
-
### 方式
|
|
121
|
+
### 方式 3:扁平路由映射字典
|
|
146
122
|
|
|
147
123
|
直接使用扁平的 `defaults` 路由字典,与结构化形式是同等的配置入口:
|
|
148
124
|
|
|
@@ -158,13 +134,14 @@ llm-deepseek:
|
|
|
158
134
|
```
|
|
159
135
|
|
|
160
136
|
> [!TIP]
|
|
161
|
-
>
|
|
137
|
+
> 各配置形式可以自由组合,归一化到同一张路由表后统一检索。命中由上表的**键特异性**全局裁决(例如精确键 `my-gateway:deepseek-reasoner` 胜过 Provider 通配键 `my-gateway:*`),与配置写在哪个文件无关。整条链路对同一个请求的生效优先级(高 → 低):
|
|
162
138
|
>
|
|
163
|
-
> 1.
|
|
164
|
-
> 2.
|
|
165
|
-
> 3.
|
|
139
|
+
> 1. 请求自带的 `reasoningEffort`(插件绝不覆盖)
|
|
140
|
+
> 2. 本插件路由表:`defaults` 显式路由键(设置页写入或 patch 层声明)> 结构化 `providers`/`models` 项
|
|
141
|
+
> 3. Provider 原生默认值(`llm-pi-ai` 的 `reasoning`、`llm-deepseek` 的 `reasoningEffort`)
|
|
142
|
+
> 4. Provider 内建兜底(如 `llm-deepseek` 的 `high`/`thinking: disabled` 时的 `off`)
|
|
166
143
|
>
|
|
167
|
-
>
|
|
144
|
+
> 因此插件路由表可以精确覆盖某个模型,而不必改动 Provider 自身的默认值。
|
|
168
145
|
|
|
169
146
|
---
|
|
170
147
|
|
|
@@ -180,7 +157,7 @@ llm-deepseek:
|
|
|
180
157
|
【Agent Loop 请求】 【手写 / 辅助请求】
|
|
181
158
|
(agent/request Waterfall) (llm.stream / prepareCall)
|
|
182
159
|
│ │
|
|
183
|
-
|
|
160
|
+
注入默认 reasoningEffort isAgentLoopRequest 判定
|
|
184
161
|
到调用配置种子(loop 随后据此构建完整 │
|
|
185
162
|
请求并深冻结、打宿主进程标识) ┌─────────────┴─────────────┐
|
|
186
163
|
│ ▼ ▼
|
|
@@ -197,27 +174,29 @@ llm-deepseek:
|
|
|
197
174
|
|
|
198
175
|
### 1. Agent 主循环请求处理
|
|
199
176
|
- 注册全局 `agent/request` waterfall 监听器。此阶段操作的是调用配置种子,完整请求对象尚未构建;插件按路由计算默认等级并注入 `reasoningEffort`。
|
|
200
|
-
- loop
|
|
201
|
-
- 请求进入 `llm.stream`
|
|
177
|
+
- loop 随后将该配置连同派生消息组装为完整请求,深冻结并打上宿主 `markAgentLoopRequest` 进程标识。
|
|
178
|
+
- 请求进入 `llm.stream` 入口时,入口包装用宿主导出的 `isAgentLoopRequest` 识别 loop 请求,**直接按原引用透传**——浅复制会丢失该进程标识,导致 compaction 与请求观察者把会话请求误判为手写一次性请求。
|
|
202
179
|
|
|
203
180
|
### 2. 手写及辅助调用处理
|
|
204
181
|
- 直接调用 `llm.stream(options)` 时,入口包装在 waterfall **开始前**执行拦截。
|
|
205
|
-
-
|
|
182
|
+
- 对不带 loop 标识的请求(包括带 `purpose: 'compaction'` 的辅助请求),通过浅复制补齐 `reasoningEffort` 后再进入下游链路。
|
|
206
183
|
|
|
207
184
|
### 3. 配置准备与解析入口
|
|
208
185
|
- 同步拦截 `llm.prepareCall` 与 `llm.resolveCallConfig`,在预计算阶段统一应用相同的路由查找规则,并完整透传 `AbortSignal`。
|
|
209
186
|
|
|
210
187
|
### 4. 共享状态与热重载安全
|
|
211
|
-
- LLM 方法包装层内部维护 `RuntimePatchState`,通过 `
|
|
212
|
-
- 插件重新加载时,新 Fiber
|
|
188
|
+
- LLM 方法包装层内部维护 `RuntimePatchState`,通过 `sources: Map<symbol, RouteTableSource>` 跟踪每个活跃 Fiber 的路由表来源(结构化字段快照 + `defaults` 的 volatile 引用)。
|
|
189
|
+
- 插件重新加载时,新 Fiber 的路由表来源会立即生效并覆盖旧来源;旧 Fiber 卸载时仅移除自身 token,只有当所有 Fiber 都卸载时才彻底还原原始 LLM 方法。
|
|
190
|
+
- 设置页保存只改动 `defaults`(volatile 字段),宿主原地更新其引用而不重载插件;插件在每次请求时读取当下快照,所以空路由表也照常安装包装层——否则运行期新增的路由要等宿主重启才会生效。
|
|
213
191
|
|
|
214
192
|
---
|
|
215
193
|
|
|
216
194
|
## 客户端支持
|
|
217
195
|
|
|
218
196
|
本插件包含 Web 客户端扩展(`src/client/index.ts`):
|
|
219
|
-
- 注册到 DSH 设置页槽位 `settings.section
|
|
220
|
-
- 数据经宿主统一 settings Remote(`remote.settings.describe` / `
|
|
197
|
+
- 注册到 DSH 设置页槽位 `settings.section`(「推理等级默认」面板):增删路由行、从已知等级集合下拉选择,保存写入本插件配置项(entry id `model-reasoning-defaults`)的 `defaults` 字段。
|
|
198
|
+
- 数据经宿主统一 settings Remote(`remote.settings.describe` / `mutate`)读取渲染;保存携带 `expectedRevision` 修订号,并发编辑时由宿主裁决而非静默覆盖。写入使用**路径寻址操作**(对 `defaults.<路由键>` 设值或删除):merge 语义下省略键不表达删除,只有 `unset` 才能把删除落盘,同时不触碰同一 entry 里的结构化字段。
|
|
199
|
+
- 保存撞上修订号冲突时保留本地编辑并刷新到宿主最新版本,提示用户再次保存覆盖。
|
|
221
200
|
- 路由键输入带建议弹层(来自 `llm-pi-ai` / `llm-deepseek` 已配置的 Provider/Model),面板内展示匹配语义与生效优先级说明。
|
|
222
201
|
|
|
223
202
|
---
|
|
@@ -271,7 +250,7 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
|
|
|
271
250
|
## 注意事项与设计说明
|
|
272
251
|
|
|
273
252
|
1. **推理等级有效性校验时机**:
|
|
274
|
-
配置阶段仅校验字符串非空,具体推理等级(如 `low` / `medium` / `high`)是否被目标模型支持,由 DSH 底层适配器在实际发起请求时校验(未支持将抛出 `UNSUPPORTED_REASONING_EFFORT
|
|
253
|
+
配置阶段仅校验字符串非空,具体推理等级(如 `low` / `medium` / `high`)是否被目标模型支持,由 DSH 底层适配器在实际发起请求时校验(未支持将抛出 `UNSUPPORTED_REASONING_EFFORT`)。插件会在激活时、以及读取运行期写入的路由表时对照已知等级集合(`off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`)提示可疑取值(同一问题只告警一次),但不阻断注入,以便上游新增等级时无需同步升级本插件。
|
|
275
254
|
2. **会话持久化影响**:
|
|
276
255
|
由插件注入的默认值经 `llm.prepareCall` 后将作为显式参数写入请求头。在长会话进行中若热更新修改了配置规则,已创建的历史请求头不会被回溯修改。
|
|
277
256
|
3. **发布与打包约束**:
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# bundle patch 层:向 profile 插入本插件入口行。
|
|
2
|
-
#
|
|
3
|
-
# config 支持结构化 providers/models
|
|
2
|
+
# 该 entry 的 config 既可由设置页写入,也可在用户层按同一 entry id 覆盖
|
|
3
|
+
# (include 补丁体系按 id 定位);config 支持结构化 providers/models、
|
|
4
|
+
# 扁平 defaults 字典或两者混用:
|
|
4
5
|
#
|
|
5
6
|
# - id: model-reasoning-defaults
|
|
6
7
|
# config:
|
|
@@ -14,7 +15,9 @@
|
|
|
14
15
|
# o3-mini: high
|
|
15
16
|
# reasoningEffort: low
|
|
16
17
|
#
|
|
17
|
-
# #
|
|
18
|
+
# # 扁平路由映射字典(与结构化产生同键时覆盖结构化项)。
|
|
19
|
+
# # 设置页只编辑这一段:它声明为 volatile,宿主原地更新引用,
|
|
20
|
+
# # 保存后无需重载插件即可对新请求生效。
|
|
18
21
|
# defaults:
|
|
19
22
|
# "my-gateway:deepseek-reasoner": high
|
|
20
23
|
# "my-gateway:*": medium
|
package/lib/client.js
CHANGED
|
@@ -6,17 +6,94 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
//#region src/client/route-table.ts
|
|
10
|
+
/** 路由表在配置项中的字段名:操作路径的前缀。 */
|
|
11
|
+
const DEFAULTS_FIELD = "defaults";
|
|
12
|
+
/**
|
|
13
|
+
* 把路由字典展开成按键排序的可编辑行。
|
|
14
|
+
*
|
|
15
|
+
* @param defaults - 路由字典。
|
|
16
|
+
* @returns 稳定排序的行列表。
|
|
17
|
+
*/
|
|
18
|
+
function rowsOf(defaults) {
|
|
19
|
+
return Object.entries(defaults).map(([key, effort]) => ({
|
|
20
|
+
key,
|
|
21
|
+
effort
|
|
22
|
+
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 把存储中"外部新增"的路由键并入当前编辑行,用于冲突后的重试保存。
|
|
26
|
+
*
|
|
27
|
+
* 只补 `latest` 相对 `previous` 新增的键:差异语义是"行里没有即删除",补进来的
|
|
28
|
+
* 外部新增键可避免重试时顺手删掉它们;而用户自己删掉或改名的键本来就在
|
|
29
|
+
* `previous` 里,绝不能再补回来——补回来等于把删除意图覆盖成"键仍在行中",
|
|
30
|
+
* 用户会发现该路由怎么都删不掉,重命名也会退化成"新增"。
|
|
31
|
+
*
|
|
32
|
+
* @param rows - 当前编辑的行。
|
|
33
|
+
* @param previous - 发起本次保存所用的基线,即用户编辑所依据的键集。
|
|
34
|
+
* @param latest - 宿主当前生效的路由表。
|
|
35
|
+
* @returns 合并后的行列表,按键排序(空键行是待填写的残渣,排在末尾)。
|
|
36
|
+
*/
|
|
37
|
+
function mergeStoredRows(rows, previous, latest) {
|
|
38
|
+
const known = new Set(rows.map((row) => row.key.trim()).filter((key) => key.length > 0));
|
|
39
|
+
const appended = Object.entries(latest).filter(([key]) => !Object.hasOwn(previous, key) && !known.has(key)).map(([key, effort]) => ({
|
|
40
|
+
key,
|
|
41
|
+
effort
|
|
42
|
+
}));
|
|
43
|
+
return [...rows, ...appended].sort((a, b) => {
|
|
44
|
+
const left = a.key.trim();
|
|
45
|
+
const right = b.key.trim();
|
|
46
|
+
if (left.length === 0) return right.length === 0 ? 0 : 1;
|
|
47
|
+
if (right.length === 0) return -1;
|
|
48
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 把编辑后的行集合与存储基线做差异,折叠成路径寻址的写入操作。
|
|
53
|
+
*
|
|
54
|
+
* 只写差异而不是整段覆盖:宿主按路径写入,删除路由才能落盘(省略键不表达删除),
|
|
55
|
+
* 同时不动同一配置项里的结构化字段。
|
|
56
|
+
*
|
|
57
|
+
* @param baseline - 存储中当前生效的路由表。
|
|
58
|
+
* @param rows - 当前编辑的行。
|
|
59
|
+
* @returns 写入操作列表;路由键重复时返回错误消息。
|
|
60
|
+
*/
|
|
61
|
+
function buildRouteOps(baseline, rows) {
|
|
62
|
+
const next = Object.create(null);
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const key = row.key.trim();
|
|
65
|
+
if (key.length === 0) continue;
|
|
66
|
+
if (Object.hasOwn(next, key)) return { error: "存在重复路由键:" + key };
|
|
67
|
+
next[key] = row.effort;
|
|
68
|
+
}
|
|
69
|
+
const ops = [];
|
|
70
|
+
for (const [key, effort] of Object.entries(next)) if (baseline[key] !== effort) ops.push({
|
|
71
|
+
op: "set",
|
|
72
|
+
path: [DEFAULTS_FIELD, key],
|
|
73
|
+
value: effort
|
|
74
|
+
});
|
|
75
|
+
for (const key of Object.keys(baseline)) if (!Object.hasOwn(next, key)) ops.push({
|
|
76
|
+
op: "unset",
|
|
77
|
+
path: [DEFAULTS_FIELD, key]
|
|
78
|
+
});
|
|
79
|
+
return ops;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
9
82
|
//#region src/client/index.ts
|
|
10
83
|
/**
|
|
11
84
|
* dsh-model-reasoning-defaults 的客户端半体。
|
|
12
85
|
*
|
|
13
|
-
* - 设置页 section
|
|
14
|
-
*
|
|
86
|
+
* - 设置页 section:编辑本插件配置项(profile patch 中 id 为
|
|
87
|
+
* model-reasoning-defaults 的 `defaults` 字段)里的扁平路由表,经宿主统一的
|
|
88
|
+
* settings Remote 写入。Host 每次请求实时读取该 volatile 字段,保存即生效。
|
|
15
89
|
*
|
|
16
90
|
* 控件一律使用官方 @deepseek-ai/dsh-client-ui-primitives(平台基线模块,
|
|
17
91
|
* 宿主模块表直接应答),与设置壳视觉一致。
|
|
18
92
|
*/
|
|
19
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* 本插件的设置命名空间。宿主侧的命名空间就是配置项 id——插件自带 bundle patch
|
|
95
|
+
* 插入的那条 loader entry 的 id(见仓库根目录 cordis.patch.yml),不是插件名。
|
|
96
|
+
*/
|
|
20
97
|
const SETTINGS_NS = "model-reasoning-defaults";
|
|
21
98
|
/** 已知推理等级集合(与 Host 侧 KNOWN_REASONING_EFFORTS 保持一致)。 */
|
|
22
99
|
const KNOWN_EFFORTS = [
|
|
@@ -45,13 +122,24 @@ window.__ModuleLoader__.load({
|
|
|
45
122
|
function findOwnNamespace(namespaces) {
|
|
46
123
|
const own = namespaces.find((entry) => entry.ns === SETTINGS_NS);
|
|
47
124
|
if (own === void 0) return void 0;
|
|
48
|
-
const section = own.value;
|
|
49
125
|
return {
|
|
50
|
-
defaults:
|
|
126
|
+
defaults: defaultsOf(own.value),
|
|
51
127
|
revision: own.revision
|
|
52
128
|
};
|
|
53
129
|
}
|
|
54
130
|
/**
|
|
131
|
+
* 从命名空间解析值中取出路由字典。
|
|
132
|
+
*
|
|
133
|
+
* @param value - 命名空间视图的解析值。
|
|
134
|
+
* @returns 路由字典副本;形状不符时为空对象。
|
|
135
|
+
*/
|
|
136
|
+
function defaultsOf(value) {
|
|
137
|
+
const section = value;
|
|
138
|
+
if (section === null || typeof section !== "object") return {};
|
|
139
|
+
const defaults = section.defaults;
|
|
140
|
+
return defaults !== void 0 && typeof defaults === "object" ? { ...defaults } : {};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
55
143
|
* 从 describe 应答中收集已知 provider 与 provider:model 路由键,作为输入建议。
|
|
56
144
|
*
|
|
57
145
|
* @param namespaces - describe 应答携带的命名空间视图列表。
|
|
@@ -166,40 +254,13 @@ window.__ModuleLoader__.load({
|
|
|
166
254
|
document.head.appendChild(tag);
|
|
167
255
|
}
|
|
168
256
|
/**
|
|
169
|
-
* 把路由字典展开成按键排序的可编辑行。
|
|
170
|
-
*
|
|
171
|
-
* @param defaults - 路由字典。
|
|
172
|
-
* @returns 稳定排序的行列表。
|
|
173
|
-
*/
|
|
174
|
-
function rowsOf(defaults) {
|
|
175
|
-
return Object.entries(defaults).map(([key, effort]) => ({
|
|
176
|
-
key,
|
|
177
|
-
effort
|
|
178
|
-
})).sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* 校验待保存的行集合并折叠回路由字典。
|
|
182
|
-
*
|
|
183
|
-
* @param rows - 当前编辑的行。
|
|
184
|
-
* @returns 校验通过时返回字典;失败时返回错误消息。
|
|
185
|
-
*/
|
|
186
|
-
function collapseRows(rows) {
|
|
187
|
-
const result = {};
|
|
188
|
-
for (const row of rows) {
|
|
189
|
-
const key = row.key.trim();
|
|
190
|
-
if (key.length === 0) continue;
|
|
191
|
-
if (key in result) return { error: "存在重复路由键:" + key };
|
|
192
|
-
result[key] = row.effort;
|
|
193
|
-
}
|
|
194
|
-
return result;
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
257
|
* 设置页「推理等级默认」编辑面板。
|
|
198
258
|
*
|
|
199
259
|
* 数据流:挂载时 describe 读一次(拿路由表、修订号、只读标记与建议键);
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
260
|
+
* 保存时把编辑差异作为路径操作连同 expectedRevision 发出,陈旧编辑会被宿主
|
|
261
|
+
* 拒绝而不是静默覆盖——撞上冲突时保留本地编辑并刷新修订号,用户再保存一次
|
|
262
|
+
* 即可覆盖。外部改动(另一标签页、手工编辑 profile patch)在本组件重新挂载时
|
|
263
|
+
* 可见——设置壳每次打开都重新挂载 section,无需订阅失效事件。
|
|
203
264
|
*
|
|
204
265
|
* @param props - 无属主属性(close 由壳持有,本面板不离开设置页)。
|
|
205
266
|
* @returns 面板节点树。
|
|
@@ -213,7 +274,10 @@ window.__ModuleLoader__.load({
|
|
|
213
274
|
const [suggestions, setSuggestions] = (0, react.useState)([]);
|
|
214
275
|
const [openMenuIndex, setOpenMenuIndex] = (0, react.useState)(null);
|
|
215
276
|
const [suggestIndex, setSuggestIndex] = (0, react.useState)(null);
|
|
216
|
-
const
|
|
277
|
+
const store = (0, react.useState)(() => ({
|
|
278
|
+
defaults: {},
|
|
279
|
+
revision: 0
|
|
280
|
+
}))[0];
|
|
217
281
|
(0, react.useEffect)(() => {
|
|
218
282
|
let cancelled = false;
|
|
219
283
|
const load = async () => {
|
|
@@ -238,12 +302,13 @@ window.__ModuleLoader__.load({
|
|
|
238
302
|
setPhase("unavailable");
|
|
239
303
|
setFeedback({
|
|
240
304
|
kind: "error",
|
|
241
|
-
text: "Host
|
|
305
|
+
text: "Host 未提供 " + SETTINGS_NS + " 配置项,请确认插件已启用并刷新页面"
|
|
242
306
|
});
|
|
243
307
|
return;
|
|
244
308
|
}
|
|
309
|
+
store.defaults = own.defaults;
|
|
310
|
+
store.revision = own.revision;
|
|
245
311
|
setRows(rowsOf(own.defaults));
|
|
246
|
-
revisionRef.revision = own.revision;
|
|
247
312
|
setWritable(response.value.writable);
|
|
248
313
|
setSuggestions(collectSuggestions(response.value.namespaces));
|
|
249
314
|
setPhase("ready");
|
|
@@ -261,37 +326,64 @@ window.__ModuleLoader__.load({
|
|
|
261
326
|
return () => {
|
|
262
327
|
cancelled = true;
|
|
263
328
|
};
|
|
264
|
-
}, [
|
|
329
|
+
}, [store]);
|
|
265
330
|
const save = async () => {
|
|
266
331
|
const api = currentApi;
|
|
267
332
|
if (api === void 0 || saving) return;
|
|
268
|
-
const
|
|
269
|
-
if ("error" in
|
|
333
|
+
const ops = buildRouteOps(store.defaults, rows);
|
|
334
|
+
if ("error" in ops) {
|
|
270
335
|
setFeedback({
|
|
271
336
|
kind: "error",
|
|
272
|
-
text:
|
|
337
|
+
text: ops.error
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (ops.length === 0) {
|
|
342
|
+
setFeedback({
|
|
343
|
+
kind: "ok",
|
|
344
|
+
text: "没有需要保存的改动"
|
|
273
345
|
});
|
|
274
346
|
return;
|
|
275
347
|
}
|
|
276
348
|
setSaving(true);
|
|
277
349
|
setFeedback(void 0);
|
|
278
350
|
try {
|
|
279
|
-
const response = await api.
|
|
351
|
+
const response = await api.mutate(SETTINGS_NS, ops, store.revision);
|
|
280
352
|
if (!response.ok) {
|
|
353
|
+
if (response.error.code === "settings/conflict") {
|
|
354
|
+
const latest = await api.describe();
|
|
355
|
+
const own = latest.ok ? findOwnNamespace(latest.value.namespaces) : void 0;
|
|
356
|
+
if (own === void 0) {
|
|
357
|
+
setFeedback({
|
|
358
|
+
kind: "error",
|
|
359
|
+
text: "配置已被其它页面或文件修改,且重新读取失败" + (latest.ok ? "(Host 未提供 " + SETTINGS_NS + " 配置项)" : ":" + latest.error.message) + ";请关闭并重新打开设置页后再试。"
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const previous = store.defaults;
|
|
364
|
+
store.defaults = own.defaults;
|
|
365
|
+
store.revision = own.revision;
|
|
366
|
+
setRows((current) => mergeStoredRows(current, previous, own.defaults));
|
|
367
|
+
setOpenMenuIndex(null);
|
|
368
|
+
setSuggestIndex(null);
|
|
369
|
+
setFeedback({
|
|
370
|
+
kind: "error",
|
|
371
|
+
text: "配置已被其它页面或文件修改,已刷新最新版本;请再次保存以覆盖。"
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
281
375
|
setFeedback({
|
|
282
376
|
kind: "error",
|
|
283
377
|
text: response.error.message
|
|
284
378
|
});
|
|
285
379
|
return;
|
|
286
380
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
}
|
|
381
|
+
const stored = defaultsOf(response.value.value);
|
|
382
|
+
store.defaults = stored;
|
|
383
|
+
store.revision = response.value.revision;
|
|
384
|
+
setRows(rowsOf(stored));
|
|
385
|
+
setOpenMenuIndex(null);
|
|
386
|
+
setSuggestIndex(null);
|
|
295
387
|
setFeedback({
|
|
296
388
|
kind: "ok",
|
|
297
389
|
text: "已保存,对新请求即时生效"
|
|
@@ -317,7 +409,7 @@ window.__ModuleLoader__.load({
|
|
|
317
409
|
if (!writable) children.push((0, react.createElement)("p", {
|
|
318
410
|
key: "readonly",
|
|
319
411
|
style: mutedStyle
|
|
320
|
-
}, "
|
|
412
|
+
}, "当前 profile 不接受写入,修改需在 Host 侧完成。"));
|
|
321
413
|
for (let index = 0; index < rows.length; index += 1) {
|
|
322
414
|
const row = rows[index];
|
|
323
415
|
const menuOpen = openMenuIndex === index;
|
|
@@ -399,12 +491,12 @@ window.__ModuleLoader__.load({
|
|
|
399
491
|
onClick: () => {
|
|
400
492
|
setOpenMenuIndex(menuOpen ? null : index);
|
|
401
493
|
}
|
|
402
|
-
}, row.effort, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.
|
|
494
|
+
}, row.effort, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: 14 }))
|
|
403
495
|
}), (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
404
496
|
key: "remove",
|
|
405
497
|
variant: "ghost",
|
|
406
498
|
size: "sm",
|
|
407
|
-
icon: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.
|
|
499
|
+
icon: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular),
|
|
408
500
|
"aria-label": "删除路由 " + row.key,
|
|
409
501
|
title: "删除该路由",
|
|
410
502
|
onClick: () => {
|
|
@@ -420,7 +512,7 @@ window.__ModuleLoader__.load({
|
|
|
420
512
|
}, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
421
513
|
variant: "outline",
|
|
422
514
|
size: "sm",
|
|
423
|
-
icon: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.
|
|
515
|
+
icon: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular),
|
|
424
516
|
onClick: () => setRows([...rows, {
|
|
425
517
|
key: "",
|
|
426
518
|
effort: "medium"
|
|
@@ -428,7 +520,7 @@ window.__ModuleLoader__.load({
|
|
|
428
520
|
}, "添加路由"), (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
429
521
|
variant: "primary",
|
|
430
522
|
size: "sm",
|
|
431
|
-
disabled: saving || !writable
|
|
523
|
+
disabled: saving || !writable,
|
|
432
524
|
onClick: () => {
|
|
433
525
|
save();
|
|
434
526
|
}
|
|
@@ -436,7 +528,7 @@ window.__ModuleLoader__.load({
|
|
|
436
528
|
children.push((0, react.createElement)("p", {
|
|
437
529
|
key: "hint",
|
|
438
530
|
style: hintStyle
|
|
439
|
-
}, "已知等级:" + KNOWN_EFFORTS.join(" / ") + "
|
|
531
|
+
}, "已知等级:" + KNOWN_EFFORTS.join(" / ") + "。路由表写入本插件的 profile 配置项,仅覆盖未显式指定 reasoningEffort 的请求。路由键支持 * 通配,聚焦输入框可查看已配置 Provider/Model 的建议。"));
|
|
440
532
|
if (feedback !== void 0) children.push((0, react.createElement)("p", {
|
|
441
533
|
key: "feedback",
|
|
442
534
|
style: { ...feedback.kind === "error" ? errorStyle : okStyle }
|