@foxden-app/foxclaw 0.5.10 → 0.5.12
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 +24 -0
- package/README.md +2 -1
- package/README_EN.md +2 -1
- package/dist/auth/cross_node_sync.d.ts +1 -0
- package/dist/auth/cross_node_sync.js +24 -0
- package/dist/auth/mirror.d.ts +9 -0
- package/dist/auth/mirror.js +121 -7
- package/dist/controller/controller.d.ts +9 -2
- package/dist/controller/controller.js +107 -43
- package/dist/i18n.d.ts +10 -4
- package/dist/i18n.js +10 -4
- package/dist/main.js +22 -0
- package/dist/store/database.d.ts +3 -2
- package/dist/store/database.js +24 -9
- package/docs/cross-node-auth-sync.md +3 -3
- package/docs/release.md +102 -0
- package/docs/user-manual.md +3 -3
- package/docs/zh/cross-node-auth-sync.md +3 -3
- package/docs/zh/release.md +102 -0
- package/docs/zh/user-manual.md +3 -3
- package/package.json +1 -1
|
@@ -1065,7 +1065,7 @@ export class BridgeSessionCore {
|
|
|
1065
1065
|
await this.handleAuthListViewCallback(event, authClearSearchMatch[1], 'clear_search', locale);
|
|
1066
1066
|
return;
|
|
1067
1067
|
}
|
|
1068
|
-
const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
|
|
1068
|
+
const authActionMatch = /^auth:([a-f0-9]+):(login_device|reload|safe_sync|refresh_all_confirm|refresh_all_cancel|refresh_all)$/.exec(event.data);
|
|
1069
1069
|
if (authActionMatch) {
|
|
1070
1070
|
await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
|
|
1071
1071
|
return;
|
|
@@ -2956,7 +2956,7 @@ export class BridgeSessionCore {
|
|
|
2956
2956
|
authDisplayBotLabel() {
|
|
2957
2957
|
if (!this.config.tgScopeBotId)
|
|
2958
2958
|
return null;
|
|
2959
|
-
return this.botUsername ? `@${this.botUsername}` : this.config.tgScopeBotId;
|
|
2959
|
+
return this.botUsername ? `@${this.botUsername} (${this.config.tgScopeBotId})` : this.config.tgScopeBotId;
|
|
2960
2960
|
}
|
|
2961
2961
|
ownsScope(scopeId) {
|
|
2962
2962
|
if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
@@ -4467,17 +4467,19 @@ export class BridgeSessionCore {
|
|
|
4467
4467
|
await this.sendMessage(scopeId, message);
|
|
4468
4468
|
return;
|
|
4469
4469
|
}
|
|
4470
|
-
if (action === 'push' && args[1]?.toLowerCase() === 'all') {
|
|
4470
|
+
if (action === 'safe' || (action === 'push' && args[1]?.toLowerCase() === 'all')) {
|
|
4471
4471
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4472
4472
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
|
|
4473
4473
|
return;
|
|
4474
4474
|
}
|
|
4475
|
-
const result = await this.
|
|
4475
|
+
const result = await this.runAuthSafeSyncAll();
|
|
4476
4476
|
if (!result) {
|
|
4477
4477
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
|
|
4478
4478
|
return;
|
|
4479
4479
|
}
|
|
4480
|
-
await this.sendMessage(scopeId, t(locale, '
|
|
4480
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_safe_done', {
|
|
4481
|
+
localSynced: result.localSynced,
|
|
4482
|
+
localSkipped: result.localSkipped,
|
|
4481
4483
|
sent: result.sent,
|
|
4482
4484
|
skipped: result.skipped,
|
|
4483
4485
|
}));
|
|
@@ -4485,6 +4487,16 @@ export class BridgeSessionCore {
|
|
|
4485
4487
|
}
|
|
4486
4488
|
await this.sendMessage(scopeId, t(locale, 'usage_auth_sync'));
|
|
4487
4489
|
}
|
|
4490
|
+
async runAuthSafeSyncAll() {
|
|
4491
|
+
const safeResult = await this.coordinator?.authSyncSafeAll?.();
|
|
4492
|
+
if (safeResult) {
|
|
4493
|
+
return safeResult;
|
|
4494
|
+
}
|
|
4495
|
+
const pushResult = await this.coordinator?.authSyncPushAll?.();
|
|
4496
|
+
return pushResult
|
|
4497
|
+
? { localSynced: 0, localSkipped: 0, sent: pushResult.sent, skipped: pushResult.skipped }
|
|
4498
|
+
: null;
|
|
4499
|
+
}
|
|
4488
4500
|
async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
|
|
4489
4501
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4490
4502
|
await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -4993,6 +5005,37 @@ export class BridgeSessionCore {
|
|
|
4993
5005
|
await this.handleLoginDeviceCommand(event.scopeId, locale);
|
|
4994
5006
|
return;
|
|
4995
5007
|
}
|
|
5008
|
+
if (action === 'safe_sync') {
|
|
5009
|
+
if (!this.canRunGlobalAuthRefresh()) {
|
|
5010
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_push_blocked_active'));
|
|
5011
|
+
return;
|
|
5012
|
+
}
|
|
5013
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_sync_safe_starting'));
|
|
5014
|
+
if (record.messageId !== null) {
|
|
5015
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_safe_starting'), []);
|
|
5016
|
+
}
|
|
5017
|
+
const result = await this.runAuthSafeSyncAll();
|
|
5018
|
+
if (!result) {
|
|
5019
|
+
if (record.messageId !== null) {
|
|
5020
|
+
await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_sync_disabled'), authChoiceKeyboard(locale, record));
|
|
5021
|
+
}
|
|
5022
|
+
return;
|
|
5023
|
+
}
|
|
5024
|
+
const state = await this.listCodexAuthState();
|
|
5025
|
+
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
5026
|
+
record.candidates = state.candidates;
|
|
5027
|
+
record.createdAt = Date.now();
|
|
5028
|
+
clampCodexAuthListOffset(record);
|
|
5029
|
+
if (record.messageId !== null) {
|
|
5030
|
+
await this.editMessage(event.scopeId, record.messageId, `${t(locale, 'auth_sync_safe_done', {
|
|
5031
|
+
localSynced: result.localSynced,
|
|
5032
|
+
localSkipped: result.localSkipped,
|
|
5033
|
+
sent: result.sent,
|
|
5034
|
+
skipped: result.skipped,
|
|
5035
|
+
})}\n\n${renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record)}`, authChoiceKeyboard(locale, record));
|
|
5036
|
+
}
|
|
5037
|
+
return;
|
|
5038
|
+
}
|
|
4996
5039
|
if (action === 'refresh_all') {
|
|
4997
5040
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4998
5041
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -5267,15 +5310,15 @@ export class BridgeSessionCore {
|
|
|
5267
5310
|
async listCodexAuthState() {
|
|
5268
5311
|
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
|
|
5269
5312
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
5270
|
-
const
|
|
5313
|
+
const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5271
5314
|
state.candidates.forEach((candidate) => {
|
|
5272
|
-
const
|
|
5315
|
+
const candidateQuotaIdentity = candidateQuotaIdentities.get(candidate.name) ?? null;
|
|
5273
5316
|
const snapshot = snapshots[candidate.name] ?? null;
|
|
5274
|
-
candidate.quota = this.
|
|
5317
|
+
candidate.quota = this.codexAuthQuotaSnapshotMatchesIdentity(snapshot, candidateQuotaIdentity)
|
|
5275
5318
|
? snapshot
|
|
5276
5319
|
: null;
|
|
5277
5320
|
});
|
|
5278
|
-
await this.applySharedCodexAuthQuotaSnapshots(state,
|
|
5321
|
+
await this.applySharedCodexAuthQuotaSnapshots(state, candidateQuotaIdentities);
|
|
5279
5322
|
return state;
|
|
5280
5323
|
}
|
|
5281
5324
|
resolveAuthDir() {
|
|
@@ -5384,13 +5427,13 @@ export class BridgeSessionCore {
|
|
|
5384
5427
|
throw new Error('Codex did not return ChatGPT rate limits after refresh');
|
|
5385
5428
|
}
|
|
5386
5429
|
const after = await readChatGptAuthMetadata(candidate.path);
|
|
5387
|
-
if (!after || after
|
|
5388
|
-
throw new Error('refreshed auth
|
|
5430
|
+
if (!after || !chatGptAuthMetadataCompatible(before, after)) {
|
|
5431
|
+
throw new Error('refreshed auth identity did not match the original candidate');
|
|
5389
5432
|
}
|
|
5390
5433
|
if (after.lastRefreshMs <= before.lastRefreshMs) {
|
|
5391
5434
|
throw new Error('Codex did not advance last_refresh');
|
|
5392
5435
|
}
|
|
5393
|
-
await this.recordCodexAuthQuotaSnapshot(candidate.name, after
|
|
5436
|
+
await this.recordCodexAuthQuotaSnapshot(candidate.name, after, snapshot);
|
|
5394
5437
|
await this.syncCodexAuthCandidate(candidate.name);
|
|
5395
5438
|
result.refreshed.push(candidate.name);
|
|
5396
5439
|
}
|
|
@@ -5616,9 +5659,9 @@ export class BridgeSessionCore {
|
|
|
5616
5659
|
if (!snapshot) {
|
|
5617
5660
|
return;
|
|
5618
5661
|
}
|
|
5619
|
-
const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null);
|
|
5662
|
+
const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null, metadata?.quotaIdentityId ?? null);
|
|
5620
5663
|
candidate.quota = quota;
|
|
5621
|
-
await this.recordCodexAuthQuotaSnapshot(candidate.name, metadata
|
|
5664
|
+
await this.recordCodexAuthQuotaSnapshot(candidate.name, metadata, snapshot);
|
|
5622
5665
|
await this.applySharedCodexAuthQuotaSnapshots(state);
|
|
5623
5666
|
await this.syncCodexAuthCandidate(candidate.name);
|
|
5624
5667
|
}
|
|
@@ -5626,36 +5669,36 @@ export class BridgeSessionCore {
|
|
|
5626
5669
|
this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
|
|
5627
5670
|
}
|
|
5628
5671
|
}
|
|
5629
|
-
async applySharedCodexAuthQuotaSnapshots(state,
|
|
5630
|
-
const
|
|
5631
|
-
const
|
|
5632
|
-
if (
|
|
5672
|
+
async applySharedCodexAuthQuotaSnapshots(state, candidateQuotaIdentities) {
|
|
5673
|
+
const quotaIdentities = candidateQuotaIdentities ?? await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5674
|
+
const uniqueQuotaIdentityIds = [...new Set([...quotaIdentities.values()].map(identity => identity.quotaIdentityId))];
|
|
5675
|
+
if (uniqueQuotaIdentityIds.length === 0) {
|
|
5633
5676
|
return;
|
|
5634
5677
|
}
|
|
5635
|
-
const
|
|
5636
|
-
for (const record of this.store.listCodexAuthQuotaSnapshots(
|
|
5678
|
+
const snapshotsByIdentity = new Map();
|
|
5679
|
+
for (const record of this.store.listCodexAuthQuotaSnapshots(uniqueQuotaIdentityIds)) {
|
|
5637
5680
|
if (!isFiniteCodexAuthQuotaSnapshotRecord(record)) {
|
|
5638
5681
|
continue;
|
|
5639
5682
|
}
|
|
5640
|
-
|
|
5683
|
+
snapshotsByIdentity.set(record.quotaIdentityId, mergeCodexAuthQuotaSnapshots(snapshotsByIdentity.get(record.quotaIdentityId) ?? null, codexAuthQuotaSnapshotFromRecord(record)));
|
|
5641
5684
|
}
|
|
5642
5685
|
for (const candidate of state.candidates) {
|
|
5643
|
-
const
|
|
5644
|
-
if (!
|
|
5686
|
+
const quotaIdentity = quotaIdentities.get(candidate.name);
|
|
5687
|
+
if (!quotaIdentity) {
|
|
5645
5688
|
continue;
|
|
5646
5689
|
}
|
|
5647
|
-
candidate.quota = mergeCodexAuthQuotaSnapshots(candidate.quota,
|
|
5690
|
+
candidate.quota = mergeCodexAuthQuotaSnapshots(candidate.quota, snapshotsByIdentity.get(quotaIdentity.quotaIdentityId) ?? null);
|
|
5648
5691
|
}
|
|
5649
5692
|
}
|
|
5650
|
-
async recordCodexAuthQuotaSnapshot(candidateName,
|
|
5651
|
-
const quota = authQuotaSnapshotFromRateLimit(snapshot, accountId);
|
|
5693
|
+
async recordCodexAuthQuotaSnapshot(candidateName, metadata, snapshot) {
|
|
5694
|
+
const quota = authQuotaSnapshotFromRateLimit(snapshot, metadata?.accountId ?? null, metadata?.quotaIdentityId ?? null);
|
|
5652
5695
|
this.authQuotaSnapshots[candidateName] = quota;
|
|
5653
|
-
if (
|
|
5654
|
-
this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidateName, accountId, quota);
|
|
5696
|
+
if (metadata) {
|
|
5697
|
+
this.store.setCodexAuthQuotaSnapshot(this.authRuntimeId(), candidateName, metadata.accountId, metadata.quotaIdentityId, quota);
|
|
5655
5698
|
}
|
|
5656
5699
|
await this.writeCodexAuthQuotaSnapshots();
|
|
5657
5700
|
}
|
|
5658
|
-
async
|
|
5701
|
+
async readCodexAuthCandidateQuotaIdentities(candidates) {
|
|
5659
5702
|
const entries = await Promise.all(candidates.map(async (candidate) => {
|
|
5660
5703
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5661
5704
|
candidate.credentialKind = metadata
|
|
@@ -5664,24 +5707,28 @@ export class BridgeSessionCore {
|
|
|
5664
5707
|
? 'api-key'
|
|
5665
5708
|
: 'invalid';
|
|
5666
5709
|
candidate.credentialLastRefreshMs = metadata?.lastRefreshMs ?? null;
|
|
5667
|
-
return [candidate.name, metadata
|
|
5710
|
+
return [candidate.name, metadata ? {
|
|
5711
|
+
accountId: metadata.accountId,
|
|
5712
|
+
quotaIdentityId: metadata.quotaIdentityId,
|
|
5713
|
+
} : null];
|
|
5668
5714
|
}));
|
|
5669
|
-
const
|
|
5670
|
-
for (const [name,
|
|
5671
|
-
if (
|
|
5672
|
-
|
|
5715
|
+
const quotaIdentities = new Map();
|
|
5716
|
+
for (const [name, quotaIdentity] of entries) {
|
|
5717
|
+
if (quotaIdentity) {
|
|
5718
|
+
quotaIdentities.set(name, quotaIdentity);
|
|
5673
5719
|
}
|
|
5674
5720
|
}
|
|
5675
|
-
return
|
|
5721
|
+
return quotaIdentities;
|
|
5676
5722
|
}
|
|
5677
|
-
|
|
5723
|
+
codexAuthQuotaSnapshotMatchesIdentity(snapshot, identity) {
|
|
5678
5724
|
if (!snapshot) {
|
|
5679
5725
|
return false;
|
|
5680
5726
|
}
|
|
5681
|
-
|
|
5682
|
-
|
|
5727
|
+
const snapshotIdentityId = snapshot.quotaIdentityId ?? snapshot.accountId ?? null;
|
|
5728
|
+
if (!snapshotIdentityId) {
|
|
5729
|
+
return identity === null;
|
|
5683
5730
|
}
|
|
5684
|
-
return
|
|
5731
|
+
return identity !== null && snapshotIdentityId === identity.quotaIdentityId;
|
|
5685
5732
|
}
|
|
5686
5733
|
async readCodexAuthQuotaSnapshots() {
|
|
5687
5734
|
if (this.authQuotaSnapshotsLoaded) {
|
|
@@ -8409,7 +8456,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8409
8456
|
if (state.candidates.length === 0) {
|
|
8410
8457
|
lines.push(t(locale, 'auth_no_candidates'));
|
|
8411
8458
|
if (includeWeixinCopyPaste) {
|
|
8412
|
-
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth reload', '/permissions');
|
|
8459
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8413
8460
|
}
|
|
8414
8461
|
return lines.join('\n');
|
|
8415
8462
|
}
|
|
@@ -8442,7 +8489,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8442
8489
|
if (includeWeixinCopyPaste) {
|
|
8443
8490
|
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...page.visible.map(({ index }) => `/auth use ${index + 1}`), ...page.visible.map(({ candidate, index }) => candidate.disabled
|
|
8444
8491
|
? `/auth enable ${index + 1}`
|
|
8445
|
-
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth reload', '/permissions');
|
|
8492
|
+
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8446
8493
|
}
|
|
8447
8494
|
return lines.join('\n');
|
|
8448
8495
|
}
|
|
@@ -8480,7 +8527,10 @@ function authChoiceKeyboard(locale, record) {
|
|
|
8480
8527
|
{ text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
|
|
8481
8528
|
{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
|
|
8482
8529
|
]);
|
|
8483
|
-
rows.push([
|
|
8530
|
+
rows.push([
|
|
8531
|
+
{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
|
|
8532
|
+
{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
|
|
8533
|
+
]);
|
|
8484
8534
|
return rows;
|
|
8485
8535
|
}
|
|
8486
8536
|
function formatCodexAuthCandidateDisplayName(name) {
|
|
@@ -9322,10 +9372,11 @@ function formatRemainingUsagePercent(usedPercent) {
|
|
|
9322
9372
|
const remainingPercent = remainingUsagePercent(usedPercent);
|
|
9323
9373
|
return remainingPercent === null ? '?' : formatUsagePercent(remainingPercent);
|
|
9324
9374
|
}
|
|
9325
|
-
function authQuotaSnapshotFromRateLimit(snapshot, accountId = null) {
|
|
9375
|
+
function authQuotaSnapshotFromRateLimit(snapshot, accountId = null, quotaIdentityId = null) {
|
|
9326
9376
|
return {
|
|
9327
9377
|
capturedAtMs: Date.now(),
|
|
9328
9378
|
accountId,
|
|
9379
|
+
quotaIdentityId,
|
|
9329
9380
|
planType: snapshot.planType,
|
|
9330
9381
|
primaryWindowDurationMins: snapshot.primary?.windowDurationMins ?? null,
|
|
9331
9382
|
primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
|
|
@@ -9337,6 +9388,7 @@ function codexAuthQuotaSnapshotFromRecord(record) {
|
|
|
9337
9388
|
return {
|
|
9338
9389
|
capturedAtMs: record.capturedAtMs,
|
|
9339
9390
|
accountId: record.accountId,
|
|
9391
|
+
quotaIdentityId: record.quotaIdentityId,
|
|
9340
9392
|
planType: record.planType,
|
|
9341
9393
|
primaryWindowDurationMins: record.primaryWindowDurationMins,
|
|
9342
9394
|
primaryRemainingPercent: record.primaryRemainingPercent,
|
|
@@ -9356,6 +9408,7 @@ function mergeCodexAuthQuotaSnapshots(current, incoming) {
|
|
|
9356
9408
|
return {
|
|
9357
9409
|
...freshest,
|
|
9358
9410
|
accountId: freshest.accountId ?? older.accountId ?? null,
|
|
9411
|
+
quotaIdentityId: freshest.quotaIdentityId ?? older.quotaIdentityId ?? null,
|
|
9359
9412
|
};
|
|
9360
9413
|
}
|
|
9361
9414
|
function isFiniteCodexAuthQuotaSnapshotRecord(record) {
|
|
@@ -9404,6 +9457,7 @@ function isCodexAuthQuotaSnapshot(value) {
|
|
|
9404
9457
|
return typeof snapshot.capturedAtMs === 'number'
|
|
9405
9458
|
&& Number.isFinite(snapshot.capturedAtMs)
|
|
9406
9459
|
&& (snapshot.accountId === undefined || snapshot.accountId === null || typeof snapshot.accountId === 'string')
|
|
9460
|
+
&& (snapshot.quotaIdentityId === undefined || snapshot.quotaIdentityId === null || typeof snapshot.quotaIdentityId === 'string')
|
|
9407
9461
|
&& (snapshot.planType === undefined || isNullableString(snapshot.planType))
|
|
9408
9462
|
&& (snapshot.primaryWindowDurationMins === undefined || isNullableFiniteNumber(snapshot.primaryWindowDurationMins))
|
|
9409
9463
|
&& isNullableFiniteNumber(snapshot.primaryRemainingPercent)
|
|
@@ -9414,6 +9468,7 @@ function normalizeCodexAuthQuotaSnapshot(snapshot) {
|
|
|
9414
9468
|
return {
|
|
9415
9469
|
capturedAtMs: snapshot.capturedAtMs,
|
|
9416
9470
|
accountId: snapshot.accountId ?? null,
|
|
9471
|
+
quotaIdentityId: snapshot.quotaIdentityId ?? snapshot.accountId ?? null,
|
|
9417
9472
|
planType: snapshot.planType ?? null,
|
|
9418
9473
|
primaryWindowDurationMins: snapshot.primaryWindowDurationMins ?? null,
|
|
9419
9474
|
primaryRemainingPercent: snapshot.primaryRemainingPercent,
|
|
@@ -9421,6 +9476,15 @@ function normalizeCodexAuthQuotaSnapshot(snapshot) {
|
|
|
9421
9476
|
secondaryRemainingPercent: snapshot.secondaryRemainingPercent,
|
|
9422
9477
|
};
|
|
9423
9478
|
}
|
|
9479
|
+
function chatGptAuthMetadataCompatible(left, right) {
|
|
9480
|
+
if (left.accountId !== right.accountId) {
|
|
9481
|
+
return false;
|
|
9482
|
+
}
|
|
9483
|
+
if (left.quotaIdentityId === left.accountId || right.quotaIdentityId === right.accountId) {
|
|
9484
|
+
return true;
|
|
9485
|
+
}
|
|
9486
|
+
return left.quotaIdentityId === right.quotaIdentityId;
|
|
9487
|
+
}
|
|
9424
9488
|
function isNullableFiniteNumber(value) {
|
|
9425
9489
|
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
9426
9490
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -143,8 +143,8 @@ declare const MESSAGES: {
|
|
|
143
143
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
144
144
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
145
145
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
146
|
-
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]";
|
|
147
|
-
readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>";
|
|
146
|
+
readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]";
|
|
147
|
+
readonly usage_auth_sync: "Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>";
|
|
148
148
|
readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
|
|
149
149
|
readonly auth_list_title: "Codex auth files:";
|
|
150
150
|
readonly auth_bot: "Bot runtime: {value}";
|
|
@@ -212,6 +212,8 @@ declare const MESSAGES: {
|
|
|
212
212
|
readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
|
|
213
213
|
readonly auth_sync_test_missing: "Missing replies: {value}";
|
|
214
214
|
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
215
|
+
readonly auth_sync_safe_starting: "Safely syncing auth across local bot runtimes and cross-node peers...";
|
|
216
|
+
readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
|
|
215
217
|
readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
|
|
216
218
|
readonly auth_sync_status_title: "Cross-node auth sync:";
|
|
217
219
|
readonly auth_sync_status_node: "Node: {value}";
|
|
@@ -235,6 +237,7 @@ declare const MESSAGES: {
|
|
|
235
237
|
readonly auth_sync_trace_missing: "Usage: /auth sync trace <requestId>";
|
|
236
238
|
readonly button_login_device: "🔑 Login";
|
|
237
239
|
readonly button_auth_reload: "🔄 Reload auth";
|
|
240
|
+
readonly button_auth_safe_sync: "🧷 Safe sync";
|
|
238
241
|
readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
|
|
239
242
|
readonly button_auth_enable: "✅";
|
|
240
243
|
readonly button_auth_disable: "⏸️";
|
|
@@ -796,8 +799,8 @@ declare const MESSAGES: {
|
|
|
796
799
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
797
800
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
798
801
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
799
|
-
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]";
|
|
800
|
-
readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>";
|
|
802
|
+
readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]";
|
|
803
|
+
readonly usage_auth_sync: "用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>";
|
|
801
804
|
readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
|
|
802
805
|
readonly auth_list_title: "Codex auth 文件:";
|
|
803
806
|
readonly auth_bot: "Bot runtime:{value}";
|
|
@@ -865,6 +868,8 @@ declare const MESSAGES: {
|
|
|
865
868
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
866
869
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
867
870
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
871
|
+
readonly auth_sync_safe_starting: "正在安全同步本机多 bot runtime 和跨节点 auth...";
|
|
872
|
+
readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
|
|
868
873
|
readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
|
|
869
874
|
readonly auth_sync_status_title: "跨节点 auth 同步:";
|
|
870
875
|
readonly auth_sync_status_node: "节点:{value}";
|
|
@@ -888,6 +893,7 @@ declare const MESSAGES: {
|
|
|
888
893
|
readonly auth_sync_trace_missing: "用法:/auth sync trace <requestId>";
|
|
889
894
|
readonly button_login_device: "🔑 设备登录";
|
|
890
895
|
readonly button_auth_reload: "🔄 重载 auth";
|
|
896
|
+
readonly button_auth_safe_sync: "🧷 安全同步";
|
|
891
897
|
readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
|
|
892
898
|
readonly button_auth_enable: "✅";
|
|
893
899
|
readonly button_auth_disable: "⏸️";
|
package/dist/i18n.js
CHANGED
|
@@ -141,8 +141,8 @@ const MESSAGES = {
|
|
|
141
141
|
auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
|
|
142
142
|
auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
|
|
143
143
|
auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
144
|
-
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]',
|
|
145
|
-
usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|push all>',
|
|
144
|
+
usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <name>]',
|
|
145
|
+
usage_auth_sync: 'Usage: /auth sync <status|events [filter]|trace <requestId>|test|safe|push all>',
|
|
146
146
|
usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
|
|
147
147
|
auth_list_title: 'Codex auth files:',
|
|
148
148
|
auth_bot: 'Bot runtime: {value}',
|
|
@@ -210,6 +210,8 @@ const MESSAGES = {
|
|
|
210
210
|
auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
|
|
211
211
|
auth_sync_test_missing: 'Missing replies: {value}',
|
|
212
212
|
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
213
|
+
auth_sync_safe_starting: 'Safely syncing auth across local bot runtimes and cross-node peers...',
|
|
214
|
+
auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
|
|
213
215
|
auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
|
|
214
216
|
auth_sync_status_title: 'Cross-node auth sync:',
|
|
215
217
|
auth_sync_status_node: 'Node: {value}',
|
|
@@ -233,6 +235,7 @@ const MESSAGES = {
|
|
|
233
235
|
auth_sync_trace_missing: 'Usage: /auth sync trace <requestId>',
|
|
234
236
|
button_login_device: '🔑 Login',
|
|
235
237
|
button_auth_reload: '🔄 Reload auth',
|
|
238
|
+
button_auth_safe_sync: '🧷 Safe sync',
|
|
236
239
|
button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
|
|
237
240
|
button_auth_enable: '✅',
|
|
238
241
|
button_auth_disable: '⏸️',
|
|
@@ -794,8 +797,8 @@ const MESSAGES = {
|
|
|
794
797
|
auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
|
|
795
798
|
auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
|
|
796
799
|
auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
|
|
797
|
-
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]',
|
|
798
|
-
usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|push all>',
|
|
800
|
+
usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|safe|push all>|add <名称>]',
|
|
801
|
+
usage_auth_sync: '用法:/auth sync <status|events [过滤]|trace <requestId>|test|safe|push all>',
|
|
799
802
|
usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
|
|
800
803
|
auth_list_title: 'Codex auth 文件:',
|
|
801
804
|
auth_bot: 'Bot runtime:{value}',
|
|
@@ -863,6 +866,8 @@ const MESSAGES = {
|
|
|
863
866
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
864
867
|
auth_sync_test_missing: '未回应:{value}',
|
|
865
868
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
869
|
+
auth_sync_safe_starting: '正在安全同步本机多 bot runtime 和跨节点 auth...',
|
|
870
|
+
auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
|
|
866
871
|
auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
|
|
867
872
|
auth_sync_status_title: '跨节点 auth 同步:',
|
|
868
873
|
auth_sync_status_node: '节点:{value}',
|
|
@@ -886,6 +891,7 @@ const MESSAGES = {
|
|
|
886
891
|
auth_sync_trace_missing: '用法:/auth sync trace <requestId>',
|
|
887
892
|
button_login_device: '🔑 设备登录',
|
|
888
893
|
button_auth_reload: '🔄 重载 auth',
|
|
894
|
+
button_auth_safe_sync: '🧷 安全同步',
|
|
889
895
|
button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',
|
|
890
896
|
button_auth_enable: '✅',
|
|
891
897
|
button_auth_disable: '⏸️',
|
package/dist/main.js
CHANGED
|
@@ -334,6 +334,7 @@ async function runServeCli() {
|
|
|
334
334
|
?? await mirror.readNewestCandidate(candidateName);
|
|
335
335
|
return await authSync?.requestRecovery(candidateName, {
|
|
336
336
|
accountId: current?.accountId ?? null,
|
|
337
|
+
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
337
338
|
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
338
339
|
}) ?? false;
|
|
339
340
|
},
|
|
@@ -342,6 +343,16 @@ async function runServeCli() {
|
|
|
342
343
|
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
343
344
|
?? localAuthRefreshLease.release(leaseId),
|
|
344
345
|
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
346
|
+
authSyncSafeAll: async () => {
|
|
347
|
+
const local = await mirror.syncAllRuntimeCandidates();
|
|
348
|
+
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
349
|
+
return {
|
|
350
|
+
localSynced: local.synced,
|
|
351
|
+
localSkipped: local.skipped,
|
|
352
|
+
sent: remote.sent,
|
|
353
|
+
skipped: remote.skipped,
|
|
354
|
+
};
|
|
355
|
+
},
|
|
345
356
|
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
346
357
|
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
347
358
|
statusUpdated: () => writeAggregateStatus(),
|
|
@@ -492,6 +503,7 @@ async function runServeCli() {
|
|
|
492
503
|
?? null;
|
|
493
504
|
return await singleAuthSync?.requestRecovery(candidateName, {
|
|
494
505
|
accountId: current?.accountId ?? null,
|
|
506
|
+
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
495
507
|
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
496
508
|
}) ?? false;
|
|
497
509
|
},
|
|
@@ -500,6 +512,16 @@ async function runServeCli() {
|
|
|
500
512
|
releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
|
|
501
513
|
?? singleLocalAuthRefreshLease.release(leaseId),
|
|
502
514
|
getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
|
|
515
|
+
authSyncSafeAll: async () => {
|
|
516
|
+
const local = await singleMirror?.syncAllRuntimeCandidates() ?? { synced: 0, skipped: 0 };
|
|
517
|
+
const remote = await singleAuthSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
518
|
+
return {
|
|
519
|
+
localSynced: local.synced,
|
|
520
|
+
localSkipped: local.skipped,
|
|
521
|
+
sent: remote.sent,
|
|
522
|
+
skipped: remote.skipped,
|
|
523
|
+
};
|
|
524
|
+
},
|
|
503
525
|
authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
504
526
|
authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
505
527
|
statusUpdated: (status) => {
|
package/dist/store/database.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface CodexAuthQuotaSnapshotRecord {
|
|
|
28
28
|
runtimeId: string;
|
|
29
29
|
candidateName: string;
|
|
30
30
|
accountId: string;
|
|
31
|
+
quotaIdentityId: string;
|
|
31
32
|
capturedAtMs: number;
|
|
32
33
|
planType: string | null;
|
|
33
34
|
primaryWindowDurationMins: number | null;
|
|
@@ -117,7 +118,7 @@ export declare class BridgeStore {
|
|
|
117
118
|
setWeixinContextToken(scopeId: string, contextToken: string): void;
|
|
118
119
|
listDisabledCodexAuthCandidateNames(runtimeId?: string): Set<string>;
|
|
119
120
|
setCodexAuthCandidateDisabled(name: string, disabled: boolean, runtimeId?: string): void;
|
|
120
|
-
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
121
|
-
listCodexAuthQuotaSnapshots(
|
|
121
|
+
setCodexAuthQuotaSnapshot(runtimeId: string, candidateName: string, accountId: string, quotaIdentityId: string, snapshot: Pick<CodexAuthQuotaSnapshotRecord, 'capturedAtMs' | 'planType' | 'primaryWindowDurationMins' | 'primaryRemainingPercent' | 'secondaryWindowDurationMins' | 'secondaryRemainingPercent'>): void;
|
|
122
|
+
listCodexAuthQuotaSnapshots(quotaIdentityIds: string[]): CodexAuthQuotaSnapshotRecord[];
|
|
122
123
|
private ensureColumn;
|
|
123
124
|
}
|
package/dist/store/database.js
CHANGED
|
@@ -182,6 +182,7 @@ export class BridgeStore {
|
|
|
182
182
|
runtime_id TEXT NOT NULL,
|
|
183
183
|
candidate_name TEXT NOT NULL,
|
|
184
184
|
account_id TEXT NOT NULL,
|
|
185
|
+
quota_identity_id TEXT NOT NULL DEFAULT '',
|
|
185
186
|
captured_at_ms INTEGER NOT NULL,
|
|
186
187
|
plan_type TEXT,
|
|
187
188
|
primary_window_duration_mins REAL,
|
|
@@ -209,6 +210,16 @@ export class BridgeStore {
|
|
|
209
210
|
this.ensureColumn('codex_auth_quota_snapshots', 'plan_type', 'TEXT');
|
|
210
211
|
this.ensureColumn('codex_auth_quota_snapshots', 'primary_window_duration_mins', 'REAL');
|
|
211
212
|
this.ensureColumn('codex_auth_quota_snapshots', 'secondary_window_duration_mins', 'REAL');
|
|
213
|
+
this.ensureColumn('codex_auth_quota_snapshots', 'quota_identity_id', "TEXT NOT NULL DEFAULT ''");
|
|
214
|
+
this.db.prepare(`
|
|
215
|
+
UPDATE codex_auth_quota_snapshots
|
|
216
|
+
SET quota_identity_id = account_id
|
|
217
|
+
WHERE quota_identity_id = ''
|
|
218
|
+
`).run();
|
|
219
|
+
this.db.exec(`
|
|
220
|
+
CREATE INDEX IF NOT EXISTS codex_auth_quota_snapshots_quota_identity_idx
|
|
221
|
+
ON codex_auth_quota_snapshots(quota_identity_id);
|
|
222
|
+
`);
|
|
212
223
|
migrateLegacyBridgeScopeIds(this.db);
|
|
213
224
|
}
|
|
214
225
|
getTelegramOffset(botKey) {
|
|
@@ -839,12 +850,13 @@ export class BridgeStore {
|
|
|
839
850
|
ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
|
|
840
851
|
`).run(name, disabled ? 1 : 0, Date.now());
|
|
841
852
|
}
|
|
842
|
-
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, snapshot) {
|
|
853
|
+
setCodexAuthQuotaSnapshot(runtimeId, candidateName, accountId, quotaIdentityId, snapshot) {
|
|
843
854
|
this.db.prepare(`
|
|
844
855
|
INSERT INTO codex_auth_quota_snapshots (
|
|
845
856
|
runtime_id,
|
|
846
857
|
candidate_name,
|
|
847
858
|
account_id,
|
|
859
|
+
quota_identity_id,
|
|
848
860
|
captured_at_ms,
|
|
849
861
|
plan_type,
|
|
850
862
|
primary_window_duration_mins,
|
|
@@ -853,9 +865,10 @@ export class BridgeStore {
|
|
|
853
865
|
secondary_remaining_percent,
|
|
854
866
|
updated_at
|
|
855
867
|
)
|
|
856
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
868
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
857
869
|
ON CONFLICT(runtime_id, candidate_name) DO UPDATE SET
|
|
858
870
|
account_id = excluded.account_id,
|
|
871
|
+
quota_identity_id = excluded.quota_identity_id,
|
|
859
872
|
captured_at_ms = excluded.captured_at_ms,
|
|
860
873
|
plan_type = excluded.plan_type,
|
|
861
874
|
primary_window_duration_mins = excluded.primary_window_duration_mins,
|
|
@@ -863,19 +876,20 @@ export class BridgeStore {
|
|
|
863
876
|
secondary_window_duration_mins = excluded.secondary_window_duration_mins,
|
|
864
877
|
secondary_remaining_percent = excluded.secondary_remaining_percent,
|
|
865
878
|
updated_at = excluded.updated_at
|
|
866
|
-
`).run(runtimeId, candidateName, accountId, snapshot.capturedAtMs, snapshot.planType, snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, Date.now());
|
|
879
|
+
`).run(runtimeId, candidateName, accountId, quotaIdentityId, snapshot.capturedAtMs, snapshot.planType, snapshot.primaryWindowDurationMins, snapshot.primaryRemainingPercent, snapshot.secondaryWindowDurationMins, snapshot.secondaryRemainingPercent, Date.now());
|
|
867
880
|
}
|
|
868
|
-
listCodexAuthQuotaSnapshots(
|
|
869
|
-
const
|
|
870
|
-
if (
|
|
881
|
+
listCodexAuthQuotaSnapshots(quotaIdentityIds) {
|
|
882
|
+
const uniqueQuotaIdentityIds = [...new Set(quotaIdentityIds.filter(Boolean))];
|
|
883
|
+
if (uniqueQuotaIdentityIds.length === 0) {
|
|
871
884
|
return [];
|
|
872
885
|
}
|
|
873
|
-
const placeholders =
|
|
886
|
+
const placeholders = uniqueQuotaIdentityIds.map(() => '?').join(', ');
|
|
874
887
|
const rows = this.db.prepare(`
|
|
875
888
|
SELECT
|
|
876
889
|
runtime_id,
|
|
877
890
|
candidate_name,
|
|
878
891
|
account_id,
|
|
892
|
+
quota_identity_id,
|
|
879
893
|
captured_at_ms,
|
|
880
894
|
plan_type,
|
|
881
895
|
primary_window_duration_mins,
|
|
@@ -884,12 +898,13 @@ export class BridgeStore {
|
|
|
884
898
|
secondary_remaining_percent,
|
|
885
899
|
updated_at
|
|
886
900
|
FROM codex_auth_quota_snapshots
|
|
887
|
-
WHERE
|
|
888
|
-
`).all(...
|
|
901
|
+
WHERE quota_identity_id IN (${placeholders})
|
|
902
|
+
`).all(...uniqueQuotaIdentityIds);
|
|
889
903
|
return rows.map(row => ({
|
|
890
904
|
runtimeId: String(row.runtime_id),
|
|
891
905
|
candidateName: String(row.candidate_name),
|
|
892
906
|
accountId: String(row.account_id),
|
|
907
|
+
quotaIdentityId: String(row.quota_identity_id),
|
|
893
908
|
capturedAtMs: Number(row.captured_at_ms),
|
|
894
909
|
planType: nullableString(row.plan_type),
|
|
895
910
|
primaryWindowDurationMins: nullableNumber(row.primary_window_duration_mins),
|
|
@@ -33,7 +33,7 @@ Safety boundaries:
|
|
|
33
33
|
- FoxClaw only accepts sync files from bots listed in `AUTH_SYNC_PEERS`.
|
|
34
34
|
- Wrong `AUTH_SYNC_KEY`, cluster, nonce, or payload validation never writes files.
|
|
35
35
|
- Remote imports wait for global local idleness, then run temporary usage validation before writing a candidate.
|
|
36
|
-
- A same-name candidate known to belong to a different account id is never overwritten.
|
|
36
|
+
- A same-name candidate known to belong to a different account id, or to a different identifiable ChatGPT user/email under the same account, is never overwritten.
|
|
37
37
|
- Sync packets do not create reply chains. FoxClaw filters by packet type, nonce, and peer allowlist to avoid bot-to-bot loops.
|
|
38
38
|
|
|
39
39
|
Telegram's official Bot Features documentation says private bot-to-bot messaging requires Bot-to-Bot Communication Mode on both sender and recipient, and it calls out loop-prevention requirements. See https://core.telegram.org/bots/features#bot-to-bot-communication
|
|
@@ -164,7 +164,7 @@ If it shows `Missing replies: @peer_bot`, Telegram delivery may have succeeded,
|
|
|
164
164
|
|
|
165
165
|
Confirm that pending imports were processed, or that the candidate exists or has a newer timestamp.
|
|
166
166
|
|
|
167
|
-
Note: `/auth sync push all` saying “sent” only means this node successfully handed encrypted packages to Telegram. It does not prove the peer wrote files. The peer imports only when it is globally idle, usage validation succeeds, same-name candidates belong to the same account id, and the remote `last_refresh` is newer than the local copy. If the local file is already equal or newer, it will not change and `Last import` may remain empty.
|
|
167
|
+
Note: `/auth sync push all` saying “sent” only means this node successfully handed encrypted packages to Telegram. It does not prove the peer wrote files. The peer imports only when it is globally idle, usage validation succeeds, same-name candidates belong to the same account id and compatible ChatGPT user/email identity, and the remote `last_refresh` is newer than the local copy. If the local file is already equal or newer, it will not change and `Last import` may remain empty.
|
|
168
168
|
|
|
169
169
|
When cross-node sync is enabled, the contact bot private chat receives node-level notifications: local auth updates and the peers being contacted, received remote bundles and whether they were queued or immediately validated, import success/skip/failure reasons, recovery peer queries and peer replies, and a manual-intervention notice when every peer lacks an importable copy. Notifications never include auth contents, tokens, or encrypted bundle payloads.
|
|
170
170
|
|
|
@@ -198,7 +198,7 @@ With cross-node sync enabled, this command first requests a cross-node refresh l
|
|
|
198
198
|
|
|
199
199
|
- The local node may not be globally idle. Active turns, approvals, inputs, login flows, and mirror writes make imports wait.
|
|
200
200
|
- Usage validation failure rejects the write.
|
|
201
|
-
- Same-name candidates from different account ids are refused.
|
|
201
|
+
- Same-name candidates from different account ids, or from different identifiable ChatGPT users/emails under the same account, are refused.
|
|
202
202
|
- Run `/auth sync events <candidate>` or `/auth sync trace <requestId>` to see the receive, validation, skip, or failure records kept by FoxClaw.
|
|
203
203
|
|
|
204
204
|
**Should I periodically run `/auth refresh all confirm` as keepalive?**
|