mslxdff 0.1.92 → 0.1.93

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,6 +65,8 @@ export async function handleProvider(args) {
65
65
  }
66
66
  const { handleClineLogin } = await import("./cline-login.js");
67
67
  if (await handleClineLogin(id, sub)) return true;
68
+ const { handleWorkbuddyLogin } = await import("./workbuddy-login.js");
69
+ if (await handleWorkbuddyLogin(id, sub, rest)) return true;
68
70
  const { handleProviderConfig } = await import("./config.js");
69
71
  if (await handleProviderConfig(id, sub, rest)) return true;
70
72
  const { handleProviderAllowlist } = await import("./allowlist.js");
@@ -0,0 +1,152 @@
1
+ // mslxdff -provider workbuddy login — WorkBuddy 官方设备授权流(对标 clinebot login)。
2
+ // 学自 Sliverkiss/workbuddy2api(cmd/login):POST /v2/plugin/auth/state 拿 state+authUrl →
3
+ // 浏览器登录 → GET /v2/plugin/auth/token?state= 轮询 → GET /v2/plugin/login/account?state= 拿 uid。
4
+ // 不走 whistle/MITM,不依赖 codebuddy CLI,自然支持多账号追加。
5
+
6
+ const BASE = "https://copilot.tencent.com";
7
+ // UA 是上游指纹:集中一处,版本过期时改这里或用 MSLXDFF_WORKBUDDY_UA 覆盖。
8
+ const UA = process.env.MSLXDFF_WORKBUDDY_UA || "CLI/2.63.2 CodeBuddy/2.63.2";
9
+ const ORIGIN = "https://www.codebuddy.cn";
10
+
11
+ function baseHeaders() {
12
+ return {
13
+ "Content-Type": "application/json",
14
+ Accept: "application/json, text/plain, */*",
15
+ "X-Requested-With": "XMLHttpRequest",
16
+ Origin: ORIGIN,
17
+ Referer: `${ORIGIN}/`,
18
+ "User-Agent": UA,
19
+ };
20
+ }
21
+
22
+ async function readEnvelope(res) {
23
+ const txt = await res.text();
24
+ let j;
25
+ try { j = JSON.parse(txt); } catch { throw new Error(`WorkBuddy 返回非 JSON: ${txt.slice(0, 200)}`); }
26
+ return j;
27
+ }
28
+
29
+ export async function requestDeviceState(fetchImpl = globalThis.fetch) {
30
+ const res = await fetchImpl(`${BASE}/v2/plugin/auth/state?platform=CLI`, {
31
+ method: "POST",
32
+ headers: baseHeaders(),
33
+ body: "{}",
34
+ signal: AbortSignal.timeout(20000),
35
+ });
36
+ const j = await readEnvelope(res);
37
+ if (j.code !== 0 || !j.data?.state || !j.data?.authUrl) {
38
+ throw new Error(`获取授权失败: code=${j.code} ${j.msg || ""}`.trim());
39
+ }
40
+ return { state: j.data.state, authUrl: j.data.authUrl };
41
+ }
42
+
43
+ // 单次轮询:pending(业务 code 非 0)返回 null;HTTP 5xx 抛错;成功返回 token bundle。
44
+ export async function pollDeviceToken(fetchImpl = globalThis.fetch, state) {
45
+ let res;
46
+ try {
47
+ res = await fetchImpl(`${BASE}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, {
48
+ headers: baseHeaders(),
49
+ signal: AbortSignal.timeout(20000),
50
+ });
51
+ } catch (e) { throw new Error(`轮询失败: ${e.message}`); }
52
+ if (res.status >= 500) throw new Error(`token 端点故障: HTTP ${res.status}`);
53
+ const j = await readEnvelope(res);
54
+ if (j.code !== 0 || !j.data?.accessToken) return null; // pending: "login ing"
55
+ return {
56
+ accessToken: j.data.accessToken,
57
+ refreshToken: j.data.refreshToken || "",
58
+ expiresIn: Number(j.data.expiresIn) || 5184000,
59
+ domain: j.data.domain || "www.codebuddy.cn",
60
+ };
61
+ }
62
+
63
+ export async function fetchDeviceAccount(fetchImpl = globalThis.fetch, state, accessToken) {
64
+ const res = await fetchImpl(`${BASE}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, {
65
+ headers: { ...baseHeaders(), Authorization: `Bearer ${accessToken}` },
66
+ signal: AbortSignal.timeout(20000),
67
+ });
68
+ const j = await readEnvelope(res);
69
+ if (j.code !== 0 || !j.data?.uid) throw new Error(`获取账号失败: code=${j.code} ${j.msg || ""}`.trim());
70
+ return { uid: j.data.uid, enterpriseId: j.data.enterpriseId || "", nickname: j.data.nickname || "" };
71
+ }
72
+
73
+ // 从本机桌面端登录态直接导入(学自 xiaofan6ya/workbuddy2api):
74
+ // 桌面端明文存着当前登录号的 token,定位逻辑在 desktop-info.js(跨平台发现,不硬编码盘符/厂商目录)。
75
+ // 桌面已登录新号时用这个,无需浏览器、无需抓包。
76
+ export async function importDesktopAccount({ file, localAppData, roots, save } = {}) {
77
+ const { resolveDesktopInfoPath, readDesktopAccount } = await import("../../../providers/workbuddy/desktop-info.js");
78
+ const { homedir } = await import("node:os");
79
+ const { path: fp, via } = resolveDesktopInfoPath({ file, roots, extraRoots: localAppData ? [localAppData] : [], homedir: homedir() });
80
+ const saveFn = save || (await import("../../../providers/workbuddy/account-store.js")).saveWorkbuddyAccount;
81
+ const saved = await saveFn(readDesktopAccount(fp));
82
+ return { ...saved, desktopFile: fp, via };
83
+ }
84
+
85
+ export async function handleWorkbuddyLogin(id, sub, rest = [], deps = {}) {
86
+ if (id !== "workbuddy" && id !== "wb") return false;
87
+ if (sub === "import" || sub === "import-desktop" || sub === "from-desktop") {
88
+ const fileArg = rest.find((x) => String(x).startsWith("--file="))?.slice("--file=".length);
89
+ try {
90
+ const saved = await importDesktopAccount({ file: fileArg, save: deps.saveAccount });
91
+ console.log(`✅ 已从桌面端导入账号(现 ${saved.accounts} 个账号):${saved.file}`);
92
+ console.log("下一步:mslxdff -workbuddy balance 验证余额");
93
+ } catch (e) {
94
+ console.error(`❌ ${e.message}`);
95
+ process.exit(1);
96
+ }
97
+ process.exit(0);
98
+ }
99
+ if (sub !== "login" && sub !== "auth" && sub !== "oauth") return false;
100
+ const fetchImpl = deps.fetchImpl || globalThis.fetch;
101
+
102
+ console.log("🚀 启动 WorkBuddy 设备授权流程...\n");
103
+ let dev;
104
+ try {
105
+ dev = await requestDeviceState(fetchImpl);
106
+ } catch (e) {
107
+ console.error(`❌ 获取授权失败: ${e.message}`);
108
+ process.exit(1);
109
+ }
110
+ console.log("=".repeat(60));
111
+ console.log("1️⃣ 在浏览器打开下面这个链接(用要添加的账号登录):");
112
+ console.log(` ${dev.authUrl}`);
113
+ console.log("2️⃣ 登录成功后回到这里,无需回车,自动继续");
114
+ console.log("=".repeat(60));
115
+ console.log("\n🔄 等待你登录(自动轮询,每 5s,最多 5 分钟)...\n");
116
+
117
+ const deadline = Date.now() + 5 * 60 * 1000;
118
+ let bundle = null;
119
+ while (Date.now() < deadline) {
120
+ await new Promise((r) => setTimeout(r, 5000));
121
+ try {
122
+ bundle = await pollDeviceToken(fetchImpl, dev.state);
123
+ if (bundle) break;
124
+ process.stdout.write(".");
125
+ } catch (e) {
126
+ console.log(`\n 轮询出错: ${e.message}`);
127
+ }
128
+ }
129
+ if (!bundle) {
130
+ console.error("\n❌ 登录超时,请确认浏览器已完成登录后重跑 mslxdff -provider workbuddy login");
131
+ process.exit(1);
132
+ }
133
+ console.log("\n✅ 授权成功!获取账号信息...");
134
+ let acct;
135
+ try {
136
+ acct = await fetchDeviceAccount(fetchImpl, dev.state, bundle.accessToken);
137
+ } catch (e) {
138
+ console.error(`❌ ${e.message}`);
139
+ process.exit(1);
140
+ }
141
+ const expiresAt = Math.floor(Date.now() / 1000) + bundle.expiresIn;
142
+ const save = deps.saveAccount || (await import("../../../providers/workbuddy/account-store.js")).saveWorkbuddyAccount;
143
+ const saved = await save({ uid: acct.uid, enterpriseId: acct.enterpriseId, nickname: acct.nickname, accessToken: bundle.accessToken, refreshToken: bundle.refreshToken, expiresAt, domain: bundle.domain });
144
+ console.log("=".repeat(60));
145
+ console.log(`✅ 登录成功! 账号: ${acct.nickname || acct.uid} (${acct.uid.slice(0, 8)}…,现 ${saved.accounts} 个账号${saved.updated ? "·旧号凭证已更新" : ""})`);
146
+ console.log("=".repeat(60));
147
+ console.log("\n下一步:");
148
+ console.log(" mslxdff -restart 重启网关使新账号生效");
149
+ console.log(" mslxdff -workbuddy balance 看多号余额(新号次日自动纳入每日签到)");
150
+ console.log("\n多账号:重复 login 追加,每号独立 uid,对话自动轮换");
151
+ process.exit(0);
152
+ }
@@ -0,0 +1,41 @@
1
+ // WorkBuddy 账号落盘:auths/workbuddy-<uid>.json(0600 tmp+rename)+ state.json keys/auths 按 uid 去重追加。
2
+ // token-auto 与 device-login 共用,保证两种入口写出完全一致。
3
+ import { writeFileSync, mkdirSync, existsSync, renameSync, unlinkSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ export function resolveAuthDir() {
8
+ if (process.env.WORKBUDDY_AUTH_DIR) return process.env.WORKBUDDY_AUTH_DIR;
9
+ const sf = process.env.MSLXDFF_STATE_FILE || "";
10
+ if (sf.includes("mslxdff-test") || process.env.NODE_ENV === "test") return join(tmpdir(), "mslxdff-test-auths");
11
+ if (sf && sf.includes("mslxdff-")) return join(dirname(sf), "auths");
12
+ return join(process.cwd(), "auths");
13
+ }
14
+
15
+ export async function saveWorkbuddyAccount({ uid, enterpriseId = "", nickname = "", accessToken, refreshToken = "", expiresAt = 0, domain = "www.codebuddy.cn", file } = {}) {
16
+ if (!uid || !accessToken) throw new Error("saveWorkbuddyAccount: 缺少 uid/accessToken");
17
+ const authDir = resolveAuthDir();
18
+ mkdirSync(authDir, { recursive: true });
19
+ const exp = expiresAt || Math.floor(Date.now() / 1000) + 5184000;
20
+ const doc = { account: { uid, enterpriseId, nickname }, auth: { accessToken, refreshToken, expiresAt: exp, domain } };
21
+ const fp = join(authDir, `workbuddy-${uid}.json`);
22
+ const tmp = `${fp}.tmp`;
23
+ writeFileSync(tmp, JSON.stringify(doc, null, 2), { mode: 0o600 });
24
+ try {
25
+ if (existsSync(fp)) unlinkSync(fp);
26
+ renameSync(tmp, fp);
27
+ } catch {
28
+ writeFileSync(fp, JSON.stringify(doc, null, 2), { mode: 0o600 });
29
+ }
30
+ // 同步 state(对话轮换只读 state)
31
+ const { loadProviderConfigs, saveProviderConfig } = await import("../../state.js");
32
+ const cfg = (loadProviderConfigs(file ? { file } : {})?.workbuddy) || {};
33
+ const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
34
+ const auths = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
35
+ const row = { uid, domain, enterpriseId, refreshToken };
36
+ const atIdx = auths.findIndex((a) => a?.uid === uid);
37
+ if (atIdx >= 0) { keys[atIdx] = accessToken; auths[atIdx] = row; }
38
+ else { keys.push(accessToken); auths.push(row); }
39
+ saveProviderConfig("workbuddy", { baseUrl: cfg.baseUrl || "https://copilot.tencent.com", keys, auths }, file ? { file } : {});
40
+ return { file: fp, accounts: keys.length, updated: atIdx >= 0 };
41
+ }
@@ -0,0 +1,78 @@
1
+ // 桌面端登录态定位:跨机器可用,不硬编码盘符/厂商目录名。
2
+ // 优先级:显式 file > MSLXDFF_WORKBUDDY_DESKTOP_INFO > 已知后缀快查 > 按文件名 BFS(depth≤6,多命中取 mtime 最新)。
3
+ import { existsSync, readdirSync, statSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ export const DESKTOP_INFO_NAME = "workbuddy-desktop.info";
7
+ const KNOWN_SUFFIX = join("CodeBuddyExtension", "Data", "Public", "auth", DESKTOP_INFO_NAME);
8
+ const MAX_DEPTH = 6;
9
+ const MAX_ENTRIES = 30000;
10
+
11
+ export function candidateRoots({ platform = process.platform, env = process.env, homedir = "" } = {}) {
12
+ if (platform === "win32") return [env.LOCALAPPDATA, env.APPDATA].filter(Boolean);
13
+ if (platform === "darwin") return [join(homedir, "Library", "Application Support")].filter(Boolean);
14
+ return [join(homedir, ".config"), join(homedir, ".local", "share")].filter(Boolean);
15
+ }
16
+
17
+ function searchByName(roots) {
18
+ const hits = [];
19
+ let seen = 0;
20
+ for (const root of roots) {
21
+ const queue = [{ dir: root, depth: 0 }];
22
+ while (queue.length && seen < MAX_ENTRIES) {
23
+ const { dir, depth } = queue.shift();
24
+ let names;
25
+ try { names = readdirSync(dir); } catch { continue; }
26
+ for (const n of names) {
27
+ if (++seen > MAX_ENTRIES) break;
28
+ const fp = join(dir, n);
29
+ let st;
30
+ try { st = statSync(fp); } catch { continue; }
31
+ if (st.isFile() && n === DESKTOP_INFO_NAME) hits.push({ path: fp, mtimeMs: st.mtimeMs });
32
+ else if (st.isDirectory() && depth < MAX_DEPTH) queue.push({ dir: fp, depth: depth + 1 });
33
+ }
34
+ }
35
+ }
36
+ hits.sort((a, b) => b.mtimeMs - a.mtimeMs);
37
+ return hits;
38
+ }
39
+
40
+ export function resolveDesktopInfoPath({ file, env = process.env, platform = process.platform, homedir = "", extraRoots = [], roots: rootsArg = null } = {}) {
41
+ const explicit = file || env.MSLXDFF_WORKBUDDY_DESKTOP_INFO;
42
+ if (explicit) {
43
+ if (!existsSync(explicit)) throw new Error(`桌面登录态文件不存在: ${explicit}`);
44
+ return { path: explicit, via: explicit === file ? "file" : "env" };
45
+ }
46
+ const roots = rootsArg ?? [...extraRoots, ...candidateRoots({ platform, env, homedir })];
47
+ for (const r of roots) {
48
+ const fast = join(r, KNOWN_SUFFIX);
49
+ if (existsSync(fast)) return { path: fast, via: "known-path" };
50
+ }
51
+ const hits = searchByName(roots);
52
+ if (hits.length) return { path: hits[0].path, via: "search" };
53
+ throw new Error(
54
+ `找不到桌面端登录态(已搜: ${roots.join("; ") || "无可用根目录"})。` +
55
+ "确认桌面端已登录,或用 --file=路径 / MSLXDFF_WORKBUDDY_DESKTOP_INFO 显式指定 workbuddy-desktop.info"
56
+ );
57
+ }
58
+
59
+ export function readDesktopAccount(fp) {
60
+ let j;
61
+ try {
62
+ j = JSON.parse(readFileSync(fp, "utf8"));
63
+ } catch (e) {
64
+ throw new Error(`读桌面登录态失败: ${fp}(${e.message},确认桌面端已登录)`);
65
+ }
66
+ if (!j?.account?.uid || !j?.auth?.accessToken) throw new Error(`桌面登录态不完整(无 uid/accessToken): ${fp}`);
67
+ let exp = Number(j.auth.expiresAt) || 0;
68
+ if (exp > 1e12) exp = Math.floor(exp / 1000);
69
+ return {
70
+ uid: j.account.uid,
71
+ enterpriseId: "",
72
+ nickname: j.account.nickname || j.account.phoneNumber || "",
73
+ accessToken: j.auth.accessToken,
74
+ refreshToken: j.auth.refreshToken || "",
75
+ expiresAt: exp,
76
+ domain: j.auth.domain || "www.codebuddy.cn",
77
+ };
78
+ }