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.
- package/package.json +2 -2
- package/src/chat/gateway.js +1 -1
- package/src/chat/prompt.js +1 -1
- package/src/chat/terminal.js +1 -1
- package/src/chooser.js +31 -12
- package/src/cli/commands/model/list-live.js +22 -0
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/list-render.js +2 -2
- package/src/cli/commands/model/list.js +2 -0
- package/src/cli/commands/model/live-models.js +24 -0
- package/src/cli/commands/provider/add.js +2 -1
- package/src/cli/commands/provider/bench-via.js +1 -1
- package/src/cli/commands/provider/cline-free.js +94 -0
- package/src/cli/commands/provider/cline-login.js +20 -15
- package/src/cli/commands/provider/codearts-login.js +45 -0
- package/src/cli/commands/provider/index.js +34 -0
- package/src/cli/commands/provider/keys.js +13 -2
- package/src/cli/commands/provider/models.js +13 -1
- package/src/cli/commands/provider/qoder-checkin.js +81 -0
- package/src/cli/commands/provider/qoder-login.js +103 -0
- package/src/cli/commands/provider/traework-login.js +202 -0
- package/src/cli/commands/provider/workbuddy-login.js +1 -1
- package/src/cli/commands/workbuddy.js +14 -21
- package/src/cli/index.js +3 -0
- package/src/cli/interactive.js +41 -4
- package/src/cli/provider-row.js +1 -1
- package/src/compat.js +23 -5
- package/src/model-capabilities/merge.js +2 -2
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +1 -1
- package/src/providers/cline/chat.js +14 -11
- package/src/providers/cline/free-catalog.js +71 -0
- package/src/providers/cline/index.js +2 -16
- package/src/providers/cline/models.js +28 -51
- package/src/providers/codearts/auth-pool.js +148 -0
- package/src/providers/codearts/chat.js +176 -0
- package/src/providers/codearts/const.js +60 -0
- package/src/providers/codearts/dpop.js +95 -0
- package/src/providers/codearts/index.js +160 -0
- package/src/providers/codearts/login.js +214 -0
- package/src/providers/codearts/models.js +182 -0
- package/src/providers/codearts/sign.js +96 -0
- package/src/providers/codearts/sse.js +204 -0
- package/src/providers/codearts/stream.js +209 -0
- package/src/providers/codearts/sts.js +124 -0
- package/src/providers/codearts.js +1 -0
- package/src/providers/model-id.js +4 -4
- package/src/providers/qoder/account-store.js +107 -0
- package/src/providers/qoder/aggregate.js +51 -0
- package/src/providers/qoder/baseprompt.json +592 -0
- package/src/providers/qoder/chat.js +72 -0
- package/src/providers/qoder/checkin.js +160 -0
- package/src/providers/qoder/constants.js +45 -0
- package/src/providers/qoder/encode.js +42 -0
- package/src/providers/qoder/fingerprint.js +39 -0
- package/src/providers/qoder/index.js +106 -0
- package/src/providers/qoder/models.js +62 -0
- package/src/providers/qoder/oauth.js +85 -0
- package/src/providers/qoder/payload.js +156 -0
- package/src/providers/qoder/request.js +26 -0
- package/src/providers/qoder/session.js +135 -0
- package/src/providers/qoder/sse.js +46 -0
- package/src/providers/qoder/stream.js +81 -0
- package/src/providers/qoder.js +2 -0
- package/src/providers/registry.js +16 -1
- package/src/providers/share-keys.js +7 -2
- package/src/providers/traework/account-store.js +149 -0
- package/src/providers/traework/chat.js +211 -0
- package/src/providers/traework/checkin.js +35 -0
- package/src/providers/traework/constants.js +21 -0
- package/src/providers/traework/errors.js +52 -0
- package/src/providers/traework/headers.js +47 -0
- package/src/providers/traework/index.js +122 -0
- package/src/providers/traework/models.js +97 -0
- package/src/providers/traework/payload.js +86 -0
- package/src/providers/traework/sse.js +184 -0
- package/src/providers/traework/token.js +52 -0
- package/src/providers/traework.js +2 -0
- package/src/providers/workbuddy/account-store.js +73 -7
- package/src/providers/workbuddy/chat.js +1 -1
- package/src/providers/workbuddy/index.js +10 -20
- package/src/readline-compat.js +9 -10
- package/src/routes/models-route.js +19 -1
- package/src/runtime/bootstrap.js +10 -0
- package/src/runtime/provider-gate.js +30 -0
- package/src/runtime/providers-setup.js +18 -3
- package/src/runtime/qoder-checkin.js +93 -0
- package/src/runtime/server-lifecycle.js +2 -2
- package/src/runtime/traework-checkin.js +104 -0
- package/src/state/migrations/cline-unify.js +94 -0
- package/src/state/migrations.js +33 -0
- package/src/state/schemas/token.js +6 -2
- package/src/state/schemas/use-group.js +4 -4
- package/src/sync-workbuddy.js +1 -1
- 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 官方设备授权流(对标
|
|
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
|
|
133
|
-
const
|
|
134
|
-
|
|
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 {
|
|
159
|
-
const {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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;
|
package/src/cli/interactive.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
// 交互式选择器:↑/↓
|
|
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
|
|
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
|
-
// 多选勾选:↑/↓
|
|
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;
|
package/src/cli/provider-row.js
CHANGED
|
@@ -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
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
// "
|
|
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 后缀 → 二级厂商前缀(
|
|
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 === "
|
|
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
|
|
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");
|