creditdaddy 0.9.3 → 1.1.1

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