mslxdff 0.1.138 → 0.1.141
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 +1 -1
- package/src/chooser.js +31 -12
- package/src/cli/commands/model/list-live.js +22 -0
- package/src/cli/commands/model/list.js +2 -0
- package/src/cli/commands/model/live-models.js +24 -0
- package/src/cli/commands/provider/cline-login.js +74 -5
- package/src/cli/commands/provider/codearts-login.js +45 -0
- package/src/cli/commands/provider/index.js +12 -0
- package/src/cli/commands/provider/keys.js +21 -2
- package/src/cli/commands/provider/models.js +9 -0
- 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/interactive.js +41 -4
- package/src/cli/provider-row.js +1 -1
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/index.js +9 -3
- 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/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 +15 -0
- package/src/providers/share-keys.js +2 -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/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 +9 -3
- package/src/runtime/qoder-checkin.js +93 -0
- package/src/runtime/traework-checkin.js +104 -0
|
@@ -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
|
+
}
|
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
|
|
@@ -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") 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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createKeyRing } from "../keyring.js";
|
|
2
|
-
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath, saveProviderConfig } from "../../state.js";
|
|
2
|
+
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath, saveProviderConfig, loadProviderAuths } from "../../state.js";
|
|
3
3
|
import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner } from "../base.js";
|
|
4
4
|
import { compatFetch } from "../../compat.js";
|
|
5
5
|
import { joinModelId } from "../model-id.js";
|
|
@@ -68,8 +68,14 @@ export function createClineProvider({
|
|
|
68
68
|
const idx = cur.indexOf(oldRefreshToken);
|
|
69
69
|
if (idx >= 0) {
|
|
70
70
|
const next = [...cur]; next[idx] = newRefreshToken;
|
|
71
|
-
// 优先写 providerConfigs,兼容旧路径由 saveProviderConfig
|
|
72
|
-
|
|
71
|
+
// 优先写 providerConfigs,兼容旧路径由 saveProviderConfig 处理;auths 邮箱映射同步新串防漂移
|
|
72
|
+
let auths;
|
|
73
|
+
try {
|
|
74
|
+
const list = loadProviderAuths(id, file ? { file } : {});
|
|
75
|
+
const ai = list.findIndex((a) => String(a?.refreshToken || "") === String(oldRefreshToken || ""));
|
|
76
|
+
if (ai >= 0) { auths = [...list]; auths[ai] = { ...auths[ai], refreshToken: newRefreshToken }; }
|
|
77
|
+
} catch {}
|
|
78
|
+
saveProviderConfig(id, { baseUrl: resolvedBase, keys: next, ...(auths ? { auths } : {}) }, file ? { file } : {});
|
|
73
79
|
}
|
|
74
80
|
// 只回写 cline 自己(旧 id 轮换回写已随 id 统一删除,见 .scratch/cline-unify/SPEC.md §4.1)
|
|
75
81
|
} catch {}
|
|
@@ -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
|
+
}
|