dsh-model-reasoning-defaults 0.0.3 → 0.0.5

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
@@ -9,6 +9,11 @@
9
9
 
10
10
  - **仓库**:<https://github.com/xht-code/dsh-model-reasoning-defaults>
11
11
 
12
+ > [!IMPORTANT]
13
+ > 本版本(0.0.5+)面向 **DSH 0.1.7-alpha.2 及以上的设置体系**:设置项由 profile entry 的 `Config` schema 派生,可编辑字段必须声明为 `volatile`,写回落在 profile patch。旧版宿主(0.1.6 及以前)请使用 0.0.3。
14
+ >
15
+ > 插件把 `@deepseek-ai/schemastery` 声明为**运行时依赖**(`~3.18.4`,`.volatile()` 自该版本提供)而非交给 profile 解析的 peer:`dsh plugin` 生成的 profile 使用 `autoInstallPeers: false`,如果 profile 中已存在更低的 schemastery(例如与 `dsh-remote-terminal` 共用的 3.18.2),peer 会被解析成不满足的版本,插件将在加载期直接失败。
16
+
12
17
  ## 安装 / 更新
13
18
 
14
19
  安装插件到指定的 DSH profile:
@@ -34,10 +39,10 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
34
39
  ## 核心特性
35
40
 
36
41
  - **多层级灵活路由**:支持从具体模型、Provider 通配到全局兜底的 6 级优先级匹配机制。
37
- - **设置页可视化编辑**:在 Web 设置页「推理等级默认」中直接增删路由并选择等级,保存即时生效(带修订号并发保护)。
42
+ - **设置页可视化编辑**:在 Web 设置页「推理等级默认」中直接增删路由并选择等级。路由表存放在插件自身的配置项里并声明为 `volatile`,宿主原地更新引用而不重载插件,保存后对新请求立即生效(带修订号并发保护)。
38
43
  - **全入口统一注入**:无缝覆盖 Agent 主循环(`agent/request`)、直接调用(`llm.stream`)以及预准备入口(`prepareCall` / `resolveCallConfig`)。
39
44
  - **对象安全与不可变性保证**:采用非破坏性浅复制机制,安全处理 `Object.freeze` 深冻结请求,原请求对象与未命中的配置始终保持只读与纯净。
40
- - **跨副本的 Loop 边界识别**:不依赖 `@deepseek-ai/dsh-llm` 副本私有的 `WeakSet`,而按公开请求契约识别同时具备 `sessionId` 与顶层冻结状态的 Loop 请求,避免宿主与插件解析到不同模块副本时误复制请求。
45
+ - **宿主标识直读**:loop 请求识别直接使用 DSH 导出的 `isAgentLoopRequest`(基于宿主 `markAgentLoopRequest` 进程标识),不维护自建登记表,跨模块副本与热重载天然一致。
41
46
  - **无缝热重载(Hot-Reload Safe)**:基于 `RuntimePatchState` 管理多 Fiber 状态,配置变更即时生效,仅在最后一个插件 Fiber 卸载时还原原始方法,杜绝方法悬挂。
42
47
 
43
48
  ---
@@ -65,59 +70,32 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
65
70
 
66
71
  ### 方式 1:在 Web 设置页中直接编辑(推荐)
67
72
 
68
- 打开 Web 端 **设置 →「推理等级默认」**,即可增删路由行并从下拉框选择推理等级,保存后对新请求**即时生效**(无需重启或重载)。数据存储在本插件注册的 `model-reasoning-defaults` 设置命名空间(落盘于 `settings.yaml`),经宿主统一 settings API 写入:
73
+ 打开 Web 端 **设置 →「推理等级默认」**,即可增删路由行并从下拉框选择推理等级,保存后对新请求**即时生效**(无需重启或重载)。设置页经宿主统一 settings Remote 把改动写回插件自身的配置项(profile patch 中 id 为 `model-reasoning-defaults` entry):
69
74
 
70
75
  ```yaml
71
- # settings.yaml(设置页保存后的落盘结果)
72
- model-reasoning-defaults:
73
- defaults:
74
- "deepseek-official:*": high
75
- "*": medium
76
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml(设置页保存后的落盘结果)
77
+ - id: model-reasoning-defaults
78
+ name: dsh-model-reasoning-defaults
79
+ config:
80
+ defaults:
81
+ "deepseek-official:*": high
82
+ "*": medium
76
83
  ```
77
84
 
78
85
  > [!NOTE]
86
+ > - 写入按路径寻址(对 `defaults.<路由键>` 设值或删除),因此删除路由会真正落盘,也不会碰到同一 entry 里的结构化字段。
79
87
  > - 路由键支持全部六级写法;等级下拉框为已知集合(off/minimal/low/medium/high/xhigh/max),也接受手工输入其它值。
80
- > - 编辑器带修订号保护:两个标签页同时编辑时,后保存的一方会被拒绝而不是静默覆盖。
88
+ > - 编辑器带修订号保护:两个标签页同时编辑时,后保存的一方会被拒绝并刷新到最新版本,而不是静默覆盖。
81
89
  > - 面板内嵌匹配语义说明(六级优先级次序与生效优先级),输入路由键时可参考已配置的 Provider/Model 建议。
90
+ > - 面板展示的是**解析后**的生效路由表(组合层 + 用户层)。如果某个键是由 **bundle 组合层**(而非 profile 用户层)提供的,宿主会把删除改写成"写回该层原值",删除这类键需要改动提供它的那一层;本插件自带 bundle 不提供 `defaults`,按方式 2 / 方式 3 在用户层声明的键不受影响。
82
91
 
83
- ### 方式 2:直接在 DSH 模型设置中声明(零侵入)
84
-
85
- DSH 设置服务(宿主通常从 `$DSH_HOME/settings.yaml` 装载)中,插件会自动读取 `llm-pi-ai` 与 `llm-deepseek` 命名空间里已声明的思考等级,无需单独配置插件项:
86
-
87
- ```yaml
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`,两者不可混写。
92
+ > [!NOTE]
93
+ > **Provider 级默认值由 DSH 各 Provider 原生处理,本插件不再重复读取。**
94
+ > `llm-pi-ai` 的 `providers.<id>.reasoning` 与 `llm-deepseek` 的 `reasoningEffort`
95
+ > 在请求未显式携带 `reasoningEffort` 时由适配器自行应用,与本插件注入的结果等价;
96
+ > 需要**模型级**或**跨 Provider** 的默认值时,请用下面的方式 2 / 方式 3 写进本插件的路由表。
119
97
 
120
- ### 方式 3:在插件配置中按 Provider/Model 结构声明
98
+ ### 方式 2:在插件配置中按 Provider/Model 结构声明
121
99
 
122
100
  插件的配置通过 loader 补丁体系按 entry id 定位。在用户补丁层(如 `~/.dsh/profiles/<profile>/cordis.patch.yml`)按同一 id 覆盖 `config`:
123
101
 
@@ -142,7 +120,7 @@ llm-deepseek:
142
120
  another-gateway: low
143
121
  ```
144
122
 
145
- ### 方式 4:扁平路由映射字典
123
+ ### 方式 3:扁平路由映射字典
146
124
 
147
125
  直接使用扁平的 `defaults` 路由字典,与结构化形式是同等的配置入口:
148
126
 
@@ -158,13 +136,14 @@ llm-deepseek:
158
136
  ```
159
137
 
160
138
  > [!TIP]
161
- > 各配置形式可以自由组合,归一化到同一张路由表后统一检索。命中由上表的**键特异性**全局裁决,与配置来自哪个文件无关;来源仅在**同一路由键**冲突时按以下优先级覆盖(高 → 低):
139
+ > 各配置形式可以自由组合,归一化到同一张路由表后统一检索。命中由上表的**键特异性**全局裁决(例如精确键 `my-gateway:deepseek-reasoner` 胜过 Provider 通配键 `my-gateway:*`),与配置写在哪个文件无关。整条链路对同一个请求的生效优先级(高 → 低):
162
140
  >
163
- > 1. 插件 cordis 配置(patch 层 `defaults` 显式路由键 > 结构化 `providers`/`models` 项)
164
- > 2. 设置页 UI(`model-reasoning-defaults` 命名空间,方式 1)
165
- > 3. 设置服务自动提取项(`llm-pi-ai` / `llm-deepseek` 原生字段,方式 2)
141
+ > 1. 请求自带的 `reasoningEffort`(插件绝不覆盖)
142
+ > 2. 本插件路由表:`defaults` 显式路由键(设置页写入或 patch 层声明)> 结构化 `providers`/`models` 项
143
+ > 3. Provider 原生默认值(`llm-pi-ai` `reasoning`、`llm-deepseek` `reasoningEffort`)
144
+ > 4. Provider 内建兜底(如 `llm-deepseek` 的 `high`/`thinking: disabled` 时的 `off`)
166
145
  >
167
- > 例如设置服务给出精确键 `my-gateway:deepseek-reasoner: high`、插件仅配通配键 `providers.my-gateway = medium` 时,精确键胜出(得 `high`)。
146
+ > 因此插件路由表可以精确覆盖某个模型,而不必改动 Provider 自身的默认值。
168
147
 
169
148
  ---
170
149
 
@@ -180,7 +159,7 @@ llm-deepseek:
180
159
  【Agent Loop 请求】 【手写 / 辅助请求】
181
160
  (agent/request Waterfall) (llm.stream / prepareCall)
182
161
  │ │
183
- 注入默认 reasoningEffort sessionId + 冻结边界判定
162
+ 注入默认 reasoningEffort isAgentLoopRequest 判定
184
163
  到调用配置种子(loop 随后据此构建完整 │
185
164
  请求并深冻结、打宿主进程标识) ┌─────────────┴─────────────┐
186
165
  │ ▼ ▼
@@ -197,27 +176,29 @@ llm-deepseek:
197
176
 
198
177
  ### 1. Agent 主循环请求处理
199
178
  - 注册全局 `agent/request` waterfall 监听器。此阶段操作的是调用配置种子,完整请求对象尚未构建;插件按路由计算默认等级并注入 `reasoningEffort`。
200
- - loop 随后将该配置连同派生消息组装为完整请求,写入 `sessionId`、深冻结并打上宿主 `markAgentLoopRequest` 进程标识。
201
- - 请求进入 `llm.stream` 入口时,入口包装按公开的 `sessionId + Object.freeze` 契约识别 loop 请求,**直接按原引用透传**。不读取模块副本私有的 `isAgentLoopRequest` 标识,避免宿主与插件加载不同 `dsh-llm` 副本时误复制请求。
179
+ - loop 随后将该配置连同派生消息组装为完整请求,深冻结并打上宿主 `markAgentLoopRequest` 进程标识。
180
+ - 请求进入 `llm.stream` 入口时,入口包装用宿主导出的 `isAgentLoopRequest` 识别 loop 请求,**直接按原引用透传**——浅复制会丢失该进程标识,导致 compaction 与请求观察者把会话请求误判为手写一次性请求。
202
181
 
203
182
  ### 2. 手写及辅助调用处理
204
183
  - 直接调用 `llm.stream(options)` 时,入口包装在 waterfall **开始前**执行拦截。
205
- - 对不带该 Loop 边界契约的请求(包括带 `purpose: 'compaction'` 的辅助请求),通过浅复制补齐 `reasoningEffort` 后再进入下游链路。
184
+ - 对不带 loop 标识的请求(包括带 `purpose: 'compaction'` 的辅助请求),通过浅复制补齐 `reasoningEffort` 后再进入下游链路。
206
185
 
207
186
  ### 3. 配置准备与解析入口
208
187
  - 同步拦截 `llm.prepareCall` 与 `llm.resolveCallConfig`,在预计算阶段统一应用相同的路由查找规则,并完整透传 `AbortSignal`。
209
188
 
210
189
  ### 4. 共享状态与热重载安全
211
- - LLM 方法包装层内部维护 `RuntimePatchState`,通过 `defaults: Map<symbol, ReasoningDefaults>` 跟踪每个活跃 Fiber 的配置版本。
212
- - 插件重新加载时,新 Fiber 创建的配置层会立即生效并覆盖旧配置;旧 Fiber 卸载时仅移除自身 token,只有当所有 Fiber 都卸载时才彻底还原原始 LLM 方法。
190
+ - LLM 方法包装层内部维护 `RuntimePatchState`,通过 `sources: Map<symbol, RouteTableSource>` 跟踪每个活跃 Fiber 的路由表来源(结构化字段快照 + `defaults` 的 volatile 引用)。
191
+ - 插件重新加载时,新 Fiber 的路由表来源会立即生效并覆盖旧来源;旧 Fiber 卸载时仅移除自身 token,只有当所有 Fiber 都卸载时才彻底还原原始 LLM 方法。
192
+ - 设置页保存只改动 `defaults`(volatile 字段),宿主原地更新其引用而不重载插件;插件在每次请求时读取当下快照,所以空路由表也照常安装包装层——否则运行期新增的路由要等宿主重启才会生效。
213
193
 
214
194
  ---
215
195
 
216
196
  ## 客户端支持
217
197
 
218
198
  本插件包含 Web 客户端扩展(`src/client/index.ts`):
219
- - 注册到 DSH 设置页槽位 `settings.section`(「推理等级默认」面板):增删路由行、从已知等级集合下拉选择,保存写入本插件的 `model-reasoning-defaults` 设置命名空间。
220
- - 数据经宿主统一 settings Remote(`remote.settings.describe` / `replace`)实时读取渲染;保存携带 `expectedRevision` 修订号,并发编辑时由宿主裁决而非静默覆盖。保存走 `replace`(整段替换)而非 `update`(merge patch):merge 语义下省略键不表达删除,删除的路由会被存储层原样保留。
199
+ - 注册到 DSH 设置页槽位 `settings.section`(「推理等级默认」面板):增删路由行、从已知等级集合下拉选择,保存写入本插件配置项(entry id `model-reasoning-defaults`)的 `defaults` 字段。
200
+ - 数据经宿主统一 settings Remote(`remote.settings.describe` / `mutate`)读取渲染;保存携带 `expectedRevision` 修订号,并发编辑时由宿主裁决而非静默覆盖。写入使用**路径寻址操作**(对 `defaults.<路由键>` 设值或删除):merge 语义下省略键不表达删除,只有 `unset` 才能把删除落盘,同时不触碰同一 entry 里的结构化字段。
201
+ - 保存撞上修订号冲突时保留本地编辑并刷新到宿主最新版本,提示用户再次保存覆盖。
221
202
  - 路由键输入带建议弹层(来自 `llm-pi-ai` / `llm-deepseek` 已配置的 Provider/Model),面板内展示匹配语义与生效优先级说明。
222
203
 
223
204
  ---
@@ -271,7 +252,7 @@ dsh plugin --profile web remove dsh-model-reasoning-defaults
271
252
  ## 注意事项与设计说明
272
253
 
273
254
  1. **推理等级有效性校验时机**:
274
- 配置阶段仅校验字符串非空,具体推理等级(如 `low` / `medium` / `high`)是否被目标模型支持,由 DSH 底层适配器在实际发起请求时校验(未支持将抛出 `UNSUPPORTED_REASONING_EFFORT`)。插件会在激活时与设置提取过程中对照已知等级集合(`off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`)提示可疑取值,但不阻断注入,以便上游新增等级时无需同步升级本插件。
255
+ 配置阶段仅校验字符串非空,具体推理等级(如 `low` / `medium` / `high`)是否被目标模型支持,由 DSH 底层适配器在实际发起请求时校验(未支持将抛出 `UNSUPPORTED_REASONING_EFFORT`)。插件会在激活时、以及读取运行期写入的路由表时对照已知等级集合(`off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`)提示可疑取值(同一问题只告警一次),但不阻断注入,以便上游新增等级时无需同步升级本插件。
275
256
  2. **会话持久化影响**:
276
257
  由插件注入的默认值经 `llm.prepareCall` 后将作为显式参数写入请求头。在长会话进行中若热更新修改了配置规则,已创建的历史请求头不会被回溯修改。
277
258
  3. **发布与打包约束**:
package/cordis.patch.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  # bundle patch 层:向 profile 插入本插件入口行。
2
- # 启用时在用户层按同一 entry id 覆盖整个 config(include 补丁体系按 id 定位),
3
- # config 支持结构化 providers/models、扁平 defaults 字典或两者混用:
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
- * settings API 写入。Host 每次请求实时读取该层,保存即生效。
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
- /** 本插件的设置命名空间,与 Host 侧 UI_DEFAULTS_NAMESPACE 一致。 */
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: section !== null && typeof section === "object" && section.defaults !== void 0 && typeof section.defaults === "object" ? { ...section.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
- * 保存时带 expectedRevision 做 update,陈旧编辑会被宿主拒绝而不是静默覆盖。
201
- * 外部改动(另一标签页、手工编辑 yaml)在本组件重新挂载时可见——设置壳每次
202
- * 打开都重新挂载 section,无需订阅失效事件。
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 revisionRef = (0, react.useState)(() => ({ revision: 0 }))[0];
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 未注册 " + SETTINGS_NS + " 命名空间,请确认插件已启用并刷新页面"
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
- }, [revisionRef]);
329
+ }, [store]);
265
330
  const save = async () => {
266
331
  const api = currentApi;
267
332
  if (api === void 0 || saving) return;
268
- const collapsed = collapseRows(rows);
269
- if ("error" in collapsed) {
333
+ const ops = buildRouteOps(store.defaults, rows);
334
+ if ("error" in ops) {
270
335
  setFeedback({
271
336
  kind: "error",
272
- text: collapsed.error
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.replace(SETTINGS_NS, { defaults: collapsed }, revisionRef.revision);
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 reread = await api.describe();
288
- if (reread.ok) {
289
- const own = findOwnNamespace(reread.value.namespaces);
290
- if (own !== void 0) {
291
- setRows(rowsOf(own.defaults));
292
- revisionRef.revision = own.revision;
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
- }, "当前设置文档为只读,修改需在 Host 侧完成。"));
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.IconChevronDownOutline14))
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.IconTrashOutline16),
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.IconPlusOutline16),
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 || rows.length === 0,
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(" / ") + "。生效优先级:patch 层插件配置 > 此处 > Host 各 Provider 已声明的思考等级。路由键支持 * 通配,聚焦输入框可查看已配置 Provider/Model 的建议。"));
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 }