creditdaddy 1.2.0 → 1.3.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,237 +1,237 @@
1
- /**
2
- * Qoder OpenAPI 客户端 — 覆盖:userinfo、配额查询、活动列表、领取、PAT→jobToken 兑换。
3
- *
4
- * 签到协议对齐 Qoder App 0.2.x 客户端:
5
- * GET /sash/api/v1/me/campaigns → { uid, showCampaign, claimable, campaigns:[…] }
6
- * POST /sash/api/v1/me/campaigns/{id}/claim
7
- * 国际版必须携带设备风控身份(Cosy-MachineToken/Code/Type)才会下发「每天领 100 Credits」,
8
- * 风控身份由本机 Qoder 客户端的 runtime-info 生成(见 qoderApp.js);国内版无需。
9
- */
10
-
11
- import {
12
- OPENAPI_BASE, CN_OPENAPI_BASE,
13
- USERINFO_PATH, QUOTA_USAGE_PATH, WEB_USAGE_PATH,
14
- CAMPAIGNS_PATH, CAMPAIGN_CLAIM_PATH, JOB_TOKEN_EXCHANGE_PATH,
15
- buildQoderHeaders, buildExchangeHeaders, FETCH_TIMEOUT_MS, webBase,
16
- } from './constants.js';
17
- import { getRiskIdentity, clientVersion, machineOs, machineHostname, machineId } from './qoderApp.js';
18
- import { logger } from './logger.js';
19
-
20
- export function apiBase(provider) {
21
- return provider === 'qoder-cn' ? CN_OPENAPI_BASE : OPENAPI_BASE;
22
- }
23
-
24
- async function req(url, options = {}) {
25
- return fetch(url, { ...options, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
26
- }
27
-
28
- /** PAT (pt-...) 兑换短期 job token (jt-...)。普通 JSON POST,无需 COSY 签名。 */
29
- export async function exchangePatToJobToken(pat, isCn = false) {
30
- const url = `${isCn ? CN_OPENAPI_BASE : OPENAPI_BASE}${JOB_TOKEN_EXCHANGE_PATH}`;
31
- const res = await req(url, {
32
- method: 'POST',
33
- headers: buildExchangeHeaders(),
34
- body: JSON.stringify({ personal_token: pat }),
35
- });
36
- if (!res.ok) {
37
- const text = await res.text().catch(() => '');
38
- throw new Error(`PAT 兑换失败: HTTP ${res.status} ${text.slice(0, 120)}`);
39
- }
40
- const data = await res.json();
41
- if (!data.token) throw new Error('PAT 兑换未返回 job token');
42
- return { jobToken: data.token, expiresAt: data.expires_at || null };
43
- }
44
-
45
- /** 解析账号的有效请求 token:PAT 先兑换成 job token */
46
- export async function resolveToken(account) {
47
- const token = String(account.token || '').trim();
48
- if (!token) return null;
49
- if (token.startsWith('pt-')) {
50
- try {
51
- const { jobToken } = await exchangePatToJobToken(token, account.provider === 'qoder-cn');
52
- return jobToken;
53
- } catch {
54
- return null; // 兑换失败按无 token 处理,由上层报告
55
- }
56
- }
57
- return token;
58
- }
59
-
60
- /** 拉取账号信息(昵称/邮箱/uid),用于给账号起显示名 */
61
- export async function fetchUserinfo(account) {
62
- const token = await resolveToken(account);
63
- if (!token) throw new Error('token 不可用');
64
- const res = await req(`${apiBase(account.provider)}${USERINFO_PATH}`, {
65
- headers: buildQoderHeaders(token),
66
- });
67
- if (!res.ok) throw new Error(`userinfo HTTP ${res.status}`);
68
- return res.json();
69
- }
70
-
71
- /** 查询配额/积分用量 */
72
- export async function fetchQuotaUsage(account) {
73
- const token = await resolveToken(account);
74
- if (!token) throw new Error('token 不可用');
75
- const res = await req(`${apiBase(account.provider)}${QUOTA_USAGE_PATH}`, {
76
- headers: buildQoderHeaders(token),
77
- });
78
- if (res.status === 401 || res.status === 403) throw new Error(`鉴权失败 (HTTP ${res.status}),token 可能已过期`);
79
- if (!res.ok) throw new Error(`quota HTTP ${res.status}`);
80
- return res.json();
81
- }
82
-
83
- function webHeaders(cookie, provider) {
84
- return {
85
- Cookie: cookie,
86
- Accept: 'application/json',
87
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
88
- Referer: webBase(provider) + '/account/usage',
89
- };
90
- }
91
-
92
- /** 用原始 Cookie 探测网页会话属于哪个 Qoder 用户(登录窗口抓来的 Cookie 按 uid 归到账号上) */
93
- export async function probeWebSession(provider, cookie) {
94
- const res = await req(`${webBase(provider)}${WEB_USAGE_PATH}`, { headers: webHeaders(cookie, provider) });
95
- if (!res.ok) return null;
96
- const data = await res.json().catch(() => null);
97
- return data?.user_id ? data : null;
98
- }
99
-
100
- /**
101
- * 网页端逐资源包用量明细(GET {webBase}/api/v2/me/usages/big_model_credits)。
102
- *
103
- * 与 openapi 的聚合 quota/usage 不同,这个接口只认浏览器登录 qoder.cn / qoder.com 后的
104
- * 会话 Cookie(存在 account.meta.qoderWebSession,桌面版登录窗口关闭前自动抓取)。
105
- * 返回 null = 该账号没有可用会话(调用方回落聚合数据);会话失效(401/403)抛错,
106
- * 由调用方清除 Cookie 并回落。
107
- */
108
- export async function fetchUsageDetail(account) {
109
- const cookie = String(account.meta?.qoderWebSession?.cookie || '').trim();
110
- if (!cookie) return null;
111
- const res = await req(`${webBase(account.provider)}${WEB_USAGE_PATH}`, { headers: webHeaders(cookie, account.provider) });
112
- if (res.status === 401 || res.status === 403) {
113
- const err = new Error(`网页会话已失效 (HTTP ${res.status})`);
114
- err.auth = true;
115
- throw err;
116
- }
117
- if (!res.ok) throw new Error(`usage detail HTTP ${res.status}`);
118
- return res.json();
119
- }
120
-
121
- /**
122
- * 在基础请求头上叠加客户端身份与设备风控身份(与 Qoder App 的 createAuthorizedHeaders 一致)。
123
- * 风控身份不可用时只加客户端身份,并在 riskError 中说明原因。
124
- */
125
- export async function buildCampaignHeaders(account, token, uid) {
126
- const headers = {
127
- ...buildQoderHeaders(token),
128
- 'Cosy-Version': clientVersion(account.provider),
129
- 'Cosy-MachineOS': machineOs(),
130
- 'Cosy-MachineId': machineId(account.provider),
131
- };
132
- const host = machineHostname();
133
- if (host) headers['Cosy-MachineHostname'] = host;
134
- let risk = null, riskError = null;
135
- try {
136
- risk = await getRiskIdentity(account.provider, uid);
137
- if (!risk) riskError = process.platform === 'linux' ? '未安装 Qoder 设备身份组件(面板 Qoder 页可一键安装),无法生成设备风控身份' : '本机未安装 Qoder 客户端,无法生成设备风控身份';
138
- } catch (e) {
139
- riskError = e.message;
140
- }
141
- if (risk) {
142
- headers['Cosy-MachineToken'] = risk.machineToken;
143
- headers['Cosy-MachineCode'] = risk.machineCode;
144
- headers['Cosy-MachineType'] = risk.machineType;
145
- }
146
- return { headers, risk: Boolean(risk), riskError };
147
- }
148
-
149
- async function getCampaigns(base, headers) {
150
- const res = await req(`${base}${CAMPAIGNS_PATH}`, { headers });
151
- if (res.status === 401 || res.status === 403) {
152
- const err = new Error(`鉴权失败 (HTTP ${res.status}),token 可能已过期`);
153
- err.auth = true;
154
- throw err;
155
- }
156
- if (!res.ok) throw new Error(`活动列表 HTTP ${res.status}`);
157
- const payload = await res.json();
158
- return {
159
- uid: typeof payload?.uid === 'string' ? payload.uid : null,
160
- campaigns: Array.isArray(payload?.campaigns) ? payload.campaigns : [],
161
- };
162
- }
163
-
164
- const creditCampaign = (c) => c.actionType === 'CLAIM_BENEFIT';
165
-
166
- /**
167
- * 对单个账号执行一次签到(领取所有可领活动)。
168
- * 返回结果对象,status 取值:
169
- * checked-in 领取成功(含金额)
170
- * already 有积分活动但今日已领
171
- * no-activity 当前无可领活动(不代表今天签过)
172
- * failed 失败(错误见 error 字段)
173
- * 附带 uid(Qoder 用户 ID),供调用方回写账号。
174
- */
175
- export async function checkinOne(account) {
176
- const label = account.name || account.id;
177
- const base = { accountId: account.id, account: label, provider: account.provider };
178
- const token = await resolveToken(account);
179
- if (!token) {
180
- return { ...base, status: 'failed', error: 'token 不可用(PAT 兑换失败或 token 为空)' };
181
- }
182
-
183
- try {
184
- // 1) 基础请求拿 uid(风控身份按 uid 生成);uid 已知时可直接跳过
185
- let uid = account.uid || null;
186
- let first = null;
187
- if (!uid) {
188
- first = await getCampaigns(apiBase(account.provider), buildQoderHeaders(token));
189
- uid = first.uid;
190
- }
191
-
192
- // 2) 带客户端身份 + 设备风控身份重新拉取(国际版只有这样才会出现每日积分活动)
193
- const { headers, risk, riskError } = await buildCampaignHeaders(account, token, uid);
194
- if (riskError) logger.debug('CHECKIN', `${label}:${riskError}`);
195
- const { campaigns, uid: uid2 } = (risk || !first) ? await getCampaigns(apiBase(account.provider), headers) : first;
196
- uid = uid2 || uid;
197
-
198
- const claimable = campaigns.filter((c) => creditCampaign(c) && c.claimStatus === 'CLAIMABLE');
199
- if (claimable.length === 0) {
200
- if (!campaigns.some(creditCampaign)) {
201
- const hint = account.provider !== 'qoder' ? '当前无可领取的活动'
202
- : risk ? '当前无可领取的活动(国际版每台设备每天限领一次,可能已被本机其他账号领取)'
203
- : `当前无可领取的活动(${riskError || '缺少设备风控身份'};国际版需要本机安装 Qoder 客户端)`;
204
- return { ...base, uid, status: 'no-activity', message: hint, claimedAmount: 0, risk };
205
- }
206
- return { ...base, uid, status: 'already', message: '今日已领', claimedAmount: 0, risk };
207
- }
208
-
209
- // 3) 逐个领取
210
- let totalClaimed = 0;
211
- const claimedList = [];
212
- const errors = [];
213
- for (const c of claimable) {
214
- try {
215
- const claimRes = await req(`${apiBase(account.provider)}${CAMPAIGN_CLAIM_PATH(c.campaignId)}`, {
216
- method: 'POST', headers,
217
- });
218
- if (claimRes.ok) {
219
- const rj = await claimRes.json().catch(() => ({}));
220
- const amount = rj.benefit?.amount || c.benefit?.amount || 0;
221
- totalClaimed += amount;
222
- claimedList.push({ campaignId: c.campaignId, campaignKey: c.campaignKey, amount, status: 'claimed' });
223
- } else {
224
- const text = await claimRes.text().catch(() => '');
225
- errors.push(`HTTP ${claimRes.status} ${text.slice(0, 120)}`);
226
- }
227
- } catch (e) { errors.push(e.message); }
228
- }
229
-
230
- if (claimedList.length > 0) {
231
- return { ...base, uid, status: 'checked-in', claimedAmount: totalClaimed, campaigns: claimedList, risk };
232
- }
233
- return { ...base, uid, status: 'failed', error: `领取失败:${errors.join(';') || '未知原因'}`, risk };
234
- } catch (err) {
235
- return { ...base, status: 'failed', error: err?.message || '网络错误' };
236
- }
237
- }
1
+ /**
2
+ * Qoder OpenAPI 客户端 — 覆盖:userinfo、配额查询、活动列表、领取、PAT→jobToken 兑换。
3
+ *
4
+ * 签到协议对齐 Qoder App 0.2.x 客户端:
5
+ * GET /sash/api/v1/me/campaigns → { uid, showCampaign, claimable, campaigns:[…] }
6
+ * POST /sash/api/v1/me/campaigns/{id}/claim
7
+ * 国际版必须携带设备风控身份(Cosy-MachineToken/Code/Type)才会下发「每天领 100 Credits」,
8
+ * 风控身份由本机 Qoder 客户端的 runtime-info 生成(见 qoderApp.js);国内版无需。
9
+ */
10
+
11
+ import {
12
+ OPENAPI_BASE, CN_OPENAPI_BASE,
13
+ USERINFO_PATH, QUOTA_USAGE_PATH, WEB_USAGE_PATH,
14
+ CAMPAIGNS_PATH, CAMPAIGN_CLAIM_PATH, JOB_TOKEN_EXCHANGE_PATH,
15
+ buildQoderHeaders, buildExchangeHeaders, FETCH_TIMEOUT_MS, webBase,
16
+ } from './constants.js';
17
+ import { getRiskIdentity, clientVersion, machineOs, machineHostname, machineId } from './qoderApp.js';
18
+ import { logger } from './logger.js';
19
+
20
+ export function apiBase(provider) {
21
+ return provider === 'qoder-cn' ? CN_OPENAPI_BASE : OPENAPI_BASE;
22
+ }
23
+
24
+ async function req(url, options = {}) {
25
+ return fetch(url, { ...options, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
26
+ }
27
+
28
+ /** PAT (pt-...) 兑换短期 job token (jt-...)。普通 JSON POST,无需 COSY 签名。 */
29
+ export async function exchangePatToJobToken(pat, isCn = false) {
30
+ const url = `${isCn ? CN_OPENAPI_BASE : OPENAPI_BASE}${JOB_TOKEN_EXCHANGE_PATH}`;
31
+ const res = await req(url, {
32
+ method: 'POST',
33
+ headers: buildExchangeHeaders(),
34
+ body: JSON.stringify({ personal_token: pat }),
35
+ });
36
+ if (!res.ok) {
37
+ const text = await res.text().catch(() => '');
38
+ throw new Error(`PAT 兑换失败: HTTP ${res.status} ${text.slice(0, 120)}`);
39
+ }
40
+ const data = await res.json();
41
+ if (!data.token) throw new Error('PAT 兑换未返回 job token');
42
+ return { jobToken: data.token, expiresAt: data.expires_at || null };
43
+ }
44
+
45
+ /** 解析账号的有效请求 token:PAT 先兑换成 job token */
46
+ export async function resolveToken(account) {
47
+ const token = String(account.token || '').trim();
48
+ if (!token) return null;
49
+ if (token.startsWith('pt-')) {
50
+ try {
51
+ const { jobToken } = await exchangePatToJobToken(token, account.provider === 'qoder-cn');
52
+ return jobToken;
53
+ } catch {
54
+ return null; // 兑换失败按无 token 处理,由上层报告
55
+ }
56
+ }
57
+ return token;
58
+ }
59
+
60
+ /** 拉取账号信息(昵称/邮箱/uid),用于给账号起显示名 */
61
+ export async function fetchUserinfo(account) {
62
+ const token = await resolveToken(account);
63
+ if (!token) throw new Error('token 不可用');
64
+ const res = await req(`${apiBase(account.provider)}${USERINFO_PATH}`, {
65
+ headers: buildQoderHeaders(token),
66
+ });
67
+ if (!res.ok) throw new Error(`userinfo HTTP ${res.status}`);
68
+ return res.json();
69
+ }
70
+
71
+ /** 查询配额/积分用量 */
72
+ export async function fetchQuotaUsage(account) {
73
+ const token = await resolveToken(account);
74
+ if (!token) throw new Error('token 不可用');
75
+ const res = await req(`${apiBase(account.provider)}${QUOTA_USAGE_PATH}`, {
76
+ headers: buildQoderHeaders(token),
77
+ });
78
+ if (res.status === 401 || res.status === 403) throw new Error(`鉴权失败 (HTTP ${res.status}),token 可能已过期`);
79
+ if (!res.ok) throw new Error(`quota HTTP ${res.status}`);
80
+ return res.json();
81
+ }
82
+
83
+ function webHeaders(cookie, provider) {
84
+ return {
85
+ Cookie: cookie,
86
+ Accept: 'application/json',
87
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
88
+ Referer: webBase(provider) + '/account/usage',
89
+ };
90
+ }
91
+
92
+ /** 用原始 Cookie 探测网页会话属于哪个 Qoder 用户(登录窗口抓来的 Cookie 按 uid 归到账号上) */
93
+ export async function probeWebSession(provider, cookie) {
94
+ const res = await req(`${webBase(provider)}${WEB_USAGE_PATH}`, { headers: webHeaders(cookie, provider) });
95
+ if (!res.ok) return null;
96
+ const data = await res.json().catch(() => null);
97
+ return data?.user_id ? data : null;
98
+ }
99
+
100
+ /**
101
+ * 网页端逐资源包用量明细(GET {webBase}/api/v2/me/usages/big_model_credits)。
102
+ *
103
+ * 与 openapi 的聚合 quota/usage 不同,这个接口只认浏览器登录 qoder.cn / qoder.com 后的
104
+ * 会话 Cookie(存在 account.meta.qoderWebSession,桌面版登录窗口关闭前自动抓取)。
105
+ * 返回 null = 该账号没有可用会话(调用方回落聚合数据);会话失效(401/403)抛错,
106
+ * 由调用方清除 Cookie 并回落。
107
+ */
108
+ export async function fetchUsageDetail(account) {
109
+ const cookie = String(account.meta?.qoderWebSession?.cookie || '').trim();
110
+ if (!cookie) return null;
111
+ const res = await req(`${webBase(account.provider)}${WEB_USAGE_PATH}`, { headers: webHeaders(cookie, account.provider) });
112
+ if (res.status === 401 || res.status === 403) {
113
+ const err = new Error(`网页会话已失效 (HTTP ${res.status})`);
114
+ err.auth = true;
115
+ throw err;
116
+ }
117
+ if (!res.ok) throw new Error(`usage detail HTTP ${res.status}`);
118
+ return res.json();
119
+ }
120
+
121
+ /**
122
+ * 在基础请求头上叠加客户端身份与设备风控身份(与 Qoder App 的 createAuthorizedHeaders 一致)。
123
+ * 风控身份不可用时只加客户端身份,并在 riskError 中说明原因。
124
+ */
125
+ export async function buildCampaignHeaders(account, token, uid) {
126
+ const headers = {
127
+ ...buildQoderHeaders(token),
128
+ 'Cosy-Version': clientVersion(account.provider),
129
+ 'Cosy-MachineOS': machineOs(),
130
+ 'Cosy-MachineId': machineId(account.provider),
131
+ };
132
+ const host = machineHostname();
133
+ if (host) headers['Cosy-MachineHostname'] = host;
134
+ let risk = null, riskError = null;
135
+ try {
136
+ risk = await getRiskIdentity(account.provider, uid);
137
+ if (!risk) riskError = process.platform === 'linux' ? '未安装 Qoder 设备身份组件(面板 Qoder 页可一键安装),无法生成设备风控身份' : '本机未安装 Qoder 客户端,无法生成设备风控身份';
138
+ } catch (e) {
139
+ riskError = e.message;
140
+ }
141
+ if (risk) {
142
+ headers['Cosy-MachineToken'] = risk.machineToken;
143
+ headers['Cosy-MachineCode'] = risk.machineCode;
144
+ headers['Cosy-MachineType'] = risk.machineType;
145
+ }
146
+ return { headers, risk: Boolean(risk), riskError };
147
+ }
148
+
149
+ async function getCampaigns(base, headers) {
150
+ const res = await req(`${base}${CAMPAIGNS_PATH}`, { headers });
151
+ if (res.status === 401 || res.status === 403) {
152
+ const err = new Error(`鉴权失败 (HTTP ${res.status}),token 可能已过期`);
153
+ err.auth = true;
154
+ throw err;
155
+ }
156
+ if (!res.ok) throw new Error(`活动列表 HTTP ${res.status}`);
157
+ const payload = await res.json();
158
+ return {
159
+ uid: typeof payload?.uid === 'string' ? payload.uid : null,
160
+ campaigns: Array.isArray(payload?.campaigns) ? payload.campaigns : [],
161
+ };
162
+ }
163
+
164
+ const creditCampaign = (c) => c.actionType === 'CLAIM_BENEFIT';
165
+
166
+ /**
167
+ * 对单个账号执行一次签到(领取所有可领活动)。
168
+ * 返回结果对象,status 取值:
169
+ * checked-in 领取成功(含金额)
170
+ * already 有积分活动但今日已领
171
+ * no-activity 当前无可领活动(不代表今天签过)
172
+ * failed 失败(错误见 error 字段)
173
+ * 附带 uid(Qoder 用户 ID),供调用方回写账号。
174
+ */
175
+ export async function checkinOne(account) {
176
+ const label = account.name || account.id;
177
+ const base = { accountId: account.id, account: label, provider: account.provider };
178
+ const token = await resolveToken(account);
179
+ if (!token) {
180
+ return { ...base, status: 'failed', error: 'token 不可用(PAT 兑换失败或 token 为空)' };
181
+ }
182
+
183
+ try {
184
+ // 1) 基础请求拿 uid(风控身份按 uid 生成);uid 已知时可直接跳过
185
+ let uid = account.uid || null;
186
+ let first = null;
187
+ if (!uid) {
188
+ first = await getCampaigns(apiBase(account.provider), buildQoderHeaders(token));
189
+ uid = first.uid;
190
+ }
191
+
192
+ // 2) 带客户端身份 + 设备风控身份重新拉取(国际版只有这样才会出现每日积分活动)
193
+ const { headers, risk, riskError } = await buildCampaignHeaders(account, token, uid);
194
+ if (riskError) logger.debug('CHECKIN', `${label}:${riskError}`);
195
+ const { campaigns, uid: uid2 } = (risk || !first) ? await getCampaigns(apiBase(account.provider), headers) : first;
196
+ uid = uid2 || uid;
197
+
198
+ const claimable = campaigns.filter((c) => creditCampaign(c) && c.claimStatus === 'CLAIMABLE');
199
+ if (claimable.length === 0) {
200
+ if (!campaigns.some(creditCampaign)) {
201
+ const hint = account.provider !== 'qoder' ? '当前无可领取的活动'
202
+ : risk ? '当前无可领取的活动(国际版每台设备每天限领一次,可能已被本机其他账号领取)'
203
+ : `当前无可领取的活动(${riskError || '缺少设备风控身份'};国际版需要本机安装 Qoder 客户端)`;
204
+ return { ...base, uid, status: 'no-activity', message: hint, claimedAmount: 0, risk };
205
+ }
206
+ return { ...base, uid, status: 'already', message: '今日已领', claimedAmount: 0, risk };
207
+ }
208
+
209
+ // 3) 逐个领取
210
+ let totalClaimed = 0;
211
+ const claimedList = [];
212
+ const errors = [];
213
+ for (const c of claimable) {
214
+ try {
215
+ const claimRes = await req(`${apiBase(account.provider)}${CAMPAIGN_CLAIM_PATH(c.campaignId)}`, {
216
+ method: 'POST', headers,
217
+ });
218
+ if (claimRes.ok) {
219
+ const rj = await claimRes.json().catch(() => ({}));
220
+ const amount = rj.benefit?.amount || c.benefit?.amount || 0;
221
+ totalClaimed += amount;
222
+ claimedList.push({ campaignId: c.campaignId, campaignKey: c.campaignKey, amount, status: 'claimed' });
223
+ } else {
224
+ const text = await claimRes.text().catch(() => '');
225
+ errors.push(`HTTP ${claimRes.status} ${text.slice(0, 120)}`);
226
+ }
227
+ } catch (e) { errors.push(e.message); }
228
+ }
229
+
230
+ if (claimedList.length > 0) {
231
+ return { ...base, uid, status: 'checked-in', claimedAmount: totalClaimed, campaigns: claimedList, risk };
232
+ }
233
+ return { ...base, uid, status: 'failed', error: `领取失败:${errors.join(';') || '未知原因'}`, risk };
234
+ } catch (err) {
235
+ return { ...base, status: 'failed', error: err?.message || '网络错误' };
236
+ }
237
+ }