@leaf233/dsh-llm-rate-limiter 0.2.1 → 0.3.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/CHANGELOG.md +189 -1
- package/README.md +48 -21
- package/README.zh.md +284 -0
- package/lib/client.js +182 -140
- package/lib/index.js +88 -44
- package/lib/types/config.js +48 -16
- package/lib/types/index.js +1 -1
- package/package.json +12 -12
- package/COMPATIBILITY.md +0 -221
package/lib/types/config.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rate limiter configuration schema.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
4
|
+
* The plugin's Config is the profile entry's own schema: Cordis validates the
|
|
5
|
+
* entry's `config` against it, so the host reads values from the references
|
|
6
|
+
* this schema produces rather than from a settings namespace.
|
|
7
|
+
*
|
|
8
|
+
* Fields marked `.volatile()` become separately addressable live references
|
|
9
|
+
* (`config.enabled.get()`). Only volatile fields appear in the settings form
|
|
10
|
+
* (`dsh-settings`'s `volatileForm()`), and an entry whose Config has NO
|
|
11
|
+
* volatile field is dropped from the form list entirely — so the set of
|
|
12
|
+
* `.volatile()` marks below IS the settings UI's field list.
|
|
6
13
|
*
|
|
7
14
|
* @module dsh-llm-rate-limiter/types/config
|
|
8
15
|
*/
|
|
@@ -12,6 +19,12 @@ import Schema from "@deepseek-ai/schemastery";
|
|
|
12
19
|
/**
|
|
13
20
|
* Per-model overrides — key is `"provider/model"` (e.g. `"deepseek/deepseek-chat"`).
|
|
14
21
|
* Every field is optional; omitted fields inherit from `defaults`.
|
|
22
|
+
*
|
|
23
|
+
* This sub-schema deliberately carries NO `.volatile()`: it is the `inner`
|
|
24
|
+
* of a `dict`, and two volatile layers on one path are rejected
|
|
25
|
+
* (`volatile fields require a fixed object path without an enclosing volatile
|
|
26
|
+
* field`, `schemastery/lib/index.mjs:246`). The whole `models` dict is made
|
|
27
|
+
* volatile instead — see below.
|
|
15
28
|
*/
|
|
16
29
|
const ModelOverride = Schema.object({
|
|
17
30
|
maxConcurrent: Schema.number().step(1).min(1).description("Max concurrent requests for this model"),
|
|
@@ -21,33 +34,52 @@ const ModelOverride = Schema.object({
|
|
|
21
34
|
enabled: Schema.boolean().description("Enable/disable rate limiting for this model"),
|
|
22
35
|
});
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Per-model defaults — applied to every model unless overridden.
|
|
39
|
+
* The object is volatile AS A WHOLE so `config.defaults.get()` yields one
|
|
40
|
+
* plain object and the settings form edits `["defaults", field]` paths.
|
|
41
|
+
*/
|
|
42
|
+
const Defaults = Schema.object({
|
|
43
|
+
maxConcurrent: Schema.number().step(1).min(1).default(5).description("Max concurrent requests"),
|
|
44
|
+
maxRpm: Schema.number().step(1).min(1).default(60).description("Max requests per minute"),
|
|
45
|
+
burstSize: Schema.number().step(1).min(1).default(10).description("Burst capacity (token-bucket)"),
|
|
46
|
+
refillRate: Schema.number().min(0.1).default(1).description("Tokens per second (token-bucket)"),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const Config = Schema.object({
|
|
25
50
|
/** Master switch */
|
|
26
|
-
enabled: Schema.boolean().default(true).description("Enable the rate limiter globally"),
|
|
51
|
+
enabled: Schema.boolean().default(true).volatile().description("Enable the rate limiter globally"),
|
|
27
52
|
|
|
28
53
|
/** Default strategy */
|
|
29
54
|
strategy: Schema.union([
|
|
30
55
|
Schema.const("token-bucket").description("Token Bucket — allows bursts up to burstSize"),
|
|
31
56
|
Schema.const("sliding-window").description("Sliding Window — smooth, fixed requests-per-minute"),
|
|
32
|
-
]).default("token-bucket").description("Rate limiting algorithm"),
|
|
57
|
+
]).default("token-bucket").volatile().description("Rate limiting algorithm"),
|
|
33
58
|
|
|
34
59
|
/** Global defaults — applied to every model unless overridden */
|
|
35
|
-
defaults:
|
|
36
|
-
maxConcurrent: Schema.number().step(1).min(1).default(5).description("Max concurrent requests"),
|
|
37
|
-
maxRpm: Schema.number().step(1).min(1).default(60).description("Max requests per minute"),
|
|
38
|
-
burstSize: Schema.number().step(1).min(1).default(10).description("Burst capacity (token-bucket)"),
|
|
39
|
-
refillRate: Schema.number().min(0.1).default(1).description("Tokens per second (token-bucket)"),
|
|
40
|
-
}).default({}).description("Default limits applied to all models"),
|
|
60
|
+
defaults: Defaults.default({}).volatile().description("Default limits applied to all models"),
|
|
41
61
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Per-model overrides — key is "provider/model".
|
|
64
|
+
*
|
|
65
|
+
* WHOLE-BLOCK volatile: `dict(inner).volatile()`, never
|
|
66
|
+
* `dict(inner.volatile())`. The latter marks the dict's `inner` while the
|
|
67
|
+
* `sKey`/`inner` nodes are "blocked" (`schemastery/lib/index.mjs:249-250`),
|
|
68
|
+
* which throws at validation time.
|
|
69
|
+
*
|
|
70
|
+
* Because `isVolatilePath` (`dsh-settings/lib/index.js:153-158`) returns
|
|
71
|
+
* true on reaching ANY volatile node without inspecting its descendants,
|
|
72
|
+
* paths such as `["models", "provider/model", "maxRpm"]` remain writable
|
|
73
|
+
* through the settings form under this whole-block mark.
|
|
74
|
+
*/
|
|
75
|
+
models: Schema.dict(ModelOverride).default({}).volatile().description("Per-model rate limit overrides"),
|
|
44
76
|
|
|
45
77
|
/** Queue behaviour when a request is throttled */
|
|
46
78
|
onThrottled: Schema.union([
|
|
47
79
|
Schema.const("queue").description("Wait in queue until a slot opens"),
|
|
48
80
|
Schema.const("reject").description("Immediately return an error"),
|
|
49
|
-
]).default("queue").description("What happens when a request exceeds the limit"),
|
|
81
|
+
]).default("queue").volatile().description("What happens when a request exceeds the limit"),
|
|
50
82
|
|
|
51
83
|
/** Maximum time a request waits in queue before being rejected */
|
|
52
|
-
maxQueueWaitMs: Schema.number().step(1).min(1000).default(60_000).description("Max queue wait (ms)"),
|
|
53
|
-
}).description("LLM call rate limiter settings");
|
|
84
|
+
maxQueueWaitMs: Schema.number().step(1).min(1000).default(60_000).volatile().description("Max queue wait (ms)"),
|
|
85
|
+
}).description("LLM call rate limiter settings");
|
package/lib/types/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { Config } from "./config.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leaf233/dsh-llm-rate-limiter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Per-model LLM call rate limiter for DeepSeek Harness with queue support and a live status panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -15,20 +15,16 @@
|
|
|
15
15
|
"client": {
|
|
16
16
|
"inject": [
|
|
17
17
|
"@deepseek-ai/dsh-client-connection",
|
|
18
|
-
"@deepseek-ai/dsh-client-
|
|
18
|
+
"@deepseek-ai/dsh-client-locale",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
19
21
|
],
|
|
20
22
|
"platform": "web"
|
|
21
|
-
},
|
|
22
|
-
"compatibility": {
|
|
23
|
-
"dshReleases": {
|
|
24
|
-
"0.1.2-rc.1": "compatible",
|
|
25
|
-
"0.1.5-rc.3": "compatible"
|
|
26
|
-
}
|
|
27
23
|
}
|
|
28
24
|
},
|
|
29
25
|
"scripts": {
|
|
30
26
|
"build": "echo 'no build needed'",
|
|
31
|
-
"test": "node test-strategies.mjs && node test-status-rpc.mjs && node test-client-bundle.mjs && node test-host-integration.mjs && node test-status-route.mjs",
|
|
27
|
+
"test": "node test-strategies.mjs && node test-status-rpc.mjs && node test-client-bundle.mjs && node test-host-integration.mjs && node test-status-route.mjs && node test-config-schema.mjs",
|
|
32
28
|
"verify:live": "node tools/verify-live-route.mjs"
|
|
33
29
|
},
|
|
34
30
|
"repository": {
|
|
@@ -51,18 +47,22 @@
|
|
|
51
47
|
"license": "MIT",
|
|
52
48
|
"peerDependencies": {
|
|
53
49
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
54
|
-
"@deepseek-ai/
|
|
50
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1",
|
|
51
|
+
"@deepseek-ai/schemastery": ">=3.18.4"
|
|
55
52
|
},
|
|
56
53
|
"dependencies": {
|
|
57
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
54
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"lib/",
|
|
61
61
|
"cordis.patch.yml",
|
|
62
62
|
"package.json",
|
|
63
63
|
"README.md",
|
|
64
|
+
"README.zh.md",
|
|
64
65
|
"CHANGELOG.md",
|
|
65
|
-
"COMPATIBILITY.md",
|
|
66
66
|
"LICENSE"
|
|
67
67
|
]
|
|
68
68
|
}
|
package/COMPATIBILITY.md
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# dsh-llm-rate-limiter — DSH 版本兼容性检测报告
|
|
2
|
-
|
|
3
|
-
> 初检日期: 2026-07-28 · 复检日期: **2026-09-25**
|
|
4
|
-
> 已验证 DSH 版本: **0.1.2-rc.1**、**0.1.5-rc.3**
|
|
5
|
-
> 测试方式: 源码分析 + 可执行回归测试(`test-status-route.mjs`、`tools/verify-live-route.mjs` 走真实 socket)
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 〇、0.1.5-rc.3 回归与修复(2026-09-25 复检)
|
|
10
|
-
|
|
11
|
-
### 现象
|
|
12
|
-
升级到 DSH **0.1.5-rc.3** 后,状态面板永久停在「○ 重试中」,实时数据不再刷新;限速本身与设置页配置完全正常。
|
|
13
|
-
|
|
14
|
-
### 根因(已用真实包 + 真实 Cordis 复现,非推断)
|
|
15
|
-
DSH 0.1.5-rc.3 把 **`@deepseek-ai/dsh-client-connection` 这个框架插件自身**的 `inject` 从
|
|
16
|
-
`["webServer", "credentials"]` 改成了 `["credentials"]`(lib/index.js:736),
|
|
17
|
-
但其 `HostConnectionService.register()` 仍然直接解引用 `owner.webServer`:
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
// dsh-client-connection/lib/index.js:618
|
|
21
|
-
return owner.effect(() => owner.webServer.register(route), `... rpc channel`);
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
而 `connection.rpc` 的 getter 取 `const owner = this.ctx`,Cordis 的 `createTraceable`
|
|
25
|
-
会把跨 fiber 读取的 service 的 `ctx` 重绑定到**读取者**的 fiber
|
|
26
|
-
(实测 `owner.fiber === consumer.fiber` 为 true)。因此 `owner.webServer` 的解析从读取者的
|
|
27
|
-
inject 出发 —— 读取者只 inject 了 `["connection"]`,于是抛:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
cannot get property "webServer" without inject
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
该异常发生在插件 `ctx.inject(["connection"], ...)` 回调内,被插件自己的 try/catch 吞掉,
|
|
34
|
-
只留一条 `logger.warn`,于是通道**静默未注册**。
|
|
35
|
-
|
|
36
|
-
### 线上证据
|
|
37
|
-
| 探测 | 修复前 | 含义 |
|
|
38
|
-
|---|---|---|
|
|
39
|
-
| `GET /api/xxx` | 401 | `/api` 路由存在,仅认证被拒 |
|
|
40
|
-
| `GET /llm-rate-limiter/snapshot` | **404** | 该前缀**无路由**(走 SPA fallback) |
|
|
41
|
-
| `POST /llm-rate-limiter/snapshot` | **405** | 同上 |
|
|
42
|
-
|
|
43
|
-
而 `dsh --profile web --dump-config` 中插件条目仍存在 → 插件确实被加载,只是通道注册失败。
|
|
44
|
-
|
|
45
|
-
### 交叉验证(隔离变量)
|
|
46
|
-
| 实验 | 结果 |
|
|
47
|
-
|---|---|
|
|
48
|
-
| 0.1.2 代码 + fiber `[webServer,credentials]` | ✅ 注册成功 |
|
|
49
|
-
| 0.1.2 代码 + fiber `[credentials]` | ❌ 失败 |
|
|
50
|
-
| 0.1.5 代码 + fiber `[webServer,credentials]` | ✅ 注册成功 |
|
|
51
|
-
| 0.1.5 代码 + fiber `[credentials]`(真实形态) | ❌ 失败 |
|
|
52
|
-
|
|
53
|
-
→ 差异**只由 inject 列表决定**,与两版 `register` 代码差异无关。另实测:仅给**消费者**外层 fiber 加
|
|
54
|
-
`webServer`(或嵌套 `ctx.inject(["connection","webServer"])`)**均无效** —— 因为 `owner` 绑定的是
|
|
55
|
-
connection service 自身那条 fiber。
|
|
56
|
-
|
|
57
|
-
### 修复(v0.2.1,方案 E)
|
|
58
|
-
保留路径 1 为首选,新增路径 2 兜底,两者**同一套 wire 协议**,浏览器端零改动:
|
|
59
|
-
|
|
60
|
-
| 路径 | 触发条件 | 做法 |
|
|
61
|
-
|---|---|---|
|
|
62
|
-
| 1(首选) | `connection.rpc.handle()` 正常 | 框架持有路由、请求校验与 fiber 级撤销 |
|
|
63
|
-
| 2(兜底) | 路径 1 抛错 | 插件在**自己的 fiber**(能看到 `webServer`)注册 `kind:"prefix"` 路由,并复用 `connection.requestRejection()` 保留 403/401 栅栏 |
|
|
64
|
-
|
|
65
|
-
- 若 `connection.requestRejection()` 缺失或抛错,插件**拒绝挂载**,绝不发布未鉴权路由
|
|
66
|
-
- 上游修复该 inject 不一致后,路径 1 会自动重新生效,无需再改插件
|
|
67
|
-
|
|
68
|
-
### 回归测试
|
|
69
|
-
- `test-status-route.mjs`(73 断言)四层:路由语法、请求形态判定、**模拟 0.1.5 回归**(同时验证「兜底生效」与「路径 1 可用时优先」)、真实 0.1.5 包(无 DSH 时自动跳过)
|
|
70
|
-
- `tools/verify-live-route.mjs`(19 断言):真实 `dsh-host-webserver` 真 socket + 真实 `dsh-client-connection`,发真实 HTTP 请求;无 DSH 时退出码 2(跳过)
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 一、依赖 API 清单与存在性验证
|
|
75
|
-
|
|
76
|
-
### Host 端(lib/index.js)— 6 个 API 依赖
|
|
77
|
-
|
|
78
|
-
| # | API | 所属包 | 本地存在 | 稳定性评估 |
|
|
79
|
-
|---|-----|--------|----------|------------|
|
|
80
|
-
| 1 | `ctx.settings.register(ns, schema, { base })` | dsh-settings | ✅ 3处 | 核心 API,SettingsService 的公共接口 |
|
|
81
|
-
| 2 | `scope.get()` | dsh-settings (返回值) | ✅ `registration.resolved` | 简单属性读取,极低风险 |
|
|
82
|
-
| 3 | `scope.watch(callback)` → `unwatch()` | dsh-settings (返回值) | ✅ watcher Set 管理 | 标准 observer 模式,极低风险 |
|
|
83
|
-
| 4 | `ctx.on("llm/stream", async function*(options, next))` | dsh-llm + cordis | ✅ 3处 | waterfall 中间件,LLM 调用的核心拦截点 |
|
|
84
|
-
| 5 | `ctx.effect(() => cleanup, label)` | cordis | ✅ | Cordis 核心生命周期 API |
|
|
85
|
-
| 6 | `ctx.logger?.("rate-limiter").info/warn(...)` | cordis | ✅ | 日志 API,如不存在用 `?.` 降级 |
|
|
86
|
-
|
|
87
|
-
### Client 端(lib/client.js)— 5 个 API 依赖
|
|
88
|
-
|
|
89
|
-
| # | API | 所属包 | 本地存在 | 稳定性评估 |
|
|
90
|
-
|---|-----|--------|----------|------------|
|
|
91
|
-
| 1 | `window.__ModuleLoader__.load({ id, factory })` | DSH web shell | ✅ 1处 | 客户端模块加载器,所有 client 插件都用 |
|
|
92
|
-
| 2 | `settingsScope.get?.()` / `getSnapshot?.()` | dsh-client-ui-settings | ✅ 各1处 | get() 来自 host scope, getSnapshot 来自 client controller; 双兼容 |
|
|
93
|
-
| 3 | `settingsScope.subscribe(listener)` | dsh-client-ui-settings | ✅ 1处 | SettingsScopeController.subscribe |
|
|
94
|
-
| 4 | `settingsScope.set(field, value)` | dsh-client-ui-settings | ✅ 2处 | SettingsScopeController.set (单字段写) |
|
|
95
|
-
| 5 | `settingsScope.mutate([{ op, path, value }])` | dsh-client-ui-settings | ✅ 1处 | SettingsScopeController.mutate (嵌套操作) |
|
|
96
|
-
| 6 | `ctx.slots.inject("settings.plugin.item", ...)` | dsh-client-ui-settings-plugins | ✅ 6处 | 插件设置卡片注册 slot |
|
|
97
|
-
|
|
98
|
-
### v0.2.0 新增依赖(状态通道)
|
|
99
|
-
|
|
100
|
-
| # | API | 所属包 | 本地存在 | 稳定性评估 |
|
|
101
|
-
|---|-----|--------|----------|------------|
|
|
102
|
-
| 1 | `ctx.inject(["connection"], cb)` + `conn.rpc.handle(channel, handler)` | dsh-client-connection | ⚠️ 0.1.2 可用 / **0.1.5 抛错** | 首选路径。0.1.5-rc.3 起因该包自身 `inject` 缺 `webServer` 而抛 `cannot get property "webServer" without inject`,故 v0.2.1 增加路径 2 兜底 |
|
|
103
|
-
| 1b | `conn.requestRejection(req)` + `ctx.inject(["webServer"], cb)` + `webServer.register({kind:"prefix",...})` | dsh-client-connection + dsh-host-webserver | ✅ 0.1.2 / 0.1.5 | **v0.2.1 兜底路径**。复用框架的 403/401 栅栏,自行注册前缀路由;wire 协议与路径 1 逐字一致 |
|
|
104
|
-
| 2 | handler 签名 `(endpoint, payload, signal) => envelope` | dsh-client-connection | ✅ `rpcFetchHandler`(605-631) | 框架强制 POST + `application/json`(否则 404/415),并自动套 `requestRejection`(Host/Origin 不信→403,未认证→401) |
|
|
105
|
-
| 3 | envelope `{ok:true,value}` / `{ok:false,error:{code,message,details}}` | dsh-client-connection | ✅ 与 `clientRequestSchema` / `errorResponse` 一致 | 契约字面量,dsh-context 逐字复制同一范式 |
|
|
106
|
-
| 4 | 客户端 `ctx.get("connection")?.rpc.call(channel, endpoint, payload, signal)` | dsh-client-connection | ✅ `createWebConnectionRpc.call`(client.js 4606-4628) | 传输失败 reject;返回 `result`(即 envelope)。经 `rpcCallOf` 反射读取,服务缺席/敌意时降级为 `undefined` |
|
|
107
|
-
| 5 | `dsh.client.inject` 声明 `@deepseek-ai/dsh-client-connection` | DSH web shell | ✅ | bundle 依赖声明,确保连接服务先于本插件可用;**不放进模块级 `inject`**,以便服务缺席时面板降级而非整卡不加载 |
|
|
108
|
-
| 6 | `dsh.compatibility.dshReleases` 声明 | DSH 打包约定 | ✅ 对齐 `dsh-context` | 成熟插件物料约定 |
|
|
109
|
-
|
|
110
|
-
> 证据:dsh-context v0.50.0 使用同一组 API(`watchDetailChannel`,lib/index.js 1943-2012),其 compatibility 矩阵覆盖 DSH `0.1.2-rc.1` → `0.1.5-rc.1`,说明 `connection.rpc` 是**持久公开契约**而非临时接口。
|
|
111
|
-
|
|
112
|
-
### Schema 依赖 — 1 个包
|
|
113
|
-
|
|
114
|
-
| 包 | 本地版本 | 用到的 API |
|
|
115
|
-
|----|---------|-----------|
|
|
116
|
-
| @deepseek-ai/schemastery | 3.18.2+ | `Schema.object()`, `Schema.union()`, `Schema.const()`, `Schema.dict()`, `.default()`, `.int()`, `.min()`, `.description()` |
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 二、版本矩阵评估
|
|
121
|
-
|
|
122
|
-
### 已知版本
|
|
123
|
-
|
|
124
|
-
| 组件 | 本地安装版本 | peerDep 要求 | DSH 用法 |
|
|
125
|
-
|------|------------|-------------|---------|
|
|
126
|
-
| @deepseek-ai/dsh | **0.1.2-rc.1** | — | 安装的 DSH 主包 |
|
|
127
|
-
| @deepseek-ai/cordis | **4.0.2** | ≥4.0.2 | DSH 所有插件统一用 ^4.0.2 |
|
|
128
|
-
| @deepseek-ai/schemastery | 3.18.2+ | ≥3.18.0 | DSH 用 ^3.18.2 |
|
|
129
|
-
| @deepseek-ai/dsh-settings | **0.1.2-rc.1** | — | 提供 `ctx.settings.register()` |
|
|
130
|
-
| @deepseek-ai/dsh-llm | **0.1.2-rc.1** | — | 提供 `llm/stream` waterfall |
|
|
131
|
-
| @deepseek-ai/dsh-llm-retry | **0.1.2-rc.1** | — | 本插件的参考实现 |
|
|
132
|
-
| @deepseek-ai/dsh-client-ui-settings | **0.1.2-rc.1** | — | 提供 `settingsScope` 服务 |
|
|
133
|
-
| @deepseek-ai/dsh-client-ui-settings-plugins | **0.1.2-rc.1** | — | 提供 `settings.plugin.item` slot |
|
|
134
|
-
|
|
135
|
-
### 兼容性矩阵(推断)
|
|
136
|
-
|
|
137
|
-
| DSH 版本 | 预期兼容 | 风险点 |
|
|
138
|
-
|----------|---------|--------|
|
|
139
|
-
| 0.1.0 ~ 0.1.2 | ✅ 应兼容 | RC 阶段 API 趋于稳定,settings.register / llm/stream 已是核心 |
|
|
140
|
-
| 0.1.3+ (同 minor) | ✅ 应兼容 | 遵循 semver,接口不大改 |
|
|
141
|
-
| 0.2.x (minor 升级) | ⚠️ 需验证 | 可能新增/重命名 settings 参数、llm/stream 签名变体 |
|
|
142
|
-
| 1.0+ (major) | ⚠️ 必须重测 | 核心 API 可能重构(cordis 升级、settings 层重构) |
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## 三、关键风险点分析
|
|
147
|
-
|
|
148
|
-
### 🔴 高风险
|
|
149
|
-
|
|
150
|
-
| 风险 | 说明 | 影响 | 缓解措施 |
|
|
151
|
-
|------|------|------|----------|
|
|
152
|
-
| **`llm/stream` waterfall 签名变更** | DSH 当前签名: `(options: GenerateOptions, next) => AsyncIterable`,如果未来增加参数或改变 options 结构 | 插件读不到 `provider`/`model` 字段 | 用可选链 `options.provider ?? "unknown"` 降级 |
|
|
153
|
-
| **cordis major 升级** | cordis 是 DSH 的运行时内核,major 版本会改变插件生命周期 | apply/signature/effect 全部受影响 | peerDep 已锁定 `≥4.0.2`,major 升级时必须更新 |
|
|
154
|
-
| **0.1.x 是 RC 阶段** | 预发布版本的 API 不保证向后兼容 | 任何 patch 版本都可能引入 breaking change | 紧跟 DSH 版本更新 |
|
|
155
|
-
|
|
156
|
-
### 🟡 中等风险
|
|
157
|
-
|
|
158
|
-
| 风险 | 说明 | 影响 | 缓解措施 |
|
|
159
|
-
|------|------|------|----------|
|
|
160
|
-
| **`settingsScope` API 名字变更** | Service 字符串 `"settingsScope"` 硬编码在 `SettingsScopeBinder` 构造函数中 | Client 端服务注入失败 | 该 Service 名是 UI 基础设施,改名成本极高,短期低概率 |
|
|
161
|
-
| **`settings.plugin.item` slot 名变更** | 插件设置页的 slot 注册点 | 卡片不会显示 | slot 名已被多处硬编码引用(Bash、AgentLoop 等),改名需全量迁移 |
|
|
162
|
-
| **schemastery 3.x → 4.x** | 新 major 可能改 `Schema.union/const` 等 API | 配置 Schema 编译失败 | peerDep 锁 ≥3.18.0,major 时必须适配 |
|
|
163
|
-
|
|
164
|
-
### 🟢 低风险
|
|
165
|
-
|
|
166
|
-
| 风险 | 说明 |
|
|
167
|
-
|------|------|
|
|
168
|
-
| `ctx.on("llm/stream")` 事件名变更 | LLM waterfall 是 LLM 模块的核心公开接口 |
|
|
169
|
-
| `scope.get()` / `scope.watch()` 签名变更 | 简单 getter/observer,改动概率极低 |
|
|
170
|
-
| `settingsScope.set(field, value)` 签名变更 | 标准 setter,多个 UI 卡片都在用 |
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## 四、已验证的兼容性事实
|
|
175
|
-
|
|
176
|
-
### API 表面稳定性证据
|
|
177
|
-
|
|
178
|
-
1. **`llm/stream` waterfall 被 2 处引用**:`dsh-llm/lib/index.js` 的 `stream()` 和 `invariant.js` 的验证层,形成双重稳定约束
|
|
179
|
-
2. **`settings.register` 返回的 scope**:host 侧返回 `{ get, watch, update, replace }`,结构明确且简洁
|
|
180
|
-
3. **`settingsScope.bind`** 由 `dsh-client-ui-settings` 提供,client 端 scope 返回 `{ set, unset, mutate, subscribe, getSnapshot }`,被 `dsh-client-ui-settings-plugins`、`dsh-client-ui-settings-models` 等多个官方 UI 包使用
|
|
181
|
-
4. **`dsh-llm-retry` 作为参考**:同样使用 `ctx.on` 事件 + `ctx.effect` 清理,peerDep 了 `@deepseek-ai/cordis: ^4.0.2`,与本插件策略一致
|
|
182
|
-
5. **cordis ^4.0.2 被所有 DSH 插件统一引用**:`dsh-llm`、`dsh-llm-retry`、`dsh-client-ui-settings` 都声明同一个范围
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## 五、兼容性加固措施(已应用)
|
|
187
|
-
|
|
188
|
-
| # | 措施 | 位置 |
|
|
189
|
-
|---|------|------|
|
|
190
|
-
| 1 | client.js 的 `settingsScope.get?.()` / `.getSnapshot?.()` 双兼容 | 读取初始值 |
|
|
191
|
-
| 2 | client.js 的 `watch` / `subscribe` 双兼容(typeof 检测) | 监听配置变化 |
|
|
192
|
-
| 3 | host.js 的 `ctx.logger?.()` 可选链 | 日志降级 |
|
|
193
|
-
| 4 | peerDep 使用 `≥4.0.2` 范围(允许 minor 升级) | package.json |
|
|
194
|
-
| 5 | schemastery peerDep 使用 `≥3.18.0`(允许 minor 升级) | package.json |
|
|
195
|
-
| 6 | 终端 chunk 使用 DSH 标准格式 `{ type: "finish", reason: { kind, failure } }` | 与 adapterFailureChunk 一致 |
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## 六、建议
|
|
200
|
-
|
|
201
|
-
1. **当前 DSH 0.1.2-rc.1**:插件完全兼容,所有 API 已验证
|
|
202
|
-
2. **发布时建议声明 peerDep**:
|
|
203
|
-
- `@deepseek-ai/cordis`: `>=4.0.2`
|
|
204
|
-
- `@deepseek-ai/dsh-llm`: `>=0.1.0`(因为我们只用 `llm/stream` 事件)
|
|
205
|
-
- `@deepseek-ai/schemastery`: `>=3.18.0`
|
|
206
|
-
3. **DSH 升级到 0.2.x+ 时必须回归测试**以下清单:
|
|
207
|
-
- [ ] `llm/stream` 事件签名是否变化
|
|
208
|
-
- [ ] `options.provider` / `options.model` 字段是否存在
|
|
209
|
-
- [ ] `settings.register` 返回值结构是否变化
|
|
210
|
-
- [ ] `settingsScope.set/mutate` 接口是否变化
|
|
211
|
-
- [ ] `settings.plugin.item` slot 是否仍可注入
|
|
212
|
-
- [ ] **状态通道**:`connection.rpc.handle` 是否仍抛 `webServer` 相关错误(0.1.5-rc.3 会)
|
|
213
|
-
- [ ] **状态通道**:`connection.requestRejection` 仍为公开 API(兜底路径的鉴权依赖它)
|
|
214
|
-
- [ ] **状态通道**:`webServer.register({kind:"prefix"})` 仍接受前缀路由
|
|
215
|
-
- [ ] **状态通道**:`snapshot` / `reset` 端点往返成功(面板显示"● 实时"而非"重试中")
|
|
216
|
-
- [ ] **状态通道**:折叠卡片后 DevTools Network 无 `/llm-rate-limiter/snapshot` 轮询
|
|
217
|
-
- [ ] **状态通道**:无 connection 服务的组合下,卡片配置区仍可用且面板显示降级文案
|
|
218
|
-
- [ ] **状态通道**:直接 `POST /llm-rate-limiter/snapshot` 应返回 401(路由存在且被栅栏保护),而非 404/405(路由缺失)
|
|
219
|
-
|
|
220
|
-
> 上述清单已由 `test-status-route.mjs` + `tools/verify-live-route.mjs` 自动覆盖,升级 DSH 后先跑 `pnpm test && pnpm run verify:live`。
|
|
221
|
-
4. **如果 cordis 升级到 5.0+**:整个 `apply(ctx)` 接口、`ctx.effect()`、`ctx.on()` 签名可能重写,需要全面适配
|