@foxden-app/foxclaw 0.4.3 → 0.4.5
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/README.md +1 -1
- package/README_EN.md +1 -1
- package/dist/auth/mirror.d.ts +9 -0
- package/dist/auth/mirror.js +63 -5
- package/dist/controller/controller.d.ts +3 -0
- package/dist/controller/controller.js +56 -3
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +2 -0
- package/dist/main.js +4 -4
- package/docs/user-manual.md +2 -2
- package/docs/zh/user-manual.md +2 -2
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ TG_BOT_TOKENS=123456:token_a,234567:token_b
|
|
|
206
206
|
TG_BOT_TOKEN=123456:token_a
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw
|
|
209
|
+
FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw 在登录或刷新在线验证后镜像同步;切换或重载前还会从其他 Codex home 恢复同账号较新凭据。各 bot 的当前选择互不影响。每个 bot 首次私聊发送 `/help` 和 `/status`;`/auth` 会标明正在操作的 bot runtime,`/status` 会列出全部 bot 的连接、runtime 类型、当前 auth 和活动 turn 摘要。
|
|
210
210
|
|
|
211
211
|
如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
|
|
212
212
|
|
package/README_EN.md
CHANGED
|
@@ -206,7 +206,7 @@ TG_BOT_TOKENS=123456:token_a,234567:token_b
|
|
|
206
206
|
TG_BOT_TOKEN=123456:token_a
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
FoxClaw remains one system service. By default, it starts an independent `codex app-server` and `CODEX_HOME` for each bot. While bot A is running a turn, bot B can switch its own `/auth` selection. Candidate credentials are mirrored only after validated login or refresh;
|
|
209
|
+
FoxClaw remains one system service. By default, it starts an independent `codex app-server` and `CODEX_HOME` for each bot. While bot A is running a turn, bot B can switch its own `/auth` selection. Candidate credentials are mirrored only after online-validated login or refresh; before switching or reloading, FoxClaw also restores a newer same-account credential from another Codex home when available. Current selections remain independent. Send `/help` and `/status` in a private chat with each bot after installation; `/auth` names the runtime being managed, and `/status` summarizes every bot's connection, runtime type, selected auth, and active turns.
|
|
210
210
|
|
|
211
211
|
To keep one Telegram bot interoperable with terminal Codex sessions, put the same token in both `TG_BOT_TOKENS` and `TG_BOT_TOKEN`. That bot uses the default `CODEX_HOME` (usually `~/.codex` when unset) and default auth, so it can see local terminal sessions. It no longer has the isolated runtime guarantee: `/auth` switches also affect the terminal and other default runtimes.
|
|
212
212
|
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface AuthMirrorStatus {
|
|
|
12
12
|
sourceLabel: string;
|
|
13
13
|
syncedAt: string;
|
|
14
14
|
}
|
|
15
|
+
export interface AuthMirrorRecovery {
|
|
16
|
+
candidateName: string;
|
|
17
|
+
sourceRuntimeId: string;
|
|
18
|
+
sourceCandidateName: string;
|
|
19
|
+
lastRefreshMs: number;
|
|
20
|
+
}
|
|
15
21
|
export interface AuthMirrorValidationContext {
|
|
16
22
|
candidateName: string;
|
|
17
23
|
accountId: string;
|
|
@@ -32,6 +38,7 @@ export declare class AuthCandidateMirror {
|
|
|
32
38
|
private readonly statusPath;
|
|
33
39
|
private timer;
|
|
34
40
|
private readonly lastSyncedRefresh;
|
|
41
|
+
private readonly lastValidationFailures;
|
|
35
42
|
private activeOperations;
|
|
36
43
|
private lastStatus;
|
|
37
44
|
constructor(canonicalDir: string, runtimes: AuthMirrorRuntime[], logger: Logger, statusPath?: string | null);
|
|
@@ -41,12 +48,14 @@ export declare class AuthCandidateMirror {
|
|
|
41
48
|
isIdle(): boolean;
|
|
42
49
|
getStatus(): AuthMirrorStatus | null;
|
|
43
50
|
syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
|
|
51
|
+
recoverRuntimeCandidate(runtimeId: string, candidateName: string): Promise<AuthMirrorRecovery | null>;
|
|
44
52
|
private scan;
|
|
45
53
|
private propagateValidatedCandidate;
|
|
46
54
|
private validateRuntimeCandidate;
|
|
47
55
|
private withActivity;
|
|
48
56
|
private ensureCanonicalDefaultCandidate;
|
|
49
57
|
private collectCandidateNames;
|
|
58
|
+
private collectAuthRecords;
|
|
50
59
|
private reconcileCandidateAtStartup;
|
|
51
60
|
private resolveCanonicalCurrentCandidate;
|
|
52
61
|
}
|
package/dist/auth/mirror.js
CHANGED
|
@@ -8,6 +8,7 @@ export class AuthCandidateMirror {
|
|
|
8
8
|
statusPath;
|
|
9
9
|
timer = null;
|
|
10
10
|
lastSyncedRefresh = new Map();
|
|
11
|
+
lastValidationFailures = new Map();
|
|
11
12
|
activeOperations = 0;
|
|
12
13
|
lastStatus = null;
|
|
13
14
|
constructor(canonicalDir, runtimes, logger, statusPath = null) {
|
|
@@ -71,6 +72,39 @@ export class AuthCandidateMirror {
|
|
|
71
72
|
return false;
|
|
72
73
|
return this.withActivity(() => this.propagateValidatedCandidate(runtime, candidateName));
|
|
73
74
|
}
|
|
75
|
+
async recoverRuntimeCandidate(runtimeId, candidateName) {
|
|
76
|
+
if (!isAuthCandidateName(candidateName))
|
|
77
|
+
return null;
|
|
78
|
+
const runtime = this.runtimes.find((entry) => entry.id === runtimeId);
|
|
79
|
+
if (!runtime)
|
|
80
|
+
return null;
|
|
81
|
+
return this.withActivity(async () => {
|
|
82
|
+
const destinationPath = path.join(runtime.authDir, candidateName);
|
|
83
|
+
const destination = await readChatGptAuthRecord(destinationPath);
|
|
84
|
+
if (!destination)
|
|
85
|
+
return null;
|
|
86
|
+
const sources = await this.collectAuthRecords();
|
|
87
|
+
const newest = sources
|
|
88
|
+
.filter(entry => entry.record.accountId === destination.accountId)
|
|
89
|
+
.reduce((current, entry) => (!current || entry.record.lastRefreshMs > current.record.lastRefreshMs ? entry : current), null);
|
|
90
|
+
if (!newest || newest.record.lastRefreshMs <= destination.lastRefreshMs) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
await atomicWrite(destinationPath, newest.record.raw);
|
|
94
|
+
this.logger.info('auth.mirror.recovered', {
|
|
95
|
+
runtimeId,
|
|
96
|
+
candidateName,
|
|
97
|
+
sourceRuntimeId: newest.runtimeId,
|
|
98
|
+
sourceCandidateName: newest.candidateName,
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
candidateName,
|
|
102
|
+
sourceRuntimeId: newest.runtimeId,
|
|
103
|
+
sourceCandidateName: newest.candidateName,
|
|
104
|
+
lastRefreshMs: newest.record.lastRefreshMs,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
74
108
|
async scan() {
|
|
75
109
|
await this.withActivity(async () => {
|
|
76
110
|
for (const runtime of this.runtimes) {
|
|
@@ -98,13 +132,20 @@ export class AuthCandidateMirror {
|
|
|
98
132
|
}
|
|
99
133
|
const validation = await this.validateRuntimeCandidate(runtime, name, record);
|
|
100
134
|
if (!validation.ok) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
const reason = validation.reason ?? 'unknown';
|
|
136
|
+
const failureKey = `${runtime.id}:${name}`;
|
|
137
|
+
const failureValue = `${record.lastRefreshMs}:${reason}`;
|
|
138
|
+
if (this.lastValidationFailures.get(failureKey) !== failureValue) {
|
|
139
|
+
this.lastValidationFailures.set(failureKey, failureValue);
|
|
140
|
+
this.logger.warn('auth.mirror.validation_failed', {
|
|
141
|
+
runtimeId: runtime.id,
|
|
142
|
+
name,
|
|
143
|
+
reason,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
106
146
|
return false;
|
|
107
147
|
}
|
|
148
|
+
this.lastValidationFailures.delete(`${runtime.id}:${name}`);
|
|
108
149
|
await atomicWrite(canonicalPath, record.raw);
|
|
109
150
|
for (const target of this.runtimes) {
|
|
110
151
|
if (target.id !== runtime.id) {
|
|
@@ -174,6 +215,23 @@ export class AuthCandidateMirror {
|
|
|
174
215
|
}
|
|
175
216
|
return [...all];
|
|
176
217
|
}
|
|
218
|
+
async collectAuthRecords() {
|
|
219
|
+
const directories = [
|
|
220
|
+
{ runtimeId: 'canonical', authDir: this.canonicalDir },
|
|
221
|
+
...this.runtimes.map(runtime => ({ runtimeId: runtime.id, authDir: runtime.authDir })),
|
|
222
|
+
];
|
|
223
|
+
const records = [];
|
|
224
|
+
for (const directory of directories) {
|
|
225
|
+
const names = new Set(['auth.json', ...await listAuthCandidateNames(directory.authDir)]);
|
|
226
|
+
for (const candidateName of names) {
|
|
227
|
+
const record = await readChatGptAuthRecord(path.join(directory.authDir, candidateName));
|
|
228
|
+
if (record) {
|
|
229
|
+
records.push({ runtimeId: directory.runtimeId, candidateName, record });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return records;
|
|
234
|
+
}
|
|
177
235
|
async reconcileCandidateAtStartup(name) {
|
|
178
236
|
const paths = [
|
|
179
237
|
path.join(this.canonicalDir, name),
|
|
@@ -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
|
+
recoverAuthCandidate?: (runtimeId: string, candidateName: string) => Promise<boolean>;
|
|
12
13
|
statusUpdated?: (status: RuntimeStatus) => void;
|
|
13
14
|
getServiceStatus?: () => Promise<{
|
|
14
15
|
bots: NonNullable<RuntimeStatus['bots']>;
|
|
@@ -285,6 +286,8 @@ export declare class BridgeSessionCore {
|
|
|
285
286
|
private readCodexAuthSwitchLabels;
|
|
286
287
|
private codexAuthSwitchParams;
|
|
287
288
|
private switchCodexAuthAndRestart;
|
|
289
|
+
private recoverCodexAuthCandidate;
|
|
290
|
+
private syncCodexAuthCandidate;
|
|
288
291
|
private buildNativeCollaborationMode;
|
|
289
292
|
private buildCodexUsageStatusLines;
|
|
290
293
|
private buildCodexLocalUsageStatusLines;
|
|
@@ -3762,10 +3762,20 @@ export class BridgeSessionCore {
|
|
|
3762
3762
|
return;
|
|
3763
3763
|
}
|
|
3764
3764
|
await this.sendMessage(scopeId, t(locale, 'auth_reload_restarting'));
|
|
3765
|
+
const currentCandidate = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
|
|
3766
|
+
const recovered = currentCandidate
|
|
3767
|
+
? await this.recoverCodexAuthCandidate(currentCandidate.name)
|
|
3768
|
+
: false;
|
|
3765
3769
|
this.pendingTurnErrors.clear();
|
|
3766
3770
|
this.attachedThreads.clear();
|
|
3767
3771
|
await this.app.restart();
|
|
3772
|
+
if (currentCandidate) {
|
|
3773
|
+
await this.syncCodexAuthCandidate(currentCandidate.name);
|
|
3774
|
+
}
|
|
3768
3775
|
const lines = [t(locale, 'auth_reload_done')];
|
|
3776
|
+
if (recovered) {
|
|
3777
|
+
lines.push(t(locale, 'auth_recovered_newer_candidate', { value: currentCandidate.name }));
|
|
3778
|
+
}
|
|
3769
3779
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
3770
3780
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
3771
3781
|
}
|
|
@@ -4398,13 +4408,19 @@ export class BridgeSessionCore {
|
|
|
4398
4408
|
return;
|
|
4399
4409
|
}
|
|
4400
4410
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_recorded'));
|
|
4401
|
-
this.pendingAuthChoiceLists.delete(localId);
|
|
4402
4411
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
4403
4412
|
const switchingMessage = t(locale, 'auth_switching', this.codexAuthSwitchParams(locale, switchLabels.fromLabel, switchLabels.toLabel));
|
|
4404
4413
|
if (record.messageId !== null) {
|
|
4405
4414
|
await this.editMessage(event.scopeId, record.messageId, switchingMessage, []);
|
|
4406
4415
|
}
|
|
4407
|
-
await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false);
|
|
4416
|
+
await this.switchCodexAuthAndRestart(event.scopeId, locale, candidate, false, false);
|
|
4417
|
+
const state = await this.listCodexAuthState();
|
|
4418
|
+
await this.refreshCurrentCodexAuthQuota(state);
|
|
4419
|
+
record.candidates = state.candidates;
|
|
4420
|
+
record.createdAt = Date.now();
|
|
4421
|
+
if (record.messageId !== null) {
|
|
4422
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4423
|
+
}
|
|
4408
4424
|
}
|
|
4409
4425
|
async maybeRunPendingAuthRotation() {
|
|
4410
4426
|
if (!this.pendingAuthRotation || this.authRotationInProgress) {
|
|
@@ -4541,16 +4557,52 @@ export class BridgeSessionCore {
|
|
|
4541
4557
|
to: toLabel,
|
|
4542
4558
|
};
|
|
4543
4559
|
}
|
|
4544
|
-
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic) {
|
|
4560
|
+
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic, sendResult = true) {
|
|
4561
|
+
const recovered = await this.recoverCodexAuthCandidate(candidate.name);
|
|
4545
4562
|
const result = await switchCodexAuth(candidate.path, this.resolveAuthDir());
|
|
4546
4563
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
4547
4564
|
this.pendingTurnErrors.clear();
|
|
4548
4565
|
this.attachedThreads.clear();
|
|
4549
4566
|
await this.app.restart();
|
|
4567
|
+
await this.syncCodexAuthCandidate(candidate.name);
|
|
4568
|
+
if (!sendResult) {
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4550
4571
|
const lines = [t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
4572
|
+
if (recovered) {
|
|
4573
|
+
lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
|
|
4574
|
+
}
|
|
4551
4575
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
4552
4576
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
4553
4577
|
}
|
|
4578
|
+
async recoverCodexAuthCandidate(candidateName) {
|
|
4579
|
+
if (!this.coordinator?.recoverAuthCandidate) {
|
|
4580
|
+
return false;
|
|
4581
|
+
}
|
|
4582
|
+
try {
|
|
4583
|
+
return await this.coordinator.recoverAuthCandidate(this.authRuntimeId(), candidateName);
|
|
4584
|
+
}
|
|
4585
|
+
catch (error) {
|
|
4586
|
+
this.logger.warn('codex.auth_candidate_recovery_failed', {
|
|
4587
|
+
candidate: candidateName,
|
|
4588
|
+
runtimeId: this.authRuntimeId(),
|
|
4589
|
+
error: toErrorMeta(error),
|
|
4590
|
+
});
|
|
4591
|
+
return false;
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
async syncCodexAuthCandidate(candidateName) {
|
|
4595
|
+
try {
|
|
4596
|
+
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), candidateName);
|
|
4597
|
+
}
|
|
4598
|
+
catch (error) {
|
|
4599
|
+
this.logger.warn('codex.auth_candidate_sync_failed', {
|
|
4600
|
+
candidate: candidateName,
|
|
4601
|
+
runtimeId: this.authRuntimeId(),
|
|
4602
|
+
error: toErrorMeta(error),
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4554
4606
|
async buildNativeCollaborationMode(settings, cwd, modeOverride) {
|
|
4555
4607
|
const mode = resolveCollaborationMode(modeOverride === undefined ? settings?.collaborationMode ?? null : modeOverride);
|
|
4556
4608
|
try {
|
|
@@ -4761,6 +4813,7 @@ export class BridgeSessionCore {
|
|
|
4761
4813
|
}
|
|
4762
4814
|
await this.writeCodexAuthQuotaSnapshots();
|
|
4763
4815
|
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
4816
|
+
await this.syncCodexAuthCandidate(candidate.name);
|
|
4764
4817
|
}
|
|
4765
4818
|
catch (error) {
|
|
4766
4819
|
this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
|
package/dist/i18n.d.ts
CHANGED
|
@@ -157,6 +157,7 @@ declare const MESSAGES: {
|
|
|
157
157
|
readonly auth_choice_recorded: "Auth selected";
|
|
158
158
|
readonly auth_switching: "Switching Codex auth: {from} -> {to}...";
|
|
159
159
|
readonly auth_switch_done: "Codex auth switched: {from} -> {to}.";
|
|
160
|
+
readonly auth_recovered_newer_candidate: "Recovered a newer same-account credential for {value} from another Codex home before reload.";
|
|
160
161
|
readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
|
|
161
162
|
readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
|
|
162
163
|
readonly auth_auto_retrying: "Retrying the same request with the new auth...";
|
|
@@ -732,6 +733,7 @@ declare const MESSAGES: {
|
|
|
732
733
|
readonly auth_choice_recorded: "已选择 auth";
|
|
733
734
|
readonly auth_switching: "正在切换 Codex auth:{from} -> {to}...";
|
|
734
735
|
readonly auth_switch_done: "Codex auth 已切换:{from} -> {to}。";
|
|
736
|
+
readonly auth_recovered_newer_candidate: "重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。";
|
|
735
737
|
readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
|
|
736
738
|
readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
|
|
737
739
|
readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
|
package/dist/i18n.js
CHANGED
|
@@ -155,6 +155,7 @@ const MESSAGES = {
|
|
|
155
155
|
auth_choice_recorded: 'Auth selected',
|
|
156
156
|
auth_switching: 'Switching Codex auth: {from} -> {to}...',
|
|
157
157
|
auth_switch_done: 'Codex auth switched: {from} -> {to}.',
|
|
158
|
+
auth_recovered_newer_candidate: 'Recovered a newer same-account credential for {value} from another Codex home before reload.',
|
|
158
159
|
auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
|
|
159
160
|
auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
|
|
160
161
|
auth_auto_retrying: 'Retrying the same request with the new auth...',
|
|
@@ -730,6 +731,7 @@ const MESSAGES = {
|
|
|
730
731
|
auth_choice_recorded: '已选择 auth',
|
|
731
732
|
auth_switching: '正在切换 Codex auth:{from} -> {to}...',
|
|
732
733
|
auth_switch_done: 'Codex auth 已切换:{from} -> {to}。',
|
|
734
|
+
auth_recovered_newer_candidate: '重载前已从其他 Codex home 恢复 {value} 的同账号较新凭据。',
|
|
733
735
|
auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
|
|
734
736
|
auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
|
|
735
737
|
auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
|
package/dist/main.js
CHANGED
|
@@ -279,6 +279,7 @@ async function runServeCli() {
|
|
|
279
279
|
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
280
280
|
&& mirror.isIdle(),
|
|
281
281
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
282
|
+
recoverAuthCandidate: (runtimeId, candidateName) => mirror.recoverRuntimeCandidate(runtimeId, candidateName).then(recovery => recovery !== null),
|
|
282
283
|
statusUpdated: () => writeAggregateStatus(),
|
|
283
284
|
getServiceStatus: async () => ({
|
|
284
285
|
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
@@ -441,11 +442,10 @@ async function validateRefreshedAuthCandidate(runtime, candidateName) {
|
|
|
441
442
|
if (!runtime.app.isConnected()) {
|
|
442
443
|
return { ok: false, reason: 'source app-server is not connected' };
|
|
443
444
|
}
|
|
444
|
-
const
|
|
445
|
-
if (!
|
|
446
|
-
return { ok: false, reason: 'source app-server did not
|
|
445
|
+
const rateLimits = await runtime.app.readAccountRateLimits();
|
|
446
|
+
if (!rateLimits) {
|
|
447
|
+
return { ok: false, reason: 'source app-server did not return ChatGPT rate limits' };
|
|
447
448
|
}
|
|
448
|
-
await runtime.app.readAccountRateLimits();
|
|
449
449
|
return { ok: true };
|
|
450
450
|
}
|
|
451
451
|
async function initConfig() {
|
package/docs/user-manual.md
CHANGED
|
@@ -359,7 +359,7 @@ FoxClaw recognizes candidate names in these forms:
|
|
|
359
359
|
- `auth.json.<name>`
|
|
360
360
|
- `auth.json-<name>`
|
|
361
361
|
|
|
362
|
-
`auth.json` is what Codex currently uses. When switching accounts, FoxClaw points `auth.json` at one candidate. Candidate contents are Codex-generated JSON and should not be hand-written. In multi-bot mode, FoxClaw mirrors a candidate only when its account identity matches
|
|
362
|
+
`auth.json` is what Codex currently uses. When switching accounts, FoxClaw points `auth.json` at one candidate. Candidate contents are Codex-generated JSON and should not be hand-written. In multi-bot mode, FoxClaw mirrors a candidate only when its account identity matches, its refresh timestamp is newer, and the active app-server verifies it against the ChatGPT usage endpoint, preventing a same-name candidate from overwriting a different account. Before an auth switch or reload, FoxClaw also searches the other Codex homes for a newer credential with the same account ID, restores it into the requesting runtime, then verifies and mirrors it after restart.
|
|
363
363
|
|
|
364
364
|
If you already have a working `auth.json`, you can save it as a candidate:
|
|
365
365
|
|
|
@@ -408,7 +408,7 @@ Quota remaining: 5h|7d|auth
|
|
|
408
408
|
[🔄 Reload auth]
|
|
409
409
|
```
|
|
410
410
|
|
|
411
|
-
The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. `--|--` means no quota snapshot has been observed for that candidate yet.
|
|
411
|
+
The right-side `✅` / `⏸️` button shows the current state. 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.
|
|
412
412
|
|
|
413
413
|
Equivalent commands:
|
|
414
414
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -359,7 +359,7 @@ FoxClaw 识别这些候选文件名:
|
|
|
359
359
|
- `auth.json.<name>`
|
|
360
360
|
- `auth.json-<name>`
|
|
361
361
|
|
|
362
|
-
`auth.json` 是 Codex 当前使用的文件。切换账号时,FoxClaw 会把 `auth.json` 指向某个候选文件。候选文件内容是 Codex CLI 生成的 JSON,不建议手写这些字段。多 bot
|
|
362
|
+
`auth.json` 是 Codex 当前使用的文件。切换账号时,FoxClaw 会把 `auth.json` 指向某个候选文件。候选文件内容是 Codex CLI 生成的 JSON,不建议手写这些字段。多 bot 镜像只在账号标识一致、刷新时间更新,并且由当前 app-server 通过 ChatGPT 用量接口在线验证时复制候选,避免同名候选意外覆盖成另一个账号。切换或重载 auth 之前,FoxClaw 还会在其他 Codex home 中查找同账号 ID 的较新凭据,先恢复到发起操作的 runtime,再在重启后验证并镜像。
|
|
363
363
|
|
|
364
364
|
如果你已经有一个可用的 `auth.json`,可以先备份成候选:
|
|
365
365
|
|
|
@@ -408,7 +408,7 @@ Candidates: 2
|
|
|
408
408
|
[🔄 Reload auth]
|
|
409
409
|
```
|
|
410
410
|
|
|
411
|
-
右侧 `✅` / `⏸️`
|
|
411
|
+
右侧 `✅` / `⏸️` 表示当前状态。点一下会切换启用/禁用,列表刷新后图标会随状态变化。点击候选会切换 auth、重启对应 runtime,并在原消息上刷新面板且保留按钮,因此可以立即连续切换。`--|--` 表示该候选还没有额度历史快照。
|
|
412
412
|
|
|
413
413
|
命令等价用法:
|
|
414
414
|
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -66,6 +66,7 @@ Telegram behavior:
|
|
|
66
66
|
- `TG_ALLOWED_CHAT_ID` and `TG_ALLOWED_TOPIC_ID` choose the default group or topic scope; they do not disable private chat
|
|
67
67
|
- when `TG_BOT_TOKENS` contains multiple bots, one FoxClaw service starts independent Codex app-servers and auth selections per bot
|
|
68
68
|
- isolated Telegram runtimes use independent `CODEX_HOME` directories and file-backed credentials; they do not share Codex sessions
|
|
69
|
+
- refreshed ChatGPT candidates are mirrored only after online usage-endpoint validation; before auth switch or reload, a runtime restores any newer same-account credential found in another Codex home
|
|
69
70
|
- if `TG_BOT_TOKEN` is also set to one exact token from `TG_BOT_TOKENS`, that bot uses the default/shared-terminal runtime instead of an isolated Telegram home
|
|
70
71
|
- if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime
|
|
71
72
|
- in a group with multiple configured bots, address a bot by mention, reply, or suffixed command such as `/status@botname`
|