@foxden-app/foxclaw 0.5.23 → 0.5.25
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 +22 -0
- package/dist/controller/controller.d.ts +6 -3
- package/dist/controller/controller.js +190 -59
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +6 -0
- package/dist/main.js +25 -0
- package/dist/types.d.ts +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.25 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- FoxClaw 重启后会优先重新接管 Codex app-server 中仍在运行的 live turn,包括 turn id 已变化但线程仍有 live turn 的情况,避免误开第二个续跑任务。
|
|
9
|
+
- 如果重启前记录的活动 turn 已被 app-server 中断且没有完成结果,FoxClaw 会自动在同一线程启动一个“继续中断工作”的新 turn,并复用原 Telegram 状态卡继续更新,不再要求用户手动发送“继续”。
|
|
10
|
+
- 如果旧 turn 已经完成,FoxClaw 只收尾旧状态卡,不会误触发自动续跑。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- After a FoxClaw restart, live Codex app-server turns are reattached first, including cases where the live turn id changed but the thread still has an active turn, avoiding a duplicate resume turn.
|
|
14
|
+
- If the previously tracked active turn was interrupted by the app-server restart and has no completed result, FoxClaw automatically starts a continuation turn in the same thread and reuses the existing Telegram status card.
|
|
15
|
+
- If the old turn already completed, FoxClaw only retires the old status card and does not auto-resume it.
|
|
16
|
+
|
|
17
|
+
## 0.5.24 - 2026-06-09
|
|
18
|
+
|
|
19
|
+
### 中文
|
|
20
|
+
- 后台主动 auth 刷新不再向私聊推送开始、跳过、完成或失败消息;最近一次刷新状态会写入 runtime status,并可在 `/status` 和 `/auth sync status` 主动查看。
|
|
21
|
+
- 本机 auth mirror 和跨节点 auth sync 的刷新/导入/同步摘要也默认静默,不再推送 `auth 刷新/同步汇总` 或候选已同步提示,减少后台维护对注意力的打扰。
|
|
22
|
+
|
|
23
|
+
### English
|
|
24
|
+
- Background proactive auth refresh no longer pushes private start, skipped, completed, or failed messages. The latest refresh state is stored in runtime status and can be checked with `/status` and `/auth sync status`.
|
|
25
|
+
- Same-node auth mirroring and cross-node auth sync refresh/import/sync summaries are quiet by default, so background maintenance no longer pushes auth refresh/sync summaries or per-candidate synced notices.
|
|
26
|
+
|
|
5
27
|
## 0.5.23 - 2026-06-09
|
|
6
28
|
|
|
7
29
|
### 中文
|
|
@@ -6,6 +6,7 @@ import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js'
|
|
|
6
6
|
import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
|
|
7
7
|
import type { CodexAppClient } from '../codex_app/client.js';
|
|
8
8
|
import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
|
|
9
|
+
type AuthProactiveRefreshStatus = NonNullable<RuntimeStatus['authProactiveRefresh']>;
|
|
9
10
|
export interface CoreCoordinator {
|
|
10
11
|
canSelfUpdate?: () => boolean;
|
|
11
12
|
authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
@@ -41,6 +42,7 @@ export interface CoreCoordinator {
|
|
|
41
42
|
weixinRuntime?: RuntimeStatus['weixinRuntime'];
|
|
42
43
|
authMirror?: RuntimeStatus['authMirror'];
|
|
43
44
|
authSync?: RuntimeStatus['authSync'];
|
|
45
|
+
authProactiveRefresh?: AuthProactiveRefreshStatus | null;
|
|
44
46
|
lastUpdate?: SelfUpdateStatus | null;
|
|
45
47
|
}>;
|
|
46
48
|
selfUpdateCompleted?: (status: SelfUpdateStatus) => void;
|
|
@@ -89,7 +91,7 @@ export declare class BridgeSessionCore {
|
|
|
89
91
|
private selfUpdatePollTimer;
|
|
90
92
|
private proactiveAuthRefreshTimer;
|
|
91
93
|
private proactiveAuthRefreshInProgress;
|
|
92
|
-
private
|
|
94
|
+
private proactiveAuthRefreshStatus;
|
|
93
95
|
private attachedThreads;
|
|
94
96
|
private botUsername;
|
|
95
97
|
private lastError;
|
|
@@ -290,8 +292,7 @@ export declare class BridgeSessionCore {
|
|
|
290
292
|
private scheduleProactiveAuthRefresh;
|
|
291
293
|
private clearProactiveAuthRefreshTimer;
|
|
292
294
|
private runProactiveAuthRefresh;
|
|
293
|
-
private
|
|
294
|
-
private notifyProactiveAuthRefresh;
|
|
295
|
+
private recordProactiveAuthRefreshStatus;
|
|
295
296
|
private handleAuthCommand;
|
|
296
297
|
private handleAuthSyncCommand;
|
|
297
298
|
private runAuthSafeSyncAll;
|
|
@@ -418,6 +419,8 @@ export declare class BridgeSessionCore {
|
|
|
418
419
|
private syncTurnStream;
|
|
419
420
|
private cleanupStaleTurnPreviews;
|
|
420
421
|
private recoverLiveTurnPreview;
|
|
422
|
+
private attachRecoveredTurnPreview;
|
|
423
|
+
private resumeInterruptedTurnAfterRestart;
|
|
421
424
|
private interruptOrphanWaitingUserInput;
|
|
422
425
|
private cleanupFinishedPreview;
|
|
423
426
|
private cleanupStaleInterruptButton;
|
|
@@ -139,7 +139,7 @@ export class BridgeSessionCore {
|
|
|
139
139
|
selfUpdatePollTimer = null;
|
|
140
140
|
proactiveAuthRefreshTimer = null;
|
|
141
141
|
proactiveAuthRefreshInProgress = false;
|
|
142
|
-
|
|
142
|
+
proactiveAuthRefreshStatus = null;
|
|
143
143
|
attachedThreads = new Set();
|
|
144
144
|
botUsername = null;
|
|
145
145
|
lastError = null;
|
|
@@ -284,6 +284,7 @@ export class BridgeSessionCore {
|
|
|
284
284
|
telegram: this.ownsTelegramRuntime,
|
|
285
285
|
weixin: Boolean(this.config.wxEnabled && this.messaging.hasWeixinTransport),
|
|
286
286
|
},
|
|
287
|
+
authProactiveRefresh: this.proactiveAuthRefreshStatus,
|
|
287
288
|
};
|
|
288
289
|
}
|
|
289
290
|
async handleText(event) {
|
|
@@ -478,6 +479,11 @@ export class BridgeSessionCore {
|
|
|
478
479
|
}));
|
|
479
480
|
}
|
|
480
481
|
}
|
|
482
|
+
if (serviceStatus.authProactiveRefresh) {
|
|
483
|
+
lines.push(t(locale, 'status_auth_proactive_refresh', {
|
|
484
|
+
value: formatAuthProactiveRefreshStatus(locale, serviceStatus.authProactiveRefresh),
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
481
487
|
if (serviceStatus.lastUpdate) {
|
|
482
488
|
lines.push(t(locale, 'status_last_update', {
|
|
483
489
|
from: serviceStatus.lastUpdate.fromVersion,
|
|
@@ -4423,65 +4429,72 @@ export class BridgeSessionCore {
|
|
|
4423
4429
|
return;
|
|
4424
4430
|
}
|
|
4425
4431
|
this.proactiveAuthRefreshInProgress = true;
|
|
4426
|
-
const
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4432
|
+
const startedAt = new Date().toISOString();
|
|
4433
|
+
const dueCandidateNames = dueCandidates.map(candidate => candidate.name);
|
|
4434
|
+
this.recordProactiveAuthRefreshStatus({
|
|
4435
|
+
state: 'running',
|
|
4436
|
+
startedAt,
|
|
4437
|
+
finishedAt: null,
|
|
4438
|
+
candidates: dueCandidateNames,
|
|
4439
|
+
refreshed: 0,
|
|
4440
|
+
skipped: 0,
|
|
4441
|
+
failed: 0,
|
|
4442
|
+
error: null,
|
|
4443
|
+
details: [],
|
|
4444
|
+
});
|
|
4430
4445
|
let lease;
|
|
4431
4446
|
try {
|
|
4432
|
-
lease = await this.coordinator?.acquireAuthRefreshLease?.(`proactive auth refresh: ${
|
|
4447
|
+
lease = await this.coordinator?.acquireAuthRefreshLease?.(`proactive auth refresh: ${dueCandidateNames.join(', ')}`);
|
|
4433
4448
|
if (lease && !lease.ok) {
|
|
4434
4449
|
this.logger.warn('codex.auth_proactive_refresh_lease_failed', { reason: lease.reason });
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4450
|
+
this.recordProactiveAuthRefreshStatus({
|
|
4451
|
+
state: 'lease_failed',
|
|
4452
|
+
startedAt,
|
|
4453
|
+
finishedAt: new Date().toISOString(),
|
|
4454
|
+
candidates: dueCandidateNames,
|
|
4455
|
+
refreshed: 0,
|
|
4456
|
+
skipped: 0,
|
|
4457
|
+
failed: 0,
|
|
4458
|
+
error: lease.reason ?? 'unknown',
|
|
4459
|
+
details: [],
|
|
4460
|
+
});
|
|
4438
4461
|
return;
|
|
4439
4462
|
}
|
|
4440
|
-
const result = await this.refreshCodexAuthCandidates(new Set(
|
|
4441
|
-
|
|
4463
|
+
const result = await this.refreshCodexAuthCandidates(new Set(dueCandidateNames));
|
|
4464
|
+
this.recordProactiveAuthRefreshStatus({
|
|
4465
|
+
state: 'completed',
|
|
4466
|
+
startedAt,
|
|
4467
|
+
finishedAt: new Date().toISOString(),
|
|
4468
|
+
candidates: dueCandidateNames,
|
|
4469
|
+
refreshed: result.refreshed.length,
|
|
4470
|
+
skipped: result.skipped.length,
|
|
4471
|
+
failed: result.failed.length,
|
|
4472
|
+
error: result.failed.length > 0 ? `${result.failed.length} failed` : null,
|
|
4473
|
+
details: result.failed.slice(0, 5).map(failure => `${failure.name}: ${failure.error}`),
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
catch (error) {
|
|
4477
|
+
this.recordProactiveAuthRefreshStatus({
|
|
4478
|
+
state: 'failed',
|
|
4479
|
+
startedAt,
|
|
4480
|
+
finishedAt: new Date().toISOString(),
|
|
4481
|
+
candidates: dueCandidateNames,
|
|
4482
|
+
refreshed: 0,
|
|
4483
|
+
skipped: 0,
|
|
4484
|
+
failed: dueCandidateNames.length,
|
|
4485
|
+
error: formatUserError(error),
|
|
4486
|
+
details: [],
|
|
4487
|
+
});
|
|
4488
|
+
throw error;
|
|
4442
4489
|
}
|
|
4443
4490
|
finally {
|
|
4444
4491
|
await this.coordinator?.releaseAuthRefreshLease?.(lease?.leaseId ?? null);
|
|
4445
4492
|
this.proactiveAuthRefreshInProgress = false;
|
|
4446
4493
|
}
|
|
4447
4494
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
return 'en';
|
|
4452
|
-
const privateScope = this.store.getTelegramPrivateScope(identity);
|
|
4453
|
-
if (!privateScope)
|
|
4454
|
-
return 'en';
|
|
4455
|
-
return this.localeForChat(privateScope.scopeId);
|
|
4456
|
-
}
|
|
4457
|
-
async notifyProactiveAuthRefresh(locale, message, final = false) {
|
|
4458
|
-
const identity = this.bot.identity;
|
|
4459
|
-
if (!identity)
|
|
4460
|
-
return;
|
|
4461
|
-
const privateScope = this.store.getTelegramPrivateScope(identity);
|
|
4462
|
-
if (!privateScope)
|
|
4463
|
-
return;
|
|
4464
|
-
const previous = this.proactiveAuthRefreshStatusMessage;
|
|
4465
|
-
if (previous?.chatId === privateScope.chatId) {
|
|
4466
|
-
try {
|
|
4467
|
-
await this.bot.editMessage(privateScope.chatId, previous.messageId, message, []);
|
|
4468
|
-
if (final) {
|
|
4469
|
-
this.proactiveAuthRefreshStatusMessage = null;
|
|
4470
|
-
}
|
|
4471
|
-
return;
|
|
4472
|
-
}
|
|
4473
|
-
catch (error) {
|
|
4474
|
-
this.logger.warn('codex.auth_proactive_refresh_notify_edit_failed', { error: toErrorMeta(error) });
|
|
4475
|
-
}
|
|
4476
|
-
}
|
|
4477
|
-
await this.bot.sendMessage(privateScope.chatId, message).then((messageId) => {
|
|
4478
|
-
this.proactiveAuthRefreshStatusMessage = final ? null : { chatId: privateScope.chatId, messageId };
|
|
4479
|
-
}).catch((error) => {
|
|
4480
|
-
if (final) {
|
|
4481
|
-
this.proactiveAuthRefreshStatusMessage = null;
|
|
4482
|
-
}
|
|
4483
|
-
this.logger.warn('codex.auth_proactive_refresh_notify_failed', { error: toErrorMeta(error) });
|
|
4484
|
-
});
|
|
4495
|
+
recordProactiveAuthRefreshStatus(status) {
|
|
4496
|
+
this.proactiveAuthRefreshStatus = status;
|
|
4497
|
+
this.updateStatus();
|
|
4485
4498
|
}
|
|
4486
4499
|
async handleAuthCommand(scopeId, locale, args) {
|
|
4487
4500
|
const action = args[0]?.toLowerCase() ?? 'list';
|
|
@@ -4532,7 +4545,7 @@ export class BridgeSessionCore {
|
|
|
4532
4545
|
async handleAuthSyncCommand(scopeId, locale, args) {
|
|
4533
4546
|
const action = args[0]?.toLowerCase() ?? 'status';
|
|
4534
4547
|
if (action === 'status') {
|
|
4535
|
-
await this.sendMessage(scopeId, formatAuthSyncStatus(locale, this.coordinator?.getAuthSyncStatus?.() ?? null));
|
|
4548
|
+
await this.sendMessage(scopeId, formatAuthSyncStatus(locale, this.coordinator?.getAuthSyncStatus?.() ?? null, this.getRuntimeStatus().authProactiveRefresh ?? null));
|
|
4536
4549
|
return;
|
|
4537
4550
|
}
|
|
4538
4551
|
if (action === 'events') {
|
|
@@ -7216,16 +7229,35 @@ export class BridgeSessionCore {
|
|
|
7216
7229
|
return false;
|
|
7217
7230
|
}
|
|
7218
7231
|
const liveTurn = findLiveTurn(snapshot);
|
|
7219
|
-
if (
|
|
7220
|
-
|
|
7232
|
+
if (liveTurn) {
|
|
7233
|
+
if (liveTurn.turnId === preview.turnId
|
|
7234
|
+
&& snapshot.activeFlags.includes('waitingOnUserInput')
|
|
7235
|
+
&& !this.hasPendingUserInputForTurn(preview.scopeId, preview.turnId)) {
|
|
7236
|
+
return this.interruptOrphanWaitingUserInput(preview);
|
|
7237
|
+
}
|
|
7238
|
+
return this.attachRecoveredTurnPreview(preview, target, snapshot, liveTurn);
|
|
7239
|
+
}
|
|
7240
|
+
const previousTurn = snapshot.turns.find(turn => turn.turnId === preview.turnId) ?? null;
|
|
7241
|
+
if (previousTurn && previousTurn.status !== 'inProgress') {
|
|
7242
|
+
await this.retirePreviewMessage(preview.scopeId, preview.messageId, t(this.localeForChat(preview.scopeId), 'completed_see_reply_below'), preview.turnId);
|
|
7243
|
+
return true;
|
|
7221
7244
|
}
|
|
7222
|
-
if (
|
|
7223
|
-
|
|
7224
|
-
return this.interruptOrphanWaitingUserInput(preview);
|
|
7245
|
+
if (preview.isObserved) {
|
|
7246
|
+
return false;
|
|
7225
7247
|
}
|
|
7248
|
+
return this.resumeInterruptedTurnAfterRestart(preview, target);
|
|
7249
|
+
}
|
|
7250
|
+
async attachRecoveredTurnPreview(preview, target, snapshot, liveTurn) {
|
|
7226
7251
|
await this.stopWatchingScopeThread(preview.scopeId, preview.threadId);
|
|
7227
|
-
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId,
|
|
7228
|
-
this.setActiveTurn(preview.scopeId,
|
|
7252
|
+
const active = this.createActiveTurnState(preview.scopeId, target.chatId, target.chatType, target.topicId, preview.threadId, liveTurn.turnId, preview.messageId, preview.isObserved);
|
|
7253
|
+
this.setActiveTurn(preview.scopeId, liveTurn.turnId, active);
|
|
7254
|
+
this.store.saveActiveTurnPreview({
|
|
7255
|
+
turnId: liveTurn.turnId,
|
|
7256
|
+
scopeId: preview.scopeId,
|
|
7257
|
+
threadId: preview.threadId,
|
|
7258
|
+
messageId: preview.messageId,
|
|
7259
|
+
isObserved: preview.isObserved,
|
|
7260
|
+
});
|
|
7229
7261
|
const watcher = {
|
|
7230
7262
|
scopeId: preview.scopeId,
|
|
7231
7263
|
chatId: target.chatId,
|
|
@@ -7235,7 +7267,7 @@ export class BridgeSessionCore {
|
|
|
7235
7267
|
mode: 'app_snapshot',
|
|
7236
7268
|
timer: null,
|
|
7237
7269
|
cursor: seedObservedTurnCursor(liveTurn),
|
|
7238
|
-
activeTurnId:
|
|
7270
|
+
activeTurnId: liveTurn.turnId,
|
|
7239
7271
|
waitingOnApproval: snapshot.activeFlags.includes('waitingOnApproval'),
|
|
7240
7272
|
sessionPath: null,
|
|
7241
7273
|
sessionOffset: -1,
|
|
@@ -7250,11 +7282,59 @@ export class BridgeSessionCore {
|
|
|
7250
7282
|
this.logger.info('telegram.preview_recovered', {
|
|
7251
7283
|
scopeId: preview.scopeId,
|
|
7252
7284
|
threadId: preview.threadId,
|
|
7253
|
-
turnId:
|
|
7285
|
+
turnId: liveTurn.turnId,
|
|
7286
|
+
previousTurnId: preview.turnId,
|
|
7254
7287
|
isObserved: preview.isObserved,
|
|
7255
7288
|
});
|
|
7256
7289
|
return true;
|
|
7257
7290
|
}
|
|
7291
|
+
async resumeInterruptedTurnAfterRestart(preview, target) {
|
|
7292
|
+
const locale = this.localeForChat(preview.scopeId);
|
|
7293
|
+
const binding = {
|
|
7294
|
+
threadId: preview.threadId,
|
|
7295
|
+
cwd: this.store.getBinding(preview.scopeId)?.cwd ?? this.config.defaultCwd,
|
|
7296
|
+
};
|
|
7297
|
+
const input = [{
|
|
7298
|
+
type: 'text',
|
|
7299
|
+
text: t(locale, 'restart_auto_resume_prompt'),
|
|
7300
|
+
text_elements: [],
|
|
7301
|
+
}];
|
|
7302
|
+
try {
|
|
7303
|
+
await this.sendTyping(preview.scopeId);
|
|
7304
|
+
const turnState = await this.startTurnWithRecovery(preview.scopeId, binding, input, {
|
|
7305
|
+
recoverMissingThread: false,
|
|
7306
|
+
});
|
|
7307
|
+
if (turnState.collaborationMode === 'plan') {
|
|
7308
|
+
this.store.setChatCollaborationMode(preview.scopeId, DEFAULT_COLLABORATION_MODE);
|
|
7309
|
+
}
|
|
7310
|
+
await this.registerActiveTurn(preview.scopeId, target.chatId, target.chatType, target.topicId, turnState.threadId, turnState.turnId, preview.messageId, {
|
|
7311
|
+
input,
|
|
7312
|
+
threadId: turnState.threadId,
|
|
7313
|
+
cwd: binding.cwd,
|
|
7314
|
+
chatId: target.chatId,
|
|
7315
|
+
chatType: target.chatType,
|
|
7316
|
+
topicId: target.topicId,
|
|
7317
|
+
collaborationMode: turnState.collaborationMode,
|
|
7318
|
+
failedAuthTargets: new Set(),
|
|
7319
|
+
}, turnState.collaborationMode);
|
|
7320
|
+
this.logger.info('telegram.preview_auto_resumed_after_restart', {
|
|
7321
|
+
scopeId: preview.scopeId,
|
|
7322
|
+
threadId: preview.threadId,
|
|
7323
|
+
previousTurnId: preview.turnId,
|
|
7324
|
+
turnId: turnState.turnId,
|
|
7325
|
+
});
|
|
7326
|
+
return true;
|
|
7327
|
+
}
|
|
7328
|
+
catch (error) {
|
|
7329
|
+
this.logger.warn('telegram.preview_auto_resume_after_restart_failed', {
|
|
7330
|
+
scopeId: preview.scopeId,
|
|
7331
|
+
threadId: preview.threadId,
|
|
7332
|
+
turnId: preview.turnId,
|
|
7333
|
+
error: toErrorMeta(error),
|
|
7334
|
+
});
|
|
7335
|
+
return false;
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7258
7338
|
async interruptOrphanWaitingUserInput(preview) {
|
|
7259
7339
|
try {
|
|
7260
7340
|
await this.app.interruptTurn(preview.threadId, preview.turnId);
|
|
@@ -9180,7 +9260,7 @@ function formatAuthRefreshAllResult(locale, result, mode = 'manual') {
|
|
|
9180
9260
|
}
|
|
9181
9261
|
return lines.join('\n');
|
|
9182
9262
|
}
|
|
9183
|
-
function formatAuthSyncStatus(locale, status) {
|
|
9263
|
+
function formatAuthSyncStatus(locale, status, proactiveRefresh = null) {
|
|
9184
9264
|
if (!status?.enabled) {
|
|
9185
9265
|
return t(locale, 'auth_sync_disabled');
|
|
9186
9266
|
}
|
|
@@ -9209,6 +9289,11 @@ function formatAuthSyncStatus(locale, status) {
|
|
|
9209
9289
|
if (status.lastError) {
|
|
9210
9290
|
lines.push(t(locale, 'auth_sync_status_error', { value: status.lastError }));
|
|
9211
9291
|
}
|
|
9292
|
+
if (proactiveRefresh) {
|
|
9293
|
+
lines.push(t(locale, 'auth_sync_status_proactive_refresh', {
|
|
9294
|
+
value: formatAuthProactiveRefreshStatus(locale, proactiveRefresh),
|
|
9295
|
+
}));
|
|
9296
|
+
}
|
|
9212
9297
|
if (status.peerStatuses?.length) {
|
|
9213
9298
|
lines.push(t(locale, 'auth_sync_status_peer_activity'));
|
|
9214
9299
|
for (const peer of status.peerStatuses.slice(0, 5)) {
|
|
@@ -9239,6 +9324,52 @@ function formatAuthSyncStatus(locale, status) {
|
|
|
9239
9324
|
}
|
|
9240
9325
|
return lines.join('\n');
|
|
9241
9326
|
}
|
|
9327
|
+
function formatAuthProactiveRefreshStatus(locale, status) {
|
|
9328
|
+
const time = status.finishedAt ?? status.startedAt;
|
|
9329
|
+
const candidates = formatLimitedList(status.candidates, 3);
|
|
9330
|
+
const state = formatAuthProactiveRefreshState(locale, status.state);
|
|
9331
|
+
const result = locale === 'zh'
|
|
9332
|
+
? `已刷新 ${status.refreshed},已跳过 ${status.skipped},失败 ${status.failed}`
|
|
9333
|
+
: `refreshed ${status.refreshed}, skipped ${status.skipped}, failed ${status.failed}`;
|
|
9334
|
+
const parts = [
|
|
9335
|
+
`${state} @ ${time}`,
|
|
9336
|
+
locale === 'zh' ? `候选 ${candidates}` : `candidates ${candidates}`,
|
|
9337
|
+
result,
|
|
9338
|
+
];
|
|
9339
|
+
if (status.error) {
|
|
9340
|
+
parts.push(locale === 'zh'
|
|
9341
|
+
? `原因 ${formatShortStatusError(status.error)}`
|
|
9342
|
+
: `reason ${formatShortStatusError(status.error)}`);
|
|
9343
|
+
}
|
|
9344
|
+
if (status.details.length > 0) {
|
|
9345
|
+
parts.push(locale === 'zh'
|
|
9346
|
+
? `明细 ${formatLimitedList(status.details.map(formatShortStatusError), 2)}`
|
|
9347
|
+
: `details ${formatLimitedList(status.details.map(formatShortStatusError), 2)}`);
|
|
9348
|
+
}
|
|
9349
|
+
return parts.join('; ');
|
|
9350
|
+
}
|
|
9351
|
+
function formatAuthProactiveRefreshState(locale, state) {
|
|
9352
|
+
if (locale === 'zh') {
|
|
9353
|
+
switch (state) {
|
|
9354
|
+
case 'running': return '进行中';
|
|
9355
|
+
case 'completed': return '已完成';
|
|
9356
|
+
case 'lease_failed': return '锁未授予';
|
|
9357
|
+
case 'failed': return '失败';
|
|
9358
|
+
}
|
|
9359
|
+
}
|
|
9360
|
+
switch (state) {
|
|
9361
|
+
case 'running': return 'running';
|
|
9362
|
+
case 'completed': return 'completed';
|
|
9363
|
+
case 'lease_failed': return 'lease not granted';
|
|
9364
|
+
case 'failed': return 'failed';
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
function formatLimitedList(values, limit) {
|
|
9368
|
+
if (values.length === 0)
|
|
9369
|
+
return '-';
|
|
9370
|
+
const shown = values.slice(0, limit).join(', ');
|
|
9371
|
+
return values.length > limit ? `${shown}, +${values.length - limit}` : shown;
|
|
9372
|
+
}
|
|
9242
9373
|
function formatAuthSyncEvents(locale, status, filter) {
|
|
9243
9374
|
if (!status?.enabled) {
|
|
9244
9375
|
return t(locale, 'auth_sync_disabled');
|
package/dist/i18n.d.ts
CHANGED
|
@@ -129,6 +129,7 @@ declare const MESSAGES: {
|
|
|
129
129
|
readonly status_auth_sync: "Cross-node auth sync: node {node}, contact {contact}, peers {peers}, pending imports {pending}";
|
|
130
130
|
readonly status_auth_sync_error: "Cross-node auth sync error: {value}";
|
|
131
131
|
readonly status_auth_sync_candidate_failures: "Auth sync candidate failures: {value}";
|
|
132
|
+
readonly status_auth_proactive_refresh: "Recent proactive auth refresh: {value}";
|
|
132
133
|
readonly status_last_update_none: "Last service update: none recorded";
|
|
133
134
|
readonly status_last_update: "Last service update: {from} -> {to} at {time}";
|
|
134
135
|
readonly status_last_codex_update: "Last Codex update: {value}";
|
|
@@ -255,6 +256,7 @@ declare const MESSAGES: {
|
|
|
255
256
|
readonly auth_sync_status_pull: "Last pull: {value}";
|
|
256
257
|
readonly auth_sync_status_lease: "Active refresh lease: {value}";
|
|
257
258
|
readonly auth_sync_status_error: "Last error: {value}";
|
|
259
|
+
readonly auth_sync_status_proactive_refresh: "Recent proactive refresh: {value}";
|
|
258
260
|
readonly auth_sync_status_candidate_failures: "Candidate failures:";
|
|
259
261
|
readonly auth_sync_status_candidate_failure: "- {candidate}: {reason} (source {source}, peer {peer}, at {time})";
|
|
260
262
|
readonly auth_sync_status_peer_activity: "Peer activity:";
|
|
@@ -416,6 +418,7 @@ declare const MESSAGES: {
|
|
|
416
418
|
readonly stale_preview_restarted: "The bridge restarted while turn {threadId} was still live. This status card is no longer live. Open the thread in Codex.app or send another message to continue.";
|
|
417
419
|
readonly stale_preview_expired: "This live preview expired after the bridge stopped tracking the turn. If a final reply arrived, see below. Otherwise send the message again.";
|
|
418
420
|
readonly stale_user_input_interrupted: "The bridge found a stale Codex input request for thread {threadId} with no recoverable Telegram question. It interrupted that stuck turn; send the message again to continue.";
|
|
421
|
+
readonly restart_auto_resume_prompt: "FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off.";
|
|
419
422
|
readonly streaming_reply_below: "Streaming reply below...";
|
|
420
423
|
readonly interrupt_requested_preview: "Interrupt requested...";
|
|
421
424
|
readonly interrupt_requested_waiting: "Interrupt requested. Waiting for Codex to stop...";
|
|
@@ -823,6 +826,7 @@ declare const MESSAGES: {
|
|
|
823
826
|
readonly status_auth_sync: "跨节点 auth 同步:节点 {node},联系人 {contact},peer {peers},待导入 {pending}";
|
|
824
827
|
readonly status_auth_sync_error: "跨节点 auth 同步错误:{value}";
|
|
825
828
|
readonly status_auth_sync_candidate_failures: "auth 同步候选失败:{value}";
|
|
829
|
+
readonly status_auth_proactive_refresh: "最近主动 auth 刷新:{value}";
|
|
826
830
|
readonly status_last_update_none: "最近服务升级:暂无记录";
|
|
827
831
|
readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
|
|
828
832
|
readonly status_last_codex_update: "最近 Codex 升级:{value}";
|
|
@@ -949,6 +953,7 @@ declare const MESSAGES: {
|
|
|
949
953
|
readonly auth_sync_status_pull: "最近拉取:{value}";
|
|
950
954
|
readonly auth_sync_status_lease: "当前刷新锁:{value}";
|
|
951
955
|
readonly auth_sync_status_error: "最近错误:{value}";
|
|
956
|
+
readonly auth_sync_status_proactive_refresh: "最近主动刷新:{value}";
|
|
952
957
|
readonly auth_sync_status_candidate_failures: "候选失败:";
|
|
953
958
|
readonly auth_sync_status_candidate_failure: "- {candidate}:{reason}(来源 {source},peer {peer},时间 {time})";
|
|
954
959
|
readonly auth_sync_status_peer_activity: "Peer 活动:";
|
|
@@ -1110,6 +1115,7 @@ declare const MESSAGES: {
|
|
|
1110
1115
|
readonly stale_preview_restarted: "桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。";
|
|
1111
1116
|
readonly stale_preview_expired: "之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。";
|
|
1112
1117
|
readonly stale_user_input_interrupted: "桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。";
|
|
1118
|
+
readonly restart_auto_resume_prompt: "FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。";
|
|
1113
1119
|
readonly streaming_reply_below: "回复正在下方持续输出...";
|
|
1114
1120
|
readonly interrupt_requested_preview: "已请求中断...";
|
|
1115
1121
|
readonly interrupt_requested_waiting: "已请求中断,正在等待 Codex 停止...";
|
package/dist/i18n.js
CHANGED
|
@@ -127,6 +127,7 @@ const MESSAGES = {
|
|
|
127
127
|
status_auth_sync: 'Cross-node auth sync: node {node}, contact {contact}, peers {peers}, pending imports {pending}',
|
|
128
128
|
status_auth_sync_error: 'Cross-node auth sync error: {value}',
|
|
129
129
|
status_auth_sync_candidate_failures: 'Auth sync candidate failures: {value}',
|
|
130
|
+
status_auth_proactive_refresh: 'Recent proactive auth refresh: {value}',
|
|
130
131
|
status_last_update_none: 'Last service update: none recorded',
|
|
131
132
|
status_last_update: 'Last service update: {from} -> {to} at {time}',
|
|
132
133
|
status_last_codex_update: 'Last Codex update: {value}',
|
|
@@ -253,6 +254,7 @@ const MESSAGES = {
|
|
|
253
254
|
auth_sync_status_pull: 'Last pull: {value}',
|
|
254
255
|
auth_sync_status_lease: 'Active refresh lease: {value}',
|
|
255
256
|
auth_sync_status_error: 'Last error: {value}',
|
|
257
|
+
auth_sync_status_proactive_refresh: 'Recent proactive refresh: {value}',
|
|
256
258
|
auth_sync_status_candidate_failures: 'Candidate failures:',
|
|
257
259
|
auth_sync_status_candidate_failure: '- {candidate}: {reason} (source {source}, peer {peer}, at {time})',
|
|
258
260
|
auth_sync_status_peer_activity: 'Peer activity:',
|
|
@@ -414,6 +416,7 @@ const MESSAGES = {
|
|
|
414
416
|
stale_preview_restarted: 'The bridge restarted while turn {threadId} was still live. This status card is no longer live. Open the thread in Codex.app or send another message to continue.',
|
|
415
417
|
stale_preview_expired: 'This live preview expired after the bridge stopped tracking the turn. If a final reply arrived, see below. Otherwise send the message again.',
|
|
416
418
|
stale_user_input_interrupted: 'The bridge found a stale Codex input request for thread {threadId} with no recoverable Telegram question. It interrupted that stuck turn; send the message again to continue.',
|
|
419
|
+
restart_auto_resume_prompt: 'FoxClaw restarted while the previous turn was running. Continue the interrupted work from where it left off.',
|
|
417
420
|
streaming_reply_below: 'Streaming reply below...',
|
|
418
421
|
interrupt_requested_preview: 'Interrupt requested...',
|
|
419
422
|
interrupt_requested_waiting: 'Interrupt requested. Waiting for Codex to stop...',
|
|
@@ -821,6 +824,7 @@ const MESSAGES = {
|
|
|
821
824
|
status_auth_sync: '跨节点 auth 同步:节点 {node},联系人 {contact},peer {peers},待导入 {pending}',
|
|
822
825
|
status_auth_sync_error: '跨节点 auth 同步错误:{value}',
|
|
823
826
|
status_auth_sync_candidate_failures: 'auth 同步候选失败:{value}',
|
|
827
|
+
status_auth_proactive_refresh: '最近主动 auth 刷新:{value}',
|
|
824
828
|
status_last_update_none: '最近服务升级:暂无记录',
|
|
825
829
|
status_last_update: '最近服务升级:{from} -> {to}({time})',
|
|
826
830
|
status_last_codex_update: '最近 Codex 升级:{value}',
|
|
@@ -947,6 +951,7 @@ const MESSAGES = {
|
|
|
947
951
|
auth_sync_status_pull: '最近拉取:{value}',
|
|
948
952
|
auth_sync_status_lease: '当前刷新锁:{value}',
|
|
949
953
|
auth_sync_status_error: '最近错误:{value}',
|
|
954
|
+
auth_sync_status_proactive_refresh: '最近主动刷新:{value}',
|
|
950
955
|
auth_sync_status_candidate_failures: '候选失败:',
|
|
951
956
|
auth_sync_status_candidate_failure: '- {candidate}:{reason}(来源 {source},peer {peer},时间 {time})',
|
|
952
957
|
auth_sync_status_peer_activity: 'Peer 活动:',
|
|
@@ -1108,6 +1113,7 @@ const MESSAGES = {
|
|
|
1108
1113
|
stale_preview_restarted: '桥接在该线程 {threadId} 仍处于活动状态时重启了。这张状态卡已经不再实时更新。你可以去 Codex.app 打开线程,或重新发一条消息继续。',
|
|
1109
1114
|
stale_preview_expired: '之前的实时预览在桥接停止跟踪这轮回复后已经失效。如果下方已有最终回复,请以最终回复为准;否则请重新发送消息。',
|
|
1110
1115
|
stale_user_input_interrupted: '桥接发现线程 {threadId} 有一条已经无法恢复到 Telegram 的 Codex 输入请求,已中断这条卡住的回复。请重新发送消息继续。',
|
|
1116
|
+
restart_auto_resume_prompt: 'FoxClaw 重启时上一轮回复仍在运行。请从中断处继续完成刚才的工作。',
|
|
1111
1117
|
streaming_reply_below: '回复正在下方持续输出...',
|
|
1112
1118
|
interrupt_requested_preview: '已请求中断...',
|
|
1113
1119
|
interrupt_requested_waiting: '已请求中断,正在等待 Codex 停止...',
|
package/dist/main.js
CHANGED
|
@@ -255,6 +255,7 @@ async function runServeCli() {
|
|
|
255
255
|
validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
|
|
256
256
|
notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
|
|
257
257
|
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
258
|
+
suppressBackgroundNotifications: () => true,
|
|
258
259
|
}),
|
|
259
260
|
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
260
261
|
onSynced: async (event) => {
|
|
@@ -279,6 +280,7 @@ async function runServeCli() {
|
|
|
279
280
|
const writeAggregateStatus = (running = true) => {
|
|
280
281
|
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
281
282
|
const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
|
|
283
|
+
const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
|
|
282
284
|
const first = statuses[0] ?? null;
|
|
283
285
|
writeRuntimeStatus(config.statusPath, {
|
|
284
286
|
running,
|
|
@@ -316,6 +318,7 @@ async function runServeCli() {
|
|
|
316
318
|
} : {}),
|
|
317
319
|
authMirror: mirror.getStatus(),
|
|
318
320
|
authSync: authSync?.getStatus() ?? null,
|
|
321
|
+
authProactiveRefresh,
|
|
319
322
|
lastUpdate: lastSelfUpdate,
|
|
320
323
|
});
|
|
321
324
|
};
|
|
@@ -384,6 +387,10 @@ async function runServeCli() {
|
|
|
384
387
|
} : {}),
|
|
385
388
|
authMirror: mirror.getStatus(),
|
|
386
389
|
authSync: authSync?.getStatus() ?? null,
|
|
390
|
+
authProactiveRefresh: newestAuthProactiveRefreshStatus([
|
|
391
|
+
...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
|
|
392
|
+
...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
|
|
393
|
+
]),
|
|
387
394
|
lastUpdate: lastSelfUpdate,
|
|
388
395
|
}),
|
|
389
396
|
selfUpdateCompleted: (status) => {
|
|
@@ -444,6 +451,7 @@ async function runServeCli() {
|
|
|
444
451
|
isIdle: authSyncLocalIdle,
|
|
445
452
|
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
446
453
|
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
454
|
+
suppressBackgroundNotifications: () => true,
|
|
447
455
|
}),
|
|
448
456
|
});
|
|
449
457
|
await authSync.initialize();
|
|
@@ -604,6 +612,7 @@ async function runServeCli() {
|
|
|
604
612
|
isIdle: singleAuthSyncLocalIdle,
|
|
605
613
|
notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
|
|
606
614
|
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
615
|
+
suppressBackgroundNotifications: () => true,
|
|
607
616
|
}),
|
|
608
617
|
});
|
|
609
618
|
await singleAuthSync.initialize();
|
|
@@ -676,6 +685,8 @@ async function runServeCli() {
|
|
|
676
685
|
}
|
|
677
686
|
function createAuthMirrorNotifier(store, botId, bot, aggregator, options = {}) {
|
|
678
687
|
return async (event) => {
|
|
688
|
+
if (options.suppressBackgroundNotifications?.())
|
|
689
|
+
return;
|
|
679
690
|
if (options.quietAuthPoolMode?.())
|
|
680
691
|
return;
|
|
681
692
|
const privateScope = store.getTelegramPrivateScope(botId);
|
|
@@ -711,6 +722,8 @@ function createAuthSyncNotifier(store, bot, aggregator, options = {}) {
|
|
|
711
722
|
});
|
|
712
723
|
};
|
|
713
724
|
return async (event) => {
|
|
725
|
+
if (options.suppressBackgroundNotifications?.())
|
|
726
|
+
return;
|
|
714
727
|
if (!bot.identity)
|
|
715
728
|
return;
|
|
716
729
|
const privateScope = store.getTelegramPrivateScope(bot.identity);
|
|
@@ -742,6 +755,18 @@ function formatAuthPoolSummary(locale, stats) {
|
|
|
742
755
|
? `auth 池:历史 ${stats.totalSeen},存活 ${stats.alive},因失效剔除 ${stats.deletedInvalid}。`
|
|
743
756
|
: `Auth pool: total seen ${stats.totalSeen}, alive ${stats.alive}, invalid-deleted ${stats.deletedInvalid}.`;
|
|
744
757
|
}
|
|
758
|
+
function newestAuthProactiveRefreshStatus(statuses) {
|
|
759
|
+
const entries = statuses
|
|
760
|
+
.map(status => status.authProactiveRefresh ?? null)
|
|
761
|
+
.filter((status) => status !== null);
|
|
762
|
+
if (entries.length === 0)
|
|
763
|
+
return null;
|
|
764
|
+
return entries.reduce((newest, status) => {
|
|
765
|
+
const newestTime = newest.finishedAt ?? newest.startedAt;
|
|
766
|
+
const statusTime = status.finishedAt ?? status.startedAt;
|
|
767
|
+
return Date.parse(statusTime) >= Date.parse(newestTime) ? status : newest;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
745
770
|
function isQuietAuthPoolNotification(event) {
|
|
746
771
|
switch (event.kind) {
|
|
747
772
|
case 'candidate_publish_started':
|
package/dist/types.d.ts
CHANGED
|
@@ -452,6 +452,17 @@ export interface RuntimeStatus {
|
|
|
452
452
|
detail: string | null;
|
|
453
453
|
}>;
|
|
454
454
|
} | null;
|
|
455
|
+
authProactiveRefresh?: {
|
|
456
|
+
state: 'running' | 'completed' | 'lease_failed' | 'failed';
|
|
457
|
+
startedAt: string;
|
|
458
|
+
finishedAt: string | null;
|
|
459
|
+
candidates: string[];
|
|
460
|
+
refreshed: number;
|
|
461
|
+
skipped: number;
|
|
462
|
+
failed: number;
|
|
463
|
+
error: string | null;
|
|
464
|
+
details: string[];
|
|
465
|
+
} | null;
|
|
455
466
|
lastUpdate?: {
|
|
456
467
|
state: string;
|
|
457
468
|
fromVersion: string;
|