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
@@ -0,0 +1,81 @@
1
+ // mslxdff -provider qoder checkin [--json] [--region cn|global] [--any] [--dry]
2
+ // 四态反馈:空(无账号→引导 login)→ 执行中(逐号打印)→ 成功 ✅ / 失败 ❌ + 下一步。
3
+ // 账号单一源 = auths/qoder-<uid>.json(含每号 region);device token 直接取该文件。
4
+ import { normalizeProviderId } from "../../../providers/model-id.js";
5
+ import { normalizeRegion } from "../../../providers/qoder/constants.js";
6
+ import { compatFetch } from "../../../compat.js";
7
+
8
+ function parseFlags(rest) {
9
+ const has = (...names) => names.some((n) => rest.includes(n));
10
+ const regionArg = (() => {
11
+ const i = rest.findIndex((x) => x === "--region");
12
+ return i >= 0 ? String(rest[i + 1] || "") : "";
13
+ })();
14
+ return {
15
+ json: has("--json"),
16
+ dry: has("--dry", "--dry-run"),
17
+ any: has("--any", "--promo"),
18
+ region: regionArg ? normalizeRegion(regionArg) : "",
19
+ badRegion: Boolean(regionArg) && !["cn", "global"].includes(String(regionArg).trim().toLowerCase()),
20
+ };
21
+ }
22
+
23
+ export async function handleQoderCheckin(id, sub, rest = [], deps = {}) {
24
+ if (normalizeProviderId(id) !== "qoder") return false;
25
+ if (sub !== "checkin" && sub !== "check-in" && sub !== "signin") return false;
26
+ const log = deps.log || ((m) => console.log(m));
27
+ const err = deps.err || ((m) => console.error(m));
28
+ const fetchImpl = deps.fetchImpl || compatFetch;
29
+ const flags = parseFlags(rest.slice(1));
30
+ if (flags.badRegion) { err("usage: mslxdff -provider qoder checkin [--json] [--region cn|global] [--any] [--dry]"); if (!deps.noExit) process.exit(1); return true; }
31
+
32
+ const accounts = deps.accounts
33
+ || (await import("../../../providers/qoder/account-store.js")).listAccountDocs()
34
+ .map((d) => ({ uid: d.uid, deviceToken: d.doc?.auth?.deviceToken || "", region: d.doc?.auth?.region || "global", name: d.doc?.account?.name || "" }));
35
+ const picked = flags.region ? accounts.filter((a) => normalizeRegion(a.region) === flags.region) : accounts;
36
+
37
+ // 空状态:不留白
38
+ if (!picked.length) {
39
+ log(accounts.length
40
+ ? `qoder 签到:没有 ${flags.region} 区账号(现有 ${accounts.length} 个:${accounts.map((a) => normalizeRegion(a.region)).join("/")})`
41
+ : "qoder 签到:还没有账号");
42
+ log(" 下一步:mslxdff -provider qoder login [--region cn|global]");
43
+ if (!deps.noExit) process.exit(accounts.length ? 0 : 1);
44
+ return true;
45
+ }
46
+
47
+ const { runCheckin, fetchQuota } = await import("../../../providers/qoder/checkin.js");
48
+ log(`Qoder 签到 · ${picked.length} 个账号(cn ${picked.filter((a) => normalizeRegion(a.region) === "cn").length} / global ${picked.filter((a) => normalizeRegion(a.region) === "global").length})${flags.dry ? " · 干跑(只查不领)" : ""}`);
49
+ const results = [];
50
+ for (let i = 0; i < picked.length; i++) {
51
+ const a = picked[i];
52
+ const tag = `${String(a.uid).slice(0, 8)}…(${normalizeRegion(a.region)})`;
53
+ if (!a.deviceToken) {
54
+ results.push({ uid: a.uid, region: normalizeRegion(a.region), ok: false, status: "no_token", message: "无 device token" });
55
+ log(` ❌ [${i + 1}/${picked.length}] ${tag} 无 device token → 重新 login`);
56
+ continue;
57
+ }
58
+ log(` ⏳ [${i + 1}/${picked.length}] ${tag} 签到中…`);
59
+ const r = flags.dry
60
+ ? { ...(await runCheckin({ deviceToken: a.deviceToken, region: a.region, fetchImpl: deps.fetchImpl, allowPromo: flags.any, probeOnly: true })), dry: true }
61
+ : await runCheckin({ deviceToken: a.deviceToken, region: a.region, fetchImpl: deps.fetchImpl, allowPromo: flags.any });
62
+ const quota = await fetchQuota({ deviceToken: a.deviceToken, region: a.region, fetchImpl: deps.fetchImpl }).catch(() => ({ ok: false }));
63
+ r.quota = quota.ok ? `${quota.remaining}/${quota.total} ${quota.unit}${quota.exhausted ? "(已耗尽)" : ""}` : "余额不可用";
64
+ r.name = a.name;
65
+ results.push(r);
66
+ const icon = r.ok ? (r.status === "claimed" ? "✅" : "⚠️") : "❌";
67
+ const stats = r.streak || r.totalDays ? ` · 连续 ${r.streak} 天 · 累计 ${r.totalDays} 天 · 共 ${r.totalCredits} 积分` : "";
68
+ log(` ${icon} [${i + 1}/${picked.length}] ${tag} ${r.message}${stats} · 余额 ${r.quota}`);
69
+ }
70
+
71
+ const okCount = results.filter((r) => r.ok).length;
72
+ const claimed = results.filter((r) => r.status === "claimed").length;
73
+ if (flags.json) {
74
+ log(JSON.stringify({ ok: okCount, total: results.length, claimed, results }, null, 2));
75
+ } else {
76
+ log(`\n结果:${okCount}/${results.length} 正常${claimed ? ` · 新领取 ${claimed} 个` : ""}`);
77
+ if (claimed) log(" 积分到账可能有延迟(上游记账异步),可用 -provider qoder checkin 复查余额");
78
+ }
79
+ if (!deps.noExit) process.exit(results.some((r) => r.status === "error" || r.status === "no_token") ? 1 : 0);
80
+ return true;
81
+ }
@@ -0,0 +1,103 @@
1
+ // mslxdff -provider qoder login — Qoder 设备授权(PKCE + poll),对齐 cline-login/traework-login。
2
+ // 授权后落盘 auths/qoder-<uid>.json + state.json,完成后进程退出。
3
+ import { compatFetch, timeoutSignal } from "../../../compat.js";
4
+ import { normalizeProviderId } from "../../../providers/model-id.js";
5
+ import { pkce, hex16, buildLoginUrl, waitForAuth, fetchUserInfo } from "../../../providers/qoder/oauth.js";
6
+
7
+ const REGIONS = ["global", "cn"];
8
+
9
+ export async function handleQoderLogin(id, sub, rest = [], deps = {}) {
10
+ if (normalizeProviderId(id) !== "qoder") return false;
11
+ if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
12
+ const fetchImpl = deps.fetchImpl || compatFetch;
13
+ const log = deps.log || ((m) => console.log(m));
14
+
15
+ // 可选参数:--region cn 或 --region global(默认 global)
16
+ const regionIdx = rest.indexOf("--region") >= 0 ? rest.indexOf("--region") : rest.indexOf("-r");
17
+ const region = regionIdx >= 0 ? String(rest[regionIdx + 1] || "global").trim() : "global";
18
+ const normRegion = region === "cn" ? "cn" : "global";
19
+
20
+ const { verifier, challenge } = pkce();
21
+ const nonce = hex16();
22
+ const loginUrl = buildLoginUrl({ region: normRegion, nonce, challenge });
23
+
24
+ log("=".repeat(60));
25
+ log(" Qoder 登录");
26
+ log(` Region: ${normRegion === "cn" ? "中国站 (qoder.com.cn)" : "国际站 (qoder.com)"}`);
27
+ log("=".repeat(60));
28
+ log("");
29
+ log(" 1. 在浏览器打开下面链接,用 Qoder 账号登录(OAuth 授权)");
30
+ log(" 2. 登录成功后会自动完成授权,请等待回显");
31
+ log("");
32
+ log(` ${loginUrl}`);
33
+ log("");
34
+ log("🔄 等待你授权(自动轮询,每 3 秒,最多 10 分钟)...");
35
+
36
+ let bundle;
37
+ try {
38
+ bundle = await waitForAuth({ region: normRegion, nonce, verifier, fetchImpl, log });
39
+ } catch (e) {
40
+ console.error(`\n❌ ${e.message}`);
41
+ process.exit(1);
42
+ }
43
+ if (!bundle || !bundle.deviceToken) {
44
+ console.error("\n❌ 未获取到 device token");
45
+ process.exit(1);
46
+ }
47
+
48
+ log(`\n✅ 授权成功!deviceToken=${bundle.deviceToken.slice(0, 12)}...`);
49
+ if (bundle.refreshToken) log(` refreshToken=${bundle.refreshToken.slice(0, 12)}...`);
50
+
51
+ // 取 userinfo
52
+ let user = { uid: "", name: "", email: "", userType: "personal_standard", organizationId: "", organizationName: "" };
53
+ try {
54
+ user = await fetchUserInfo({ region: normRegion, deviceToken: bundle.deviceToken, fetchImpl });
55
+ log(` UID: ${user.uid} 昵称: ${user.name || "(空)"} 邮箱: ${user.email || "(空)"}`);
56
+ } catch (e) {
57
+ log(` ⚠️ userinfo 失败: ${e.message}(使用 device token 前 8 位兜底 uid)`);
58
+ user.uid = bundle.deviceToken.slice(0, 8);
59
+ }
60
+ if (!user.uid) {
61
+ console.error("❌ 无法获取用户标识");
62
+ process.exit(1);
63
+ }
64
+
65
+ // 落盘
66
+ const { saveQoderAccount } = await import("../../../providers/qoder/account-store.js");
67
+ const save = deps.saveAccount || saveQoderAccount;
68
+ const saved = await save({
69
+ uid: user.uid,
70
+ deviceToken: bundle.deviceToken,
71
+ refreshToken: bundle.refreshToken || "",
72
+ region: normRegion,
73
+ name: user.name,
74
+ email: user.email,
75
+ userType: user.userType,
76
+ organizationId: user.organizationId,
77
+ organizationName: user.organizationName,
78
+ });
79
+
80
+ log("");
81
+ log("=".repeat(60));
82
+ log(`✅ 登录完成!`);
83
+ log(` UID: ${user.uid}`);
84
+ log(` 账号数: ${saved.accounts}${saved.updated ? "(旧号凭证已更新)" : ""}`);
85
+ log(` 数据文件: ${saved.file}`);
86
+ log("=".repeat(60));
87
+ // 默认 allowAny on(免费福利供应商,对齐 codearts 惯例)
88
+ try {
89
+ const { saveProviderAllowAnyModels } = await import("../../../state.js");
90
+ saveProviderAllowAnyModels("qoder", true);
91
+ log(" allowAnyModels=on(免费福利供应商默认放行;收紧:mslxdff -provider qoder allowAny off)");
92
+ } catch {}
93
+
94
+ log("下一步:");
95
+ log(" mslxdff -provider qoder models 列模型列表");
96
+ log(" mslxdff -restart(用户终端跑) 重启网关使新账号生效");
97
+ log(" 然后 curl http://localhost:8989/v1/chat/completions -d '{\"model\":\"qoder/auto\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'");
98
+ log("");
99
+ log("多账号:重复 `mslxdff -provider qoder login` 追加,每号独立 uid,对话自动轮换");
100
+
101
+ if (!deps.noExit) process.exit(0);
102
+ return true;
103
+ }
@@ -0,0 +1,202 @@
1
+ // mslxdff -provider traework login — 复刻 traework2api login.sh。
2
+ // 随机 hex16 machine/device id → 构造 trae.cn 授权链接 → 打印 → stdin 读回调链接 →
3
+ // 解析 refreshToken/userInfo/userJwt → ExchangeToken → GetUserInfo → 落盘 → 自动签到+查积分。
4
+ import { randomBytes } from "node:crypto";
5
+ import { compatFetch, timeoutSignal } from "../../../compat.js";
6
+ import { CLIENT_ID, IDE_VERSION, OAUTH_HOST } from "../../../providers/traework/constants.js";
7
+ import { normalizeExpiresAt } from "../../../providers/traework/token.js";
8
+
9
+ const API_HOST = OAUTH_HOST;
10
+ const APP_VERSION = IDE_VERSION;
11
+
12
+ function hex(n) { return randomBytes(n).toString("hex"); }
13
+
14
+ function readStdinLine(prompt) {
15
+ return new Promise((resolve) => {
16
+ try { process.stdout.write(`${prompt}`); } catch {}
17
+ let data = "";
18
+ const onData = (chunk) => { data += String(chunk || ""); if (data.includes("\n")) done(); };
19
+ const done = () => {
20
+ try { process.stdin.removeListener("data", onData); } catch {}
21
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {}
22
+ try { process.stdin.pause(); } catch {}
23
+ resolve(data.trim());
24
+ };
25
+ try {
26
+ process.stdin.resume();
27
+ process.stdin.setEncoding("utf8");
28
+ process.stdin.on("data", onData);
29
+ process.stdin.on("end", done);
30
+ } catch { resolve(""); }
31
+ });
32
+ }
33
+ export function buildLoginUrl({ machineId, deviceId, traceId = hex(8) } = {}) {
34
+ const params = new URLSearchParams({
35
+ login_version: "1",
36
+ auth_from: "solo",
37
+ login_channel: "native_ide",
38
+ plugin_version: "2.3.62834",
39
+ auth_type: "local",
40
+ client_id: CLIENT_ID,
41
+ redirect: "0",
42
+ login_trace_id: traceId,
43
+ auth_callback_url: "http://127.0.0.1:18080/authorize",
44
+ machine_id: machineId,
45
+ device_id: deviceId,
46
+ x_device_id: deviceId,
47
+ x_machine_id: machineId,
48
+ x_device_brand: "PC",
49
+ x_device_type: "PC",
50
+ x_os_version: "1.0",
51
+ x_app_version: APP_VERSION,
52
+ x_app_type: "stable",
53
+ });
54
+ return `https://www.trae.cn/authorization?${params.toString()}`;
55
+ }
56
+
57
+ // 解析回调链接:parse_qs + 双层 unquote 容错 → { refreshToken, userInfo, userJwt }。
58
+ export function parseCallbackUrl(url) {
59
+ let u;
60
+ try { u = new URL(String(url || "").trim()); } catch { throw new Error("回调链接不是合法 URL,请复制浏览器地址栏完整链接"); }
61
+ const qs = u.searchParams;
62
+ const rawRefresh = qs.get("refreshToken") || "";
63
+ const parseJsonParam = (raw) => {
64
+ if (!raw) return {};
65
+ for (const v of [raw, decodeURIComponent(raw)]) {
66
+ try { const o = JSON.parse(v); if (o && typeof o === "object") return o; } catch {}
67
+ }
68
+ return {};
69
+ };
70
+ const userInfo = parseJsonParam(qs.get("userInfo") || "");
71
+ const userJwt = parseJsonParam(qs.get("userJwt") || "");
72
+ let refreshToken = rawRefresh || String(userJwt.RefreshToken || "");
73
+ return {
74
+ refreshToken,
75
+ userInfo: { uid: String(userInfo.UserID || ""), nickname: String(userInfo.ScreenName || ""), enterpriseId: String(userInfo.TenantID || "") },
76
+ userJwt: { token: String(userJwt.Token || ""), refreshToken: String(userJwt.RefreshToken || ""), expiresAt: Number(userJwt.TokenExpireAt) || 0 },
77
+ };
78
+ }
79
+
80
+ async function postJson(fetchImpl, url, obj, headers = {}) {
81
+ const res = await fetchImpl(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": `Trae/${APP_VERSION}`, ...headers }, body: JSON.stringify(obj), signal: timeoutSignal(30000) });
82
+ const txt = await res.text();
83
+ let j;
84
+ try { j = JSON.parse(txt); } catch { throw new Error(`上游返回非 JSON: ${txt.slice(0, 200)}`); }
85
+ if (res.status >= 400) throw new Error(`上游 HTTP ${res.status}: ${txt.slice(0, 200)}`);
86
+ return j;
87
+ }
88
+
89
+ export async function handleTraeworkLogin(id, sub, rest = [], deps = {}) {
90
+ if (String(id || "").toLowerCase() !== "traework") return false;
91
+ if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
92
+ const fetchImpl = deps.fetchImpl || compatFetch;
93
+ const log = deps.log || ((m) => console.log(m));
94
+ const machineId = hex(16);
95
+ const deviceId = hex(16);
96
+ const loginUrl = buildLoginUrl({ machineId, deviceId });
97
+ log("=".repeat(60));
98
+ log(" TRAE SOLO 登录");
99
+ log("=".repeat(60));
100
+ log("");
101
+ log(" 1. 在浏览器打开下面链接,用手机号/验证码登录");
102
+ log(" 2. 登录成功后浏览器会跳到打不开的 127.0.0.1 地址");
103
+ log(" 3. 复制浏览器地址栏的完整链接,粘贴到下面");
104
+ log("");
105
+ log(` ${loginUrl}`);
106
+ log("");
107
+ let callback;
108
+ if (deps.callbackUrl) callback = deps.callbackUrl;
109
+ else callback = await readStdinLine("登录完成后,请粘贴浏览器地址栏的完整回调链接: ");
110
+ if (!callback || !String(callback).trim()) { console.error("未输入回调链接,已取消"); process.exit(1); }
111
+ const parsed = parseCallbackUrl(callback);
112
+ let token = "";
113
+ let refreshToken = parsed.refreshToken;
114
+ let expiresAt = 0;
115
+ if (refreshToken) {
116
+ const resp = await postJson(fetchImpl, `${API_HOST}/cloudide/api/v3/trae/oauth/ExchangeToken`, { ClientID: CLIENT_ID, RefreshToken: refreshToken, ClientSecret: "-", UserID: "" });
117
+ const result = resp?.Result || {};
118
+ token = result.Token || "";
119
+ if (!token) { console.error(`ExchangeToken 失败: ${JSON.stringify(resp).slice(0, 300)}`); process.exit(1); }
120
+ refreshToken = result.RefreshToken || refreshToken;
121
+ expiresAt = normalizeExpiresAt(result.TokenExpireAt || 0);
122
+ if (!(expiresAt > Math.floor(Date.now() / 1000))) expiresAt = Math.floor(Date.now() / 1000) + (Number(result.TokenExpireDuration) || 1209600);
123
+ log("ExchangeToken 成功");
124
+ } else {
125
+ token = parsed.userJwt.token;
126
+ expiresAt = normalizeExpiresAt(parsed.userJwt.expiresAt);
127
+ if (!token) { console.error("回调链接缺少 refreshToken,且 userJwt 也没有 Token"); process.exit(1); }
128
+ log("无 refreshToken,使用 userJwt 的 Token 兜底");
129
+ }
130
+ let uid = parsed.userInfo.uid;
131
+ let nickname = parsed.userInfo.nickname;
132
+ let enterpriseId = parsed.userInfo.enterpriseId;
133
+ try {
134
+ const ui = await postJson(fetchImpl, `${API_HOST}/cloudide/api/v3/trae/GetUserInfo`, { ReqSource: "IDE", IDEVersion: APP_VERSION }, { "x-cloudide-token": token });
135
+ const u = ui?.Result || ui || {};
136
+ if (u.UserID) { uid = String(u.UserID); nickname = String(u.ScreenName || nickname); enterpriseId = String(u.EnterpriseID || enterpriseId); }
137
+ } catch (e) { log(`GetUserInfo 失败(使用回调 userInfo): ${e.message}`); }
138
+ if (!uid) { console.error("未能获取 uid,请检查 token 是否有效"); process.exit(1); }
139
+ const { saveTraeworkAccount } = await import("../../../providers/traework/account-store.js");
140
+ const save = deps.saveAccount || saveTraeworkAccount;
141
+ const saved = await save({ uid, enterpriseId, nickname, accessToken: token, refreshToken, expiresAt, domain: "trae.cn", apiHost: API_HOST, machineId, deviceId });
142
+ // 自动签到 + 查积分
143
+ try {
144
+ const { checkinStatus, checkinClaim, entUsage } = await import("../../../providers/traework/checkin.js");
145
+ const cred = { accessToken: token, deviceId };
146
+ const st = await checkinStatus({ cred, fetchImpl });
147
+ log(`签到状态: checked_in=${st.checkedIn} credits=${st.credits} enable=${st.enable}`);
148
+ if (!st.checkedIn && st.enable) {
149
+ await checkinClaim({ cred, fetchImpl });
150
+ log("签到: success");
151
+ }
152
+ const ent = await entUsage({ cred, fetchImpl });
153
+ log(`当前积分: ${ent.remain}`);
154
+ } catch (e) { log(`签到/查积分: ${String(e?.message || e).slice(0, 200)}`); }
155
+ log("");
156
+ log("=".repeat(60));
157
+ log(" 登录完成!");
158
+ log(` UID: ${uid}`);
159
+ log(` Nickname: ${nickname || "(未获取到)"}`);
160
+ log(` 账号数: ${saved.accounts}`);
161
+ log("=".repeat(60));
162
+ log("下一步:mslxdff -restart(用户终端跑),然后 mslxdff -provider traework models 验证");
163
+ if (!deps.noExit) process.exit(0);
164
+ return true;
165
+ }
166
+
167
+ // 手动签到:-provider traework checkin
168
+ export async function handleTraeworkCheckin(id, sub, rest = [], deps = {}) {
169
+ if (String(id || "").toLowerCase() !== "traework") return false;
170
+ if (sub !== "checkin") return false;
171
+ const fetchImpl = deps.fetchImpl || compatFetch;
172
+ const log = deps.log || ((m) => console.log(m));
173
+ const { loadProviderConfig, loadProviderKeys, defaultStateFile } = await import("../../../state.js");
174
+ const cfg = loadProviderConfig("traework");
175
+ const keys = deps.keys || loadProviderKeys("traework");
176
+ const authList = deps.auths || cfg?.auths || [];
177
+ if (!keys.length || !authList.length) { console.error("没有 traework 账号,请先 mslxdff -provider traework login"); process.exit(1); }
178
+ const { checkinStatus, checkinClaim, entUsage } = await import("../../../providers/traework/checkin.js");
179
+ for (let i = 0; i < Math.min(keys.length, authList.length); i++) {
180
+ const auth = authList[i];
181
+ const at = keys[i];
182
+ if (!auth?.uid || !at) continue;
183
+ const cred = { accessToken: at, deviceId: auth?.deviceId || "", uid: auth.uid };
184
+ log(`账号 ${auth.uid.slice(0, 8)}...`);
185
+ try {
186
+ const st = await checkinStatus({ cred, fetchImpl });
187
+ log(` 签到状态: checked_in=${st.checkedIn} credits=${st.credits} enable=${st.enable}`);
188
+ if (!st.checkedIn && st.enable) {
189
+ await checkinClaim({ cred, fetchImpl });
190
+ log(` 签到: success`);
191
+ const st2 = await checkinStatus({ cred, fetchImpl });
192
+ log(` 签到后积分: ${st2.credits}`);
193
+ }
194
+ const ent = await entUsage({ cred, fetchImpl });
195
+ log(` 总积分: ${ent.remain}`);
196
+ } catch (e) {
197
+ log(` 失败: ${String(e?.message || e).slice(0, 120)}`);
198
+ }
199
+ }
200
+ if (!deps.noExit) process.exit(0);
201
+ return true;
202
+ }
@@ -1,5 +1,5 @@
1
1
  import { compatFetch, timeoutSignal } from "../../../compat.js";
2
- // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 clinebot login)。
2
+ // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 cline login)。
3
3
  // 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
4
4
  // 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
5
5
  // 不走 whistle/MITM,不依赖 codebuddy CLI,自然支持多账号追加。
@@ -126,12 +126,15 @@ export async function handleWorkbuddy(args) {
126
126
  auths.splice(rmIdx,1); keys.splice(rmIdx,1);
127
127
  saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl||"https://copilot.tencent.com", keys, auths });
128
128
  if (!keep) {
129
+ // 旧实现只删 cwd/auths 那一份,留下的旧副本会被读取兜底"复活"账号 → 扫所有候选目录
129
130
  try {
130
131
  const { existsSync, unlinkSync } = await import("node:fs");
131
132
  const { join } = await import("node:path");
132
- const dir = process.env.WORKBUDDY_AUTH_DIR || join(process.cwd(), "auths");
133
- const fp = join(dir, `workbuddy-${uid}.json`);
134
- if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
133
+ const { resolveAuthDirs } = await import("../../providers/workbuddy/account-store.js");
134
+ for (const dir of resolveAuthDirs()) {
135
+ const fp = join(dir, `workbuddy-${uid}.json`);
136
+ if (existsSync(fp)) { unlinkSync(fp); console.log(`removed file ${fp}`); }
137
+ }
135
138
  } catch {}
136
139
  }
137
140
  try { const { getBalanceCache } = await import("../../providers/workbuddy-balance.js"); getBalanceCache().delete(uid); } catch {}
@@ -155,24 +158,14 @@ async function loadWorkbuddyAccounts() {
155
158
  })).filter((it) => it.uid && it.key);
156
159
  if (items.length) return items;
157
160
  try {
158
- const { readdirSync, readFileSync, existsSync } = await import("node:fs");
159
- const { join } = await import("node:path");
160
- const { resolveAuthDir } = await import("../../providers/workbuddy/account-store.js");
161
- const dir = resolveAuthDir();
162
- if (existsSync(dir)) {
163
- for (const f of readdirSync(dir).filter((x) => x.startsWith("workbuddy-") && x.endsWith(".json"))) {
164
- try {
165
- const j = JSON.parse(readFileSync(join(dir, f), "utf8"));
166
- if (j?.account?.uid && j?.auth?.accessToken) {
167
- const domain = j.auth.domain || "www.codebuddy.cn";
168
- items.push({
169
- uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "",
170
- key: j.auth.accessToken,
171
- auth: { uid: j.account.uid, domain, enterpriseId: j.account.enterpriseId || "", refreshToken: j.auth.refreshToken || "" },
172
- });
173
- }
174
- } catch {}
175
- }
161
+ const { listAccountDocs } = await import("../../providers/workbuddy/account-store.js");
162
+ for (const { uid, doc } of listAccountDocs()) {
163
+ const domain = doc.auth.domain || "www.codebuddy.cn";
164
+ items.push({
165
+ uid, domain, enterpriseId: doc.account.enterpriseId || "",
166
+ key: doc.auth.accessToken,
167
+ auth: { uid, domain, enterpriseId: doc.account.enterpriseId || "", refreshToken: doc.auth.refreshToken || "" },
168
+ });
176
169
  }
177
170
  } catch {}
178
171
  return items;
package/src/cli/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { assertMinNode } from "../compat.js";
4
5
 
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  const VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8")).version;
7
8
 
8
9
  // deep CLI entry — single inlet: run(args) — hides all command branching inside
9
10
  export async function run(args = process.argv.slice(2)) {
11
+ // 运行环境门(强制):项目要求 Node >=18(ADR-0024),不满足则人话退出
12
+ if (!assertMinNode()) process.exit(1);
10
13
  const { handleHelp, handleUpdate, handleRefreshToken, handleShowToken, handleUninstall, handleLog, handlePlugins, handleChat, handleStatus, handleFree, handleAutostart } = await import("./commands/system.js");
11
14
  if (await handleHelp(args, VERSION)) return;
12
15
  if (await handleUpdate(args, VERSION)) return;
@@ -1,9 +1,21 @@
1
- // 交互式选择器:↑/↓ 移动,Enter 确认,q/Esc 取消;ANSI 原地重绘
1
+ // 交互式选择器:↑/↓ 移动,←/→ 翻页,Enter 确认,q/Esc 取消;ANSI 原地重绘。
2
+ // 页高按终端行数自适应(items ≤ 页高时不分页,行为与旧版全量渲染一致)。
3
+ const PAGE_RESERVE = 6; // 页头 1 + 帮助 2 + 余量
4
+
5
+ function pageSizeFor(items) {
6
+ const rows = Number(process.stdout?.rows) || 24;
7
+ const size = Math.max(4, rows - PAGE_RESERVE);
8
+ return items.length > size ? size : items.length || 1;
9
+ }
10
+
2
11
  export async function pickInteractive(items, startCursor = 0) {
3
12
  const { renderChooser, renderChooserHelp, parseKey } = await import("../chooser.js");
13
+ const size = pageSizeFor(items);
14
+ let page = 0;
4
15
  let cursor = Math.min(Math.max(startCursor, 0), items.length - 1);
5
16
  const draw = () => {
6
- const lines = [...renderChooser(items, cursor), ...renderChooserHelp()];
17
+ const usePage = size < items.length;
18
+ const lines = [...renderChooser(items, cursor, usePage ? { index: page, size } : {}), ...renderChooserHelp(false, usePage)];
7
19
  process.stdout.write("\x1b[2J\x1b[H" + lines.join("\n"));
8
20
  };
9
21
  draw();
@@ -19,11 +31,22 @@ export async function pickInteractive(items, startCursor = 0) {
19
31
  };
20
32
  const onData = (chunk) => {
21
33
  const key = parseKey(String(chunk));
34
+ const pages = Math.max(1, Math.ceil(items.length / size));
22
35
  if (key === "up") {
23
36
  cursor = (cursor - 1 + items.length) % items.length;
37
+ page = Math.floor(cursor / size);
24
38
  draw();
25
39
  } else if (key === "down") {
26
40
  cursor = (cursor + 1) % items.length;
41
+ page = Math.floor(cursor / size);
42
+ draw();
43
+ } else if (key === "pageup") {
44
+ page = (page - 1 + pages) % pages;
45
+ cursor = page * size;
46
+ draw();
47
+ } else if (key === "pagedown") {
48
+ page = (page + 1) % pages;
49
+ cursor = Math.min(page * size, items.length - 1);
27
50
  draw();
28
51
  } else if (key === "enter") {
29
52
  cleanup();
@@ -37,14 +60,17 @@ export async function pickInteractive(items, startCursor = 0) {
37
60
  });
38
61
  }
39
62
 
40
- // 多选勾选:↑/↓ 移动,Space 勾选/取消,Enter 保存,q/Esc 取消(返回 Set 或 null)
63
+ // 多选勾选:↑/↓ 移动,←/→ 翻页,Space 勾选/取消,Enter 保存,q/Esc 取消(返回 Set 或 null)
41
64
  export async function pickInteractiveMulti(items, initialPicked = new Set(), startCursor = 0) {
42
65
  const { renderChooser, renderChooserHelp, parseKey } = await import("../chooser.js");
66
+ const size = pageSizeFor(items);
67
+ let page = 0;
43
68
  let cursor = Math.min(Math.max(startCursor, 0), items.length - 1);
44
69
  const picked = new Set(items.filter((it) => initialPicked.has(it.id)).map((it) => it.id));
45
70
  const draw = () => {
71
+ const usePage = size < items.length;
46
72
  const rows = items.map((it, i) => ({ ...it, picked: picked.has(it.id) }));
47
- const lines = [...renderChooser(rows, cursor, { multi: true }), ...renderChooserHelp(true)];
73
+ const lines = [...renderChooser(rows, cursor, { multi: true, ...(usePage ? { index: page, size } : {}) }), ...renderChooserHelp(true, usePage)];
48
74
  process.stdout.write("\x1b[2J\x1b[H" + lines.join("\n"));
49
75
  };
50
76
  draw();
@@ -60,11 +86,22 @@ export async function pickInteractiveMulti(items, initialPicked = new Set(), sta
60
86
  };
61
87
  const onData = (chunk) => {
62
88
  const key = parseKey(String(chunk));
89
+ const pages = Math.max(1, Math.ceil(items.length / size));
63
90
  if (key === "up") {
64
91
  cursor = (cursor - 1 + items.length) % items.length;
92
+ page = Math.floor(cursor / size);
65
93
  draw();
66
94
  } else if (key === "down") {
67
95
  cursor = (cursor + 1) % items.length;
96
+ page = Math.floor(cursor / size);
97
+ draw();
98
+ } else if (key === "pageup") {
99
+ page = (page - 1 + pages) % pages;
100
+ cursor = page * size;
101
+ draw();
102
+ } else if (key === "pagedown") {
103
+ page = (page + 1) % pages;
104
+ cursor = Math.min(page * size, items.length - 1);
68
105
  draw();
69
106
  } else if (key === "space") {
70
107
  const id = items[cursor].id;
@@ -40,7 +40,7 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
40
40
  let allowed = [];
41
41
  let allowAny = false;
42
42
  try { keys = loadProviderKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
43
- try { baseUrl = loadProviderBaseUrl(gid, stateFile ? { file: stateFile } : undefined) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : ""); } catch { baseUrl = cfg?.baseUrl || ""; }
43
+ try { baseUrl = loadProviderBaseUrl(gid, stateFile ? { file: stateFile } : undefined) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : gid === "traework" ? "https://trae-api-cn.mchost.guru" : gid === "qoder" ? "qoder://native" : ""); } catch { baseUrl = cfg?.baseUrl || ""; }
44
44
  try { allowed = loadProviderAllowedModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
45
45
  try { allowAny = loadProviderAllowAnyModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
46
46
  // openrouter 特殊:opencode 例外默认 allowAny true,其余默认 false
package/src/compat.js CHANGED
@@ -1,8 +1,8 @@
1
- // 老 Node 兼容层(engines >=16):Node 16 缺 globalThis.fetch(18+)、
2
- // AbortSignal.timeout(17.3+)、structuredClone(17+);裸全局 crypto 是
3
- // WebCrypto,其 randomUUID 要 19+,只有 node:crypto 的 14.17+ 可用。
4
- // undici 8 需 Node 22+,依赖已锁 5.x(老 Node 可跑,API 兼容我们用到的面)。
5
- // 统一从这里取,源码里勿直接用这些全局。
1
+ // 运行时兼容层(项目要求 Node >=18,见 ADR-0024;源码里勿直接用裸 fetch/globalThis.fetch)。
2
+ // 仍保留本层的原因:① undici 是显式依赖(Agent 连接池/keep-alive 要用),
3
+ // 各家 fetch 实现统一从这里取,避免 globalThis.fetch 与 undici.fetch 混用;
4
+ // ② AbortSignal.timeout/structuredClone 在 18+ 已原生,这里的兜底只为可读报错,
5
+ // 不再承诺 <18 可用(<18 请走 assertMinNode 的人话提示)。
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
8
  let _undici = null;
@@ -33,3 +33,21 @@ export function clone(v) {
33
33
  }
34
34
 
35
35
  export const uuid = () => randomUUID();
36
+
37
+ // ---- 运行环境门(强制):项目要求 Node >=18(ADR-0024)----
38
+ // Node <18 缺 Response/Headers/ReadableStream/TransformStream 等 Web 全局,
39
+ // 上游适配层有 29 处裸用,跑起来必 ReferenceError;与其半途炸掉,不如入口给人话。
40
+ export const MIN_NODE_MAJOR = 18;
41
+
42
+ export function nodeMajor() {
43
+ return Number(String(process.versions?.node || "0").split(".")[0]) || 0;
44
+ }
45
+
46
+ export function assertMinNode({ min = MIN_NODE_MAJOR, label = "mslxdff" } = {}) {
47
+ const major = nodeMajor();
48
+ if (major >= min) return true;
49
+ console.error(`[运行环境不满足] ${label} 需要 Node ${min}+,当前 v${process.versions.node}。`);
50
+ console.error("原因:Node <18 没有 Response/Headers/ReadableStream 等全局对象,上游转发必然失败。");
51
+ console.error("升级方式:nvm install 20 && nvm use 20,或到 https://nodejs.org/ 装 LTS(推荐 20/22)。");
52
+ return false;
53
+ }
@@ -4,7 +4,7 @@
4
4
  // 全程 best-effort:目录不可用 → 原样返回,绝不让 /models 因富化失败挂掉。
5
5
  import { globalCapabilities } from "./index.js";
6
6
 
7
- // "clinebot/deepseek/deepseek-v4-flash" → { provider: "clinebot", raw: "deepseek/deepseek-v4-flash" };裸 id 归 opencode
7
+ // "cline/deepseek/deepseek-v4-flash" → { provider: "cline", raw: "deepseek/deepseek-v4-flash" };裸 id 归 opencode
8
8
  export function splitModelId(id) {
9
9
  const s = String(id || "").trim();
10
10
  const i = s.indexOf("/");
@@ -55,7 +55,7 @@ export function capsPayloadFor(caps, raw) {
55
55
  return payload;
56
56
  }
57
57
 
58
- // 单条 id 的目录匹配:精确裸 id → 剥 -free 后缀 → 二级厂商前缀(clinebot/deepseek/x → deepseek/x)
58
+ // 单条 id 的目录匹配:精确裸 id → 剥 -free 后缀 → 二级厂商前缀(cline/deepseek/x → deepseek/x)
59
59
  function lookupCaps(svc, provider, raw) {
60
60
  return (
61
61
  svc.get(provider, raw) ||
@@ -4,7 +4,7 @@
4
4
  // latency-compare:key/token 类,direct vs link+remote 比延迟
5
5
  export function classifyProvider(id) {
6
6
  const s = String(id || "").trim().toLowerCase();
7
- if (s === "workbuddy" || s === "cline" || s === "clinebot") return "local-only";
7
+ if (s === "workbuddy" || s === "cline" || s === "codearts" || s === "traework" || s === "qoder") return "local-only";
8
8
  if (s === "opencode") return "quota-pool";
9
9
  return "latency-compare";
10
10
  }
@@ -120,7 +120,7 @@ export function createAuthPool({
120
120
  if (account.dead) throw new Error("invalid_grant");
121
121
  if (account.cooldownUntil > now) throw new Error("account_cooldown");
122
122
  if (account.accessToken && now < account.expiry) return account.accessToken;
123
- // base 可能已含 /api/v1(state 里 clinebot baseUrl 就是 …/api/v1),直接拼会 double 成
123
+ // base 可能已含 /api/v1(老 clinebot 配置就是 …/api/v1),直接拼会 double 成
124
124
  // …/api/v1/api/v1/auth/refresh → 上游回 401 Unauthorized(版本/重认证提示),绝不能当 token 死。
125
125
  const baseNoV1 = resolvedBase.replace(/\/api\/v1\/?$/, "");
126
126
  const url = joinUrl(baseNoV1, "/api/v1/auth/refresh");