@foxden-app/foxclaw 0.5.14 → 0.5.15
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/CHANGELOG.md +12 -0
- package/dist/auth/mirror.d.ts +1 -0
- package/dist/auth/mirror.js +39 -0
- package/dist/controller/controller.d.ts +7 -0
- package/dist/controller/controller.js +228 -16
- package/dist/i18n.d.ts +32 -0
- package/dist/i18n.js +32 -0
- package/dist/main.js +2 -0
- package/dist/store/database.d.ts +4 -0
- package/dist/store/database.js +50 -4
- package/docs/user-manual.md +3 -1
- package/docs/zh/user-manual.md +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.5.15 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- `/auth` 现在会把已确认不可用、并且本机/跨节点同步恢复失败的候选标记为“需要登录修复”,用 `?` 按钮显示,并从自动轮换、主动刷新和 enabled 视图中排除。
|
|
9
|
+
- 点击 `?` 会进入修复菜单,可选择“登录修复”对该候选执行设备码登录,成功后清除修复状态并重新参与轮换;也可选择“删除”,从 canonical 和所有本机 bot runtime 中删除该候选并清理额度缓存。
|
|
10
|
+
- 删除 auth 候选现在走 auth mirror 统一删除,避免只删一个 runtime 后又被其他 runtime 或 canonical 副本恢复。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- `/auth` now marks candidates that have been proven unusable and could not be recovered through local/cross-node sync as “needs login repair”, shows a `?` action, and excludes them from auto-rotation, proactive refresh, and the enabled filter.
|
|
14
|
+
- Tapping `?` opens a repair menu: Login repair runs device-code login for that candidate and clears the repair state on success; Delete removes the candidate from canonical storage and all local bot runtimes while clearing quota cache.
|
|
15
|
+
- Auth candidate deletion now flows through the auth mirror so deleting a candidate from one runtime is not undone by another runtime or canonical copy.
|
|
16
|
+
|
|
5
17
|
## 0.5.14 - 2026-06-08
|
|
6
18
|
|
|
7
19
|
### 中文
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export declare class AuthCandidateMirror {
|
|
|
84
84
|
readRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorCandidateRecord | null>;
|
|
85
85
|
listNewestCandidates(): Promise<AuthMirrorCandidateRecord[]>;
|
|
86
86
|
syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
|
|
87
|
+
deleteCandidate(candidateName: string): Promise<boolean>;
|
|
87
88
|
syncAllRuntimeCandidates(): Promise<AuthMirrorSyncAllResult>;
|
|
88
89
|
recoverRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorRecovery | null>;
|
|
89
90
|
private scan;
|
package/dist/auth/mirror.js
CHANGED
|
@@ -126,6 +126,27 @@ export class AuthCandidateMirror {
|
|
|
126
126
|
return false;
|
|
127
127
|
return this.withActivity(() => this.propagateValidatedCandidate(runtime, candidateName));
|
|
128
128
|
}
|
|
129
|
+
async deleteCandidate(candidateName) {
|
|
130
|
+
if (!isAuthCandidateName(candidateName))
|
|
131
|
+
return false;
|
|
132
|
+
return this.withActivity(async () => {
|
|
133
|
+
const directories = [
|
|
134
|
+
this.canonicalDir,
|
|
135
|
+
...this.runtimes.map(runtime => runtime.authDir),
|
|
136
|
+
];
|
|
137
|
+
for (const authDir of directories) {
|
|
138
|
+
await removeAuthCandidate(authDir, candidateName);
|
|
139
|
+
}
|
|
140
|
+
this.lastSyncedRefresh.delete(candidateName);
|
|
141
|
+
for (const key of [...this.lastValidationFailures.keys()]) {
|
|
142
|
+
if (key.endsWith(`:${candidateName}`)) {
|
|
143
|
+
this.lastValidationFailures.delete(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.logger.info('auth.mirror.deleted', { candidateName });
|
|
147
|
+
return true;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
129
150
|
async syncAllRuntimeCandidates() {
|
|
130
151
|
return this.withActivity(async () => {
|
|
131
152
|
let synced = 0;
|
|
@@ -687,6 +708,24 @@ async function pointAuthSymlink(dir, candidateName) {
|
|
|
687
708
|
await fs.symlink(path.join(dir, candidateName), temporary);
|
|
688
709
|
await fs.rename(temporary, path.join(dir, 'auth.json'));
|
|
689
710
|
}
|
|
711
|
+
async function removeAuthCandidate(dir, candidateName) {
|
|
712
|
+
const candidatePath = path.join(dir, candidateName);
|
|
713
|
+
const authPath = path.join(dir, 'auth.json');
|
|
714
|
+
const currentTarget = await resolveCurrentAuthCandidatePath(authPath);
|
|
715
|
+
await fs.rm(candidatePath, { force: true }).catch(() => undefined);
|
|
716
|
+
if (currentTarget === candidatePath) {
|
|
717
|
+
await fs.rm(authPath, { force: true }).catch(() => undefined);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function resolveCurrentAuthCandidatePath(authPath) {
|
|
721
|
+
const stat = await fs.lstat(authPath).catch(() => null);
|
|
722
|
+
if (!stat)
|
|
723
|
+
return null;
|
|
724
|
+
if (stat.isSymbolicLink()) {
|
|
725
|
+
return resolveFinalPath(authPath);
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
690
729
|
async function removeValidationTempFiles(dir) {
|
|
691
730
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
692
731
|
await Promise.all(entries
|
|
@@ -9,6 +9,7 @@ import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
|
|
|
9
9
|
export interface CoreCoordinator {
|
|
10
10
|
canSelfUpdate?: () => boolean;
|
|
11
11
|
authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
12
|
+
authCandidateDeleted?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
12
13
|
recoverAuthCandidate?: (runtimeId: string, candidateName: string, options?: {
|
|
13
14
|
crossNode?: boolean;
|
|
14
15
|
}) => Promise<boolean>;
|
|
@@ -295,6 +296,8 @@ export declare class BridgeSessionCore {
|
|
|
295
296
|
private handleAuthToggleCommand;
|
|
296
297
|
private findLatestAuthChoiceList;
|
|
297
298
|
private handleAuthAddCommand;
|
|
299
|
+
private startAuthRepairLogin;
|
|
300
|
+
private deleteCodexAuthCandidate;
|
|
298
301
|
private handleAccountCommand;
|
|
299
302
|
private handleQuotaCommand;
|
|
300
303
|
private handleQuotaNudgeCommand;
|
|
@@ -331,6 +334,8 @@ export declare class BridgeSessionCore {
|
|
|
331
334
|
private requireReadyBinding;
|
|
332
335
|
private handleAuthPanelActionCallback;
|
|
333
336
|
private handleAuthListViewCallback;
|
|
337
|
+
private handleAuthRepairMenuCallback;
|
|
338
|
+
private handleAuthRepairActionCallback;
|
|
334
339
|
private handleAuthToggleCallback;
|
|
335
340
|
private handleAuthSwitchCallback;
|
|
336
341
|
private maybeRunPendingAuthRotation;
|
|
@@ -342,6 +347,8 @@ export declare class BridgeSessionCore {
|
|
|
342
347
|
private codexAuthSwitchParams;
|
|
343
348
|
private switchCodexAuthAndRestart;
|
|
344
349
|
private recoverCodexAuthCandidate;
|
|
350
|
+
private markCodexAuthCandidateNeedsRepair;
|
|
351
|
+
private markCodexAuthCandidateActive;
|
|
345
352
|
private syncCodexAuthCandidate;
|
|
346
353
|
private refreshAllCodexAuthCandidates;
|
|
347
354
|
private refreshCodexAuthCandidates;
|
|
@@ -1045,6 +1045,16 @@ export class BridgeSessionCore {
|
|
|
1045
1045
|
await this.handleSettingsCallback(event, settingsMatch[1], settingsMatch[2], locale);
|
|
1046
1046
|
return;
|
|
1047
1047
|
}
|
|
1048
|
+
const authRepairActionMatch = /^auth:([a-f0-9]+):repair_(login|delete|cancel):(\d+)$/.exec(event.data);
|
|
1049
|
+
if (authRepairActionMatch) {
|
|
1050
|
+
await this.handleAuthRepairActionCallback(event, authRepairActionMatch[1], authRepairActionMatch[2], Number.parseInt(authRepairActionMatch[3], 10), locale);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const authRepairMatch = /^auth:([a-f0-9]+):repair:(\d+)$/.exec(event.data);
|
|
1054
|
+
if (authRepairMatch) {
|
|
1055
|
+
await this.handleAuthRepairMenuCallback(event, authRepairMatch[1], Number.parseInt(authRepairMatch[2], 10), locale);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1048
1058
|
const authToggleMatch = /^auth:([a-f0-9]+):toggle:(\d+)$/.exec(event.data);
|
|
1049
1059
|
if (authToggleMatch) {
|
|
1050
1060
|
await this.handleAuthToggleCallback(event, authToggleMatch[1], Number.parseInt(authToggleMatch[2], 10), locale);
|
|
@@ -1657,8 +1667,11 @@ export class BridgeSessionCore {
|
|
|
1657
1667
|
if (!success) {
|
|
1658
1668
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1659
1669
|
await this.sendMessage(scopeId, [
|
|
1660
|
-
t(locale, '
|
|
1661
|
-
|
|
1670
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_failed' : 'auth_add_failed', {
|
|
1671
|
+
value: pendingAuthAdd.name,
|
|
1672
|
+
error: params?.error ?? t(locale, 'unknown'),
|
|
1673
|
+
}),
|
|
1674
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_reverted' : 'auth_add_reverted'),
|
|
1662
1675
|
].join('\n'));
|
|
1663
1676
|
return;
|
|
1664
1677
|
}
|
|
@@ -1666,12 +1679,26 @@ export class BridgeSessionCore {
|
|
|
1666
1679
|
if (!stat?.isFile()) {
|
|
1667
1680
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1668
1681
|
await this.sendMessage(scopeId, [
|
|
1669
|
-
t(locale, 'auth_add_missing_file', { value: pendingAuthAdd.name }),
|
|
1670
|
-
t(locale, 'auth_add_reverted'),
|
|
1682
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_missing_file' : 'auth_add_missing_file', { value: pendingAuthAdd.name }),
|
|
1683
|
+
t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_reverted' : 'auth_add_reverted'),
|
|
1671
1684
|
].join('\n'));
|
|
1672
1685
|
return;
|
|
1673
1686
|
}
|
|
1674
|
-
|
|
1687
|
+
if (pendingAuthAdd.mode === 'repair') {
|
|
1688
|
+
const metadata = await readChatGptAuthMetadata(pendingAuthAdd.path);
|
|
1689
|
+
if (!metadata || !chatGptAuthMetadataMatchesCandidateName(pendingAuthAdd.name, metadata)) {
|
|
1690
|
+
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
1691
|
+
await this.sendMessage(scopeId, [
|
|
1692
|
+
t(locale, 'auth_repair_identity_mismatch', { value: pendingAuthAdd.name }),
|
|
1693
|
+
t(locale, 'auth_repair_reverted'),
|
|
1694
|
+
].join('\n'));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
this.markCodexAuthCandidateActive(pendingAuthAdd.name);
|
|
1698
|
+
this.store.setCodexAuthCandidateDisabled(pendingAuthAdd.name, false);
|
|
1699
|
+
this.store.setCodexAuthCandidateDisabled(pendingAuthAdd.name, false, this.authRuntimeId());
|
|
1700
|
+
}
|
|
1701
|
+
const lines = [t(locale, pendingAuthAdd.mode === 'repair' ? 'auth_repair_done' : 'auth_add_done', { value: pendingAuthAdd.name })];
|
|
1675
1702
|
try {
|
|
1676
1703
|
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), pendingAuthAdd.name);
|
|
1677
1704
|
}
|
|
@@ -1692,6 +1719,7 @@ export class BridgeSessionCore {
|
|
|
1692
1719
|
}
|
|
1693
1720
|
const currentCandidate = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
|
|
1694
1721
|
if (currentCandidate) {
|
|
1722
|
+
this.markCodexAuthCandidateActive(currentCandidate.name);
|
|
1695
1723
|
await this.syncCodexAuthCandidate(currentCandidate.name);
|
|
1696
1724
|
}
|
|
1697
1725
|
await this.sendMessage(scopeId, t(locale, 'login_completed'));
|
|
@@ -4340,6 +4368,7 @@ export class BridgeSessionCore {
|
|
|
4340
4368
|
}
|
|
4341
4369
|
const state = await this.listCodexAuthState();
|
|
4342
4370
|
const dueCandidates = state.candidates.filter(candidate => !candidate.disabled
|
|
4371
|
+
&& candidate.state !== 'needs_repair'
|
|
4343
4372
|
&& candidate.credentialKind === 'chatgpt'
|
|
4344
4373
|
&& candidate.credentialLastRefreshMs !== null
|
|
4345
4374
|
&& candidate.credentialLastRefreshMs <= Date.now() - CODEX_AUTH_PROACTIVE_REFRESH_DAYS * 24 * 60 * 60_000);
|
|
@@ -4553,6 +4582,10 @@ export class BridgeSessionCore {
|
|
|
4553
4582
|
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null));
|
|
4554
4583
|
return;
|
|
4555
4584
|
}
|
|
4585
|
+
if (candidate.state === 'needs_repair') {
|
|
4586
|
+
await this.sendMessage(scopeId, t(locale, 'auth_candidate_needs_repair', { value: candidate.name }));
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4556
4589
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
4557
4590
|
await this.sendMessage(scopeId, t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel)));
|
|
4558
4591
|
await this.switchCodexAuthAndRestart(scopeId, locale, candidate, false);
|
|
@@ -4625,6 +4658,7 @@ export class BridgeSessionCore {
|
|
|
4625
4658
|
name: candidateName,
|
|
4626
4659
|
path: targetPath,
|
|
4627
4660
|
previousTargetPath: state.currentTargetPath,
|
|
4661
|
+
mode: 'add',
|
|
4628
4662
|
createdAt: Date.now(),
|
|
4629
4663
|
});
|
|
4630
4664
|
await this.sendMessage(scopeId, [
|
|
@@ -4641,6 +4675,85 @@ export class BridgeSessionCore {
|
|
|
4641
4675
|
throw error;
|
|
4642
4676
|
}
|
|
4643
4677
|
}
|
|
4678
|
+
async startAuthRepairLogin(scopeId, locale, candidate) {
|
|
4679
|
+
const state = await this.listCodexAuthState();
|
|
4680
|
+
const target = state.candidates.find(entry => entry.name === candidate.name) ?? null;
|
|
4681
|
+
if (!target) {
|
|
4682
|
+
await this.sendMessage(scopeId, t(locale, 'auth_choice_expired'));
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
await pointCodexAuthAtTarget(state.authDir, state.authPath, target.path);
|
|
4686
|
+
this.pendingTurnErrors.clear();
|
|
4687
|
+
this.attachedThreads.clear();
|
|
4688
|
+
try {
|
|
4689
|
+
await this.app.restart();
|
|
4690
|
+
const login = await this.app.startDeviceLogin();
|
|
4691
|
+
const oldLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
4692
|
+
if (oldLoginId) {
|
|
4693
|
+
this.pendingLoginScopesById.delete(oldLoginId);
|
|
4694
|
+
this.pendingAuthAddsByLoginId.delete(oldLoginId);
|
|
4695
|
+
}
|
|
4696
|
+
this.pendingLoginsByScope.set(scopeId, login.loginId);
|
|
4697
|
+
this.pendingLoginScopesById.set(login.loginId, scopeId);
|
|
4698
|
+
this.pendingAuthAddsByLoginId.set(login.loginId, {
|
|
4699
|
+
loginId: login.loginId,
|
|
4700
|
+
scopeId,
|
|
4701
|
+
name: target.name,
|
|
4702
|
+
path: target.path,
|
|
4703
|
+
previousTargetPath: state.currentTargetPath,
|
|
4704
|
+
mode: 'repair',
|
|
4705
|
+
createdAt: Date.now(),
|
|
4706
|
+
});
|
|
4707
|
+
await this.sendMessage(scopeId, [
|
|
4708
|
+
t(locale, 'auth_repair_started', { value: target.name }),
|
|
4709
|
+
t(locale, 'login_device_prereq'),
|
|
4710
|
+
t(locale, 'login_url', { value: login.verificationUrl }),
|
|
4711
|
+
t(locale, 'login_code', { value: login.userCode }),
|
|
4712
|
+
t(locale, 'login_id', { value: login.loginId }),
|
|
4713
|
+
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
4714
|
+
].join('\n'));
|
|
4715
|
+
}
|
|
4716
|
+
catch (error) {
|
|
4717
|
+
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
4718
|
+
throw error;
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
async deleteCodexAuthCandidate(candidate) {
|
|
4722
|
+
const wasCurrent = candidate.isCurrent;
|
|
4723
|
+
const authDir = this.resolveAuthDir();
|
|
4724
|
+
const authPath = path.join(authDir, 'auth.json');
|
|
4725
|
+
let deletedByCoordinator = false;
|
|
4726
|
+
try {
|
|
4727
|
+
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name);
|
|
4728
|
+
deletedByCoordinator = Boolean(this.coordinator?.authCandidateDeleted);
|
|
4729
|
+
}
|
|
4730
|
+
catch (error) {
|
|
4731
|
+
this.logger.warn('codex.auth_candidate_delete_sync_failed', {
|
|
4732
|
+
candidate: candidate.name,
|
|
4733
|
+
runtimeId: this.authRuntimeId(),
|
|
4734
|
+
error: toErrorMeta(error),
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
if (!deletedByCoordinator) {
|
|
4738
|
+
await fs.rm(candidate.path, { force: true }).catch(() => undefined);
|
|
4739
|
+
if (wasCurrent) {
|
|
4740
|
+
await fs.rm(authPath, { force: true }).catch(() => undefined);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
this.store.deleteCodexAuthCandidate(candidate.name);
|
|
4744
|
+
this.authRotationFailedTargets.delete(candidate.path);
|
|
4745
|
+
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4746
|
+
if (Object.prototype.hasOwnProperty.call(snapshots, candidate.name)) {
|
|
4747
|
+
delete snapshots[candidate.name];
|
|
4748
|
+
await this.writeCodexAuthQuotaSnapshots();
|
|
4749
|
+
}
|
|
4750
|
+
if (wasCurrent) {
|
|
4751
|
+
this.pendingTurnErrors.clear();
|
|
4752
|
+
this.attachedThreads.clear();
|
|
4753
|
+
await this.app.restart();
|
|
4754
|
+
}
|
|
4755
|
+
return wasCurrent;
|
|
4756
|
+
}
|
|
4644
4757
|
async handleAccountCommand(scopeId, locale) {
|
|
4645
4758
|
const account = await this.app.readAccount();
|
|
4646
4759
|
const lines = [
|
|
@@ -5126,6 +5239,74 @@ export class BridgeSessionCore {
|
|
|
5126
5239
|
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
5127
5240
|
}
|
|
5128
5241
|
}
|
|
5242
|
+
async handleAuthRepairMenuCallback(event, localId, index, locale) {
|
|
5243
|
+
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5244
|
+
if (!record) {
|
|
5245
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_expired'));
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
5248
|
+
if (record.chatId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
5249
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
5250
|
+
return;
|
|
5251
|
+
}
|
|
5252
|
+
const candidate = record.candidates[index];
|
|
5253
|
+
if (!candidate) {
|
|
5254
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
|
|
5258
|
+
if (record.messageId !== null) {
|
|
5259
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
async handleAuthRepairActionCallback(event, localId, action, index, locale) {
|
|
5263
|
+
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5264
|
+
if (!record) {
|
|
5265
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_expired'));
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
if (record.chatId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
5269
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
const candidate = record.candidates[index];
|
|
5273
|
+
if (!candidate) {
|
|
5274
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
if (action === 'cancel') {
|
|
5278
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
5279
|
+
const state = await this.listCodexAuthState();
|
|
5280
|
+
record.candidates = state.candidates;
|
|
5281
|
+
record.createdAt = Date.now();
|
|
5282
|
+
clampCodexAuthListOffset(record);
|
|
5283
|
+
if (record.messageId !== null) {
|
|
5284
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
5285
|
+
}
|
|
5286
|
+
return;
|
|
5287
|
+
}
|
|
5288
|
+
if (this.hasLocalBlockingActivity()) {
|
|
5289
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_blocked_active'));
|
|
5290
|
+
return;
|
|
5291
|
+
}
|
|
5292
|
+
if (action === 'login') {
|
|
5293
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'login_device_started'));
|
|
5294
|
+
if (record.messageId !== null) {
|
|
5295
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_login_preparing', { value: candidate.name }), []);
|
|
5296
|
+
}
|
|
5297
|
+
await this.startAuthRepairLogin(event.scopeId, locale, candidate);
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_candidate_deleted_short'));
|
|
5301
|
+
const restarted = await this.deleteCodexAuthCandidate(candidate);
|
|
5302
|
+
const state = await this.listCodexAuthState();
|
|
5303
|
+
record.candidates = state.candidates;
|
|
5304
|
+
record.createdAt = Date.now();
|
|
5305
|
+
clampCodexAuthListOffset(record);
|
|
5306
|
+
if (record.messageId !== null) {
|
|
5307
|
+
await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_candidate_deleted', { value: candidate.name })}${restarted ? `\n${t(locale, 'auth_delete_current_restarted')}` : ''}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5129
5310
|
async handleAuthToggleCallback(event, localId, index, locale) {
|
|
5130
5311
|
const record = this.pendingAuthChoiceLists.get(localId);
|
|
5131
5312
|
if (!record) {
|
|
@@ -5170,6 +5351,10 @@ export class BridgeSessionCore {
|
|
|
5170
5351
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
5171
5352
|
return;
|
|
5172
5353
|
}
|
|
5354
|
+
if (candidate.state === 'needs_repair') {
|
|
5355
|
+
await this.handleAuthRepairMenuCallback(event, localId, index, locale);
|
|
5356
|
+
return;
|
|
5357
|
+
}
|
|
5173
5358
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_recorded'));
|
|
5174
5359
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
5175
5360
|
const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
|
|
@@ -5217,6 +5402,7 @@ export class BridgeSessionCore {
|
|
|
5217
5402
|
}
|
|
5218
5403
|
return false;
|
|
5219
5404
|
}
|
|
5405
|
+
this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5220
5406
|
}
|
|
5221
5407
|
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5222
5408
|
if (!selection) {
|
|
@@ -5291,7 +5477,9 @@ export class BridgeSessionCore {
|
|
|
5291
5477
|
if (state.currentTargetPath) {
|
|
5292
5478
|
failedTargets.add(state.currentTargetPath);
|
|
5293
5479
|
}
|
|
5294
|
-
const candidates = state.candidates.filter(candidate => !candidate.disabled
|
|
5480
|
+
const candidates = state.candidates.filter(candidate => !candidate.disabled
|
|
5481
|
+
&& candidate.state !== 'needs_repair'
|
|
5482
|
+
&& !failedTargets.has(candidate.path));
|
|
5295
5483
|
if (candidates.length === 0) {
|
|
5296
5484
|
return null;
|
|
5297
5485
|
}
|
|
@@ -5300,7 +5488,10 @@ export class BridgeSessionCore {
|
|
|
5300
5488
|
: -1;
|
|
5301
5489
|
for (let offset = 1; offset <= state.candidates.length; offset += 1) {
|
|
5302
5490
|
const candidate = state.candidates[(currentIndex + offset + state.candidates.length) % state.candidates.length];
|
|
5303
|
-
if (candidate
|
|
5491
|
+
if (candidate
|
|
5492
|
+
&& !candidate.disabled
|
|
5493
|
+
&& candidate.state !== 'needs_repair'
|
|
5494
|
+
&& !failedTargets.has(candidate.path)) {
|
|
5304
5495
|
return { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) };
|
|
5305
5496
|
}
|
|
5306
5497
|
}
|
|
@@ -5308,7 +5499,7 @@ export class BridgeSessionCore {
|
|
|
5308
5499
|
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
5309
5500
|
}
|
|
5310
5501
|
async listCodexAuthState() {
|
|
5311
|
-
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
|
|
5502
|
+
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.store.listCodexAuthCandidateStates(this.authRuntimeId()), this.resolveAuthDir());
|
|
5312
5503
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
5313
5504
|
const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5314
5505
|
state.candidates.forEach((candidate) => {
|
|
@@ -5374,6 +5565,12 @@ export class BridgeSessionCore {
|
|
|
5374
5565
|
return false;
|
|
5375
5566
|
}
|
|
5376
5567
|
}
|
|
5568
|
+
markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5569
|
+
this.store.setCodexAuthCandidateState(candidateName, 'needs_repair');
|
|
5570
|
+
}
|
|
5571
|
+
markCodexAuthCandidateActive(candidateName) {
|
|
5572
|
+
this.store.setCodexAuthCandidateState(candidateName, 'active');
|
|
5573
|
+
}
|
|
5377
5574
|
async syncCodexAuthCandidate(candidateName) {
|
|
5378
5575
|
try {
|
|
5379
5576
|
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), candidateName);
|
|
@@ -5410,7 +5607,7 @@ export class BridgeSessionCore {
|
|
|
5410
5607
|
try {
|
|
5411
5608
|
for (const candidate of candidates) {
|
|
5412
5609
|
const before = await readChatGptAuthMetadata(candidate.path);
|
|
5413
|
-
if (!before || !chatGptAuthMetadataMatchesCandidateName(candidate.name, before)) {
|
|
5610
|
+
if (candidate.disabled || candidate.state === 'needs_repair' || !before || !chatGptAuthMetadataMatchesCandidateName(candidate.name, before)) {
|
|
5414
5611
|
result.skipped.push(candidate.name);
|
|
5415
5612
|
continue;
|
|
5416
5613
|
}
|
|
@@ -5655,6 +5852,9 @@ export class BridgeSessionCore {
|
|
|
5655
5852
|
if (!candidate) {
|
|
5656
5853
|
return;
|
|
5657
5854
|
}
|
|
5855
|
+
if (candidate.state === 'needs_repair') {
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5658
5858
|
try {
|
|
5659
5859
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5660
5860
|
if (!metadata || !chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)) {
|
|
@@ -8234,7 +8434,7 @@ function parseActiveTurnKey(key) {
|
|
|
8234
8434
|
function codexAuthDir(explicitAuthDir = null) {
|
|
8235
8435
|
return explicitAuthDir || process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
|
|
8236
8436
|
}
|
|
8237
|
-
async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = null) {
|
|
8437
|
+
async function listCodexAuthState(disabledNames = new Set(), candidateStates = new Map(), explicitAuthDir = null) {
|
|
8238
8438
|
const authDir = codexAuthDir(explicitAuthDir);
|
|
8239
8439
|
const authPath = path.join(authDir, 'auth.json');
|
|
8240
8440
|
const currentTargetPath = await resolveCurrentAuthTarget(authDir, authPath);
|
|
@@ -8259,6 +8459,7 @@ async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = n
|
|
|
8259
8459
|
path: candidatePath,
|
|
8260
8460
|
isCurrent: currentTargetPath === candidatePath,
|
|
8261
8461
|
disabled: disabledNames.has(entry.name),
|
|
8462
|
+
state: candidateStates.get(entry.name) ?? 'active',
|
|
8262
8463
|
mtimeMs: stat.mtimeMs,
|
|
8263
8464
|
credentialKind: 'invalid',
|
|
8264
8465
|
credentialLastRefreshMs: null,
|
|
@@ -8370,7 +8571,7 @@ async function restoreCodexAuthTarget(authDir, authPath, targetPath, regularAuth
|
|
|
8370
8571
|
});
|
|
8371
8572
|
}
|
|
8372
8573
|
async function switchCodexAuth(targetPath, explicitAuthDir = null) {
|
|
8373
|
-
const state = await listCodexAuthState(new Set(), explicitAuthDir);
|
|
8574
|
+
const state = await listCodexAuthState(new Set(), new Map(), explicitAuthDir);
|
|
8374
8575
|
const candidate = state.candidates.find(entry => entry.path === targetPath);
|
|
8375
8576
|
if (!candidate) {
|
|
8376
8577
|
throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
|
|
@@ -8446,7 +8647,7 @@ function filterCodexAuthCandidates(candidates, view) {
|
|
|
8446
8647
|
.map((candidate, index) => ({ candidate, index }))
|
|
8447
8648
|
.filter(({ candidate }) => !searchTerm || candidate.name.toLowerCase().includes(searchTerm))
|
|
8448
8649
|
.filter(({ candidate }) => view.filter === 'all'
|
|
8449
|
-
|| (view.filter === 'enabled' && !candidate.disabled)
|
|
8650
|
+
|| (view.filter === 'enabled' && !candidate.disabled && candidate.state !== 'needs_repair')
|
|
8450
8651
|
|| (view.filter === 'attention' && codexAuthCandidateNeedsAttention(candidate)));
|
|
8451
8652
|
}
|
|
8452
8653
|
function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopyPaste = false, view = null) {
|
|
@@ -8505,12 +8706,12 @@ function authChoiceKeyboard(locale, record) {
|
|
|
8505
8706
|
const page = codexAuthListPage(record.candidates, record);
|
|
8506
8707
|
const rows = page.visible.map(({ candidate, index }) => [
|
|
8507
8708
|
{
|
|
8508
|
-
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
|
|
8509
|
-
callback_data: `auth:${record.localId}:${index}`,
|
|
8709
|
+
text: clipButtonText(`${candidate.state === 'needs_repair' ? '? ' : candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
|
|
8710
|
+
callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:${index}`,
|
|
8510
8711
|
},
|
|
8511
8712
|
{
|
|
8512
|
-
text: t(locale, candidate.disabled ? 'button_auth_disable' : 'button_auth_enable'),
|
|
8513
|
-
callback_data: `auth:${record.localId}:toggle:${index}`,
|
|
8713
|
+
text: candidate.state === 'needs_repair' ? '?' : t(locale, candidate.disabled ? 'button_auth_disable' : 'button_auth_enable'),
|
|
8714
|
+
callback_data: candidate.state === 'needs_repair' ? `auth:${record.localId}:repair:${index}` : `auth:${record.localId}:toggle:${index}`,
|
|
8514
8715
|
},
|
|
8515
8716
|
]);
|
|
8516
8717
|
const navigationRow = [];
|
|
@@ -8555,6 +8756,9 @@ function authFilterButton(locale, record, filter) {
|
|
|
8555
8756
|
};
|
|
8556
8757
|
}
|
|
8557
8758
|
function codexAuthCandidateHealth(candidate) {
|
|
8759
|
+
if (candidate.state === 'needs_repair') {
|
|
8760
|
+
return 'needs_repair';
|
|
8761
|
+
}
|
|
8558
8762
|
if (candidate.disabled) {
|
|
8559
8763
|
return 'disabled';
|
|
8560
8764
|
}
|
|
@@ -8589,6 +8793,7 @@ function codexAuthCandidateNeedsAttention(candidate) {
|
|
|
8589
8793
|
|| health === 'unknown'
|
|
8590
8794
|
|| health === 'low'
|
|
8591
8795
|
|| health === 'exhausted'
|
|
8796
|
+
|| health === 'needs_repair'
|
|
8592
8797
|
|| health === 'invalid';
|
|
8593
8798
|
}
|
|
8594
8799
|
function formatCodexAuthCandidateStatus(locale, candidate) {
|
|
@@ -8613,6 +8818,13 @@ function authRefreshAllConfirmKeyboard(locale, record) {
|
|
|
8613
8818
|
[{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:refresh_all_cancel` }],
|
|
8614
8819
|
];
|
|
8615
8820
|
}
|
|
8821
|
+
function authRepairKeyboard(locale, record, index) {
|
|
8822
|
+
return [
|
|
8823
|
+
[{ text: t(locale, 'button_auth_repair_login'), callback_data: `auth:${record.localId}:repair_login:${index}` }],
|
|
8824
|
+
[{ text: t(locale, 'button_auth_delete'), callback_data: `auth:${record.localId}:repair_delete:${index}` }],
|
|
8825
|
+
[{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:repair_cancel:${index}` }],
|
|
8826
|
+
];
|
|
8827
|
+
}
|
|
8616
8828
|
function formatAuthRefreshAllResult(locale, result, mode = 'manual') {
|
|
8617
8829
|
const lines = [t(locale, mode === 'proactive' ? 'auth_proactive_refresh_done' : 'auth_refresh_all_done', {
|
|
8618
8830
|
refreshed: String(result.refreshed.length),
|
package/dist/i18n.d.ts
CHANGED
|
@@ -162,6 +162,7 @@ declare const MESSAGES: {
|
|
|
162
162
|
readonly auth_candidate_status_enabled: "enabled";
|
|
163
163
|
readonly auth_candidate_status_disabled: "disabled";
|
|
164
164
|
readonly auth_candidate_health_disabled: "disabled";
|
|
165
|
+
readonly auth_candidate_health_needs_repair: "needs login repair";
|
|
165
166
|
readonly auth_candidate_health_ready: "ready";
|
|
166
167
|
readonly auth_candidate_health_low: "low quota";
|
|
167
168
|
readonly auth_candidate_health_exhausted: "quota exhausted";
|
|
@@ -174,6 +175,19 @@ declare const MESSAGES: {
|
|
|
174
175
|
readonly auth_candidate_disabled: "Disabled auth candidate for auto-rotation: {value}";
|
|
175
176
|
readonly auth_candidate_enabled_short: "Auth enabled";
|
|
176
177
|
readonly auth_candidate_disabled_short: "Auth disabled";
|
|
178
|
+
readonly auth_candidate_needs_repair: "{value} needs login repair. Open /auth and use the ? action to repair or delete it.";
|
|
179
|
+
readonly auth_repair_actions_short: "Repair actions";
|
|
180
|
+
readonly auth_repair_actions_message: "{value} has been verified unusable and could not be recovered from sync. Choose an action.";
|
|
181
|
+
readonly auth_repair_login_preparing: "Preparing device login repair for {value}...";
|
|
182
|
+
readonly auth_repair_started: "Device login repair started for {value}.";
|
|
183
|
+
readonly auth_repair_done: "Auth candidate repaired: {value}";
|
|
184
|
+
readonly auth_repair_failed: "Auth repair failed for {value}: {error}";
|
|
185
|
+
readonly auth_repair_missing_file: "Auth repair finished but {value} was not written.";
|
|
186
|
+
readonly auth_repair_identity_mismatch: "Auth repair finished, but the logged-in account does not match {value}.";
|
|
187
|
+
readonly auth_repair_reverted: "Restored previous auth. The candidate still needs repair.";
|
|
188
|
+
readonly auth_candidate_deleted: "Deleted auth candidate: {value}";
|
|
189
|
+
readonly auth_candidate_deleted_short: "Auth deleted";
|
|
190
|
+
readonly auth_delete_current_restarted: "The deleted candidate was current, so Codex app-server was restarted without it.";
|
|
177
191
|
readonly auth_no_candidates: "No auth candidates found. Expected files like auth.json_personal in the auth dir.";
|
|
178
192
|
readonly auth_choice_expired: "This auth list is no longer active";
|
|
179
193
|
readonly auth_choice_mismatch: "Auth choice does not match this message";
|
|
@@ -239,6 +253,8 @@ declare const MESSAGES: {
|
|
|
239
253
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
240
254
|
readonly button_auth_safe_sync: "🧷 Safe sync";
|
|
241
255
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
256
|
+
readonly button_auth_repair_login: "🔑 Login repair";
|
|
257
|
+
readonly button_auth_delete: "🗑️ Delete";
|
|
242
258
|
readonly button_auth_enable: "✅";
|
|
243
259
|
readonly button_auth_disable: "⏸️";
|
|
244
260
|
readonly button_auth_filter_all: "All";
|
|
@@ -818,6 +834,7 @@ declare const MESSAGES: {
|
|
|
818
834
|
readonly auth_candidate_status_enabled: "启用";
|
|
819
835
|
readonly auth_candidate_status_disabled: "禁用";
|
|
820
836
|
readonly auth_candidate_health_disabled: "已禁用";
|
|
837
|
+
readonly auth_candidate_health_needs_repair: "需要登录修复";
|
|
821
838
|
readonly auth_candidate_health_ready: "正常";
|
|
822
839
|
readonly auth_candidate_health_low: "额度偏低";
|
|
823
840
|
readonly auth_candidate_health_exhausted: "额度耗尽";
|
|
@@ -830,6 +847,19 @@ declare const MESSAGES: {
|
|
|
830
847
|
readonly auth_candidate_disabled: "已禁用 auth 候选自动轮换:{value}";
|
|
831
848
|
readonly auth_candidate_enabled_short: "auth 已启用";
|
|
832
849
|
readonly auth_candidate_disabled_short: "auth 已禁用";
|
|
850
|
+
readonly auth_candidate_needs_repair: "{value} 需要登录修复。打开 /auth 后使用 ? 操作修复或删除。";
|
|
851
|
+
readonly auth_repair_actions_short: "修复操作";
|
|
852
|
+
readonly auth_repair_actions_message: "{value} 已确认不可用,并且同步恢复失败。请选择处理方式。";
|
|
853
|
+
readonly auth_repair_login_preparing: "正在准备为 {value} 进行设备登录修复...";
|
|
854
|
+
readonly auth_repair_started: "已开始为 {value} 进行设备登录修复。";
|
|
855
|
+
readonly auth_repair_done: "auth 候选已修复:{value}";
|
|
856
|
+
readonly auth_repair_failed: "{value} 修复失败:{error}";
|
|
857
|
+
readonly auth_repair_missing_file: "修复流程已结束,但没有写入 {value}。";
|
|
858
|
+
readonly auth_repair_identity_mismatch: "修复流程已结束,但登录账号与 {value} 不匹配。";
|
|
859
|
+
readonly auth_repair_reverted: "已恢复之前的 auth。该候选仍需要修复。";
|
|
860
|
+
readonly auth_candidate_deleted: "已删除 auth 候选:{value}";
|
|
861
|
+
readonly auth_candidate_deleted_short: "auth 已删除";
|
|
862
|
+
readonly auth_delete_current_restarted: "被删除的候选是当前 auth,已在删除后重启 Codex app-server。";
|
|
833
863
|
readonly auth_no_candidates: "没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。";
|
|
834
864
|
readonly auth_choice_expired: "这个 auth 列表已经不再有效";
|
|
835
865
|
readonly auth_choice_mismatch: "这个 auth 选择按钮不属于当前消息";
|
|
@@ -895,6 +925,8 @@ declare const MESSAGES: {
|
|
|
895
925
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
896
926
|
readonly button_auth_safe_sync: "🧷 安全同步";
|
|
897
927
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
928
|
+
readonly button_auth_repair_login: "🔑 登录修复";
|
|
929
|
+
readonly button_auth_delete: "🗑️ 删除";
|
|
898
930
|
readonly button_auth_enable: "✅";
|
|
899
931
|
readonly button_auth_disable: "⏸️";
|
|
900
932
|
readonly button_auth_filter_all: "全部";
|
package/dist/i18n.js
CHANGED
|
@@ -160,6 +160,7 @@ const MESSAGES = {
|
|
|
160
160
|
auth_candidate_status_enabled: 'enabled',
|
|
161
161
|
auth_candidate_status_disabled: 'disabled',
|
|
162
162
|
auth_candidate_health_disabled: 'disabled',
|
|
163
|
+
auth_candidate_health_needs_repair: 'needs login repair',
|
|
163
164
|
auth_candidate_health_ready: 'ready',
|
|
164
165
|
auth_candidate_health_low: 'low quota',
|
|
165
166
|
auth_candidate_health_exhausted: 'quota exhausted',
|
|
@@ -172,6 +173,19 @@ const MESSAGES = {
|
|
|
172
173
|
auth_candidate_disabled: 'Disabled auth candidate for auto-rotation: {value}',
|
|
173
174
|
auth_candidate_enabled_short: 'Auth enabled',
|
|
174
175
|
auth_candidate_disabled_short: 'Auth disabled',
|
|
176
|
+
auth_candidate_needs_repair: '{value} needs login repair. Open /auth and use the ? action to repair or delete it.',
|
|
177
|
+
auth_repair_actions_short: 'Repair actions',
|
|
178
|
+
auth_repair_actions_message: '{value} has been verified unusable and could not be recovered from sync. Choose an action.',
|
|
179
|
+
auth_repair_login_preparing: 'Preparing device login repair for {value}...',
|
|
180
|
+
auth_repair_started: 'Device login repair started for {value}.',
|
|
181
|
+
auth_repair_done: 'Auth candidate repaired: {value}',
|
|
182
|
+
auth_repair_failed: 'Auth repair failed for {value}: {error}',
|
|
183
|
+
auth_repair_missing_file: 'Auth repair finished but {value} was not written.',
|
|
184
|
+
auth_repair_identity_mismatch: 'Auth repair finished, but the logged-in account does not match {value}.',
|
|
185
|
+
auth_repair_reverted: 'Restored previous auth. The candidate still needs repair.',
|
|
186
|
+
auth_candidate_deleted: 'Deleted auth candidate: {value}',
|
|
187
|
+
auth_candidate_deleted_short: 'Auth deleted',
|
|
188
|
+
auth_delete_current_restarted: 'The deleted candidate was current, so Codex app-server was restarted without it.',
|
|
175
189
|
auth_no_candidates: 'No auth candidates found. Expected files like auth.json_personal in the auth dir.',
|
|
176
190
|
auth_choice_expired: 'This auth list is no longer active',
|
|
177
191
|
auth_choice_mismatch: 'Auth choice does not match this message',
|
|
@@ -237,6 +251,8 @@ const MESSAGES = {
|
|
|
237
251
|
button_auth_reload: '🔄 Reload auth',
|
|
238
252
|
button_auth_safe_sync: '🧷 Safe sync',
|
|
239
253
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
254
|
+
button_auth_repair_login: '🔑 Login repair',
|
|
255
|
+
button_auth_delete: '🗑️ Delete',
|
|
240
256
|
button_auth_enable: '✅',
|
|
241
257
|
button_auth_disable: '⏸️',
|
|
242
258
|
button_auth_filter_all: 'All',
|
|
@@ -816,6 +832,7 @@ const MESSAGES = {
|
|
|
816
832
|
auth_candidate_status_enabled: '启用',
|
|
817
833
|
auth_candidate_status_disabled: '禁用',
|
|
818
834
|
auth_candidate_health_disabled: '已禁用',
|
|
835
|
+
auth_candidate_health_needs_repair: '需要登录修复',
|
|
819
836
|
auth_candidate_health_ready: '正常',
|
|
820
837
|
auth_candidate_health_low: '额度偏低',
|
|
821
838
|
auth_candidate_health_exhausted: '额度耗尽',
|
|
@@ -828,6 +845,19 @@ const MESSAGES = {
|
|
|
828
845
|
auth_candidate_disabled: '已禁用 auth 候选自动轮换:{value}',
|
|
829
846
|
auth_candidate_enabled_short: 'auth 已启用',
|
|
830
847
|
auth_candidate_disabled_short: 'auth 已禁用',
|
|
848
|
+
auth_candidate_needs_repair: '{value} 需要登录修复。打开 /auth 后使用 ? 操作修复或删除。',
|
|
849
|
+
auth_repair_actions_short: '修复操作',
|
|
850
|
+
auth_repair_actions_message: '{value} 已确认不可用,并且同步恢复失败。请选择处理方式。',
|
|
851
|
+
auth_repair_login_preparing: '正在准备为 {value} 进行设备登录修复...',
|
|
852
|
+
auth_repair_started: '已开始为 {value} 进行设备登录修复。',
|
|
853
|
+
auth_repair_done: 'auth 候选已修复:{value}',
|
|
854
|
+
auth_repair_failed: '{value} 修复失败:{error}',
|
|
855
|
+
auth_repair_missing_file: '修复流程已结束,但没有写入 {value}。',
|
|
856
|
+
auth_repair_identity_mismatch: '修复流程已结束,但登录账号与 {value} 不匹配。',
|
|
857
|
+
auth_repair_reverted: '已恢复之前的 auth。该候选仍需要修复。',
|
|
858
|
+
auth_candidate_deleted: '已删除 auth 候选:{value}',
|
|
859
|
+
auth_candidate_deleted_short: 'auth 已删除',
|
|
860
|
+
auth_delete_current_restarted: '被删除的候选是当前 auth,已在删除后重启 Codex app-server。',
|
|
831
861
|
auth_no_candidates: '没有找到 auth 候选文件。请在 auth 目录中放置类似 auth.json_personal 的文件。',
|
|
832
862
|
auth_choice_expired: '这个 auth 列表已经不再有效',
|
|
833
863
|
auth_choice_mismatch: '这个 auth 选择按钮不属于当前消息',
|
|
@@ -893,6 +923,8 @@ const MESSAGES = {
|
|
|
893
923
|
button_auth_reload: '🔄 重载 auth',
|
|
894
924
|
button_auth_safe_sync: '🧷 安全同步',
|
|
895
925
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
926
|
+
button_auth_repair_login: '🔑 登录修复',
|
|
927
|
+
button_auth_delete: '🗑️ 删除',
|
|
896
928
|
button_auth_enable: '✅',
|
|
897
929
|
button_auth_disable: '⏸️',
|
|
898
930
|
button_auth_filter_all: '全部',
|
package/dist/main.js
CHANGED
|
@@ -324,6 +324,7 @@ async function runServeCli() {
|
|
|
324
324
|
canSelfUpdate: () => authSyncLocalIdle()
|
|
325
325
|
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
326
326
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
327
|
+
authCandidateDeleted: (_runtimeId, candidateName) => mirror.deleteCandidate(candidateName).then(() => undefined),
|
|
327
328
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
328
329
|
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
329
330
|
if (local)
|
|
@@ -492,6 +493,7 @@ async function runServeCli() {
|
|
|
492
493
|
canSelfUpdate: () => singleAuthSyncLocalIdle()
|
|
493
494
|
&& (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
|
|
494
495
|
authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
496
|
+
authCandidateDeleted: (_runtimeId, candidateName) => singleMirror?.deleteCandidate(candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
495
497
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
496
498
|
const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
|
|
497
499
|
if (local)
|
package/dist/store/database.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface CodexAuthQuotaSnapshotRecord {
|
|
|
37
37
|
secondaryRemainingPercent: number | null;
|
|
38
38
|
updatedAt: number;
|
|
39
39
|
}
|
|
40
|
+
export type CodexAuthCandidateState = 'active' | 'needs_repair';
|
|
40
41
|
export declare class BridgeStore {
|
|
41
42
|
private db;
|
|
42
43
|
constructor(dbPath: string);
|
|
@@ -117,7 +118,10 @@ export declare class BridgeStore {
|
|
|
117
118
|
getWeixinContextToken(scopeId: string): string | null;
|
|
118
119
|
setWeixinContextToken(scopeId: string, contextToken: string): void;
|
|
119
120
|
listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
|
|
121
|
+
listCodexAuthCandidateStates(runtimeId?: string): Map<string, CodexAuthCandidateState>;
|
|
120
122
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
123
|
+
setCodexAuthCandidateState(name: string, state: CodexAuthCandidateState, runtimeId?: string): void;
|
|
124
|
+
deleteCodexAuthCandidate(name: string): void;
|
|
121
125
|
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
122
126
|
listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
123
127
|
private ensureColumn;
|
package/dist/store/database.js
CHANGED
|
@@ -169,12 +169,14 @@ export class BridgeStore {
|
|
|
169
169
|
CREATE TABLE IF NOT EXISTS codex_auth_candidates (
|
|
170
170
|
name TEXT PRIMARY KEY,
|
|
171
171
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
172
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
172
173
|
updated_at INTEGER NOT NULL
|
|
173
174
|
);
|
|
174
175
|
CREATE TABLE IF NOT EXISTS codex_auth_candidate_runtime (
|
|
175
176
|
runtime_id TEXT NOT NULL,
|
|
176
177
|
name TEXT NOT NULL,
|
|
177
178
|
disabled INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
178
180
|
updated_at INTEGER NOT NULL,
|
|
179
181
|
PRIMARY KEY (runtime_id, name)
|
|
180
182
|
);
|
|
@@ -207,6 +209,8 @@ export class BridgeStore {
|
|
|
207
209
|
this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
|
|
208
210
|
this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
|
|
209
211
|
this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
|
|
212
|
+
this.ensureColumn('codex_auth_candidates', 'state', "TEXT NOT NULL DEFAULT 'active'");
|
|
213
|
+
this.ensureColumn('codex_auth_candidate_runtime', 'state', "TEXT NOT NULL DEFAULT 'active'");
|
|
210
214
|
this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
|
|
211
215
|
this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
|
|
212
216
|
this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
|
|
@@ -835,21 +839,60 @@ export class BridgeStore {
|
|
|
835
839
|
const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
|
|
836
840
|
return new Set(rows.map(row => String(row.name)));
|
|
837
841
|
}
|
|
842
|
+
listCodexAuthCandidateStates(runtimeId = 'default') {
|
|
843
|
+
const states = new Map();
|
|
844
|
+
const globalRows = this.db.prepare('SELECT name, state FROM codex_auth_candidates').all();
|
|
845
|
+
for (const row of globalRows) {
|
|
846
|
+
states.set(String(row.name), normalizeCodexAuthCandidateState(row.state));
|
|
847
|
+
}
|
|
848
|
+
if (runtimeId !== 'default') {
|
|
849
|
+
const runtimeRows = this.db.prepare('SELECT name, state FROM codex_auth_candidate_runtime WHERE runtime_id = ?').all(runtimeId);
|
|
850
|
+
for (const row of runtimeRows) {
|
|
851
|
+
states.set(String(row.name), normalizeCodexAuthCandidateState(row.state));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return states;
|
|
855
|
+
}
|
|
838
856
|
setCodexAuthCandidateDisabled(name, disabled, runtimeId = 'default') {
|
|
839
857
|
if (runtimeId !== 'default') {
|
|
840
858
|
this.db.prepare(`
|
|
841
|
-
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, updated_at)
|
|
842
|
-
VALUES (?, ?, ?, ?)
|
|
859
|
+
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, state, updated_at)
|
|
860
|
+
VALUES (?, ?, ?, 'active', ?)
|
|
843
861
|
ON CONFLICT(runtime_id, name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
844
862
|
`).run(runtimeId, name, disabled ? 1 : 0, Date.now());
|
|
845
863
|
return;
|
|
846
864
|
}
|
|
847
865
|
this.db.prepare(`
|
|
848
|
-
INSERT INTO codex_auth_candidates (name, disabled, updated_at)
|
|
849
|
-
VALUES (?, ?, ?)
|
|
866
|
+
INSERT INTO codex_auth_candidates (name, disabled, state, updated_at)
|
|
867
|
+
VALUES (?, ?, 'active', ?)
|
|
850
868
|
ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
851
869
|
`).run(name, disabled ? 1 : 0, Date.now());
|
|
852
870
|
}
|
|
871
|
+
setCodexAuthCandidateState(name, state, runtimeId = 'default') {
|
|
872
|
+
if (runtimeId !== 'default') {
|
|
873
|
+
this.db.prepare(`
|
|
874
|
+
INSERT INTO codex_auth_candidate_runtime (runtime_id, name, disabled, state, updated_at)
|
|
875
|
+
VALUES (?, ?, 0, ?, ?)
|
|
876
|
+
ON CONFLICT(runtime_id, name) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at
|
|
877
|
+
`).run(runtimeId, name, state, Date.now());
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
this.db.prepare(`
|
|
881
|
+
INSERT INTO codex_auth_candidates (name, disabled, state, updated_at)
|
|
882
|
+
VALUES (?, 0, ?, ?)
|
|
883
|
+
ON CONFLICT(name) DO UPDATE SET state = excluded.state, updated_at = excluded.updated_at
|
|
884
|
+
`).run(name, state, Date.now());
|
|
885
|
+
this.db.prepare(`
|
|
886
|
+
UPDATE codex_auth_candidate_runtime
|
|
887
|
+
SET state = ?, updated_at = ?
|
|
888
|
+
WHERE name = ?
|
|
889
|
+
`).run(state, Date.now(), name);
|
|
890
|
+
}
|
|
891
|
+
deleteCodexAuthCandidate(name) {
|
|
892
|
+
this.db.prepare('DELETE FROM codex_auth_candidates WHERE name = ?').run(name);
|
|
893
|
+
this.db.prepare('DELETE FROM codex_auth_candidate_runtime WHERE name = ?').run(name);
|
|
894
|
+
this.db.prepare('DELETE FROM codex_auth_quota_snapshots WHERE candidate_name = ?').run(name);
|
|
895
|
+
}
|
|
853
896
|
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, quotaIdentityId, snapshot) {
|
|
854
897
|
this.db.prepare(`
|
|
855
898
|
INSERT INTO codex_auth_quota_snapshots (
|
|
@@ -932,6 +975,9 @@ function nullableNumber(value) {
|
|
|
932
975
|
function nullableString(value) {
|
|
933
976
|
return typeof value === 'string' && value.trim() ? value : null;
|
|
934
977
|
}
|
|
978
|
+
function normalizeCodexAuthCandidateState(value) {
|
|
979
|
+
return value === 'needs_repair' ? 'needs_repair' : 'active';
|
|
980
|
+
}
|
|
935
981
|
function normalizeCollaborationMode(value) {
|
|
936
982
|
return value === 'default' || value === 'plan' ? value : null;
|
|
937
983
|
}
|
package/docs/user-manual.md
CHANGED
|
@@ -415,7 +415,9 @@ Quota remaining: window:percent|auth
|
|
|
415
415
|
[🔄 Reload auth]
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
-
The right-side `✅` / `⏸️` button controls whether the candidate participates in auto-rotation. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `--` means no quota snapshot has been observed for that candidate yet. Health summaries distinguish ready, low quota, quota exhausted, quota unknown, not recently refreshed, API key,
|
|
418
|
+
The right-side `✅` / `⏸️` button controls whether the candidate participates in auto-rotation. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. Tapping a candidate switches auth, restarts that runtime, and refreshes the same panel with its buttons intact so you can switch again immediately. `--` means no quota snapshot has been observed for that candidate yet. Health summaries distinguish ready, low quota, quota exhausted, quota unknown, not recently refreshed, API key, invalid auth file, and needs login repair states.
|
|
419
|
+
|
|
420
|
+
When an auth candidate has already failed while in use and FoxClaw cannot recover a newer same-account credential from local mirror or cross-node sync, it is marked as `needs login repair`. These candidates are skipped by auto-rotation and proactive refresh, and are hidden from the `Enabled` filter. Their row shows a `?` action. Tapping it opens two choices: Login repair starts device-code login with that candidate selected; Delete removes the candidate from canonical storage and all local bot runtimes, and clears cached quota for it.
|
|
419
421
|
|
|
420
422
|
`/auth refresh all` is a command-only maintenance action because ChatGPT refresh tokens are rotated. It is allowed only when every Telegram runtime, the Weixin runtime, approvals, inputs, logins, and auth mirroring are idle. The command first shows a risk confirmation: if OpenAI/Codex consumes an old refresh token but the new token cannot be saved because of network, process, or disk failure, that candidate may require device login or phone verification again. After confirmation, FoxClaw visits every ChatGPT candidate, asks Codex to force-refresh tokens with `account/read refreshToken=true`, verifies the result through the usage endpoint, mirrors successful candidates, restores the original current auth, and shows a summary.
|
|
421
423
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -415,7 +415,9 @@ Candidates: 2
|
|
|
415
415
|
[🔄 Reload auth]
|
|
416
416
|
```
|
|
417
417
|
|
|
418
|
-
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key
|
|
418
|
+
右侧 `✅` / `⏸️` 表示当前是否参与自动轮转。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--` 表示该候选还没有额度历史快照。健康摘要会区分正常、额度偏低、额度耗尽、额度未知、长期未刷新、API key、无效 auth 文件和需要登录修复。
|
|
419
|
+
|
|
420
|
+
当某个候选在实际使用中已经失败,并且 FoxClaw 无法从本机 mirror 或跨节点同步恢复同账号较新凭据时,会标为“需要登录修复”。这类候选不会参与自动轮转和后台主动刷新,也不会出现在“已启用”筛选里。它的按钮会显示 `?`。点击后有两个选择:`登录修复` 会在选中该候选的状态下启动设备码登录;`删除` 会从 canonical 和所有本机 bot runtime 中删除这个候选,并清理它的额度缓存。
|
|
419
421
|
|
|
420
422
|
`/auth refresh all` 是仅命令入口的维护操作,因为 ChatGPT refresh token 会被轮换。只有所有 Telegram runtime、微信 runtime、审批、待输入、登录流程和 auth 镜像写入都空闲时才允许执行。命令会先显示风险确认:如果 OpenAI/Codex 已经消费旧 refresh token,但因为网络、进程或磁盘故障导致新 token 没能成功保存,该候选可能需要重新设备登录,甚至重新手机号验证。确认后,它会逐个访问 ChatGPT 候选,让 Codex 通过 `account/read refreshToken=true` 强制刷新 token,再用 usage 接口验证,成功后镜像到其他 bot home,最后恢复原本的当前 auth 并显示摘要。
|
|
421
423
|
|