dsh-router-core 0.3.8 → 0.3.10

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/lib/index.js CHANGED
@@ -1,14 +1,12 @@
1
1
  import { basename, dirname, join } from "node:path";
2
2
  import { mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
3
- import { CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, attributionHeaders } from "@deepseek-ai/dsh-llm";
3
+ import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { DatabaseSync } from "node:sqlite";
7
7
  //#region src/shared.ts
8
8
  /** Same-origin API base the client panel fetches (host half serves it). */
9
9
  const ROUTER_API_BASE = "/router/api";
10
- /** Bearer key env / credential reference protecting /v1/*. */
11
- const API_KEY_REF = "TW2A_API_KEY";
12
10
  //#endregion
13
11
  //#region src/router/account-pool.ts
14
12
  const MINUTE = 6e4;
@@ -258,6 +256,13 @@ function readCredits(raw) {
258
256
  for (const [uid, v] of Object.entries(raw)) if (typeof v === "number" && Number.isFinite(v) && v >= 0) out[uid] = v;
259
257
  return out;
260
258
  }
259
+ /** 读盘时的连接显示别名校验:只留非空字符串。 */
260
+ function readNames(raw) {
261
+ const out = {};
262
+ if (typeof raw !== "object" || raw === null) return out;
263
+ for (const [uid, v] of Object.entries(raw)) if (typeof v === "string" && v.trim() !== "") out[uid] = v.trim();
264
+ return out;
265
+ }
261
266
  /** 通用供应商配置存储。 */
262
267
  var SupplierConfigStore = class {
263
268
  fp = "";
@@ -288,7 +293,8 @@ var SupplierConfigStore = class {
288
293
  custom: [],
289
294
  poolOrder: [],
290
295
  poolStrategy: "fallback",
291
- credits: {}
296
+ credits: {},
297
+ accountNames: {}
292
298
  };
293
299
  this.bySupplier.set(supplierId, cfg);
294
300
  }
@@ -340,6 +346,24 @@ var SupplierConfigStore = class {
340
346
  this.get(supplierId).poolStrategy = strategy;
341
347
  this.saveLocked();
342
348
  }
349
+ /** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
350
+ getAccountName(supplierId, uid) {
351
+ const n = this.get(supplierId).accountNames[uid];
352
+ return typeof n === "string" && n !== "" ? n : void 0;
353
+ }
354
+ /** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
355
+ setAccountName(supplierId, uid, name) {
356
+ const cfg = this.get(supplierId);
357
+ const clean = (name ?? "").trim();
358
+ if (clean === "") {
359
+ if (cfg.accountNames[uid] === void 0) return;
360
+ delete cfg.accountNames[uid];
361
+ this.saveLocked();
362
+ return;
363
+ }
364
+ cfg.accountNames[uid] = clean;
365
+ this.saveLocked();
366
+ }
343
367
  /**
344
368
  * 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
345
369
  * 显示什么——不要把 -1 当 0 展示。
@@ -380,7 +404,8 @@ var SupplierConfigStore = class {
380
404
  custom: Array.isArray(c.custom) ? c.custom.filter((m) => typeof m === "string") : [],
381
405
  poolOrder: Array.isArray(c.poolOrder) ? c.poolOrder.filter((u) => typeof u === "string") : [],
382
406
  poolStrategy: c.poolStrategy === "round-robin" ? "round-robin" : "fallback",
383
- credits: readCredits(c.credits)
407
+ credits: readCredits(c.credits),
408
+ accountNames: readNames(c.accountNames)
384
409
  });
385
410
  }
386
411
  saveLocked() {
@@ -392,7 +417,8 @@ var SupplierConfigStore = class {
392
417
  custom: [...cfg.custom],
393
418
  poolOrder: [...cfg.poolOrder],
394
419
  poolStrategy: cfg.poolStrategy,
395
- credits: { ...cfg.credits }
420
+ credits: { ...cfg.credits },
421
+ accountNames: { ...cfg.accountNames }
396
422
  };
397
423
  try {
398
424
  const dir = dirname(this.fp);
@@ -1921,7 +1947,7 @@ var Router = class {
1921
1947
  const pool = s.pool;
1922
1948
  const cfg = this.store.get(s.id);
1923
1949
  if (s.accounts !== void 0 && s.accounts().length === 0) {
1924
- const r = await s.chatOnce("", req);
1950
+ const r = await s.chatOnce("", req.lv ?? "auto", req);
1925
1951
  if (!r.ok) {
1926
1952
  if (r.state === "no_such_model") return false;
1927
1953
  if (trace !== void 0) trace.lastState = r.state;
@@ -1941,7 +1967,7 @@ var Router = class {
1941
1967
  const uid = pool.pick(s.accounts().filter((a) => !tried.has(a.uid)), cfg.poolOrder, cfg.poolStrategy, req.model);
1942
1968
  if (uid === void 0) return false;
1943
1969
  tried.add(uid);
1944
- const r = await s.chatOnce(uid, req);
1970
+ const r = await s.chatOnce(uid, req.lv ?? "auto", req);
1945
1971
  if (!r.ok) {
1946
1972
  if (r.state === "no_such_model") {
1947
1973
  if (trace !== void 0) trace.lastError = "no_such_model";
@@ -2176,7 +2202,7 @@ async function* translateSse(payloads) {
2176
2202
  index: block.index,
2177
2203
  block: {
2178
2204
  type: "tool-call",
2179
- id: CallId(block.callId ?? ""),
2205
+ id: ToolCallId(block.callId ?? ""),
2180
2206
  name: "",
2181
2207
  arguments: "{}"
2182
2208
  }
@@ -2272,7 +2298,7 @@ async function* translateSse(payloads) {
2272
2298
  yield {
2273
2299
  type: "tool-call-delta",
2274
2300
  index: block.index,
2275
- id: CallId(block.callId ?? ""),
2301
+ id: ToolCallId(block.callId ?? ""),
2276
2302
  ...block.name !== void 0 ? { name: block.name } : {},
2277
2303
  argumentsDelta: fragment
2278
2304
  };
@@ -2318,7 +2344,7 @@ function closeBlock(block) {
2318
2344
  };
2319
2345
  case "tool-call": return {
2320
2346
  type: "tool-call",
2321
- id: CallId(block.callId ?? ""),
2347
+ id: ToolCallId(block.callId ?? ""),
2322
2348
  name: block.name ?? "",
2323
2349
  arguments: block.text === "" ? "{}" : block.text
2324
2350
  };
@@ -2344,6 +2370,25 @@ function completeJson(text) {
2344
2370
  * DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
2345
2371
  * /v1/chat/completions(组合路由在 /v1 内完成)。
2346
2372
  */
2373
+ /** Router provider 对外暴露的推理等级(对齐 dsh/DeepSeek:off/low/high/max)。 */
2374
+ const ROUTER_REASONING_EFFORTS = [
2375
+ {
2376
+ id: ReasoningEffortId("off"),
2377
+ name: "Off"
2378
+ },
2379
+ {
2380
+ id: ReasoningEffortId("low"),
2381
+ name: "Low"
2382
+ },
2383
+ {
2384
+ id: ReasoningEffortId("high"),
2385
+ name: "High"
2386
+ },
2387
+ {
2388
+ id: ReasoningEffortId("max"),
2389
+ name: "Max"
2390
+ }
2391
+ ];
2347
2392
  var RouterAdapter = class extends LlmAdapter {
2348
2393
  baseURL;
2349
2394
  source;
@@ -2373,6 +2418,18 @@ var RouterAdapter = class extends LlmAdapter {
2373
2418
  }
2374
2419
  return out;
2375
2420
  }
2421
+ async resolveModel(_provider, model) {
2422
+ const found = (await this.source.comboModels()).find((m) => m.id === model || m.name === model);
2423
+ return {
2424
+ provider: "router",
2425
+ id: model,
2426
+ name: found?.name ?? found?.id ?? model,
2427
+ reasoning: {
2428
+ efforts: ROUTER_REASONING_EFFORTS,
2429
+ defaultEffort: ReasoningEffortId("high")
2430
+ }
2431
+ };
2432
+ }
2376
2433
  async *stream(options) {
2377
2434
  const body = wireRequest(options);
2378
2435
  const controller = new AbortController();
@@ -2478,7 +2535,7 @@ async function* ssePayloads(body) {
2478
2535
  *
2479
2536
  * /v1/* 鉴权规则:
2480
2537
  * - requireApiKey=false(默认)→ 不鉴权
2481
- * - requireApiKey=true → Bearer 必须是「启用的库内 key」或 TW2A_API_KEY env
2538
+ * - requireApiKey=true → Bearer 必须是「启用的库内 key」
2482
2539
  */
2483
2540
  function newId() {
2484
2541
  return randomBytes(6).toString("hex");
@@ -2575,13 +2632,11 @@ var KeysStore = class {
2575
2632
  this.require = v;
2576
2633
  this.save();
2577
2634
  }
2578
- /** 校验 Bearer key:requireApiKey 关 → 放行;开 → 库内启用 key 或 TW2A_API_KEY env。 */
2635
+ /** 校验 Bearer key:requireApiKey 关 → 放行;开 → 必须是库内启用的 key。 */
2579
2636
  verify(bearer) {
2580
2637
  if (!this.require) return true;
2581
2638
  if (bearer === void 0 || bearer === "") return false;
2582
- if (this.keys.some((k) => k.isActive && k.key === bearer)) return true;
2583
- const envKey = process.env[API_KEY_REF];
2584
- return envKey !== void 0 && envKey !== "" && envKey === bearer;
2639
+ return this.keys.some((k) => k.isActive && k.key === bearer);
2585
2640
  }
2586
2641
  };
2587
2642
  //#endregion
@@ -2667,10 +2722,17 @@ function wrapModule(instance, env, source, kind = "external") {
2667
2722
  icon: instance.icon,
2668
2723
  status: () => {
2669
2724
  const now = instance.status();
2725
+ const accounts = now.accounts.map((a) => {
2726
+ const alias = env.store.getAccountName(instance.id, a.uid);
2727
+ return alias === void 0 || alias === a.nickname ? a : {
2728
+ ...a,
2729
+ nickname: alias
2730
+ };
2731
+ });
2670
2732
  return {
2671
2733
  id: now.id,
2672
2734
  name: now.name,
2673
- accounts: pool.decorate(hydrateCredits(instance.id, env.store, now.accounts))
2735
+ accounts: pool.decorate(hydrateCredits(instance.id, env.store, accounts))
2674
2736
  };
2675
2737
  },
2676
2738
  listModels: (force) => instance.listModels(force),
@@ -2687,7 +2749,7 @@ function wrapModule(instance, env, source, kind = "external") {
2687
2749
  getAlias: () => env.store.get(instance.id).alias || instance.id,
2688
2750
  customModelIds: () => [...env.store.get(instance.id).custom],
2689
2751
  accounts: () => instance.status().accounts,
2690
- chatOnce: (uid, req) => instance.chatOnce(uid, req),
2752
+ chatOnce: (uid, lv, req) => instance.chatOnce(uid, lv, req),
2691
2753
  removeLink: isFn(instance.removeLink) ? (uid) => instance.removeLink(uid) : void 0,
2692
2754
  dispose: () => instance.dispose(),
2693
2755
  pool
@@ -3220,9 +3282,9 @@ function readBody(req, limit) {
3220
3282
  function apply(rawContext) {
3221
3283
  const ctx = rawContext;
3222
3284
  const log = (msg) => ctx.logger.info(`[dsh-router] ${msg}`);
3223
- const stateFile = process.env.TW2A_STATE_FILE ?? "data/state.json";
3285
+ const stateFile = "data/state.json";
3224
3286
  const store = new SupplierConfigStore(stateFile);
3225
- const credentials = new CredentialStore(process.env.TW2A_AUTH_DIR ?? join(dirname(stateFile), "auths"));
3287
+ const credentials = new CredentialStore(join(dirname(stateFile), "auths"));
3226
3288
  const router = new Router(stateFile, store, log);
3227
3289
  const keys = new KeysStore(stateFile);
3228
3290
  const disposers = [];
@@ -3300,7 +3362,7 @@ function apply(rawContext) {
3300
3362
  });
3301
3363
  (async () => {
3302
3364
  try {
3303
- const userDir = join(process.env.DSH_PROFILE_DIR ?? join(process.env.HOME ?? "", ".dsh", "profiles", "web"), "suppliers");
3365
+ const userDir = join(join(process.env.HOME ?? "", ".dsh", "profiles", "web"), "suppliers");
3304
3366
  const { suppliers, errors } = await loadSuppliers({
3305
3367
  builtinDir: join(import.meta.dirname, "suppliers"),
3306
3368
  userDir,
@@ -3360,10 +3422,12 @@ function apply(rawContext) {
3360
3422
  } });
3361
3423
  return;
3362
3424
  }
3425
+ const lv = typeof peek.reasoning_effort === "string" && peek.reasoning_effort !== "" ? peek.reasoning_effort : "auto";
3363
3426
  await router.chatCompletions({
3364
3427
  rawBody: body,
3365
3428
  stream: !!peek.stream,
3366
- model: typeof peek.model === "string" ? peek.model : ""
3429
+ model: typeof peek.model === "string" ? peek.model : "",
3430
+ lv
3367
3431
  }, res);
3368
3432
  }));
3369
3433
  route(`${ROUTER_API_BASE}/health`, (_req, res) => {
@@ -3392,6 +3456,27 @@ function apply(rawContext) {
3392
3456
  })))
3393
3457
  });
3394
3458
  });
3459
+ route(`${ROUTER_API_BASE}/status/rename`, async (req, res) => {
3460
+ let body;
3461
+ try {
3462
+ body = JSON.parse(await readBody(req, 65536));
3463
+ } catch {
3464
+ writeJson(res, 400, {
3465
+ ok: false,
3466
+ error: "invalid JSON body"
3467
+ });
3468
+ return;
3469
+ }
3470
+ if (typeof body.supplier !== "string" || body.supplier === "" || typeof body.uid !== "string" || body.uid === "") {
3471
+ writeJson(res, 400, {
3472
+ ok: false,
3473
+ error: "supplier and uid are required"
3474
+ });
3475
+ return;
3476
+ }
3477
+ store.setAccountName(body.supplier, body.uid, typeof body.name === "string" ? body.name : "");
3478
+ writeJson(res, 200, { ok: true });
3479
+ });
3395
3480
  route(`${ROUTER_API_BASE}/models`, async (_req, res) => {
3396
3481
  try {
3397
3482
  writeJson(res, 200, {
@@ -260,7 +260,7 @@ function factory(env) {
260
260
  return true;
261
261
  },
262
262
  /** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
263
- async chatOnce(uid, req) {
263
+ async chatOnce(uid, lv, req) {
264
264
  const base = stripAlias(req.model, currentAlias());
265
265
  if (!(await allModels(false)).some((m) => m.id === base)) return {
266
266
  ok: false,
@@ -277,6 +277,11 @@ function factory(env) {
277
277
  try {
278
278
  const obj = JSON.parse(body);
279
279
  obj.model = base;
280
+ if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
281
+ else if (lv === "none" || lv === "off") {
282
+ delete obj.reasoning_effort;
283
+ delete obj.reasoning_summary;
284
+ }
280
285
  body = JSON.stringify(obj);
281
286
  } catch {}
282
287
  let upstream;
@@ -64,7 +64,7 @@ function factory(env) {
64
64
  }),
65
65
  listModels: (force) => allModels(!!force),
66
66
  /** 无账号供应商:uid 恒为空,只认 free 模型。 */
67
- async chatOnce(_uid, req) {
67
+ async chatOnce(_uid, lv, req) {
68
68
  if (!isFreeModel(req.model)) return {
69
69
  ok: false,
70
70
  state: "no_such_model",
@@ -74,6 +74,11 @@ function factory(env) {
74
74
  try {
75
75
  const obj = JSON.parse(body);
76
76
  obj.model = stripAlias(String(obj.model ?? req.model), currentAlias());
77
+ if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
78
+ else if (lv === "none" || lv === "off") {
79
+ delete obj.reasoning_effort;
80
+ delete obj.reasoning_summary;
81
+ }
77
82
  body = JSON.stringify(obj);
78
83
  } catch {}
79
84
  let upstream;
@@ -142,7 +142,7 @@ function factory(env) {
142
142
  return true;
143
143
  },
144
144
  /** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
145
- async chatOnce(uid, req) {
145
+ async chatOnce(uid, lv, req) {
146
146
  const base = stripAlias(req.model, currentAlias());
147
147
  if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) return {
148
148
  ok: false,
@@ -159,6 +159,11 @@ function factory(env) {
159
159
  try {
160
160
  const obj = JSON.parse(body);
161
161
  obj.model = base;
162
+ if (lv !== "auto" && lv !== "" && lv !== "none" && lv !== "off") obj.reasoning_effort = lv;
163
+ else if (lv === "none" || lv === "off") {
164
+ delete obj.reasoning_effort;
165
+ delete obj.reasoning_summary;
166
+ }
162
167
  body = JSON.stringify(obj);
163
168
  } catch {}
164
169
  let upstream;
@@ -34,6 +34,6 @@ export declare class KeysStore {
34
34
  /** requireApiKey 开关。 */
35
35
  get requireApiKey(): boolean;
36
36
  set requireApiKey(v: boolean);
37
- /** 校验 Bearer key:requireApiKey 关 → 放行;开 → 库内启用 key 或 TW2A_API_KEY env。 */
37
+ /** 校验 Bearer key:requireApiKey 关 → 放行;开 → 必须是库内启用的 key。 */
38
38
  verify(bearer: string | undefined): boolean;
39
39
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 纯文本流:组合为文本模型,本 adapter 不处理图片附件。
8
8
  */
9
- import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
9
+ import { LlmAdapter, type GenerateOptions, type LlmModelInfo, type LlmResolvedModelInfo, type StreamChunk } from '@deepseek-ai/dsh-llm';
10
10
  /** 模型目录来源:组合。 */
11
11
  export interface RouterAdapterSource {
12
12
  comboModels: () => Promise<Array<{
@@ -22,10 +22,6 @@ export interface RouterAdapterSource {
22
22
  * 用 `for await` 是因为它天然兼容两者 —— 不必为流式再写一份。
23
23
  */
24
24
  export declare function translateSse(payloads: AsyncIterable<string> | Iterable<string>): AsyncGenerator<StreamChunk>;
25
- /**
26
- * DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
27
- * /v1/chat/completions(组合路由在 /v1 内完成)。
28
- */
29
25
  export declare class RouterAdapter extends LlmAdapter {
30
26
  private readonly baseURL;
31
27
  private readonly source;
@@ -35,5 +31,6 @@ export declare class RouterAdapter extends LlmAdapter {
35
31
  name: string;
36
32
  };
37
33
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
34
+ resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
38
35
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
39
36
  }
@@ -48,6 +48,8 @@ export interface ChatRequest {
48
48
  rawBody: string;
49
49
  stream: boolean;
50
50
  model: string;
51
+ /** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
52
+ lv?: string;
51
53
  }
52
54
  /**
53
55
  * 供应商接口。核心负责账号池(选号/冷却/禁用/错误累计/遍历回退),
@@ -71,7 +73,7 @@ export interface Supplier {
71
73
  /** 账号「现在状态」(从 status() 派生,插件只报它观察到的)。空 = 无账号供应商。 */
72
74
  accounts(): SupplierAccountNow[];
73
75
  /** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
74
- chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
76
+ chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
75
77
  /** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
76
78
  removeLink?(uid: string): Promise<boolean>;
77
79
  dispose(): void;
@@ -14,8 +14,6 @@ export declare const ROUTER_ACTIVE_ATTR = "data-dsh-router-active";
14
14
  export declare const ROUTER_TITLE = "\u8DEF\u7531";
15
15
  /** Same-origin API base the client panel fetches (host half serves it). */
16
16
  export declare const ROUTER_API_BASE = "/router/api";
17
- /** Bearer key env / credential reference protecting /v1/*. */
18
- export declare const API_KEY_REF = "TW2A_API_KEY";
19
17
  /** One account row from `/router/api/status`. */
20
18
  export interface RouterAccount {
21
19
  uid: string;
@@ -16,6 +16,8 @@ export interface SupplierConfig {
16
16
  poolStrategy: PoolStrategy;
17
17
  /** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
18
18
  credits: Record<string, number>;
19
+ /** 每连接显示别名:uid → 显示名(空/缺省用供应商原始昵称,纯展示不改路由键)。 */
20
+ accountNames: Record<string, string>;
19
21
  }
20
22
  /** 通用供应商配置存储。 */
21
23
  export declare class SupplierConfigStore {
@@ -45,6 +47,10 @@ export declare class SupplierConfigStore {
45
47
  removeCustomModel(supplierId: string, modelId: string): void;
46
48
  setPoolOrder(supplierId: string, uids: string[]): void;
47
49
  setPoolStrategy(supplierId: string, strategy: string): void;
50
+ /** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
51
+ getAccountName(supplierId: string, uid: string): string | undefined;
52
+ /** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
53
+ setAccountName(supplierId: string, uid: string, name: string): void;
48
54
  /**
49
55
  * 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
50
56
  * 显示什么——不要把 -1 当 0 展示。
@@ -102,10 +102,14 @@ export interface SupplierModule {
102
102
  * 选号/回退/健康判定全是核心的活。
103
103
  * 无账号供应商(如 opencode)传空 uid,忽略即可。
104
104
  *
105
+ * `lv` 是推理等级(reasoning effort),`'auto'` 表示不显式下发;
106
+ * 供应商各自决定怎么把 `lv` 映射到上游(OpenAI 兼容一般是
107
+ * `reasoning_effort` 字段)。
108
+ *
105
109
  * 失败原因通过返回值的 `message` 报(核心在测试模型时直接用它做诊断提示),
106
110
  * 插件不必自己存一份「上次失败」的状态。
107
111
  */
108
- chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
112
+ chatOnce(uid: string, lv: string, req: ChatRequest): Promise<ChatOnceResult>;
109
113
  dispose(): void;
110
114
  /** 生成登录链接(加账号)。有它才显示连接池(账号池)。 */
111
115
  generateLoginUrl?(): string | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -34,7 +34,6 @@
34
34
  "react",
35
35
  "react-dom",
36
36
  "cordis",
37
- "@deepseek-ai/dsh-client-runtime",
38
37
  "@deepseek-ai/dsh-client-ui-slots",
39
38
  "@deepseek-ai/dsh-client-locale"
40
39
  ],
@@ -67,10 +66,9 @@
67
66
  "license": "MIT",
68
67
  "peerDependencies": {
69
68
  "@deepseek-ai/cordis": "^4.0.2",
70
- "@deepseek-ai/dsh-client-locale": "*",
71
- "@deepseek-ai/dsh-client-runtime": "*",
72
- "@deepseek-ai/dsh-client-ui-slots": "*",
73
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
71
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
74
72
  "cordis": "^4.0.0-rc.7",
75
73
  "react": "^18.2.0",
76
74
  "react-dom": "^18.2.0"
@@ -82,24 +80,22 @@
82
80
  "@deepseek-ai/dsh-client-locale": {
83
81
  "optional": true
84
82
  },
85
- "@deepseek-ai/dsh-client-runtime": {
86
- "optional": true
87
- },
88
83
  "@deepseek-ai/dsh-client-ui-slots": {
89
84
  "optional": true
90
85
  }
91
86
  },
92
87
  "devDependencies": {
93
- "@types/node": "^24.13.3",
94
- "@types/react": "~18.3.31",
95
- "@types/react-dom": "~18.3.7",
96
- "cordis": "^4.0.0-rc.9",
97
- "react": "^18.3.1",
98
- "react-dom": "18.2.0",
99
- "tsdown": "^0.22.14",
100
- "typescript": "^5.9.3",
101
- "vite": "8.1.3",
102
- "vite-plugin-css-injected-by-js": "^3.5.2"
88
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
89
+ "@types/node": "^26.4.1",
90
+ "@types/react": "~19.2.18",
91
+ "@types/react-dom": "~19.2.7",
92
+ "cordis": "4.0.0-rc.9",
93
+ "react": "^19.2.8",
94
+ "react-dom": "19.2.8",
95
+ "tsdown": "^0.23.0",
96
+ "typescript": "^7.0.2",
97
+ "vite": "8.2.2",
98
+ "vite-plugin-css-injected-by-js": "^5.0.2"
103
99
  },
104
100
  "publishConfig": {
105
101
  "registry": "https://registry.npmjs.org"
@@ -112,6 +112,8 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
112
112
  const [checkingIn, setCheckingIn] = useState(false)
113
113
  const [refreshing, setRefreshing] = useState(false)
114
114
  const [dragIndex, setDragIndex] = useState<number | null>(null)
115
+ const [renameUid, setRenameUid] = useState<string | null>(null)
116
+ const [renameDraft, setRenameDraft] = useState('')
115
117
  const mounted = useRef(true)
116
118
 
117
119
  useEffect(() => {
@@ -343,6 +345,33 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
343
345
  setRemoveTarget(null)
344
346
  }
345
347
 
348
+ const startRename = (account: RouterAccount): void => {
349
+ setRenameDraft(account.nickname ?? '')
350
+ setRenameUid(account.uid)
351
+ }
352
+
353
+ const saveRename = async (account: RouterAccount): Promise<void> => {
354
+ try {
355
+ const response = await fetch(`${ROUTER_API_BASE}/status/rename`, {
356
+ method: 'POST',
357
+ headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify({ supplier: supplier.id, uid: account.uid, name: renameDraft.trim() }),
359
+ cache: 'no-store',
360
+ })
361
+ if (!response.ok) {
362
+ showToast(`保存失败:HTTP ${response.status}`)
363
+ setRenameUid(null)
364
+ return
365
+ }
366
+ onRefresh()
367
+ showToast('连接名已保存')
368
+ } catch (err) {
369
+ showToast(`保存失败:${(err as Error).message}`)
370
+ } finally {
371
+ setRenameUid(null)
372
+ }
373
+ }
374
+
346
375
  const toggleModel = async (id: string, enabled: boolean): Promise<void> => {
347
376
  setToggling(true)
348
377
  try {
@@ -644,7 +673,7 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
644
673
  <div
645
674
  key={account.uid}
646
675
  className={`dshr-linkRow${dragIndex === index ? ' dshr-linkRowDragging' : ''}`}
647
- draggable
676
+ draggable={renameUid !== account.uid}
648
677
  onDragStart={(e) => { setDragIndex(index); e.dataTransfer.effectAllowed = 'move' }}
649
678
  onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move' }}
650
679
  onDrop={(e) => {
@@ -667,7 +696,20 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
667
696
  </div>
668
697
  <div className="dshr-linkMain">
669
698
  <div className="dshr-linkTitleLine">
670
- <span className="dshr-linkName">{account.nickname || account.uid}</span>
699
+ {renameUid === account.uid
700
+ ? (
701
+ <input
702
+ className="dshr-input dshr-mono"
703
+ value={renameDraft}
704
+ autoFocus
705
+ onChange={(e) => setRenameDraft(e.target.value)}
706
+ onKeyDown={(e) => {
707
+ if (e.key === 'Enter') void saveRename(account)
708
+ if (e.key === 'Escape') setRenameUid(null)
709
+ }}
710
+ />
711
+ )
712
+ : <span className="dshr-linkName">{account.nickname || account.uid}</span>}
671
713
  <span className={`dshr-badge ${badge.cls}`}>{badge.text}</span>
672
714
  </div>
673
715
  <div className="dshr-linkSub">
@@ -689,6 +731,17 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
689
731
  </div>
690
732
  </div>
691
733
  <div className="dshr-linkOps">
734
+ <button
735
+ type="button"
736
+ className={`dshr-iconBtn dshr-iconBtn-sm dshr-linkEdit${renameUid === account.uid ? ' dshr-linkEdit-active' : ''}`}
737
+ title={renameUid === account.uid ? '保存名字' : '改名'}
738
+ onClick={() => {
739
+ if (renameUid === account.uid) void saveRename(account)
740
+ else startRename(account)
741
+ }}
742
+ >
743
+ <Icon d={renameUid === account.uid ? I.check : I.edit} size={15} />
744
+ </button>
692
745
  <button type="button" className="dshr-iconBtn dshr-iconBtn-sm dshr-linkDelete" title="删除链接" onClick={() => setRemoveTarget(account)}>
693
746
  <Icon d={I.delete} size={15} />
694
747
  </button>
@@ -0,0 +1,7 @@
1
+ // React 19 类型不再提供全局 `JSX` 命名空间,声明回退到 react 的 JSX 命名空间。
2
+ declare namespace JSX {
3
+ type Element = import('react').JSX.Element
4
+ }
5
+
6
+ // CSS 副作用导入的模块声明(vite 产物内联,仅静态类型需要)。
7
+ declare module '*.css'
@@ -1634,6 +1634,20 @@ html[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-
1634
1634
  background: color-mix(in srgb, var(--rs-danger) 14%, transparent);
1635
1635
  }
1636
1636
 
1637
+ /* 改名:与删除一致,hover 行才显示;编辑中保持可见(否则保存按钮被藏住) */
1638
+ .dshr-linkEdit {
1639
+ color: var(--rs-muted);
1640
+ opacity: 0;
1641
+ transition: opacity .12s ease, color .12s ease, background .12s ease;
1642
+ }
1643
+ .dshr-linkRow:hover .dshr-linkEdit { opacity: 1; }
1644
+ .dshr-linkEdit:focus-visible { opacity: 1; }
1645
+ .dshr-linkEdit:hover {
1646
+ color: var(--rs-accent);
1647
+ background: color-mix(in srgb, var(--rs-accent) 14%, transparent);
1648
+ }
1649
+ .dshr-linkEdit.dshr-linkEdit-active { opacity: 1; }
1650
+
1637
1651
  /* 模型 chips(9router ModelRow 风格) */
1638
1652
  .dshr-modelChips {
1639
1653
  display: flex;