creditdaddy 1.3.2 → 1.4.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.
@@ -49,6 +49,44 @@ function writeJson(file, data) {
49
49
  fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
50
50
  }
51
51
 
52
+ /**
53
+ * recordKey → uid 的小缓存。
54
+ *
55
+ * 为什么需要:auth.json 的 record 只有 accessToken / refreshToken / clientId 这些,
56
+ * 稳定 uid 只能靠 profile 接口拿;profile 偶发失败时(离线 / 接口抖动),
57
+ * 「客户端当前是哪个账号」就没法判断了,缓存一份兜底。
58
+ *
59
+ * 注意:缓存**只对 auth.json 里仍然存在的 record 生效**(见 currentMiniMaxUid)。
60
+ * 客户端登出后 records 为空,此时若还回落到缓存,面板会把某个账号一直标成
61
+ * 「客户端当前」——这正是「客户端明明没登录却显示已登录」的来源,所以读的时候必须
62
+ * 与当前 record 集合求交集,不能无条件回落。
63
+ */
64
+ function readUidCache() {
65
+ return readJson(minimaxUidCachePath()) || {};
66
+ }
67
+
68
+ /** 写入 uid 缓存,并顺手丢掉 auth.json 里已经不存在的 record(防止缓存无限膨胀 / 陈旧) */
69
+ function writeUidCache(recordKey, uid) {
70
+ if (!recordKey || !uid) return;
71
+ try {
72
+ const cache = readUidCache();
73
+ if (cache[recordKey] === String(uid)) return;
74
+ cache[recordKey] = String(uid);
75
+ const live = liveRecordKeys();
76
+ if (live) for (const k of Object.keys(cache)) if (!live.has(k)) delete cache[k];
77
+ writeJson(minimaxUidCachePath(), cache);
78
+ } catch {
79
+ // 缓存写失败不影响调用方
80
+ }
81
+ }
82
+
83
+ /** auth.json 里当前仍有 accessToken 的 recordKey 集合;读不到(文件缺失/损坏)返回 null */
84
+ function liveRecordKeys() {
85
+ const authData = readJson(minimaxAuthPath());
86
+ if (!authData?.records) return null;
87
+ return new Set(Object.keys(authData.records).filter((k) => authData.records[k]?.accessToken));
88
+ }
89
+
52
90
  /**
53
91
  * 寻找 MiniMax Code.exe 安装路径
54
92
  */
@@ -115,18 +153,10 @@ export async function liveToAccount() {
115
153
  // profile 失败不影响返回基础账号信息
116
154
  }
117
155
 
118
- // auth.json 记录没有稳定 uid 字段,token 又高频轮换:
119
- // 把 profile 解析出的 uid 按 authRecordKey 缓存,供 currentMiniMaxUid 离线比对。
120
- const cached = readJson(minimaxUidCachePath()) || {};
121
- const uid = profile?.userId || record.subject || record.accountId || cached[recordKey] || null;
122
- if (profile?.userId && cached[recordKey] !== profile.userId) {
123
- cached[recordKey] = profile.userId;
124
- try {
125
- writeJson(minimaxUidCachePath(), cached);
126
- } catch {
127
- // 缓存写失败不影响本次返回
128
- }
129
- }
156
+ // auth.json 的记录没有稳定 uid 字段,token 又高频轮换,profile 偶发失败时无处可查 ——
157
+ // 用 recordKey 把 uid 记住,供 currentMiniMaxUid 离线比对(见 minimaxUidCachePath 注释)。
158
+ const uid = profile?.userId || record.subject || record.accountId || readUidCache()[recordKey] || null;
159
+ if (uid) writeUidCache(recordKey, uid);
130
160
  const name = profile?.name || (uid ? `MiniMax_${String(uid).slice(-6)}` : 'MiniMax Code');
131
161
 
132
162
  return {
@@ -139,7 +169,10 @@ export async function liveToAccount() {
139
169
  email: profile?.email || null,
140
170
  source: 'local-app',
141
171
  meta: {
142
- authRecordKey: recordKey,
172
+ // 不写 authRecordKey:它是「这条账号绑定本机客户端凭据链」的开关,一旦挂上,
173
+ // 该账号刷新前就会去 auth.json 重读 refreshToken(可能会被别的账号的链顶掉)、
174
+ // 刷新后还会回写 auth.json。本机导入/浏览器登录混用时曾因此让两个账号共用同一条链
175
+ // 互相作废,故这里只留来源标记,链的绑定交给 alignMiniMaxFromLocal 按 uid 判断。
143
176
  clientId: record.clientId,
144
177
  scopes: record.scopes,
145
178
  capturedAt: new Date().toISOString(),
@@ -149,7 +182,11 @@ export async function liveToAccount() {
149
182
 
150
183
  /**
151
184
  * 获取当前登录的 UID。
152
- * 优先用 record 自带字段;没有则按 authRecordKey 命中 uid 缓存(token 会轮换,uid 不会)。
185
+ * 优先用 record 自带字段;没有则按 recordKey 命中 uid 缓存(token 会轮换,uid 不会)。
186
+ *
187
+ * records 为空(客户端从未登录 / 已登出)时一律返回 null——绝不回落到缓存。
188
+ * 回落的代价是:客户端登出后 uid 缓存还留着,面板会继续把那个旧账号标成
189
+ * 「客户端当前」,看起来像登出没生效。
153
190
  */
154
191
  export function currentMiniMaxUid() {
155
192
  const authFile = minimaxAuthPath();
@@ -160,8 +197,7 @@ export function currentMiniMaxUid() {
160
197
  const record = authData.records[recordKey];
161
198
  const own = record?.subject || record?.accountId;
162
199
  if (own) return String(own);
163
- const cached = readJson(minimaxUidCachePath());
164
- return cached?.[recordKey] || null;
200
+ return readUidCache()[recordKey] || null;
165
201
  }
166
202
 
167
203
  /**
@@ -209,8 +245,17 @@ export function alignMiniMaxFromLocal(account, log) {
209
245
  const key = account?.meta?.authRecordKey;
210
246
  if (!key) return false;
211
247
  try {
248
+ // 归属校验:本机客户端同一时刻只登录一个账号,auth.json 里那条 record 属于**它**,
249
+ // 不一定属于调用方这个账号。若两个账号(如「本机导入」与「浏览器登录」)因为历史原因
250
+ // 挂了同一个 authRecordKey,不校验就会拿别人的链去刷新——两个账号共用一次性 refreshToken,
251
+ // 必然互相作废(invalid_grant)。uid 对不上就认定不是本账号的链,直接跳过对齐。
212
252
  const rec = readMiniMaxRecord(key);
213
253
  if (!rec?.refreshToken || rec.refreshToken === account.refreshToken) return false;
254
+ const recUid = rec.subject || rec.accountId || readUidCache()[key] || null;
255
+ if (recUid && account.uid && String(recUid) !== String(account.uid)) {
256
+ log?.('本机客户端当前登录的是另一个 MiniMax 账号,跳过凭据对齐(避免两条链互相作废)');
257
+ return false;
258
+ }
214
259
  account.refreshToken = rec.refreshToken;
215
260
  if (rec.accessToken) account.token = rec.accessToken;
216
261
  if (rec.expiresAtMs) account.expiresAt = new Date(rec.expiresAtMs).toISOString();
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { fetchJsonRace } from './zcodeClient.js';
10
10
  import { FETCH_TIMEOUT_MS } from './constants.js';
11
+ import { withRefreshDedupe } from './refreshGuard.js';
11
12
 
12
13
  const AUTH_ME_URL = 'https://auth.mirasim.ai/auth/me';
13
14
  const RELAY_LIMITS_URL = 'https://relay.mirasim.ai/v1/limits';
@@ -64,7 +65,11 @@ async function withToken(account, fn, ctx) {
64
65
  let token = account.token;
65
66
  let refreshed = false;
66
67
  const doRefresh = async () => {
67
- const creds = await refreshMirasimToken(account);
68
+ // 并发互斥 + 失效链熔断:与 minimax / workbuddy 共用 refreshGuard
69
+ const creds = await withRefreshDedupe(account, refreshMirasimToken, {
70
+ deadMessage: 'mirasim 凭据链已被服务端作废,等待重新登录',
71
+ deadNotice: 'mirasim 凭据链已失效,将不再自动重试(重新登录或重新导入该账号后自动恢复)',
72
+ });
68
73
  account.token = creds.token;
69
74
  account.refreshToken = creds.refreshToken;
70
75
  if (creds.expiresAt) account.expiresAt = creds.expiresAt;