creditdaddy 0.9.5 → 1.2.0

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.
@@ -1,126 +1,127 @@
1
- /**
2
- * WorkBuddy / CodeBuddy 浏览器登录 — 与 WorkBuddy 桌面端(platform=WorkBuddy)同一流程:
3
- *
4
- * 1. POST {host}/v2/plugin/auth/state?platform=WorkBuddy → {state, authUrl}
5
- * 2. 用户在浏览器(桌面版为隐私窗口)打开 authUrl 登录
6
- * 3. GET {host}/v2/plugin/auth/token?state=… code 11217 = 等待;成功返回 accessToken 等
7
- * 4. GET {host}/v2/plugin/login/account?state=…(Bearer) code 12151 = 等待;返回当前账号
8
- * 5. GET {host}/v2/plugin/accounts(Bearer) 账号列表(best-effort)
9
- *
10
- * 3~5 组装成与 workbuddy-desktop.info 相同结构的会话,经 sessionToAccount 入库,
11
- * 因此浏览器登录的账号与「本机导入」一样可签到、查积分、一键切换 WorkBuddy 客户端。
12
- * 国内版走 www.codebuddy.cn(WorkBuddy 客户端默认域名),国际版走 www.codebuddy.ai。
13
- */
14
-
15
- import { FETCH_TIMEOUT_MS } from './constants.js';
16
- import { sessionToAccount } from './workbuddyLocal.js';
17
-
18
- const HOSTS = { workbuddy: 'www.codebuddy.cn', 'workbuddy-intl': 'www.codebuddy.ai' };
19
- const PLATFORM = 'WorkBuddy';
20
- const USER_AGENT = 'CLI/2.108.1 CodeBuddy/2.108.1';
21
- const RETRY_FETCH_TOKEN = 11217;
22
- const RETRY_FETCH_ACCOUNT = 12151;
23
- const ACCOUNT_MAX_ATTEMPTS = 8; // 拿到 token 后账号信息迟迟不就绪时,退化为仅 token 入库
24
-
25
- function headers(host, extra = {}) {
26
- return {
27
- Accept: 'application/json',
28
- 'Content-Type': 'application/json',
29
- 'User-Agent': USER_AGENT,
30
- 'X-Requested-With': 'XMLHttpRequest',
31
- 'X-Domain': host,
32
- 'X-Product': 'SaaS',
33
- ...extra,
34
- };
35
- }
36
- const ANON = { 'X-No-Authorization': 'true', 'X-No-User-Id': 'true', 'X-No-Enterprise-Id': 'true', 'X-No-Department-Info': 'true' };
37
-
38
- async function fetchJson(url, init = {}) {
39
- const res = await fetch(url, { ...init, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
40
- const text = await res.text();
41
- let body = null;
42
- try { body = text ? JSON.parse(text) : null; } catch {}
43
- return { res, body };
44
- }
45
-
46
- export async function startWorkbuddyLogin(provider) {
47
- const host = HOSTS[provider] || HOSTS.workbuddy;
48
- const { res, body } = await fetchJson(`https://${host}/v2/plugin/auth/state?platform=${PLATFORM}`, {
49
- method: 'POST',
50
- headers: headers(host, { 'X-No-Authorization': 'true', 'X-No-User-Id': 'true' }),
51
- body: '{}',
52
- });
53
- const d = body?.data;
54
- if (!res.ok || body?.code !== 0 || !d?.state || !d?.authUrl) {
55
- throw new Error('WorkBuddy 登录初始化失败:' + (body?.msg || 'HTTP ' + res.status));
56
- }
57
- return {
58
- url: d.authUrl,
59
- expiresInMs: 10 * 60e3,
60
- intervalMs: 3000,
61
- data: { provider, host, state: d.state, token: null, accountAttempts: 0 },
62
- };
63
- }
64
-
65
- /** 单次轮询(按阶段推进,状态存在 data 上):{status:'pending'} | {status:'ok', input};失败抛错 */
66
- export async function pollWorkbuddyLogin(data) {
67
- const { host, state } = data;
68
- if (!data.token) {
69
- const { res, body } = await fetchJson(`https://${host}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, {
70
- headers: headers(host, ANON),
71
- });
72
- if (body?.code === RETRY_FETCH_TOKEN) return { status: 'pending' };
73
- if (res.status >= 500 || res.status === 429) return { status: 'pending' };
74
- if (!res.ok || body?.code !== 0 || !body?.data?.accessToken) {
75
- throw new Error('WorkBuddy 授权失败:' + (body?.msg || 'HTTP ' + res.status));
76
- }
77
- const now = Date.now();
78
- const t = body.data;
79
- data.token = {
80
- ...t,
81
- domain: t.domain || host,
82
- lastRefreshTime: now,
83
- ...(Number.isFinite(t.expiresIn) ? { expiresAt: now + t.expiresIn * 1000 } : {}),
84
- ...(Number.isFinite(t.refreshExpiresIn) ? { refreshExpiresAt: now + t.refreshExpiresIn * 1000 } : {}),
85
- };
86
- }
87
-
88
- const auth = { Authorization: `Bearer ${data.token.accessToken}` };
89
- let account = null;
90
- data.accountAttempts++;
91
- try {
92
- const { res, body } = await fetchJson(`https://${host}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, {
93
- headers: headers(host, { ...auth, 'X-No-User-Id': 'true', 'X-No-Enterprise-Id': 'true', 'X-No-Department-Info': 'true' }),
94
- });
95
- if (res.ok && body?.code === 0 && body.data && typeof body.data === 'object') account = body.data;
96
- else if (data.accountAttempts < ACCOUNT_MAX_ATTEMPTS
97
- && (body?.code === RETRY_FETCH_ACCOUNT || res.status === 401 || res.status === 403 || res.status >= 500)) {
98
- return { status: 'pending' };
99
- }
100
- } catch {
101
- if (data.accountAttempts < ACCOUNT_MAX_ATTEMPTS) return { status: 'pending' };
102
- }
103
-
104
- let list = [];
105
- if (account) {
106
- try {
107
- const { res, body } = await fetchJson(`https://${host}/v2/plugin/accounts`, { headers: headers(host, auth) });
108
- if (res.ok && Array.isArray(body?.data?.accounts)) list = body.data.accounts;
109
- } catch {}
110
- }
111
-
112
- // 与 WorkBuddy 客户端的会话文件同构;拿不到账号信息时只保留 token(可签到 / 查积分,不能切换客户端)
113
- const session = account
114
- ? {
115
- account: { ...(list.find((a) => a.uid === account.uid) || {}), ...account, lastLogin: true },
116
- accounts: list.filter((a) => a.pluginEnabled),
117
- allAccounts: list,
118
- auth: data.token,
119
- }
120
- : { auth: data.token };
121
- const input = sessionToAccount(session, '浏览器登录');
122
- if (!input) throw new Error('WorkBuddy 返回的 token 无法识别(签发方不是 CodeBuddy / WorkBuddy)');
123
- if (!account) delete input.meta.session;
124
- input.source = 'browser';
125
- return { status: 'ok', input };
126
- }
1
+ /**
2
+ * WorkBuddy / CodeBuddy 浏览器登录 — 与 WorkBuddy 桌面端(platform=WorkBuddy)同一流程:
3
+ *
4
+ * 1. POST {host}/v2/plugin/auth/state?platform=WorkBuddy → {state, authUrl}
5
+ * 2. 用户在浏览器(桌面版为隐私窗口)打开 authUrl 登录
6
+ * 3. GET {host}/v2/plugin/auth/token?state=… code 11217 = 等待;成功返回 accessToken 等
7
+ * 4. GET {host}/v2/plugin/login/account?state=…(Bearer) code 12151 = 等待;返回当前账号
8
+ * 5. GET {host}/v2/plugin/accounts(Bearer) 账号列表(best-effort)
9
+ *
10
+ * 3~5 组装成与 workbuddy-desktop.info 相同结构的会话,经 sessionToAccount 入库,
11
+ * 因此浏览器登录的账号与「本机导入」一样可签到、查积分、一键切换 WorkBuddy 客户端。
12
+ * 国内版走 www.codebuddy.cn;国际版走 www.workbuddy.ai(WorkBuddy 海外版客户端的站点——
13
+ * www.codebuddy.ai 是 CodeBuddy 国际版,登录页不是 WorkBuddy 的)。
14
+ */
15
+
16
+ import { FETCH_TIMEOUT_MS } from './constants.js';
17
+ import { sessionToAccount } from './workbuddyLocal.js';
18
+
19
+ const HOSTS = { workbuddy: 'www.codebuddy.cn', 'workbuddy-intl': 'www.workbuddy.ai' };
20
+ const PLATFORM = 'WorkBuddy';
21
+ const USER_AGENT = 'CLI/2.108.1 CodeBuddy/2.108.1';
22
+ const RETRY_FETCH_TOKEN = 11217;
23
+ const RETRY_FETCH_ACCOUNT = 12151;
24
+ const ACCOUNT_MAX_ATTEMPTS = 8; // 拿到 token 后账号信息迟迟不就绪时,退化为仅 token 入库
25
+
26
+ function headers(host, extra = {}) {
27
+ return {
28
+ Accept: 'application/json',
29
+ 'Content-Type': 'application/json',
30
+ 'User-Agent': USER_AGENT,
31
+ 'X-Requested-With': 'XMLHttpRequest',
32
+ 'X-Domain': host,
33
+ 'X-Product': 'SaaS',
34
+ ...extra,
35
+ };
36
+ }
37
+ const ANON = { 'X-No-Authorization': 'true', 'X-No-User-Id': 'true', 'X-No-Enterprise-Id': 'true', 'X-No-Department-Info': 'true' };
38
+
39
+ async function fetchJson(url, init = {}) {
40
+ const res = await fetch(url, { ...init, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
41
+ const text = await res.text();
42
+ let body = null;
43
+ try { body = text ? JSON.parse(text) : null; } catch {}
44
+ return { res, body };
45
+ }
46
+
47
+ export async function startWorkbuddyLogin(provider) {
48
+ const host = HOSTS[provider] || HOSTS.workbuddy;
49
+ const { res, body } = await fetchJson(`https://${host}/v2/plugin/auth/state?platform=${PLATFORM}`, {
50
+ method: 'POST',
51
+ headers: headers(host, { 'X-No-Authorization': 'true', 'X-No-User-Id': 'true' }),
52
+ body: '{}',
53
+ });
54
+ const d = body?.data;
55
+ if (!res.ok || body?.code !== 0 || !d?.state || !d?.authUrl) {
56
+ throw new Error('WorkBuddy 登录初始化失败:' + (body?.msg || 'HTTP ' + res.status));
57
+ }
58
+ return {
59
+ url: d.authUrl,
60
+ expiresInMs: 10 * 60e3,
61
+ intervalMs: 3000,
62
+ data: { provider, host, state: d.state, token: null, accountAttempts: 0 },
63
+ };
64
+ }
65
+
66
+ /** 单次轮询(按阶段推进,状态存在 data 上):{status:'pending'} | {status:'ok', input};失败抛错 */
67
+ export async function pollWorkbuddyLogin(data) {
68
+ const { host, state } = data;
69
+ if (!data.token) {
70
+ const { res, body } = await fetchJson(`https://${host}/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, {
71
+ headers: headers(host, ANON),
72
+ });
73
+ if (body?.code === RETRY_FETCH_TOKEN) return { status: 'pending' };
74
+ if (res.status >= 500 || res.status === 429) return { status: 'pending' };
75
+ if (!res.ok || body?.code !== 0 || !body?.data?.accessToken) {
76
+ throw new Error('WorkBuddy 授权失败:' + (body?.msg || 'HTTP ' + res.status));
77
+ }
78
+ const now = Date.now();
79
+ const t = body.data;
80
+ data.token = {
81
+ ...t,
82
+ domain: t.domain || host,
83
+ lastRefreshTime: now,
84
+ ...(Number.isFinite(t.expiresIn) ? { expiresAt: now + t.expiresIn * 1000 } : {}),
85
+ ...(Number.isFinite(t.refreshExpiresIn) ? { refreshExpiresAt: now + t.refreshExpiresIn * 1000 } : {}),
86
+ };
87
+ }
88
+
89
+ const auth = { Authorization: `Bearer ${data.token.accessToken}` };
90
+ let account = null;
91
+ data.accountAttempts++;
92
+ try {
93
+ const { res, body } = await fetchJson(`https://${host}/v2/plugin/login/account?state=${encodeURIComponent(state)}`, {
94
+ headers: headers(host, { ...auth, 'X-No-User-Id': 'true', 'X-No-Enterprise-Id': 'true', 'X-No-Department-Info': 'true' }),
95
+ });
96
+ if (res.ok && body?.code === 0 && body.data && typeof body.data === 'object') account = body.data;
97
+ else if (data.accountAttempts < ACCOUNT_MAX_ATTEMPTS
98
+ && (body?.code === RETRY_FETCH_ACCOUNT || res.status === 401 || res.status === 403 || res.status >= 500)) {
99
+ return { status: 'pending' };
100
+ }
101
+ } catch {
102
+ if (data.accountAttempts < ACCOUNT_MAX_ATTEMPTS) return { status: 'pending' };
103
+ }
104
+
105
+ let list = [];
106
+ if (account) {
107
+ try {
108
+ const { res, body } = await fetchJson(`https://${host}/v2/plugin/accounts`, { headers: headers(host, auth) });
109
+ if (res.ok && Array.isArray(body?.data?.accounts)) list = body.data.accounts;
110
+ } catch {}
111
+ }
112
+
113
+ // 与 WorkBuddy 客户端的会话文件同构;拿不到账号信息时只保留 token(可签到 / 查积分,不能切换客户端)
114
+ const session = account
115
+ ? {
116
+ account: { ...(list.find((a) => a.uid === account.uid) || {}), ...account, lastLogin: true },
117
+ accounts: list.filter((a) => a.pluginEnabled),
118
+ allAccounts: list,
119
+ auth: data.token,
120
+ }
121
+ : { auth: data.token };
122
+ const input = sessionToAccount(session, '浏览器登录');
123
+ if (!input) throw new Error('WorkBuddy 返回的 token 无法识别(签发方不是 CodeBuddy / WorkBuddy)');
124
+ if (!account) delete input.meta.session;
125
+ input.source = 'browser';
126
+ return { status: 'ok', input };
127
+ }