creditdaddy 1.1.1 → 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.
- package/LICENSE +21 -21
- package/README.md +306 -289
- package/bin/creditdaddy.js +8 -4
- package/package.json +29 -28
- package/src/accounts.js +161 -161
- package/src/authDevice.js +205 -205
- package/src/catpawClient.js +103 -103
- package/src/catpawLocal.js +229 -229
- package/src/checkin.js +357 -340
- package/src/constants.js +97 -95
- package/src/daemon.js +130 -14
- package/src/localDetect.js +147 -147
- package/src/mirasimClient.js +189 -189
- package/src/panel.html +465 -142
- package/src/providers.js +190 -181
- package/src/qoderApp.js +93 -7
- package/src/qoderClient.js +237 -237
- package/src/qoderUmid.js +198 -198
- package/src/store.js +238 -238
- package/src/tenrouter.js +18 -8
- package/src/tenrouterAccounts.js +163 -163
- package/src/traeClient.js +258 -0
- package/src/traeLocal.js +337 -0
- package/src/transfer.js +160 -160
- package/src/usageSync.js +505 -505
- package/src/workbuddyLocal.js +133 -133
- package/src/zcodeAuth.js +131 -131
- package/src/zcodeAutoClaim.js +97 -97
- package/src/zcodeClient.js +33 -2
- package/src/zcodeGateway.js +26 -30
- package/src/zcodeLocal.js +334 -319
- package/src/zcrypto.js +160 -160
package/bin/creditdaddy.js
CHANGED
|
@@ -17,7 +17,7 @@ import { logger } from '../src/logger.js';
|
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
18
|
const cmd = args[0] || 'daemon';
|
|
19
19
|
|
|
20
|
-
const HELP = `CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 多账号管理 + 每日积分自动领取
|
|
20
|
+
const HELP = `CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 多账号管理 + 每日积分自动领取
|
|
21
21
|
|
|
22
22
|
用法:
|
|
23
23
|
creditdaddy daemon [--port 47860] [--host 127.0.0.1] 启动守护进程 + Web 面板
|
|
@@ -29,7 +29,7 @@ const HELP = `CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 多
|
|
|
29
29
|
creditdaddy export [file.json] --password 口令 [--cn|--intl]
|
|
30
30
|
导出账号(口令必填,至少 4 位;与 10router 迁移文件互通)
|
|
31
31
|
creditdaddy import <file.json> [--password 口令] 导入 CreditDaddy / 10router 导出文件
|
|
32
|
-
creditdaddy scan 导入本机 Qoder / WorkBuddy / ZCode / mirasim / 妙手 客户端已登录的账号
|
|
32
|
+
creditdaddy scan 导入本机 Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 客户端已登录的账号
|
|
33
33
|
creditdaddy umid [install|remove] Qoder 设备身份组件(Linux / fnOS 国际版领取用)
|
|
34
34
|
creditdaddy logs 查看领取状态
|
|
35
35
|
creditdaddy help 显示本帮助`;
|
|
@@ -138,6 +138,7 @@ async function main() {
|
|
|
138
138
|
const { liveToAccount: zcodeLive } = await import('../src/zcodeLocal.js');
|
|
139
139
|
const { liveToAccount: mirasimLive } = await import('../src/mirasimLocal.js');
|
|
140
140
|
const { liveToAccount: catpawLive } = await import('../src/catpawLocal.js');
|
|
141
|
+
const { liveToAccount: traeLive } = await import('../src/traeLocal.js');
|
|
141
142
|
const { addAccount } = await import('../src/accounts.js');
|
|
142
143
|
const qa = await readQoderAppAccounts();
|
|
143
144
|
const wb = readWorkbuddySessions();
|
|
@@ -148,10 +149,12 @@ async function main() {
|
|
|
148
149
|
try { miraAccount = await mirasimLive(); } catch (e) { skipped.push({ file: '~/.mirasim/setting.json', error: e.message }); }
|
|
149
150
|
let cpAccount = null;
|
|
150
151
|
try { cpAccount = await catpawLive(); } catch (e) { skipped.push({ file: 'catpaw-moon/catx-credential.json', error: e.message }); }
|
|
152
|
+
let traeAccount = null;
|
|
153
|
+
try { traeAccount = await traeLive(); } catch (e) { skipped.push({ file: 'TRAE SOLO CN/User/globalStorage/storage.json', error: e.message }); }
|
|
151
154
|
const records = [
|
|
152
155
|
...qa.accounts.map((c) => ({
|
|
153
156
|
label: c.source,
|
|
154
|
-
rec: { provider: c.provider, token: c.token, name: c.user.name || c.user.email, uid: c.user.id, email: c.user.email, refreshToken: c.refreshToken, expiresAt: c.expiresAt, source: 'local-app' },
|
|
157
|
+
rec: { provider: c.provider, token: c.token, name: c.user.name || c.user.email, uid: c.user.id, email: c.user.email, refreshToken: c.refreshToken, expiresAt: c.expiresAt, source: 'local-app', meta: { qoderAuth: c.authJson, qoderAuthFile: c.file } },
|
|
155
158
|
})),
|
|
156
159
|
...wb.accounts.map(({ file: _f, fileTime: _t, current, source, ...rec }) => ({
|
|
157
160
|
label: source, rec: { ...rec, source: current ? 'workbuddy-current' : 'workbuddy-history' },
|
|
@@ -159,9 +162,10 @@ async function main() {
|
|
|
159
162
|
...(zAccount ? [{ label: 'ZCode 当前登录', rec: zAccount }] : []),
|
|
160
163
|
...(miraAccount ? [{ label: 'mirasim 当前登录', rec: miraAccount }] : []),
|
|
161
164
|
...(cpAccount ? [{ label: '妙手当前登录', rec: cpAccount }] : []),
|
|
165
|
+
...(traeAccount ? [{ label: 'Trae 当前登录', rec: traeAccount }] : []),
|
|
162
166
|
];
|
|
163
167
|
for (const e of [...qa.errors, ...wb.errors, ...skipped]) console.log('⚠', e.file, e.error);
|
|
164
|
-
if (!records.length) { console.log('(本机 Qoder / WorkBuddy / ZCode / mirasim / 妙手 客户端未登录或未安装)'); break; }
|
|
168
|
+
if (!records.length) { console.log('(本机 Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 客户端未登录或未安装)'); break; }
|
|
165
169
|
for (const { label, rec } of records) {
|
|
166
170
|
const r = await addAccount(rec, { trusted: true });
|
|
167
171
|
console.log(r.duplicate ? (r.updated ? '↻ 已更新' : '· 已存在') : '✓ 已导入', r.account.name || r.account.id, `(${label})`);
|
package/package.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "creditdaddy",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim 多账号本地管理与每日积分自动签到助手",
|
|
5
|
-
"homepage": "https://github.com/techysy/CreditDaddy",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/techysy/CreditDaddy.git"
|
|
9
|
-
},
|
|
10
|
-
"type": "module",
|
|
11
|
-
"bin": {
|
|
12
|
-
"creditdaddy": "bin/creditdaddy.js"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"bin",
|
|
16
|
-
"src",
|
|
17
|
-
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
],
|
|
20
|
-
"engines": {
|
|
21
|
-
"node": ">=20"
|
|
22
|
-
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"start": "node bin/creditdaddy.js daemon",
|
|
25
|
-
"test": "node --test"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "creditdaddy",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "CreditDaddy — Qoder / WorkBuddy / ZCode / mirasim / 妙手 / Trae 多账号本地管理与每日积分自动签到助手",
|
|
5
|
+
"homepage": "https://github.com/techysy/CreditDaddy",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/techysy/CreditDaddy.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"creditdaddy": "bin/creditdaddy.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node bin/creditdaddy.js daemon",
|
|
25
|
+
"test": "node --test",
|
|
26
|
+
"release-check": "bash scripts/release-check.sh"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
package/src/accounts.js
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 账号新增 / 导入的统一入口 — daemon、CLI、设备码登录、本机导入、文件导入共用。
|
|
3
|
-
*
|
|
4
|
-
* 网络校验(userinfo)在锁外完成;去重 + 入库在 withAccounts 串行队列内完成,
|
|
5
|
-
* 并发请求(如签到轮与添加账号同时发生)不会互相覆盖。
|
|
6
|
-
* 同一 provider 下 uid 相同但 token 不同 → 视为 token 续期,原地更新而不是新增。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { normalizeAccountInput, findDuplicate, loadAccounts, withAccounts, loadSettings, saveSettings } from './store.js';
|
|
10
|
-
import { productImpl, displayNameFrom } from './providers.js';
|
|
11
|
-
|
|
12
|
-
export { displayNameFrom };
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 若该 Qoder 账号有 uid 但还没有网页会话(逐资源包明细用),从 settings 的暂存区取货挂上。
|
|
16
|
-
* 暂存区由桌面版登录窗口写入(会话可能先于账号入库出现)。返回是否有变化。
|
|
17
|
-
*/
|
|
18
|
-
async function attachPendingWebSession(target) {
|
|
19
|
-
if (!String(target.provider || '').startsWith('qoder') || !target.uid) return false;
|
|
20
|
-
if (target.meta?.qoderWebSession) return false;
|
|
21
|
-
const pending = (await loadSettings()).pendingQoderWebSessions || {};
|
|
22
|
-
const item = pending[target.uid];
|
|
23
|
-
if (!item || item.kind !== target.provider || !item.cookie) return false;
|
|
24
|
-
const { [target.uid]: _used, ...rest } = pending;
|
|
25
|
-
await saveSettings({ pendingQoderWebSessions: rest });
|
|
26
|
-
target.meta = { ...(target.meta || {}), qoderWebSession: { cookie: item.cookie, capturedAt: item.capturedAt || new Date().toISOString() } };
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 用新记录刷新已存在账号(保留 id / 名字 / 签到记录):
|
|
32
|
-
* token 不同 → 续期(换 token + 凭据元数据);token 相同 → 只补全缺失的 uid / 邮箱 / 有效期 / refreshToken
|
|
33
|
-
* 返回是否有变化。
|
|
34
|
-
*/
|
|
35
|
-
function refreshExisting(existing, incoming) {
|
|
36
|
-
let changed = false;
|
|
37
|
-
const set = (k, v) => { if (v && existing[k] !== v) { existing[k] = v; changed = true; } };
|
|
38
|
-
if (existing.token !== incoming.token) {
|
|
39
|
-
set('token', incoming.token);
|
|
40
|
-
set('refreshToken', incoming.refreshToken);
|
|
41
|
-
set('expiresAt', incoming.expiresAt);
|
|
42
|
-
if (incoming.verified !== undefined) existing.verified = incoming.verified;
|
|
43
|
-
// 换上了新凭据:旧 token 留下的失败结果(如「登录已失效,请重新登录」)不再成立,
|
|
44
|
-
// 否则卡片会一直显示旧错误,直到下一次签到才被覆盖
|
|
45
|
-
if (existing.lastResult?.status === 'failed') { existing.lastResult = null; changed = true; }
|
|
46
|
-
if (existing.verifyError) { delete existing.verifyError; changed = true; }
|
|
47
|
-
} else {
|
|
48
|
-
if (!existing.refreshToken) set('refreshToken', incoming.refreshToken);
|
|
49
|
-
if (!existing.expiresAt) set('expiresAt', incoming.expiresAt);
|
|
50
|
-
}
|
|
51
|
-
if (!existing.uid) set('uid', incoming.uid);
|
|
52
|
-
if (!existing.email) set('email', incoming.email);
|
|
53
|
-
// 产品元数据(如 WorkBuddy 会话 / 域名)以最新导入为准
|
|
54
|
-
const incomingMeta = incoming.meta && Object.keys(incoming.meta).length ? incoming.meta : null;
|
|
55
|
-
if (incomingMeta && JSON.stringify({ ...existing.meta, ...incomingMeta }) !== JSON.stringify(existing.meta || {})) {
|
|
56
|
-
existing.meta = { ...(existing.meta || {}), ...incomingMeta };
|
|
57
|
-
changed = true;
|
|
58
|
-
}
|
|
59
|
-
if (changed) existing.updatedAt = new Date().toISOString();
|
|
60
|
-
return changed;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 在锁内合并一条记录:新增 / 续期 / 重复;顺带挂上暂存的 Qoder 网页会话 */
|
|
64
|
-
async function mergeInto(accounts, account) {
|
|
65
|
-
const dup = findDuplicate(accounts, account.provider, account.token, account.uid);
|
|
66
|
-
if (!dup) {
|
|
67
|
-
await attachPendingWebSession(account);
|
|
68
|
-
accounts.push(account);
|
|
69
|
-
return { account, duplicate: false, updated: false };
|
|
70
|
-
}
|
|
71
|
-
const refreshed = refreshExisting(dup, account);
|
|
72
|
-
const attached = await attachPendingWebSession(dup);
|
|
73
|
-
return { account: dup, duplicate: true, updated: refreshed || attached };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* 新增一个账号。
|
|
78
|
-
* @param {{name?, provider, token, uid?, email?, refreshToken?, expiresAt?, source?}} input
|
|
79
|
-
* @param {{verify?: boolean, trusted?: boolean}} opts
|
|
80
|
-
* verify 调用 userinfo 校验 token 并补全昵称 / uid / 邮箱(默认 true,失败仍会保存)
|
|
81
|
-
* trusted token 来源已可信(如设备码登录、本机客户端),直接标记 verified
|
|
82
|
-
* @returns {Promise<{account, duplicate: boolean, updated: boolean}>}
|
|
83
|
-
* duplicate=true 时 account 为已存在的记录;updated=true 表示已用新 token 续期
|
|
84
|
-
*/
|
|
85
|
-
export async function addAccount(input, { verify = true, trusted = false } = {}) {
|
|
86
|
-
const account = normalizeAccountInput(input);
|
|
87
|
-
|
|
88
|
-
// 先快速查重:完全相同的 token 无需再发网络请求(只在锁内补全缺失的元数据)
|
|
89
|
-
const existing = findDuplicate(await loadAccounts(), account.provider, account.token);
|
|
90
|
-
if (existing && existing.token === account.token) return withAccounts((accounts) => mergeInto(accounts, account));
|
|
91
|
-
|
|
92
|
-
if (trusted) {
|
|
93
|
-
account.verified = true;
|
|
94
|
-
} else if (verify) {
|
|
95
|
-
try {
|
|
96
|
-
const info = await productImpl(account.provider).verify(account);
|
|
97
|
-
if (!account.name && info.name) account.name = info.name;
|
|
98
|
-
if (!account.uid && info.uid) account.uid = info.uid;
|
|
99
|
-
if (!account.email && info.email) account.email = info.email;
|
|
100
|
-
account.verified = true;
|
|
101
|
-
} catch (e) {
|
|
102
|
-
account.verified = false;
|
|
103
|
-
account.verifyError = e.message;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return withAccounts((accounts) => mergeInto(accounts, account));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 批量导入(已解析的记录数组,见 transfer.parseImport),不做网络校验。
|
|
112
|
-
* @returns {Promise<{added, updated, skipped}>}
|
|
113
|
-
*/
|
|
114
|
-
export function importAccounts(list) {
|
|
115
|
-
return withAccounts(async (accounts) => {
|
|
116
|
-
let added = 0, updated = 0, skipped = 0;
|
|
117
|
-
for (const item of list) {
|
|
118
|
-
try {
|
|
119
|
-
const r = await mergeInto(accounts, normalizeAccountInput(item));
|
|
120
|
-
if (!r.duplicate) added++;
|
|
121
|
-
else if (r.updated) updated++;
|
|
122
|
-
else skipped++;
|
|
123
|
-
} catch { skipped++; }
|
|
124
|
-
}
|
|
125
|
-
return { added, updated, skipped };
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* token 刷新后的回写上下文(传给 productImpl(...).checkin / quota 的 ctx):
|
|
131
|
-
* 新 token 立即写回账号库;若该账号正是 WorkBuddy 客户端当前登录的账号,同步写回客户端会话文件,
|
|
132
|
-
* 避免客户端手里被轮换掉的旧 refreshToken 失效导致掉线。
|
|
133
|
-
*/
|
|
134
|
-
export function refreshContext(account, log) {
|
|
135
|
-
return {
|
|
136
|
-
onRefresh: async (creds) => {
|
|
137
|
-
account.token = creds.token;
|
|
138
|
-
account.refreshToken = creds.refreshToken;
|
|
139
|
-
account.expiresAt = creds.expiresAt;
|
|
140
|
-
await withAccounts((accounts) => {
|
|
141
|
-
const cur = accounts.find((a) => a.id === account.id);
|
|
142
|
-
if (!cur) return;
|
|
143
|
-
cur.token = creds.token;
|
|
144
|
-
cur.refreshToken = creds.refreshToken;
|
|
145
|
-
cur.expiresAt = creds.expiresAt;
|
|
146
|
-
if (creds.refreshExpiresAt) cur.meta = { ...(cur.meta || {}), refreshExpiresAt: creds.refreshExpiresAt };
|
|
147
|
-
cur.updatedAt = new Date().toISOString();
|
|
148
|
-
});
|
|
149
|
-
log?.('token 已刷新');
|
|
150
|
-
if (account.provider.startsWith('workbuddy')) {
|
|
151
|
-
const { currentWorkbuddyUid, writeWorkbuddySession } = await import('./workbuddyLocal.js');
|
|
152
|
-
try {
|
|
153
|
-
if (account.uid && currentWorkbuddyUid() === account.uid) {
|
|
154
|
-
writeWorkbuddySession(account);
|
|
155
|
-
log?.('已同步新 token 到 WorkBuddy 客户端');
|
|
156
|
-
}
|
|
157
|
-
} catch (e) { log?.('同步到 WorkBuddy 客户端失败:' + e.message); }
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 账号新增 / 导入的统一入口 — daemon、CLI、设备码登录、本机导入、文件导入共用。
|
|
3
|
+
*
|
|
4
|
+
* 网络校验(userinfo)在锁外完成;去重 + 入库在 withAccounts 串行队列内完成,
|
|
5
|
+
* 并发请求(如签到轮与添加账号同时发生)不会互相覆盖。
|
|
6
|
+
* 同一 provider 下 uid 相同但 token 不同 → 视为 token 续期,原地更新而不是新增。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { normalizeAccountInput, findDuplicate, loadAccounts, withAccounts, loadSettings, saveSettings } from './store.js';
|
|
10
|
+
import { productImpl, displayNameFrom } from './providers.js';
|
|
11
|
+
|
|
12
|
+
export { displayNameFrom };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 若该 Qoder 账号有 uid 但还没有网页会话(逐资源包明细用),从 settings 的暂存区取货挂上。
|
|
16
|
+
* 暂存区由桌面版登录窗口写入(会话可能先于账号入库出现)。返回是否有变化。
|
|
17
|
+
*/
|
|
18
|
+
async function attachPendingWebSession(target) {
|
|
19
|
+
if (!String(target.provider || '').startsWith('qoder') || !target.uid) return false;
|
|
20
|
+
if (target.meta?.qoderWebSession) return false;
|
|
21
|
+
const pending = (await loadSettings()).pendingQoderWebSessions || {};
|
|
22
|
+
const item = pending[target.uid];
|
|
23
|
+
if (!item || item.kind !== target.provider || !item.cookie) return false;
|
|
24
|
+
const { [target.uid]: _used, ...rest } = pending;
|
|
25
|
+
await saveSettings({ pendingQoderWebSessions: rest });
|
|
26
|
+
target.meta = { ...(target.meta || {}), qoderWebSession: { cookie: item.cookie, capturedAt: item.capturedAt || new Date().toISOString() } };
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 用新记录刷新已存在账号(保留 id / 名字 / 签到记录):
|
|
32
|
+
* token 不同 → 续期(换 token + 凭据元数据);token 相同 → 只补全缺失的 uid / 邮箱 / 有效期 / refreshToken
|
|
33
|
+
* 返回是否有变化。
|
|
34
|
+
*/
|
|
35
|
+
function refreshExisting(existing, incoming) {
|
|
36
|
+
let changed = false;
|
|
37
|
+
const set = (k, v) => { if (v && existing[k] !== v) { existing[k] = v; changed = true; } };
|
|
38
|
+
if (existing.token !== incoming.token) {
|
|
39
|
+
set('token', incoming.token);
|
|
40
|
+
set('refreshToken', incoming.refreshToken);
|
|
41
|
+
set('expiresAt', incoming.expiresAt);
|
|
42
|
+
if (incoming.verified !== undefined) existing.verified = incoming.verified;
|
|
43
|
+
// 换上了新凭据:旧 token 留下的失败结果(如「登录已失效,请重新登录」)不再成立,
|
|
44
|
+
// 否则卡片会一直显示旧错误,直到下一次签到才被覆盖
|
|
45
|
+
if (existing.lastResult?.status === 'failed') { existing.lastResult = null; changed = true; }
|
|
46
|
+
if (existing.verifyError) { delete existing.verifyError; changed = true; }
|
|
47
|
+
} else {
|
|
48
|
+
if (!existing.refreshToken) set('refreshToken', incoming.refreshToken);
|
|
49
|
+
if (!existing.expiresAt) set('expiresAt', incoming.expiresAt);
|
|
50
|
+
}
|
|
51
|
+
if (!existing.uid) set('uid', incoming.uid);
|
|
52
|
+
if (!existing.email) set('email', incoming.email);
|
|
53
|
+
// 产品元数据(如 WorkBuddy 会话 / 域名)以最新导入为准
|
|
54
|
+
const incomingMeta = incoming.meta && Object.keys(incoming.meta).length ? incoming.meta : null;
|
|
55
|
+
if (incomingMeta && JSON.stringify({ ...existing.meta, ...incomingMeta }) !== JSON.stringify(existing.meta || {})) {
|
|
56
|
+
existing.meta = { ...(existing.meta || {}), ...incomingMeta };
|
|
57
|
+
changed = true;
|
|
58
|
+
}
|
|
59
|
+
if (changed) existing.updatedAt = new Date().toISOString();
|
|
60
|
+
return changed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 在锁内合并一条记录:新增 / 续期 / 重复;顺带挂上暂存的 Qoder 网页会话 */
|
|
64
|
+
async function mergeInto(accounts, account) {
|
|
65
|
+
const dup = findDuplicate(accounts, account.provider, account.token, account.uid);
|
|
66
|
+
if (!dup) {
|
|
67
|
+
await attachPendingWebSession(account);
|
|
68
|
+
accounts.push(account);
|
|
69
|
+
return { account, duplicate: false, updated: false };
|
|
70
|
+
}
|
|
71
|
+
const refreshed = refreshExisting(dup, account);
|
|
72
|
+
const attached = await attachPendingWebSession(dup);
|
|
73
|
+
return { account: dup, duplicate: true, updated: refreshed || attached };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 新增一个账号。
|
|
78
|
+
* @param {{name?, provider, token, uid?, email?, refreshToken?, expiresAt?, source?}} input
|
|
79
|
+
* @param {{verify?: boolean, trusted?: boolean}} opts
|
|
80
|
+
* verify 调用 userinfo 校验 token 并补全昵称 / uid / 邮箱(默认 true,失败仍会保存)
|
|
81
|
+
* trusted token 来源已可信(如设备码登录、本机客户端),直接标记 verified
|
|
82
|
+
* @returns {Promise<{account, duplicate: boolean, updated: boolean}>}
|
|
83
|
+
* duplicate=true 时 account 为已存在的记录;updated=true 表示已用新 token 续期
|
|
84
|
+
*/
|
|
85
|
+
export async function addAccount(input, { verify = true, trusted = false } = {}) {
|
|
86
|
+
const account = normalizeAccountInput(input);
|
|
87
|
+
|
|
88
|
+
// 先快速查重:完全相同的 token 无需再发网络请求(只在锁内补全缺失的元数据)
|
|
89
|
+
const existing = findDuplicate(await loadAccounts(), account.provider, account.token);
|
|
90
|
+
if (existing && existing.token === account.token) return withAccounts((accounts) => mergeInto(accounts, account));
|
|
91
|
+
|
|
92
|
+
if (trusted) {
|
|
93
|
+
account.verified = true;
|
|
94
|
+
} else if (verify) {
|
|
95
|
+
try {
|
|
96
|
+
const info = await productImpl(account.provider).verify(account);
|
|
97
|
+
if (!account.name && info.name) account.name = info.name;
|
|
98
|
+
if (!account.uid && info.uid) account.uid = info.uid;
|
|
99
|
+
if (!account.email && info.email) account.email = info.email;
|
|
100
|
+
account.verified = true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
account.verified = false;
|
|
103
|
+
account.verifyError = e.message;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return withAccounts((accounts) => mergeInto(accounts, account));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 批量导入(已解析的记录数组,见 transfer.parseImport),不做网络校验。
|
|
112
|
+
* @returns {Promise<{added, updated, skipped}>}
|
|
113
|
+
*/
|
|
114
|
+
export function importAccounts(list) {
|
|
115
|
+
return withAccounts(async (accounts) => {
|
|
116
|
+
let added = 0, updated = 0, skipped = 0;
|
|
117
|
+
for (const item of list) {
|
|
118
|
+
try {
|
|
119
|
+
const r = await mergeInto(accounts, normalizeAccountInput(item));
|
|
120
|
+
if (!r.duplicate) added++;
|
|
121
|
+
else if (r.updated) updated++;
|
|
122
|
+
else skipped++;
|
|
123
|
+
} catch { skipped++; }
|
|
124
|
+
}
|
|
125
|
+
return { added, updated, skipped };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* token 刷新后的回写上下文(传给 productImpl(...).checkin / quota 的 ctx):
|
|
131
|
+
* 新 token 立即写回账号库;若该账号正是 WorkBuddy 客户端当前登录的账号,同步写回客户端会话文件,
|
|
132
|
+
* 避免客户端手里被轮换掉的旧 refreshToken 失效导致掉线。
|
|
133
|
+
*/
|
|
134
|
+
export function refreshContext(account, log) {
|
|
135
|
+
return {
|
|
136
|
+
onRefresh: async (creds) => {
|
|
137
|
+
account.token = creds.token;
|
|
138
|
+
account.refreshToken = creds.refreshToken;
|
|
139
|
+
account.expiresAt = creds.expiresAt;
|
|
140
|
+
await withAccounts((accounts) => {
|
|
141
|
+
const cur = accounts.find((a) => a.id === account.id);
|
|
142
|
+
if (!cur) return;
|
|
143
|
+
cur.token = creds.token;
|
|
144
|
+
cur.refreshToken = creds.refreshToken;
|
|
145
|
+
cur.expiresAt = creds.expiresAt;
|
|
146
|
+
if (creds.refreshExpiresAt) cur.meta = { ...(cur.meta || {}), refreshExpiresAt: creds.refreshExpiresAt };
|
|
147
|
+
cur.updatedAt = new Date().toISOString();
|
|
148
|
+
});
|
|
149
|
+
log?.('token 已刷新');
|
|
150
|
+
if (account.provider.startsWith('workbuddy')) {
|
|
151
|
+
const { currentWorkbuddyUid, writeWorkbuddySession } = await import('./workbuddyLocal.js');
|
|
152
|
+
try {
|
|
153
|
+
if (account.uid && currentWorkbuddyUid() === account.uid) {
|
|
154
|
+
writeWorkbuddySession(account);
|
|
155
|
+
log?.('已同步新 token 到 WorkBuddy 客户端');
|
|
156
|
+
}
|
|
157
|
+
} catch (e) { log?.('同步到 WorkBuddy 客户端失败:' + e.message); }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|