@foxden-app/foxclaw 0.4.2 → 0.4.3
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 +5 -0
- package/dist/auth/mirror.js +20 -1
- package/dist/controller/controller.d.ts +3 -0
- package/dist/controller/controller.js +96 -3
- package/dist/store/database.d.ts +11 -0
- package/dist/store/database.js +67 -0
- package/docs/user-manual.md +1 -1
- package/docs/zh/user-manual.md +1 -1
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ FoxClaw 只响应 `TG_ALLOWED_USER_ID` 的消息——把机器人拉进群不
|
|
|
101
101
|
**多账号管理:**
|
|
102
102
|
- Codex 账户管理:`/account`、`/quota`、`/login_device`、`/auth add <name>`
|
|
103
103
|
- 触发用量限制时自动在本地 `auth.json_*` 之间切换认证——5 小时限制到了自动换号
|
|
104
|
-
- `/auth`
|
|
104
|
+
- `/auth` 面板查看、启用、禁用、切换候选账号;多 bot 模式下会按账号 ID 汇总各 runtime 最近掌握的额度快照
|
|
105
105
|
|
|
106
106
|
**线程与会话:**
|
|
107
107
|
- `/threads`、`/open`、`/new`、`/where`、`/interrupt`——稳定的聊天-线程绑定
|
package/README_EN.md
CHANGED
|
@@ -101,7 +101,7 @@ FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a gr
|
|
|
101
101
|
**Multi-account management:**
|
|
102
102
|
- Codex account controls: `/account`, `/quota`, `/login_device`, `/auth add <name>`
|
|
103
103
|
- Automatic auth rotation across local `auth.json_*` files when a usage limit is hit — seamless account switching
|
|
104
|
-
- `/auth` panel to view, enable, disable, and switch between candidate accounts
|
|
104
|
+
- `/auth` panel to view, enable, disable, and switch between candidate accounts; in multi-bot mode it merges recent quota snapshots by account ID across runtimes
|
|
105
105
|
|
|
106
106
|
**Threads and sessions:**
|
|
107
107
|
- `/threads`, `/open`, `/new`, `/where`, `/interrupt` — sticky chat-to-thread binding
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export interface AuthMirrorValidationResult {
|
|
|
21
21
|
ok: boolean;
|
|
22
22
|
reason?: string | null;
|
|
23
23
|
}
|
|
24
|
+
export interface ChatGptAuthMetadata {
|
|
25
|
+
accountId: string;
|
|
26
|
+
lastRefreshMs: number;
|
|
27
|
+
}
|
|
24
28
|
export declare class AuthCandidateMirror {
|
|
25
29
|
private readonly canonicalDir;
|
|
26
30
|
private readonly runtimes;
|
|
@@ -47,3 +51,4 @@ export declare class AuthCandidateMirror {
|
|
|
47
51
|
private resolveCanonicalCurrentCandidate;
|
|
48
52
|
}
|
|
49
53
|
export declare function isAuthCandidateName(name: string): boolean;
|
|
54
|
+
export declare function readChatGptAuthMetadata(filePath: string): Promise<ChatGptAuthMetadata | null>;
|
package/dist/auth/mirror.js
CHANGED
|
@@ -216,12 +216,31 @@ async function listAuthCandidateNames(dir) {
|
|
|
216
216
|
async function readChatGptAuthRecord(filePath) {
|
|
217
217
|
try {
|
|
218
218
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
219
|
+
const metadata = parseChatGptAuthMetadata(raw);
|
|
220
|
+
if (!metadata)
|
|
221
|
+
return null;
|
|
222
|
+
return { raw, ...metadata };
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export async function readChatGptAuthMetadata(filePath) {
|
|
229
|
+
try {
|
|
230
|
+
return parseChatGptAuthMetadata(await fs.readFile(filePath, 'utf8'));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function parseChatGptAuthMetadata(raw) {
|
|
237
|
+
try {
|
|
219
238
|
const parsed = JSON.parse(raw);
|
|
220
239
|
const accountId = typeof parsed.tokens?.account_id === 'string' ? parsed.tokens.account_id : '';
|
|
221
240
|
const lastRefreshMs = typeof parsed.last_refresh === 'string' ? Date.parse(parsed.last_refresh) : NaN;
|
|
222
241
|
if (!accountId || !Number.isFinite(lastRefreshMs))
|
|
223
242
|
return null;
|
|
224
|
-
return {
|
|
243
|
+
return { accountId, lastRefreshMs };
|
|
225
244
|
}
|
|
226
245
|
catch {
|
|
227
246
|
return null;
|
|
@@ -295,6 +295,9 @@ export declare class BridgeSessionCore {
|
|
|
295
295
|
private refreshCodexLocalUsageStats;
|
|
296
296
|
private codexLocalUsageSnapshotPath;
|
|
297
297
|
private refreshCurrentCodexAuthQuota;
|
|
298
|
+
private applySharedCodexAuthQuotaSnapshots;
|
|
299
|
+
private readCodexAuthCandidateAccountIds;
|
|
300
|
+
private codexAuthQuotaSnapshotMatchesAccount;
|
|
298
301
|
private readCodexAuthQuotaSnapshots;
|
|
299
302
|
private writeCodexAuthQuotaSnapshots;
|
|
300
303
|
private codexAuthQuotaSnapshotPath;
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { normalizeLocale, t } from '../i18n.js';
|
|
6
|
+
import { readChatGptAuthMetadata } from '../auth/mirror.js';
|
|
6
7
|
import { parseCommand } from './commands.js';
|
|
7
8
|
import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
|
|
8
9
|
import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
|
|
@@ -4510,9 +4511,15 @@ export class BridgeSessionCore {
|
|
|
4510
4511
|
async listCodexAuthState() {
|
|
4511
4512
|
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
|
|
4512
4513
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4514
|
+
const candidateAccountIds = await this.readCodexAuthCandidateAccountIds(state.candidates);
|
|
4513
4515
|
state.candidates.forEach((candidate) => {
|
|
4514
|
-
|
|
4516
|
+
const candidateAccountId = candidateAccountIds.get(candidate.name) ?? null;
|
|
4517
|
+
const snapshot = snapshots[candidate.name] ?? null;
|
|
4518
|
+
candidate.quota = this.codexAuthQuotaSnapshotMatchesAccount(snapshot, candidateAccountId)
|
|
4519
|
+
? snapshot
|
|
4520
|
+
: null;
|
|
4515
4521
|
});
|
|
4522
|
+
await this.applySharedCodexAuthQuotaSnapshots(state, candidateAccountIds);
|
|
4516
4523
|
return state;
|
|
4517
4524
|
}
|
|
4518
4525
|
resolveAuthDir() {
|
|
@@ -4741,19 +4748,67 @@ export class BridgeSessionCore {
|
|
|
4741
4748
|
return;
|
|
4742
4749
|
}
|
|
4743
4750
|
try {
|
|
4751
|
+
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
4744
4752
|
const snapshot = selectCodexRateLimitSnapshot(await this.app.readAccountRateLimits());
|
|
4745
4753
|
if (!snapshot) {
|
|
4746
4754
|
return;
|
|
4747
4755
|
}
|
|
4748
|
-
const quota = authQuotaSnapshotFromRateLimit(snapshot);
|
|
4756
|
+
const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null);
|
|
4749
4757
|
candidate.quota = quota;
|
|
4750
4758
|
this.authQuotaSnapshots[candidate.name] = quota;
|
|
4759
|
+
if (metadata?.accountId) {
|
|
4760
|
+
this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidate.name, metadata.accountId, quota);
|
|
4761
|
+
}
|
|
4751
4762
|
await this.writeCodexAuthQuotaSnapshots();
|
|
4763
|
+
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
4752
4764
|
}
|
|
4753
4765
|
catch (error) {
|
|
4754
4766
|
this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
|
|
4755
4767
|
}
|
|
4756
4768
|
}
|
|
4769
|
+
async applySharedCodexAuthQuotaSnapshots(state, candidateAccountIds) {
|
|
4770
|
+
const accountIds = candidateAccountIds ?? await this.readCodexAuthCandidateAccountIds(state.candidates);
|
|
4771
|
+
const uniqueAccountIds = [...new Set(accountIds.values())];
|
|
4772
|
+
if (uniqueAccountIds.length === 0) {
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
const snapshotsByAccount = new Map();
|
|
4776
|
+
for (const record of this.store.listCodexAuthQuotaSnapshots(uniqueAccountIds)) {
|
|
4777
|
+
if (!isFiniteCodexAuthQuotaSnapshotRecord(record)) {
|
|
4778
|
+
continue;
|
|
4779
|
+
}
|
|
4780
|
+
snapshotsByAccount.set(record.accountId, mergeCodexAuthQuotaSnapshots(snapshotsByAccount.get(record.accountId) ?? null, codexAuthQuotaSnapshotFromRecord(record)));
|
|
4781
|
+
}
|
|
4782
|
+
for (const candidate of state.candidates) {
|
|
4783
|
+
const accountId = accountIds.get(candidate.name);
|
|
4784
|
+
if (!accountId) {
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
candidate.quota = mergeCodexAuthQuotaSnapshots(candidate.quota, snapshotsByAccount.get(accountId) ?? null);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
async readCodexAuthCandidateAccountIds(candidates) {
|
|
4791
|
+
const entries = await Promise.all(candidates.map(async (candidate) => {
|
|
4792
|
+
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
4793
|
+
return [candidate.name, metadata?.accountId ?? null];
|
|
4794
|
+
}));
|
|
4795
|
+
const accountIds = new Map();
|
|
4796
|
+
for (const [name, accountId] of entries) {
|
|
4797
|
+
if (accountId) {
|
|
4798
|
+
accountIds.set(name, accountId);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
return accountIds;
|
|
4802
|
+
}
|
|
4803
|
+
codexAuthQuotaSnapshotMatchesAccount(snapshot, accountId) {
|
|
4804
|
+
if (!snapshot) {
|
|
4805
|
+
return false;
|
|
4806
|
+
}
|
|
4807
|
+
if (!snapshot.accountId) {
|
|
4808
|
+
return accountId === null;
|
|
4809
|
+
}
|
|
4810
|
+
return accountId !== null && snapshot.accountId === accountId;
|
|
4811
|
+
}
|
|
4757
4812
|
async readCodexAuthQuotaSnapshots() {
|
|
4758
4813
|
if (this.authQuotaSnapshotsLoaded) {
|
|
4759
4814
|
return this.authQuotaSnapshots;
|
|
@@ -7934,13 +7989,50 @@ function formatRemainingUsagePercent(usedPercent) {
|
|
|
7934
7989
|
const remainingPercent = remainingUsagePercent(usedPercent);
|
|
7935
7990
|
return remainingPercent === null ? '?' : formatUsagePercent(remainingPercent);
|
|
7936
7991
|
}
|
|
7937
|
-
function authQuotaSnapshotFromRateLimit(snapshot) {
|
|
7992
|
+
function authQuotaSnapshotFromRateLimit(snapshot, accountId = null) {
|
|
7938
7993
|
return {
|
|
7939
7994
|
capturedAtMs: Date.now(),
|
|
7995
|
+
accountId,
|
|
7940
7996
|
primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
|
|
7941
7997
|
secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
|
|
7942
7998
|
};
|
|
7943
7999
|
}
|
|
8000
|
+
function codexAuthQuotaSnapshotFromRecord(record) {
|
|
8001
|
+
return {
|
|
8002
|
+
capturedAtMs: record.capturedAtMs,
|
|
8003
|
+
accountId: record.accountId,
|
|
8004
|
+
primaryRemainingPercent: record.primaryRemainingPercent,
|
|
8005
|
+
secondaryRemainingPercent: record.secondaryRemainingPercent,
|
|
8006
|
+
};
|
|
8007
|
+
}
|
|
8008
|
+
function mergeCodexAuthQuotaSnapshots(current, incoming) {
|
|
8009
|
+
if (!current) {
|
|
8010
|
+
return incoming;
|
|
8011
|
+
}
|
|
8012
|
+
if (!incoming) {
|
|
8013
|
+
return current;
|
|
8014
|
+
}
|
|
8015
|
+
return {
|
|
8016
|
+
capturedAtMs: Math.max(current.capturedAtMs, incoming.capturedAtMs),
|
|
8017
|
+
accountId: current.accountId ?? incoming.accountId ?? null,
|
|
8018
|
+
primaryRemainingPercent: fresherNullableQuotaValue(current.primaryRemainingPercent, current.capturedAtMs, incoming.primaryRemainingPercent, incoming.capturedAtMs),
|
|
8019
|
+
secondaryRemainingPercent: fresherNullableQuotaValue(current.secondaryRemainingPercent, current.capturedAtMs, incoming.secondaryRemainingPercent, incoming.capturedAtMs),
|
|
8020
|
+
};
|
|
8021
|
+
}
|
|
8022
|
+
function fresherNullableQuotaValue(currentValue, currentCapturedAtMs, incomingValue, incomingCapturedAtMs) {
|
|
8023
|
+
if (currentValue === null) {
|
|
8024
|
+
return incomingValue;
|
|
8025
|
+
}
|
|
8026
|
+
if (incomingValue === null) {
|
|
8027
|
+
return currentValue;
|
|
8028
|
+
}
|
|
8029
|
+
return incomingCapturedAtMs >= currentCapturedAtMs ? incomingValue : currentValue;
|
|
8030
|
+
}
|
|
8031
|
+
function isFiniteCodexAuthQuotaSnapshotRecord(record) {
|
|
8032
|
+
return Number.isFinite(record.capturedAtMs)
|
|
8033
|
+
&& isNullableFiniteNumber(record.primaryRemainingPercent)
|
|
8034
|
+
&& isNullableFiniteNumber(record.secondaryRemainingPercent);
|
|
8035
|
+
}
|
|
7944
8036
|
function remainingUsagePercent(usedPercent) {
|
|
7945
8037
|
if (!Number.isFinite(usedPercent)) {
|
|
7946
8038
|
return null;
|
|
@@ -7962,6 +8054,7 @@ function isCodexAuthQuotaSnapshot(value) {
|
|
|
7962
8054
|
const snapshot = value;
|
|
7963
8055
|
return typeof snapshot.capturedAtMs === 'number'
|
|
7964
8056
|
&& Number.isFinite(snapshot.capturedAtMs)
|
|
8057
|
+
&& (snapshot.accountId === undefined || snapshot.accountId === null || typeof snapshot.accountId === 'string')
|
|
7965
8058
|
&& isNullableFiniteNumber(snapshot.primaryRemainingPercent)
|
|
7966
8059
|
&& isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
|
|
7967
8060
|
}
|
package/dist/store/database.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export interface PendingUserInputStoredRecord {
|
|
|
24
24
|
submittedAt: number | null;
|
|
25
25
|
resolvedAt: number | null;
|
|
26
26
|
}
|
|
27
|
+
export interface CodexAuthQuotaSnapshotRecord {
|
|
28
|
+
runtimeId: string;
|
|
29
|
+
candidateName: string;
|
|
30
|
+
accountId: string;
|
|
31
|
+
capturedAtMs: number;
|
|
32
|
+
primaryRemainingPercent: number | null;
|
|
33
|
+
secondaryRemainingPercent: number | null;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
27
36
|
export declare class BridgeStore {
|
|
28
37
|
private db;
|
|
29
38
|
constructor(dbPath: string);
|
|
@@ -77,5 +86,7 @@ export declare class BridgeStore {
|
|
|
77
86
|
setWeixinContextToken(scopeId: string, contextToken: string): void;
|
|
78
87
|
listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
|
|
79
88
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
89
|
+
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'primaryRemainingPercent' | 'secondaryRemainingPercent'>): void;
|
|
90
|
+
listCodexAuthQuotaSnapshots(accountIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
80
91
|
private ensureColumn;
|
|
81
92
|
}
|
package/dist/store/database.js
CHANGED
|
@@ -123,6 +123,18 @@ export class BridgeStore {
|
|
|
123
123
|
updated_at INTEGER NOT NULL,
|
|
124
124
|
PRIMARY KEY (runtime_id, name)
|
|
125
125
|
);
|
|
126
|
+
CREATE TABLE IF NOT EXISTS codex_auth_quota_snapshots (
|
|
127
|
+
runtime_id TEXT NOT NULL,
|
|
128
|
+
candidate_name TEXT NOT NULL,
|
|
129
|
+
account_id TEXT NOT NULL,
|
|
130
|
+
captured_at_ms INTEGER NOT NULL,
|
|
131
|
+
primary_remaining_percent REAL,
|
|
132
|
+
secondary_remaining_percent REAL,
|
|
133
|
+
updated_at INTEGER NOT NULL,
|
|
134
|
+
PRIMARY KEY (runtime_id, candidate_name)
|
|
135
|
+
);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS codex_auth_quota_snapshots_account_idx
|
|
137
|
+
ON codex_auth_quota_snapshots(account_id);
|
|
126
138
|
`);
|
|
127
139
|
this.ensureColumn('thread_cache', 'name', 'TEXT');
|
|
128
140
|
this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
|
|
@@ -509,6 +521,54 @@ export class BridgeStore {
|
|
|
509
521
|
ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
510
522
|
`).run(name, disabled ? 1 : 0, Date.now());
|
|
511
523
|
}
|
|
524
|
+
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, snapshot) {
|
|
525
|
+
this.db.prepare(`
|
|
526
|
+
INSERT INTO codex_auth_quota_snapshots (
|
|
527
|
+
runtime_id,
|
|
528
|
+
candidate_name,
|
|
529
|
+
account_id,
|
|
530
|
+
captured_at_ms,
|
|
531
|
+
primary_remaining_percent,
|
|
532
|
+
secondary_remaining_percent,
|
|
533
|
+
updated_at
|
|
534
|
+
)
|
|
535
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
536
|
+
ON CONFLICT(runtime_id, candidate_name) DO UPDATE SET
|
|
537
|
+
account_id = excluded.account_id,
|
|
538
|
+
captured_at_ms = excluded.captured_at_ms,
|
|
539
|
+
primary_remaining_percent = excluded.primary_remaining_percent,
|
|
540
|
+
secondary_remaining_percent = excluded.secondary_remaining_percent,
|
|
541
|
+
updated_at = excluded.updated_at
|
|
542
|
+
`).run(runtimeId, candidateName, accountId, snapshot.capturedAtMs, snapshot.primaryRemainingPercent, snapshot.secondaryRemainingPercent, Date.now());
|
|
543
|
+
}
|
|
544
|
+
listCodexAuthQuotaSnapshots(accountIds) {
|
|
545
|
+
const uniqueAccountIds = [...new Set(accountIds.filter(Boolean))];
|
|
546
|
+
if (uniqueAccountIds.length === 0) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
const placeholders = uniqueAccountIds.map(() => '?').join(', ');
|
|
550
|
+
const rows = this.db.prepare(`
|
|
551
|
+
SELECT
|
|
552
|
+
runtime_id,
|
|
553
|
+
candidate_name,
|
|
554
|
+
account_id,
|
|
555
|
+
captured_at_ms,
|
|
556
|
+
primary_remaining_percent,
|
|
557
|
+
secondary_remaining_percent,
|
|
558
|
+
updated_at
|
|
559
|
+
FROM codex_auth_quota_snapshots
|
|
560
|
+
WHERE account_id IN (${placeholders})
|
|
561
|
+
`).all(...uniqueAccountIds);
|
|
562
|
+
return rows.map(row => ({
|
|
563
|
+
runtimeId: String(row.runtime_id),
|
|
564
|
+
candidateName: String(row.candidate_name),
|
|
565
|
+
accountId: String(row.account_id),
|
|
566
|
+
capturedAtMs: Number(row.captured_at_ms),
|
|
567
|
+
primaryRemainingPercent: nullableNumber(row.primary_remaining_percent),
|
|
568
|
+
secondaryRemainingPercent: nullableNumber(row.secondary_remaining_percent),
|
|
569
|
+
updatedAt: Number(row.updated_at),
|
|
570
|
+
}));
|
|
571
|
+
}
|
|
512
572
|
ensureColumn(table, column, definition) {
|
|
513
573
|
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
514
574
|
if (columns.some(entry => entry.name === column)) {
|
|
@@ -517,6 +577,13 @@ export class BridgeStore {
|
|
|
517
577
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
518
578
|
}
|
|
519
579
|
}
|
|
580
|
+
function nullableNumber(value) {
|
|
581
|
+
if (value === null || value === undefined) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const numberValue = Number(value);
|
|
585
|
+
return Number.isFinite(numberValue) ? numberValue : null;
|
|
586
|
+
}
|
|
520
587
|
function normalizeCollaborationMode(value) {
|
|
521
588
|
return value === 'default' || value === 'plan' ? value : null;
|
|
522
589
|
}
|
package/docs/user-manual.md
CHANGED
|
@@ -389,7 +389,7 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
|
|
|
389
389
|
|
|
390
390
|
### 6.3 The `/auth` Panel
|
|
391
391
|
|
|
392
|
-
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota.
|
|
392
|
+
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. In multi-bot mode the panel names the `@botname` runtime being managed, because private chats, groups, and topics on one bot share that bot's current auth. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota. When multiple bot runtimes have recently used the same ChatGPT account, FoxClaw combines their cached quota snapshots by verified account ID, so one bot's `/auth` panel can show quota information learned by another bot without mixing different accounts.
|
|
393
393
|
|
|
394
394
|
Approximation:
|
|
395
395
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -389,7 +389,7 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
|
|
|
389
389
|
|
|
390
390
|
### 6.3 `/auth` 面板
|
|
391
391
|
|
|
392
|
-
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth
|
|
392
|
+
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。多 bot 模式中,面板顶部还会显示当前正在管理的 `@botname`,因为该 bot 内的私聊、群聊和话题共享同一个当前 auth。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。如果多个 bot runtime 最近使用过同一个 ChatGPT 账号,FoxClaw 会按已验证的账号 ID 合并它们缓存到的额度快照,因此一个 bot 的 `/auth` 面板可以显示另一个 bot 掌握到的额度信息,同时不会把不同账号混在一起。
|
|
393
393
|
|
|
394
394
|
示意:
|
|
395
395
|
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -146,7 +146,7 @@ After either bootstrap path:
|
|
|
146
146
|
- the bot is an admin in the group
|
|
147
147
|
- the configured `TG_ALLOWED_CHAT_ID` and `TG_ALLOWED_TOPIC_ID` match the target group/topic
|
|
148
148
|
5. If group or topic mode is enabled, also verify that private chat still responds for the configured `TG_ALLOWED_USER_ID`.
|
|
149
|
-
6. With multiple bots, send `/status` and `/auth` privately to each bot; verify the status lists each app-server
|
|
149
|
+
6. With multiple bots, send `/status` and `/auth` privately to each bot; verify the status lists each app-server, the auth panel names the intended bot runtime, and `/auth` quota snapshots are combined only for matching ChatGPT account IDs.
|
|
150
150
|
|
|
151
151
|
## First Telegram Message Check
|
|
152
152
|
|