@foxden-app/foxclaw 0.3.18 → 0.4.0
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/.env.example +8 -3
- package/README.md +17 -7
- package/README_EN.md +17 -7
- package/dist/auth/mirror.d.ts +38 -0
- package/dist/auth/mirror.js +259 -0
- package/dist/codex_app/client.d.ts +4 -1
- package/dist/codex_app/client.js +23 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +18 -1
- package/dist/controller/controller.d.ts +24 -2
- package/dist/controller/controller.js +161 -35
- package/dist/core/bridge_scope.d.ts +5 -2
- package/dist/core/bridge_scope.js +7 -3
- package/dist/i18n.d.ts +20 -2
- package/dist/i18n.js +20 -2
- package/dist/main.js +198 -10
- package/dist/store/database.d.ts +4 -2
- package/dist/store/database.js +42 -6
- package/dist/telegram/addressing.d.ts +1 -0
- package/dist/telegram/addressing.js +3 -0
- package/dist/telegram/gateway.d.ts +4 -1
- package/dist/telegram/gateway.js +23 -5
- package/dist/types.d.ts +26 -0
- package/dist/update.d.ts +5 -0
- package/dist/update.js +93 -5
- package/docs/agent-assisted-install.md +7 -6
- package/docs/install-for-beginners.md +12 -4
- package/docs/troubleshooting.md +13 -1
- package/docs/user-manual.md +12 -12
- package/docs/zh/agent-assisted-install.md +7 -6
- package/docs/zh/foxclaw-skill.md +4 -2
- package/docs/zh/install-for-beginners.md +12 -4
- package/docs/zh/troubleshooting.md +13 -1
- package/docs/zh/user-manual.md +12 -12
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +28 -20
- package/skills/foxclaw/references/telegram-setup.md +9 -6
- package/skills/foxclaw/scripts/bootstrap_host.py +11 -8
- package/skills/foxclaw/scripts/bootstrap_remote.py +8 -4
|
@@ -5,7 +5,19 @@ import type { RuntimeStatus } from '../types.js';
|
|
|
5
5
|
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
|
-
import type { SelfUpdateRuntime } from '../update.js';
|
|
8
|
+
import type { SelfUpdateRuntime, SelfUpdateStatus } from '../update.js';
|
|
9
|
+
export interface CoreCoordinator {
|
|
10
|
+
canSelfUpdate?: () => boolean;
|
|
11
|
+
authCandidateUpdated?: (runtimeId: string, candidateName: string) => Promise<void>;
|
|
12
|
+
statusUpdated?: (status: RuntimeStatus) => void;
|
|
13
|
+
getServiceStatus?: () => Promise<{
|
|
14
|
+
bots: NonNullable<RuntimeStatus['bots']>;
|
|
15
|
+
weixinRuntime?: RuntimeStatus['weixinRuntime'];
|
|
16
|
+
authMirror?: RuntimeStatus['authMirror'];
|
|
17
|
+
lastUpdate?: SelfUpdateStatus | null;
|
|
18
|
+
}>;
|
|
19
|
+
selfUpdateCompleted?: (status: SelfUpdateStatus) => void;
|
|
20
|
+
}
|
|
9
21
|
export declare class BridgeSessionCore {
|
|
10
22
|
private readonly config;
|
|
11
23
|
private readonly store;
|
|
@@ -13,6 +25,8 @@ export declare class BridgeSessionCore {
|
|
|
13
25
|
private readonly bot;
|
|
14
26
|
private readonly app;
|
|
15
27
|
private readonly selfUpdater;
|
|
28
|
+
private readonly coordinator;
|
|
29
|
+
private readonly ownsTelegramRuntime;
|
|
16
30
|
private activeTurns;
|
|
17
31
|
private activeTurnsByTurnId;
|
|
18
32
|
private observedThreadWatchers;
|
|
@@ -51,7 +65,7 @@ export declare class BridgeSessionCore {
|
|
|
51
65
|
/** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
|
|
52
66
|
private threadListPresentationState;
|
|
53
67
|
private readonly messaging;
|
|
54
|
-
constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter, selfUpdater?: SelfUpdateRuntime | null);
|
|
68
|
+
constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter, selfUpdater?: SelfUpdateRuntime | null, coordinator?: CoreCoordinator | null, ownsTelegramRuntime?: boolean);
|
|
55
69
|
/** Wire Telegram inbound events. Call before {@link startCodexApp}. */
|
|
56
70
|
registerTelegramInboundHandlers(): void;
|
|
57
71
|
/**
|
|
@@ -140,6 +154,12 @@ export declare class BridgeSessionCore {
|
|
|
140
154
|
private stageAttachments;
|
|
141
155
|
private registerActiveTurn;
|
|
142
156
|
private createActiveTurnState;
|
|
157
|
+
getCurrentAuthLabel(): Promise<string | null>;
|
|
158
|
+
isIdleForServiceUpdate(): boolean;
|
|
159
|
+
private hasLocalBlockingActivity;
|
|
160
|
+
private authRuntimeId;
|
|
161
|
+
private authDisplayBotLabel;
|
|
162
|
+
private ownsScope;
|
|
143
163
|
private setActiveTurn;
|
|
144
164
|
private getActiveTurn;
|
|
145
165
|
private getActiveTurnsForTurn;
|
|
@@ -261,6 +281,7 @@ export declare class BridgeSessionCore {
|
|
|
261
281
|
private retryTurnAfterAuthRotation;
|
|
262
282
|
private selectNextCodexAuthCandidate;
|
|
263
283
|
private listCodexAuthState;
|
|
284
|
+
private resolveAuthDir;
|
|
264
285
|
private readCodexAuthSwitchLabels;
|
|
265
286
|
private codexAuthSwitchParams;
|
|
266
287
|
private switchCodexAuthAndRestart;
|
|
@@ -277,6 +298,7 @@ export declare class BridgeSessionCore {
|
|
|
277
298
|
private readCodexAuthQuotaSnapshots;
|
|
278
299
|
private writeCodexAuthQuotaSnapshots;
|
|
279
300
|
private codexAuthQuotaSnapshotPath;
|
|
301
|
+
private runtimeSnapshotFilename;
|
|
280
302
|
private sendThreadContextSummary;
|
|
281
303
|
private handleModelCommand;
|
|
282
304
|
private handleEffortCommand;
|
|
@@ -93,6 +93,8 @@ export class BridgeSessionCore {
|
|
|
93
93
|
bot;
|
|
94
94
|
app;
|
|
95
95
|
selfUpdater;
|
|
96
|
+
coordinator;
|
|
97
|
+
ownsTelegramRuntime;
|
|
96
98
|
activeTurns = new Map();
|
|
97
99
|
activeTurnsByTurnId = new Map();
|
|
98
100
|
observedThreadWatchers = new Map();
|
|
@@ -131,13 +133,15 @@ export class BridgeSessionCore {
|
|
|
131
133
|
/** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
|
|
132
134
|
threadListPresentationState = new Map();
|
|
133
135
|
messaging;
|
|
134
|
-
constructor(config, store, logger, bot, app, outbound, selfUpdater = null) {
|
|
136
|
+
constructor(config, store, logger, bot, app, outbound, selfUpdater = null, coordinator = null, ownsTelegramRuntime = true) {
|
|
135
137
|
this.config = config;
|
|
136
138
|
this.store = store;
|
|
137
139
|
this.logger = logger;
|
|
138
140
|
this.bot = bot;
|
|
139
141
|
this.app = app;
|
|
140
142
|
this.selfUpdater = selfUpdater;
|
|
143
|
+
this.coordinator = coordinator;
|
|
144
|
+
this.ownsTelegramRuntime = ownsTelegramRuntime;
|
|
141
145
|
this.messaging = outbound;
|
|
142
146
|
}
|
|
143
147
|
/** Wire Telegram inbound events. Call before {@link startCodexApp}. */
|
|
@@ -196,13 +200,13 @@ export class BridgeSessionCore {
|
|
|
196
200
|
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
197
201
|
});
|
|
198
202
|
this.updateStatus();
|
|
203
|
+
this.scheduleSelfUpdateStatusPoll(0);
|
|
199
204
|
}
|
|
200
205
|
/** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
|
|
201
206
|
async startTelegramPolling() {
|
|
202
207
|
await this.bot.start();
|
|
203
208
|
this.botUsername = this.bot.username;
|
|
204
209
|
this.updateStatus();
|
|
205
|
-
this.scheduleSelfUpdateStatusPoll(0);
|
|
206
210
|
}
|
|
207
211
|
/** Telegram-only default startup (single channel). */
|
|
208
212
|
async start() {
|
|
@@ -256,7 +260,7 @@ export class BridgeSessionCore {
|
|
|
256
260
|
lastError: this.lastError,
|
|
257
261
|
updatedAt: new Date().toISOString(),
|
|
258
262
|
channels: {
|
|
259
|
-
telegram:
|
|
263
|
+
telegram: this.ownsTelegramRuntime,
|
|
260
264
|
weixin: Boolean(this.config.wxEnabled && this.messaging.hasWeixinTransport),
|
|
261
265
|
},
|
|
262
266
|
};
|
|
@@ -323,6 +327,7 @@ export class BridgeSessionCore {
|
|
|
323
327
|
allowedChatId: this.config.tgAllowedChatId,
|
|
324
328
|
allowedTopicId: this.config.tgAllowedTopicId,
|
|
325
329
|
topicId: event.topicId,
|
|
330
|
+
requireExplicitGroupAddressing: this.config.tgRequireExplicitGroupAddressing,
|
|
326
331
|
}),
|
|
327
332
|
replyToBot: event.replyToBot,
|
|
328
333
|
});
|
|
@@ -357,10 +362,13 @@ export class BridgeSessionCore {
|
|
|
357
362
|
const binding = this.store.getBinding(scopeId);
|
|
358
363
|
const settings = this.store.getChatSettings(scopeId);
|
|
359
364
|
const access = this.resolveEffectiveAccess(scopeId, settings);
|
|
360
|
-
const [fastStatus, codexUsageLines, codexLocalUsageLines] = await Promise.all([
|
|
365
|
+
const [fastStatus, codexUsageLines, codexLocalUsageLines, serviceStatus] = await Promise.all([
|
|
361
366
|
this.resolveFastStatusLabel(locale, settings),
|
|
362
367
|
this.buildCodexUsageStatusLines(locale),
|
|
363
368
|
this.buildCodexLocalUsageStatusLines(locale),
|
|
369
|
+
this.config.tgMultiBotMode
|
|
370
|
+
? (this.coordinator?.getServiceStatus?.() ?? Promise.resolve(null))
|
|
371
|
+
: Promise.resolve(null),
|
|
364
372
|
]);
|
|
365
373
|
const appServer = this.app.getServerStatus();
|
|
366
374
|
const appServerLabel = appServer.pid && appServer.port
|
|
@@ -389,6 +397,43 @@ export class BridgeSessionCore {
|
|
|
389
397
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
390
398
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
391
399
|
];
|
|
400
|
+
if (serviceStatus) {
|
|
401
|
+
lines.push('', t(locale, 'status_runtime_overview'));
|
|
402
|
+
for (const runtime of serviceStatus.bots) {
|
|
403
|
+
lines.push(t(locale, 'status_runtime_bot', {
|
|
404
|
+
bot: runtime.username ? `@${runtime.username}` : runtime.id,
|
|
405
|
+
connected: t(locale, runtime.connected ? 'yes' : 'no'),
|
|
406
|
+
auth: runtime.currentAuth ?? t(locale, 'none'),
|
|
407
|
+
turns: runtime.activeTurns,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
if (serviceStatus.weixinRuntime) {
|
|
411
|
+
lines.push(t(locale, 'status_runtime_weixin', {
|
|
412
|
+
connected: t(locale, serviceStatus.weixinRuntime.connected ? 'yes' : 'no'),
|
|
413
|
+
turns: serviceStatus.weixinRuntime.activeTurns,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
lines.push(serviceStatus.authMirror
|
|
417
|
+
? t(locale, 'status_auth_mirror_synced', {
|
|
418
|
+
candidate: serviceStatus.authMirror.candidateName,
|
|
419
|
+
source: serviceStatus.authMirror.sourceLabel,
|
|
420
|
+
time: serviceStatus.authMirror.syncedAt,
|
|
421
|
+
})
|
|
422
|
+
: t(locale, 'status_auth_mirror_none'));
|
|
423
|
+
if (serviceStatus.lastUpdate) {
|
|
424
|
+
lines.push(t(locale, 'status_last_update', {
|
|
425
|
+
from: serviceStatus.lastUpdate.fromVersion,
|
|
426
|
+
to: serviceStatus.lastUpdate.toVersion ?? t(locale, 'unknown'),
|
|
427
|
+
time: serviceStatus.lastUpdate.updatedAt,
|
|
428
|
+
}));
|
|
429
|
+
if (serviceStatus.lastUpdate.codexUpdate) {
|
|
430
|
+
lines.push(t(locale, 'status_last_codex_update', { value: serviceStatus.lastUpdate.codexUpdate }));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
lines.push(t(locale, 'status_last_update_none'));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
392
437
|
lines.push(...codexUsageLines);
|
|
393
438
|
lines.push(...codexLocalUsageLines);
|
|
394
439
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
@@ -1563,6 +1608,16 @@ export class BridgeSessionCore {
|
|
|
1563
1608
|
return;
|
|
1564
1609
|
}
|
|
1565
1610
|
const lines = [t(locale, 'auth_add_done', { value: pendingAuthAdd.name })];
|
|
1611
|
+
try {
|
|
1612
|
+
await this.coordinator?.authCandidateUpdated?.(this.authRuntimeId(), pendingAuthAdd.name);
|
|
1613
|
+
}
|
|
1614
|
+
catch (error) {
|
|
1615
|
+
this.logger.warn('codex.auth_candidate_sync_failed', {
|
|
1616
|
+
candidate: pendingAuthAdd.name,
|
|
1617
|
+
runtimeId: this.authRuntimeId(),
|
|
1618
|
+
error: toErrorMeta(error),
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1566
1621
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
1567
1622
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
1568
1623
|
return;
|
|
@@ -1572,7 +1627,7 @@ export class BridgeSessionCore {
|
|
|
1572
1627
|
: t(locale, 'login_failed', { error: params?.error ?? t(locale, 'unknown') }));
|
|
1573
1628
|
}
|
|
1574
1629
|
async restorePendingAuthAdd(record) {
|
|
1575
|
-
const state = await listCodexAuthState();
|
|
1630
|
+
const state = await this.listCodexAuthState();
|
|
1576
1631
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
|
|
1577
1632
|
}
|
|
1578
1633
|
async restoreAuthAfterAddFailure(authDir, authPath, previousTargetPath) {
|
|
@@ -1849,6 +1904,9 @@ export class BridgeSessionCore {
|
|
|
1849
1904
|
}
|
|
1850
1905
|
async restorePendingUserInputs() {
|
|
1851
1906
|
for (const stored of this.store.listPendingUserInputs()) {
|
|
1907
|
+
if (!this.ownsScope(stored.chatId)) {
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1852
1910
|
const record = parseStoredPendingUserInput(stored);
|
|
1853
1911
|
if (!record) {
|
|
1854
1912
|
this.store.markPendingUserInputResolved(stored.localId);
|
|
@@ -2535,6 +2593,45 @@ export class BridgeSessionCore {
|
|
|
2535
2593
|
resolver,
|
|
2536
2594
|
};
|
|
2537
2595
|
}
|
|
2596
|
+
async getCurrentAuthLabel() {
|
|
2597
|
+
return (await this.listCodexAuthState()).currentLabel;
|
|
2598
|
+
}
|
|
2599
|
+
isIdleForServiceUpdate() {
|
|
2600
|
+
return this.activeTurns.size === 0
|
|
2601
|
+
&& this.pendingApprovalMessages.size === 0
|
|
2602
|
+
&& this.pendingUserInputs.size === 0
|
|
2603
|
+
&& this.pendingMcpElicitations.size === 0
|
|
2604
|
+
&& this.pendingLoginsByScope.size === 0
|
|
2605
|
+
&& !this.authRotationInProgress;
|
|
2606
|
+
}
|
|
2607
|
+
hasLocalBlockingActivity() {
|
|
2608
|
+
return !this.isIdleForServiceUpdate();
|
|
2609
|
+
}
|
|
2610
|
+
authRuntimeId() {
|
|
2611
|
+
return this.config.tgScopeBotId ?? 'default';
|
|
2612
|
+
}
|
|
2613
|
+
authDisplayBotLabel() {
|
|
2614
|
+
if (!this.config.tgScopeBotId)
|
|
2615
|
+
return null;
|
|
2616
|
+
return this.botUsername ? `@${this.botUsername}` : this.config.tgScopeBotId;
|
|
2617
|
+
}
|
|
2618
|
+
ownsScope(scopeId) {
|
|
2619
|
+
if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
2620
|
+
return this.messaging.hasWeixinTransport;
|
|
2621
|
+
}
|
|
2622
|
+
if (!this.ownsTelegramRuntime) {
|
|
2623
|
+
return false;
|
|
2624
|
+
}
|
|
2625
|
+
if (!this.config.tgScopeBotId) {
|
|
2626
|
+
return parseTelegramTargetFromBridgeScope(scopeId).botId === null;
|
|
2627
|
+
}
|
|
2628
|
+
try {
|
|
2629
|
+
return parseTelegramTargetFromBridgeScope(scopeId).botId === this.config.tgScopeBotId;
|
|
2630
|
+
}
|
|
2631
|
+
catch {
|
|
2632
|
+
return false;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2538
2635
|
setActiveTurn(scopeId, turnId, active) {
|
|
2539
2636
|
const key = activeTurnKey(scopeId, turnId);
|
|
2540
2637
|
this.activeTurns.set(key, active);
|
|
@@ -2785,7 +2882,7 @@ export class BridgeSessionCore {
|
|
|
2785
2882
|
}
|
|
2786
2883
|
}
|
|
2787
2884
|
for (const scopeId of this.store.findAllChatIdsByThreadId(threadId)) {
|
|
2788
|
-
if (!this.messaging.canSendToScope(scopeId)) {
|
|
2885
|
+
if (!this.ownsScope(scopeId) || !this.messaging.canSendToScope(scopeId)) {
|
|
2789
2886
|
continue;
|
|
2790
2887
|
}
|
|
2791
2888
|
scopes.add(scopeId);
|
|
@@ -2818,7 +2915,12 @@ export class BridgeSessionCore {
|
|
|
2818
2915
|
return next;
|
|
2819
2916
|
}
|
|
2820
2917
|
updateStatus() {
|
|
2821
|
-
|
|
2918
|
+
const status = this.getRuntimeStatus();
|
|
2919
|
+
if (this.coordinator?.statusUpdated) {
|
|
2920
|
+
this.coordinator.statusUpdated(status);
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
writeRuntimeStatus(this.config.statusPath, status);
|
|
2822
2924
|
}
|
|
2823
2925
|
async sendMessage(scopeId, text, inlineKeyboard) {
|
|
2824
2926
|
return this.messaging.sendPlain(scopeId, text, inlineKeyboard);
|
|
@@ -3653,7 +3755,7 @@ export class BridgeSessionCore {
|
|
|
3653
3755
|
].join('\n'));
|
|
3654
3756
|
}
|
|
3655
3757
|
async handleAuthReloadCommand(scopeId, locale) {
|
|
3656
|
-
if (this.
|
|
3758
|
+
if (this.hasLocalBlockingActivity()) {
|
|
3657
3759
|
await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
|
|
3658
3760
|
return;
|
|
3659
3761
|
}
|
|
@@ -3670,7 +3772,7 @@ export class BridgeSessionCore {
|
|
|
3670
3772
|
await this.sendMessage(scopeId, t(locale, 'update_unavailable'));
|
|
3671
3773
|
return;
|
|
3672
3774
|
}
|
|
3673
|
-
if (this.
|
|
3775
|
+
if (!this.isIdleForServiceUpdate() || this.store.countPendingApprovals() > 0 || this.store.countPendingUserInputs() > 0 || (this.coordinator?.canSelfUpdate && !this.coordinator.canSelfUpdate())) {
|
|
3674
3776
|
await this.sendMessage(scopeId, t(locale, 'update_blocked_active'));
|
|
3675
3777
|
return;
|
|
3676
3778
|
}
|
|
@@ -3721,17 +3823,24 @@ export class BridgeSessionCore {
|
|
|
3721
3823
|
this.scheduleSelfUpdateStatusPoll();
|
|
3722
3824
|
return;
|
|
3723
3825
|
}
|
|
3826
|
+
if (!this.ownsScope(status.scopeId)) {
|
|
3827
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
this.coordinator?.selfUpdateCompleted?.(status);
|
|
3724
3831
|
await this.sendMessage(status.scopeId, this.formatSelfUpdateResult(status));
|
|
3725
3832
|
await this.selfUpdater?.clearStatus();
|
|
3726
3833
|
}
|
|
3727
3834
|
formatSelfUpdateResult(status) {
|
|
3728
3835
|
if (status.state === 'succeeded') {
|
|
3729
|
-
|
|
3836
|
+
const result = t(status.locale, 'update_succeeded', {
|
|
3730
3837
|
from: status.fromVersion,
|
|
3731
3838
|
to: status.toVersion ?? t(status.locale, 'unknown'),
|
|
3732
3839
|
});
|
|
3840
|
+
return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
|
|
3733
3841
|
}
|
|
3734
|
-
|
|
3842
|
+
const result = t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
|
|
3843
|
+
return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
|
|
3735
3844
|
}
|
|
3736
3845
|
async handleAuthCommand(scopeId, locale, args) {
|
|
3737
3846
|
const action = args[0]?.toLowerCase() ?? 'list';
|
|
@@ -3765,11 +3874,11 @@ export class BridgeSessionCore {
|
|
|
3765
3874
|
createdAt: Date.now(),
|
|
3766
3875
|
};
|
|
3767
3876
|
this.pendingAuthChoiceLists.set(record.localId, record);
|
|
3768
|
-
const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
3877
|
+
const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
3769
3878
|
record.messageId = messageId;
|
|
3770
3879
|
}
|
|
3771
3880
|
async handleAuthUseCommand(scopeId, locale, args) {
|
|
3772
|
-
if (this.
|
|
3881
|
+
if (this.hasLocalBlockingActivity()) {
|
|
3773
3882
|
await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
|
|
3774
3883
|
return;
|
|
3775
3884
|
}
|
|
@@ -3787,7 +3896,7 @@ export class BridgeSessionCore {
|
|
|
3787
3896
|
if (!candidate) {
|
|
3788
3897
|
await this.sendMessage(scopeId, t(locale, 'auth_choice_expired'));
|
|
3789
3898
|
const state = await this.listCodexAuthState();
|
|
3790
|
-
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(scopeId) !== null));
|
|
3899
|
+
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null));
|
|
3791
3900
|
return;
|
|
3792
3901
|
}
|
|
3793
3902
|
const switchLabels = await this.readCodexAuthSwitchLabels(candidate);
|
|
@@ -3804,10 +3913,10 @@ export class BridgeSessionCore {
|
|
|
3804
3913
|
const candidate = state.candidates[index - 1];
|
|
3805
3914
|
if (!candidate) {
|
|
3806
3915
|
await this.sendMessage(scopeId, t(locale, 'auth_choice_expired'));
|
|
3807
|
-
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(scopeId) !== null));
|
|
3916
|
+
await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null));
|
|
3808
3917
|
return;
|
|
3809
3918
|
}
|
|
3810
|
-
this.store.setCodexAuthCandidateDisabled(candidate.name, disabled);
|
|
3919
|
+
this.store.setCodexAuthCandidateDisabled(candidate.name, disabled, this.authRuntimeId());
|
|
3811
3920
|
await this.sendMessage(scopeId, t(locale, disabled ? 'auth_candidate_disabled' : 'auth_candidate_enabled', {
|
|
3812
3921
|
value: candidate.name,
|
|
3813
3922
|
}));
|
|
@@ -3825,7 +3934,7 @@ export class BridgeSessionCore {
|
|
|
3825
3934
|
return latest;
|
|
3826
3935
|
}
|
|
3827
3936
|
async handleAuthAddCommand(scopeId, locale, args) {
|
|
3828
|
-
if (this.
|
|
3937
|
+
if (this.hasLocalBlockingActivity()) {
|
|
3829
3938
|
await this.sendMessage(scopeId, t(locale, 'auth_reload_blocked_active'));
|
|
3830
3939
|
return;
|
|
3831
3940
|
}
|
|
@@ -4259,12 +4368,12 @@ export class BridgeSessionCore {
|
|
|
4259
4368
|
return;
|
|
4260
4369
|
}
|
|
4261
4370
|
const disabled = !candidate.disabled;
|
|
4262
|
-
this.store.setCodexAuthCandidateDisabled(candidate.name, disabled);
|
|
4371
|
+
this.store.setCodexAuthCandidateDisabled(candidate.name, disabled, this.authRuntimeId());
|
|
4263
4372
|
const state = await this.listCodexAuthState();
|
|
4264
4373
|
record.candidates = state.candidates;
|
|
4265
4374
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, disabled ? 'auth_candidate_disabled_short' : 'auth_candidate_enabled_short'));
|
|
4266
4375
|
if (record.messageId !== null) {
|
|
4267
|
-
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4376
|
+
await this.editMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null), authChoiceKeyboard(locale, record));
|
|
4268
4377
|
}
|
|
4269
4378
|
}
|
|
4270
4379
|
async handleAuthSwitchCallback(event, localId, index, locale) {
|
|
@@ -4277,7 +4386,7 @@ export class BridgeSessionCore {
|
|
|
4277
4386
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_choice_mismatch'));
|
|
4278
4387
|
return;
|
|
4279
4388
|
}
|
|
4280
|
-
if (this.
|
|
4389
|
+
if (this.hasLocalBlockingActivity()) {
|
|
4281
4390
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_reload_blocked_active'));
|
|
4282
4391
|
return;
|
|
4283
4392
|
}
|
|
@@ -4299,7 +4408,7 @@ export class BridgeSessionCore {
|
|
|
4299
4408
|
if (!this.pendingAuthRotation || this.authRotationInProgress) {
|
|
4300
4409
|
return false;
|
|
4301
4410
|
}
|
|
4302
|
-
if (this.
|
|
4411
|
+
if (this.hasLocalBlockingActivity()) {
|
|
4303
4412
|
return false;
|
|
4304
4413
|
}
|
|
4305
4414
|
const rotation = this.pendingAuthRotation;
|
|
@@ -4398,13 +4507,19 @@ export class BridgeSessionCore {
|
|
|
4398
4507
|
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
4399
4508
|
}
|
|
4400
4509
|
async listCodexAuthState() {
|
|
4401
|
-
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames());
|
|
4510
|
+
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.resolveAuthDir());
|
|
4402
4511
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4403
4512
|
state.candidates.forEach((candidate) => {
|
|
4404
4513
|
candidate.quota = snapshots[candidate.name] ?? null;
|
|
4405
4514
|
});
|
|
4406
4515
|
return state;
|
|
4407
4516
|
}
|
|
4517
|
+
resolveAuthDir() {
|
|
4518
|
+
return this.config.codexAuthDir
|
|
4519
|
+
?? (this.config.tgMultiBotMode ? this.config.codexHome : null)
|
|
4520
|
+
?? process.env.CODEX_AUTH_DIR
|
|
4521
|
+
?? path.join(os.homedir(), '.codex');
|
|
4522
|
+
}
|
|
4408
4523
|
async readCodexAuthSwitchLabels(candidate) {
|
|
4409
4524
|
const state = await this.listCodexAuthState();
|
|
4410
4525
|
return {
|
|
@@ -4419,7 +4534,7 @@ export class BridgeSessionCore {
|
|
|
4419
4534
|
};
|
|
4420
4535
|
}
|
|
4421
4536
|
async switchCodexAuthAndRestart(scopeId, locale, candidate, automatic) {
|
|
4422
|
-
const result = await switchCodexAuth(candidate.path);
|
|
4537
|
+
const result = await switchCodexAuth(candidate.path, this.resolveAuthDir());
|
|
4423
4538
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
4424
4539
|
this.pendingTurnErrors.clear();
|
|
4425
4540
|
this.attachedThreads.clear();
|
|
@@ -4610,14 +4725,14 @@ export class BridgeSessionCore {
|
|
|
4610
4725
|
await this.localUsageRefresh;
|
|
4611
4726
|
}
|
|
4612
4727
|
async refreshCodexLocalUsageStats() {
|
|
4613
|
-
const stats = await readCodexLocalUsageStats();
|
|
4728
|
+
const stats = await readCodexLocalUsageStats(this.config.codexHome ?? undefined);
|
|
4614
4729
|
const snapshot = { computedAtMs: Date.now(), stats };
|
|
4615
4730
|
this.localUsageCache = snapshot;
|
|
4616
4731
|
this.localUsageCacheLoaded = true;
|
|
4617
4732
|
await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
|
|
4618
4733
|
}
|
|
4619
4734
|
codexLocalUsageSnapshotPath() {
|
|
4620
|
-
return path.join(path.dirname(this.config.statusPath), CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME);
|
|
4735
|
+
return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
|
|
4621
4736
|
}
|
|
4622
4737
|
async refreshCurrentCodexAuthQuota(state) {
|
|
4623
4738
|
const candidate = state.candidates.find(entry => entry.isCurrent);
|
|
@@ -4669,7 +4784,13 @@ export class BridgeSessionCore {
|
|
|
4669
4784
|
await fs.rename(temporaryPath, snapshotPath);
|
|
4670
4785
|
}
|
|
4671
4786
|
codexAuthQuotaSnapshotPath() {
|
|
4672
|
-
return path.join(path.dirname(this.config.statusPath), CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME);
|
|
4787
|
+
return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME));
|
|
4788
|
+
}
|
|
4789
|
+
runtimeSnapshotFilename(filename) {
|
|
4790
|
+
if (!this.config.tgScopeBotId) {
|
|
4791
|
+
return filename;
|
|
4792
|
+
}
|
|
4793
|
+
return filename.replace(/\.json$/, `-${this.config.tgScopeBotId}.json`);
|
|
4673
4794
|
}
|
|
4674
4795
|
async sendThreadContextSummary(scopeId, locale, threadId) {
|
|
4675
4796
|
try {
|
|
@@ -5588,6 +5709,9 @@ export class BridgeSessionCore {
|
|
|
5588
5709
|
}
|
|
5589
5710
|
async cleanupStaleTurnPreviews() {
|
|
5590
5711
|
for (const preview of this.store.listActiveTurnPreviews()) {
|
|
5712
|
+
if (!this.ownsScope(preview.scopeId)) {
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5591
5715
|
if (!this.messaging.canSendToScope(preview.scopeId)) {
|
|
5592
5716
|
this.store.removeActiveTurnPreview(preview.turnId);
|
|
5593
5717
|
this.logger.info('telegram.preview_dropped_disabled_channel', {
|
|
@@ -7119,11 +7243,11 @@ function parseActiveTurnKey(key) {
|
|
|
7119
7243
|
turnId: decodeURIComponent(key.slice(split + 1)),
|
|
7120
7244
|
};
|
|
7121
7245
|
}
|
|
7122
|
-
function codexAuthDir() {
|
|
7123
|
-
return process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
|
|
7246
|
+
function codexAuthDir(explicitAuthDir = null) {
|
|
7247
|
+
return explicitAuthDir || process.env.CODEX_AUTH_DIR || path.join(os.homedir(), '.codex');
|
|
7124
7248
|
}
|
|
7125
|
-
async function listCodexAuthState(disabledNames = new Set()) {
|
|
7126
|
-
const authDir = codexAuthDir();
|
|
7249
|
+
async function listCodexAuthState(disabledNames = new Set(), explicitAuthDir = null) {
|
|
7250
|
+
const authDir = codexAuthDir(explicitAuthDir);
|
|
7127
7251
|
const authPath = path.join(authDir, 'auth.json');
|
|
7128
7252
|
const currentTargetPath = await resolveCurrentAuthTarget(authDir, authPath);
|
|
7129
7253
|
const candidates = [];
|
|
@@ -7222,8 +7346,8 @@ async function pointCodexAuthAtTarget(authDir, authPath, targetPath) {
|
|
|
7222
7346
|
throw error;
|
|
7223
7347
|
}
|
|
7224
7348
|
}
|
|
7225
|
-
async function switchCodexAuth(targetPath) {
|
|
7226
|
-
const state = await listCodexAuthState();
|
|
7349
|
+
async function switchCodexAuth(targetPath, explicitAuthDir = null) {
|
|
7350
|
+
const state = await listCodexAuthState(new Set(), explicitAuthDir);
|
|
7227
7351
|
const candidate = state.candidates.find(entry => entry.path === targetPath);
|
|
7228
7352
|
if (!candidate) {
|
|
7229
7353
|
throw new Error(`Auth candidate is no longer available: ${path.basename(targetPath)}`);
|
|
@@ -7234,12 +7358,14 @@ async function switchCodexAuth(targetPath) {
|
|
|
7234
7358
|
toLabel: await authPathDisplayLabel(candidate.path),
|
|
7235
7359
|
};
|
|
7236
7360
|
}
|
|
7237
|
-
function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
7361
|
+
function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopyPaste = false) {
|
|
7238
7362
|
const lines = [
|
|
7239
7363
|
t(locale, 'auth_list_title'),
|
|
7240
|
-
t(locale, 'auth_current', { value: state.currentLabel ?? t(locale, 'none') }),
|
|
7241
|
-
t(locale, 'auth_dir', { value: state.authDir }),
|
|
7242
7364
|
];
|
|
7365
|
+
if (botLabel) {
|
|
7366
|
+
lines.push(t(locale, 'auth_bot', { value: botLabel }));
|
|
7367
|
+
}
|
|
7368
|
+
lines.push(t(locale, 'auth_current', { value: state.currentLabel ?? t(locale, 'none') }), t(locale, 'auth_dir', { value: state.authDir }));
|
|
7243
7369
|
if (state.candidates.length === 0) {
|
|
7244
7370
|
lines.push(t(locale, 'auth_no_candidates'));
|
|
7245
7371
|
if (includeWeixinCopyPaste) {
|
|
@@ -7,12 +7,15 @@ export interface WeixinBridgeScope {
|
|
|
7
7
|
accountId: string;
|
|
8
8
|
fromUserId: string;
|
|
9
9
|
}
|
|
10
|
+
export interface TelegramBridgeTarget extends TelegramScope {
|
|
11
|
+
botId: string | null;
|
|
12
|
+
}
|
|
10
13
|
export declare function isBridgeScopedKey(key: string): boolean;
|
|
11
14
|
/** Wrap legacy Telegram inner scope (`chat::topic`) for storage and routing. */
|
|
12
|
-
export declare function toTelegramBridgeScopeId(telegramInnerScopeId: string): string;
|
|
15
|
+
export declare function toTelegramBridgeScopeId(telegramInnerScopeId: string, botId?: string | null): string;
|
|
13
16
|
/** Strip `telegram:` prefix; returns `null` if not a Telegram bridge scope. */
|
|
14
17
|
export declare function telegramInnerScopeFromBridge(bridgeScopeId: string): string | null;
|
|
15
|
-
export declare function parseTelegramTargetFromBridgeScope(bridgeScopeId: string):
|
|
18
|
+
export declare function parseTelegramTargetFromBridgeScope(bridgeScopeId: string): TelegramBridgeTarget;
|
|
16
19
|
/** Parse `weixin:<accountId>:<from_user_id>`; returns `null` if not a Weixin scope. */
|
|
17
20
|
export declare function parseWeixinBridgeScope(bridgeScopeId: string): WeixinBridgeScope | null;
|
|
18
21
|
export declare function toWeixinBridgeScopeId(accountId: string, fromUserId: string): string;
|
|
@@ -7,8 +7,8 @@ export function isBridgeScopedKey(key) {
|
|
|
7
7
|
return key.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX) || key.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX);
|
|
8
8
|
}
|
|
9
9
|
/** Wrap legacy Telegram inner scope (`chat::topic`) for storage and routing. */
|
|
10
|
-
export function toTelegramBridgeScopeId(telegramInnerScopeId) {
|
|
11
|
-
return `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${telegramInnerScopeId}`;
|
|
10
|
+
export function toTelegramBridgeScopeId(telegramInnerScopeId, botId = null) {
|
|
11
|
+
return `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${botId ? `${botId}:` : ''}${telegramInnerScopeId}`;
|
|
12
12
|
}
|
|
13
13
|
/** Strip `telegram:` prefix; returns `null` if not a Telegram bridge scope. */
|
|
14
14
|
export function telegramInnerScopeFromBridge(bridgeScopeId) {
|
|
@@ -22,7 +22,11 @@ export function parseTelegramTargetFromBridgeScope(bridgeScopeId) {
|
|
|
22
22
|
if (inner === null) {
|
|
23
23
|
throw new Error(`Expected ${BRIDGE_SCOPE_TELEGRAM_PREFIX} scope, got: ${bridgeScopeId}`);
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
const namespaced = /^(bot\d+):(.*)$/.exec(inner);
|
|
26
|
+
if (!namespaced) {
|
|
27
|
+
return { ...parseTelegramScopeId(inner), botId: null };
|
|
28
|
+
}
|
|
29
|
+
return { ...parseTelegramScopeId(namespaced[2]), botId: namespaced[1] };
|
|
26
30
|
}
|
|
27
31
|
/** Parse `weixin:<accountId>:<from_user_id>`; returns `null` if not a Weixin scope. */
|
|
28
32
|
export function parseWeixinBridgeScope(bridgeScopeId) {
|
package/dist/i18n.d.ts
CHANGED
|
@@ -118,18 +118,27 @@ declare const MESSAGES: {
|
|
|
118
118
|
readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
|
|
119
119
|
readonly status_codex_credits: "Codex credits: {value}";
|
|
120
120
|
readonly status_codex_limit_reached: "Codex limit: {value}";
|
|
121
|
+
readonly status_runtime_overview: "Telegram bot runtimes:";
|
|
122
|
+
readonly status_runtime_bot: "- {bot}: connected {connected}, auth {auth}, active turns {turns}";
|
|
123
|
+
readonly status_runtime_weixin: "- Weixin default runtime: connected {connected}, active turns {turns}";
|
|
124
|
+
readonly status_auth_mirror_none: "Last auth mirror: none recorded";
|
|
125
|
+
readonly status_auth_mirror_synced: "Last auth mirror: {candidate} from {source} at {time}";
|
|
126
|
+
readonly status_last_update_none: "Last service update: none recorded";
|
|
127
|
+
readonly status_last_update: "Last service update: {from} -> {to} at {time}";
|
|
128
|
+
readonly status_last_codex_update: "Last Codex update: {value}";
|
|
121
129
|
readonly update_started: "FoxClaw update started. I will report here after installation, checks, and service restart complete.";
|
|
122
130
|
readonly update_succeeded: "FoxClaw updated and restarted: {from} -> {to}.";
|
|
123
131
|
readonly update_failed: "FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.";
|
|
124
132
|
readonly update_unavailable: "Self-update is unavailable in this runtime. Run foxclaw update in a terminal.";
|
|
125
133
|
readonly update_already_running: "A FoxClaw update is already running. I will report here when it finishes.";
|
|
126
|
-
readonly update_blocked_active: "Cannot update FoxClaw while a
|
|
134
|
+
readonly update_blocked_active: "Cannot update FoxClaw while a runtime activity or auth mirror write is active. Wait or use /interrupt first.";
|
|
127
135
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
128
136
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
129
137
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
130
138
|
readonly usage_auth: "Usage: /auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]";
|
|
131
139
|
readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
|
|
132
140
|
readonly auth_list_title: "Codex auth files:";
|
|
141
|
+
readonly auth_bot: "Bot runtime: {value}";
|
|
133
142
|
readonly auth_current: "Current auth: {value}";
|
|
134
143
|
readonly auth_dir: "Auth dir: {value}";
|
|
135
144
|
readonly auth_candidate_count: "Candidates: {value}";
|
|
@@ -682,18 +691,27 @@ declare const MESSAGES: {
|
|
|
682
691
|
readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
|
|
683
692
|
readonly status_codex_credits: "Codex 额度:{value}";
|
|
684
693
|
readonly status_codex_limit_reached: "Codex 限制:{value}";
|
|
694
|
+
readonly status_runtime_overview: "Telegram bot 运行时:";
|
|
695
|
+
readonly status_runtime_bot: "- {bot}:连接 {connected},auth {auth},进行中回复 {turns}";
|
|
696
|
+
readonly status_runtime_weixin: "- 微信默认运行时:连接 {connected},进行中回复 {turns}";
|
|
697
|
+
readonly status_auth_mirror_none: "最近 auth 镜像:暂无记录";
|
|
698
|
+
readonly status_auth_mirror_synced: "最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步";
|
|
699
|
+
readonly status_last_update_none: "最近服务升级:暂无记录";
|
|
700
|
+
readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
|
|
701
|
+
readonly status_last_codex_update: "最近 Codex 升级:{value}";
|
|
685
702
|
readonly update_started: "已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。";
|
|
686
703
|
readonly update_succeeded: "FoxClaw 已升级并重启:{from} -> {to}。";
|
|
687
704
|
readonly update_failed: "FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。";
|
|
688
705
|
readonly update_unavailable: "当前运行方式不支持自升级,请在终端运行 foxclaw update。";
|
|
689
706
|
readonly update_already_running: "FoxClaw 升级已经在进行中,结束后我会在这里回报结果。";
|
|
690
|
-
readonly update_blocked_active: "
|
|
707
|
+
readonly update_blocked_active: "当前有 runtime 操作或 auth 镜像写入在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。";
|
|
691
708
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
692
709
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
693
710
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
694
711
|
readonly usage_auth: "用法:/auth [list|use <编号>|enable <编号>|disable <编号>|reload|add <名称>]";
|
|
695
712
|
readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
|
|
696
713
|
readonly auth_list_title: "Codex auth 文件:";
|
|
714
|
+
readonly auth_bot: "Bot runtime:{value}";
|
|
697
715
|
readonly auth_current: "当前 auth:{value}";
|
|
698
716
|
readonly auth_dir: "Auth 目录:{value}";
|
|
699
717
|
readonly auth_candidate_count: "候选数量:{value}";
|