@foxden-app/foxclaw 0.5.9 → 0.5.11
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 +26 -0
- package/README.md +1 -0
- package/README_EN.md +1 -0
- package/dist/auth/mirror.d.ts +6 -0
- package/dist/auth/mirror.js +49 -0
- package/dist/controller/controller.d.ts +21 -2
- package/dist/controller/controller.js +516 -55
- package/dist/i18n.d.ts +36 -6
- package/dist/i18n.js +36 -6
- package/dist/main.js +22 -0
- package/dist/store/database.d.ts +25 -1
- package/dist/store/database.js +321 -0
- package/dist/telegram/gateway.d.ts +1 -0
- package/dist/telegram/gateway.js +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/update.d.ts +3 -0
- package/dist/update.js +72 -3
- package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/01./346/212/212 Codex /345/270/246/345/210/260/346/234/200/345/220/216/344/270/200/345/205/254/351/207/214/357/274/232FoxClaw /347/232/204 auth /347/224/237/345/221/275/345/221/250/346/234/237/344/270/216/350/267/250/350/212/202/347/202/271/345/220/214/346/255/245.md" +341 -0
- package/docs/release.md +102 -0
- package/docs/zh/release.md +102 -0
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@ const CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS = 60 * 60_000;
|
|
|
35
35
|
const CODEX_AUTH_PROACTIVE_REFRESH_INITIAL_DELAY_MS = 5 * 60_000;
|
|
36
36
|
const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
|
|
37
37
|
const SELF_UPDATE_STATUS_POLL_MS = 1000;
|
|
38
|
+
const ATTACHMENT_BATCH_MERGE_WINDOW_MS = 120_000;
|
|
38
39
|
const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
|
|
39
40
|
const PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX = 'A previous agent produced the plan below to accomplish the user\'s task. Implement the plan in a fresh context. Treat the plan as the source of user intent, re-read files as needed, and carry the work through implementation and verification.';
|
|
40
41
|
const PINNED_HELP_COMMANDS = [
|
|
@@ -105,11 +106,9 @@ export class BridgeSessionCore {
|
|
|
105
106
|
activeTurns = new Map();
|
|
106
107
|
activeTurnsByTurnId = new Map();
|
|
107
108
|
observedThreadWatchers = new Map();
|
|
108
|
-
queuedPrompts = new Map();
|
|
109
109
|
pendingTurnErrors = new Map();
|
|
110
110
|
pendingUserInputs = new Map();
|
|
111
111
|
pendingMcpElicitations = new Map();
|
|
112
|
-
pendingPlanImplementations = new Map();
|
|
113
112
|
pendingLoginsByScope = new Map();
|
|
114
113
|
pendingLoginScopesById = new Map();
|
|
115
114
|
pendingAuthAddsByLoginId = new Map();
|
|
@@ -195,7 +194,6 @@ export class BridgeSessionCore {
|
|
|
195
194
|
});
|
|
196
195
|
this.app.on('disconnected', () => {
|
|
197
196
|
this.attachedThreads.clear();
|
|
198
|
-
this.queuedPrompts.clear();
|
|
199
197
|
this.threadTokenUsageAlerts.clear();
|
|
200
198
|
this.clearObservedThreadWatchers();
|
|
201
199
|
void this.abandonActiveTurns().catch((error) => {
|
|
@@ -204,8 +202,13 @@ export class BridgeSessionCore {
|
|
|
204
202
|
this.updateStatus();
|
|
205
203
|
});
|
|
206
204
|
await this.app.start();
|
|
205
|
+
this.store.requeueInterruptedQueuedTurnInputs();
|
|
207
206
|
await this.restorePendingUserInputs();
|
|
207
|
+
await this.restoreGuidedPlanSessions();
|
|
208
208
|
await this.cleanupStaleTurnPreviews();
|
|
209
|
+
void this.recoverQueuedTurns().catch((error) => {
|
|
210
|
+
this.logger.warn('codex.queued_turn_recovery_failed', { error: toErrorMeta(error) });
|
|
211
|
+
});
|
|
209
212
|
void this.refreshCodexLocalUsageIfNeeded().catch((error) => {
|
|
210
213
|
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
211
214
|
});
|
|
@@ -226,11 +229,9 @@ export class BridgeSessionCore {
|
|
|
226
229
|
await this.startTelegramPolling();
|
|
227
230
|
}
|
|
228
231
|
async stop() {
|
|
229
|
-
this.queuedPrompts.clear();
|
|
230
232
|
this.pendingTurnErrors.clear();
|
|
231
233
|
this.pendingUserInputs.clear();
|
|
232
234
|
this.pendingMcpElicitations.clear();
|
|
233
|
-
this.pendingPlanImplementations.clear();
|
|
234
235
|
this.pendingLoginsByScope.clear();
|
|
235
236
|
this.pendingLoginScopesById.clear();
|
|
236
237
|
this.pendingAuthAddsByLoginId.clear();
|
|
@@ -269,6 +270,7 @@ export class BridgeSessionCore {
|
|
|
269
270
|
currentBindings: this.store.countBindings(),
|
|
270
271
|
pendingApprovals: this.store.countPendingApprovals(),
|
|
271
272
|
pendingUserInputs: this.store.countPendingUserInputs(),
|
|
273
|
+
queuedTurns: this.store.countQueuedTurnInputs(),
|
|
272
274
|
activeTurns: this.activeTurns.size,
|
|
273
275
|
lastError: this.lastError,
|
|
274
276
|
updatedAt: new Date().toISOString(),
|
|
@@ -351,6 +353,13 @@ export class BridgeSessionCore {
|
|
|
351
353
|
await this.handleCommand(event, locale, decision.command.name, decision.command.args);
|
|
352
354
|
return;
|
|
353
355
|
}
|
|
356
|
+
if (event.attachments.length > 0) {
|
|
357
|
+
await this.handleTelegramAttachmentBatch(event, locale, decision.text);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (await this.consumePendingAttachmentBatchWithText(event, locale, decision.text)) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
354
363
|
if (this.findActiveTurn(scopeId)) {
|
|
355
364
|
await this.handleActiveTurnInboundMessage(event, locale, decision.text);
|
|
356
365
|
return;
|
|
@@ -408,6 +417,7 @@ export class BridgeSessionCore {
|
|
|
408
417
|
t(locale, 'status_sync_on_turn_complete', { value: t(locale, this.config.codexAppSyncOnTurnComplete ? 'yes' : 'no') }),
|
|
409
418
|
t(locale, 'status_pending_approvals', { value: this.store.countPendingApprovals() }),
|
|
410
419
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
420
|
+
t(locale, 'status_queued_turns', { value: this.store.countQueuedTurnInputs(scopeId) }),
|
|
411
421
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
412
422
|
];
|
|
413
423
|
if (serviceStatus) {
|
|
@@ -582,7 +592,7 @@ export class BridgeSessionCore {
|
|
|
582
592
|
await this.sendMessage(scopeId, t(locale, 'thread_is_archived_use_unarchive'));
|
|
583
593
|
return;
|
|
584
594
|
}
|
|
585
|
-
this.
|
|
595
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
586
596
|
await this.stopWatchingScopeThread(scopeId, thread.threadId);
|
|
587
597
|
let binding;
|
|
588
598
|
try {
|
|
@@ -1055,7 +1065,7 @@ export class BridgeSessionCore {
|
|
|
1055
1065
|
await this.handleAuthListViewCallback(event, authClearSearchMatch[1], 'clear_search', locale);
|
|
1056
1066
|
return;
|
|
1057
1067
|
}
|
|
1058
|
-
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);
|
|
1059
1069
|
if (authActionMatch) {
|
|
1060
1070
|
await this.handleAuthPanelActionCallback(event, authActionMatch[1], authActionMatch[2], locale);
|
|
1061
1071
|
return;
|
|
@@ -1075,6 +1085,11 @@ export class BridgeSessionCore {
|
|
|
1075
1085
|
await this.handlePlanImplementationCallback(event, planImplMatch[1], planImplMatch[2], locale);
|
|
1076
1086
|
return;
|
|
1077
1087
|
}
|
|
1088
|
+
const attachmentBatchMatch = /^attach:([a-f0-9]+):(analyze|clear)$/.exec(event.data);
|
|
1089
|
+
if (attachmentBatchMatch) {
|
|
1090
|
+
await this.handleAttachmentBatchCallback(event, attachmentBatchMatch[1], attachmentBatchMatch[2], locale);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1078
1093
|
const mcpElicitationMatch = /^mcpel:([a-f0-9]+):(accept|decline|cancel)$/.exec(event.data);
|
|
1079
1094
|
if (mcpElicitationMatch) {
|
|
1080
1095
|
await this.handleMcpElicitationCallback(event, mcpElicitationMatch[1], mcpElicitationMatch[2], locale);
|
|
@@ -1782,6 +1797,7 @@ export class BridgeSessionCore {
|
|
|
1782
1797
|
await this.completeTurn(active);
|
|
1783
1798
|
await this.cleanupObservedTransientMessages(active);
|
|
1784
1799
|
await this.finalizeUserInputsForTurn(active, 'resolved');
|
|
1800
|
+
this.markQueuedTurnCompleted(active);
|
|
1785
1801
|
}
|
|
1786
1802
|
finally {
|
|
1787
1803
|
if (active.isObserved) {
|
|
@@ -2076,7 +2092,7 @@ export class BridgeSessionCore {
|
|
|
2076
2092
|
return turn?.status === 'inProgress' && snapshot.activeFlags.includes('waitingOnUserInput');
|
|
2077
2093
|
}
|
|
2078
2094
|
async maybeSendPlanImplementationPrompt(active) {
|
|
2079
|
-
if (active.interruptRequested || this.
|
|
2095
|
+
if (active.interruptRequested || this.store.countQueuedTurnInputs(active.scopeId) > 0) {
|
|
2080
2096
|
return false;
|
|
2081
2097
|
}
|
|
2082
2098
|
if (this.hasPendingUserInputForTurn(active.scopeId, active.turnId)) {
|
|
@@ -2092,8 +2108,8 @@ export class BridgeSessionCore {
|
|
|
2092
2108
|
return false;
|
|
2093
2109
|
}
|
|
2094
2110
|
const binding = this.store.getBinding(active.scopeId);
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2111
|
+
const session = {
|
|
2112
|
+
sessionId: crypto.randomBytes(8).toString('hex'),
|
|
2097
2113
|
scopeId: active.scopeId,
|
|
2098
2114
|
chatId: active.chatId,
|
|
2099
2115
|
chatType: active.chatType,
|
|
@@ -2103,31 +2119,68 @@ export class BridgeSessionCore {
|
|
|
2103
2119
|
cwd: binding?.cwd ?? null,
|
|
2104
2120
|
planMarkdown,
|
|
2105
2121
|
messageId: null,
|
|
2122
|
+
state: 'awaiting_confirmation',
|
|
2106
2123
|
createdAt: Date.now(),
|
|
2124
|
+
updatedAt: Date.now(),
|
|
2125
|
+
resolvedAt: null,
|
|
2107
2126
|
};
|
|
2108
|
-
this.
|
|
2127
|
+
this.store.saveGuidedPlanSession(session);
|
|
2128
|
+
const record = this.pendingPlanImplementationFromSession(session);
|
|
2109
2129
|
const locale = this.localeForChat(active.scopeId);
|
|
2110
2130
|
const messageId = await this.sendMessage(active.scopeId, renderPlanImplementationPrompt(locale, record), planImplementationKeyboard(locale, record.localId));
|
|
2111
|
-
|
|
2131
|
+
this.store.updateGuidedPlanSessionMessage(session.sessionId, messageId);
|
|
2112
2132
|
return true;
|
|
2113
2133
|
}
|
|
2114
2134
|
findPendingPlanImplementation(scopeId, turnId) {
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
continue;
|
|
2118
|
-
}
|
|
2119
|
-
if (turnId !== undefined && record.turnId !== turnId) {
|
|
2120
|
-
continue;
|
|
2121
|
-
}
|
|
2122
|
-
return record;
|
|
2123
|
-
}
|
|
2124
|
-
return null;
|
|
2135
|
+
const session = this.store.findOpenGuidedPlanSession(scopeId, turnId);
|
|
2136
|
+
return session ? this.pendingPlanImplementationFromSession(session) : null;
|
|
2125
2137
|
}
|
|
2126
2138
|
clearPlanImplementationPromptsForScope(scopeId) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2139
|
+
const record = this.store.findOpenGuidedPlanSession(scopeId);
|
|
2140
|
+
if (record) {
|
|
2141
|
+
this.store.updateGuidedPlanSessionState(record.sessionId, 'cancelled');
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
pendingPlanImplementationFromSession(session) {
|
|
2145
|
+
return {
|
|
2146
|
+
localId: session.sessionId,
|
|
2147
|
+
scopeId: session.scopeId,
|
|
2148
|
+
chatId: session.chatId,
|
|
2149
|
+
chatType: session.chatType,
|
|
2150
|
+
topicId: session.topicId,
|
|
2151
|
+
threadId: session.threadId,
|
|
2152
|
+
turnId: session.turnId,
|
|
2153
|
+
cwd: session.cwd,
|
|
2154
|
+
planMarkdown: session.planMarkdown,
|
|
2155
|
+
messageId: session.messageId,
|
|
2156
|
+
createdAt: session.createdAt,
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
async restoreGuidedPlanSessions() {
|
|
2160
|
+
for (const session of this.store.listOpenGuidedPlanSessions()) {
|
|
2161
|
+
if (!this.messaging.canSendToScope(session.scopeId)) {
|
|
2162
|
+
continue;
|
|
2130
2163
|
}
|
|
2164
|
+
const locale = this.localeForChat(session.scopeId);
|
|
2165
|
+
const record = this.pendingPlanImplementationFromSession(session);
|
|
2166
|
+
if (session.messageId !== null) {
|
|
2167
|
+
try {
|
|
2168
|
+
await this.editMessage(session.scopeId, session.messageId, renderPlanImplementationPrompt(locale, record), planImplementationKeyboard(locale, record.localId));
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
catch (error) {
|
|
2172
|
+
if (!isTelegramMessageGone(error)) {
|
|
2173
|
+
this.logger.warn('telegram.plan_impl_restore_edit_failed', {
|
|
2174
|
+
sessionId: session.sessionId,
|
|
2175
|
+
scopeId: session.scopeId,
|
|
2176
|
+
messageId: session.messageId,
|
|
2177
|
+
error: toErrorMeta(error),
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
const messageId = await this.sendMessage(session.scopeId, renderPlanImplementationPrompt(locale, record), planImplementationKeyboard(locale, record.localId));
|
|
2183
|
+
this.store.updateGuidedPlanSessionMessage(session.sessionId, messageId);
|
|
2131
2184
|
}
|
|
2132
2185
|
}
|
|
2133
2186
|
hasPendingUserInputForTurn(scopeId, turnId) {
|
|
@@ -2180,17 +2233,18 @@ export class BridgeSessionCore {
|
|
|
2180
2233
|
}
|
|
2181
2234
|
}
|
|
2182
2235
|
async handlePlanImplementationCallback(event, localId, action, locale) {
|
|
2183
|
-
const
|
|
2184
|
-
if (!
|
|
2236
|
+
const session = this.store.getGuidedPlanSession(localId);
|
|
2237
|
+
if (!session || session.state !== 'awaiting_confirmation') {
|
|
2185
2238
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'plan_impl_expired'));
|
|
2186
2239
|
return;
|
|
2187
2240
|
}
|
|
2241
|
+
const record = this.pendingPlanImplementationFromSession(session);
|
|
2188
2242
|
if (record.scopeId !== event.scopeId || (record.messageId !== null && record.messageId !== event.messageId)) {
|
|
2189
2243
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'plan_impl_mismatch'));
|
|
2190
2244
|
return;
|
|
2191
2245
|
}
|
|
2192
2246
|
if (action === 'stay') {
|
|
2193
|
-
this.
|
|
2247
|
+
this.store.updateGuidedPlanSessionState(localId, 'cancelled');
|
|
2194
2248
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
2195
2249
|
if (record.messageId !== null) {
|
|
2196
2250
|
await this.editMessage(record.scopeId, record.messageId, t(locale, 'plan_impl_staying'), []);
|
|
@@ -2201,9 +2255,9 @@ export class BridgeSessionCore {
|
|
|
2201
2255
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'wait_current_turn'));
|
|
2202
2256
|
return;
|
|
2203
2257
|
}
|
|
2204
|
-
this.pendingPlanImplementations.delete(localId);
|
|
2205
2258
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
2206
2259
|
const turn = await this.startPlanImplementationTurn(record, action === 'fresh');
|
|
2260
|
+
this.store.updateGuidedPlanSessionState(localId, 'completed');
|
|
2207
2261
|
if (record.messageId !== null) {
|
|
2208
2262
|
await this.editMessage(record.scopeId, record.messageId, t(locale, action === 'fresh' ? 'plan_impl_started_fresh' : 'plan_impl_started', {
|
|
2209
2263
|
threadId: turn.threadId,
|
|
@@ -2218,17 +2272,18 @@ export class BridgeSessionCore {
|
|
|
2218
2272
|
await this.sendMessage(scopeId, t(locale, 'usage_planimpl'));
|
|
2219
2273
|
return;
|
|
2220
2274
|
}
|
|
2221
|
-
const
|
|
2222
|
-
if (!
|
|
2275
|
+
const session = this.store.getGuidedPlanSession(localId);
|
|
2276
|
+
if (!session || session.state !== 'awaiting_confirmation') {
|
|
2223
2277
|
await this.sendMessage(scopeId, t(locale, 'plan_impl_expired'));
|
|
2224
2278
|
return;
|
|
2225
2279
|
}
|
|
2280
|
+
const record = this.pendingPlanImplementationFromSession(session);
|
|
2226
2281
|
if (record.scopeId !== scopeId) {
|
|
2227
2282
|
await this.sendMessage(scopeId, t(locale, 'plan_impl_mismatch'));
|
|
2228
2283
|
return;
|
|
2229
2284
|
}
|
|
2230
2285
|
if (action === 'stay') {
|
|
2231
|
-
this.
|
|
2286
|
+
this.store.updateGuidedPlanSessionState(localId, 'cancelled');
|
|
2232
2287
|
if (record.messageId !== null) {
|
|
2233
2288
|
await this.editMessage(record.scopeId, record.messageId, t(locale, 'plan_impl_staying'), []);
|
|
2234
2289
|
}
|
|
@@ -2241,8 +2296,8 @@ export class BridgeSessionCore {
|
|
|
2241
2296
|
await this.sendMessage(scopeId, t(locale, 'wait_current_turn'));
|
|
2242
2297
|
return;
|
|
2243
2298
|
}
|
|
2244
|
-
this.pendingPlanImplementations.delete(localId);
|
|
2245
2299
|
const turn = await this.startPlanImplementationTurn(record, action === 'fresh');
|
|
2300
|
+
this.store.updateGuidedPlanSessionState(localId, 'completed');
|
|
2246
2301
|
const message = t(locale, action === 'fresh' ? 'plan_impl_started_fresh' : 'plan_impl_started', {
|
|
2247
2302
|
threadId: turn.threadId,
|
|
2248
2303
|
turnId: turn.turnId,
|
|
@@ -2497,6 +2552,22 @@ export class BridgeSessionCore {
|
|
|
2497
2552
|
}
|
|
2498
2553
|
return input;
|
|
2499
2554
|
}
|
|
2555
|
+
buildTurnInputFromStagedAttachments(text, attachments) {
|
|
2556
|
+
const input = [{
|
|
2557
|
+
type: 'text',
|
|
2558
|
+
text: buildAttachmentPrompt(text, attachments),
|
|
2559
|
+
text_elements: [],
|
|
2560
|
+
}];
|
|
2561
|
+
for (const attachment of attachments) {
|
|
2562
|
+
if (!attachment.nativeImage)
|
|
2563
|
+
continue;
|
|
2564
|
+
input.push({
|
|
2565
|
+
type: 'localImage',
|
|
2566
|
+
path: attachment.localPath,
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
return input;
|
|
2570
|
+
}
|
|
2500
2571
|
async resolveServiceTierForTurn(scopeId, settings) {
|
|
2501
2572
|
if (!settings) {
|
|
2502
2573
|
return undefined;
|
|
@@ -2582,8 +2653,174 @@ export class BridgeSessionCore {
|
|
|
2582
2653
|
}
|
|
2583
2654
|
return staged;
|
|
2584
2655
|
}
|
|
2585
|
-
async
|
|
2586
|
-
const active = this.
|
|
2656
|
+
async handleTelegramAttachmentBatch(event, locale, text) {
|
|
2657
|
+
const active = this.findActiveTurn(event.scopeId);
|
|
2658
|
+
if (active?.isObserved) {
|
|
2659
|
+
await this.sendMessage(event.scopeId, t(locale, 'watch_read_only_active'));
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const existingBinding = this.store.getBinding(event.scopeId);
|
|
2663
|
+
const binding = active
|
|
2664
|
+
? {
|
|
2665
|
+
threadId: active.threadId,
|
|
2666
|
+
cwd: existingBinding?.cwd ?? this.config.defaultCwd,
|
|
2667
|
+
}
|
|
2668
|
+
: existingBinding
|
|
2669
|
+
? await this.ensureThreadReady(event.scopeId, existingBinding)
|
|
2670
|
+
: await this.createBinding(event.scopeId, null);
|
|
2671
|
+
const cwd = binding.cwd ?? this.config.defaultCwd;
|
|
2672
|
+
const stagedAttachments = await this.stageAttachments(cwd, binding.threadId, event.attachments, locale);
|
|
2673
|
+
const mediaGroupId = event.mediaGroupId ?? null;
|
|
2674
|
+
const now = Date.now();
|
|
2675
|
+
const existingBatch = mediaGroupId
|
|
2676
|
+
? this.store.findPendingAttachmentBatchByMediaGroup(event.scopeId, mediaGroupId)
|
|
2677
|
+
: findReusableStandaloneAttachmentBatch(this.store.getLatestPendingAttachmentBatch(event.scopeId), now);
|
|
2678
|
+
const existingAttachments = existingBatch ? parseStagedTelegramAttachments(existingBatch.attachmentsJson) : [];
|
|
2679
|
+
const record = {
|
|
2680
|
+
batchId: existingBatch?.batchId ?? crypto.randomBytes(8).toString('hex'),
|
|
2681
|
+
scopeId: event.scopeId,
|
|
2682
|
+
chatId: event.chatId,
|
|
2683
|
+
chatType: event.chatType,
|
|
2684
|
+
topicId: event.topicId,
|
|
2685
|
+
threadId: binding.threadId,
|
|
2686
|
+
cwd,
|
|
2687
|
+
mediaGroupId,
|
|
2688
|
+
attachmentsJson: JSON.stringify([...existingAttachments, ...stagedAttachments]),
|
|
2689
|
+
caption: mergeAttachmentBatchCaption(existingBatch?.caption ?? '', text.trim()),
|
|
2690
|
+
messageId: existingBatch?.messageId ?? null,
|
|
2691
|
+
status: 'pending',
|
|
2692
|
+
createdAt: existingBatch?.createdAt ?? now,
|
|
2693
|
+
updatedAt: now,
|
|
2694
|
+
resolvedAt: null,
|
|
2695
|
+
};
|
|
2696
|
+
this.store.savePendingAttachmentBatch(record);
|
|
2697
|
+
await this.renderAttachmentBatchCard(record, locale);
|
|
2698
|
+
}
|
|
2699
|
+
async consumePendingAttachmentBatchWithText(event, locale, text) {
|
|
2700
|
+
const prompt = text.trim();
|
|
2701
|
+
if (!prompt) {
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2704
|
+
const batch = this.store.getLatestPendingAttachmentBatch(event.scopeId);
|
|
2705
|
+
if (!batch) {
|
|
2706
|
+
return false;
|
|
2707
|
+
}
|
|
2708
|
+
await this.consumePendingAttachmentBatch(batch, locale, prompt, event.messageId);
|
|
2709
|
+
return true;
|
|
2710
|
+
}
|
|
2711
|
+
async handleAttachmentBatchCallback(event, batchId, action, locale) {
|
|
2712
|
+
const batch = this.store.getPendingAttachmentBatch(batchId);
|
|
2713
|
+
if (!batch || batch.status !== 'pending') {
|
|
2714
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'attachment_batch_missing'));
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
if (batch.scopeId !== event.scopeId || (batch.messageId !== null && batch.messageId !== event.messageId)) {
|
|
2718
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'attachment_batch_mismatch'));
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
if (action === 'clear') {
|
|
2722
|
+
this.store.resolvePendingAttachmentBatch(batch.batchId, 'cleared');
|
|
2723
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
2724
|
+
if (batch.messageId !== null) {
|
|
2725
|
+
await this.editMessage(batch.scopeId, batch.messageId, t(locale, 'attachment_batch_cleared'), []);
|
|
2726
|
+
}
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
const prompt = batch.caption.trim() || t(locale, 'attachment_batch_default_prompt');
|
|
2730
|
+
await this.consumePendingAttachmentBatch(batch, locale, prompt, batch.messageId);
|
|
2731
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
2732
|
+
}
|
|
2733
|
+
async consumePendingAttachmentBatch(batch, locale, prompt, sourceMessageId) {
|
|
2734
|
+
const active = this.findActiveTurn(batch.scopeId);
|
|
2735
|
+
if (active?.isObserved) {
|
|
2736
|
+
await this.sendMessage(batch.scopeId, t(locale, 'watch_read_only_active'));
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
const attachments = parseStagedTelegramAttachments(batch.attachmentsJson);
|
|
2740
|
+
const input = this.buildTurnInputFromStagedAttachments(prompt, attachments);
|
|
2741
|
+
if (active) {
|
|
2742
|
+
const queueId = this.enqueuePreparedTurnInput({
|
|
2743
|
+
scopeId: batch.scopeId,
|
|
2744
|
+
chatId: batch.chatId,
|
|
2745
|
+
chatType: batch.chatType,
|
|
2746
|
+
topicId: batch.topicId,
|
|
2747
|
+
threadId: active.threadId,
|
|
2748
|
+
input,
|
|
2749
|
+
sourceSummary: summarizeStagedAttachmentInput(prompt, attachments),
|
|
2750
|
+
messageId: sourceMessageId,
|
|
2751
|
+
});
|
|
2752
|
+
this.store.resolvePendingAttachmentBatch(batch.batchId, 'consumed');
|
|
2753
|
+
if (batch.messageId !== null) {
|
|
2754
|
+
await this.editMessage(batch.scopeId, batch.messageId, t(locale, 'attachment_batch_queued', {
|
|
2755
|
+
id: shortId(queueId),
|
|
2756
|
+
position: this.store.countQueuedTurnInputs(batch.scopeId),
|
|
2757
|
+
}), []);
|
|
2758
|
+
}
|
|
2759
|
+
else {
|
|
2760
|
+
await this.sendMessage(batch.scopeId, t(locale, 'attachment_batch_queued', {
|
|
2761
|
+
id: shortId(queueId),
|
|
2762
|
+
position: this.store.countQueuedTurnInputs(batch.scopeId),
|
|
2763
|
+
}));
|
|
2764
|
+
}
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
this.store.resolvePendingAttachmentBatch(batch.batchId, 'consumed');
|
|
2768
|
+
if (batch.messageId !== null) {
|
|
2769
|
+
await this.editMessage(batch.scopeId, batch.messageId, t(locale, 'attachment_batch_consumed'), []);
|
|
2770
|
+
}
|
|
2771
|
+
await this.startTurnFromPreparedAttachmentBatch(batch, input);
|
|
2772
|
+
}
|
|
2773
|
+
async startTurnFromPreparedAttachmentBatch(batch, input) {
|
|
2774
|
+
this.clearPlanImplementationPromptsForScope(batch.scopeId);
|
|
2775
|
+
await this.stopWatchingScopeThread(batch.scopeId);
|
|
2776
|
+
const existingBinding = this.store.getBinding(batch.scopeId);
|
|
2777
|
+
const binding = await this.ensureThreadReady(batch.scopeId, {
|
|
2778
|
+
chatId: batch.scopeId,
|
|
2779
|
+
threadId: batch.threadId,
|
|
2780
|
+
cwd: existingBinding?.cwd ?? batch.cwd,
|
|
2781
|
+
updatedAt: Date.now(),
|
|
2782
|
+
});
|
|
2783
|
+
this.store.setBinding(batch.scopeId, binding.threadId, binding.cwd);
|
|
2784
|
+
await this.sendTyping(batch.scopeId);
|
|
2785
|
+
const turnState = await this.startTurnWithRecovery(batch.scopeId, binding, input);
|
|
2786
|
+
if (turnState.collaborationMode === 'plan') {
|
|
2787
|
+
this.store.setChatCollaborationMode(batch.scopeId, DEFAULT_COLLABORATION_MODE);
|
|
2788
|
+
}
|
|
2789
|
+
await this.registerActiveTurn(batch.scopeId, batch.chatId, batch.chatType, batch.topicId, turnState.threadId, turnState.turnId, 0, {
|
|
2790
|
+
input,
|
|
2791
|
+
threadId: turnState.threadId,
|
|
2792
|
+
cwd: this.store.getBinding(batch.scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
|
|
2793
|
+
chatId: batch.chatId,
|
|
2794
|
+
chatType: batch.chatType,
|
|
2795
|
+
topicId: batch.topicId,
|
|
2796
|
+
collaborationMode: turnState.collaborationMode,
|
|
2797
|
+
failedAuthTargets: new Set(),
|
|
2798
|
+
}, turnState.collaborationMode);
|
|
2799
|
+
}
|
|
2800
|
+
async renderAttachmentBatchCard(record, locale) {
|
|
2801
|
+
const text = renderAttachmentBatchMessage(locale, record);
|
|
2802
|
+
const keyboard = attachmentBatchKeyboard(locale, record.batchId);
|
|
2803
|
+
if (record.messageId !== null) {
|
|
2804
|
+
try {
|
|
2805
|
+
await this.editMessage(record.scopeId, record.messageId, text, keyboard);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
catch (error) {
|
|
2809
|
+
if (!isTelegramMessageGone(error)) {
|
|
2810
|
+
this.logger.warn('telegram.attachment_batch_edit_failed', {
|
|
2811
|
+
batchId: record.batchId,
|
|
2812
|
+
scopeId: record.scopeId,
|
|
2813
|
+
messageId: record.messageId,
|
|
2814
|
+
error: toErrorMeta(error),
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
const messageId = await this.sendMessage(record.scopeId, text, keyboard);
|
|
2820
|
+
this.store.updatePendingAttachmentBatchMessage(record.batchId, messageId);
|
|
2821
|
+
}
|
|
2822
|
+
async registerActiveTurn(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, authRetry = null, collaborationMode = DEFAULT_COLLABORATION_MODE, queuedInputId = null) {
|
|
2823
|
+
const active = this.createActiveTurnState(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, false, collaborationMode, queuedInputId);
|
|
2587
2824
|
active.authRetry = authRetry;
|
|
2588
2825
|
this.setActiveTurn(scopeId, turnId, active);
|
|
2589
2826
|
const pendingError = this.pendingTurnErrors.get(turnId);
|
|
@@ -2607,7 +2844,7 @@ export class BridgeSessionCore {
|
|
|
2607
2844
|
this.logger.warn('telegram.preview_keyboard_attach_failed', { error: String(error), turnId });
|
|
2608
2845
|
}
|
|
2609
2846
|
}
|
|
2610
|
-
createActiveTurnState(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, isObserved = false, collaborationMode = DEFAULT_COLLABORATION_MODE) {
|
|
2847
|
+
createActiveTurnState(scopeId, chatId, chatType, topicId, threadId, turnId, previewMessageId, isObserved = false, collaborationMode = DEFAULT_COLLABORATION_MODE, queuedInputId = null) {
|
|
2611
2848
|
let resolver = () => { };
|
|
2612
2849
|
const completion = new Promise((resolve) => {
|
|
2613
2850
|
resolver = resolve;
|
|
@@ -2621,6 +2858,7 @@ export class BridgeSessionCore {
|
|
|
2621
2858
|
isObserved,
|
|
2622
2859
|
threadId,
|
|
2623
2860
|
turnId,
|
|
2861
|
+
queuedInputId,
|
|
2624
2862
|
previewMessageId,
|
|
2625
2863
|
previewActive: previewMessageId > 0,
|
|
2626
2864
|
draftId: null,
|
|
@@ -2718,7 +2956,7 @@ export class BridgeSessionCore {
|
|
|
2718
2956
|
authDisplayBotLabel() {
|
|
2719
2957
|
if (!this.config.tgScopeBotId)
|
|
2720
2958
|
return null;
|
|
2721
|
-
return this.botUsername ? `@${this.botUsername}` : this.config.tgScopeBotId;
|
|
2959
|
+
return this.botUsername ? `@${this.botUsername} (${this.config.tgScopeBotId})` : this.config.tgScopeBotId;
|
|
2722
2960
|
}
|
|
2723
2961
|
ownsScope(scopeId) {
|
|
2724
2962
|
if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
@@ -2889,6 +3127,7 @@ export class BridgeSessionCore {
|
|
|
2889
3127
|
await this.cleanupObservedTransientMessages(active);
|
|
2890
3128
|
await this.finalizeUserInputsForTurn(active, active.interruptRequested ? 'interrupted' : 'resolved');
|
|
2891
3129
|
await this.maybeSendPlanImplementationPrompt(active);
|
|
3130
|
+
this.markQueuedTurnCompleted(active);
|
|
2892
3131
|
if (this.config.codexAppSyncOnTurnComplete) {
|
|
2893
3132
|
const revealError = await this.tryRevealThread(active.scopeId, active.threadId, 'turn-complete');
|
|
2894
3133
|
if (revealError) {
|
|
@@ -3595,7 +3834,7 @@ export class BridgeSessionCore {
|
|
|
3595
3834
|
await this.sendMessage(scopeId, t(locale, 'usage_takeover'));
|
|
3596
3835
|
return;
|
|
3597
3836
|
}
|
|
3598
|
-
this.
|
|
3837
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
3599
3838
|
const active = this.findActiveTurn(scopeId);
|
|
3600
3839
|
if (active) {
|
|
3601
3840
|
if (active.isObserved) {
|
|
@@ -3626,9 +3865,7 @@ export class BridgeSessionCore {
|
|
|
3626
3865
|
await this.sendMessage(scopeId, t(locale, 'watch_read_only_active'));
|
|
3627
3866
|
return;
|
|
3628
3867
|
}
|
|
3629
|
-
|
|
3630
|
-
this.queuedPrompts.set(scopeId, { event, text: nextPrompt });
|
|
3631
|
-
await this.sendMessage(scopeId, t(locale, replaced ? 'queued_prompt_replaced' : 'queued_prompt_set'));
|
|
3868
|
+
await this.queuePromptAfterActiveTurn(event, locale, nextPrompt);
|
|
3632
3869
|
}
|
|
3633
3870
|
async handleActiveTurnInboundMessage(event, locale, text) {
|
|
3634
3871
|
const active = this.findActiveTurn(event.scopeId);
|
|
@@ -3663,9 +3900,29 @@ export class BridgeSessionCore {
|
|
|
3663
3900
|
}
|
|
3664
3901
|
}
|
|
3665
3902
|
async queuePromptAfterActiveTurn(event, locale, text) {
|
|
3666
|
-
const
|
|
3667
|
-
|
|
3668
|
-
|
|
3903
|
+
const active = this.findActiveTurn(event.scopeId);
|
|
3904
|
+
if (!active) {
|
|
3905
|
+
await this.startBoundTurnFromEvent(event, locale, text);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
const input = await this.buildTurnInput({
|
|
3909
|
+
threadId: active.threadId,
|
|
3910
|
+
cwd: this.store.getBinding(event.scopeId)?.cwd ?? this.config.defaultCwd,
|
|
3911
|
+
}, { ...event, text }, locale);
|
|
3912
|
+
const queueId = this.enqueuePreparedTurnInput({
|
|
3913
|
+
scopeId: event.scopeId,
|
|
3914
|
+
chatId: event.chatId,
|
|
3915
|
+
chatType: event.chatType,
|
|
3916
|
+
topicId: event.topicId,
|
|
3917
|
+
threadId: active.threadId,
|
|
3918
|
+
input,
|
|
3919
|
+
sourceSummary: summarizeTelegramInput(text, event.attachments),
|
|
3920
|
+
messageId: event.messageId,
|
|
3921
|
+
});
|
|
3922
|
+
await this.sendMessage(event.scopeId, t(locale, 'queued_prompt_set', {
|
|
3923
|
+
id: shortId(queueId),
|
|
3924
|
+
position: this.store.countQueuedTurnInputs(event.scopeId),
|
|
3925
|
+
}));
|
|
3669
3926
|
}
|
|
3670
3927
|
async steerActiveTurn(active, event, locale, text) {
|
|
3671
3928
|
const binding = {
|
|
@@ -3678,16 +3935,87 @@ export class BridgeSessionCore {
|
|
|
3678
3935
|
await this.queueTurnRender(active, { forceStatus: true });
|
|
3679
3936
|
await this.sendMessage(event.scopeId, t(locale, 'steer_sent', { turnId: active.turnId }));
|
|
3680
3937
|
}
|
|
3938
|
+
enqueuePreparedTurnInput(params) {
|
|
3939
|
+
const now = Date.now();
|
|
3940
|
+
const queueId = crypto.randomBytes(8).toString('hex');
|
|
3941
|
+
this.store.saveQueuedTurnInput({
|
|
3942
|
+
queueId,
|
|
3943
|
+
scopeId: params.scopeId,
|
|
3944
|
+
chatId: params.chatId,
|
|
3945
|
+
chatType: params.chatType,
|
|
3946
|
+
topicId: params.topicId,
|
|
3947
|
+
threadId: params.threadId,
|
|
3948
|
+
inputJson: JSON.stringify(params.input),
|
|
3949
|
+
sourceSummary: params.sourceSummary,
|
|
3950
|
+
messageId: params.messageId,
|
|
3951
|
+
status: 'queued',
|
|
3952
|
+
error: null,
|
|
3953
|
+
createdAt: now,
|
|
3954
|
+
updatedAt: now,
|
|
3955
|
+
resolvedAt: null,
|
|
3956
|
+
});
|
|
3957
|
+
return queueId;
|
|
3958
|
+
}
|
|
3959
|
+
async startBoundTurnFromQueuedInput(record) {
|
|
3960
|
+
const scopeId = record.scopeId;
|
|
3961
|
+
this.clearPlanImplementationPromptsForScope(scopeId);
|
|
3962
|
+
await this.stopWatchingScopeThread(scopeId);
|
|
3963
|
+
const existingBinding = this.store.getBinding(scopeId);
|
|
3964
|
+
const binding = await this.ensureThreadReady(scopeId, {
|
|
3965
|
+
chatId: scopeId,
|
|
3966
|
+
threadId: record.threadId,
|
|
3967
|
+
cwd: existingBinding?.cwd ?? null,
|
|
3968
|
+
updatedAt: Date.now(),
|
|
3969
|
+
});
|
|
3970
|
+
this.store.setBinding(scopeId, binding.threadId, binding.cwd);
|
|
3971
|
+
await this.sendTyping(scopeId);
|
|
3972
|
+
const input = parseStoredTurnInput(record.inputJson);
|
|
3973
|
+
const turnState = await this.startTurnWithRecovery(scopeId, binding, input);
|
|
3974
|
+
if (turnState.collaborationMode === 'plan') {
|
|
3975
|
+
this.store.setChatCollaborationMode(scopeId, DEFAULT_COLLABORATION_MODE);
|
|
3976
|
+
}
|
|
3977
|
+
await this.registerActiveTurn(scopeId, record.chatId, record.chatType, record.topicId, turnState.threadId, turnState.turnId, 0, {
|
|
3978
|
+
input,
|
|
3979
|
+
threadId: turnState.threadId,
|
|
3980
|
+
cwd: this.store.getBinding(scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
|
|
3981
|
+
chatId: record.chatId,
|
|
3982
|
+
chatType: record.chatType,
|
|
3983
|
+
topicId: record.topicId,
|
|
3984
|
+
collaborationMode: turnState.collaborationMode,
|
|
3985
|
+
failedAuthTargets: new Set(),
|
|
3986
|
+
}, turnState.collaborationMode, record.queueId);
|
|
3987
|
+
}
|
|
3681
3988
|
async startQueuedPromptIfPresent(scopeId) {
|
|
3682
3989
|
if (this.findActiveTurn(scopeId)) {
|
|
3683
3990
|
return;
|
|
3684
3991
|
}
|
|
3685
|
-
const queued = this.
|
|
3992
|
+
const queued = this.store.peekQueuedTurnInput(scopeId);
|
|
3686
3993
|
if (!queued) {
|
|
3687
3994
|
return;
|
|
3688
3995
|
}
|
|
3689
|
-
this.
|
|
3690
|
-
|
|
3996
|
+
this.store.updateQueuedTurnInputStatus(queued.queueId, 'processing');
|
|
3997
|
+
try {
|
|
3998
|
+
await this.startBoundTurnFromQueuedInput(queued);
|
|
3999
|
+
}
|
|
4000
|
+
catch (error) {
|
|
4001
|
+
this.store.updateQueuedTurnInputStatus(queued.queueId, 'failed', formatUserError(error));
|
|
4002
|
+
throw error;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
async recoverQueuedTurns() {
|
|
4006
|
+
const scopeIds = new Set(this.store.listQueuedTurnInputs().map((record) => record.scopeId));
|
|
4007
|
+
for (const scopeId of scopeIds) {
|
|
4008
|
+
if (!this.messaging.canSendToScope(scopeId)) {
|
|
4009
|
+
continue;
|
|
4010
|
+
}
|
|
4011
|
+
await this.withLock(scopeId, async () => this.startQueuedPromptIfPresent(scopeId));
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
markQueuedTurnCompleted(active) {
|
|
4015
|
+
if (!active.queuedInputId) {
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
this.store.updateQueuedTurnInputStatus(active.queuedInputId, 'completed');
|
|
3691
4019
|
}
|
|
3692
4020
|
async handleModeCommand(scopeId, locale, args) {
|
|
3693
4021
|
const raw = args.join(' ').trim();
|
|
@@ -3954,16 +4282,28 @@ export class BridgeSessionCore {
|
|
|
3954
4282
|
}
|
|
3955
4283
|
formatSelfUpdateResult(status) {
|
|
3956
4284
|
const codexUpdateLine = this.formatCodexUpdateResult(status);
|
|
4285
|
+
const releaseNotes = this.formatSelfUpdateReleaseNotes(status);
|
|
3957
4286
|
if (status.state === 'succeeded') {
|
|
3958
4287
|
const result = t(status.locale, 'update_succeeded', {
|
|
3959
4288
|
from: status.fromVersion,
|
|
3960
4289
|
to: status.toVersion ?? t(status.locale, 'unknown'),
|
|
3961
4290
|
});
|
|
3962
|
-
return
|
|
4291
|
+
return [result, releaseNotes, codexUpdateLine].filter(Boolean).join('\n');
|
|
3963
4292
|
}
|
|
3964
4293
|
const result = t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
|
|
3965
4294
|
return codexUpdateLine ? `${result}\n${codexUpdateLine}` : result;
|
|
3966
4295
|
}
|
|
4296
|
+
formatSelfUpdateReleaseNotes(status) {
|
|
4297
|
+
const notes = status.releaseNotes?.filter(note => note.trim()) ?? [];
|
|
4298
|
+
if (status.state !== 'succeeded' || notes.length === 0) {
|
|
4299
|
+
return null;
|
|
4300
|
+
}
|
|
4301
|
+
return [
|
|
4302
|
+
'',
|
|
4303
|
+
t(status.locale, 'update_changes_title'),
|
|
4304
|
+
...notes.map(note => `- ${note}`),
|
|
4305
|
+
].join('\n');
|
|
4306
|
+
}
|
|
3967
4307
|
formatCodexUpdateResult(status) {
|
|
3968
4308
|
if (status.codexFromVersion || status.codexToVersion) {
|
|
3969
4309
|
return t(status.locale, 'update_codex_result', {
|
|
@@ -4127,17 +4467,19 @@ export class BridgeSessionCore {
|
|
|
4127
4467
|
await this.sendMessage(scopeId, message);
|
|
4128
4468
|
return;
|
|
4129
4469
|
}
|
|
4130
|
-
if (action === 'push' && args[1]?.toLowerCase() === 'all') {
|
|
4470
|
+
if (action === 'safe' || (action === 'push' && args[1]?.toLowerCase() === 'all')) {
|
|
4131
4471
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4132
4472
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
|
|
4133
4473
|
return;
|
|
4134
4474
|
}
|
|
4135
|
-
const result = await this.
|
|
4475
|
+
const result = await this.runAuthSafeSyncAll();
|
|
4136
4476
|
if (!result) {
|
|
4137
4477
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
|
|
4138
4478
|
return;
|
|
4139
4479
|
}
|
|
4140
|
-
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,
|
|
4141
4483
|
sent: result.sent,
|
|
4142
4484
|
skipped: result.skipped,
|
|
4143
4485
|
}));
|
|
@@ -4145,6 +4487,16 @@ export class BridgeSessionCore {
|
|
|
4145
4487
|
}
|
|
4146
4488
|
await this.sendMessage(scopeId, t(locale, 'usage_auth_sync'));
|
|
4147
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
|
+
}
|
|
4148
4500
|
async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
|
|
4149
4501
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4150
4502
|
await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -4653,6 +5005,37 @@ export class BridgeSessionCore {
|
|
|
4653
5005
|
await this.handleLoginDeviceCommand(event.scopeId, locale);
|
|
4654
5006
|
return;
|
|
4655
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
|
+
}
|
|
4656
5039
|
if (action === 'refresh_all') {
|
|
4657
5040
|
if (!this.canRunGlobalAuthRefresh()) {
|
|
4658
5041
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_refresh_all_blocked_active'));
|
|
@@ -5707,7 +6090,7 @@ export class BridgeSessionCore {
|
|
|
5707
6090
|
await this.createAndBindNewThread(scopeId, locale, cwd, panelMessageId);
|
|
5708
6091
|
}
|
|
5709
6092
|
async createAndBindNewThread(scopeId, locale, cwd, panelMessageId) {
|
|
5710
|
-
this.
|
|
6093
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
5711
6094
|
await this.stopWatchingScopeThread(scopeId);
|
|
5712
6095
|
const binding = await this.createBinding(scopeId, cwd);
|
|
5713
6096
|
await this.sendNewThreadStartedMessage(scopeId, locale, binding, cwd);
|
|
@@ -6453,6 +6836,9 @@ export class BridgeSessionCore {
|
|
|
6453
6836
|
if (active.previewActive) {
|
|
6454
6837
|
await this.retirePreviewMessage(active.scopeId, active.previewMessageId, t(this.localeForChat(active.scopeId), 'stale_preview_expired'), active.turnId);
|
|
6455
6838
|
}
|
|
6839
|
+
if (active.queuedInputId) {
|
|
6840
|
+
this.store.updateQueuedTurnInputStatus(active.queuedInputId, 'queued');
|
|
6841
|
+
}
|
|
6456
6842
|
active.resolver();
|
|
6457
6843
|
this.deleteActiveTurnRecord(active);
|
|
6458
6844
|
}
|
|
@@ -8066,7 +8452,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8066
8452
|
if (state.candidates.length === 0) {
|
|
8067
8453
|
lines.push(t(locale, 'auth_no_candidates'));
|
|
8068
8454
|
if (includeWeixinCopyPaste) {
|
|
8069
|
-
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth reload', '/permissions');
|
|
8455
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8070
8456
|
}
|
|
8071
8457
|
return lines.join('\n');
|
|
8072
8458
|
}
|
|
@@ -8099,7 +8485,7 @@ function renderAuthListMessage(locale, state, botLabel = null, includeWeixinCopy
|
|
|
8099
8485
|
if (includeWeixinCopyPaste) {
|
|
8100
8486
|
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
|
|
8101
8487
|
? `/auth enable ${index + 1}`
|
|
8102
|
-
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth reload', '/permissions');
|
|
8488
|
+
: `/auth disable ${index + 1}`), '/auth filter all', '/auth filter enabled', '/auth filter attention', '/auth list <keyword>', '/login_device', '/auth sync safe', '/auth reload', '/permissions');
|
|
8103
8489
|
}
|
|
8104
8490
|
return lines.join('\n');
|
|
8105
8491
|
}
|
|
@@ -8137,7 +8523,10 @@ function authChoiceKeyboard(locale, record) {
|
|
|
8137
8523
|
{ text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
|
|
8138
8524
|
{ text: t(locale, 'button_login_device'), callback_data: `auth:${record.localId}:login_device` },
|
|
8139
8525
|
]);
|
|
8140
|
-
rows.push([
|
|
8526
|
+
rows.push([
|
|
8527
|
+
{ text: t(locale, 'button_auth_safe_sync'), callback_data: `auth:${record.localId}:safe_sync` },
|
|
8528
|
+
{ text: t(locale, 'button_auth_reload'), callback_data: `auth:${record.localId}:reload` },
|
|
8529
|
+
]);
|
|
8141
8530
|
return rows;
|
|
8142
8531
|
}
|
|
8143
8532
|
function formatCodexAuthCandidateDisplayName(name) {
|
|
@@ -8743,6 +9132,78 @@ function userInputKeyboard(record) {
|
|
|
8743
9132
|
});
|
|
8744
9133
|
return rows;
|
|
8745
9134
|
}
|
|
9135
|
+
function parseStoredTurnInput(inputJson) {
|
|
9136
|
+
const value = JSON.parse(inputJson);
|
|
9137
|
+
if (!Array.isArray(value)) {
|
|
9138
|
+
throw new Error('Queued turn input is not an array');
|
|
9139
|
+
}
|
|
9140
|
+
return value;
|
|
9141
|
+
}
|
|
9142
|
+
function parseStagedTelegramAttachments(attachmentsJson) {
|
|
9143
|
+
const value = JSON.parse(attachmentsJson);
|
|
9144
|
+
if (!Array.isArray(value)) {
|
|
9145
|
+
throw new Error('Attachment batch payload is not an array');
|
|
9146
|
+
}
|
|
9147
|
+
return value.map((entry) => {
|
|
9148
|
+
if (!entry || typeof entry !== 'object') {
|
|
9149
|
+
throw new Error('Attachment batch contains an invalid entry');
|
|
9150
|
+
}
|
|
9151
|
+
const attachment = entry;
|
|
9152
|
+
if (typeof attachment.localPath !== 'string' || typeof attachment.relativePath !== 'string') {
|
|
9153
|
+
throw new Error('Attachment batch entry is missing a local path');
|
|
9154
|
+
}
|
|
9155
|
+
return attachment;
|
|
9156
|
+
});
|
|
9157
|
+
}
|
|
9158
|
+
function findReusableStandaloneAttachmentBatch(batch, now) {
|
|
9159
|
+
if (!batch || batch.mediaGroupId !== null) {
|
|
9160
|
+
return null;
|
|
9161
|
+
}
|
|
9162
|
+
return now - batch.updatedAt <= ATTACHMENT_BATCH_MERGE_WINDOW_MS ? batch : null;
|
|
9163
|
+
}
|
|
9164
|
+
function mergeAttachmentBatchCaption(existing, next) {
|
|
9165
|
+
const normalizedNext = next.trim();
|
|
9166
|
+
if (!normalizedNext) {
|
|
9167
|
+
return existing;
|
|
9168
|
+
}
|
|
9169
|
+
const normalizedExisting = existing.trim();
|
|
9170
|
+
if (!normalizedExisting) {
|
|
9171
|
+
return normalizedNext;
|
|
9172
|
+
}
|
|
9173
|
+
return normalizedExisting === normalizedNext ? normalizedExisting : `${normalizedExisting}\n\n${normalizedNext}`;
|
|
9174
|
+
}
|
|
9175
|
+
function summarizeStagedAttachmentInput(text, attachments) {
|
|
9176
|
+
const lines = [text.trim() || '(no text)'];
|
|
9177
|
+
if (attachments.length > 0) {
|
|
9178
|
+
lines.push(`[attachments: ${attachments.map((attachment) => `${attachment.kind}:${attachment.fileName}`).join(', ')}]`);
|
|
9179
|
+
}
|
|
9180
|
+
return lines.join('\n');
|
|
9181
|
+
}
|
|
9182
|
+
function renderAttachmentBatchMessage(locale, record) {
|
|
9183
|
+
const attachments = parseStagedTelegramAttachments(record.attachmentsJson);
|
|
9184
|
+
const lines = [
|
|
9185
|
+
t(locale, 'attachment_batch_staged', { count: attachments.length }),
|
|
9186
|
+
];
|
|
9187
|
+
if (record.caption.trim()) {
|
|
9188
|
+
lines.push('', t(locale, 'attachment_batch_caption', { value: truncateInline(record.caption.trim(), 600) }));
|
|
9189
|
+
}
|
|
9190
|
+
lines.push('');
|
|
9191
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
9192
|
+
const name = attachment.fileName || attachment.fileUniqueId;
|
|
9193
|
+
const size = attachment.fileSize === null ? '' : `, ${attachment.fileSize} bytes`;
|
|
9194
|
+
lines.push(`${index + 1}. ${attachment.kind}: ${name}${size}`);
|
|
9195
|
+
}
|
|
9196
|
+
return lines.join('\n');
|
|
9197
|
+
}
|
|
9198
|
+
function attachmentBatchKeyboard(locale, batchId) {
|
|
9199
|
+
return [[
|
|
9200
|
+
{ text: t(locale, 'attachment_batch_button_analyze'), callback_data: `attach:${batchId}:analyze` },
|
|
9201
|
+
{ text: t(locale, 'attachment_batch_button_clear'), callback_data: `attach:${batchId}:clear` },
|
|
9202
|
+
]];
|
|
9203
|
+
}
|
|
9204
|
+
function shortId(value) {
|
|
9205
|
+
return value.slice(0, 8);
|
|
9206
|
+
}
|
|
8746
9207
|
function renderPlanImplementationPrompt(locale, record) {
|
|
8747
9208
|
const lines = [
|
|
8748
9209
|
t(locale, 'plan_impl_title'),
|