mslxdff 0.1.136 → 0.1.140

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 (95) hide show
  1. package/package.json +2 -2
  2. package/src/chat/gateway.js +1 -1
  3. package/src/chat/prompt.js +1 -1
  4. package/src/chat/terminal.js +1 -1
  5. package/src/chooser.js +31 -12
  6. package/src/cli/commands/model/list-live.js +22 -0
  7. package/src/cli/commands/model/list-providers.js +1 -1
  8. package/src/cli/commands/model/list-render.js +2 -2
  9. package/src/cli/commands/model/list.js +2 -0
  10. package/src/cli/commands/model/live-models.js +24 -0
  11. package/src/cli/commands/provider/add.js +2 -1
  12. package/src/cli/commands/provider/bench-via.js +1 -1
  13. package/src/cli/commands/provider/cline-free.js +94 -0
  14. package/src/cli/commands/provider/cline-login.js +20 -15
  15. package/src/cli/commands/provider/codearts-login.js +45 -0
  16. package/src/cli/commands/provider/index.js +34 -0
  17. package/src/cli/commands/provider/keys.js +13 -2
  18. package/src/cli/commands/provider/models.js +13 -1
  19. package/src/cli/commands/provider/qoder-checkin.js +81 -0
  20. package/src/cli/commands/provider/qoder-login.js +103 -0
  21. package/src/cli/commands/provider/traework-login.js +202 -0
  22. package/src/cli/commands/provider/workbuddy-login.js +1 -1
  23. package/src/cli/commands/workbuddy.js +14 -21
  24. package/src/cli/index.js +3 -0
  25. package/src/cli/interactive.js +41 -4
  26. package/src/cli/provider-row.js +1 -1
  27. package/src/compat.js +23 -5
  28. package/src/model-capabilities/merge.js +2 -2
  29. package/src/providers/classify.js +1 -1
  30. package/src/providers/cline/auth.js +1 -1
  31. package/src/providers/cline/chat.js +14 -11
  32. package/src/providers/cline/free-catalog.js +71 -0
  33. package/src/providers/cline/index.js +2 -16
  34. package/src/providers/cline/models.js +28 -51
  35. package/src/providers/codearts/auth-pool.js +148 -0
  36. package/src/providers/codearts/chat.js +176 -0
  37. package/src/providers/codearts/const.js +60 -0
  38. package/src/providers/codearts/dpop.js +95 -0
  39. package/src/providers/codearts/index.js +160 -0
  40. package/src/providers/codearts/login.js +214 -0
  41. package/src/providers/codearts/models.js +182 -0
  42. package/src/providers/codearts/sign.js +96 -0
  43. package/src/providers/codearts/sse.js +204 -0
  44. package/src/providers/codearts/stream.js +209 -0
  45. package/src/providers/codearts/sts.js +124 -0
  46. package/src/providers/codearts.js +1 -0
  47. package/src/providers/model-id.js +4 -4
  48. package/src/providers/qoder/account-store.js +107 -0
  49. package/src/providers/qoder/aggregate.js +51 -0
  50. package/src/providers/qoder/baseprompt.json +592 -0
  51. package/src/providers/qoder/chat.js +72 -0
  52. package/src/providers/qoder/checkin.js +160 -0
  53. package/src/providers/qoder/constants.js +45 -0
  54. package/src/providers/qoder/encode.js +42 -0
  55. package/src/providers/qoder/fingerprint.js +39 -0
  56. package/src/providers/qoder/index.js +106 -0
  57. package/src/providers/qoder/models.js +62 -0
  58. package/src/providers/qoder/oauth.js +85 -0
  59. package/src/providers/qoder/payload.js +156 -0
  60. package/src/providers/qoder/request.js +26 -0
  61. package/src/providers/qoder/session.js +135 -0
  62. package/src/providers/qoder/sse.js +46 -0
  63. package/src/providers/qoder/stream.js +81 -0
  64. package/src/providers/qoder.js +2 -0
  65. package/src/providers/registry.js +16 -1
  66. package/src/providers/share-keys.js +7 -2
  67. package/src/providers/traework/account-store.js +149 -0
  68. package/src/providers/traework/chat.js +211 -0
  69. package/src/providers/traework/checkin.js +35 -0
  70. package/src/providers/traework/constants.js +21 -0
  71. package/src/providers/traework/errors.js +52 -0
  72. package/src/providers/traework/headers.js +47 -0
  73. package/src/providers/traework/index.js +122 -0
  74. package/src/providers/traework/models.js +97 -0
  75. package/src/providers/traework/payload.js +86 -0
  76. package/src/providers/traework/sse.js +184 -0
  77. package/src/providers/traework/token.js +52 -0
  78. package/src/providers/traework.js +2 -0
  79. package/src/providers/workbuddy/account-store.js +73 -7
  80. package/src/providers/workbuddy/chat.js +1 -1
  81. package/src/providers/workbuddy/index.js +10 -20
  82. package/src/readline-compat.js +9 -10
  83. package/src/routes/models-route.js +19 -1
  84. package/src/runtime/bootstrap.js +10 -0
  85. package/src/runtime/provider-gate.js +30 -0
  86. package/src/runtime/providers-setup.js +18 -3
  87. package/src/runtime/qoder-checkin.js +93 -0
  88. package/src/runtime/server-lifecycle.js +2 -2
  89. package/src/runtime/traework-checkin.js +104 -0
  90. package/src/state/migrations/cline-unify.js +94 -0
  91. package/src/state/migrations.js +33 -0
  92. package/src/state/schemas/token.js +6 -2
  93. package/src/state/schemas/use-group.js +4 -4
  94. package/src/sync-workbuddy.js +1 -1
  95. package/src/upstream-engine/index.js +1 -1
@@ -1,20 +1,20 @@
1
1
  import { joinUrl, sleep } from "../base.js";
2
2
  import { clineHeaders } from "./headers.js";
3
3
  import { createTransport } from "../../transport/index.js";
4
+ import { normalizeProviderId } from "../model-id.js";
4
5
  import { createSdkDispatch } from "../../upstream-engine/sdk/dispatch.js";
5
6
 
6
7
  function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
7
8
 
8
- // stripProviderPrefix 去掉本地供应商前缀,发上游用裸模型 id。
9
- // 上游(对标 cline2api-workers MODELS)只认裸 id:"deepseek/deepseek-v4-flash"、
10
- // "poolside/laguna-s-2.1:free";本地 "clinebot/deepseek/..." 需取后两段。
11
- // 两段及以内视为已是裸 id,原样透传。
12
- function stripProviderPrefix(id) {
13
- const parts = String(id || "").split("/").filter(Boolean);
14
- if (parts.length > 2) return parts.slice(1).join("/");
15
- return String(id || "");
9
+ // stripProviderPrefix 只剥「本供应商」前缀(cline/<裸 id> → 裸 id),非本前缀(含上游自带多段 id
10
+ // 如 meta/vendor/x)原样透传,不误削首段(旧实现按段数剥曾是潜在 bug)。
11
+ function stripProviderPrefix(model, providerId) {
12
+ const s = String(model || "");
13
+ const i = s.indexOf("/");
14
+ if (i <= 0) return s;
15
+ const low = (v) => normalizeProviderId(String(v || "").toLowerCase());
16
+ return low(s.slice(0, i)) === low(providerId) ? s.slice(i + 1) : s;
16
17
  }
17
-
18
18
  function unwrapData(obj) {
19
19
  if (obj && obj.data && typeof obj.data === "object") {
20
20
  const d = obj.data;
@@ -169,7 +169,7 @@ export function createChatService({
169
169
  const model = body?.model || "deepseek/deepseek-v4-flash";
170
170
  const sessionId = genSessionId();
171
171
  const isStream = body?.stream === true;
172
- const upstreamModel = stripProviderPrefix(model);
172
+ const upstreamModel = stripProviderPrefix(model, id);
173
173
  // token 口径双写:对标官方 withMaxCompletionTokensForReasoningModels——
174
174
  // cline 上游默认 reasoning_effort high,推理模型认 max_completion_tokens,
175
175
  // 只发 max_tokens 会被部分通道拒;双写兼容最稳。
@@ -182,7 +182,10 @@ export function createChatService({
182
182
  reasoning_effort: body?.reasoning_effort || body?.reasoningEffort || "high",
183
183
  messages: body?.messages || [],
184
184
  };
185
- const forceStream = !isStream && String(upstreamModel).startsWith("deepseek/");
185
+ // 部分免费通道(deepseek 家族,含 cline-free/deepseek-*)原生非流式不可靠
186
+ //(500 empty response):这类模型在内部走 stream+聚合成 JSON,对外仍按请求方
187
+ // stream 标志返回(true 直透,false 聚合)。其它模型完全尊重请求方,不强制。
188
+ const forceStream = !isStream && String(upstreamModel).toLowerCase().includes("deepseek");
186
189
  if (isStream || forceStream) upstreamBody.stream = true;
187
190
  for (const k of ["temperature", "top_p", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"]) {
188
191
  if (body[k] !== undefined) upstreamBody[k] = body[k];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cline 免费目录深模块:只认上游 `free` 数组(公开接口,无需鉴权、无凭据)。
3
+ * GET {bareHost}/api/v1/ai/cline/recommended-models → { recommended, free, clinePass, clineCloud }
4
+ * 供 cline/models.js(聚合目录 + 启动自检)与 `-provider cline free` CLI 共用;缓存由调用方持有。
5
+ */
6
+ import { compatFetch } from "../../compat.js";
7
+ import { joinUrl } from "../base.js";
8
+
9
+ export const FREE_CATALOG_PATH = "/api/v1/ai/cline/recommended-models";
10
+ export const DEFAULT_CLINE_BASE = "https://api.cline.bot";
11
+
12
+ // 上游不可达时的内置兜底(2026-09-20 实测 5 个;只有 id 与短名,无凭据)
13
+ export const FALLBACK_FREE = Object.freeze([
14
+ { id: "cline-free/deepseek-v4.1-flash", name: "deepseek-v4.1-flash" },
15
+ { id: "cline-free/muse-spark-1.3-contributor", name: "muse-spark-1.3-contributor" },
16
+ { id: "z-ai/glm-5.3-flash", name: "glm-5.3-flash" },
17
+ { id: "cline-free/solar-pro4", name: "solar-pro4" },
18
+ { id: "poolside/laguna-s-2.1:free", name: "laguna-s-2.1:free" },
19
+ ]);
20
+ export const FALLBACK_FREE_IDS = FALLBACK_FREE.map((m) => m.id);
21
+
22
+ /** 目录 URL 归一:baseUrl 可能是裸 host 或带 /api/v1 的老 clinebot 形态;customPath 尊重用户自定义端点 */
23
+ export function catalogUrl(baseUrl, customPath) {
24
+ const raw = String(baseUrl || "").trim() || DEFAULT_CLINE_BASE;
25
+ const bare = raw.replace(/\/+$/, "").replace(/\/api\/v1\/?$/, "") || DEFAULT_CLINE_BASE;
26
+ const p = String(customPath || "").trim();
27
+ if (p && p !== "/models" && p !== FREE_CATALOG_PATH) return joinUrl(bare, p);
28
+ return joinUrl(bare, FREE_CATALOG_PATH);
29
+ }
30
+
31
+ /** 只取 json.free(对象 {id,name,...} 或裸字符串两种形态都容忍) */
32
+ function pickFree(json) {
33
+ const arr = Array.isArray(json?.free) ? json.free : [];
34
+ const out = [];
35
+ for (const m of arr) {
36
+ if (typeof m === "string" && m.trim()) out.push({ id: m.trim() });
37
+ else if (m && typeof m.id === "string" && m.id.trim()) out.push({ ...m, id: m.id.trim(), name: String(m.name || "") });
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * 拉免费目录。
44
+ * @returns {Promise<{ok:boolean, status:number, url:string, models:object[], ids:string[], error?:string}>}
45
+ * ok=false 时 models/ids 为空(兜底由调用方决定);authorization 供私有/自定义目录可选透传。
46
+ */
47
+ export async function fetchFreeCatalog({ baseUrl, customPath, fetchImpl, dispatcher, authorization, timeoutMs = 15_000 } = {}) {
48
+ const url = catalogUrl(baseUrl, customPath);
49
+ const impl = fetchImpl || compatFetch;
50
+ const headers = { Accept: "application/json" };
51
+ if (authorization) headers.Authorization = authorization;
52
+ const opts = { headers };
53
+ if (dispatcher) opts.dispatcher = dispatcher;
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(new Error("cline free catalog timed out")), timeoutMs);
56
+ timer.unref?.();
57
+ opts.signal = controller.signal;
58
+ try {
59
+ const res = await impl(url, opts);
60
+ const status = Number(res?.status) || 0;
61
+ if (!res?.ok) return { ok: false, status, url, models: [], ids: [], error: `HTTP ${status}` };
62
+ const json = await res.json().catch(() => null);
63
+ const models = pickFree(json);
64
+ if (!models.length) return { ok: false, status, url, models: [], ids: [], error: "empty free list" };
65
+ return { ok: true, status, url, models, ids: models.map((m) => m.id) };
66
+ } catch (err) {
67
+ return { ok: false, status: 0, url, models: [], ids: [], error: String(err?.message || err) };
68
+ } finally {
69
+ clearTimeout(timer);
70
+ }
71
+ }
@@ -41,14 +41,7 @@ export function createClineProvider({
41
41
  if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
42
42
 
43
43
  const rawKeys = collectApiKeysGeneric(id, apiKeys, apiKey, (pid) => loadProviderKeys(pid, file ? { file } : {}));
44
- // 同时兼容 cline 与 clinebot 两个 id 的 keys(用户可能配在任一)
45
- let extraKeys = [];
46
- try {
47
- const altId = id === "cline" ? "clinebot" : "cline";
48
- const alt = loadProviderKeys(altId, file ? { file } : {});
49
- if (alt.length) extraKeys = alt;
50
- } catch {}
51
- const allKeys = [...new Set([...rawKeys, ...extraKeys].map((k) => String(k).trim()).filter(Boolean))];
44
+ const allKeys = [...new Set(rawKeys.map((k) => String(k).trim()).filter(Boolean))];
52
45
  const hasRefresh = allKeys.some((k) => isRefreshToken(k, id));
53
46
  const ring = createKeyRing(allKeys.filter((k) => !isRefreshToken(k, id)), { cooldownMs });
54
47
 
@@ -78,14 +71,7 @@ export function createClineProvider({
78
71
  // 优先写 providerConfigs,兼容旧路径由 saveProviderConfig 处理
79
72
  saveProviderConfig(id, { baseUrl: resolvedBase, keys: next }, file ? { file } : {});
80
73
  }
81
- // altId 同步
82
- const altId = id === "cline" ? "clinebot" : "cline";
83
- const cur2 = loadProviderKeys(altId, file ? { file } : {});
84
- const idx2 = cur2.indexOf(oldRefreshToken);
85
- if (idx2 >= 0) {
86
- const next2 = [...cur2]; next2[idx2] = newRefreshToken;
87
- saveProviderConfig(altId, { baseUrl: resolvedBase, keys: next2 }, file ? { file } : {});
88
- }
74
+ // 只回写 cline 自己(旧 id 轮换回写已随 id 统一删除,见 .scratch/cline-unify/SPEC.md §4.1)
89
75
  } catch {}
90
76
  },
91
77
  });
@@ -3,6 +3,7 @@ import { compatFetch } from "../../compat.js";
3
3
  import { joinModelId } from "../model-id.js";
4
4
  import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
5
  import { dirname } from "node:path";
6
+ import { fetchFreeCatalog, catalogUrl, FALLBACK_FREE as BUNDLED_FREE } from "./free-catalog.js";
6
7
 
7
8
  const { UndiciFetch } = getUndici();
8
9
 
@@ -18,30 +19,21 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
18
19
  let cache = null;
19
20
  let fetchedAt = 0;
20
21
 
21
- // 离线兜底:与上游 recommended-models 的 free 对齐(2026-09-16 实测 5 个)。
22
- // 上游挂了/401 时也不返回空数组,保证 -provider clinebot models 与 picks 仍有免费可用。
23
- const FALLBACK_FREE = [
24
- { id: "cline-free/deepseek-v4.1-flash", name: "deepseek-v4.1-flash" },
25
- { id: "cline-free/muse-spark-1.3-contributor", name: "muse-spark-1.3-contributor" },
26
- { id: "z-ai/glm-5.3-flash", name: "glm-5.3-flash" },
27
- { id: "cline-free/solar-pro4", name: "solar-pro4" },
28
- { id: "poolside/laguna-s-2.1:free", name: "laguna-s-2.1:free" },
29
- ];
22
+ // 离线兜底:与上游 recommended-models 的 free 对齐(常量在 free-catalog.js,2026-09-20 实测 5 个)。
23
+ // 上游挂了/401 时也不返回空数组,保证 -provider cline models 与 picks 仍有免费可用。
30
24
 
31
25
  function fallbackList() {
32
- const out = FALLBACK_FREE.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
26
+ const out = BUNDLED_FREE.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
33
27
  cache = out; fetchedAt = Date.now();
34
28
  return out;
35
29
  }
36
30
 
37
- // 端点归一化:官方取 {bareHost}/api/v1/ai/cline/recommended-models。
38
- // baseUrl 可能是裸 host(https://api.cline.bot)也可能是带 /api/v1 的,
39
- // 统一收敛到 …/api/v1/ai/cline/recommended-models;用户自定义 path 原样尊重。
31
+ // 端点归一化:官方取 {bareHost}/api/v1/ai/cline/recommended-models(URL 拼装与 CLI 共用 free-catalog.catalogUrl)。
32
+ // baseUrl 可能是裸 host(https://api.cline.bot)也可能是带 /api/v1 的,统一收敛;用户自定义 path 原样尊重。
40
33
  function resolveModelsUrl() {
41
34
  const custom = modelsPath && modelsPath !== "/models" && modelsPath !== "/ai/cline/recommended-models";
42
35
  if (custom) return joinUrl(resolvedBase, resolvedPath);
43
- const bare = resolvedBase.replace(/\/api\/v1\/?$/, "");
44
- return joinUrl(bare, "/api/v1/ai/cline/recommended-models");
36
+ return catalogUrl(resolvedBase);
45
37
  }
46
38
 
47
39
  async function listModels() {
@@ -85,7 +77,7 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
85
77
  } catch {}
86
78
  }
87
79
 
88
- // Note: clinebot free 目录三处同源(CLI 直查/聚合目录/兜底)+ daemon 启动自检快照(diff 写 daemon.log,不自动改 picks)— 见 .agents/notes/implemented/bug-fix/2026-09-16-clinebot-free-catalog-unify.md
80
+ // Note: cline free 目录三处同源(CLI 直查/聚合目录/兜底,统一在 free-catalog.js)+ daemon 启动自检快照(diff 写 daemon.log,不自动改 picks)— 见 .agents/notes/implemented/bug-fix/2026-09-16-clinebot-free-catalog-unify.md
89
81
  function detectFreeChanges(ids) {
90
82
  const prev = readSnapshotFree();
91
83
  if (!prev) {
@@ -104,46 +96,31 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
104
96
  return { added, removed };
105
97
  }
106
98
 
107
- // 启动自检入口:拉 recommended-models → 填缓存 → 对比快照报 free 增删。
99
+ // 启动自检入口:拉 recommended-models → 填缓存 → 对比快照报 free 增删(拉取与 CLI 共用 free-catalog.fetchFreeCatalog)。
108
100
  async function checkFreeUpdates() {
109
- const url = resolveModelsUrl();
110
101
  const t0 = performance.now();
111
- try {
112
- const headers = { Accept: "application/json" };
113
- const key = (loadKeys ? loadKeys(id)[0] : null) || (ring ? ring.next() : null);
114
- if (key && !String(key).includes(".")) headers["Authorization"] = `Bearer ${key}`;
115
- const opts = { headers };
116
- if (dispatcher) opts.dispatcher = dispatcher;
117
- const res = await fetchImpl(url, opts);
118
- let freeIds = null;
119
- try {
120
- const text = await res.text().catch(() => "");
121
- if (res.ok && text && isClineBotHost(resolvedBase)) {
122
- const json = JSON.parse(text);
123
- if (Array.isArray(json.free)) {
124
- const valid = json.free.filter((m) => m && typeof m.id === "string");
125
- if (valid.length) {
126
- cache = valid.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
127
- fetchedAt = Date.now();
128
- freeIds = valid.map((m) => m.id);
129
- }
130
- }
131
- }
132
- } catch {}
133
- let change = null;
134
- if (freeIds && snapshotPath) { try { change = detectFreeChanges(freeIds); } catch {} }
135
- return {
136
- ok: res.ok,
137
- status: res.status,
138
- ms: Math.round(performance.now() - t0),
139
- ...(change?.added ? { freeAdded: change.added, freeRemoved: change.removed } : {}),
140
- };
141
- } catch (err) {
142
- return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
102
+ const key = (loadKeys ? loadKeys(id)[0] : null) || (ring ? ring.next() : null);
103
+ const authorization = key && !String(key).includes(".") ? `Bearer ${key}` : undefined;
104
+ const cat = await fetchFreeCatalog({ baseUrl: resolvedBase, fetchImpl, dispatcher, authorization });
105
+ const ms = () => Math.round(performance.now() - t0);
106
+ const httpOk = cat.status >= 200 && cat.status < 300;
107
+ if (!httpOk || !cat.models.length || !isClineBotHost(resolvedBase)) {
108
+ return { ok: httpOk, status: cat.status, ...(httpOk ? {} : { error: cat.error }), ms: ms() };
143
109
  }
110
+ const valid = cat.models;
111
+ cache = valid.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
112
+ fetchedAt = Date.now();
113
+ let change = null;
114
+ if (snapshotPath) { try { change = detectFreeChanges(valid.map((m) => m.id)); } catch {} }
115
+ return {
116
+ ok: true,
117
+ status: cat.status,
118
+ ms: ms(),
119
+ ...(change?.added ? { freeAdded: change.added, freeRemoved: change.removed } : {}),
120
+ };
144
121
  }
145
122
 
146
- // preheat 保留为别名:dispatcher 已不再调 clinebot,手动/测试/未来钩子仍可用,行为与自检一致
123
+ // preheat 保留为别名:dispatcher 已不再调 cline,手动/测试/未来钩子仍可用,行为与自检一致
147
124
  async function preheat() {
148
125
  return checkFreeUpdates();
149
126
  }
@@ -0,0 +1,148 @@
1
+ // 账号池:keyring 的 key = 凭证 blob JSON 字符串(一华为账号一 blob,多账号=多 key 自动轮转)。
2
+ // 职责:blob 解析;STS 凭证临期(默认提前 30min)单飞刷新;轮转写回(saveFn,原位换 key);
3
+ // 死号(终态失效)标记。瞬时失败保留旧凭证继续用,绝不清已存凭证(对齐 cline isInvalidGrant 纪律)。
4
+ import { refreshStsCredential } from "./sts.js";
5
+ import { REFRESH_SKEW_MS_DEFAULT } from "./const.js";
6
+
7
+ /** 校验并解析 blob 字符串 → 账号运行时对象;非 blob(如 Bearer key)返回 null。 */
8
+ export function accountFromBlob(raw) {
9
+ if (typeof raw !== "string" || !raw.trim().startsWith("{")) return null;
10
+ let j;
11
+ try { j = JSON.parse(raw); } catch { return null; }
12
+ if (!j || typeof j !== "object" || !j.refreshToken || !j.codeVerifier || !j.dpopJwk) return null;
13
+ const expMs = j.expiration ? Date.parse(j.expiration) : NaN;
14
+ return {
15
+ blob: raw,
16
+ userId: String(j.userId || j.user_id || ""),
17
+ userName: String(j.userName || j.user_name || ""),
18
+ domainId: String(j.domainId || j.domain_id || ""),
19
+ refreshToken: String(j.refreshToken || ""),
20
+ clientId: String(j.clientId || "codearts-agent"),
21
+ codeVerifier: String(j.codeVerifier || ""),
22
+ dpopJwk: j.dpopJwk || null,
23
+ accessKeyId: String(j.accessKeyId || ""),
24
+ secretAccessKey: String(j.secretAccessKey || ""),
25
+ securityToken: String(j.securityToken || ""),
26
+ expiration: String(j.expiration || ""),
27
+ expiresAtMs: Number.isFinite(expMs) ? expMs : 0,
28
+ dead: false,
29
+ lastError: "",
30
+ refreshInflight: null,
31
+ };
32
+ }
33
+
34
+ /** 账号对象 → 可持久化 blob(JSON 字符串;AK/SK/securityToken 是 STS 临时凭证快照)。 */
35
+ export function blobFromAccount(acc) {
36
+ return JSON.stringify({
37
+ userId: acc.userId,
38
+ userName: acc.userName,
39
+ domainId: acc.domainId,
40
+ refreshToken: acc.refreshToken,
41
+ clientId: acc.clientId,
42
+ codeVerifier: acc.codeVerifier,
43
+ dpopJwk: acc.dpopJwk,
44
+ accessKeyId: acc.accessKeyId,
45
+ secretAccessKey: acc.secretAccessKey,
46
+ securityToken: acc.securityToken,
47
+ expiration: acc.expiration,
48
+ });
49
+ }
50
+
51
+ export function createAuthPool({
52
+ keys = [],
53
+ fetchImpl,
54
+ stsHost,
55
+ skewMs = REFRESH_SKEW_MS_DEFAULT,
56
+ clock = () => Date.now(),
57
+ saveFn,
58
+ } = {}) {
59
+ const accounts = new Map(); // blob → account
60
+ for (const k of keys) {
61
+ const acc = accountFromBlob(k);
62
+ if (acc) accounts.set(k, acc);
63
+ }
64
+
65
+ function findByBlob(blob) { return accounts.get(blob) || null; }
66
+ function aliveAccounts() { return [...accounts.values()].filter((a) => !a.dead); }
67
+ function pickPrimary() { return aliveAccounts()[0] || null; }
68
+
69
+ async function doRefresh(acc) {
70
+ const r = await refreshStsCredential(
71
+ { refreshToken: acc.refreshToken, codeVerifier: acc.codeVerifier, dpopJwk: acc.dpopJwk, clientId: acc.clientId },
72
+ { fetchImpl, stsHost },
73
+ );
74
+ if (r.relogin) {
75
+ acc.dead = true;
76
+ acc.lastError = r.error || "re-login required";
77
+ return;
78
+ }
79
+ if (r.error) { acc.lastError = r.error; return; } // 瞬时失败:旧凭证还能用就继续用
80
+ // 成功:STS 字段更新(refreshToken 可能轮转),原位换 blob 写回
81
+ acc.accessKeyId = r.account.accessKeyId;
82
+ acc.secretAccessKey = r.account.secretAccessKey;
83
+ acc.securityToken = r.account.securityToken;
84
+ acc.expiration = r.account.expiration;
85
+ acc.expiresAtMs = r.account.expiresAtMs;
86
+ if (r.account.refreshToken) acc.refreshToken = r.account.refreshToken;
87
+ if (r.account.userId) acc.userId = r.account.userId;
88
+ if (r.account.userName) acc.userName = r.account.userName;
89
+ if (r.account.domainId) acc.domainId = r.account.domainId;
90
+ acc.lastError = "";
91
+ acc.dead = false;
92
+ const newBlob = blobFromAccount(acc);
93
+ if (newBlob !== acc.blob) {
94
+ const oldBlob = acc.blob;
95
+ accounts.delete(oldBlob);
96
+ acc.blob = newBlob;
97
+ accounts.set(newBlob, acc);
98
+ if (saveFn) { try { await saveFn({ oldBlob, newBlob, account: acc }); } catch {} }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * 取某账号的有效 STS 凭证(临期自动单飞刷新)。
104
+ * @throws {Error} blob 不在池内 / 死号(message 带 re-login 提示)
105
+ */
106
+ async function getCredential(blob) {
107
+ const acc = findByBlob(blob);
108
+ if (!acc) throw new Error("codearts: credential not in pool — restart daemon or re-login");
109
+ if (acc.dead) throw new Error(`codearts account ${acc.userId || "?"} dead: ${acc.lastError || "re-login required"} — run: mslxdff -provider codearts login`);
110
+ const now = clock();
111
+ const needRefresh = !acc.securityToken || !acc.accessKeyId
112
+ || (acc.expiresAtMs > 0 && now >= acc.expiresAtMs - skewMs);
113
+ if (needRefresh && !acc.refreshInflight) {
114
+ acc.refreshInflight = doRefresh(acc).finally(() => { acc.refreshInflight = null; });
115
+ }
116
+ if (acc.refreshInflight) await acc.refreshInflight;
117
+ if (acc.dead) throw new Error(`codearts account ${acc.userId || "?"} dead: ${acc.lastError || "re-login required"} — run: mslxdff -provider codearts login`);
118
+ return acc;
119
+ }
120
+
121
+ /** 强制下次刷新(401 后调用;清掉缓存的 STS 临时凭证)。 */
122
+ function invalidate(blob) {
123
+ const acc = findByBlob(blob);
124
+ if (!acc) return;
125
+ acc.securityToken = "";
126
+ acc.accessKeyId = "";
127
+ acc.secretAccessKey = "";
128
+ acc.expiresAtMs = 0;
129
+ }
130
+
131
+ function markDead(blob, reason) {
132
+ const acc = findByBlob(blob);
133
+ if (!acc) return;
134
+ acc.dead = true;
135
+ acc.lastError = reason || acc.lastError;
136
+ }
137
+
138
+ /** ring 轮转写回后的重排(saveFn 成功后由 index 调用;saveFn 已处理则幂等)。 */
139
+ function replace(oldBlob, newBlob) {
140
+ const acc = accounts.get(oldBlob);
141
+ if (!acc || oldBlob === newBlob) return;
142
+ accounts.delete(oldBlob);
143
+ acc.blob = newBlob;
144
+ accounts.set(newBlob, acc);
145
+ }
146
+
147
+ return { findByBlob, aliveAccounts, pickPrimary, getCredential, invalidate, markDead, replace, accounts: () => [...accounts.values()] };
148
+ }
@@ -0,0 +1,176 @@
1
+ // CodeArts 聊天服务:上游 body/头组装 + SDK-HMAC-SHA256 签名 + 401 刷新重试 + 环形切号。
2
+ // 上游恒 stream:true(chatBodyV2 契约);流式客户端转 OpenAI SSE,非流式聚合回 JSON。
3
+ import crypto from "node:crypto";
4
+ import { joinUrl } from "../base.js";
5
+ import { createTransport } from "../../transport/index.js";
6
+ import { SNAP_BASE, EP_CHAT_V2, chatBaseHeaders, MAAS_TYPE_HEADER, MAAS_BENEFIT } from "./const.js";
7
+ import { signRequest } from "./sign.js";
8
+ import { newChatId, preflightResponse, sseToOpenAIResponse, aggregateToCompletion, UpstreamEventError } from "./stream.js";
9
+
10
+ const MODEL_RULES = [
11
+ { match: (m) => String(m || "").toLowerCase().includes("deepseek"), scope: "all" },
12
+ { match: (m) => /(^kimi-|kimi)/i.test(String(m || "")), scope: "toolCalls" },
13
+ ];
14
+
15
+ // DeepSeek 系思考模式要求 assistant 消息回传 reasoning_content,缺失补 " " 占位(ADR-0001 同款)。
16
+ export function injectReasoningPlaceholder(messages, model) {
17
+ const rule = MODEL_RULES.find((r) => r.match(model));
18
+ if (!rule || !Array.isArray(messages)) return messages;
19
+ return messages.map((m) => {
20
+ if (m?.role !== "assistant") return m;
21
+ if (typeof m.reasoning_content === "string" && m.reasoning_content.length) return m;
22
+ if (rule.scope === "toolCalls" && !(Array.isArray(m.tool_calls) && m.tool_calls.length)) return m;
23
+ return { ...m, reasoning_content: " " };
24
+ });
25
+ }
26
+
27
+ const PASSTHROUGH_KEYS = ["temperature", "top_p", "max_tokens", "max_completion_tokens", "reasoning_effort", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"];
28
+
29
+ export function buildUpstreamBody(body, { chatId, upstreamModel }) {
30
+ const out = {
31
+ model: upstreamModel,
32
+ stream: true, // v2 端点契约:恒流式,非流式由本层聚合
33
+ messages: injectReasoningPlaceholder(body?.messages || [], upstreamModel),
34
+ prompt_cache_key: chatId,
35
+ tool_stream: true,
36
+ chat_id: chatId,
37
+ };
38
+ for (const k of PASSTHROUGH_KEYS) {
39
+ if (body?.[k] !== undefined && body?.[k] !== null) out[k] = body[k];
40
+ }
41
+ return out;
42
+ }
43
+
44
+ // Session-Id:由 chatId 派生的稳定会话标识(prompt cache 亲和,对齐 Go sessionIDFor)。
45
+ function sessionIdFor(chatId) {
46
+ return crypto.createHash("sha256").update(`codearts-session:${chatId}`, "utf8").digest("hex").slice(0, 32);
47
+ }
48
+
49
+ function stripOwnPrefix(model, providerId) {
50
+ const s = String(model || "");
51
+ const i = s.indexOf("/");
52
+ if (i <= 0) return s;
53
+ return s.slice(0, i).toLowerCase() === String(providerId || "").toLowerCase() ? s.slice(i + 1) : s;
54
+ }
55
+
56
+ export function createChatService({
57
+ id = "codearts",
58
+ baseUrl = SNAP_BASE,
59
+ chatPath = EP_CHAT_V2,
60
+ fetchImpl,
61
+ dispatcher,
62
+ authPool,
63
+ catalog,
64
+ connectTimeoutMs = 30_000,
65
+ clock = Date.now,
66
+ } = {}) {
67
+ const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "") || SNAP_BASE;
68
+ const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
69
+
70
+ async function sendOnce({ bodyStr, account, benefit, chatId, sessionId, traceId }) {
71
+ const url = joinUrl(resolvedBase, chatPath);
72
+ const h = chatBaseHeaders(account.securityToken, traceId);
73
+ if (benefit) h[MAAS_TYPE_HEADER] = MAAS_BENEFIT;
74
+ const { headers } = signRequest({ method: "POST", url, headers: h, body: bodyStr, cred: account });
75
+ // Chat-Id / Session-Id 在签名之后追加(对齐官方客户端:不进 SignedHeaders)。
76
+ headers["chat-id"] = chatId;
77
+ headers["session-id"] = sessionId;
78
+ return transport.request({ url, method: "POST", headers, body: bodyStr, stream: true, timeoutMs: connectTimeoutMs });
79
+ }
80
+
81
+ function errRes(status, msg, code) {
82
+ return new Response(JSON.stringify({ error: { message: msg, type: "upstream_error", ...(code ? { code } : {}) } }), {
83
+ status,
84
+ headers: { "Content-Type": "application/json" },
85
+ });
86
+ }
87
+
88
+ function withTiming(res, t0) {
89
+ try { res._t = { attempts: [], waitMs: 0, totalMs: Math.round(clock() - t0) }; } catch {}
90
+ return res;
91
+ }
92
+
93
+ /**
94
+ * 跑一次对话。body.model 已由 dispatcher 剥前缀(容忍自带前缀)。
95
+ * @returns {Promise<Response>} 流式 → OpenAI SSE;非流式 → chat.completion JSON
96
+ */
97
+ async function runChat(body, activeRing, opts = {}) {
98
+ const t0 = clock();
99
+ const requestedModel = String(body?.model || "");
100
+ const rawModel = stripOwnPrefix(requestedModel, id);
101
+ const upstreamModel = catalog?.canonical(rawModel) || rawModel;
102
+ const chatId = newChatId(body, opts);
103
+ const sessionId = sessionIdFor(chatId);
104
+ const upstreamBody = buildUpstreamBody(body, { chatId, upstreamModel });
105
+ const bodyStr = JSON.stringify(upstreamBody);
106
+ const isStream = body?.stream === true;
107
+ let lastErr = null;
108
+
109
+ const maxTries = Math.max(1, activeRing.size || 1);
110
+ for (let attempt = 0; attempt < maxTries; attempt++) {
111
+ const blob = activeRing.next();
112
+ if (!blob) break;
113
+ let account;
114
+ try {
115
+ account = await authPool.getCredential(blob);
116
+ } catch (err) {
117
+ try { activeRing.onError(blob); } catch {}
118
+ lastErr = err;
119
+ continue; // 死号/不在池:换下一个账号
120
+ }
121
+ const benefit = catalog?.isBenefit ? catalog.isBenefit(account.userId, upstreamModel) : false;
122
+ const traceId = crypto.randomBytes(8).toString("hex") + String(clock());
123
+ let res;
124
+ try {
125
+ res = await sendOnce({ bodyStr, account, benefit, chatId, sessionId, traceId });
126
+ } catch (err) {
127
+ try { activeRing.onError(blob); } catch {}
128
+ lastErr = err;
129
+ continue;
130
+ }
131
+ if (res.status === 401 || res.status === 403) {
132
+ // STS 临时凭证失效:强制刷新重试一次,仍 401/403 换号
133
+ authPool.invalidate(blob);
134
+ let account2 = null;
135
+ try { account2 = await authPool.getCredential(blob); } catch { /* 死号 → 换号 */ }
136
+ if (account2) {
137
+ const traceId2 = crypto.randomBytes(8).toString("hex") + String(clock());
138
+ try { res = await sendOnce({ bodyStr, account: account2, benefit, chatId, sessionId, traceId: traceId2 }); } catch (err2) { lastErr = err2; try { activeRing.onError(blob); } catch {} continue; }
139
+ if (res.status === 401 || res.status === 403) {
140
+ try { await res.text(); } catch {}
141
+ try { activeRing.onError(blob); } catch {}
142
+ lastErr = new Error(`codearts auth still failing after refresh (http ${res.status})`);
143
+ continue;
144
+ }
145
+ } else {
146
+ try { activeRing.onError(blob); } catch {}
147
+ continue;
148
+ }
149
+ }
150
+ if (res.status >= 400) {
151
+ const txt = await res.text().catch(() => "");
152
+ if (res.status === 429 || res.status >= 500) try { activeRing.onError(blob); } catch {}
153
+ return withTiming(errRes(res.status, `codearts upstream http ${res.status}: ${String(txt).slice(0, 300)}`), t0);
154
+ }
155
+ try {
156
+ const pre = await preflightResponse(res); // HTTP 200 内嵌错误(排队/未注册/福利未领)→ 抛 UpstreamEventError
157
+ if (isStream) return withTiming(sseToOpenAIResponse(pre, { model: requestedModel || upstreamModel, id: `chatcmpl-${chatId.slice(0, 12)}` }), t0);
158
+ const data = await aggregateToCompletion(pre, { model: requestedModel || upstreamModel, id: `chatcmpl-${chatId.slice(0, 12)}` });
159
+ return withTiming(new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" } }), t0);
160
+ } catch (err) {
161
+ if (err instanceof UpstreamEventError) {
162
+ if (err.status === 429 || err.status >= 500) try { activeRing.onError(blob); } catch {}
163
+ return withTiming(errRes(err.status, `codearts upstream: ${err.message}`, err.code), t0);
164
+ }
165
+ try { activeRing.onError(blob); } catch {}
166
+ lastErr = err;
167
+ continue;
168
+ }
169
+ }
170
+ throw Object.assign(lastErr || new Error("codearts: all accounts unavailable"), {
171
+ _t: { attempts: [], waitMs: 0, totalMs: Math.round(clock() - t0) },
172
+ });
173
+ }
174
+
175
+ return { runChat, buildUpstreamBody, injectReasoningPlaceholder, _sendOnce: sendOnce };
176
+ }
@@ -0,0 +1,60 @@
1
+ // CodeArts Agent 云端 API 常量(逆向自 HITZY2002/codearts2api,源自 vscode-codebot 26.7.0)。
2
+ // 端点契约见 docs/adr/0027-codearts-provider.md。
3
+
4
+ // 盘古引擎/聊天网关(product.json snapEngineDomain,商业版 cn-north-4)。
5
+ export const SNAP_BASE = "https://snap-access.cn-north-4.myhuaweicloud.com";
6
+ // STS 令牌端点(refresh_token 与 client_id + DPoP 公钥 + code_verifier 三重绑定)。
7
+ export const STS_HOST = "https://sts.cn-north-4.myhuaweicloud.com";
8
+ // 限时福利(免费套餐)网关(模型发现 + 领取)。
9
+ export const BENEFIT_HOST = "https://opengw.developer.huaweicloud.com";
10
+ // OAuth authorize 入口(注意带 /portal)。
11
+ export const PORTAL_HOST = "https://codearts.huaweicloud.com/portal";
12
+
13
+ export const EP_CHAT_V2 = "/api/v2/chat/completions";
14
+ export const EP_MODEL_BUILTIN = "/v1/model/builtin";
15
+ export const EP_AGENT_LIST = "/v1/agent-center/agents/useragents";
16
+ export const EP_AGENT_DETAIL = "/v1/agent-center/agents/detail";
17
+ export const EP_BENEFIT_CONFIG = "/api/v1/gateway/config";
18
+ export const EP_BENEFIT_CLAIM = "/api/v1/benefit/claim";
19
+ export const EP_OAUTH_TOKENS = "/v1/oauth2/tokens";
20
+ export const EP_LOGIN_TICKET = "/v1/login/ticket";
21
+ export const SNAP_MANAGER_PATH = "/snap-manager";
22
+
23
+ // OAuth client:官方 CodeArts Agent 插件的 uri_scheme;refresh_token 与之绑定。
24
+ export const CLIENT_ID = "codearts-agent";
25
+ // ticket 轮询的插件标识(逆向自 huaweicloud.authentication 扩展)。
26
+ export const PLUGIN_NAME = "snap_AIIDE";
27
+ export const PLUGIN_VERSION = "5.2.0";
28
+
29
+ // 聊天路由头(官方 AgentKernel 形状;均进入 SDK-HMAC-SHA256 签名)。
30
+ export function chatBaseHeaders(securityToken, traceId) {
31
+ return {
32
+ "content-type": "application/json",
33
+ "accept": "text/event-stream",
34
+ "x-auth-token": securityToken || "",
35
+ "x-snap-traceid": traceId,
36
+ "x-language": "zh-cn",
37
+ "app-id": "CodeAgent3.0",
38
+ "is_confidential": "false",
39
+ };
40
+ }
41
+
42
+ // 福利路由头:必须在 signRequest 之前设置(计入 SignedHeaders),否则 InferHub.002002009.404。
43
+ export const MAAS_TYPE_HEADER = "maas_type";
44
+ export const MAAS_BENEFIT = "benefit";
45
+
46
+ // 冷启动种子:福利套餐轮换后新模型在首次发现前也按福利路由(宁多带不少带,少带即 404)。
47
+ export const SEED_BENEFIT_MODELS = ["deepseek-v4-flash-0731", "deepseek-v4-pro-0813", "glm-5.3-flash"];
48
+ // 内置模型种子(仅用于大小写归一,不标福利)。
49
+ export const SEED_KNOWN_MODELS = [
50
+ "GLM-5.2", "GLM-5.2-ArkTS-SPARK", "GLM-5.1", "GLM-4.7",
51
+ "OpenPangu-2.0-Pro", "OpenPangu-2.0-Flash",
52
+ "Qwen3-VL-235B", "Qwen3.5-397B-A17B-VL", "Qwen3.6-27B-VL",
53
+ ];
54
+
55
+ // 目录 TTL:超过则建议重新发现(对齐 codearts2api CatalogTTL)。
56
+ export const CATALOG_TTL_MS = 60 * 60 * 1000;
57
+ // listModels 缓存(与全仓 10min 对齐)。
58
+ export const MODELS_CACHE_TTL_MS = 10 * 60 * 1000;
59
+ // STS 凭证提前刷新量(对齐 codearts2api RefreshSkew 30min)。
60
+ export const REFRESH_SKEW_MS_DEFAULT = 30 * 60 * 1000;