dsh-router-core 0.4.4 → 0.4.6

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.
Files changed (147) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/cordis.patch.yml +0 -0
  4. package/docs/ext.md +0 -0
  5. package/docs/pool-sticky-block.md +59 -1
  6. package/docs/screenshot.png +0 -0
  7. package/docs/suppliers.md +22 -3
  8. package/lib/client-registry.js +24 -14
  9. package/lib/client.js +24 -14
  10. package/lib/ext-prompt/locale/en.json +0 -0
  11. package/lib/ext-prompt/locale/zh.json +0 -0
  12. package/lib/ext-prompt/package.json +0 -0
  13. package/lib/ext-test/locale/en.json +0 -0
  14. package/lib/ext-test/locale/zh.json +0 -0
  15. package/lib/ext-test/package.json +0 -0
  16. package/lib/index.js +101 -15
  17. package/lib/suppliers/nvidia/locale/en.json +0 -0
  18. package/lib/suppliers/nvidia/locale/zh.json +0 -0
  19. package/lib/suppliers/nvidia/package.json +0 -0
  20. package/lib/suppliers/opencode/index.js +58 -30
  21. package/lib/suppliers/opencode/locale/en.json +0 -0
  22. package/lib/suppliers/opencode/locale/zh.json +0 -0
  23. package/lib/suppliers/opencode/package.json +0 -0
  24. package/lib/suppliers/openrouter/locale/en.json +0 -0
  25. package/lib/suppliers/openrouter/locale/zh.json +0 -0
  26. package/lib/suppliers/openrouter/package.json +0 -0
  27. package/lib/types/router/account-pool.d.ts +30 -3
  28. package/lib/types/router/index.d.ts +25 -0
  29. package/lib/types/router/types.d.ts +16 -4
  30. package/lib/types/router/usage-store.d.ts +4 -2
  31. package/lib/types/shared.d.ts +19 -0
  32. package/lib/types/suppliers/contract.d.ts +7 -1
  33. package/package.json +1 -1
  34. package/src/client/CheckinCard.tsx +0 -0
  35. package/src/client/CombosTab.tsx +0 -0
  36. package/src/client/EndpointTab.tsx +0 -0
  37. package/src/client/ExtDetail.tsx +0 -0
  38. package/src/client/ExtTab.tsx +0 -0
  39. package/src/client/ExtTestPanel.tsx +0 -0
  40. package/src/client/LastHitDock.tsx +0 -0
  41. package/src/client/Modal.tsx +0 -0
  42. package/src/client/RouterComponentsSection.tsx +0 -0
  43. package/src/client/RouterView.tsx +0 -0
  44. package/src/client/StatsTab.tsx +0 -0
  45. package/src/client/SupplierDetail.tsx +1 -5
  46. package/src/client/account-badge.test.ts +37 -0
  47. package/src/client/account-badge.ts +24 -0
  48. package/src/client/comp-button-scope.test.ts +0 -0
  49. package/src/client/comp-panel-layout.test.ts +0 -0
  50. package/src/client/env.d.ts +0 -0
  51. package/src/client/ext-panels.test.ts +0 -0
  52. package/src/client/ext-panels.ts +0 -0
  53. package/src/client/index.tsx +0 -0
  54. package/src/client/last-hit-store.ts +0 -0
  55. package/src/client/model-choice.test.ts +0 -0
  56. package/src/client/model-choice.ts +0 -0
  57. package/src/client/model-hint-copy.test.ts +0 -0
  58. package/src/client/model-hint-copy.ts +0 -0
  59. package/src/client/router-components.test.ts +0 -0
  60. package/src/client/router-components.ts +0 -0
  61. package/src/client/router.css +0 -0
  62. package/src/client/settings-nav-icon.ts +0 -0
  63. package/src/client/settings-section.tsx +0 -0
  64. package/src/client/sidebar-entry.ts +0 -0
  65. package/src/client/supplier-visibility.test.ts +0 -0
  66. package/src/client/supplier-visibility.ts +0 -0
  67. package/src/client/workspace-controller.ts +0 -0
  68. package/src/client/workspace-mount.tsx +0 -0
  69. package/src/credential-store.ts +0 -0
  70. package/src/data-dir.test.ts +0 -0
  71. package/src/data-dir.ts +0 -0
  72. package/src/ext/contract.ts +0 -0
  73. package/src/ext/store.test.ts +0 -0
  74. package/src/ext/store.ts +0 -0
  75. package/src/ext-prompt/assembly.test.ts +0 -0
  76. package/src/ext-prompt/content.ts +0 -0
  77. package/src/ext-prompt/ext-prompt.test.ts +0 -0
  78. package/src/ext-prompt/index.ts +0 -0
  79. package/src/ext-prompt/locale/en.json +0 -0
  80. package/src/ext-prompt/locale/zh.json +0 -0
  81. package/src/ext-prompt/logo.svg +0 -0
  82. package/src/ext-prompt/logo.ts +0 -0
  83. package/src/ext-prompt/mount.test.ts +0 -0
  84. package/src/ext-prompt/mount.ts +0 -0
  85. package/src/ext-prompt/plugin.ts +0 -0
  86. package/src/ext-prompt/render.test.ts +0 -0
  87. package/src/ext-prompt/render.ts +0 -0
  88. package/src/ext-prompt/row.json +0 -0
  89. package/src/ext-route.test.ts +0 -0
  90. package/src/ext-test/ext-test.test.ts +0 -0
  91. package/src/ext-test/index.ts +0 -0
  92. package/src/ext-test/locale/en.json +0 -0
  93. package/src/ext-test/locale/zh.json +0 -0
  94. package/src/ext-test/plugin.ts +0 -0
  95. package/src/ext-test/row.json +0 -0
  96. package/src/host-base-url.test.ts +0 -0
  97. package/src/host-base-url.ts +0 -0
  98. package/src/host-version.test.ts +0 -0
  99. package/src/host-version.ts +0 -0
  100. package/src/index.ts +20 -6
  101. package/src/keys.ts +0 -0
  102. package/src/llm/adapter.test.ts +54 -0
  103. package/src/llm/adapter.ts +24 -2
  104. package/src/router/account-pool.test.ts +109 -0
  105. package/src/router/account-pool.ts +52 -9
  106. package/src/router/combo-change.test.ts +125 -0
  107. package/src/router/disconnect-leak.test.ts +0 -0
  108. package/src/router/first-byte-budget.test.ts +0 -0
  109. package/src/router/index.ts +44 -4
  110. package/src/router/models-cache.test.ts +0 -0
  111. package/src/router/precise-dispatch.test.ts +0 -0
  112. package/src/router/prefix-affinity.ts +0 -0
  113. package/src/router/supplier-switch.test.ts +91 -0
  114. package/src/router/types.ts +16 -4
  115. package/src/router/usage-store.ts +4 -2
  116. package/src/router/usage-tokens.ts +0 -0
  117. package/src/router/usage.test.ts +0 -0
  118. package/src/shared.test.ts +35 -0
  119. package/src/shared.ts +28 -0
  120. package/src/supplier-config.test.ts +0 -0
  121. package/src/supplier-config.ts +0 -0
  122. package/src/suppliers/contract.ts +7 -2
  123. package/src/suppliers/http-state.test.ts +0 -0
  124. package/src/suppliers/http-state.ts +0 -0
  125. package/src/suppliers/loader.test.ts +53 -0
  126. package/src/suppliers/loader.ts +0 -0
  127. package/src/suppliers/nvidia/index.ts +0 -0
  128. package/src/suppliers/nvidia/locale/en.json +0 -0
  129. package/src/suppliers/nvidia/locale/zh.json +0 -0
  130. package/src/suppliers/nvidia/plugin.ts +0 -0
  131. package/src/suppliers/nvidia/row.json +0 -0
  132. package/src/suppliers/opencode/index.ts +0 -0
  133. package/src/suppliers/opencode/locale/en.json +0 -0
  134. package/src/suppliers/opencode/locale/zh.json +0 -0
  135. package/src/suppliers/opencode/plugin.test.ts +79 -25
  136. package/src/suppliers/opencode/plugin.ts +60 -29
  137. package/src/suppliers/opencode/row.json +0 -0
  138. package/src/suppliers/openrouter/index.ts +0 -0
  139. package/src/suppliers/openrouter/locale/en.json +0 -0
  140. package/src/suppliers/openrouter/locale/zh.json +0 -0
  141. package/src/suppliers/openrouter/plugin.ts +0 -0
  142. package/src/suppliers/openrouter/row.json +0 -0
  143. package/src/suppliers/probe.test.ts +0 -0
  144. package/src/suppliers/probe.ts +0 -0
  145. package/src/suppliers/registry.test.ts +0 -0
  146. package/src/suppliers/registry.ts +0 -0
  147. package/src/types.d.ts +0 -0
@@ -2,46 +2,70 @@ import { n as registerSupplierRow, t as stateFromHttpStatus } from "../../http-s
2
2
  import { randomBytes } from "node:crypto";
3
3
  //#region src/suppliers/opencode/plugin.ts
4
4
  /**
5
- * opencode 供应商插件 —— 参考 9Router(open-sse) 的 opencode free 实现。
5
+ * opencode 供应商插件 —— OpenCode Zen 免费档(匿名 `Bearer public` 通道)。
6
6
  *
7
- * 上游:https://opencode.ai/zen/v1(OpenAI 兼容,免费档)
8
- * - chat: POST /zen/v1/chat/completions(**只能流式**)
9
- * - models: GET /zen/v1/models(透传,只取免费模型)
7
+ * 上游(2026-10-15 实测):
8
+ * - chat: POST https://opencode.ai/inference/v1/chat/completions(**只能流式**)
9
+ * - models: GET https://opencode.ai/inference/v1/models(两个端点都通,取免费模型)
10
10
  *
11
- * 「免费」不等于「无鉴权」:Zen 免费档仍要求**模拟真 CLI 的握手**,否则一律
12
- * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`
13
- * (2026-09-22 实测)。三个条件缺一即 403,见下方 `UA` / `sessionId` / `decoyTools`。
14
- * 免费档也没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
11
+ * ⚠️ `/zen/v1/chat/completions` **已废弃**:2026-10-15 全矩阵实测该端点对任何
12
+ * 头部/工具组合一律 403,迁到 `/inference/v1` 后同样请求立刻 200。旧的
13
+ * `zen/v1` 常量是本次故障的根因之一,别再改回去。
14
+ * 注:2026-10-16 复核,`/zen/v1/chat/completions` 在握手完全正确时也能 200
15
+ * (即"一律 403"仅对当时的随机 hex session 成立);两个端点的差异对正确握手
16
+ * 不可见。`/inference/v1` 仍是**实测更稳**的那个,保留不回退。
17
+ *
18
+ * 「免费」不等于「无鉴权」:仍要求**模拟真 CLI 的握手**,否则一律
19
+ * 403 `FreeTierError: OpenCode's free tier can only be used from within OpenCode`。
20
+ * 2026-10-15 逐项消融后,真正的门只剩两条(其余全不敏感):
21
+ *
22
+ * 1. `tools` 里必须存在一个**小写精确名为 `read`** 的工具。
23
+ * —— `parameters` 完全不看(实测 `properties:{}`、无 `parameters`、
24
+ * `parameters:null` 全部 200);但名字大小写敏感(`READ` → 403),且
25
+ * `bash`/`glob`/`write` 等**不能替代** `read`(各自单独都给 403)。
26
+ * 2. `x-opencode-session` 的 12 位 hex 前缀必须是**倒序时间戳**,且其低 12 位
27
+ * (counter)落在 `[0,63]`(见 `newSessionId`)。
28
+ *
29
+ * 已实测**不敏感**(改了照样 200,故不写死):UA 具体版本(`opencode/1.18.31`
30
+ * 与 `opencode/latest/2.0.29/cli` 等价)、`x-opencode-client` 的 `cli`/`desktop`、
31
+ * `x-opencode-request` 有无、`x-opencode-project` 是否 40-hex、工具 `description`。
32
+ *
33
+ * 免费档没有账号可填 —— 所以无凭证/连接池/签到,面板只显示模型卡片
15
34
  * (连接池、签到由能力检测自动隐藏)。
16
35
  */
17
36
  const id = "opencode";
18
37
  const name$1 = "OpenCode";
19
38
  /** 面板图标。 */
20
39
  const icon = "data:image/png;base64,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";
21
- const BASE = "https://opencode.ai/zen/v1";
40
+ /** 上游免费档的推理端点。`/zen/v1` 已于 2026-10-15 废弃(全组合 403)。 */
41
+ const BASE = "https://opencode.ai/inference/v1";
22
42
  const MODELS_URL = `${BASE}/models`;
23
43
  const CHAT_URL = `${BASE}/chat/completions`;
24
44
  /**
25
- * User-Agent 必须带**版本号**且 ≥ 1.18。
26
- *
27
- * 裸 `opencode` → 403 FreeTierError;`opencode/1.17.x` → **426 UpgradeRequired**
28
- * (上游明确要求升级客户端);`opencode/1.18.0` 起通过。别去掉版本号,
29
- * 也别写死成能过的最高版本——取一个已实测通过的稳定值即可。
45
+ * User-Agent。**不是**门禁(实测 `opencode/1.18.31` 与真 CLI 的
46
+ * `opencode/1.19.0 ai-sdk/...` 等价,裸 `opencode` 也不影响本门)。
47
+ * 保留一个带版本号的值只是别让上游按非 CLI 流量处理。
30
48
  */
31
49
  const UA = "opencode/1.18.31";
32
50
  /**
33
- * 免费档按 session 记账(每次生成一个新的即可——见 `chatOnce` 的实测说明),
34
- * `x-opencode-session` / `x-opencode-request` **必须**是上游认的形状,格式不对
35
- * 同样 403(不是忽略,是拒绝):
36
- * ses_<12 位小写 hex><14 位 base62>、msg_<同形状>
51
+ * 造上游认的 session/request id。
52
+ *
53
+ * ⚠️ 12 位 hex 前缀**不是任意 hex,也不是摘要派生**,它是真客户端的
54
+ * `~(Date.now() * 0x1000 + counter)`:倒序时间戳。2026-10-15 实测(逐项消融):
55
+ * - 倒序时间戳 counter=0/1/62/63 → 200;counter=64/65/100 → 403
56
+ * - 纯随机 hex → 403;sha256 派生 hex → 403(9router #4780 的成因)
57
+ * 即上游会把低 12 位当每毫秒计数器读,只接受 `[0,63]`;随机/派生值落进该窗口
58
+ * 只有约 1.6% 概率,所以旧实现几乎必然 403。(2026-10-16 每个边界值各打 3 次
59
+ * 复核:63 三次全 200、64 三次全 403,边界就是 63/64 之间。)
60
+ *
61
+ * counter 固定取 1:本插件是自己 session 的唯一写入方,做每毫秒计数器只增加状态。
37
62
  */
38
63
  function newSessionId(prefix) {
39
64
  const B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
40
- const bytes = randomBytes(20);
41
- let time = "";
42
- for (let i = 0; i < 6; i++) time += bytes[i].toString(16).padStart(2, "0");
65
+ const time = (~(BigInt(Date.now()) * 4096n + 1n) & 281474976710655n).toString(16).padStart(12, "0");
66
+ const bytes = randomBytes(14);
43
67
  let rand = "";
44
- for (let i = 6; i < 20; i++) rand += B62[bytes[i] % 62] ?? "";
68
+ for (let i = 0; i < 14; i++) rand += B62[bytes[i] % 62] ?? "";
45
69
  return `${prefix}${time}${rand}`;
46
70
  }
47
71
  /**
@@ -58,17 +82,21 @@ function sessionHeaders() {
58
82
  };
59
83
  }
60
84
  /**
61
- * 免费档要求 tools 里**同时**存在 `bash` 和 `read`,否则 403。
85
+ * 免费档要求的占位工具:**只需要 `read` 一个**,小写精确。
62
86
  *
63
- * 为什么是「注入」而不是「覆盖」:上游只是**校验这两个名字在不在**,真正的
64
- * 工具照常工作(实测真实工具在前、占位在后,混合下发 200)。所以调用方已经
65
- * 带了同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的定义,
66
- * 否则会把真实工具签名糊掉。
87
+ * 2026-10-15 逐项消融实测:
88
+ * - `read` 单独 → 200;`bash` 单独 → 403;`glob`/`write`/`shell` 单独 → 403
89
+ * - `read` + 任意其他(含空 schema 的 bash/glob/grep…)→ 200
90
+ * - 把 `read` 改名成 `READ` → 403(大小写敏感)
91
+ * - `read` 的 `parameters` 完全不看:`properties:{}`、无 `parameters`、
92
+ * `parameters:null`、`parameters:{}` 全部 200
93
+ *
94
+ * 所以旧版「必须同时有 bash 和 read」是错的——**`bash` 不是门,`read` 才是**。
95
+ * 为什么是「注入」而不是「覆盖」:上游只校验名字在不在,真实工具照常工作,
96
+ * 所以调用方已带同名工具时保持原样不动,缺哪个补哪个——绝不覆盖调用方自己的
97
+ * 定义,否则会把真实工具签名糊掉。
67
98
  */
68
99
  const DECOY_TOOLS = [{
69
- name: "bash",
70
- description: "This tool is currently unavailable and must not be used."
71
- }, {
72
100
  name: "read",
73
101
  description: "This tool is currently unavailable and must not be used."
74
102
  }];
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -66,6 +66,22 @@ export declare class AccountPool {
66
66
  * - 且此 (supplier, model, uid) 冷却单元不在冷却中。
67
67
  */
68
68
  private healthy;
69
+ /**
70
+ * 插件报的「现在状态」是否意味着这个号此刻不该被选中。
71
+ *
72
+ * 只认两种**账号级持久事实**(要靠外部动作才能恢复,不是抖动):
73
+ * - `quota` —— 额度/积分用完,充值才能恢复(traework 积分归零时报它)
74
+ * - `session_dead` —— 凭证失效,重新登录才能恢复
75
+ *
76
+ * 其余一律不看:`transport`/`unknown`/`unavailable` 是插件某一次采样的瞬时
77
+ * 观察,拿它排除等于「一次抖动就踢号」,而且没有冷却计时器兜底;`rate_limit`
78
+ * 的指数退避由核心的冷却单元管,比状态采样准。
79
+ *
80
+ * 这是「插件只报现在怎么了、核心决定怎么办」的分工落地:插件报出它观察到
81
+ * 的事实,核心决定此刻不选它。恢复同样自动——充值/重登后插件报回 `ok`,
82
+ * 该号立刻回到候选(没有额外状态要清)。
83
+ */
84
+ private servableByReport;
69
85
  /**
70
86
  * 为「某个模型」选一个健康账号 —— **会话亲和选号,无策略旋钮**。
71
87
  *
@@ -81,11 +97,17 @@ export declare class AccountPool {
81
97
  * "策略",是无会话身份时的替身。
82
98
  * 4. 块统计不驱动轮转(亲和已保证驻留),只保留 `BLOCK_MAX` 次不达标 →
83
99
  * 降权该号(缓存无效,可恢复)。
100
+ * 5. 插件报的 `quota`/`session_dead`(额度用完 / 凭证失效)→ **直接不选它**:
101
+ * 这类失败要靠充值/重登才能恢复,选它必是白撞一次(还有的供应商失败在
102
+ * 流已提交之后,这次请求根本救不回来)。见 `servableByReport`。
84
103
  *
85
104
  * 天花板:号数 < 并发会话数时必然有会话共用号 → 互踩,无法用选号手法消除
86
105
  * (缓存空间是账号侧物理限制)。见 docs/pool-sticky-block.md §10。
87
106
  *
88
- * @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)
107
+ * @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)。
108
+ * `state` 会被读:`quota`/`session_dead` 的号不进候选(见 `servableByReport`)。
109
+ * `credits` **不读** —— 不同供应商的 0 含义不同(openrouter/nvidia 的 0 是
110
+ * 「没有积分这个概念」),是否已用完由插件解读成 `state` 后报上来。
89
111
  * @param poolOrder 用户在面板拖出来的顺序(核心管)
90
112
  * @param modelId 当前要路由的模型(决定查哪个 (model, uid) 冷却单元)
91
113
  * @param messages 请求体的 messages(算亲和指纹的内容兜底)
@@ -145,8 +167,13 @@ export declare class AccountPool {
145
167
  dropBlock(modelId: string): void;
146
168
  /** 块状态键:与冷却同粒度 (supplier, model),不含 uid。 */
147
169
  private blockKey;
148
- /** 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。 */
149
- noteFailure(uid: string, modelId: string, state: AccountState, message: string): void;
170
+ /**
171
+ * 记录一次失败:按状态处置(冷却/退避),落在「(模型, 连接)」这个冷却单元上。
172
+ * `resetAt` = 上游声明的重置时刻(epoch ms):限流退避优先冷到该点——
173
+ * 频率配额是小时级的,5min 上限的指数退避只会到点回来再撞 6004,
174
+ * 把号轮流打冷(2026-10-15 的 11 号全灭事故)。
175
+ */
176
+ noteFailure(uid: string, modelId: string, state: AccountState, message: string, resetAt?: number): void;
150
177
  /** 记录一次成功:清零「该模型」的限流退避等级(好号不背历史惩罚)。 */
151
178
  noteSuccess(uid: string, modelId: string): void;
152
179
  /** 手动暂停整连接(面板/管理用途,跨模型)。ms<=0 表示解除。 */
@@ -36,10 +36,26 @@ export declare class Router {
36
36
  private log;
37
37
  /** 每条降级腿的首字节预算(ms)。只给测试一个入口,生产恒为 FIRST_BYTE_BUDGET_MS。 */
38
38
  private firstByteBudgetMs;
39
+ /**
40
+ * 组合增删改**成功提交后**的通知出口。
41
+ *
42
+ * 为什么必须有:模型选择器(`ui-model-selection`)的目录是**客户端缓存**的,
43
+ * 只在宿主转发过来的事件上失效重载(`llm/adapters-updated` /
44
+ * `settings/document-updated` / 凭据事件 / 重连)。而组合只是本插件
45
+ * `combos.json` 里的一份状态:改完名字若不发通知,选择器会一直拿着旧快照 ——
46
+ * 用户能看到的症状是「面板里明明有这个名字,模型选择器里却找不到、切不过去」,
47
+ * 且新建的组合永远不出现(2026-10-14 实测)。
48
+ *
49
+ * 通知与落盘分开({@link saveCombos} 先 {@link commitCombos} 落盘、再发通知):
50
+ * 落盘失败、或 `stateFile` 为空(单测/独立运行时)时内存已经改了,客户端的
51
+ * 状态同样过期,所以这两条路径都必须照发。
52
+ */
53
+ private onCombosChanged;
39
54
  /** 用量统计(概览看板)。 */
40
55
  usage: UsageStore;
41
56
  constructor(stateFile?: string, store?: SupplierConfigStore, log?: (msg: string) => void, opts?: {
42
57
  firstByteBudgetMs?: number;
58
+ onCombosChanged?: () => void;
43
59
  });
44
60
  /**
45
61
  * 请求追踪:每次 chat 记一行,回答「这个组合到底用的哪个模型和账号」。
@@ -48,7 +64,16 @@ export declare class Router {
48
64
  */
49
65
  private logChat;
50
66
  private loadCombos;
67
+ /**
68
+ * 提交组合变更:落盘 + 通知拓扑观察者。
69
+ *
70
+ * `onCombosChanged` 的失败必须被吞掉:它是**通知**不是事务参与者,
71
+ * 监听方(宿主事件总线)出问题不能把一次已经生效的增删改报成失败 ——
72
+ * 那会让用户重试一个其实已经成功的操作(同 `schedule/changed` 的口径)。
73
+ */
51
74
  private saveCombos;
75
+ /** 组合落盘(通知由 {@link saveCombos} 单独发,落盘失败不阻断它)。 */
76
+ private commitCombos;
52
77
  add(supplier: Supplier): void;
53
78
  /**
54
79
  * **参与路由的供应商** —— 装载清单里开关为开的那一份,按 priority 排好。
@@ -3,13 +3,21 @@
3
3
  * 每个供应商提供 OpenAI 兼容的 chat/models,并暴露面板状态。
4
4
  * 仿 9router:供应商 = 一种"免费/付费"通道;dsh-router 在其上做路由。
5
5
  */
6
- import type { ChatOnceResult, SupplierAccountNow } from '../suppliers/contract.ts';
6
+ import type { AccountState, ChatOnceResult, SupplierAccountNow } from '../suppliers/contract.ts';
7
7
  import type { AccountPool } from './account-pool.ts';
8
8
  /** 面板展示的账号状态(脱敏)。 */
9
9
  export interface SupplierAccount {
10
10
  uid: string;
11
11
  nickname?: string;
12
12
  credits: number;
13
+ /**
14
+ * 插件解读上游信号后的语义状态(`quota` = 额度/积分用完,`session_dead` = 凭证失效)。
15
+ *
16
+ * `decorate()` 一直原样带出插件报的状态,核心选号也读它(`quota`/`session_dead`
17
+ * 的号不进候选)——面板类型里缺了这一格就会自相矛盾:号明明已被排除在路由之外,
18
+ * 界面上却只看得到「0 积分」,看不出它为什么不参与。
19
+ */
20
+ state?: AccountState;
13
21
  cooling: boolean;
14
22
  until?: string;
15
23
  reason?: string;
@@ -47,9 +55,13 @@ export interface ChatRequest {
47
55
  /** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
48
56
  lv?: string;
49
57
  /**
50
- * 宿主盖的会话身份(dsh-llm `GenerateOptions.sessionId`),由 adapter 转成
51
- * 内部头带进来。用于把用量记录**按会话归因**(「最近命中」徽章要按当前会话
52
- * 显示,而不是显示别的会话的命中)。缺省 = 非宿主调用(外部 OpenAI 客户端)。
58
+ * 调用方身份(归因键)。来源两种:
59
+ * 1. 宿主会话身份(dsh-llm `GenerateOptions.sessionId`,adapter 转成内部头
60
+ * `x-dsh-router-session`)——形式 `session-<uuid>`;
61
+ * 2. 外部 OpenAI 兼容客户端的 `user` 字段(Zed/IDE 等多客户端场景)——形式
62
+ * `ext:<user>`,前缀标明「这不是宿主会话」。
63
+ * 用途:把用量记录**按调用方归因**(「最近命中」徽章按当前会话显示;出事时
64
+ * 能反查是哪个客户端在打)。缺省 = 匿名外部客户端。
53
65
  */
54
66
  session?: string;
55
67
  }
@@ -26,8 +26,10 @@ export interface UsageRecord {
26
26
  /** 失败原因(ok=false 时)。 */
27
27
  error?: string;
28
28
  /**
29
- * 宿主会话身份(有则记)。用于「最近命中」按当前会话归因 —— 徽章要显示
30
- * 当前会话的命中,而不是全局最近(team 多会话下后者会张冠李戴)。
29
+ * 调用方身份(有则记):宿主会话 id(`session-<uuid>`)或外部 OpenAI 客户端
30
+ * 标签(`ext/…`,见 shared.ts 的 clientIdentityOf)。用于「最近命中」按当前
31
+ * 会话归因(徽章要显示当前会话的命中,而不是全局最近),出事时也能反查
32
+ * 是哪个客户端在打。
31
33
  */
32
34
  session?: string;
33
35
  }
@@ -19,6 +19,12 @@ export interface RouterAccount {
19
19
  uid: string;
20
20
  nickname?: string;
21
21
  credits: number;
22
+ /**
23
+ * 插件解读上游信号后的语义状态(`quota` = 额度/积分用完,`session_dead` = 凭证失效)。
24
+ * 核心选号会跳过这些号,所以面板必须显示出来——否则界面写着「健康」,而路由
25
+ * 永远不选它,用户无从知道为什么。
26
+ */
27
+ state?: string;
22
28
  cooling: boolean;
23
29
  until?: string;
24
30
  reason?: string;
@@ -296,3 +302,16 @@ export interface RouterChartResponse {
296
302
  error?: string;
297
303
  chart?: RouterChartBucket[];
298
304
  }
305
+ /**
306
+ * 外部 OpenAI 兼容客户端的归因标签(`/v1` 没有宿主内部头 `x-dsh-router-session`
307
+ * 时的兜底)——否则 usage 记录里全是匿名流量,出事反查不出是谁在打
308
+ * (2026-10-15 实测:296 条匿名记录对不上任何会话)。
309
+ *
310
+ * 取值优先级:`user`(OpenAI 标准终端用户字段)→ `prompt_cache_key`
311
+ * (Zed 等客户端的缓存键,常带线程粒度)→ `User-Agent`(保底到客户端粒度)。
312
+ * 值截断到 64 字符;三者皆无返回 undefined(保持匿名)。
313
+ */
314
+ export declare function clientIdentityOf(body: {
315
+ user?: unknown;
316
+ prompt_cache_key?: unknown;
317
+ }, userAgent: string | undefined): string | undefined;
@@ -111,11 +111,17 @@ export type ChatOnceResult =
111
111
  status: number;
112
112
  body: string;
113
113
  }
114
- /** 失败:核心据此做冷却/换号。 */
114
+ /**
115
+ * 失败:核心据此做冷却/换号。
116
+ * `resetAt` = 上游自己声明的重置时刻(epoch ms,可选):限流类错误带它时
117
+ * 核心冷却到该点,而不是指数退避——频率配额的重置是小时级的,5min 上限的
118
+ * 退避只会到点回来再撞(2026-10-15 的 6004 事故:11 个号轮流被打冷)。
119
+ */
115
120
  | {
116
121
  ok: false;
117
122
  state: AccountState;
118
123
  message: string;
124
+ resetAt?: number;
119
125
  };
120
126
  /** 供应商模块 —— 契约(核心必须,差异化可选)。 */
121
127
  export interface SupplierModule {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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": {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -12,6 +12,7 @@ import {
12
12
  type RouterSupplierModel,
13
13
  type RouterSupplierModelsResponse,
14
14
  } from '../shared.ts'
15
+ import { statusBadge } from './account-badge.ts'
15
16
  import { Modal } from './Modal.tsx'
16
17
 
17
18
  /* ---------------- SVG 图标 ---------------- */
@@ -61,11 +62,6 @@ function untilLabel(account: RouterAccount): string {
61
62
  return `剩余 ${hours} 小时`
62
63
  }
63
64
 
64
- function statusBadge(account: RouterAccount): { text: string; cls: string } {
65
- if (account.cooling) return { text: '冷却中', cls: 'dshr-badge-warn' }
66
- return { text: '健康', cls: 'dshr-badge-ok' }
67
- }
68
-
69
65
  export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRefresh }: SupplierDetailProps): JSX.Element {
70
66
  const hasCap = (name: string): boolean => supplier.capabilities?.includes(name) ?? false
71
67
  // 连接池(账号池)由「添加链接」能力控制显示——没有添加链接 = 无账号概念,不显示连接池
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 账号徽章闸门 —— 面板不许和路由说反话。
3
+ *
4
+ * 核心 `pick()` 会跳过插件报 `quota`/`session_dead` 的号。面板若把它们显示成
5
+ * 「健康」,用户看到的就是「一切正常但请求永远不走这个号」——没有线索可查。
6
+ * 这类不一致不会抛异常,只会让人误判,所以要有闸门。
7
+ */
8
+ import test from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { statusBadge } from './account-badge.ts'
11
+ import type { RouterAccount } from '../shared.ts'
12
+
13
+ const acc = (over: Partial<RouterAccount> = {}): RouterAccount => ({
14
+ uid: 'u1', credits: 100, cooling: false, ...over,
15
+ })
16
+
17
+ test('额度用完的号显示「额度用完」,不是「健康」', () => {
18
+ const b = statusBadge(acc({ credits: 0, state: 'quota' }))
19
+ assert.equal(b.text, '额度用完')
20
+ assert.notEqual(b.cls, 'dshr-badge-ok', '不能用健康色')
21
+ })
22
+
23
+ test('凭证失效显示「凭证失效」', () => {
24
+ assert.equal(statusBadge(acc({ state: 'session_dead' })).text, '凭证失效')
25
+ })
26
+
27
+ test('冷却优先于状态:冷却带 until/reason,用户更需要先看到', () => {
28
+ const b = statusBadge(acc({ state: 'quota', cooling: true, reason: 'quota: exceeded' }))
29
+ assert.equal(b.text, '冷却中')
30
+ })
31
+
32
+ test('正常号仍显示「健康」(不误伤 openrouter/nvidia 那种 credits: 0 的号)', () => {
33
+ // 这两家没有积分概念,报的是 credits: 0 + state: 'ok' —— 不能因为 0 就报警。
34
+ assert.equal(statusBadge(acc({ credits: 0, state: 'ok' })).text, '健康')
35
+ assert.equal(statusBadge(acc({ credits: 0 })).text, '健康', 'state 缺省(老插件)也不报警')
36
+ assert.equal(statusBadge(acc({ credits: 500, state: 'ok' })).text, '健康')
37
+ })
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 账号行的状态徽章判据 —— 面板必须和路由说同一句话。
3
+ *
4
+ * 核心选号会跳过插件报 `quota`(额度/积分用完)和 `session_dead`(凭证失效)的号
5
+ * (见 `account-pool.servableByReport`)。如果面板把这些号显示成「健康」,用户
6
+ * 就会看到「界面说没问题、可是请求永远不走它」——查不出原因,只能去删链接重登,
7
+ * 而真正的解法是充值。
8
+ *
9
+ * 判据顺序(先冷却后状态)的理由:冷却带 `until` 和 `reason`,比静态状态更具体,
10
+ * 用户更需要先看到「什么时候会好」。
11
+ */
12
+ import type { RouterAccount } from '../shared.ts'
13
+
14
+ export interface AccountBadge {
15
+ text: string
16
+ cls: string
17
+ }
18
+
19
+ export function statusBadge(account: RouterAccount): AccountBadge {
20
+ if (account.cooling) return { text: '冷却中', cls: 'dshr-badge-warn' }
21
+ if (account.state === 'quota') return { text: '额度用完', cls: 'dshr-badge-warn' }
22
+ if (account.state === 'session_dead') return { text: '凭证失效', cls: 'dshr-badge-warn' }
23
+ return { text: '健康', cls: 'dshr-badge-ok' }
24
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/data-dir.ts CHANGED
File without changes
File without changes
File without changes
package/src/ext/store.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes