dsh-router-core 0.3.10 → 0.3.12
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 +30 -1
- package/docs/ext.md +224 -0
- package/docs/pool-sticky-block.md +206 -0
- package/docs/suppliers.md +5 -1
- package/lib/client-registry.js +270 -96
- package/lib/client.js +270 -96
- package/lib/index.js +372 -13
- package/lib/suppliers/openrouter.js +6 -1
- package/lib/types/data-dir.d.ts +11 -0
- package/lib/types/ext/contract.d.ts +95 -0
- package/lib/types/ext/store.d.ts +16 -0
- package/lib/types/llm/adapter.d.ts +7 -0
- package/lib/types/router/account-pool.d.ts +47 -1
- package/lib/types/router/index.d.ts +15 -0
- package/lib/types/router/types.d.ts +2 -0
- package/lib/types/router/usage-store.d.ts +8 -0
- package/lib/types/shared.d.ts +20 -0
- package/lib/types/suppliers/contract.d.ts +21 -0
- package/package.json +1 -1
- package/src/client/ExtTab.tsx +143 -0
- package/src/client/RouterView.tsx +12 -4
- package/src/client/StatsTab.tsx +4 -2
- package/src/client/SupplierDetail.tsx +2 -2
- package/src/client/router.css +19 -0
- package/src/data-dir.test.ts +54 -0
- package/src/data-dir.ts +46 -0
- package/src/ext/contract.ts +115 -0
- package/src/ext/store.test.ts +144 -0
- package/src/ext/store.ts +125 -0
- package/src/index.ts +101 -6
- package/src/llm/adapter.test.ts +17 -0
- package/src/llm/adapter.ts +8 -1
- package/src/router/account-pool.test.ts +118 -1
- package/src/router/account-pool.ts +115 -3
- package/src/router/index.ts +41 -1
- package/src/router/models-cache.test.ts +38 -0
- package/src/router/types.ts +2 -0
- package/src/router/usage-store.ts +8 -0
- package/src/shared.ts +22 -0
- package/src/suppliers/contract.ts +22 -0
- package/src/suppliers/loader.test.ts +66 -1
- package/src/suppliers/loader.ts +22 -2
- package/src/suppliers/openrouter/plugin.ts +5 -0
package/lib/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { basename, dirname, join } from "node:path";
|
|
1
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
2
2
|
import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
//#region src/shared.ts
|
|
8
9
|
/** Same-origin API base the client panel fetches (host half serves it). */
|
|
9
10
|
const ROUTER_API_BASE = "/router/api";
|
|
@@ -76,6 +77,14 @@ const TRANSIENT_COOLDOWN_MS = 3e4;
|
|
|
76
77
|
const BACKOFF_BASE_MS = 2e3;
|
|
77
78
|
const BACKOFF_MAX_MS = 5 * MINUTE;
|
|
78
79
|
const BACKOFF_MAX_LEVEL = 15;
|
|
80
|
+
/** 块轮询:命中率达标就放行(`留守 = 计数<N OR 命中率<M`)。见 stickyBlock 说明。 */
|
|
81
|
+
const BLOCK_HIT_RATIO = .85;
|
|
82
|
+
/** 块轮询:最短驻留。新接到这个号的最少连续服务次数(防冷启动误切)。 */
|
|
83
|
+
const BLOCK_MIN = 16;
|
|
84
|
+
/** 块轮询:绝对兜底。轮次过多(max)无论命中率如何都让位,防死守 + 保均衡下限。 */
|
|
85
|
+
const BLOCK_MAX = 48;
|
|
86
|
+
/** 块轮询:判定「缓存无效」后的跳过时长。 */
|
|
87
|
+
const BLOCK_DEMOTE_MS = 10 * MINUTE;
|
|
79
88
|
/** 指数退避:等级 1 = BACKOFF_BASE_MS,每级翻倍,封顶 BACKOFF_MAX_MS。 */
|
|
80
89
|
function backoffMs(level) {
|
|
81
90
|
return Math.min(BACKOFF_BASE_MS * 2 ** Math.max(0, level - 1), BACKOFF_MAX_MS);
|
|
@@ -90,6 +99,10 @@ var AccountPool = class {
|
|
|
90
99
|
/** uid → 连接级禁用 / 手动暂停。 */
|
|
91
100
|
byUid = /* @__PURE__ */ new Map();
|
|
92
101
|
rrCursor = 0;
|
|
102
|
+
/** (supplier, model) → 块轮询驻留状态。 */
|
|
103
|
+
blocks = /* @__PURE__ */ new Map();
|
|
104
|
+
/** (supplier, model, uid) → 缓存无效降权。 */
|
|
105
|
+
demoted = /* @__PURE__ */ new Map();
|
|
93
106
|
constructor(supplierId = "") {
|
|
94
107
|
this.supplierId = supplierId;
|
|
95
108
|
}
|
|
@@ -155,9 +168,19 @@ var AccountPool = class {
|
|
|
155
168
|
}
|
|
156
169
|
/**
|
|
157
170
|
* 按策略为「某个模型」选一个健康账号。
|
|
171
|
+
*
|
|
172
|
+
* round-robin 走**块轮询**:一次轮转内连续服务若干请求,而不是每请求换号。
|
|
173
|
+
* 设计见 docs/pool-sticky-block.md。要点:
|
|
174
|
+
* - 留守 = `计数 < N OR 命中率 < M`(OR,用户拍板):
|
|
175
|
+
* 命中率达标(缓存热了)→ 换号保均衡;计数到 N → 换号止损。
|
|
176
|
+
* 单号最多连续服务 N 次,不会因命中率低而死守到 N_max(打架场景关键)。
|
|
177
|
+
* - 切号因子的思路:命中率是**留守**条件不是切号条件。反接会正反馈死锁
|
|
178
|
+
* (切→cold miss→命中率 0%→触发切号→…),见文档 §3。
|
|
179
|
+
* - `N_max` 绝对兜底:命中率永远不达标的号不能无限驻留(防死锁 + 防均衡归零)。
|
|
180
|
+
*
|
|
158
181
|
* @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)
|
|
159
182
|
* @param poolOrder 用户在面板拖出来的顺序(核心管)
|
|
160
|
-
* @param strategy fallback 取第一个健康 / round-robin
|
|
183
|
+
* @param strategy fallback 取第一个健康 / round-robin 块轮转健康号
|
|
161
184
|
* @param modelId 当前要路由的模型(决定查哪个 (model, uid) 冷却单元)
|
|
162
185
|
* @returns 选中的 uid;无健康账号返回 undefined
|
|
163
186
|
*/
|
|
@@ -167,10 +190,69 @@ var AccountPool = class {
|
|
|
167
190
|
const healthy = [...poolOrder.filter((uid) => byUid.has(uid)), ...accounts.map((a) => a.uid).filter((uid) => !poolOrder.includes(uid))].filter((uid) => this.healthy(uid, modelId, now));
|
|
168
191
|
if (healthy.length === 0) return void 0;
|
|
169
192
|
if (strategy !== "round-robin") return healthy[0];
|
|
170
|
-
const
|
|
171
|
-
|
|
193
|
+
const usable = healthy.filter((uid) => !this.isDemoted(uid, modelId, now));
|
|
194
|
+
const pool = usable.length > 0 ? usable : healthy;
|
|
195
|
+
const block = this.blocks.get(this.blockKey(modelId));
|
|
196
|
+
if (block !== void 0 && pool.includes(block.uid) && this.blockKeeps(block)) return block.uid;
|
|
197
|
+
const uid = pool[(block === void 0 ? this.rrCursor : Math.max(0, pool.indexOf(block.uid) + 1)) % pool.length];
|
|
198
|
+
this.rrCursor = (pool.indexOf(uid) + 1) % pool.length;
|
|
199
|
+
this.blocks.set(this.blockKey(modelId), {
|
|
200
|
+
uid,
|
|
201
|
+
served: 0,
|
|
202
|
+
hits: 0
|
|
203
|
+
});
|
|
172
204
|
return uid;
|
|
173
205
|
}
|
|
206
|
+
/** 该号在本模型上是否被判定「缓存无效」(降权中)。 */
|
|
207
|
+
isDemoted(uid, modelId, now) {
|
|
208
|
+
const e = this.demoted.get(this.key(modelId, uid));
|
|
209
|
+
return e !== void 0 && e.until > now;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 块是否还应驻留在当前号上。
|
|
213
|
+
*
|
|
214
|
+
* OR 语义(用户拍板):`留守 = 计数 < N OR 命中率 < M`。
|
|
215
|
+
* 单一账号内部:一个号先被轮到(冷启动/热),然后
|
|
216
|
+
* - 命中率**爬上去**(>= M,缓存热了)→ 让位,轮到下一个号(均衡)
|
|
217
|
+
* - 命中率**没上去**(冷启动/短块)→ 留守,继续热(不急着跳)
|
|
218
|
+
* - 轮次**实在太多**(计数 >= N_max)→ 也换(防死守 + 保均衡下限)
|
|
219
|
+
*
|
|
220
|
+
* 关键:命中率是**留守**条件不是切号执行器(反接会正反馈死锁,
|
|
221
|
+
* 见文档 §3)。换号由「命中达标 OR 份额用够」触发,都不是「命中率低」。
|
|
222
|
+
*
|
|
223
|
+
* 每个号各自守着「这个会话在自己账号上的那段前缀」,轮流热;
|
|
224
|
+
* 命中率 = 块的命中占比。单会话下每个号块都从冷到热各付一次,
|
|
225
|
+
* 收益来自块内连续驻留(缺了它就跟每请求轮询一样)。
|
|
226
|
+
*/
|
|
227
|
+
blockKeeps(block) {
|
|
228
|
+
if (block.served >= BLOCK_MAX) return false;
|
|
229
|
+
const ratio = block.served === 0 ? 0 : block.hits / block.served;
|
|
230
|
+
return block.served < BLOCK_MIN || ratio < BLOCK_HIT_RATIO;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 记录一次成功请求的缓存命中情况(块轮询的反馈信号)。
|
|
234
|
+
*
|
|
235
|
+
* 只有**成功**请求才计入:失败没产生缓存,计入会污染命中率并导致错切。
|
|
236
|
+
* @param uid 实际服务的号
|
|
237
|
+
* @param modelId 模型(块粒度)
|
|
238
|
+
* @param cachedTokens 上游报的命中 token 数;0 = 全量重算
|
|
239
|
+
*/
|
|
240
|
+
noteCache(uid, modelId, cachedTokens) {
|
|
241
|
+
const k = this.blockKey(modelId);
|
|
242
|
+
const block = this.blocks.get(k);
|
|
243
|
+
if (block === void 0 || block.uid !== uid) return;
|
|
244
|
+
block.served += 1;
|
|
245
|
+
if (cachedTokens > 0) block.hits += 1;
|
|
246
|
+
if (block.served >= BLOCK_MAX && block.hits / block.served < BLOCK_HIT_RATIO) this.demoted.set(this.key(modelId, uid), { until: Date.now() + BLOCK_DEMOTE_MS });
|
|
247
|
+
}
|
|
248
|
+
/** 故障切走时作废当前块:换号后前缀归属变了,块计数不能沿用。 */
|
|
249
|
+
dropBlock(modelId) {
|
|
250
|
+
this.blocks.delete(this.blockKey(modelId));
|
|
251
|
+
}
|
|
252
|
+
/** 块状态键:与冷却同粒度 (supplier, model),不含 uid。 */
|
|
253
|
+
blockKey(modelId) {
|
|
254
|
+
return `${this.supplierId}${SEP}${modelId}`;
|
|
255
|
+
}
|
|
174
256
|
/** 记录一次失败:按状态处置(冷却/禁用/退避),落在「(模型, 连接)」这个冷却单元上。 */
|
|
175
257
|
noteFailure(uid, modelId, state, message) {
|
|
176
258
|
const rule = RULES[state];
|
|
@@ -1608,6 +1690,36 @@ var Router = class {
|
|
|
1608
1690
|
* 只有两种情况会真的等:`force`(用户主动刷新)和**从来没有过缓存值**
|
|
1609
1691
|
* (冷启动,没有旧值可给)。后者只发生一次。
|
|
1610
1692
|
*/
|
|
1693
|
+
/**
|
|
1694
|
+
* 组合的上下文窗口(token):组合内各模型窗口的**最小值**,拿不到就 undefined。
|
|
1695
|
+
*
|
|
1696
|
+
* 只读缓存、绝不打上游:调用方是 llm adapter 的 `resolveModel`,它在每次
|
|
1697
|
+
* 请求前后都可能被调用,不能为了报一个容量去触发按供应商的网络往返。
|
|
1698
|
+
* 冷启动(还没缓存)时给 undefined —— 自动压缩会因此跳过这一轮,下次就
|
|
1699
|
+
* 有了,比撒谎说一个窗口值安全。
|
|
1700
|
+
*
|
|
1701
|
+
* 单位:`ModelInfo.context_length` 存的是 **K**(供应商插件统一
|
|
1702
|
+
* `Math.round(tokens / 1000)`,见 codebuddy plugin),这里换算回 token。
|
|
1703
|
+
*
|
|
1704
|
+
* 取最小而不是最大:组合会按策略 fallback 到任意一个模型,按最窄的声明
|
|
1705
|
+
* 才能让「快满了」这件事在任何一条分支上都成立。
|
|
1706
|
+
*/
|
|
1707
|
+
comboContextWindow(combo) {
|
|
1708
|
+
let min;
|
|
1709
|
+
for (const ref of combo.models) {
|
|
1710
|
+
const at = ref.indexOf(",");
|
|
1711
|
+
if (at < 0) continue;
|
|
1712
|
+
const supplierId = ref.slice(0, at);
|
|
1713
|
+
const modelId = ref.slice(at + 1);
|
|
1714
|
+
const hit = this.modelsCache.get(supplierId);
|
|
1715
|
+
if (hit === void 0) return void 0;
|
|
1716
|
+
const k = hit.models.find((x) => x.id === modelId)?.context_length;
|
|
1717
|
+
if (typeof k !== "number" || !Number.isFinite(k) || k <= 0) continue;
|
|
1718
|
+
const tokens = Math.round(k * 1e3);
|
|
1719
|
+
if (min === void 0 || tokens < min) min = tokens;
|
|
1720
|
+
}
|
|
1721
|
+
return min;
|
|
1722
|
+
}
|
|
1611
1723
|
async modelsOf(supplierId, force = false) {
|
|
1612
1724
|
const s = this.suppliers.find((x) => x.id === supplierId);
|
|
1613
1725
|
if (s === void 0) return [];
|
|
@@ -1836,6 +1948,7 @@ var Router = class {
|
|
|
1836
1948
|
ok,
|
|
1837
1949
|
durationMs: Date.now() - probe.startedAt,
|
|
1838
1950
|
ttfbMs: probe.ttfbMs,
|
|
1951
|
+
...ok && probe.uid !== "" ? { uid: probe.uid } : {},
|
|
1839
1952
|
...error === void 0 ? {} : { error }
|
|
1840
1953
|
}, usage);
|
|
1841
1954
|
};
|
|
@@ -1974,6 +2087,7 @@ var Router = class {
|
|
|
1974
2087
|
return false;
|
|
1975
2088
|
}
|
|
1976
2089
|
pool.noteFailure(uid, req.model, r.state, r.message);
|
|
2090
|
+
pool.dropBlock(req.model);
|
|
1977
2091
|
if (trace !== void 0) {
|
|
1978
2092
|
trace.attempts += 1;
|
|
1979
2093
|
trace.lastState = r.state;
|
|
@@ -1988,7 +2102,10 @@ var Router = class {
|
|
|
1988
2102
|
probe.model = `${s.getAlias()}/${stripAlias(req.model, s.getAlias())}`;
|
|
1989
2103
|
probe.uid = uid;
|
|
1990
2104
|
}
|
|
1991
|
-
if (await writeChatResult(res, r, req.stream, probe, probe?.startedAt ?? 0))
|
|
2105
|
+
if (await writeChatResult(res, r, req.stream, probe, probe?.startedAt ?? 0)) {
|
|
2106
|
+
pool.noteCache(uid, req.model, probe?.tokens?.cachedTokens ?? 0);
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
1992
2109
|
pool.noteFailure(uid, req.model, "transport", "stream failed before first byte");
|
|
1993
2110
|
if (trace !== void 0) {
|
|
1994
2111
|
trace.attempts += 1;
|
|
@@ -2424,6 +2541,7 @@ var RouterAdapter = class extends LlmAdapter {
|
|
|
2424
2541
|
provider: "router",
|
|
2425
2542
|
id: model,
|
|
2426
2543
|
name: found?.name ?? found?.id ?? model,
|
|
2544
|
+
...found?.contextWindow !== void 0 ? { context: { contextWindow: found.contextWindow } } : {},
|
|
2427
2545
|
reasoning: {
|
|
2428
2546
|
efforts: ROUTER_REASONING_EFFORTS,
|
|
2429
2547
|
defaultEffort: ReasoningEffortId("high")
|
|
@@ -2696,15 +2814,24 @@ const DIFF_CAPS = [
|
|
|
2696
2814
|
"pollLogin",
|
|
2697
2815
|
"checkinNow"
|
|
2698
2816
|
];
|
|
2817
|
+
/**
|
|
2818
|
+
* 每个供应商一份 env 副本:`onLateFailure` 是**按供应商**绑到本供应商的池上
|
|
2819
|
+
* 的(见 wrapModule),共用一个 env 对象会让所有插件的上报都落到最后一个
|
|
2820
|
+
* 供应商的池里。副本是浅拷贝,store/credentials 仍是同一实例。
|
|
2821
|
+
*/
|
|
2822
|
+
function ownEnv(env) {
|
|
2823
|
+
return { ...env };
|
|
2824
|
+
}
|
|
2699
2825
|
/** 从文件构造 LoadedSupplier。 */
|
|
2700
2826
|
async function loadOne(file, env, source) {
|
|
2701
2827
|
const mod = await importSupplierFile(file);
|
|
2828
|
+
const mine = ownEnv(env);
|
|
2702
2829
|
let instance;
|
|
2703
2830
|
if (typeof mod === "function") {
|
|
2704
|
-
instance = mod(
|
|
2831
|
+
instance = mod(mine);
|
|
2705
2832
|
if (typeof instance !== "object" || instance === null || typeof instance.id !== "string" || instance.id === "") throw new Error(`供应商 factory 返回无效: ${file}`);
|
|
2706
2833
|
} else instance = mod;
|
|
2707
|
-
return wrapModule(instance,
|
|
2834
|
+
return wrapModule(instance, mine, file, source);
|
|
2708
2835
|
}
|
|
2709
2836
|
/**
|
|
2710
2837
|
* 从 SupplierModule 实例构造 LoadedSupplier(能力检测 + Supplier 包装)。
|
|
@@ -2715,11 +2842,16 @@ function wrapModule(instance, env, source, kind = "external") {
|
|
|
2715
2842
|
const capabilities = /* @__PURE__ */ new Set();
|
|
2716
2843
|
for (const key of DIFF_CAPS) if (isFn(instance[key])) capabilities.add(key);
|
|
2717
2844
|
const pool = new AccountPool(instance.id);
|
|
2845
|
+
env.onLateFailure = (uid, model, state, message) => {
|
|
2846
|
+
if (uid === "") return;
|
|
2847
|
+
pool.noteFailure(uid, model, state, message);
|
|
2848
|
+
};
|
|
2718
2849
|
const supplier = {
|
|
2719
2850
|
id: instance.id,
|
|
2720
2851
|
name: instance.name,
|
|
2721
2852
|
priority: instance.priority ?? 0,
|
|
2722
2853
|
icon: instance.icon,
|
|
2854
|
+
apiKeyHint: instance.apiKeyHint,
|
|
2723
2855
|
status: () => {
|
|
2724
2856
|
const now = instance.status();
|
|
2725
2857
|
const accounts = now.accounts.map((a) => {
|
|
@@ -3245,6 +3377,153 @@ var CredentialStore = class {
|
|
|
3245
3377
|
}
|
|
3246
3378
|
};
|
|
3247
3379
|
//#endregion
|
|
3380
|
+
//#region src/data-dir.ts
|
|
3381
|
+
/**
|
|
3382
|
+
* 落盘路径解析 —— dsh-router 所有持久化路径的唯一来源。
|
|
3383
|
+
*
|
|
3384
|
+
* 以前写的是相对路径 `data/state.json`:**落盘位置跟着 `process.cwd()` 跑**。
|
|
3385
|
+
* 不在 `~/.dsh/profiles/web` 里 `dsh web` 启动,state.json / supplier-config.json
|
|
3386
|
+
* / keys.json / usage.json 就会静默写进另一个目录(面板显示空配置),切回原目录
|
|
3387
|
+
* 启动又「回来」——用户看到的是「配置丢了」。所以这里一律产出绝对路径。
|
|
3388
|
+
*/
|
|
3389
|
+
/**
|
|
3390
|
+
* 本 profile 的目录。
|
|
3391
|
+
*
|
|
3392
|
+
* 宿主把配置树锚点 `ctx.baseUrl` 挂在每个插件的 context 上,它正是 profile
|
|
3393
|
+
* 目录(`cordis.yml` 所在处)——不用猜、也不用像 dsh-market 那样嗅探 argv。
|
|
3394
|
+
* 兜底只服务「宿主没给锚点」的冷启动(单测、非常规宿主):沿用今天的事实
|
|
3395
|
+
* 标准 `web` profile。
|
|
3396
|
+
*/
|
|
3397
|
+
function profileDirOf(baseUrl) {
|
|
3398
|
+
if (typeof baseUrl === "string" && baseUrl !== "") try {
|
|
3399
|
+
return resolve(fileURLToPath(baseUrl));
|
|
3400
|
+
} catch {}
|
|
3401
|
+
return join(dshHome(), "profiles", "web");
|
|
3402
|
+
}
|
|
3403
|
+
/** 数据目录:state.json / supplier-config.json / keys.json / auths/ 都在这里。 */
|
|
3404
|
+
function dataDirOf(baseUrl) {
|
|
3405
|
+
return join(profileDirOf(baseUrl), "data");
|
|
3406
|
+
}
|
|
3407
|
+
/** `$DSH_HOME`(支持 `~` 前缀),缺省 `~/.dsh`。 */
|
|
3408
|
+
function dshHome() {
|
|
3409
|
+
const raw = process.env.DSH_HOME;
|
|
3410
|
+
if (raw === void 0 || raw.trim() === "") return join(homedir(), ".dsh");
|
|
3411
|
+
const value = raw.trim();
|
|
3412
|
+
if (value === "~") return homedir();
|
|
3413
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return resolve(homedir(), value.slice(2));
|
|
3414
|
+
return resolve(value);
|
|
3415
|
+
}
|
|
3416
|
+
//#endregion
|
|
3417
|
+
//#region src/ext/store.ts
|
|
3418
|
+
/**
|
|
3419
|
+
* 扩展存储 —— **开关状态与插件数据都归核心持久化**,插件不自己 file IO。
|
|
3420
|
+
*
|
|
3421
|
+
* 落盘 `<dataDir>/ext.json`,按扩展器 id 存:
|
|
3422
|
+
* `{ "rtk": { "enabled": true, "data": { ...插件自己的东西... } } }`
|
|
3423
|
+
*
|
|
3424
|
+
* 为什么归核心:跟 `supplier-config.json` 一个道理——开关是用户对**核心面板**的操作,
|
|
3425
|
+
* 不该由扩展插件各自决定存哪、怎么存(否则每个插件长一份互相不一致的实现,核心也无
|
|
3426
|
+
* 从统一裁决「能不能开」)。插件变薄:只报自己是否可用(`ready`)+ 做事。
|
|
3427
|
+
*
|
|
3428
|
+
* `data` 槽是给插件的通用抽屉(2026-09 加):插件不再为了存一点状态而自己开数据目录,
|
|
3429
|
+
* 统一经 `router.extStore` service 读写,落盘位置仍由核心锚定(不跟 cwd 跑)。
|
|
3430
|
+
*
|
|
3431
|
+
* 写法与核心其他存储一致:先写 `.tmp` 再 `rename` 原子覆盖(避免写一半崩掉留坏文件),
|
|
3432
|
+
* 权限 600;持久化失败不阻断。
|
|
3433
|
+
*/
|
|
3434
|
+
/** 扩展开关 + 插件数据存储。 */
|
|
3435
|
+
var ExtStore = class {
|
|
3436
|
+
fp = "";
|
|
3437
|
+
byId = {};
|
|
3438
|
+
constructor(stateFile) {
|
|
3439
|
+
this.fp = stateFile ? join(dirname(stateFile), "ext.json") : "";
|
|
3440
|
+
this.load();
|
|
3441
|
+
this.migrateLegacy();
|
|
3442
|
+
}
|
|
3443
|
+
migrateLegacy() {
|
|
3444
|
+
if (this.fp === "") return;
|
|
3445
|
+
if (Object.keys(this.byId).length > 0) return;
|
|
3446
|
+
const legacy = join(dirname(this.fp), "enhance.json");
|
|
3447
|
+
let enabled = false;
|
|
3448
|
+
try {
|
|
3449
|
+
enabled = JSON.parse(readFileSync(legacy, "utf8")).enabled === true;
|
|
3450
|
+
} catch {
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
if (!enabled) return;
|
|
3454
|
+
this.byId.rtk = {
|
|
3455
|
+
enabled: true,
|
|
3456
|
+
data: void 0
|
|
3457
|
+
};
|
|
3458
|
+
this.save();
|
|
3459
|
+
}
|
|
3460
|
+
load() {
|
|
3461
|
+
if (this.fp === "") return;
|
|
3462
|
+
let raw;
|
|
3463
|
+
try {
|
|
3464
|
+
raw = readFileSync(this.fp, "utf8");
|
|
3465
|
+
} catch {
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
try {
|
|
3469
|
+
const f = JSON.parse(raw);
|
|
3470
|
+
for (const [id, v] of Object.entries(f)) {
|
|
3471
|
+
if (id === "" || v === null || typeof v !== "object") continue;
|
|
3472
|
+
if (typeof v?.enabled !== "boolean") continue;
|
|
3473
|
+
this.byId[id] = {
|
|
3474
|
+
enabled: v.enabled,
|
|
3475
|
+
data: v.data
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
} catch {}
|
|
3479
|
+
}
|
|
3480
|
+
save() {
|
|
3481
|
+
if (this.fp === "") return;
|
|
3482
|
+
try {
|
|
3483
|
+
const dir = dirname(this.fp);
|
|
3484
|
+
if (dir !== "" && dir !== ".") mkdirSync(dir, { recursive: true });
|
|
3485
|
+
const out = {};
|
|
3486
|
+
for (const [id, e] of Object.entries(this.byId)) out[id] = e.data === void 0 ? { enabled: e.enabled } : {
|
|
3487
|
+
enabled: e.enabled,
|
|
3488
|
+
data: e.data
|
|
3489
|
+
};
|
|
3490
|
+
const raw = JSON.stringify(out, null, 2);
|
|
3491
|
+
const tmp = this.fp + ".tmp";
|
|
3492
|
+
writeFileSync(tmp, raw, { mode: 384 });
|
|
3493
|
+
renameSync(tmp, this.fp);
|
|
3494
|
+
} catch {}
|
|
3495
|
+
}
|
|
3496
|
+
/** 是否开启(未记录过 = 默认关)。 */
|
|
3497
|
+
isEnabled(id) {
|
|
3498
|
+
return this.byId[id]?.enabled === true;
|
|
3499
|
+
}
|
|
3500
|
+
setEnabled(id, enabled) {
|
|
3501
|
+
if (id === "") return;
|
|
3502
|
+
const cur = this.byId[id];
|
|
3503
|
+
if (cur?.enabled === enabled) return;
|
|
3504
|
+
this.byId[id] = {
|
|
3505
|
+
enabled,
|
|
3506
|
+
data: cur?.data
|
|
3507
|
+
};
|
|
3508
|
+
this.save();
|
|
3509
|
+
}
|
|
3510
|
+
/** 读插件自己存的数据块(没存过 = undefined)。 */
|
|
3511
|
+
readData(id) {
|
|
3512
|
+
if (id === "") return void 0;
|
|
3513
|
+
return this.byId[id]?.data;
|
|
3514
|
+
}
|
|
3515
|
+
/** 写插件自己的数据块(覆盖;与 enabled 同一文件落盘)。 */
|
|
3516
|
+
writeData(id, value) {
|
|
3517
|
+
if (id === "") return;
|
|
3518
|
+
const cur = this.byId[id];
|
|
3519
|
+
this.byId[id] = {
|
|
3520
|
+
enabled: cur?.enabled === true,
|
|
3521
|
+
data: value
|
|
3522
|
+
};
|
|
3523
|
+
this.save();
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
//#endregion
|
|
3248
3527
|
//#region src/index.ts
|
|
3249
3528
|
/**
|
|
3250
3529
|
* Plugin identity for cordis.yml rows — 必须与 package.json 的 name 一致。
|
|
@@ -3282,9 +3561,11 @@ function readBody(req, limit) {
|
|
|
3282
3561
|
function apply(rawContext) {
|
|
3283
3562
|
const ctx = rawContext;
|
|
3284
3563
|
const log = (msg) => ctx.logger.info(`[dsh-router] ${msg}`);
|
|
3285
|
-
const
|
|
3564
|
+
const dataDir = dataDirOf(ctx.baseUrl);
|
|
3565
|
+
const stateFile = join(dataDir, "state.json");
|
|
3566
|
+
log(`data dir: ${dataDir}`);
|
|
3286
3567
|
const store = new SupplierConfigStore(stateFile);
|
|
3287
|
-
const credentials = new CredentialStore(join(
|
|
3568
|
+
const credentials = new CredentialStore(join(dataDir, "auths"));
|
|
3288
3569
|
const router = new Router(stateFile, store, log);
|
|
3289
3570
|
const keys = new KeysStore(stateFile);
|
|
3290
3571
|
const disposers = [];
|
|
@@ -3316,7 +3597,10 @@ function apply(rawContext) {
|
|
|
3316
3597
|
log(`supplier unregistered: ${id}`);
|
|
3317
3598
|
};
|
|
3318
3599
|
ctx.provide("router.suppliers", {});
|
|
3319
|
-
const
|
|
3600
|
+
const exts = {};
|
|
3601
|
+
ctx.provide("router.ext", exts);
|
|
3602
|
+
const extStore = new ExtStore(stateFile);
|
|
3603
|
+
ctx.provide("router.extStore", extStore);
|
|
3320
3604
|
const registerLoaded = (loaded) => {
|
|
3321
3605
|
loadedSuppliers.push(loaded);
|
|
3322
3606
|
router.add(loaded.supplier);
|
|
@@ -3362,7 +3646,7 @@ function apply(rawContext) {
|
|
|
3362
3646
|
});
|
|
3363
3647
|
(async () => {
|
|
3364
3648
|
try {
|
|
3365
|
-
const userDir = join(
|
|
3649
|
+
const userDir = join(profileDirOf(ctx.baseUrl), "suppliers");
|
|
3366
3650
|
const { suppliers, errors } = await loadSuppliers({
|
|
3367
3651
|
builtinDir: join(import.meta.dirname, "suppliers"),
|
|
3368
3652
|
userDir,
|
|
@@ -3440,6 +3724,7 @@ function apply(rawContext) {
|
|
|
3440
3724
|
id: s.id,
|
|
3441
3725
|
name: s.name,
|
|
3442
3726
|
icon: loaded?.supplier.icon,
|
|
3727
|
+
apiKeyHint: loaded?.supplier.apiKeyHint,
|
|
3443
3728
|
capabilities: loaded ? [...loaded.capabilities] : [],
|
|
3444
3729
|
source: loaded?.source ?? "external"
|
|
3445
3730
|
};
|
|
@@ -3618,6 +3903,62 @@ function apply(rawContext) {
|
|
|
3618
3903
|
requireApiKey: keys.requireApiKey
|
|
3619
3904
|
});
|
|
3620
3905
|
});
|
|
3906
|
+
route(`${ROUTER_API_BASE}/ext`, async (req, res) => {
|
|
3907
|
+
if (req.method === "PATCH") {
|
|
3908
|
+
let body;
|
|
3909
|
+
try {
|
|
3910
|
+
body = JSON.parse(await readBody(req, 65536));
|
|
3911
|
+
} catch {
|
|
3912
|
+
writeJson(res, 400, {
|
|
3913
|
+
ok: false,
|
|
3914
|
+
error: "invalid JSON body"
|
|
3915
|
+
});
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3918
|
+
const ext = typeof body.id === "string" ? exts[body.id] : void 0;
|
|
3919
|
+
if (!ext) {
|
|
3920
|
+
writeJson(res, 404, {
|
|
3921
|
+
ok: false,
|
|
3922
|
+
error: "extension not found"
|
|
3923
|
+
});
|
|
3924
|
+
return;
|
|
3925
|
+
}
|
|
3926
|
+
if (typeof body.enabled !== "boolean") {
|
|
3927
|
+
writeJson(res, 400, {
|
|
3928
|
+
ok: false,
|
|
3929
|
+
error: "enabled must be a boolean"
|
|
3930
|
+
});
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
if (body.enabled && typeof ext.getState === "function") {
|
|
3934
|
+
const st = ext.getState();
|
|
3935
|
+
if (st?.ready !== true) {
|
|
3936
|
+
writeJson(res, 409, {
|
|
3937
|
+
ok: false,
|
|
3938
|
+
error: st?.detail ?? "extension not ready"
|
|
3939
|
+
});
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
extStore.setEnabled(ext.id, body.enabled);
|
|
3944
|
+
}
|
|
3945
|
+
writeJson(res, 200, {
|
|
3946
|
+
ok: true,
|
|
3947
|
+
enhancers: Object.values(exts).map((raw) => {
|
|
3948
|
+
const e = raw;
|
|
3949
|
+
if (!e || typeof e.id !== "string" || e.id === "") return void 0;
|
|
3950
|
+
const st = typeof e.getState === "function" ? e.getState() : { ready: false };
|
|
3951
|
+
return {
|
|
3952
|
+
id: e.id,
|
|
3953
|
+
name: e.name ?? e.id,
|
|
3954
|
+
...e.description !== void 0 ? { description: e.description } : {},
|
|
3955
|
+
enabled: extStore.isEnabled(e.id),
|
|
3956
|
+
ready: st?.ready === true,
|
|
3957
|
+
...st?.detail !== void 0 ? { detail: st.detail } : {}
|
|
3958
|
+
};
|
|
3959
|
+
}).filter((e) => e !== void 0)
|
|
3960
|
+
});
|
|
3961
|
+
});
|
|
3621
3962
|
/** 解析并校验周期参数(不接受任意字符串)。 */
|
|
3622
3963
|
const periodOf = (raw) => {
|
|
3623
3964
|
return raw === "24h" || raw === "7d" || raw === "30d" ? raw : "today";
|
|
@@ -3676,8 +4017,23 @@ function apply(rawContext) {
|
|
|
3676
4017
|
disposers.push(ctx.llm.registerModelDiscovery(LLM_NS, async () => {
|
|
3677
4018
|
return (await router.combos()).map((c) => ({ id: c.name }));
|
|
3678
4019
|
}));
|
|
3679
|
-
disposers.push(ctx.llm.registerAdapter(["router"], new RouterAdapter("http://localhost:3080/v1", { comboModels: async () => (await router.combos()).map((c) =>
|
|
4020
|
+
disposers.push(ctx.llm.registerAdapter(["router"], new RouterAdapter("http://localhost:3080/v1", { comboModels: async () => (await router.combos()).map((c) => {
|
|
4021
|
+
const w = router.comboContextWindow(c);
|
|
4022
|
+
return {
|
|
4023
|
+
id: c.name,
|
|
4024
|
+
...w !== void 0 ? { contextWindow: w } : {}
|
|
4025
|
+
};
|
|
4026
|
+
}) })));
|
|
3680
4027
|
log("llm provider (Router) + discovery + adapter registered ok");
|
|
4028
|
+
(async () => {
|
|
4029
|
+
for (const c of await router.combos()) for (const ref of c.models) {
|
|
4030
|
+
const at = ref.indexOf(",");
|
|
4031
|
+
if (at < 0) continue;
|
|
4032
|
+
try {
|
|
4033
|
+
await router.modelsOf(ref.slice(0, at));
|
|
4034
|
+
} catch {}
|
|
4035
|
+
}
|
|
4036
|
+
})().catch(() => {});
|
|
3681
4037
|
} catch (err) {
|
|
3682
4038
|
ctx.logger.warn(`[dsh-router] llm registration failed: ${err.message}`);
|
|
3683
4039
|
}
|
|
@@ -3703,6 +4059,9 @@ function apply(rawContext) {
|
|
|
3703
4059
|
});
|
|
3704
4060
|
});
|
|
3705
4061
|
ctx.effect(() => () => {
|
|
4062
|
+
for (const raw of Object.values(exts)) try {
|
|
4063
|
+
raw?.dispose?.();
|
|
4064
|
+
} catch {}
|
|
3706
4065
|
for (const dispose of disposers.splice(0)) dispose();
|
|
3707
4066
|
router.dispose();
|
|
3708
4067
|
}, "dsh-router: teardown");
|
|
@@ -4,6 +4,10 @@ const name = "OpenRouter";
|
|
|
4
4
|
const priority = 10;
|
|
5
5
|
/** 面板图标。 */
|
|
6
6
|
const icon = "data:image/png;base64,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";
|
|
7
|
+
/**
|
|
8
|
+
* 「添加 API key」弹窗提示(OpenRouter API key 获取页)。
|
|
9
|
+
*/
|
|
10
|
+
const apiKeyHint = "填入 OpenRouter API key(https://openrouter.ai/settings/keys),可起名字区分多个 key。";
|
|
7
11
|
const BASE = "https://openrouter.ai/api/v1";
|
|
8
12
|
const MODELS_URL = `${BASE}/models`;
|
|
9
13
|
const CHAT_URL = `${BASE}/chat/completions`;
|
|
@@ -79,6 +83,7 @@ function factory(env) {
|
|
|
79
83
|
name,
|
|
80
84
|
priority: 10,
|
|
81
85
|
icon,
|
|
86
|
+
apiKeyHint,
|
|
82
87
|
status: () => {
|
|
83
88
|
const accounts = orderedKeys().map(({ uid, acct }) => ({
|
|
84
89
|
uid,
|
|
@@ -224,4 +229,4 @@ function factory(env) {
|
|
|
224
229
|
};
|
|
225
230
|
}
|
|
226
231
|
//#endregion
|
|
227
|
-
export { factory as default, icon, id, name, priority };
|
|
232
|
+
export { apiKeyHint, factory as default, icon, id, name, priority };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本 profile 的目录。
|
|
3
|
+
*
|
|
4
|
+
* 宿主把配置树锚点 `ctx.baseUrl` 挂在每个插件的 context 上,它正是 profile
|
|
5
|
+
* 目录(`cordis.yml` 所在处)——不用猜、也不用像 dsh-market 那样嗅探 argv。
|
|
6
|
+
* 兜底只服务「宿主没给锚点」的冷启动(单测、非常规宿主):沿用今天的事实
|
|
7
|
+
* 标准 `web` profile。
|
|
8
|
+
*/
|
|
9
|
+
export declare function profileDirOf(baseUrl?: string): string;
|
|
10
|
+
/** 数据目录:state.json / supplier-config.json / keys.json / auths/ 都在这里。 */
|
|
11
|
+
export declare function dataDirOf(baseUrl?: string): string;
|