mslxdff 0.1.200 → 0.1.203

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 (326) hide show
  1. package/README.md +3 -3
  2. package/bin/mslxdff.js +1 -1
  3. package/package.json +38 -38
  4. package/plugins/README.md +14 -14
  5. package/plugins/prefer-model.mjs.example +14 -14
  6. package/src/anthropic/count-tokens.js +61 -61
  7. package/src/anthropic/request-body.js +126 -126
  8. package/src/anthropic/shared.js +18 -18
  9. package/src/anthropic/sse-aggregate.js +56 -56
  10. package/src/anthropic/translate.js +287 -287
  11. package/src/anthropic/web-search.js +361 -361
  12. package/src/api-kind.js +31 -31
  13. package/src/autostart.js +288 -288
  14. package/src/bench/probe.js +1 -1
  15. package/src/bench/runner.js +1 -1
  16. package/src/bench/via-probe.js +1 -1
  17. package/src/bench/via.js +144 -144
  18. package/src/bench/workbuddy-bench.js +1 -1
  19. package/src/chat/config.js +11 -11
  20. package/src/chat/cooling.js +81 -81
  21. package/src/chat/direct.js +58 -58
  22. package/src/chat/index.js +6 -6
  23. package/src/chat/spinner.js +38 -38
  24. package/src/chat/sse.js +69 -69
  25. package/src/chat/stats.js +247 -247
  26. package/src/chat/store.js +48 -48
  27. package/src/chat/tool-handlers.js +67 -67
  28. package/src/chat/tools.js +1 -1
  29. package/src/chat/upstream.js +59 -59
  30. package/src/chat-pipeline/auto-race.js +158 -158
  31. package/src/chat-pipeline/empty-turn.js +147 -147
  32. package/src/chat-pipeline/engine.js +17 -17
  33. package/src/chat-pipeline/serial-trial.js +283 -283
  34. package/src/chat-pipeline/terminal-verdict.js +147 -147
  35. package/src/chooser.js +54 -54
  36. package/src/claude-desktop/apply.js +101 -101
  37. package/src/claude-desktop/context1m.js +38 -38
  38. package/src/claude-desktop/doctor.js +146 -146
  39. package/src/claude-desktop/slots.js +152 -152
  40. package/src/cli/bootstrap.js +1 -1
  41. package/src/cli/commands/claude-desktop.js +284 -284
  42. package/src/cli/commands/daemon.js +231 -231
  43. package/src/cli/commands/model/list-live.js +22 -22
  44. package/src/cli/commands/model/list-providers.js +76 -76
  45. package/src/cli/commands/model/list-render.js +77 -77
  46. package/src/cli/commands/model/list-sort.js +52 -52
  47. package/src/cli/commands/model/list-tty.js +76 -76
  48. package/src/cli/commands/model/list.js +166 -166
  49. package/src/cli/commands/model/live-models.js +24 -24
  50. package/src/cli/commands/model/stats.js +43 -43
  51. package/src/cli/commands/model/status.js +47 -47
  52. package/src/cli/commands/model.js +51 -51
  53. package/src/cli/commands/provider/add.js +108 -108
  54. package/src/cli/commands/provider/allowlist.js +99 -99
  55. package/src/cli/commands/provider/cline-free.js +99 -99
  56. package/src/cli/commands/provider/cline-login.js +218 -218
  57. package/src/cli/commands/provider/cline-quota.js +65 -65
  58. package/src/cli/commands/provider/codearts-login.js +45 -45
  59. package/src/cli/commands/provider/config.js +119 -119
  60. package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
  61. package/src/cli/commands/provider/keys-guard.js +50 -50
  62. package/src/cli/commands/provider/keys.js +161 -161
  63. package/src/cli/commands/provider/qoder-checkin.js +81 -81
  64. package/src/cli/commands/provider/qoder-login.js +102 -102
  65. package/src/cli/commands/provider/raccoon-login.js +180 -180
  66. package/src/cli/commands/provider/raccoon-password.js +162 -162
  67. package/src/cli/commands/provider/raccoon-quota.js +109 -109
  68. package/src/cli/commands/provider/traework-checkin.js +144 -144
  69. package/src/cli/commands/provider/traework-login.js +164 -164
  70. package/src/cli/commands/provider/workbuddy-login.js +153 -153
  71. package/src/cli/commands/provider/zcode-login.js +77 -77
  72. package/src/cli/commands/provider/zcode-quota.js +55 -55
  73. package/src/cli/commands/provider.js +1 -1
  74. package/src/cli/commands/stats.js +149 -149
  75. package/src/cli/commands/system.js +261 -261
  76. package/src/cli/commands/timezone.js +47 -47
  77. package/src/cli/commands/use-group.js +60 -60
  78. package/src/cli/commands/workbuddy.js +189 -189
  79. package/src/cli/format.js +141 -141
  80. package/src/cli/group-helpers.js +70 -70
  81. package/src/cli/help.js +80 -80
  82. package/src/cli/index.js +70 -70
  83. package/src/cli/interactive.js +121 -121
  84. package/src/cli/join-core.js +99 -99
  85. package/src/cli/join-wizard.js +175 -175
  86. package/src/cli/policy.js +136 -136
  87. package/src/cli/provider-row.js +126 -126
  88. package/src/cli/util.js +24 -24
  89. package/src/compat.js +53 -53
  90. package/src/events.js +38 -38
  91. package/src/free-lane.js +131 -131
  92. package/src/free-watcher.js +63 -63
  93. package/src/groups.js +262 -262
  94. package/src/logs.js +195 -195
  95. package/src/metrics.js +62 -62
  96. package/src/model-capabilities/context-lookup.js +65 -65
  97. package/src/model-capabilities/merge.js +142 -142
  98. package/src/model-trace.js +187 -187
  99. package/src/models.js +1 -1
  100. package/src/opencode-identity.js +72 -72
  101. package/src/peers.js +254 -251
  102. package/src/plugins.js +107 -107
  103. package/src/providers/base.js +164 -164
  104. package/src/providers/classify.js +22 -22
  105. package/src/providers/cline/allowlist-sync.js +93 -93
  106. package/src/providers/cline/free-catalog.js +71 -71
  107. package/src/providers/cline/headers.js +37 -37
  108. package/src/providers/cline/index.js +130 -130
  109. package/src/providers/cline/models.js +150 -150
  110. package/src/providers/cline/usage.js +274 -274
  111. package/src/providers/codearts/auth-pool.js +148 -148
  112. package/src/providers/codearts/concurrency.js +34 -34
  113. package/src/providers/codearts/const.js +65 -65
  114. package/src/providers/codearts/dpop.js +95 -95
  115. package/src/providers/codearts/index.js +175 -175
  116. package/src/providers/codearts/login.js +214 -214
  117. package/src/providers/codearts/models.js +182 -182
  118. package/src/providers/codearts/sign.js +96 -96
  119. package/src/providers/codearts/sse.js +204 -204
  120. package/src/providers/codearts/stream.js +209 -209
  121. package/src/providers/codearts/sts.js +124 -124
  122. package/src/providers/codearts.js +1 -1
  123. package/src/providers/dispatcher.js +146 -146
  124. package/src/providers/generic.js +156 -156
  125. package/src/providers/globalqwenwork/account-store.js +141 -141
  126. package/src/providers/globalqwenwork/constants.js +73 -73
  127. package/src/providers/globalqwenwork/crypto.js +2 -2
  128. package/src/providers/globalqwenwork/http.js +2 -2
  129. package/src/providers/globalqwenwork/index.js +331 -331
  130. package/src/providers/globalqwenwork/payload.js +147 -147
  131. package/src/providers/globalqwenwork/rsa.js +2 -2
  132. package/src/providers/globalqwenwork/sse.js +2 -2
  133. package/src/providers/globalqwenwork/stream.js +2 -2
  134. package/src/providers/globalqwenwork.js +1 -1
  135. package/src/providers/globalworkbuddy/account-store.js +198 -198
  136. package/src/providers/globalworkbuddy/auth.js +97 -97
  137. package/src/providers/globalworkbuddy/catalog.js +138 -138
  138. package/src/providers/globalworkbuddy/constants.js +73 -73
  139. package/src/providers/globalworkbuddy/device-auth.js +120 -120
  140. package/src/providers/globalworkbuddy/errors.js +98 -98
  141. package/src/providers/globalworkbuddy/headers.js +89 -89
  142. package/src/providers/globalworkbuddy/models.js +96 -96
  143. package/src/providers/globalworkbuddy/payload.js +80 -80
  144. package/src/providers/index.js +5 -5
  145. package/src/providers/model-id.js +172 -172
  146. package/src/providers/opencode.js +19 -19
  147. package/src/providers/openrouter.js +31 -31
  148. package/src/providers/qoder/account-store.js +106 -106
  149. package/src/providers/qoder/aggregate.js +60 -60
  150. package/src/providers/qoder/baseprompt.json +591 -591
  151. package/src/providers/qoder/checkin.js +160 -160
  152. package/src/providers/qoder/constants.js +44 -44
  153. package/src/providers/qoder/encode.js +41 -41
  154. package/src/providers/qoder/fingerprint.js +38 -38
  155. package/src/providers/qoder/models.js +77 -77
  156. package/src/providers/qoder/oauth.js +84 -84
  157. package/src/providers/qoder/payload.js +162 -162
  158. package/src/providers/qoder/request.js +27 -27
  159. package/src/providers/qoder/session.js +134 -134
  160. package/src/providers/qoder/sticky.js +38 -38
  161. package/src/providers/qoder/stream.js +116 -116
  162. package/src/providers/qoder.js +1 -1
  163. package/src/providers/raccoon/account-store.js +216 -216
  164. package/src/providers/raccoon/auth.js +67 -67
  165. package/src/providers/raccoon/chat.js +228 -228
  166. package/src/providers/raccoon/const.js +180 -165
  167. package/src/providers/raccoon/credits.js +185 -175
  168. package/src/providers/raccoon/crypto.js +43 -0
  169. package/src/providers/raccoon/envelope.js +31 -31
  170. package/src/providers/raccoon/headers.js +45 -45
  171. package/src/providers/raccoon/index.js +216 -216
  172. package/src/providers/raccoon/login.js +279 -279
  173. package/src/providers/raccoon/models.js +160 -160
  174. package/src/providers/raccoon/password-login.js +102 -87
  175. package/src/providers/raccoon/qr-codewords.js +115 -115
  176. package/src/providers/raccoon/qr.js +236 -236
  177. package/src/providers/raccoon/sse.js +135 -135
  178. package/src/providers/responses-channel.js +120 -120
  179. package/src/providers/share-keys.js +53 -53
  180. package/src/providers/traework/account-store.js +234 -234
  181. package/src/providers/traework/chat.js +217 -217
  182. package/src/providers/traework/checkin.js +77 -77
  183. package/src/providers/traework/constants.js +41 -41
  184. package/src/providers/traework/errors.js +52 -52
  185. package/src/providers/traework/headers.js +79 -79
  186. package/src/providers/traework/index.js +132 -132
  187. package/src/providers/traework/market-identity.js +116 -116
  188. package/src/providers/traework/models.js +98 -98
  189. package/src/providers/traework/payload.js +86 -86
  190. package/src/providers/traework/sse.js +184 -184
  191. package/src/providers/traework/token.js +52 -52
  192. package/src/providers/traework.js +2 -2
  193. package/src/providers/workbuddy/account-store.js +186 -186
  194. package/src/providers/workbuddy/auth.js +158 -158
  195. package/src/providers/workbuddy/balance.js +86 -86
  196. package/src/providers/workbuddy/cat-travel.js +125 -125
  197. package/src/providers/workbuddy/checkin.js +75 -75
  198. package/src/providers/workbuddy/desktop-info.js +78 -78
  199. package/src/providers/workbuddy/growth-api.js +67 -67
  200. package/src/providers/workbuddy/growth-plans.js +63 -63
  201. package/src/providers/workbuddy/growth.js +181 -181
  202. package/src/providers/workbuddy/index.js +217 -217
  203. package/src/providers/workbuddy/models.js +126 -126
  204. package/src/providers/workbuddy/payload.js +141 -141
  205. package/src/providers/workbuddy/rotation-log.js +83 -83
  206. package/src/providers/workbuddy/sanitize-tools.js +61 -61
  207. package/src/providers/workbuddy/sdk-chat.js +26 -26
  208. package/src/providers/workbuddy-balance.js +78 -78
  209. package/src/providers/zcode/account-store.js +129 -129
  210. package/src/providers/zcode/auth.js +28 -28
  211. package/src/providers/zcode/chat.js +179 -179
  212. package/src/providers/zcode/const.js +54 -54
  213. package/src/providers/zcode/context-shape.js +162 -162
  214. package/src/providers/zcode/headers.js +93 -93
  215. package/src/providers/zcode/index.js +124 -124
  216. package/src/providers/zcode/models.js +65 -65
  217. package/src/providers/zcode/oauth.js +120 -120
  218. package/src/providers/zcode/quota.js +176 -176
  219. package/src/providers/zcode/sse.js +196 -196
  220. package/src/readline-compat.js +25 -25
  221. package/src/responses/translate.js +311 -311
  222. package/src/routes/chat/broadband-handler.js +72 -72
  223. package/src/routes/chat/empty-turn-report.js +31 -31
  224. package/src/routes/chat/exhausted-handler.js +87 -87
  225. package/src/routes/chat/hedge-handler.js +130 -130
  226. package/src/routes/chat/index.js +1 -1
  227. package/src/routes/chat/local-handler.js +63 -63
  228. package/src/routes/chat/peer-handler.js +81 -81
  229. package/src/routes/chat.js +3 -3
  230. package/src/routes/fallback.js +88 -88
  231. package/src/routes/groups-relay.js +214 -214
  232. package/src/routes/groups.js +122 -122
  233. package/src/routes/helpers.js +83 -83
  234. package/src/routes/index.js +147 -147
  235. package/src/routes/messages-route.js +365 -365
  236. package/src/routes/peers.js +23 -1
  237. package/src/routes/relay-queue.js +181 -181
  238. package/src/routes/relay.js +60 -60
  239. package/src/routes/responses-route.js +213 -213
  240. package/src/routes/stream-body.js +84 -84
  241. package/src/routes/stream-hold.js +337 -337
  242. package/src/routes/stream-scan.js +366 -366
  243. package/src/routes.js +3 -3
  244. package/src/runtime/auto-update.js +113 -113
  245. package/src/runtime/bootstrap.js +8 -0
  246. package/src/runtime/broadband-stream.js +79 -79
  247. package/src/runtime/broadband.js +103 -103
  248. package/src/runtime/group-sync.js +81 -81
  249. package/src/runtime/keep-warm.js +120 -0
  250. package/src/runtime/lifecycle-forensics.js +116 -116
  251. package/src/runtime/lifecycle-log.js +87 -87
  252. package/src/runtime/log-stamp.js +73 -73
  253. package/src/runtime/proc-info.js +137 -137
  254. package/src/runtime/qoder-checkin.js +93 -93
  255. package/src/runtime/raccoon-checkin.js +126 -126
  256. package/src/runtime/server-lifecycle.js +189 -189
  257. package/src/runtime/traework-checkin.js +118 -118
  258. package/src/runtime/workbuddy-checkin.js +120 -120
  259. package/src/runtime/workbuddy-growth.js +133 -133
  260. package/src/server.js +7 -0
  261. package/src/state/facade.js +58 -58
  262. package/src/state/memory.js +162 -162
  263. package/src/state/merge.js +25 -25
  264. package/src/state/migrations/cline-unify.js +94 -94
  265. package/src/state/migrations.js +33 -33
  266. package/src/state/persist.js +42 -42
  267. package/src/state/provider-config.js +142 -142
  268. package/src/state/schemas/group.js +31 -31
  269. package/src/state/schemas/model.js +94 -94
  270. package/src/state/schemas/peer.js +31 -31
  271. package/src/state/schemas/port.js +10 -10
  272. package/src/state/schemas/provider.js +202 -202
  273. package/src/state/schemas/timezone.js +70 -70
  274. package/src/state/schemas/token.js +48 -48
  275. package/src/state/schemas/use-group.js +102 -102
  276. package/src/state/store.js +176 -176
  277. package/src/state.js +1 -1
  278. package/src/sync-claude-desktop.js +234 -234
  279. package/src/sync-claude.js +152 -152
  280. package/src/sync-codex.js +116 -116
  281. package/src/sync-workbuddy.js +158 -158
  282. package/src/talk-full-redact.js +129 -129
  283. package/src/talk-full.js +131 -131
  284. package/src/talk-log.js +257 -257
  285. package/src/text-tokens.js +52 -52
  286. package/src/thinking-effort.js +67 -67
  287. package/src/time.js +106 -106
  288. package/src/timeline.js +26 -26
  289. package/src/transport/index.js +1 -1
  290. package/src/upstream-engine/index.js +74 -74
  291. package/src/upstream-engine/mode.js +15 -15
  292. package/src/upstream-engine/sdk/attempt.js +221 -221
  293. package/src/upstream-engine/sdk/chat.js +28 -28
  294. package/src/upstream-engine/sdk/convert.js +192 -192
  295. package/src/upstream-engine/sdk/diagnose.js +62 -62
  296. package/src/upstream-engine/sdk/dispatch.js +55 -55
  297. package/src/upstream-engine/sdk/responses.js +176 -176
  298. package/src/upstream-engine/sdk/sse.js +166 -166
  299. package/src/upstream-responses.js +305 -305
  300. package/src/usage/record.js +106 -106
  301. package/src/usage/report.js +195 -195
  302. package/src/v6pool/claim.js +157 -157
  303. package/src/v6pool/config.js +65 -65
  304. package/src/v6pool/detect.js +130 -130
  305. package/src/v6pool/index.js +152 -152
  306. package/src/v6pool/prefix.js +140 -140
  307. package/src/v6pool/probe.js +40 -40
  308. package/src/v6pool/report.js +104 -104
  309. package/src/v6pool/rotate.js +263 -263
  310. package/src/warp-pool/config.js +74 -74
  311. package/src/warp-pool/health.js +106 -106
  312. package/src/warp-pool/identity.js +141 -141
  313. package/src/warp-pool/index.js +358 -358
  314. package/src/warp-pool/keys.js +29 -29
  315. package/src/warp-pool/nodes.js +95 -95
  316. package/src/warp-pool/ports.js +160 -160
  317. package/src/warp-pool/register.js +113 -113
  318. package/src/warp-pool/report.js +232 -232
  319. package/src/warp-pool/revive.js +168 -168
  320. package/src/warp-pool/rotate.js +214 -214
  321. package/src/warp-pool/strategy.js +43 -43
  322. package/src/warp-pool/verdict.js +69 -69
  323. package/src/warp-pool/wireproxy.js +162 -162
  324. package/docs/cli_help_mini.md +0 -146
  325. package/src/providers/AGENTS.md +0 -49
  326. package/src/routes/AGENTS.md +0 -37
@@ -1,135 +1,135 @@
1
- // raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
2
- // 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
3
- // 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
4
- import { createSseParser } from "../../transport/sse.js";
5
- import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
6
-
7
- /**
8
- * 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
9
- * 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
10
- * 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
11
- */
12
- const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
13
-
14
- export function isRaccoonSseContentType(value) {
15
- return String(value || "").includes("text/event-stream");
16
- }
17
-
18
- function isQuotaLike(envelope) {
19
- const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
20
- if (QUOTA_HINTS.test(text)) return true;
21
- return envelope?.status === 402;
22
- }
23
-
24
- /**
25
- * 失败分类 → `{ kind, code, status, message }`。
26
- * kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
27
- */
28
- export function classifyRaccoonFailure(status, payload) {
29
- const envelope = parseRaccoonEnvelope(payload, status);
30
- let kind = "unknown";
31
- if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
32
- else if (isQuotaLike(envelope)) kind = "quota";
33
- else if (status === 429) kind = "rate_limit";
34
- else if (status >= 500) kind = "server";
35
- return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
36
- }
37
-
38
- const ERROR_MAP = {
39
- auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
40
- quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
41
- rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
42
- server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
43
- // unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
44
- unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
45
- };
46
-
47
- /** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
48
- export function raccoonErrorResponse(fail = {}) {
49
- const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
50
- const entry = ERROR_MAP[kind];
51
- const headers = { "Content-Type": "application/json" };
52
- if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
53
- return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
54
- status: entry.status,
55
- headers,
56
- });
57
- }
58
-
59
- /** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
60
- function mergeRaccoonToolCallDelta(merged, delta) {
61
- for (const key of ["id", "type"]) {
62
- if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
63
- }
64
- const dfn = delta.function;
65
- if (!dfn || typeof dfn !== "object") return;
66
- if (!merged.function || typeof merged.function !== "object") merged.function = {};
67
- if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
68
- if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
69
- merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
70
- }
71
- }
72
-
73
- /** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
74
- export async function aggregateRaccoonSse(text, { model } = {}) {
75
- const parser = createSseParser();
76
- // 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
77
- const events = parser.push(`${String(text || "")}\n\n`);
78
- const content = [];
79
- const reasoning = [];
80
- const toolCalls = new Map();
81
- const toolOrder = [];
82
- let finishReason = "stop";
83
- let usage;
84
- let id = "";
85
- let created = 0;
86
- for (const raw of events) {
87
- if (raw === "[DONE]" || !raw) continue;
88
- let payload;
89
- try {
90
- payload = JSON.parse(raw);
91
- } catch {
92
- continue;
93
- }
94
- // 流内错误信封:优先按失败分类返回,不要静默当成空回复
95
- if (payload?.code !== undefined && Number(payload.code) !== 0) {
96
- return { error: classifyRaccoonFailure(200, payload) };
97
- }
98
- if (payload?.error) {
99
- return { error: classifyRaccoonFailure(200, payload) };
100
- }
101
- if (!id && typeof payload?.id === "string") id = payload.id;
102
- if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
103
- if (payload?.usage) usage = payload.usage;
104
- const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
105
- const delta = choice?.delta ?? choice?.message;
106
- if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
107
- if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
108
- for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
109
- if (!call || typeof call !== "object") continue;
110
- const idx = typeof call.index === "number" ? call.index : 0;
111
- if (!toolCalls.has(idx)) {
112
- toolCalls.set(idx, { index: idx });
113
- toolOrder.push(idx);
114
- }
115
- mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
116
- }
117
- if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
118
- }
119
- const message = { role: "assistant", content: content.join("") || null };
120
- if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
121
- if (toolOrder.length > 0) {
122
- toolOrder.sort((a, b) => a - b);
123
- message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
124
- }
125
- return {
126
- openAi: {
127
- id: id || `chatcmpl-raccoon-${Date.now()}`,
128
- object: "chat.completion",
129
- created: created || Math.floor(Date.now() / 1000),
130
- model: model || "",
131
- choices: [{ index: 0, message, finish_reason: finishReason }],
132
- ...(usage ? { usage } : {}),
133
- },
134
- };
135
- }
1
+ // raccoon 流处理:上游就是 OpenAI 形(`/api/web/llm/v2/chat/completions`),
2
+ // 因此**不做协议转换**——本文件只做三件事:形状判定、错误信封分类、非流式聚合。
3
+ // 流式路径直接把上游 body 透传(逐字节),避免任何整形引入的失真。
4
+ import { createSseParser } from "../../transport/sse.js";
5
+ import { isRaccoonAuthFailure, parseRaccoonEnvelope, raccoonEnvelopeText } from "./envelope.js";
6
+
7
+ /**
8
+ * 额度类措辞(业务码未取证,见 ADR-0050 决策 8):**宁可漏判也不误报**。
9
+ * 刻意不收裸 `insufficient`/`balance`/`points` —— 那些词会命中「权限不足」这类非额度错,
10
+ * 一旦误判成 quota 就会长冷 1h 并回 429 `quota_exhausted`,违反 ADR-0044 的「非额度原因不得谎报」。
11
+ */
12
+ const QUOTA_HINTS = /(积分|额度|quota|欠费)/i;
13
+
14
+ export function isRaccoonSseContentType(value) {
15
+ return String(value || "").includes("text/event-stream");
16
+ }
17
+
18
+ function isQuotaLike(envelope) {
19
+ const text = `${envelope?.message ?? ""} ${envelope?.details ?? ""}`;
20
+ if (QUOTA_HINTS.test(text)) return true;
21
+ return envelope?.status === 402;
22
+ }
23
+
24
+ /**
25
+ * 失败分类 → `{ kind, code, status, message }`。
26
+ * kind 取值:auth(重登)/ quota(长冷却)/ rate_limit(短冷却)/ server(上游 5xx,短冷却)/ unknown(4xx 如实透出)。
27
+ */
28
+ export function classifyRaccoonFailure(status, payload) {
29
+ const envelope = parseRaccoonEnvelope(payload, status);
30
+ let kind = "unknown";
31
+ if (isRaccoonAuthFailure(envelope, status)) kind = "auth";
32
+ else if (isQuotaLike(envelope)) kind = "quota";
33
+ else if (status === 429) kind = "rate_limit";
34
+ else if (status >= 500) kind = "server";
35
+ return { kind, code: envelope.code, status, message: raccoonEnvelopeText(envelope, `HTTP ${status}`) };
36
+ }
37
+
38
+ const ERROR_MAP = {
39
+ auth: { status: 401, type: "auth_error", text: (f) => `raccoon: 登录态已失效${f.code ? `(code ${f.code})` : ""}。请运行 mslxdff -provider raccoon login 重新登录` },
40
+ quota: { status: 429, type: "quota_exhausted", text: (f) => `raccoon: 积分不足${f.message ? `(${f.message})` : ""}。请运行 mslxdff -provider raccoon checkin 领取每日积分,或更换账号` },
41
+ rate_limit: { status: 429, type: "rate_limited", text: () => "raccoon: 上游限流(账号冷却中),请稍后重试" },
42
+ server: { status: 502, type: "upstream_error", text: (f) => `raccoon: 上游服务异常${f.code ? `(code ${f.code})` : ""}${f.message ? `:${f.message}` : ""}` },
43
+ // unknown 只覆盖 4xx(5xx 已被上面分流成 server):按请求缺陷如实回 400,不谎报成网关故障
44
+ unknown: { status: 400, type: "invalid_request", text: (f) => `raccoon: 上游拒绝请求(HTTP ${f.status || "?"}${f.code ? ` code ${f.code}` : ""})${f.message ? `:${f.message}` : ""}` },
45
+ };
46
+
47
+ /** 失败 → Response(带 `x-mslxdff-raccoon-kind`,供工厂决定冷却档位)。 */
48
+ export function raccoonErrorResponse(fail = {}) {
49
+ const kind = ERROR_MAP[fail.kind] ? fail.kind : "unknown";
50
+ const entry = ERROR_MAP[kind];
51
+ const headers = { "Content-Type": "application/json" };
52
+ if (fail.kind) headers["x-mslxdff-raccoon-kind"] = fail.kind;
53
+ return new Response(JSON.stringify({ error: { message: entry.text(fail), type: entry.type, code: fail.code || 0 } }), {
54
+ status: entry.status,
55
+ headers,
56
+ });
57
+ }
58
+
59
+ /** 工具调用增量合并(对齐 `qwenwork/sse.js` 的 mergeToolCallDelta:id/type 取一次,name/arguments 追加)。 */
60
+ function mergeRaccoonToolCallDelta(merged, delta) {
61
+ for (const key of ["id", "type"]) {
62
+ if (merged[key] === undefined && typeof delta[key] === "string" && delta[key] !== "") merged[key] = delta[key];
63
+ }
64
+ const dfn = delta.function;
65
+ if (!dfn || typeof dfn !== "object") return;
66
+ if (!merged.function || typeof merged.function !== "object") merged.function = {};
67
+ if (typeof dfn.name === "string" && dfn.name !== "") merged.function.name = (merged.function.name || "") + dfn.name;
68
+ if (typeof dfn.arguments === "string" && dfn.arguments !== "") {
69
+ merged.function.arguments = (merged.function.arguments || "") + dfn.arguments;
70
+ }
71
+ }
72
+
73
+ /** 把 SSE 文本聚合成一条 OpenAI chat completion(非流式请求、以及「header 说是 JSON 但体是 SSE」时走这里)。 */
74
+ export async function aggregateRaccoonSse(text, { model } = {}) {
75
+ const parser = createSseParser();
76
+ // 聚合拿到的是**完整 body**(不是分片):补一个定界符,否则末尾缺 `\n\n` 的最后一帧会被当半包丢掉
77
+ const events = parser.push(`${String(text || "")}\n\n`);
78
+ const content = [];
79
+ const reasoning = [];
80
+ const toolCalls = new Map();
81
+ const toolOrder = [];
82
+ let finishReason = "stop";
83
+ let usage;
84
+ let id = "";
85
+ let created = 0;
86
+ for (const raw of events) {
87
+ if (raw === "[DONE]" || !raw) continue;
88
+ let payload;
89
+ try {
90
+ payload = JSON.parse(raw);
91
+ } catch {
92
+ continue;
93
+ }
94
+ // 流内错误信封:优先按失败分类返回,不要静默当成空回复
95
+ if (payload?.code !== undefined && Number(payload.code) !== 0) {
96
+ return { error: classifyRaccoonFailure(200, payload) };
97
+ }
98
+ if (payload?.error) {
99
+ return { error: classifyRaccoonFailure(200, payload) };
100
+ }
101
+ if (!id && typeof payload?.id === "string") id = payload.id;
102
+ if (!created && Number.isFinite(payload?.created)) created = Number(payload.created);
103
+ if (payload?.usage) usage = payload.usage;
104
+ const choice = Array.isArray(payload?.choices) ? payload.choices[0] : undefined;
105
+ const delta = choice?.delta ?? choice?.message;
106
+ if (typeof delta?.content === "string" && delta.content.length > 0) content.push(delta.content);
107
+ if (typeof delta?.reasoning_content === "string" && delta.reasoning_content.length > 0) reasoning.push(delta.reasoning_content);
108
+ for (const call of Array.isArray(delta?.tool_calls) ? delta.tool_calls : []) {
109
+ if (!call || typeof call !== "object") continue;
110
+ const idx = typeof call.index === "number" ? call.index : 0;
111
+ if (!toolCalls.has(idx)) {
112
+ toolCalls.set(idx, { index: idx });
113
+ toolOrder.push(idx);
114
+ }
115
+ mergeRaccoonToolCallDelta(toolCalls.get(idx), call);
116
+ }
117
+ if (typeof choice?.finish_reason === "string" && choice.finish_reason.length > 0) finishReason = choice.finish_reason;
118
+ }
119
+ const message = { role: "assistant", content: content.join("") || null };
120
+ if (reasoning.length > 0) message.reasoning_content = reasoning.join("");
121
+ if (toolOrder.length > 0) {
122
+ toolOrder.sort((a, b) => a - b);
123
+ message.tool_calls = toolOrder.map((idx) => toolCalls.get(idx));
124
+ }
125
+ return {
126
+ openAi: {
127
+ id: id || `chatcmpl-raccoon-${Date.now()}`,
128
+ object: "chat.completion",
129
+ created: created || Math.floor(Date.now() / 1000),
130
+ model: model || "",
131
+ choices: [{ index: 0, message, finish_reason: finishReason }],
132
+ ...(usage ? { usage } : {}),
133
+ },
134
+ };
135
+ }
@@ -1,120 +1,120 @@
1
- // 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
2
- // 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
3
- // 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
4
- // 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
5
- //
6
- // 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
7
- // · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
8
- // · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
9
- // key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
10
- import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
11
- import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
12
- import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
13
- import { sleep } from "./base.js";
14
-
15
- /** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
16
- export function envSlug(id) {
17
- return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
18
- }
19
-
20
- /**
21
- * responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
22
- * 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
23
- */
24
- export function resolveResponsesPath(id, responsesPath) {
25
- const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
26
- const s = String(raw).trim();
27
- if (!s) return "/responses";
28
- return s.startsWith("/") ? s : `/${s}`;
29
- }
30
-
31
- /**
32
- * 建 responses 通道。
33
- * @param {object} o
34
- * @param {string} o.id 供应商 id(用于报错文案与 providerName)
35
- * @param {string} o.url 已拼好的 responses 绝对 URL
36
- * @param {number} o.connectTimeoutMs 单次尝试超时
37
- * @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
38
- * @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
39
- * @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
40
- * @param {Function} o.buildHeaders (body, key, opts) → headers
41
- * @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
42
- * @param {object} [o.dispatcher] undici Agent
43
- */
44
- export function createResponsesChannel({
45
- id,
46
- url,
47
- connectTimeoutMs = 30_000,
48
- retry = {},
49
- cooldownMs = 30_000,
50
- sdkEnabled = true,
51
- buildHeaders,
52
- fetchImpl,
53
- dispatcher = null,
54
- } = {}) {
55
- async function attemptOnce(body, key, opts) {
56
- const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
57
- const wantsStream = body?.stream !== false;
58
- if (sdkEnabled && wantsStream) {
59
- try {
60
- const r = await attemptOnceResponsesSdk({
61
- url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
62
- });
63
- if (r) return r; // 适配器已产出 chat SSE
64
- } catch (e) {
65
- if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
66
- // SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
67
- }
68
- }
69
- const controller = new AbortController();
70
- const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
71
- try {
72
- const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
73
- if (dispatcher) fetchOpts.dispatcher = dispatcher;
74
- const res = await fetchImpl(url, fetchOpts);
75
- if (!res.ok) return res;
76
- if (wantsStream) return reshapeResponsesSse(res, body?.model);
77
- const json = await res.json().catch(() => null);
78
- return json ? toChatResponse(res, json) : res;
79
- } finally {
80
- clearTimeout(timer);
81
- }
82
- }
83
-
84
- /** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
85
- async function run(body, activeRing, sourceKey, opts) {
86
- const t0 = performance.now();
87
- const attempts = [];
88
- let waitMs = 0;
89
- const key = activeRing.next();
90
- if (!key && activeRing.size > 0) {
91
- const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
92
- err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
93
- throw err;
94
- }
95
- for (let attempt = 0; ; attempt++) {
96
- const t = performance.now();
97
- let result;
98
- try {
99
- result = await attemptOnce(body, key, opts);
100
- } catch (err) {
101
- result = err;
102
- }
103
- attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
104
- if (result instanceof Error) {
105
- const entry = retry?.network;
106
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
107
- activeRing.onError(key);
108
- result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
109
- throw result;
110
- }
111
- const entry = retry?.[result.status];
112
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
113
- if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
114
- try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
115
- return result;
116
- }
117
- }
118
-
119
- return { url, attemptOnce, run };
120
- }
1
+ // 通用(带 key)供应商的 responses 通道:muse-spark* 这类模型上游只挂 /responses,
2
+ // 打同 host 的 /chat/completions 会 503 "Endpoint is unavailable"(ocgo 实测 2026-09-22)。
3
+ // 判定单一来源 = isResponsesModel(models.dev 模型级 provider.npm + muse-spark 前缀兜底),
4
+ // 与 GET /v1/models 的 capabilities.upstreamApi 同源,新模型无需改码。
5
+ //
6
+ // 契约:run() 的出参恒为 **chat 形状**(SSE 或 JSON),调用方无需再判分支。
7
+ // · 流式优先 @ai-sdk/openai 的 responses 适配器(含加密思考往返,ADR-0017 复用);
8
+ // · SDK 不可用 / 非流式 → 原生 fetch + chatToResponsesBody 正转换 + 反向整形。
9
+ // key 轮换、退避重试、_t 计时与 createChatRunner(base.js)同语义,避免两条通道行为漂移。
10
+ import { chatToResponsesBody, toChatResponse, reshapeResponsesSse } from "../upstream-responses.js";
11
+ import { attemptOnceResponsesSdk } from "../upstream-engine/sdk/responses.js";
12
+ import { ENGINE_MARKER } from "../upstream-engine/sdk/chat.js";
13
+ import { sleep } from "./base.js";
14
+
15
+ /** 供应商 id → env 片段(`my-api` → `MY_API`),与 providerKeyEnv 同规则 */
16
+ export function envSlug(id) {
17
+ return String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_");
18
+ }
19
+
20
+ /**
21
+ * responses 端点路径:显式入参 > env `MSLXDFF_<ID>_RESPONSES_PATH` > 缺省 `/responses`。
22
+ * 归一为以 `/` 开头(joinUrl 会再拼 baseUrl)。
23
+ */
24
+ export function resolveResponsesPath(id, responsesPath) {
25
+ const raw = responsesPath || process.env[`MSLXDFF_${envSlug(id)}_RESPONSES_PATH`] || "";
26
+ const s = String(raw).trim();
27
+ if (!s) return "/responses";
28
+ return s.startsWith("/") ? s : `/${s}`;
29
+ }
30
+
31
+ /**
32
+ * 建 responses 通道。
33
+ * @param {object} o
34
+ * @param {string} o.id 供应商 id(用于报错文案与 providerName)
35
+ * @param {string} o.url 已拼好的 responses 绝对 URL
36
+ * @param {number} o.connectTimeoutMs 单次尝试超时
37
+ * @param {object} o.retry 重试表(与 createChatRunner 同形状:network/429/50x → {attempts, delayMs})
38
+ * @param {number} o.cooldownMs 全 key 冷却时的人话报错文案用
39
+ * @param {boolean} o.sdkEnabled 是否允许走 AI SDK responses 适配器(流式路径)
40
+ * @param {Function} o.buildHeaders (body, key, opts) → headers
41
+ * @param {Function} o.fetchImpl 注入的 fetch(连接池/测试接缝)
42
+ * @param {object} [o.dispatcher] undici Agent
43
+ */
44
+ export function createResponsesChannel({
45
+ id,
46
+ url,
47
+ connectTimeoutMs = 30_000,
48
+ retry = {},
49
+ cooldownMs = 30_000,
50
+ sdkEnabled = true,
51
+ buildHeaders,
52
+ fetchImpl,
53
+ dispatcher = null,
54
+ } = {}) {
55
+ async function attemptOnce(body, key, opts) {
56
+ const headers = buildHeaders ? buildHeaders(body, key, opts) : {};
57
+ const wantsStream = body?.stream !== false;
58
+ if (sdkEnabled && wantsStream) {
59
+ try {
60
+ const r = await attemptOnceResponsesSdk({
61
+ url, body, headers, providerName: id, marker: ENGINE_MARKER, fetchImpl,
62
+ });
63
+ if (r) return r; // 适配器已产出 chat SSE
64
+ } catch (e) {
65
+ if (!e || (!e._sdkLoadFailed && !e._sdkUnsupported)) throw e;
66
+ // SDK 装载失败 / URL 异形 → 落原生兜底(保底不变差)
67
+ }
68
+ }
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
71
+ try {
72
+ const fetchOpts = { method: "POST", headers, body: JSON.stringify(chatToResponsesBody(body)), signal: controller.signal };
73
+ if (dispatcher) fetchOpts.dispatcher = dispatcher;
74
+ const res = await fetchImpl(url, fetchOpts);
75
+ if (!res.ok) return res;
76
+ if (wantsStream) return reshapeResponsesSse(res, body?.model);
77
+ const json = await res.json().catch(() => null);
78
+ return json ? toChatResponse(res, json) : res;
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ /** key 轮换 + 退避重试 + _t 计时;语义对齐 base.js 的 createChatRunner.runChat */
85
+ async function run(body, activeRing, sourceKey, opts) {
86
+ const t0 = performance.now();
87
+ const attempts = [];
88
+ let waitMs = 0;
89
+ const key = activeRing.next();
90
+ if (!key && activeRing.size > 0) {
91
+ const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
92
+ err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
93
+ throw err;
94
+ }
95
+ for (let attempt = 0; ; attempt++) {
96
+ const t = performance.now();
97
+ let result;
98
+ try {
99
+ result = await attemptOnce(body, key, opts);
100
+ } catch (err) {
101
+ result = err;
102
+ }
103
+ attempts.push({ attempt, type: result instanceof Error ? "network" : `http${result?.status}`, ms: Math.round(performance.now() - t) });
104
+ if (result instanceof Error) {
105
+ const entry = retry?.network;
106
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
107
+ activeRing.onError(key);
108
+ result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
109
+ throw result;
110
+ }
111
+ const entry = retry?.[result.status];
112
+ if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
113
+ if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
114
+ try { result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) }; } catch {}
115
+ return result;
116
+ }
117
+ }
118
+
119
+ return { url, attemptOnce, run };
120
+ }
@@ -1,54 +1,54 @@
1
- // ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
2
- // header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
3
- import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
4
- import { classifyProvider } from "./classify.js";
5
- import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
6
- import { isRefreshToken } from "./cline/headers.js";
7
-
8
- export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
9
-
10
- // 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
11
- // 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
12
- // traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
13
- const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
14
- // 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
15
- // 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
16
- export function shareableProviderIds({ file } = {}) {
17
- const ids = [];
18
- for (const id of listProviderIdsWithKeys({ file })) {
19
- if (id === DEFAULT_PROVIDER) continue;
20
- if (NEVER_SHARE_IDS.has(id)) continue;
21
- try { if (classifyProvider(id) === "local-only") continue; } catch {}
22
- // 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
23
- // 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
24
- try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
25
- ids.push(id);
26
- }
27
- return ids;
28
- }
29
-
30
- // 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
31
- // splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
32
- // 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
33
- export function buildShareKeysHeader(model, { file } = {}) {
34
- const split = splitModelId(model, shareableProviderIds({ file }));
35
- if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
36
- const keys = loadProviderKeys(split.provider, { file });
37
- if (!keys.length) return null;
38
- return `${split.provider}=${keys.join(",")}`;
39
- }
40
-
41
- // 组员侧:解析 share header 为 { providerId -> keys[] }
42
- // 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
43
- export function parseShareKeysHeader(value) {
44
- const out = {};
45
- if (!value) return out;
46
- for (const seg of String(value).split(";")) {
47
- const [provider, keysPart] = seg.split("=");
48
- const id = String(provider || "").trim();
49
- if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
50
- const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
51
- if (keys.length) out[id] = keys;
52
- }
53
- return out;
1
+ // ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
2
+ // header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
3
+ import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
4
+ import { classifyProvider } from "./classify.js";
5
+ import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
6
+ import { isRefreshToken } from "./cline/headers.js";
7
+
8
+ export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
9
+
10
+ // 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
11
+ // 二层保护:① 按 id 硬排除(cline 恒 local-only);② 按凭据形状兜底(见 shareableProviderIds)。
12
+ // traework/workbuddy/zcode 同为本机账号绑定型(auths/*.json + uid;zcode 还带 per-account deviceMid),恒不借出。
13
+ const NEVER_SHARE_IDS = new Set(["cline", "codearts", "traework", "qoder", "zcode", "raccoon"]);
14
+ // 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
15
+ // 减去:opencode(无 key 且恒排除)、local-only(workbuddy/cline系,本就不走组员)、刷新型凭据。
16
+ export function shareableProviderIds({ file } = {}) {
17
+ const ids = [];
18
+ for (const id of listProviderIdsWithKeys({ file })) {
19
+ if (id === DEFAULT_PROVIDER) continue;
20
+ if (NEVER_SHARE_IDS.has(id)) continue;
21
+ try { if (classifyProvider(id) === "local-only") continue; } catch {}
22
+ // 形状兜底:id 白名单可能有残留(迁移未跑/回滚/手改 config),只要该供应商现有 key 里
23
+ // 有一个是 refreshToken 形态,就一律不外借——refreshToken 借出去会被对端刷新轮换,双向踢下线。
24
+ try { if (loadProviderKeys(id, { file }).some((k) => isRefreshToken(k, id))) continue; } catch {}
25
+ ids.push(id);
26
+ }
27
+ return ids;
28
+ }
29
+
30
+ // 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
31
+ // splitModelId 用 shareableProviderIds() 作为已知集合:命中 → 该供应商在此节点 shareable;
32
+ // 未命中或裸 id(默认供应商 opencode)→ 不附带。返回 null 表示本请求不需要附带。
33
+ export function buildShareKeysHeader(model, { file } = {}) {
34
+ const split = splitModelId(model, shareableProviderIds({ file }));
35
+ if (!split.provider || split.provider === DEFAULT_PROVIDER) return null;
36
+ const keys = loadProviderKeys(split.provider, { file });
37
+ if (!keys.length) return null;
38
+ return `${split.provider}=${keys.join(",")}`;
39
+ }
40
+
41
+ // 组员侧:解析 share header 为 { providerId -> keys[] }
42
+ // 防御:默认供应商(opencode)即使出现在 header 一律忽略——它不该有共享 key。
43
+ export function parseShareKeysHeader(value) {
44
+ const out = {};
45
+ if (!value) return out;
46
+ for (const seg of String(value).split(";")) {
47
+ const [provider, keysPart] = seg.split("=");
48
+ const id = String(provider || "").trim();
49
+ if (!id || id === DEFAULT_PROVIDER || !keysPart) continue;
50
+ const keys = String(keysPart).split(",").map((k) => k.trim()).filter(Boolean);
51
+ if (keys.length) out[id] = keys;
52
+ }
53
+ return out;
54
54
  }