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.
package/src/providers.js CHANGED
@@ -1,100 +1,190 @@
1
- /**
2
- * 产品线注册表 — 按账号 provider 分发签到 / 积分 / 校验。
3
- *
4
- * 每个产品实现(checkin 可缺省:该产品不参与每日签到,如 ZCode):
5
- * checkin(account, ctx) → { status: checked-in|already|limited|no-activity|failed, … }
6
- * quota(account, ctx) → { total, used, remaining, exceeded, parts: [{ name, total, used, remaining, expiresAt, recurring }] }
7
- * verify(account) → { name?, uid?, email? }(添加账号时补全资料,失败抛错)
8
- * ctx.onRefresh(creds):产品在 token 刷新后回调,由调用方负责持久化。
9
- */
10
-
11
- import { productOf } from './constants.js';
12
- import { checkinOne as checkinQoder, fetchQuotaUsage, fetchUserinfo } from './qoderClient.js';
13
- import { checkinWorkbuddy, checkinWorkbuddyIntl, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
14
- import { fetchZcodeQuota } from './zcodeClient.js';
15
- import { fetchMirasimQuota, fetchMirasimProfile } from './mirasimClient.js';
16
- /** 从 userinfo 响应中挑一个可读的显示名 */
17
- export function displayNameFrom(ui) {
18
- const pick = [ui?.nickname, ui?.name, ui?.username, ui?.email]
19
- .map((v) => (typeof v === 'string' ? v.trim() : ''))
20
- .find(Boolean);
21
- return pick || null;
22
- }
23
-
24
- /** Qoder /api/v2/quota/usage → 统一积分结构 */
25
- export function normalizeQoderQuota(q) {
26
- const names = [['userQuota', '套餐额度', true], ['addOnQuota', '附加额度', false], ['orgResourcePackage', '组织资源包', false]];
27
- const expiresAt = Number(q?.expiresAt) > 0 && Number(q.expiresAt) < 253400000000000 ? new Date(Number(q.expiresAt)).toISOString() : null;
28
- const parts = names
29
- .map(([k, name, recurring]) => ({ x: q?.[k], name, recurring }))
30
- .filter(({ x }) => x && Number(x.total) > 0)
31
- .map(({ x, name, recurring }) => ({
32
- name, recurring,
33
- total: Number(x.total) || 0,
34
- used: Number(x.used) || 0,
35
- remaining: Number(x.remaining) || 0,
36
- expiresAt,
37
- }));
38
- const sum = (k) => Math.round(parts.reduce((s, p) => s + p[k], 0) * 100) / 100;
39
- return { total: sum('total'), used: sum('used'), remaining: sum('remaining'), parts, exceeded: Boolean(q?.isQuotaExceeded) };
40
- }
41
-
42
- const PRODUCTS = {
43
- qoder: {
44
- label: 'Qoder',
45
- checkin: (account) => checkinQoder(account),
46
- quota: async (account) => normalizeQoderQuota(await fetchQuotaUsage(account)),
47
- verify: async (account) => {
48
- const ui = await fetchUserinfo(account);
49
- return { name: displayNameFrom(ui), uid: typeof ui?.id === 'string' ? ui.id : null, email: ui?.email || null };
50
- },
51
- },
52
- workbuddy: {
53
- label: 'WorkBuddy',
54
- // 国内版走 billing daily-checkin 签到;国际版没有签到接口,走「活跃领取」(免费档对话探测)
55
- checkin: (account, ctx) => (account.provider === 'workbuddy-intl' ? checkinWorkbuddyIntl(account, ctx) : checkinWorkbuddy(account, ctx)),
56
- quota: (account, ctx) => fetchWorkbuddyQuota(account, ctx),
57
- verify: async (account) => {
58
- const info = inspectToken(account.token);
59
- if (!info.provider) throw new Error('不是 WorkBuddy / CodeBuddy 的登录 token');
60
- if (info.provider !== account.provider) throw new Error(`token 属于${info.provider === 'workbuddy' ? '国内版' : '国际版'},与所选版本不符`);
61
- return { uid: info.uid, name: info.name };
62
- },
63
- },
64
- zcode: {
65
- label: 'ZCode',
66
- // 无 checkin:ZCode 积分来自活动领取(需阿里云验证码),不做后台自动签到
67
- quota: (account) => fetchZcodeQuota(account),
68
- verify: async () => {
69
- throw new Error('ZCode 账号请通过「本机导入」添加(需要客户端的完整登录快照才能查询额度与切换)');
70
- },
71
- },
72
- mirasim: {
73
- label: 'mirasim',
74
- // 无 checkin:mirasim 额度按 5h/7d 滚动窗口分配,不设签到。
75
- // token 约 1 小时过期:quota 内部 401 时自动用 refreshToken 续期,
76
- // ctx.onRefresh(refreshContext)回写账号库后,这里再把新凭据同步回 setting.json(若该账号是客户端当前登录)。
77
- quota: async (account, ctx = {}) => {
78
- const mirasimLocal = await import('./mirasimLocal.js');
79
- const wrapped = {
80
- ...ctx,
81
- onRefresh: async (creds) => {
82
- await ctx.onRefresh?.(creds);
83
- try {
84
- const synced = await mirasimLocal.writeMirasimAuth({ uid: account.uid, token: creds.token, refreshToken: creds.refreshToken, expiresAt: creds.expiresAt });
85
- if (synced) ctx.log?.('已同步新 token 到 mirasim 客户端');
86
- } catch (e) { ctx.log?.('同步到 mirasim 客户端失败:' + e.message); }
87
- },
88
- };
89
- return fetchMirasimQuota(account, wrapped);
90
- },
91
- verify: async (account) => {
92
- const p = await fetchMirasimProfile(account.token);
93
- return { name: p.name || p.email, uid: p.id, email: p.email };
94
- },
95
- },
96
- };
97
-
98
- export function productImpl(provider) {
99
- return PRODUCTS[productOf(provider)];
100
- }
1
+ /**
2
+ * 产品线注册表 — 按账号 provider 分发签到 / 积分 / 校验。
3
+ *
4
+ * 每个产品实现(checkin 可缺省:该产品不参与每日签到,如 ZCode):
5
+ * checkin(account, ctx) → { status: checked-in|already|limited|no-activity|failed, … }
6
+ * quota(account, ctx) → { total, used, remaining, exceeded, parts: [{ name, total, used, remaining, expiresAt, recurring }] }
7
+ * verify(account) → { name?, uid?, email? }(添加账号时补全资料,失败抛错)
8
+ * ctx.onRefresh(creds):产品在 token 刷新后回调,由调用方负责持久化。
9
+ */
10
+
11
+ import { productOf } from './constants.js';
12
+ import { checkinOne as checkinQoder, fetchQuotaUsage, fetchUserinfo, fetchUsageDetail } from './qoderClient.js';
13
+ import { checkinWorkbuddy, checkinWorkbuddyIntl, fetchWorkbuddyQuota, inspectToken } from './workbuddyClient.js';
14
+ import { fetchZcodeQuota } from './zcodeClient.js';
15
+ import { fetchMirasimQuota, fetchMirasimProfile } from './mirasimClient.js';
16
+ import { fetchCatpawQuota, fetchCatpawProfile } from './catpawClient.js';
17
+ import { fetchTraeQuota, checkinTrae, verifyTraeAccount } from './traeClient.js';
18
+ /** 从 userinfo 响应中挑一个可读的显示名 */
19
+ export function displayNameFrom(ui) {
20
+ const pick = [ui?.nickname, ui?.name, ui?.username, ui?.email]
21
+ .map((v) => (typeof v === 'string' ? v.trim() : ''))
22
+ .find(Boolean);
23
+ return pick || null;
24
+ }
25
+
26
+ /** Qoder /api/v2/quota/usage → 统一积分结构;detail 为网页端逐资源包明细(可选,已按 uid 校验) */
27
+ export function normalizeQoderQuota(q, detail = null) {
28
+ const names = [['userQuota', '套餐额度', true], ['addOnQuota', '附加额度', false], ['orgResourcePackage', '组织资源包', false]];
29
+ const fmtExpiry = (v) => (Number(v) > 0 && Number(v) < 253400000000000 ? new Date(Number(v)).toISOString() : null);
30
+ let parts = names
31
+ .map(([k, name, recurring]) => ({ x: q?.[k], name, recurring }))
32
+ .filter(({ x }) => x && Number(x.total) > 0)
33
+ .map(({ x, name, recurring }) => ({
34
+ name, recurring,
35
+ total: Number(x.total) || 0,
36
+ used: Number(x.used) || 0,
37
+ remaining: Number(x.remaining) || 0,
38
+ expiresAt: fmtExpiry(q?.expiresAt),
39
+ }));
40
+ // 网页端明细可用时按「每个资源包一条」展开(含各自到期时间),比聚合值更精确
41
+ const detailParts = detail ? detailQuotaParts(detail, fmtExpiry) : null;
42
+ if (detailParts) parts = detailParts;
43
+ const sum = (k) => Math.round(parts.reduce((s, p) => s + p[k], 0) * 100) / 100;
44
+ return { total: sum('total'), used: sum('used'), remaining: sum('remaining'), parts, exceeded: Boolean(q?.isQuotaExceeded) };
45
+ }
46
+
47
+ /** 网页端用量明细的分组 → 统一 parts(组内为空 / 全 0 时返回 null 让调用方回落聚合数据) */
48
+ const DETAIL_SOURCE_NAME = {
49
+ PLAN: '套餐额度',
50
+ RESOURCE_PACKAGE_SOURCE_BONUS: '获赠资源包',
51
+ RESOURCE_PACKAGE_SOURCE_PURCHASE: '购买资源包',
52
+ RESOURCE_PACKAGE_SOURCE_ORG: '组织资源包',
53
+ };
54
+ function detailQuotaParts(detail, fmtExpiry) {
55
+ const groups = [['plan_quota', '套餐额度', true], ['resource_package_quota', '资源包', false], ['dedicated_resource_package_quota', '组织资源包', false]];
56
+ const parts = [];
57
+ for (const [key, fallbackName, recurring] of groups) {
58
+ const rows = detail?.[key]?.quota_detail;
59
+ if (!Array.isArray(rows)) continue;
60
+ for (const r of rows) {
61
+ const total = Number(r?.limit_value) || 0;
62
+ if (total <= 0) continue;
63
+ parts.push({
64
+ name: DETAIL_SOURCE_NAME[r?.source] || fallbackName,
65
+ recurring,
66
+ total,
67
+ used: Number(r?.used_value) || 0,
68
+ remaining: Number(r?.remaining_value) || 0,
69
+ expiresAt: fmtExpiry(r?.expires_at),
70
+ });
71
+ }
72
+ }
73
+ return parts.length ? parts : null;
74
+ }
75
+
76
+ /**
77
+ * 网页端逐资源包明细(best-effort):无会话 Cookie 返回 null;
78
+ * 会话失效(401/403)时清除 Cookie 并回调 ctx.onWebSessionExpired 提示重新登录,同样回落。
79
+ * 明细里的 user_id 与账号 uid 不一致时丢弃(防止把别人的资源包显示到这个账号上)。
80
+ */
81
+ async function qoderWebDetail(account, ctx = {}) {
82
+ try {
83
+ const d = await fetchUsageDetail(account);
84
+ if (!d) return null;
85
+ if (account.uid && d.user_id && d.user_id !== account.uid) {
86
+ ctx.log?.('网页会话与账号不匹配,忽略资源包明细');
87
+ return null;
88
+ }
89
+ return d;
90
+ } catch (e) {
91
+ if (e.auth) {
92
+ await clearQoderWebSession(account, ctx);
93
+ ctx.log?.('Qoder 网页会话已失效,无法显示逐资源包明细(面板里重新登录一次即可)');
94
+ }
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /** 删除账号上的网页会话 Cookie(失效时),并通知桌面壳可以重新抓取 */
100
+ async function clearQoderWebSession(account, ctx = {}) {
101
+ const { withAccounts } = await import('./store.js');
102
+ await withAccounts((list) => {
103
+ const cur = list.find((a) => a.id === account.id);
104
+ if (cur?.meta?.qoderWebSession) {
105
+ const { qoderWebSession, ...meta } = cur.meta;
106
+ cur.meta = meta;
107
+ cur.updatedAt = new Date().toISOString();
108
+ if (account.meta) delete account.meta.qoderWebSession;
109
+ }
110
+ });
111
+ ctx.onWebSessionExpired?.();
112
+ }
113
+
114
+ const PRODUCTS = {
115
+ qoder: {
116
+ label: 'Qoder',
117
+ checkin: (account) => checkinQoder(account),
118
+ quota: async (account, ctx = {}) => normalizeQoderQuota(await fetchQuotaUsage(account), await qoderWebDetail(account, ctx)),
119
+ verify: async (account) => {
120
+ const ui = await fetchUserinfo(account);
121
+ return { name: displayNameFrom(ui), uid: typeof ui?.id === 'string' ? ui.id : null, email: ui?.email || null };
122
+ },
123
+ },
124
+ workbuddy: {
125
+ label: 'WorkBuddy',
126
+ // 国内版走 billing daily-checkin 签到;国际版没有签到接口,走「活跃领取」(免费档对话探测)
127
+ checkin: (account, ctx) => (account.provider === 'workbuddy-intl' ? checkinWorkbuddyIntl(account, ctx) : checkinWorkbuddy(account, ctx)),
128
+ quota: (account, ctx) => fetchWorkbuddyQuota(account, ctx),
129
+ verify: async (account) => {
130
+ const info = inspectToken(account.token);
131
+ if (!info.provider) throw new Error('不是 WorkBuddy / CodeBuddy 的登录 token');
132
+ if (info.provider !== account.provider) throw new Error(`token 属于${info.provider === 'workbuddy' ? '国内版' : '国际版'},与所选版本不符`);
133
+ return { uid: info.uid, name: info.name };
134
+ },
135
+ },
136
+ zcode: {
137
+ label: 'ZCode',
138
+ // 无 checkin:ZCode 积分来自活动领取(需阿里云验证码),不做后台自动签到
139
+ quota: (account) => fetchZcodeQuota(account),
140
+ verify: async () => {
141
+ throw new Error('ZCode 账号请通过「本机导入」添加(需要客户端的完整登录快照才能查询额度与切换)');
142
+ },
143
+ },
144
+ mirasim: {
145
+ label: 'mirasim',
146
+ // 无 checkin:mirasim 额度按 5h/7d 滚动窗口分配,不设签到。
147
+ // token 约 1 小时过期:quota 内部 401 时自动用 refreshToken 续期,
148
+ // ctx.onRefresh(refreshContext)回写账号库后,这里再把新凭据同步回 setting.json(若该账号是客户端当前登录)。
149
+ quota: async (account, ctx = {}) => {
150
+ const mirasimLocal = await import('./mirasimLocal.js');
151
+ const wrapped = {
152
+ ...ctx,
153
+ onRefresh: async (creds) => {
154
+ await ctx.onRefresh?.(creds);
155
+ try {
156
+ const synced = await mirasimLocal.writeMirasimAuth({ uid: account.uid, token: creds.token, refreshToken: creds.refreshToken, expiresAt: creds.expiresAt });
157
+ if (synced) ctx.log?.('已同步新 token 到 mirasim 客户端');
158
+ } catch (e) { ctx.log?.('同步到 mirasim 客户端失败:' + e.message); }
159
+ },
160
+ };
161
+ return fetchMirasimQuota(account, wrapped);
162
+ },
163
+ verify: async (account) => {
164
+ const p = await fetchMirasimProfile(account.token);
165
+ return { name: p.name || p.email, uid: p.id, email: p.email };
166
+ },
167
+ },
168
+ catpaw: {
169
+ label: '妙手',
170
+ // 无 checkin:妙手(美团 CatPaw)按套餐发放 Credits,无每日签到活动。
171
+ // 凭据由妙手客户端登录管理(无对外 refresh):401 时提示到客户端重新登录后重新本机导入。
172
+ quota: (account) => fetchCatpawQuota(account),
173
+ verify: async (account) => {
174
+ const p = await fetchCatpawProfile(account.token);
175
+ return { name: p.name, uid: p.userId, email: null };
176
+ },
177
+ },
178
+ trae: {
179
+ label: 'Trae',
180
+ // 每日签到发 150 通用积分(7 天有效)。凭据只来自本机 Trae 客户端的 storage.json,
181
+ // 无对外 refresh:token 约 14 天过期,失效后到 Trae 客户端重新登录再重新本机导入。
182
+ checkin: (account, ctx) => checkinTrae(account, ctx),
183
+ quota: (account) => fetchTraeQuota(account),
184
+ verify: (account) => verifyTraeAccount(account),
185
+ },
186
+ };
187
+
188
+ export function productImpl(provider) {
189
+ return PRODUCTS[productOf(provider)];
190
+ }