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
package/package.json
CHANGED
package/src/chooser.js
CHANGED
|
@@ -1,33 +1,52 @@
|
|
|
1
|
-
// 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在
|
|
1
|
+
// 交互式模型选择器的纯渲染逻辑(便于测试);键盘循环在 interactive.js
|
|
2
2
|
|
|
3
|
-
// items: [{ id, status?, ms?, fail?, current?, picked? }];cursor
|
|
3
|
+
// items: [{ id, status?, ms?, fail?, current?, picked? }];cursor 当前高亮行(全局下标)
|
|
4
4
|
// multi=true 时多选勾选(picked 显示 [x]/[ ]),否则单选默认模型(当前显示 ✓)
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// index/size 可选——窗口分页渲染(只画当页 + 页头),不传 size 则全量渲染(行为不变)
|
|
6
|
+
// Note: 分页是本模块职责(键盘循环在 interactive.js),不传 size 必全量渲染 — 见 .agents/notes/implemented/feature/2026-09-21-models-paginated-chooser.md
|
|
7
|
+
export function renderChooser(items, cursor = 0, { multi = false, index = 0, size } = {}) {
|
|
8
|
+
if (size == null) return renderRows(items, cursor, 0, multi);
|
|
9
|
+
const total = items.length;
|
|
10
|
+
const sz = Math.max(1, Number(size) || 1);
|
|
11
|
+
const pages = Math.max(1, Math.ceil(total / sz));
|
|
12
|
+
const idx = Math.min(Math.max(Number(index) || 0, 0), pages - 1);
|
|
13
|
+
const start = idx * sz;
|
|
14
|
+
const rows = [`── 第 ${idx + 1}/${pages} 页 · 共 ${total} 项(←/→ 翻页) ──`];
|
|
15
|
+
rows.push(...renderRows(items.slice(start, start + sz), cursor, start, multi));
|
|
16
|
+
return rows;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderRows(slice, cursor, offset, multi) {
|
|
20
|
+
return slice.map((it, li) => {
|
|
21
|
+
const i = offset + li;
|
|
8
22
|
const arrow = i === cursor ? "❯" : " ";
|
|
9
|
-
const check = multi
|
|
10
|
-
? (it.picked ? " [✓]" : " [ ]")
|
|
11
|
-
: (it.current ? " ✓ (current)" : "");
|
|
12
23
|
let state = "";
|
|
13
24
|
if (it.fail) state = ` [fail: ${it.fail}]`;
|
|
14
25
|
else if (it.ms != null) state = ` [${it.ms}ms]`;
|
|
15
26
|
else if (it.status && it.status !== "normal") state = ` [${it.status}]`;
|
|
27
|
+
if (multi) {
|
|
28
|
+
const box = it.picked ? "[✓]" : "[ ]";
|
|
29
|
+
return `${arrow} ${box} ${it.id}${state}`;
|
|
30
|
+
}
|
|
31
|
+
const check = it.current ? " ✓ (current)" : "";
|
|
16
32
|
return `${arrow} ${it.id}${check}${state}`;
|
|
17
33
|
});
|
|
18
34
|
}
|
|
19
35
|
|
|
20
|
-
export function renderChooserHelp(multi = false) {
|
|
36
|
+
export function renderChooserHelp(multi = false, hasPages = false) {
|
|
37
|
+
const paging = hasPages ? " · ←/→ page" : "";
|
|
21
38
|
return multi
|
|
22
|
-
? ["",
|
|
23
|
-
: ["",
|
|
39
|
+
? ["", `↑/↓ move · Space toggle pick${paging} · Enter save picks · q/Esc cancel`]
|
|
40
|
+
: ["", `↑/↓ move${paging} · Enter select as default · q/Esc cancel`];
|
|
24
41
|
}
|
|
25
42
|
|
|
26
|
-
// 解析按键:返回 "up" | "down" | "enter" | "cancel" | "space" | null(忽略)
|
|
43
|
+
// 解析按键:返回 "up" | "down" | "pageup" | "pagedown" | "enter" | "cancel" | "space" | null(忽略)
|
|
27
44
|
export function parseKey(str) {
|
|
28
45
|
if (!str) return null;
|
|
29
46
|
if (str === "\x1b[A" || str === "k") return "up";
|
|
30
47
|
if (str === "\x1b[B" || str === "j") return "down";
|
|
48
|
+
if (str === "\x1b[D" || str === "h" || str === "\x1b[5~") return "pageup";
|
|
49
|
+
if (str === "\x1b[C" || str === "l" || str === "\x1b[6~") return "pagedown";
|
|
31
50
|
if (str === "\r" || str === "\n") return "enter";
|
|
32
51
|
if (str === " " || str === "\x1b[32") return "space"; // 空格(32=0x20 的 ASC 表示)或某些终端 terminator
|
|
33
52
|
if (str === "\x1b" || str === "q" || str === "\x03") return "cancel";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 交互式 `-models` 候选合并(从 list.js 拆出以守住 ≤10KB 体积门)。
|
|
2
|
+
// allowAny 且空白名单的供应商(qoder/codearts 类)不写 allowlist,其模型只存在于
|
|
3
|
+
// daemon 聚合的 /v1/models —— 不并入这些 id,该供应商在交互列表里「一个都挑不到」。
|
|
4
|
+
import { loadProviderAllowedModels } from "../../../state.js";
|
|
5
|
+
|
|
6
|
+
/** 把「空白名单且已启用」供应商的网关 live 模型并入候选(按前缀归属,去重)。 */
|
|
7
|
+
export async function mergeLiveIds({ combinedIds, seen, knownProviders, liveIds }) {
|
|
8
|
+
const needsLive = knownProviders.filter(
|
|
9
|
+
(k) => String(k).toLowerCase() !== "opencode" && loadProviderAllowedModels(k).length === 0,
|
|
10
|
+
);
|
|
11
|
+
if (!needsLive.length) return;
|
|
12
|
+
const { fetchDaemonModelIds } = await import("./live-models.js");
|
|
13
|
+
const live = await fetchDaemonModelIds();
|
|
14
|
+
for (const id of live) {
|
|
15
|
+
if (liveIds.includes(id)) continue; // 单供应商缓存已含 → 不重复
|
|
16
|
+
const pid = String(id).includes("/") ? String(id).split("/")[0].toLowerCase() : "opencode";
|
|
17
|
+
if (!needsLive.some((k) => String(k).toLowerCase() === pid)) continue;
|
|
18
|
+
if (seen.has(id)) continue;
|
|
19
|
+
seen.add(id);
|
|
20
|
+
combinedIds.push(id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -10,6 +10,7 @@ import { pickInteractiveMulti } from "../../interactive.js";
|
|
|
10
10
|
import { buildAliasMap, readFullAliases, renderProviderList, renderFreeList, groupByProvider } from "./list-render.js";
|
|
11
11
|
import { renderOtherProviders } from "./list-providers.js";
|
|
12
12
|
import { filterStalePicks } from "../../../providers/model-id.js";
|
|
13
|
+
import { mergeLiveIds } from "./list-live.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* `-model list` 全流程:参数解析 → 刷新/回退 → provider 分支 → TTY 交互 → 分组渲染。
|
|
@@ -159,6 +160,7 @@ export async function handleModelList(args, idx, sub) {
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
await mergeLiveIds({ combinedIds, seen, knownProviders, liveIds: ids });
|
|
162
164
|
staleFilter = { knownProviders, liveIds: ids, allowedIds };
|
|
163
165
|
} catch {}
|
|
164
166
|
// provider 已不存在的 picks 孤儿不进交互列表(数据不动,status --all 仍可审计)。
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// 网关 live 模型列表:allowAny 且空白名单的供应商(qoder/codearts 类)不写 allowlist,
|
|
2
|
+
// 其模型只存在于 daemon 聚合的 /v1/models 里;交互式 `-models` 必须并入这些 id,
|
|
3
|
+
// 否则该供应商「一个模型都挑不到」(用户视角:-models 什么也没有)。
|
|
4
|
+
import { compatFetch, timeoutSignal } from "../../../compat.js";
|
|
5
|
+
import { effectivePort, effectiveHost } from "../../policy.js";
|
|
6
|
+
import { loadToken } from "../../../state.js";
|
|
7
|
+
|
|
8
|
+
export async function fetchDaemonModelIds({ fetchImpl = compatFetch, port, host, token, timeoutMs = 6000 } = {}) {
|
|
9
|
+
try {
|
|
10
|
+
const p = port ?? effectivePort([]);
|
|
11
|
+
const h = host ?? effectiveHost() ?? "127.0.0.1";
|
|
12
|
+
const tk = (token ?? (await loadToken())?.token) || "";
|
|
13
|
+
const res = await fetchImpl(`http://${h}:${p}/v1/models?all=1`, {
|
|
14
|
+
headers: tk ? { Authorization: `Bearer ${tk}` } : {},
|
|
15
|
+
signal: timeoutSignal(timeoutMs),
|
|
16
|
+
});
|
|
17
|
+
if (!res?.ok) return [];
|
|
18
|
+
const json = await res.json().catch(() => ({}));
|
|
19
|
+
return (json?.data || []).map((m) => m?.id).filter(Boolean);
|
|
20
|
+
} catch {
|
|
21
|
+
// 网关没跑 / 超时 / 鉴权失败:静默降级(候选列表退回 allowlist + picks)
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -119,7 +119,37 @@ export async function handleClineLogin(id, sub) {
|
|
|
119
119
|
console.log(`✅ 登录成功! 账号: ${email}`);
|
|
120
120
|
console.log(`🔑 refreshToken: ${rt.slice(0, 8)}…${rt.slice(-8)} (${rt.length} 字符)`);
|
|
121
121
|
// 落盘到 state(供应商 id 恒为 cline;写盘前先跑一次性迁移,保证老用户不会再写出 clinebot)
|
|
122
|
-
|
|
122
|
+
// 同邮箱去重:refreshToken 每次 login 都换新串,裸串比对拦不住重复账号,必须按 email 归一。
|
|
123
|
+
// auths 存 uid=email 索引行(domain=cline.bot),keys 与 auths 双写;老 keys 无映射时用 refresh 反查兜底。
|
|
124
|
+
const { defaultStateFile, loadProviderKeys, saveProviderConfig, loadProviderConfig, loadProviderAuths } = await import("../../../state.js");
|
|
125
|
+
function normEmail(v) { return String(v || "").trim().toLowerCase(); }
|
|
126
|
+
function emailFromJson(j) {
|
|
127
|
+
try {
|
|
128
|
+
const d = j?.data && typeof j.data === "object" ? j.data : j;
|
|
129
|
+
const u = d?.userInfo && typeof d.userInfo === "object" ? d.userInfo : null;
|
|
130
|
+
const cand = u?.email || d?.email || d?.user?.email || u?.mail || d?.mail || "";
|
|
131
|
+
if (cand && String(cand).includes("@")) return normEmail(cand);
|
|
132
|
+
const m = String(JSON.stringify(j || {}).slice(0, 4000)).match(/[\w.+-]+@[\w-]+\.[\w.]+/);
|
|
133
|
+
return m ? normEmail(m[0]) : "";
|
|
134
|
+
} catch { return ""; }
|
|
135
|
+
}
|
|
136
|
+
async function resolveClineEmail(refreshToken) {
|
|
137
|
+
try {
|
|
138
|
+
const baseNoV1 = "https://api.cline.bot";
|
|
139
|
+
const res = await compatFetch(`${baseNoV1}/api/v1/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken, grantType: "refresh_token" }), ...extraOpts, signal: timeoutSignal(15000) });
|
|
140
|
+
if (!res.ok) return "";
|
|
141
|
+
return emailFromJson(await res.json().catch(() => null));
|
|
142
|
+
} catch { return ""; }
|
|
143
|
+
}
|
|
144
|
+
function upsertAuths(list, email, rt) {
|
|
145
|
+
const norm = normEmail(email);
|
|
146
|
+
if (!norm || norm === "unknown") return [...(list || [])];
|
|
147
|
+
const next = [...(list || [])];
|
|
148
|
+
const i = next.findIndex((a) => normEmail(a?.uid) === norm);
|
|
149
|
+
if (i >= 0) next[i] = { uid: next[i].uid, domain: "cline.bot", enterpriseId: "", refreshToken: rt };
|
|
150
|
+
else next.push({ uid: String(email).trim(), domain: "cline.bot", enterpriseId: "", refreshToken: rt });
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
123
153
|
try {
|
|
124
154
|
const { runStateMigrations } = await import("../../../state/migrations.js");
|
|
125
155
|
const mig = await runStateMigrations({ file: defaultStateFile() });
|
|
@@ -128,12 +158,51 @@ export async function handleClineLogin(id, sub) {
|
|
|
128
158
|
try {
|
|
129
159
|
const pid = "cline";
|
|
130
160
|
const cur = loadProviderKeys(pid);
|
|
161
|
+
const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
|
|
162
|
+
const baseUrl = cfg.baseUrl || "https://api.cline.bot";
|
|
163
|
+
let auths = loadProviderAuths(pid) || [];
|
|
164
|
+
const norm = normEmail(email);
|
|
131
165
|
if (cur.includes(rt)) {
|
|
132
|
-
|
|
166
|
+
auths = upsertAuths(auths, email, rt);
|
|
167
|
+
saveProviderConfig(pid, { baseUrl, keys: cur, auths });
|
|
168
|
+
console.log(` ℹ️ ${pid} 已存在相同 token,已补邮箱映射(现 ${cur.length} 个账号)`);
|
|
169
|
+
} else if (norm && norm !== "unknown") {
|
|
170
|
+
const ai = auths.findIndex((a) => normEmail(a?.uid) === norm);
|
|
171
|
+
if (ai >= 0) {
|
|
172
|
+
const oldRt = String(auths[ai]?.refreshToken || "");
|
|
173
|
+
const nextKeys = [...cur];
|
|
174
|
+
const at = oldRt ? nextKeys.indexOf(oldRt) : -1;
|
|
175
|
+
if (at >= 0) nextKeys[at] = rt;
|
|
176
|
+
else { const dup = nextKeys.indexOf(rt); if (dup < 0) nextKeys.push(rt); }
|
|
177
|
+
auths = upsertAuths(auths, email, rt);
|
|
178
|
+
saveProviderConfig(pid, { baseUrl, keys: [...new Set(nextKeys.filter(Boolean))], auths });
|
|
179
|
+
console.log(` ℹ️ ${email} 已存在,token 已替换(仍 ${nextKeys.length} 个账号,未追加)`);
|
|
180
|
+
} else {
|
|
181
|
+
let dupIdx = -1;
|
|
182
|
+
const resolved = [];
|
|
183
|
+
for (let i = 0; i < cur.length; i++) {
|
|
184
|
+
const em = await resolveClineEmail(cur[i]);
|
|
185
|
+
resolved[i] = em;
|
|
186
|
+
if (em && em === norm) { dupIdx = i; break; }
|
|
187
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
188
|
+
}
|
|
189
|
+
if (dupIdx >= 0) {
|
|
190
|
+
const nextKeys = [...cur]; nextKeys[dupIdx] = rt;
|
|
191
|
+
for (let i = 0; i < cur.length; i++) if (resolved[i]) auths = upsertAuths(auths, resolved[i], i === dupIdx ? rt : cur[i]);
|
|
192
|
+
auths = upsertAuths(auths, email, rt);
|
|
193
|
+
saveProviderConfig(pid, { baseUrl, keys: [...new Set(nextKeys.filter(Boolean))], auths });
|
|
194
|
+
console.log(` ℹ️ ${email} 已存在(位置 ${dupIdx + 1}),token 已替换(仍 ${nextKeys.length} 个账号,未追加)`);
|
|
195
|
+
} else {
|
|
196
|
+
const nextKeys = [...new Set([...cur, rt].filter(Boolean))];
|
|
197
|
+
for (let i = 0; i < cur.length; i++) if (resolved[i]) auths = upsertAuths(auths, resolved[i], cur[i]);
|
|
198
|
+
auths = upsertAuths(auths, email, rt);
|
|
199
|
+
saveProviderConfig(pid, { baseUrl, keys: nextKeys, auths });
|
|
200
|
+
console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
133
203
|
} else {
|
|
134
|
-
const cfg = loadProviderConfig(pid) || { baseUrl: "", keys: [] };
|
|
135
204
|
const nextKeys = [...new Set([...(cfg.keys || cur), rt].filter(Boolean))];
|
|
136
|
-
saveProviderConfig(pid, { baseUrl
|
|
205
|
+
saveProviderConfig(pid, { baseUrl, keys: nextKeys, auths });
|
|
137
206
|
console.log(` ✅ 已写入 ${pid}(现 ${nextKeys.length} 个账号)`);
|
|
138
207
|
}
|
|
139
208
|
} catch (e) {
|
|
@@ -144,6 +213,6 @@ export async function handleClineLogin(id, sub) {
|
|
|
144
213
|
console.log(" mslxdff -restart 重启网关使新账号生效");
|
|
145
214
|
console.log(" mslxdff -provider cline bench --json 测速 deepseek 是否 200");
|
|
146
215
|
console.log(" mslxdff -chat 直接对话,模型选 deepseek/deepseek-v4-flash");
|
|
147
|
-
console.log("\n多账号:重复 `mslxdff -provider cline login`
|
|
216
|
+
console.log("\n多账号:重复 `mslxdff -provider cline login` 追加(同邮箱自动替换不追加),二号自动做后备");
|
|
148
217
|
process.exit(0);
|
|
149
218
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `-provider codearts login`:浏览器 PKCE 登录华为云 → 凭证 blob 落盘 providerConfigs.codearts。
|
|
2
|
+
// 约定(对齐 cline-login/workbuddy-login):不匹配返 false 交给下一个 handler;匹配即处理并退出。
|
|
3
|
+
import { normalizeProviderId } from "../../../providers/model-id.js";
|
|
4
|
+
import { defaultStateFile, loadProviderKeys, saveProviderConfig, saveProviderAllowAnyModels, loadProviderBaseUrl } from "../../../state.js";
|
|
5
|
+
import { SNAP_BASE } from "../../../providers/codearts/const.js";
|
|
6
|
+
import { runCodeartsLogin, accountToBlob } from "../../../providers/codearts/login.js";
|
|
7
|
+
import { accountFromBlob } from "../../../providers/codearts/auth-pool.js";
|
|
8
|
+
|
|
9
|
+
export async function handleCodeartsLogin(id, sub, rest = [], deps = {}) {
|
|
10
|
+
if (normalizeProviderId(id) !== "codearts") return false;
|
|
11
|
+
if (!(sub === "login" || sub === "auth" || sub === "oauth")) return false;
|
|
12
|
+
const noAllowAny = (rest || []).includes("--no-allow-any");
|
|
13
|
+
const file = deps.file || defaultStateFile();
|
|
14
|
+
const fetchImpl = deps.fetchImpl;
|
|
15
|
+
const log = deps.log || ((m) => console.log(m));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { account, state } = await runCodeartsLogin({ fetchImpl, log });
|
|
19
|
+
const blob = accountToBlob(account, { codeVerifier: state.codeVerifier, dpopJwk: state.dpopJwk, clientId: state.clientId });
|
|
20
|
+
// 同一账号重复登录:替换旧 blob;新账号:追加(多账号 keyring 轮转)。
|
|
21
|
+
// 去重键:userId 非空才可信;双方任一为空时退化为 refreshToken 比较
|
|
22
|
+
// (空 userId==空 userId 的误判会把第二个不同账号当同一账号替换,真机实测恒剩 1 key)
|
|
23
|
+
const cur = loadProviderKeys("codearts", { file }).filter((k) => {
|
|
24
|
+
const parsed = accountFromBlob(k);
|
|
25
|
+
if (!parsed) return false;
|
|
26
|
+
if (parsed.userId && account.userId) return parsed.userId !== account.userId;
|
|
27
|
+
return parsed.refreshToken !== account.refreshToken;
|
|
28
|
+
});
|
|
29
|
+
const baseUrl = loadProviderBaseUrl("codearts", { file }) || SNAP_BASE;
|
|
30
|
+
saveProviderConfig("codearts", { baseUrl, keys: [...cur, blob] }, { file });
|
|
31
|
+
if (!noAllowAny) saveProviderAllowAnyModels("codearts", true, { file });
|
|
32
|
+
|
|
33
|
+
log("");
|
|
34
|
+
log(`✅ codearts 登录成功:${account.userName || account.userId || "(user)"}(剩余账号数:${cur.length + 1})`);
|
|
35
|
+
log(` baseUrl: ${baseUrl}`);
|
|
36
|
+
log(` 凭证已写入 providerConfigs.codearts.keys(state.json,0600 权限目录,绝不入库)`);
|
|
37
|
+
if (!noAllowAny) log(" allowAnyModels=on(免费福利供应商默认放行全部模型;收紧:mslxdff -provider codearts allowAny off)");
|
|
38
|
+
log(" 生效:mslxdff -restart(用户终端跑),然后 mslxdff -provider codearts models 验证");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`❌ codearts 登录失败:${String(err?.message || err)}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
@@ -41,6 +41,9 @@ export async function handleProvider(args) {
|
|
|
41
41
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx gpt-4 add with allowlist");
|
|
42
42
|
console.error(" mslxdff -provider add myapi https://api.example.com/v1 sk-xxx --models-path /v1/models --chat-path /v1/chat/completions");
|
|
43
43
|
console.error(" mslxdff -provider openrouter interactive hidden input (append)");
|
|
44
|
+
console.error(" mslxdff -provider codearts login 华为云 CodeArts PKCE 授权(浏览器登录 → 凭证 blob 落盘,ADR-0027)");
|
|
45
|
+
console.error(" mslxdff -provider qoder login [--region cn|global] Qoder 设备授权(国际站默认,国内站 --region cn)");
|
|
46
|
+
console.error(" mslxdff -provider qoder checkin [--json] [--region cn|global] [--any] [--dry] 每日签到领积分(cn=daily-check-in,global=campaigns)");
|
|
44
47
|
console.error(" mslxdff -provider openrouter clear remove all keys");
|
|
45
48
|
process.exit(1);
|
|
46
49
|
}
|
|
@@ -67,6 +70,15 @@ export async function handleProvider(args) {
|
|
|
67
70
|
if (await handleClineLogin(id, sub)) return true;
|
|
68
71
|
const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
|
|
69
72
|
if (await handleWorkbuddyLogin(id, sub, rest)) return true;
|
|
73
|
+
const { handleCodeartsLogin } = await import("./codearts-login.js");
|
|
74
|
+
if (await handleCodeartsLogin(id, sub, rest)) return true;
|
|
75
|
+
const { handleTraeworkLogin, handleTraeworkCheckin } = await import("./traework-login.js");
|
|
76
|
+
if (await handleTraeworkLogin(id, sub, rest)) return true;
|
|
77
|
+
if (await handleTraeworkCheckin(id, sub, rest)) return true;
|
|
78
|
+
const { handleQoderLogin } = await import("./qoder-login.js");
|
|
79
|
+
if (await handleQoderLogin(id, sub, rest)) return true;
|
|
80
|
+
const { handleQoderCheckin } = await import("./qoder-checkin.js");
|
|
81
|
+
if (await handleQoderCheckin(id, sub, rest)) return true;
|
|
70
82
|
const { handleProviderConfig } = await import("./config.js");
|
|
71
83
|
if (await handleProviderConfig(id, sub, rest)) return true;
|
|
72
84
|
const { handleProviderAllowlist } = await import("./allowlist.js");
|
|
@@ -10,12 +10,31 @@ export async function handleProviderKeys(id, sub, rest, args) {
|
|
|
10
10
|
else console.log(`provider: ${id}${id === "openrouter" ? " (built-in baseUrl: https://openrouter.ai/api/v1)" : ""}`);
|
|
11
11
|
if (keys.length) {
|
|
12
12
|
console.log(` keys: ${keys.length} key${keys.length > 1 ? "s" : ""}`);
|
|
13
|
-
|
|
13
|
+
if (id === "codearts") {
|
|
14
|
+
const { accountFromBlob } = await import("../../../providers/codearts/auth-pool.js");
|
|
15
|
+
for (const [i, k] of keys.entries()) {
|
|
16
|
+
const a = accountFromBlob(k);
|
|
17
|
+
if (a) console.log(` [${i + 1}] user=${a.userName || "(未识别)"} uid=${a.userId ? `${a.userId.slice(0, 8)}…` : "(空,请删了重登回填)"} domain=${a.domainId ? a.domainId.slice(0, 8) + "…" : "(空)"} (${k.length} chars)`);
|
|
18
|
+
else console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`);
|
|
19
|
+
}
|
|
20
|
+
} else if (id === "cline") {
|
|
21
|
+
const { loadProviderAuths } = await import("../../../state.js");
|
|
22
|
+
const auths = loadProviderAuths(id) || [];
|
|
23
|
+
const byRt = new Map(auths.map((a) => [String(a?.refreshToken || ""), String(a?.uid || "")]));
|
|
24
|
+
keys.forEach((k, i) => {
|
|
25
|
+
const em = byRt.get(String(k || "")) || "";
|
|
26
|
+
console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)${em ? ` ${em}` : " (邮箱未映射,重登一次回填)"}`);
|
|
27
|
+
});
|
|
28
|
+
} else keys.forEach((k, i) => console.log(` [${i + 1}] ${k.slice(0, 4)}…${k.slice(-4)} (${k.length} chars)`));
|
|
14
29
|
console.log(` remove by: mslxdff -provider ${id} remove <seq> [seq...] | <key-value>`);
|
|
15
30
|
} else {
|
|
16
31
|
console.log(` keys: (no keys configured)`);
|
|
17
32
|
}
|
|
18
|
-
|
|
33
|
+
try {
|
|
34
|
+
const { shareableProviderIds } = await import("../../../providers/share-keys.js");
|
|
35
|
+
const shared = shareableProviderIds().includes(id);
|
|
36
|
+
console.log(` share keys to peers: ${shared ? "借出(随转发自动附带,ADR-0019)" : "不借出(local-only / 硬排除,codearts/cline 恒不借,ADR-0019/0015)"}`);
|
|
37
|
+
} catch { console.log(` share keys to peers: 借出(默认,key 随转发自动附带,ADR-0019)`); }
|
|
19
38
|
console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
|
|
20
39
|
if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
|
|
21
40
|
else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
|
|
@@ -41,6 +41,15 @@ export async function handleProviderModels(id, sub, args, rest) {
|
|
|
41
41
|
// 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
|
|
42
42
|
const { createClineProvider } = await import("../../../providers/cline.js");
|
|
43
43
|
provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
|
|
44
|
+
} else if (id === "codearts") {
|
|
45
|
+
const { createCodeartsProvider } = await import("../../../providers/codearts.js");
|
|
46
|
+
provider = createCodeartsProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
|
|
47
|
+
} else if (id === "traework") {
|
|
48
|
+
const { createTraeworkProvider } = await import("../../../providers/traework.js");
|
|
49
|
+
provider = createTraeworkProvider({ baseUrl: baseUrl || undefined, apiKeys: keys, auths, file: defaultStateFile() });
|
|
50
|
+
} else if (id === "qoder") {
|
|
51
|
+
const { createQoderProvider } = await import("../../../providers/qoder/index.js");
|
|
52
|
+
provider = createQoderProvider({ id, apiKeys: keys, file: defaultStateFile() });
|
|
44
53
|
} else {
|
|
45
54
|
if (!baseUrl) {
|
|
46
55
|
console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
@@ -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
|
+
}
|