@foxden-app/foxclaw 0.12.0 → 0.13.1
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 +34 -0
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -108
- package/dist/antigravity/controller.js +2 -2129
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +19 -4
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +53 -24
- package/dist/core/orchestrator.js +855 -507
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.js +18 -5
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/runtime.d.ts +0 -1
- package/dist/dsh/runtime.js +4 -13
- package/dist/dsh/ui.d.ts +8 -8
- package/dist/dsh/ui.js +1 -1
- package/dist/main.js +55 -43
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +7 -3
- package/dist/store/database.js +7 -2
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +1 -1
- package/dist/update.d.ts +27 -2
- package/dist/update.js +81 -28
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +8 -5
|
@@ -1,18 +1,17 @@
|
|
|
1
|
+
import { ListenerScope } from './listener_scope.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
1
3
|
import fs from 'node:fs';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import os from 'node:os';
|
|
4
6
|
import { parseCommand } from '../controller/commands.js';
|
|
5
|
-
import {
|
|
6
|
-
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
7
|
-
import { chunkTelegramMessage } from '../telegram/text.js';
|
|
7
|
+
import { chunkMessage, escapeHtml } from './message_text.js';
|
|
8
8
|
import { stageInboundAttachments } from './attachments.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { BackendRegistry } from './backend_registry.js';
|
|
10
|
+
import { decodeQueuedEngineInput } from './queued_engine_input.js';
|
|
11
|
+
import { ScopeOperations } from './scope_operations.js';
|
|
11
12
|
import { renderStreamPreviewContent, buildFoldedToolsSummary, combineSummaryAndResponse } from './stream_preview.js';
|
|
12
|
-
import { BRIDGE_SCOPE_TELEGRAM_PREFIX, BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope } from './bridge_scope.js';
|
|
13
|
-
import { escapeTelegramHtml } from '../telegram/html.js';
|
|
14
13
|
import { formatTokenUsageSummary, formatBackendTokenUsageBreakdown } from '../store/token_usage.js';
|
|
15
|
-
import { isTransientNetworkError } from '
|
|
14
|
+
import { isTransientNetworkError } from './network_errors.js';
|
|
16
15
|
export const STREAM_THROTTLE_MS = 700;
|
|
17
16
|
export const TYPING_INTERVAL_MS = 4000;
|
|
18
17
|
export class UnifiedChannelOrchestrator {
|
|
@@ -21,9 +20,13 @@ export class UnifiedChannelOrchestrator {
|
|
|
21
20
|
logger;
|
|
22
21
|
bot;
|
|
23
22
|
messaging;
|
|
24
|
-
queueManager;
|
|
25
23
|
customUi;
|
|
26
|
-
backends
|
|
24
|
+
backends;
|
|
25
|
+
scopeOperations = new ScopeOperations();
|
|
26
|
+
messageOperations = new ScopeOperations();
|
|
27
|
+
settlements = new Set();
|
|
28
|
+
stopped = false;
|
|
29
|
+
serviceUi;
|
|
27
30
|
defaultBackendId;
|
|
28
31
|
activeTurns = new Map();
|
|
29
32
|
stalePanelDeleteTimers = new Map();
|
|
@@ -39,7 +42,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
39
42
|
this.logger = options.logger;
|
|
40
43
|
this.bot = options.bot;
|
|
41
44
|
this.messaging = options.messaging;
|
|
42
|
-
this.
|
|
45
|
+
this.backends = new BackendRegistry(this);
|
|
46
|
+
this.serviceUi = options.serviceUi;
|
|
43
47
|
this.customUi = options.customUi;
|
|
44
48
|
this.backendProvider = options.backendProvider;
|
|
45
49
|
this.ownsScope = options.ownsScope ?? (() => true);
|
|
@@ -75,30 +79,25 @@ export class UnifiedChannelOrchestrator {
|
|
|
75
79
|
}
|
|
76
80
|
const firstId = Array.from(this.backends.keys())[0] ?? 'default';
|
|
77
81
|
this.defaultBackendId = options.defaultBackendId ?? options.adapter?.id ?? firstId;
|
|
82
|
+
if (!this.backends.has(this.defaultBackendId))
|
|
83
|
+
throw new Error(`Default backend '${this.defaultBackendId}' is not registered`);
|
|
78
84
|
}
|
|
79
85
|
getAdapterForScope(scopeId) {
|
|
80
86
|
const activeBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
81
87
|
return this.getAdapterForBackend(activeBackendId);
|
|
82
88
|
}
|
|
83
89
|
getBackendDescriptorForScope(scopeId) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
id
|
|
88
|
-
|
|
89
|
-
engineType: activeBackendId,
|
|
90
|
-
adapter: this.adapter,
|
|
91
|
-
});
|
|
90
|
+
const id = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
91
|
+
const backend = this.backends.get(id);
|
|
92
|
+
if (!backend)
|
|
93
|
+
throw new Error(`Backend '${id}' is unavailable; use /backend to select an available backend`);
|
|
94
|
+
return backend;
|
|
92
95
|
}
|
|
93
96
|
getAdapterForBackend(backendId) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!fallback) {
|
|
99
|
-
throw new Error(`No engine adapter available in orchestrator for backend '${backendId}'`);
|
|
100
|
-
}
|
|
101
|
-
return fallback.adapter;
|
|
97
|
+
const backend = this.backends.get(backendId);
|
|
98
|
+
if (!backend)
|
|
99
|
+
throw new Error(`Backend '${backendId}' is unavailable`);
|
|
100
|
+
return backend.adapter;
|
|
102
101
|
}
|
|
103
102
|
registerBackend(desc) {
|
|
104
103
|
this.backends.set(desc.id, desc);
|
|
@@ -112,9 +111,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
112
111
|
for (const b of dynamicList) {
|
|
113
112
|
list.push(b);
|
|
114
113
|
seen.add(b.id);
|
|
115
|
-
|
|
116
|
-
this.backends.set(b.id, b);
|
|
117
|
-
}
|
|
114
|
+
this.backends.set(b.id, b);
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
catch (err) {
|
|
@@ -149,7 +146,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
149
146
|
});
|
|
150
147
|
}
|
|
151
148
|
scheduleStalePanelDeletion(scopeId, messageId) {
|
|
152
|
-
if (this.config.telegramPanelTtlMs <= 0 ||
|
|
149
|
+
if (this.config.telegramPanelTtlMs <= 0 || this.messaging.capabilities?.editableMessages === false) {
|
|
153
150
|
return;
|
|
154
151
|
}
|
|
155
152
|
const key = `${scopeId}:${messageId}`;
|
|
@@ -169,6 +166,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
169
166
|
this.stalePanelDeleteTimers.set(key, timer);
|
|
170
167
|
}
|
|
171
168
|
async switchBackend(scopeId, targetBackendId, locale) {
|
|
169
|
+
return this.scopeOperations.run(scopeId, () => this.switchBackendNow(scopeId, targetBackendId, locale));
|
|
170
|
+
}
|
|
171
|
+
async switchBackendNow(scopeId, targetBackendId, locale) {
|
|
172
172
|
const currentBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
173
173
|
const targetDesc = await this.resolveBackendDescriptor(targetBackendId);
|
|
174
174
|
if (!targetDesc) {
|
|
@@ -176,8 +176,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
176
176
|
? `未找到目标后端: ${targetBackendId}`
|
|
177
177
|
: `Target backend not found: ${targetBackendId}`);
|
|
178
178
|
}
|
|
179
|
-
if (this.
|
|
180
|
-
throw new Error(
|
|
179
|
+
if (this.stopped)
|
|
180
|
+
throw new Error('Orchestrator is stopped');
|
|
181
|
+
if (this.activeTurns.has(scopeId) || (currentBackendId !== targetBackendId && (this.store.countQueuedTurnInputs(scopeId) > 0 || this.store.taskJournal.listUnfinished(scopeId).length > 0))) {
|
|
182
|
+
throw new Error(locale === 'zh' ? '请先中断当前任务并清空队列,再切换后端。' : 'Interrupt the active turn and clear its queue before switching backends.');
|
|
181
183
|
}
|
|
182
184
|
if (targetDesc.onSelect) {
|
|
183
185
|
await targetDesc.onSelect(scopeId);
|
|
@@ -212,7 +214,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
212
214
|
else {
|
|
213
215
|
this.store.clearBinding(scopeId);
|
|
214
216
|
}
|
|
215
|
-
this.store.setChatSettings(scopeId, saved.model ?? null,
|
|
217
|
+
this.store.setChatSettings(scopeId, saved.model ?? null, null);
|
|
218
|
+
this.store.setChatEngineEffort(scopeId, saved.reasoningEffort ?? null);
|
|
216
219
|
if (saved.activeTurnMessageMode !== undefined && saved.activeTurnMessageMode !== null) {
|
|
217
220
|
this.store.setChatActiveTurnMessageMode(scopeId, saved.activeTurnMessageMode ?? null);
|
|
218
221
|
}
|
|
@@ -225,14 +228,15 @@ export class UnifiedChannelOrchestrator {
|
|
|
225
228
|
}
|
|
226
229
|
else {
|
|
227
230
|
this.store.clearBinding(scopeId);
|
|
228
|
-
|
|
229
|
-
this.store.
|
|
231
|
+
this.store.setChatModel(scopeId, null);
|
|
232
|
+
this.store.setChatEngineEffort(scopeId, targetDesc.defaults?.reasoningEffort ?? null);
|
|
233
|
+
this.store.setChatAccessPreset(scopeId, null);
|
|
234
|
+
this.store.setChatCollaborationMode(scopeId, null);
|
|
230
235
|
this.store.setChatServiceTier(scopeId, null);
|
|
231
236
|
}
|
|
232
|
-
const isCodex = targetDesc.engineType === 'codex';
|
|
233
237
|
const activeSettings = this.store.getChatSettings(scopeId);
|
|
234
238
|
const modelText = activeSettings?.model ? `\`${activeSettings.model}\`` : (locale === 'zh' ? '引擎默认' : 'default');
|
|
235
|
-
const effortText = activeSettings?.reasoningEffort ??
|
|
239
|
+
const effortText = activeSettings?.reasoningEffort ?? targetDesc.defaults?.reasoningEffort ?? 'default';
|
|
236
240
|
const modeText = activeSettings?.activeTurnMessageMode ?? 'queue';
|
|
237
241
|
const switchMsg = locale === 'zh'
|
|
238
242
|
? `🔄 **已切换至后端: ${targetDesc.name}** (\`${targetDesc.id}\`)\n\n` +
|
|
@@ -254,11 +258,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
254
258
|
this.logger.warn('orchestrator.switch_announce_failed', { error: String(err) });
|
|
255
259
|
}
|
|
256
260
|
try {
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
const cmds = targetDesc.engineType === 'dsh' ? getDshTelegramCommands(locale) : isCodex ? getTelegramCommands(locale) : getAntigravityTelegramCommands(locale);
|
|
260
|
-
await this.bot.setChatCommands(target.chatId, cmds);
|
|
261
|
-
}
|
|
261
|
+
if (targetDesc.commands)
|
|
262
|
+
await this.messaging.setScopeCommands?.(scopeId, targetDesc.commands(locale));
|
|
262
263
|
}
|
|
263
264
|
catch (err) {
|
|
264
265
|
this.logger.warn('orchestrator.set_chat_commands_failed', { error: String(err) });
|
|
@@ -276,15 +277,44 @@ export class UnifiedChannelOrchestrator {
|
|
|
276
277
|
cwd: saved?.cwd ?? null,
|
|
277
278
|
};
|
|
278
279
|
}
|
|
280
|
+
removeInboundConsumer = null;
|
|
281
|
+
intakeOperations = new ScopeOperations();
|
|
282
|
+
async receiveInbound(id, inbound) {
|
|
283
|
+
if (this.stopped || !this.ownsScope(inbound.event.scopeId))
|
|
284
|
+
return;
|
|
285
|
+
if (!this.store.channelInbox.accept(id, inbound))
|
|
286
|
+
return;
|
|
287
|
+
await this.intakeOperations.run(id, async () => {
|
|
288
|
+
if (this.stopped)
|
|
289
|
+
return;
|
|
290
|
+
if (!this.store.channelInbox.accept(id, inbound))
|
|
291
|
+
return;
|
|
292
|
+
if (inbound.kind === 'text')
|
|
293
|
+
await this.handleText(inbound.event);
|
|
294
|
+
else if (inbound.kind === 'callback')
|
|
295
|
+
await this.handleCallback(inbound.event);
|
|
296
|
+
else
|
|
297
|
+
await this.stopTask(inbound.event.scopeId, inbound.event.taskId);
|
|
298
|
+
if (!this.stopped)
|
|
299
|
+
this.store.channelInbox.complete(id);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
inboundListeners = new ListenerScope();
|
|
279
303
|
registerInboundHandlers() {
|
|
280
|
-
this.
|
|
304
|
+
this.inboundListeners.clear();
|
|
305
|
+
this.removeInboundConsumer?.();
|
|
306
|
+
if (this.bot.setInboundConsumer) {
|
|
307
|
+
this.removeInboundConsumer = this.bot.setInboundConsumer((id, inbound) => this.receiveInbound(id, inbound));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.inboundListeners.listen(this.bot, 'text', (event) => {
|
|
281
311
|
this.handleText(event).catch((err) => {
|
|
282
312
|
this.logger.error('orchestrator.inbound_text_error', {
|
|
283
313
|
error: err instanceof Error ? err.message : String(err),
|
|
284
314
|
});
|
|
285
315
|
});
|
|
286
316
|
});
|
|
287
|
-
this.
|
|
317
|
+
this.inboundListeners.listen(this.bot, 'callback', (event) => {
|
|
288
318
|
this.handleCallback(event).catch((err) => {
|
|
289
319
|
this.logger.error('orchestrator.inbound_callback_error', {
|
|
290
320
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -300,141 +330,266 @@ export class UnifiedChannelOrchestrator {
|
|
|
300
330
|
});
|
|
301
331
|
}
|
|
302
332
|
async start() {
|
|
333
|
+
this.stopped = false;
|
|
334
|
+
await this.restoreDurableTasks();
|
|
335
|
+
for (const pending of this.store.channelInbox.pending())
|
|
336
|
+
if (this.ownsScope(pending.inbound.event.scopeId)) {
|
|
337
|
+
await this.receiveInbound(pending.id, pending.inbound).catch(error => this.logger.warn('orchestrator.intake_recovery_failed', { id: pending.id, error: String(error) }));
|
|
338
|
+
}
|
|
303
339
|
await this.bot.start();
|
|
304
340
|
this.logger.info('orchestrator.started', {
|
|
305
341
|
defaultEngine: this.adapter.id,
|
|
306
342
|
backends: Array.from(this.backends.keys()),
|
|
307
343
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
344
|
+
}
|
|
345
|
+
recoveryEvent(scopeId, messageId = 0) {
|
|
346
|
+
return { scopeId, chatId: scopeId, topicId: null, chatType: 'private', userId: 'system', messageId,
|
|
347
|
+
text: '', attachments: [], entities: [], replyToBot: false };
|
|
348
|
+
}
|
|
349
|
+
async showRecovery(task) {
|
|
350
|
+
const zh = task.request.locale === 'zh';
|
|
351
|
+
const actions = [[
|
|
352
|
+
{ text: zh ? '继续原任务' : 'Continue', callback_data: `engine:recover:continue:${task.id}` },
|
|
353
|
+
{ text: zh ? '重新执行' : 'Retry', callback_data: `engine:recover:retry:${task.id}` },
|
|
354
|
+
{ text: zh ? '放弃' : 'Cancel', callback_data: `engine:recover:cancel:${task.id}` },
|
|
355
|
+
]];
|
|
356
|
+
await this.sendMessage(task.event.scopeId, zh
|
|
357
|
+
? `⚠️ **任务执行结果待确认**\n任务: \`${task.id}\`\n${task.error ? `恢复失败: ${task.error}。` : '服务重启前可能已执行工具。'}原始输入与附件已保留,队列暂停。\n> ${task.sourcePrompt}\n\n/recover continue · /recover retry · /recover cancel`
|
|
358
|
+
: `⚠️ **Task outcome requires confirmation**\nTask: \`${task.id}\`\n${task.error ? `Recovery failed: ${task.error}.` : 'Tools may already have run before the restart.'} Input and attachments are preserved; the queue is paused.\n> ${task.sourcePrompt}\n\n/recover continue · /recover retry · /recover cancel`, actions);
|
|
359
|
+
}
|
|
360
|
+
async restoreDurableTasks() {
|
|
361
|
+
const tasks = this.store.taskJournal.listUnfinished().filter(task => this.ownsScope(task.event.scopeId));
|
|
362
|
+
// A queued journal entry proves the executor was never invoked, even if queue claiming was interrupted.
|
|
363
|
+
for (const task of tasks)
|
|
364
|
+
if (task.state === 'queued' && task.queueId && this.store.getQueuedTurnInput(task.queueId)?.status === 'processing') {
|
|
365
|
+
this.store.updateQueuedTurnInputStatus(task.queueId, 'queued');
|
|
314
366
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Crash recovery: check interrupted preview messages from before restart
|
|
320
|
-
try {
|
|
321
|
-
const backendIds = new Set(this.backends.keys());
|
|
322
|
-
for (const desc of this.backends.values()) {
|
|
323
|
-
backendIds.add(desc.adapter.id);
|
|
367
|
+
// Claim uncertainty synchronously before recovering messages or starting any queued work.
|
|
368
|
+
for (const task of tasks)
|
|
369
|
+
if (task.state === 'running' && !task.result) {
|
|
370
|
+
this.store.taskJournal.update(task.id, 'awaiting_confirmation');
|
|
324
371
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
372
|
+
// Import pre-journal previews and processing queues conservatively; their outcome is unknown.
|
|
373
|
+
const journalScopes = new Set(tasks.filter(task => task.state !== 'queued').map(task => task.event.scopeId));
|
|
374
|
+
for (const preview of this.store.listActiveTurnPreviews()) {
|
|
375
|
+
if (!this.ownsScope(preview.scopeId) || journalScopes.has(preview.scopeId))
|
|
376
|
+
continue;
|
|
377
|
+
const backend = [...this.backends.values()].find(b => preview.turnId.startsWith(`${b.adapter.id}_`));
|
|
378
|
+
if (!backend)
|
|
379
|
+
continue;
|
|
380
|
+
const locale = this.store.getChatSettings(preview.scopeId)?.locale ?? 'zh';
|
|
381
|
+
const queued = this.store.listQueuedTurnInputs(preview.scopeId).find(q => q.status === 'processing');
|
|
382
|
+
const input = queued ? decodeQueuedEngineInput(queued.inputJson, queued.sourceSummary) : null;
|
|
383
|
+
const event = this.recoveryEvent(preview.scopeId, preview.messageId);
|
|
384
|
+
const task = this.captureTask(event, input?.prompt ?? (locale === 'zh' ? '继续重启前的任务' : 'Continue the task interrupted by restart'), locale, input?.stagedAttachments, queued?.queueId ?? null);
|
|
385
|
+
this.store.taskJournal.update(task.id, 'running', { previewMessageId: preview.messageId, request: { ...task.request, threadId: preview.threadId || null } });
|
|
386
|
+
this.store.taskJournal.update(task.id, 'awaiting_confirmation');
|
|
387
|
+
journalScopes.add(preview.scopeId);
|
|
388
|
+
}
|
|
389
|
+
for (const queued of this.store.listQueuedTurnInputs()) {
|
|
390
|
+
if (!this.ownsScope(queued.scopeId) || queued.status !== 'processing' || this.store.taskJournal.forQueue(queued.queueId))
|
|
391
|
+
continue;
|
|
392
|
+
if (!this.backends.has(this.store.getActiveBackend(queued.scopeId) || this.defaultBackendId))
|
|
393
|
+
continue;
|
|
394
|
+
const input = decodeQueuedEngineInput(queued.inputJson, queued.sourceSummary);
|
|
395
|
+
const event = { ...this.recoveryEvent(queued.scopeId, queued.messageId ?? 0), chatId: queued.chatId, topicId: queued.topicId };
|
|
396
|
+
const locale = this.store.getChatSettings(queued.scopeId)?.locale ?? 'zh';
|
|
397
|
+
const task = this.captureTask(event, input.prompt, locale, input.stagedAttachments, queued.queueId);
|
|
398
|
+
this.store.taskJournal.update(task.id, 'running');
|
|
399
|
+
this.store.taskJournal.update(task.id, 'awaiting_confirmation');
|
|
400
|
+
}
|
|
401
|
+
for (let task of this.store.taskJournal.listUnfinished()) {
|
|
402
|
+
if (!this.ownsScope(task.event.scopeId))
|
|
403
|
+
continue;
|
|
404
|
+
if (task.state === 'running' && task.result) {
|
|
405
|
+
const text = task.result.response || task.result.error || (task.request.locale === 'zh' ? '任务已结束' : 'Task ended');
|
|
406
|
+
task = this.store.taskJournal.update(task.id, 'delivery_pending', { delivery: chunkMessage(text, this.messaging.capabilities?.maxMessageLength) });
|
|
407
|
+
}
|
|
408
|
+
if (task.state === 'awaiting_confirmation')
|
|
409
|
+
await this.showRecovery(task).catch(error => this.logger.warn('orchestrator.recovery_notice_failed', { taskId: task.id, error: String(error) }));
|
|
410
|
+
if (task.state === 'delivery_pending') {
|
|
411
|
+
try {
|
|
412
|
+
await this.deliverTaskChunks(task.id);
|
|
413
|
+
this.resolveDeliveredTask(task.id);
|
|
331
414
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const recoveredScopes = new Set();
|
|
335
|
-
for (const prev of activePreviews) {
|
|
336
|
-
this.store.removeActiveTurnPreview(prev.turnId);
|
|
337
|
-
recoveredScopes.add(prev.scopeId);
|
|
338
|
-
// Bind existing threadId immediately so auto-resume continues in the exact same thread
|
|
339
|
-
if (prev.threadId) {
|
|
340
|
-
const currentBinding = this.store.getBinding(prev.scopeId);
|
|
341
|
-
this.store.setBinding(prev.scopeId, prev.threadId, currentBinding?.cwd ?? this.config.defaultCwd);
|
|
415
|
+
catch (error) {
|
|
416
|
+
this.logger.warn('orchestrator.delivery_retry_failed', { taskId: task.id, error: String(error) });
|
|
342
417
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
reuseMessageId: prev.messageId,
|
|
366
|
-
forcedThreadId: prev.threadId || undefined,
|
|
367
|
-
}).catch((err) => {
|
|
368
|
-
this.logger.warn('orchestrator.auto_resume_failed', {
|
|
369
|
-
scopeId: prev.scopeId,
|
|
370
|
-
error: String(err),
|
|
371
|
-
});
|
|
418
|
+
}
|
|
419
|
+
if (task.state === 'accepted') {
|
|
420
|
+
const timer = setTimeout(() => {
|
|
421
|
+
this.recoveryTimers.delete(timer);
|
|
422
|
+
void this.scopeOperations.run(task.event.scopeId, async () => {
|
|
423
|
+
if (this.stopped)
|
|
424
|
+
return;
|
|
425
|
+
const attachments = task.request.stagedAttachments ?? await stageInboundAttachments(this.messaging, task.request.cwd, task.request.threadId || 'default', task.event.attachments, this.logger);
|
|
426
|
+
if (attachments.length !== task.event.attachments.length && !task.request.stagedAttachments)
|
|
427
|
+
throw new Error('Recovery attachment staging failed');
|
|
428
|
+
this.store.taskJournal.update(task.id, 'accepted', { request: { ...task.request, stagedAttachments: attachments } });
|
|
429
|
+
if (this.activeTurns.has(task.event.scopeId) || this.recoveryBlocksScope(task.event.scopeId)) {
|
|
430
|
+
await this.enqueuePromptTurn(task.event, task.sourcePrompt, task.request.locale, attachments, task.id);
|
|
431
|
+
}
|
|
432
|
+
else
|
|
433
|
+
await this.executeTurn(task.event, task.sourcePrompt, task.request.locale, 0, attachments, { taskId: task.id, forcedThreadId: task.request.threadId ?? undefined });
|
|
434
|
+
}).catch(async (error) => {
|
|
435
|
+
this.logger.warn('orchestrator.accepted_recovery_failed', { taskId: task.id, error: String(error) });
|
|
436
|
+
if (!this.stopped && this.store.taskJournal.get(task.id)?.state === 'accepted') {
|
|
437
|
+
const waiting = this.store.taskJournal.update(task.id, 'awaiting_confirmation', { error: String(error) });
|
|
438
|
+
await this.showRecovery(waiting).catch(() => { });
|
|
439
|
+
}
|
|
372
440
|
});
|
|
373
441
|
}, 1500);
|
|
374
|
-
this.recoveryTimers.add(
|
|
375
|
-
|
|
442
|
+
this.recoveryTimers.add(timer);
|
|
443
|
+
timer.unref();
|
|
376
444
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
445
|
+
}
|
|
446
|
+
const scopes = new Set(this.store.listQueuedTurnInputs().filter(input => this.ownsScope(input.scopeId)).map(input => input.scopeId));
|
|
447
|
+
for (const scopeId of scopes) {
|
|
448
|
+
const timer = setTimeout(() => {
|
|
449
|
+
this.recoveryTimers.delete(timer);
|
|
450
|
+
void this.drainNextQueuedTurn(this.recoveryEvent(scopeId), this.store.getChatSettings(scopeId)?.locale ?? 'zh')
|
|
451
|
+
.catch(error => this.logger.warn('orchestrator.queue_recovery_failed', { scopeId, error: String(error) }));
|
|
452
|
+
}, 2000);
|
|
453
|
+
this.recoveryTimers.add(timer);
|
|
454
|
+
timer.unref();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
resolveDeliveredTask(taskId) {
|
|
458
|
+
const task = this.store.taskJournal.get(taskId);
|
|
459
|
+
const status = task.result?.status ?? 'SUCCESS';
|
|
460
|
+
this.store.taskJournal.update(task.id, status === 'SUCCESS' ? 'completed' : status === 'INTERRUPTED' ? 'cancelled' : 'failed');
|
|
461
|
+
if (task.queueId)
|
|
462
|
+
this.store.updateQueuedTurnInputStatus(task.queueId, status === 'SUCCESS' ? 'completed' : status === 'INTERRUPTED' ? 'cancelled' : 'failed', task.result?.error ?? null);
|
|
463
|
+
this.store.removeActiveTurnPreviewByMessage(task.event.scopeId, task.previewMessageId);
|
|
464
|
+
}
|
|
465
|
+
async handleRecovery(scopeId, args, locale) {
|
|
466
|
+
await this.scopeOperations.run(scopeId, async () => {
|
|
467
|
+
const [action, id] = args.trim().split(/\s+/);
|
|
468
|
+
const tasks = this.store.taskJournal.listUnfinished(scopeId).filter(task => task.state === 'awaiting_confirmation' || task.state === 'delivery_pending');
|
|
469
|
+
const task = id ? tasks.find(task => task.id === id) : tasks.length === 1 ? tasks[0] : null;
|
|
470
|
+
if (!task || !['continue', 'retry', 'deliver', 'cancel'].includes(action ?? '')) {
|
|
471
|
+
await this.sendMessage(scopeId, tasks.length ? tasks.map(task => `\`${task.id}\` · ${task.state} · ${task.sourcePrompt}\n/recover continue|retry|deliver|cancel ${task.id}`).join('\n\n') : (locale === 'zh' ? '没有待恢复任务。' : 'No tasks to recover.'));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (this.activeTurns.has(scopeId)) {
|
|
475
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '请先结束当前任务。' : 'Finish the active task first.');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (action === 'cancel') {
|
|
479
|
+
this.store.taskJournal.update(task.id, 'cancelled');
|
|
480
|
+
if (task.queueId)
|
|
481
|
+
this.store.updateQueuedTurnInputStatus(task.queueId, 'cancelled');
|
|
482
|
+
this.store.removeActiveTurnPreviewByMessage(scopeId, task.previewMessageId);
|
|
483
|
+
}
|
|
484
|
+
else if (task.state === 'delivery_pending') {
|
|
485
|
+
await this.deliverTaskChunks(task.id);
|
|
486
|
+
this.resolveDeliveredTask(task.id);
|
|
487
|
+
}
|
|
488
|
+
else if (action === 'continue' || action === 'retry') {
|
|
489
|
+
if (!this.backends.has(task.backendId) || this.getBackendDescriptorForScope(scopeId).id !== task.backendId || (this.store.getBinding(scopeId)?.cwd || this.config.defaultCwd) !== task.request.cwd) {
|
|
490
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '原后端或目录不可用,请恢复原配置或放弃任务。' : 'Restore the original backend/directory, or cancel this task.');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const prompt = action === 'continue'
|
|
494
|
+
? `${locale === 'zh' ? '服务重启前任务可能已执行部分操作。请检查当前状态,继续完成原任务,避免重复已完成的操作。' : 'This task may have partially executed before restart. Inspect current state and finish it, avoiding duplicate actions.'}\n\n${task.sourcePrompt}`
|
|
495
|
+
: task.sourcePrompt;
|
|
496
|
+
const stagedAttachments = task.request.stagedAttachments ?? await stageInboundAttachments(this.messaging, task.request.cwd, task.request.threadId || 'default', task.event.attachments, this.logger);
|
|
497
|
+
if (stagedAttachments.length !== task.event.attachments.length && !task.request.stagedAttachments) {
|
|
498
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '附件恢复失败,请稍后重试或放弃任务。' : 'Attachment recovery failed. Retry later or cancel this task.');
|
|
499
|
+
return;
|
|
405
500
|
}
|
|
501
|
+
if (task.request.threadId)
|
|
502
|
+
this.store.setBinding(scopeId, task.request.threadId, task.request.cwd);
|
|
503
|
+
this.store.taskJournal.update(task.id, 'accepted', { request: { ...task.request, prompt, stagedAttachments }, result: null, delivery: [], deliveredChunks: 0, error: null });
|
|
504
|
+
await this.executeTurn(task.event, prompt, locale, 0, stagedAttachments, { taskId: task.id, queueId: task.queueId ?? undefined, reuseMessageId: task.previewMessageId || undefined, forcedThreadId: task.request.threadId ?? undefined });
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
if (!this.stopped)
|
|
508
|
+
await this.drainNextQueuedTurn(this.recoveryEvent(scopeId), locale);
|
|
509
|
+
}
|
|
510
|
+
async stopTask(scopeId, taskId) {
|
|
511
|
+
if (this.stopped || !this.ownsScope(scopeId))
|
|
512
|
+
return false;
|
|
513
|
+
return this.scopeOperations.run(scopeId, async () => {
|
|
514
|
+
const turn = this.activeTurns.get(scopeId);
|
|
515
|
+
if (!turn || turn.taskId !== taskId || turn.settling)
|
|
516
|
+
return false;
|
|
517
|
+
await this.cancelActiveTurn(scopeId);
|
|
518
|
+
return true;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
async cancelActiveTurn(scopeId, preserveRecovery = false) {
|
|
522
|
+
const turn = this.activeTurns.get(scopeId);
|
|
523
|
+
if (!turn)
|
|
524
|
+
return;
|
|
525
|
+
if (turn.settling)
|
|
526
|
+
throw new Error('任务正在收尾,请等待结果交付完成。');
|
|
527
|
+
// Retain the slot until cancellation completes. A failed native cancellation must not permit overlap.
|
|
528
|
+
turn.suppressQueueDrain = true;
|
|
529
|
+
if (turn.flushTimer)
|
|
530
|
+
clearTimeout(turn.flushTimer);
|
|
531
|
+
if (turn.typingTimer)
|
|
532
|
+
clearInterval(turn.typingTimer);
|
|
533
|
+
await turn.execution.cancel();
|
|
534
|
+
await this.messageOperations.run(`${scopeId}:preview:${turn.taskId}`, () => this.messaging.endTaskPreview?.(scopeId, turn.taskId) ?? Promise.resolve());
|
|
535
|
+
if (!preserveRecovery && this.messaging.beginTaskPreview) {
|
|
536
|
+
const task = this.store.taskJournal.get(turn.taskId);
|
|
537
|
+
const folded = buildFoldedToolsSummary({ toolLines: turn.toolLines, toolCount: turn.toolCount, stepIndex: turn.stepIndex, startTime: turn.startTime, locale: task.request.locale });
|
|
538
|
+
const response = `${task.request.locale === 'zh' ? '🛑 已停止任务;排队任务保留。' : '🛑 Task stopped; queued tasks kept.'}${turn.accumulatedText.trim() ? `\n\n${turn.accumulatedText.trim()}` : ''}`;
|
|
539
|
+
this.store.taskJournal.update(turn.taskId, 'delivery_pending', { result: { kind: 'result', status: 'INTERRUPTED', response, conversationId: turn.threadId }, delivery: combineSummaryAndResponse(folded, response), deliveredChunks: 0, previewMessageId: turn.messageId });
|
|
540
|
+
try {
|
|
541
|
+
await this.deliverTaskChunks(turn.taskId);
|
|
542
|
+
this.resolveDeliveredTask(turn.taskId);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
this.logger.warn('orchestrator.stop_delivery_failed', { taskId: turn.taskId, error: String(error) });
|
|
406
546
|
}
|
|
407
547
|
}
|
|
408
|
-
|
|
409
|
-
this.
|
|
548
|
+
if (this.activeTurns.get(scopeId) === turn)
|
|
549
|
+
this.activeTurns.delete(scopeId);
|
|
550
|
+
if (!preserveRecovery) {
|
|
551
|
+
this.store.removeActiveTurnPreview(turn.previewKey);
|
|
552
|
+
if (turn.queueId)
|
|
553
|
+
this.store.updateQueuedTurnInputStatus(turn.queueId, 'cancelled');
|
|
554
|
+
const task = this.store.taskJournal.get(turn.taskId);
|
|
555
|
+
if (!['cancelled', 'delivery_pending'].includes(task.state))
|
|
556
|
+
this.store.taskJournal.update(turn.taskId, 'cancelled');
|
|
410
557
|
}
|
|
411
558
|
}
|
|
412
559
|
async stop() {
|
|
413
|
-
this.
|
|
560
|
+
this.stopped = true;
|
|
561
|
+
this.inboundListeners.clear();
|
|
562
|
+
await this.bot.stop();
|
|
563
|
+
this.removeInboundConsumer?.();
|
|
564
|
+
this.removeInboundConsumer = null;
|
|
565
|
+
await this.intakeOperations.idle();
|
|
414
566
|
for (const timer of this.stalePanelDeleteTimers.values())
|
|
415
567
|
clearTimeout(timer);
|
|
416
568
|
this.stalePanelDeleteTimers.clear();
|
|
417
569
|
for (const timer of this.recoveryTimers)
|
|
418
570
|
clearTimeout(timer);
|
|
419
571
|
this.recoveryTimers.clear();
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
572
|
+
await Promise.allSettled([...this.activeTurns.keys()].map(scopeId => this.cancelActiveTurn(scopeId, true)));
|
|
573
|
+
await this.scopeOperations.idle();
|
|
574
|
+
await Promise.allSettled([...this.settlements]);
|
|
575
|
+
await this.messageOperations.idle();
|
|
576
|
+
await this.backends.stop();
|
|
577
|
+
}
|
|
578
|
+
isIdleForServiceUpdate() {
|
|
579
|
+
return !this.store.taskJournal.listUnfinished().some(task => this.ownsScope(task.event.scopeId)) && this.activeTurns.size === 0 && this.settlements.size === 0 && this.scopeOperations.isIdle() && this.messageOperations.isIdle() && this.getPendingApprovals() === 0 &&
|
|
580
|
+
!this.store.listQueuedTurnInputs().some(input => this.ownsScope(input.scopeId));
|
|
429
581
|
}
|
|
582
|
+
getPendingApprovals() { return this.backends.getPendingApprovals(); }
|
|
430
583
|
getActiveTurnsCount() {
|
|
431
584
|
return this.activeTurns.size;
|
|
432
585
|
}
|
|
586
|
+
ownsScopeId(scopeId) { return this.ownsScope(scopeId); }
|
|
587
|
+
hasExecutingTasks() { return this.activeTurns.size > 0 || !this.scopeOperations.isIdle() || this.settlements.size > 0; }
|
|
433
588
|
hasActiveTurn(scopeId) {
|
|
434
589
|
return this.activeTurns.has(scopeId);
|
|
435
590
|
}
|
|
436
591
|
async sendMessage(scopeId, text, keyboard) {
|
|
437
|
-
const chunks =
|
|
592
|
+
const chunks = chunkMessage(text, this.messaging.capabilities?.maxMessageLength);
|
|
438
593
|
let lastMsgId = 0;
|
|
439
594
|
for (let i = 0; i < chunks.length; i++) {
|
|
440
595
|
const isLast = i === chunks.length - 1;
|
|
@@ -448,64 +603,97 @@ export class UnifiedChannelOrchestrator {
|
|
|
448
603
|
await this.sendMessage(scopeId, text, keyboard);
|
|
449
604
|
return;
|
|
450
605
|
}
|
|
451
|
-
await this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard);
|
|
606
|
+
await this.messageOperations.run(`${scopeId}:${messageId}`, () => this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard));
|
|
452
607
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
608
|
+
trackSettlement(work) {
|
|
609
|
+
const pending = work();
|
|
610
|
+
this.settlements.add(pending);
|
|
611
|
+
void pending.catch(error => this.logger.warn('orchestrator.settlement_failed', { error: String(error) }))
|
|
612
|
+
.finally(() => this.settlements.delete(pending));
|
|
613
|
+
}
|
|
614
|
+
async deliverTaskChunks(taskId) {
|
|
615
|
+
let task = this.store.taskJournal.get(taskId);
|
|
616
|
+
while (task.deliveredChunks < task.delivery.length) {
|
|
617
|
+
if (this.stopped)
|
|
618
|
+
throw new Error('Delivery interrupted by shutdown');
|
|
619
|
+
const index = task.deliveredChunks;
|
|
620
|
+
const chunk = task.delivery[index];
|
|
621
|
+
if (index === 0 && task.previewMessageId > 0 && this.messaging.capabilities?.editableMessages !== false) {
|
|
622
|
+
try {
|
|
623
|
+
await this.editMessage(task.event.scopeId, task.previewMessageId, chunk, []);
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
await this.sendMessage(task.event.scopeId, chunk);
|
|
627
|
+
}
|
|
467
628
|
}
|
|
629
|
+
else
|
|
630
|
+
await this.sendMessage(task.event.scopeId, chunk);
|
|
631
|
+
task = this.store.taskJournal.update(task.id, 'delivery_pending', { deliveredChunks: index + 1 });
|
|
468
632
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
633
|
+
}
|
|
634
|
+
async safeDeliverChunks(scopeId, messageId, chunks) {
|
|
635
|
+
const active = this.activeTurns.get(scopeId);
|
|
636
|
+
if (!active || this.stopped || active.suppressQueueDrain)
|
|
637
|
+
return;
|
|
638
|
+
await this.messageOperations.run(`${scopeId}:preview:${active.taskId}`, () => this.messaging.endTaskPreview?.(scopeId, active.taskId) ?? Promise.resolve());
|
|
639
|
+
const task = this.store.taskJournal.get(active.taskId);
|
|
640
|
+
messageId = active.messageId;
|
|
641
|
+
this.store.taskJournal.update(task.id, 'delivery_pending', { delivery: chunks, deliveredChunks: 0, previewMessageId: messageId });
|
|
642
|
+
await this.deliverTaskChunks(task.id);
|
|
472
643
|
}
|
|
473
644
|
async safeDeliverMessage(scopeId, messageId, text) {
|
|
474
|
-
|
|
475
|
-
await this.safeDeliverChunks(scopeId, messageId, chunks);
|
|
645
|
+
await this.safeDeliverChunks(scopeId, messageId, chunkMessage(text, this.messaging.capabilities?.maxMessageLength));
|
|
476
646
|
}
|
|
477
647
|
async handleText(event) {
|
|
648
|
+
if (this.stopped || !this.ownsScope(event.scopeId))
|
|
649
|
+
return;
|
|
650
|
+
const action = this.messaging.resolveAction?.(event);
|
|
651
|
+
if (action) {
|
|
652
|
+
await this.handleCallback(action);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
478
655
|
const scopeId = event.scopeId;
|
|
479
656
|
const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
|
|
480
|
-
const isDefaultTopic = isDefaultTelegramScope({
|
|
481
|
-
chatType: event.chatType,
|
|
482
|
-
allowedChatId: this.config.tgAllowedChatId ?? null,
|
|
483
|
-
allowedTopicId: this.config.tgAllowedTopicId ?? null,
|
|
484
|
-
topicId: event.topicId ?? null,
|
|
485
|
-
requireExplicitGroupAddressing: false,
|
|
486
|
-
});
|
|
487
657
|
const parsedCommand = parseCommand(event.text);
|
|
488
|
-
const addressing =
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
entities: event.entities,
|
|
492
|
-
command: parsedCommand,
|
|
493
|
-
botUsername: this.bot.username,
|
|
494
|
-
isDefaultTopic,
|
|
495
|
-
replyToBot: event.replyToBot,
|
|
496
|
-
});
|
|
658
|
+
const addressing = this.messaging.resolveIncoming?.(event, this.config, this.bot.username) ??
|
|
659
|
+
(parsedCommand ? { kind: 'command', command: parsedCommand } :
|
|
660
|
+
event.text.trim() || event.attachments.length ? { kind: 'prompt', text: event.text } : { kind: 'ignore' });
|
|
497
661
|
if (addressing.kind === 'ignore')
|
|
498
662
|
return;
|
|
499
|
-
if (
|
|
500
|
-
const
|
|
663
|
+
if (addressing.kind === 'command') {
|
|
664
|
+
const name = addressing.command.name.toLowerCase();
|
|
665
|
+
const args = addressing.command.args.join(' ').trim();
|
|
666
|
+
if (name === 'choose') {
|
|
667
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '选择已过期或不属于此会话,请重新打开菜单。' : 'This choice expired or belongs to another scope. Open the menu again.');
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (name === 'recover') {
|
|
671
|
+
await this.handleRecovery(scopeId, args, locale);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (await this.serviceUi?.handleCustomCommand?.(scopeId, name, args, locale, event))
|
|
675
|
+
return;
|
|
676
|
+
if (['backend', 'backends', 'engine', 'engines'].includes(name)) {
|
|
677
|
+
await this.handleBackendCommand(scopeId, args, locale);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const backendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
682
|
+
if (!this.backends.has(backendId)) {
|
|
683
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `后端 \`${backendId}\` 当前不可用。发送 /backend 切换后端,原会话和设置已保留。` : `Backend \`${backendId}\` is unavailable. Use /backend to switch; its session and settings are preserved.`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const ui = this.backends.ui(this.getBackendDescriptorForScope(scopeId).id) ?? this.customUi;
|
|
687
|
+
if (ui?.handleCustomInbound) {
|
|
688
|
+
const handled = await ui.handleCustomInbound(event, locale);
|
|
501
689
|
if (handled)
|
|
502
690
|
return;
|
|
503
691
|
}
|
|
504
692
|
if (addressing.kind === 'command') {
|
|
505
693
|
const commandName = addressing.command.name.toLowerCase();
|
|
506
694
|
const argsString = addressing.command.args.join(' ').trim();
|
|
507
|
-
if (
|
|
508
|
-
const handled = await
|
|
695
|
+
if (ui?.handleCustomCommand) {
|
|
696
|
+
const handled = await ui.handleCustomCommand(scopeId, commandName, argsString, locale, event);
|
|
509
697
|
if (handled)
|
|
510
698
|
return;
|
|
511
699
|
}
|
|
@@ -564,18 +752,35 @@ export class UnifiedChannelOrchestrator {
|
|
|
564
752
|
await this.handleNewSession(scopeId, locale, argsString);
|
|
565
753
|
return;
|
|
566
754
|
}
|
|
755
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `当前后端不支持 /${commandName}。发送 /help 查看可用操作。` : `This backend does not support /${commandName}. Send /help for available commands.`);
|
|
756
|
+
return;
|
|
567
757
|
}
|
|
568
758
|
if (addressing.kind === 'prompt') {
|
|
569
759
|
await this.startPromptTurn(event, addressing.text, locale);
|
|
570
760
|
}
|
|
571
761
|
}
|
|
572
762
|
async handleCallback(event) {
|
|
763
|
+
if (this.stopped || !this.ownsScope(event.scopeId))
|
|
764
|
+
return;
|
|
573
765
|
const scopeId = event.scopeId;
|
|
574
|
-
const locale = 'zh';
|
|
766
|
+
const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
|
|
575
767
|
const data = event.data || '';
|
|
576
768
|
const messageId = event.messageId;
|
|
577
|
-
if (
|
|
578
|
-
const
|
|
769
|
+
if (data.startsWith('engine:stop:')) {
|
|
770
|
+
const status = await this.stopTask(scopeId, data.slice('engine:stop:'.length));
|
|
771
|
+
await this.messaging.answerCallback(event.callbackQueryId, status ? (locale === 'zh' ? '已停止,排队任务保留' : 'Stopped; queued tasks kept') : (locale === 'zh' ? '此任务已结束' : 'This task has ended'));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (data.startsWith('engine:recover:')) {
|
|
775
|
+
const [, , action, id] = data.split(':');
|
|
776
|
+
await this.handleRecovery(scopeId, `${action} ${id}`, locale);
|
|
777
|
+
await this.messaging.answerCallback(event.callbackQueryId, '');
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const backendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
781
|
+
const ui = this.backends.callbackOwner(data) ?? this.backends.ui(backendId) ?? (this.backends.has(backendId) ? this.customUi : undefined);
|
|
782
|
+
if (ui?.handleCustomCallback) {
|
|
783
|
+
const handled = await ui.handleCustomCallback(scopeId, data, locale, messageId, event);
|
|
579
784
|
if (handled)
|
|
580
785
|
return;
|
|
581
786
|
}
|
|
@@ -597,6 +802,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
597
802
|
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
598
803
|
return;
|
|
599
804
|
}
|
|
805
|
+
if (!this.backends.has(backendId)) {
|
|
806
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '后端不可用,请发送 /backend 重新选择。' : 'Backend unavailable; select one with /backend.');
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
600
809
|
if (data.startsWith('engine:m:') || data.startsWith('setup:model:')) {
|
|
601
810
|
const rawModel = data.startsWith('engine:m:')
|
|
602
811
|
? data.slice('engine:m:'.length)
|
|
@@ -613,6 +822,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
613
822
|
? data.slice('engine:effort:'.length)
|
|
614
823
|
: data.slice('setup:effort:'.length);
|
|
615
824
|
const targetEffort = rawEffort === 'default' ? null : rawEffort;
|
|
825
|
+
if (targetEffort && !(await this.supportedEfforts(scopeId)).includes(targetEffort)) {
|
|
826
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '当前模型不支持此推理档位。' : 'This model does not support that reasoning effort.');
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
616
829
|
const settings = this.store.getChatSettings(scopeId);
|
|
617
830
|
if (targetEffort !== 'high' && settings?.serviceTier === 'boost') {
|
|
618
831
|
this.store.setChatServiceTier(scopeId, null);
|
|
@@ -631,6 +844,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
631
844
|
return;
|
|
632
845
|
}
|
|
633
846
|
if (sub === 'boost') {
|
|
847
|
+
if (this.getBackendDescriptorForScope(scopeId).defaults?.boost === false) {
|
|
848
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '当前后端不支持 Boost。' : 'Boost is not available on this backend.');
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
634
851
|
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
635
852
|
const nextBoost = !isBoost;
|
|
636
853
|
this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
|
|
@@ -669,71 +886,112 @@ export class UnifiedChannelOrchestrator {
|
|
|
669
886
|
}
|
|
670
887
|
}
|
|
671
888
|
}
|
|
889
|
+
captureTask(event, prompt, locale, stagedAttachments, queueId = null) {
|
|
890
|
+
const scopeId = event.scopeId;
|
|
891
|
+
const binding = this.store.getBinding(scopeId);
|
|
892
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
893
|
+
const backend = this.getBackendDescriptorForScope(scopeId);
|
|
894
|
+
const boost = settings?.serviceTier === 'boost' && backend.defaults?.boost !== false;
|
|
895
|
+
const task = {
|
|
896
|
+
id: randomUUID(), backendId: backend.id, state: 'accepted', event, sourcePrompt: prompt,
|
|
897
|
+
request: { scopeId, prompt: boost && !prompt.startsWith('[Boost Mode:')
|
|
898
|
+
? `[Boost Mode: Proceed with deep thinking, strategic planning, multiple perspectives, and rigorous verification.]\n\n${prompt}` : prompt,
|
|
899
|
+
stagedAttachments, threadId: binding?.threadId || null, cwd: binding?.cwd || this.config.defaultCwd,
|
|
900
|
+
model: settings?.model || 'default', effort: boost ? 'high' : settings?.reasoningEffort ?? backend.defaults?.reasoningEffort ?? null,
|
|
901
|
+
serviceTier: boost ? 'boost' : settings?.serviceTier ?? null, locale, accessPreset: settings?.accessPreset ?? 'default' },
|
|
902
|
+
queueId, previewMessageId: 0, result: null, delivery: [], deliveredChunks: 0, error: null,
|
|
903
|
+
createdAt: Date.now(), updatedAt: Date.now(),
|
|
904
|
+
};
|
|
905
|
+
this.store.taskJournal.insert(task);
|
|
906
|
+
return task;
|
|
907
|
+
}
|
|
908
|
+
recoveryBlocksScope(scopeId) {
|
|
909
|
+
return this.store.taskJournal.listUnfinished(scopeId).some(task => task.state === 'awaiting_confirmation' || task.state === 'delivery_pending');
|
|
910
|
+
}
|
|
672
911
|
async startPromptTurn(event, prompt, locale, options) {
|
|
912
|
+
if (this.stopped)
|
|
913
|
+
return;
|
|
914
|
+
if (this.store.taskJournal.findReceipt(event, prompt))
|
|
915
|
+
return;
|
|
916
|
+
const task = this.captureTask(event, prompt, locale);
|
|
917
|
+
return this.scopeOperations.run(event.scopeId, () => this.startPromptTurnNow(event, prompt, locale, { ...options, taskId: task.id }));
|
|
918
|
+
}
|
|
919
|
+
async startPromptTurnNow(event, prompt, locale, options) {
|
|
920
|
+
if (this.stopped)
|
|
921
|
+
return;
|
|
673
922
|
const scopeId = event.scopeId;
|
|
674
923
|
const binding = this.store.getBinding(scopeId);
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
924
|
+
const effectivePrompt = prompt;
|
|
925
|
+
const task = options?.taskId ? this.store.taskJournal.get(options.taskId) : this.captureTask(event, prompt, locale);
|
|
926
|
+
const cwd = task.request.cwd;
|
|
927
|
+
const threadId = options?.forcedThreadId ?? binding?.threadId ?? task.request.threadId ?? 'default';
|
|
928
|
+
if (options?.forcedThreadId)
|
|
929
|
+
task.request.threadId = options.forcedThreadId;
|
|
678
930
|
let stagedAttachments;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
stagedAttachments = staged;
|
|
685
|
-
effectivePrompt = this.getAdapterForScope(scopeId).id === 'dsh' ? prompt : buildAttachmentPrompt(prompt, staged);
|
|
686
|
-
}
|
|
931
|
+
try {
|
|
932
|
+
if (event.attachments.length) {
|
|
933
|
+
stagedAttachments = await stageInboundAttachments(this.messaging, cwd, threadId, event.attachments, this.logger);
|
|
934
|
+
if (stagedAttachments.length !== event.attachments.length)
|
|
935
|
+
throw new Error('Some attachments could not be saved; the task was not started');
|
|
687
936
|
}
|
|
937
|
+
task.request.stagedAttachments = stagedAttachments;
|
|
938
|
+
this.store.taskJournal.update(task.id, 'accepted', { request: task.request });
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
this.store.taskJournal.update(task.id, 'failed', { error: String(error) });
|
|
942
|
+
await this.sendMessage(scopeId, `⚠️ ${String(error)}`);
|
|
943
|
+
return;
|
|
688
944
|
}
|
|
689
|
-
|
|
945
|
+
const taskOptions = { ...options, taskId: task.id };
|
|
946
|
+
if (this.activeTurns.has(scopeId) || this.recoveryBlocksScope(scopeId)) {
|
|
690
947
|
const settings = this.store.getChatSettings(scopeId);
|
|
691
948
|
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
692
|
-
if (mode === 'steer') {
|
|
693
|
-
|
|
694
|
-
if (active) {
|
|
695
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
696
|
-
active.suppressQueueDrain = true;
|
|
697
|
-
active.execution.cancel();
|
|
698
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
699
|
-
await active.execution.waitForResult();
|
|
700
|
-
if (active.typingTimer)
|
|
701
|
-
clearInterval(active.typingTimer);
|
|
702
|
-
if (active.flushTimer)
|
|
703
|
-
clearTimeout(active.flushTimer);
|
|
704
|
-
this.activeTurns.delete(scopeId);
|
|
705
|
-
}
|
|
949
|
+
if (mode === 'steer' && !this.recoveryBlocksScope(scopeId)) {
|
|
950
|
+
await this.cancelActiveTurn(scopeId);
|
|
706
951
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
707
952
|
? `⚡ **已插话中断前置任务,立即开始新指令**:\n> ${effectivePrompt}`
|
|
708
953
|
: `⚡ **Interrupted previous turn, executing new instruction**:\n> ${effectivePrompt}`);
|
|
709
|
-
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments,
|
|
954
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, taskOptions);
|
|
710
955
|
return;
|
|
711
956
|
}
|
|
712
|
-
await this.enqueuePromptTurn(event, effectivePrompt, locale);
|
|
957
|
+
await this.enqueuePromptTurn(event, effectivePrompt, locale, stagedAttachments, task.id);
|
|
713
958
|
return;
|
|
714
959
|
}
|
|
715
|
-
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments,
|
|
960
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, taskOptions);
|
|
716
961
|
}
|
|
717
|
-
|
|
962
|
+
cancelQueuedWork(scopeId) {
|
|
963
|
+
const count = this.store.cancelQueuedTurnInputs(scopeId);
|
|
964
|
+
for (const task of this.store.taskJournal.listUnfinished(scopeId)) {
|
|
965
|
+
if (task.state === 'queued' && task.queueId && this.store.getQueuedTurnInput(task.queueId)?.status === 'cancelled')
|
|
966
|
+
this.store.taskJournal.update(task.id, 'cancelled');
|
|
967
|
+
}
|
|
968
|
+
return count;
|
|
969
|
+
}
|
|
970
|
+
async enqueuePromptTurn(event, prompt, locale, stagedAttachments = [], taskId) {
|
|
718
971
|
const scopeId = event.scopeId;
|
|
719
|
-
const
|
|
972
|
+
const journalTask = taskId ? this.store.taskJournal.get(taskId) : null;
|
|
973
|
+
const adapter = journalTask ? this.getAdapterForBackend(journalTask.backendId) : this.getAdapterForScope(scopeId);
|
|
720
974
|
const queueId = `${adapter.id}_q_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
721
975
|
const binding = this.store.getBinding(scopeId);
|
|
722
|
-
this.store.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
976
|
+
this.store.taskJournal.atomic(() => {
|
|
977
|
+
this.store.saveQueuedTurnInput({
|
|
978
|
+
queueId,
|
|
979
|
+
scopeId,
|
|
980
|
+
chatId: String(event.chatId),
|
|
981
|
+
chatType: event.chatType,
|
|
982
|
+
topicId: event.topicId ?? null,
|
|
983
|
+
threadId: binding?.threadId || '',
|
|
984
|
+
inputJson: JSON.stringify({ version: 1, prompt, backendId: journalTask?.backendId ?? this.getBackendDescriptorForScope(scopeId).id, cwd: journalTask?.request.cwd ?? binding?.cwd ?? this.config.defaultCwd, stagedAttachments }),
|
|
985
|
+
sourceSummary: prompt,
|
|
986
|
+
messageId: event.messageId ?? null,
|
|
987
|
+
status: 'queued',
|
|
988
|
+
error: null,
|
|
989
|
+
createdAt: Date.now(),
|
|
990
|
+
updatedAt: Date.now(),
|
|
991
|
+
resolvedAt: null,
|
|
992
|
+
});
|
|
993
|
+
if (journalTask)
|
|
994
|
+
this.store.taskJournal.update(journalTask.id, 'queued', { queueId, request: { ...journalTask.request, stagedAttachments } });
|
|
737
995
|
});
|
|
738
996
|
const queueCount = this.store.countQueuedTurnInputs(scopeId);
|
|
739
997
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
@@ -742,55 +1000,56 @@ export class UnifiedChannelOrchestrator {
|
|
|
742
1000
|
}
|
|
743
1001
|
async drainNextQueuedTurn(event, locale) {
|
|
744
1002
|
const scopeId = event.scopeId;
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1003
|
+
await this.scopeOperations.run(scopeId, async () => {
|
|
1004
|
+
while (!this.stopped && !this.activeTurns.has(scopeId) && !this.recoveryBlocksScope(scopeId)) {
|
|
1005
|
+
const queued = this.store.peekQueuedTurnInput(scopeId);
|
|
1006
|
+
if (!queued)
|
|
1007
|
+
return;
|
|
1008
|
+
this.store.updateQueuedTurnInputStatus(queued.queueId, 'processing');
|
|
1009
|
+
try {
|
|
1010
|
+
const input = decodeQueuedEngineInput(queued.inputJson, queued.sourceSummary);
|
|
1011
|
+
if ('backendId' in input && (input.backendId !== this.getBackendDescriptorForScope(scopeId).id || input.cwd !== (this.store.getBinding(scopeId)?.cwd || this.config.defaultCwd))) {
|
|
1012
|
+
throw new Error('Queued task context changed; start a new task in the selected backend/directory');
|
|
1013
|
+
}
|
|
1014
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `⏭️ **开始执行排队任务**:\n> ${input.prompt}` : `⏭️ **Executing queued task**:\n> ${input.prompt}`);
|
|
1015
|
+
await this.executeTurn({ ...event, chatId: queued.chatId, topicId: queued.topicId, messageId: queued.messageId ?? 0, text: input.prompt, attachments: [] }, input.prompt, locale, 0, input.stagedAttachments, { queueId: queued.queueId, taskId: this.store.taskJournal.forQueue(queued.queueId)?.id });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
if (this.stopped)
|
|
1020
|
+
return; // Preserve processing for restart recovery.
|
|
1021
|
+
this.store.updateQueuedTurnInputStatus(queued.queueId, 'failed', String(error));
|
|
1022
|
+
const task = this.store.taskJournal.forQueue(queued.queueId);
|
|
1023
|
+
if (task?.state === 'queued' || task?.state === 'accepted')
|
|
1024
|
+
this.store.taskJournal.update(task.id, 'failed', { error: String(error) });
|
|
1025
|
+
this.logger.warn('orchestrator.drain_queue_failed', { error: String(error) });
|
|
1026
|
+
await this.sendMessage(scopeId, `⚠️ ${String(error)}`).catch(() => { });
|
|
757
1027
|
}
|
|
758
1028
|
}
|
|
759
|
-
|
|
760
|
-
/* ignore */
|
|
761
|
-
}
|
|
762
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
763
|
-
? `▶️ **开始执行排队任务**:\n> ${parsedInput}`
|
|
764
|
-
: `▶️ **Executing queued task**:\n> ${parsedInput}`);
|
|
765
|
-
void this.startPromptTurn({
|
|
766
|
-
...event,
|
|
767
|
-
text: parsedInput,
|
|
768
|
-
}, parsedInput, locale);
|
|
769
|
-
}
|
|
770
|
-
catch (err) {
|
|
771
|
-
this.logger.warn('orchestrator.drain_queue_failed', { error: String(err) });
|
|
772
|
-
}
|
|
1029
|
+
});
|
|
773
1030
|
}
|
|
774
1031
|
async executeTurn(event, prompt, locale, retryCount = 0, stagedAttachments, options) {
|
|
1032
|
+
if (this.stopped)
|
|
1033
|
+
throw new Error('Orchestrator is stopped');
|
|
775
1034
|
const scopeId = event.scopeId;
|
|
776
1035
|
const binding = this.store.getBinding(scopeId);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
? `[Boost Mode: Proceed with deep thinking, strategic planning, multiple perspectives, and rigorous verification.]\n\n${prompt}`
|
|
786
|
-
: prompt;
|
|
1036
|
+
let task = options?.taskId ? this.store.taskJournal.get(options.taskId) : null;
|
|
1037
|
+
if (!task)
|
|
1038
|
+
task = this.captureTask(event, prompt, locale, stagedAttachments, options?.queueId ?? null);
|
|
1039
|
+
const adapter = this.getAdapterForBackend(task.backendId);
|
|
1040
|
+
const req = { ...task.request, threadId: options?.forcedThreadId ?? binding?.threadId ?? task.request.threadId,
|
|
1041
|
+
stagedAttachments: stagedAttachments ?? task.request.stagedAttachments };
|
|
1042
|
+
const { cwd, threadId } = req;
|
|
1043
|
+
const isBoost = req.serviceTier === 'boost';
|
|
787
1044
|
let initialMsgId = 0;
|
|
788
1045
|
if (options?.reuseMessageId) {
|
|
789
1046
|
initialMsgId = options.reuseMessageId;
|
|
790
1047
|
try {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1048
|
+
const text = locale === 'zh' ? `🔄 [重启恢复] ${adapter.name} 正在继续执行…` : `🔄 ${adapter.name} is continuing…`;
|
|
1049
|
+
if (this.messaging.beginTaskPreview)
|
|
1050
|
+
initialMsgId = await this.messaging.beginTaskPreview(scopeId, task.id, text, initialMsgId);
|
|
1051
|
+
else
|
|
1052
|
+
await this.editMessage(scopeId, initialMsgId, text, [[{ text: '🛑 停止 / Stop', callback_data: `engine:stop:${task.id}` }]]);
|
|
794
1053
|
}
|
|
795
1054
|
catch (editErr) {
|
|
796
1055
|
this.logger.warn('orchestrator.reuse_message_failed', { error: String(editErr) });
|
|
@@ -798,15 +1057,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
798
1057
|
}
|
|
799
1058
|
else {
|
|
800
1059
|
try {
|
|
801
|
-
|
|
1060
|
+
const initialText = locale === 'zh'
|
|
802
1061
|
? (isBoost ? `🚀 ${adapter.name} (Boost 模式) 正在深度思考中…` : `⏳ ${adapter.name} 正在思考中…`)
|
|
803
|
-
: (isBoost ? `🚀 ${adapter.name} (Boost Mode) is thinking deeply…` : `⏳ ${adapter.name} is thinking…`)
|
|
1062
|
+
: (isBoost ? `🚀 ${adapter.name} (Boost Mode) is thinking deeply…` : `⏳ ${adapter.name} is thinking…`);
|
|
1063
|
+
initialMsgId = this.messaging.beginTaskPreview
|
|
1064
|
+
? await this.messaging.beginTaskPreview(scopeId, task.id, initialText)
|
|
1065
|
+
: await this.sendMessage(scopeId, initialText, [[{ text: '🛑 停止 / Stop', callback_data: `engine:stop:${task.id}` }]]);
|
|
804
1066
|
}
|
|
805
1067
|
catch (sendErr) {
|
|
806
1068
|
this.logger.warn('orchestrator.initial_message_failed', { error: String(sendErr) });
|
|
807
1069
|
}
|
|
808
1070
|
}
|
|
809
|
-
const turnKey = `${adapter.id}_${
|
|
1071
|
+
const turnKey = `${adapter.id}_${task.id}`;
|
|
810
1072
|
try {
|
|
811
1073
|
this.store.saveActiveTurnPreview({
|
|
812
1074
|
turnId: turnKey,
|
|
@@ -818,17 +1080,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
818
1080
|
catch {
|
|
819
1081
|
/* ignore */
|
|
820
1082
|
}
|
|
821
|
-
|
|
822
|
-
scopeId,
|
|
823
|
-
prompt: effectivePrompt,
|
|
824
|
-
stagedAttachments,
|
|
825
|
-
threadId,
|
|
826
|
-
cwd,
|
|
827
|
-
model,
|
|
828
|
-
effort,
|
|
829
|
-
serviceTier: isBoost ? 'boost' : (settings?.serviceTier ?? null),
|
|
830
|
-
locale,
|
|
831
|
-
};
|
|
1083
|
+
task = this.store.taskJournal.update(task.id, task.state, { request: req, previewMessageId: initialMsgId });
|
|
832
1084
|
if (adapter.preflightTurn) {
|
|
833
1085
|
try {
|
|
834
1086
|
await adapter.preflightTurn(req);
|
|
@@ -837,7 +1089,22 @@ export class UnifiedChannelOrchestrator {
|
|
|
837
1089
|
this.logger.warn('orchestrator.preflight_failed', { error: String(err) });
|
|
838
1090
|
}
|
|
839
1091
|
}
|
|
840
|
-
|
|
1092
|
+
if (this.stopped) {
|
|
1093
|
+
await this.messaging.endTaskPreview?.(scopeId, task.id);
|
|
1094
|
+
this.store.removeActiveTurnPreview(turnKey);
|
|
1095
|
+
throw new Error('Orchestrator is stopped');
|
|
1096
|
+
}
|
|
1097
|
+
let execution;
|
|
1098
|
+
task = this.store.taskJournal.update(task.id, 'running', { result: null, delivery: [], deliveredChunks: 0 });
|
|
1099
|
+
try {
|
|
1100
|
+
execution = adapter.executeTurn(req);
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
await this.messaging.endTaskPreview?.(scopeId, task.id);
|
|
1104
|
+
this.store.removeActiveTurnPreview(turnKey);
|
|
1105
|
+
this.store.taskJournal.update(task.id, 'failed', { error: String(error) });
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
841
1108
|
const activeTurn = {
|
|
842
1109
|
scopeId,
|
|
843
1110
|
messageId: initialMsgId,
|
|
@@ -852,6 +1119,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
852
1119
|
toolCount: 0,
|
|
853
1120
|
currentTool: null,
|
|
854
1121
|
startTime: Date.now(),
|
|
1122
|
+
previewKey: turnKey,
|
|
1123
|
+
queueId: options?.queueId,
|
|
1124
|
+
taskId: task.id,
|
|
855
1125
|
};
|
|
856
1126
|
this.activeTurns.set(scopeId, activeTurn);
|
|
857
1127
|
this.messaging.sendTypingInScope(scopeId).catch(() => { });
|
|
@@ -860,13 +1130,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
860
1130
|
scheduleFlush();
|
|
861
1131
|
}, TYPING_INTERVAL_MS);
|
|
862
1132
|
activeTurn.typingTimer?.unref?.();
|
|
1133
|
+
const ownsTurn = () => !this.stopped && !activeTurn.suppressQueueDrain && this.activeTurns.get(scopeId) === activeTurn;
|
|
863
1134
|
const scheduleFlush = () => {
|
|
1135
|
+
if (!ownsTurn() || activeTurn.settling || this.messaging.capabilities?.editableMessages === false)
|
|
1136
|
+
return;
|
|
864
1137
|
if (activeTurn.flushTimer)
|
|
865
1138
|
return;
|
|
866
1139
|
const elapsed = Date.now() - activeTurn.lastFlushTime;
|
|
867
1140
|
const delay = Math.max(0, STREAM_THROTTLE_MS - elapsed);
|
|
868
1141
|
activeTurn.flushTimer = setTimeout(() => {
|
|
869
1142
|
activeTurn.flushTimer = null;
|
|
1143
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1144
|
+
return;
|
|
870
1145
|
activeTurn.lastFlushTime = Date.now();
|
|
871
1146
|
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - activeTurn.startTime) / 1000));
|
|
872
1147
|
const content = renderStreamPreviewContent({
|
|
@@ -879,31 +1154,49 @@ export class UnifiedChannelOrchestrator {
|
|
|
879
1154
|
currentTool: activeTurn.currentTool,
|
|
880
1155
|
elapsedSeconds,
|
|
881
1156
|
});
|
|
882
|
-
this.
|
|
1157
|
+
void this.messageOperations.run(`${scopeId}:preview:${activeTurn.taskId}`, async () => {
|
|
1158
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1159
|
+
return;
|
|
1160
|
+
if (this.messaging.updateTaskPreview) {
|
|
1161
|
+
const messageId = await this.messaging.updateTaskPreview(scopeId, activeTurn.taskId, activeTurn.messageId, content);
|
|
1162
|
+
if (messageId !== activeTurn.messageId) {
|
|
1163
|
+
activeTurn.messageId = messageId;
|
|
1164
|
+
const journal = this.store.taskJournal.get(activeTurn.taskId);
|
|
1165
|
+
this.store.taskJournal.update(journal.id, journal.state, { previewMessageId: messageId });
|
|
1166
|
+
this.store.saveActiveTurnPreview({ turnId: activeTurn.previewKey, scopeId, threadId: activeTurn.threadId || '', messageId });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
else
|
|
1170
|
+
await this.messaging.editRichMarkdown(scopeId, activeTurn.messageId, content, [[{ text: '🛑 停止 / Stop', callback_data: `engine:stop:${activeTurn.taskId}` }]]);
|
|
1171
|
+
}).catch(() => { });
|
|
883
1172
|
}, delay);
|
|
884
1173
|
activeTurn.flushTimer?.unref?.();
|
|
885
1174
|
};
|
|
886
1175
|
execution.on('delta', (delta) => {
|
|
1176
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1177
|
+
return;
|
|
887
1178
|
activeTurn.accumulatedText += delta;
|
|
888
1179
|
scheduleFlush();
|
|
889
1180
|
});
|
|
890
1181
|
execution.on('tool', (tool) => {
|
|
1182
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1183
|
+
return;
|
|
891
1184
|
if (tool.stepIndex && tool.stepIndex > 0) {
|
|
892
1185
|
activeTurn.stepIndex = tool.stepIndex;
|
|
893
1186
|
}
|
|
894
1187
|
const icon = tool.status === 'running' ? '⚙️' : tool.status === 'failed' ? '❌' : '✅';
|
|
895
|
-
const summaryText = tool.summary ? ` · ${
|
|
896
|
-
const line = `${icon} <code>${
|
|
1188
|
+
const summaryText = tool.summary ? ` · ${escapeHtml(tool.summary.slice(0, 180))}` : '';
|
|
1189
|
+
const line = `${icon} <code>${escapeHtml(tool.name.slice(0, 100))}</code>${summaryText}`;
|
|
897
1190
|
if (tool.status === 'running') {
|
|
898
1191
|
activeTurn.toolCount += 1;
|
|
899
|
-
activeTurn.currentTool = tool.summary ? `${tool.name} (${tool.summary})` : tool.name;
|
|
1192
|
+
activeTurn.currentTool = tool.summary ? `${tool.name.slice(0, 100)} (${tool.summary.slice(0, 180)})` : tool.name;
|
|
900
1193
|
activeTurn.toolLines.push(line);
|
|
901
1194
|
}
|
|
902
1195
|
else {
|
|
903
1196
|
if (activeTurn.currentTool && activeTurn.currentTool.startsWith(tool.name)) {
|
|
904
1197
|
activeTurn.currentTool = null;
|
|
905
1198
|
}
|
|
906
|
-
const lastIdx = activeTurn.toolLines.findLastIndex((l) => l.includes(`<code>${
|
|
1199
|
+
const lastIdx = activeTurn.toolLines.findLastIndex((l) => l.includes(`<code>${escapeHtml(tool.name.slice(0, 100))}</code>`));
|
|
907
1200
|
if (lastIdx !== -1) {
|
|
908
1201
|
activeTurn.toolLines[lastIdx] = line;
|
|
909
1202
|
}
|
|
@@ -914,7 +1207,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
914
1207
|
scheduleFlush();
|
|
915
1208
|
});
|
|
916
1209
|
execution.on('conversation', (convId) => {
|
|
917
|
-
if (!convId)
|
|
1210
|
+
if (!convId || !ownsTurn() || activeTurn.settling)
|
|
918
1211
|
return;
|
|
919
1212
|
activeTurn.threadId = convId;
|
|
920
1213
|
this.store.setBinding(scopeId, convId, cwd);
|
|
@@ -924,56 +1217,176 @@ export class UnifiedChannelOrchestrator {
|
|
|
924
1217
|
threadId: convId,
|
|
925
1218
|
messageId: activeTurn.messageId,
|
|
926
1219
|
});
|
|
1220
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { request: { ...req, threadId: convId } });
|
|
927
1221
|
this.syncCurrentBackendSettings(scopeId);
|
|
928
1222
|
});
|
|
929
1223
|
const retryTurn = async (nextRetryCount) => {
|
|
930
|
-
await this.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
if (activeTurn.flushTimer)
|
|
935
|
-
clearTimeout(activeTurn.flushTimer);
|
|
936
|
-
if (activeTurn.typingTimer)
|
|
937
|
-
clearInterval(activeTurn.typingTimer);
|
|
1224
|
+
await this.scopeOperations.run(scopeId, async () => {
|
|
1225
|
+
if (!ownsTurn() || activeTurn.suppressQueueDrain)
|
|
1226
|
+
return;
|
|
1227
|
+
await this.messageOperations.run(`${scopeId}:preview:${activeTurn.taskId}`, () => this.messaging.endTaskPreview?.(scopeId, activeTurn.taskId) ?? Promise.resolve());
|
|
938
1228
|
this.activeTurns.delete(scopeId);
|
|
939
1229
|
this.store.removeActiveTurnPreview(turnKey);
|
|
940
|
-
|
|
941
|
-
this.
|
|
942
|
-
this.syncCurrentBackendSettings(scopeId);
|
|
1230
|
+
try {
|
|
1231
|
+
await this.executeTurn(event, prompt, locale, nextRetryCount, stagedAttachments, { ...options, reuseMessageId: activeTurn.messageId || undefined, taskId: activeTurn.taskId });
|
|
943
1232
|
}
|
|
944
|
-
|
|
945
|
-
this.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
cachedTokens: res.usage.cachedTokens,
|
|
949
|
-
totalTokens: res.usage.totalTokens,
|
|
950
|
-
}, adapter.id);
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
if (!this.stopped && !this.activeTurns.has(scopeId))
|
|
1235
|
+
this.activeTurns.set(scopeId, activeTurn);
|
|
1236
|
+
throw error;
|
|
951
1237
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1238
|
+
});
|
|
1239
|
+
};
|
|
1240
|
+
const finishTurn = async (status, error) => {
|
|
1241
|
+
if (!ownsTurn())
|
|
1242
|
+
return;
|
|
1243
|
+
this.activeTurns.delete(scopeId);
|
|
1244
|
+
this.store.removeActiveTurnPreview(turnKey);
|
|
1245
|
+
const task = this.store.taskJournal.get(activeTurn.taskId);
|
|
1246
|
+
if (task.state === 'delivery_pending' && task.deliveredChunks < task.delivery.length) {
|
|
1247
|
+
this.logger.warn('orchestrator.delivery_pending', { taskId: task.id, scopeId });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
this.store.taskJournal.update(task.id, status === 'SUCCESS' ? 'completed' : status === 'INTERRUPTED' ? 'cancelled' : 'failed', { error: error ?? null });
|
|
1251
|
+
if (activeTurn.queueId)
|
|
1252
|
+
this.store.updateQueuedTurnInputStatus(activeTurn.queueId, status === 'SUCCESS' ? 'completed' : status === 'INTERRUPTED' ? 'cancelled' : 'failed', error ?? null);
|
|
1253
|
+
if (!activeTurn.suppressQueueDrain)
|
|
1254
|
+
await this.drainNextQueuedTurn(event, locale);
|
|
1255
|
+
};
|
|
1256
|
+
execution.on('result', (res) => {
|
|
1257
|
+
this.trackSettlement(async () => {
|
|
1258
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
969
1259
|
return;
|
|
1260
|
+
activeTurn.settling = true;
|
|
1261
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { result: res, request: { ...req, threadId: res.conversationId || activeTurn.threadId } });
|
|
1262
|
+
try {
|
|
1263
|
+
if (activeTurn.flushTimer)
|
|
1264
|
+
clearTimeout(activeTurn.flushTimer);
|
|
1265
|
+
if (activeTurn.typingTimer)
|
|
1266
|
+
clearInterval(activeTurn.typingTimer);
|
|
1267
|
+
if (res.conversationId && (!binding?.threadId || binding.threadId !== res.conversationId)) {
|
|
1268
|
+
this.store.setBinding(scopeId, res.conversationId, cwd);
|
|
1269
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
1270
|
+
}
|
|
1271
|
+
if (res.usage) {
|
|
1272
|
+
this.store.recordTokenUsage({
|
|
1273
|
+
inputTokens: res.usage.inputTokens,
|
|
1274
|
+
outputTokens: res.usage.outputTokens,
|
|
1275
|
+
cachedTokens: res.usage.cachedTokens,
|
|
1276
|
+
totalTokens: res.usage.totalTokens,
|
|
1277
|
+
}, adapter.id);
|
|
1278
|
+
}
|
|
1279
|
+
if (res.status === 'SUCCESS') {
|
|
1280
|
+
let finalText = (res.response || '').trim();
|
|
1281
|
+
if (!finalText && activeTurn.accumulatedText) {
|
|
1282
|
+
finalText = activeTurn.accumulatedText.trim();
|
|
1283
|
+
}
|
|
1284
|
+
const foldedTools = buildFoldedToolsSummary({
|
|
1285
|
+
stepIndex: activeTurn.stepIndex,
|
|
1286
|
+
toolLines: activeTurn.toolLines,
|
|
1287
|
+
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
1288
|
+
startTime: activeTurn.startTime,
|
|
1289
|
+
usage: res.usage,
|
|
1290
|
+
locale,
|
|
1291
|
+
});
|
|
1292
|
+
const chunks = combineSummaryAndResponse(foldedTools, finalText || '(无输出 / No output)');
|
|
1293
|
+
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (res.status === 'ERROR') {
|
|
1297
|
+
const errorText = (res.error || res.response || res.error || 'Unknown error').trim();
|
|
1298
|
+
if (adapter.handleTurnError) {
|
|
1299
|
+
try {
|
|
1300
|
+
const handled = await adapter.handleTurnError({
|
|
1301
|
+
error: errorText,
|
|
1302
|
+
request: req,
|
|
1303
|
+
retryCount,
|
|
1304
|
+
retryTurn,
|
|
1305
|
+
sendMessage: (text) => this.sendMessage(scopeId, text),
|
|
1306
|
+
editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
|
|
1307
|
+
});
|
|
1308
|
+
if (handled)
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
catch (adapterErr) {
|
|
1312
|
+
this.logger.warn('orchestrator.adapter_handle_turn_error_failed', { error: String(adapterErr) });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (isTransientNetworkError(errorText) && retryCount < 3) {
|
|
1316
|
+
const backoffMs = Math.min(15000, 3000 * Math.pow(2, retryCount));
|
|
1317
|
+
const delaySec = Math.round(backoffMs / 1000);
|
|
1318
|
+
try {
|
|
1319
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1320
|
+
? `🌐 **网络波动重试 (第 ${retryCount + 1}/3 次)**: 正在等待网络稳定 (${delaySec}s) 后自动恢复任务…`
|
|
1321
|
+
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
/* ignore messaging error during network glitch */
|
|
1325
|
+
}
|
|
1326
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1327
|
+
try {
|
|
1328
|
+
await retryTurn(retryCount + 1);
|
|
1329
|
+
}
|
|
1330
|
+
catch (retryErr) {
|
|
1331
|
+
this.logger.error('orchestrator.retry_turn_failed', { error: String(retryErr) });
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
// If the agent actually produced substantive response text, do NOT mask it with an error box!
|
|
1336
|
+
let finalText = (res.response || '').trim();
|
|
1337
|
+
if (!finalText && activeTurn.accumulatedText) {
|
|
1338
|
+
finalText = activeTurn.accumulatedText.trim();
|
|
1339
|
+
}
|
|
1340
|
+
const isPureError = !finalText ||
|
|
1341
|
+
finalText === errorText ||
|
|
1342
|
+
finalText.startsWith('Process exited with code') ||
|
|
1343
|
+
finalText.startsWith('Verification Required') ||
|
|
1344
|
+
finalText.startsWith('Error:') ||
|
|
1345
|
+
finalText.length <= 20;
|
|
1346
|
+
if (!isPureError) {
|
|
1347
|
+
const foldedTools = buildFoldedToolsSummary({
|
|
1348
|
+
stepIndex: activeTurn.stepIndex,
|
|
1349
|
+
toolLines: activeTurn.toolLines,
|
|
1350
|
+
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
1351
|
+
startTime: activeTurn.startTime,
|
|
1352
|
+
usage: res.usage,
|
|
1353
|
+
locale,
|
|
1354
|
+
});
|
|
1355
|
+
const warningNote = errorText && errorText !== 'Unknown error' && errorText !== 'Antigravity execution failed'
|
|
1356
|
+
? `\n\n⚠️ <i>(注意:任务结束时伴随提示: ${escapeHtml(errorText.slice(0, 120))})</i>`
|
|
1357
|
+
: '';
|
|
1358
|
+
const chunks = combineSummaryAndResponse(foldedTools, finalText + warningNote);
|
|
1359
|
+
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **${adapter.name} 错误**:\n\`\`\`\n${errorText}\n\`\`\``);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, activeTurn.accumulatedText || '⚠️ 执行结束');
|
|
1366
|
+
}
|
|
1367
|
+
catch (fatalErr) {
|
|
1368
|
+
this.logger.error('orchestrator.turn_result_unhandled', { error: String(fatalErr) });
|
|
970
1369
|
}
|
|
971
|
-
|
|
972
|
-
|
|
1370
|
+
finally {
|
|
1371
|
+
await finishTurn(res.status, res.error).catch(error => this.logger.warn('orchestrator.finish_failed', { error: String(error) }));
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
execution.on('error', (err) => {
|
|
1376
|
+
this.trackSettlement(async () => {
|
|
1377
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1378
|
+
return;
|
|
1379
|
+
activeTurn.settling = true;
|
|
1380
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { result: { kind: 'result', status: 'ERROR', response: activeTurn.accumulatedText, error: err.message, conversationId: activeTurn.threadId } });
|
|
1381
|
+
try {
|
|
1382
|
+
if (activeTurn.flushTimer)
|
|
1383
|
+
clearTimeout(activeTurn.flushTimer);
|
|
1384
|
+
if (activeTurn.typingTimer)
|
|
1385
|
+
clearInterval(activeTurn.typingTimer);
|
|
973
1386
|
if (adapter.handleTurnError) {
|
|
974
1387
|
try {
|
|
975
1388
|
const handled = await adapter.handleTurnError({
|
|
976
|
-
error:
|
|
1389
|
+
error: err.message,
|
|
977
1390
|
request: req,
|
|
978
1391
|
retryCount,
|
|
979
1392
|
retryTurn,
|
|
@@ -984,10 +1397,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
984
1397
|
return;
|
|
985
1398
|
}
|
|
986
1399
|
catch (adapterErr) {
|
|
987
|
-
this.logger.warn('orchestrator.
|
|
1400
|
+
this.logger.warn('orchestrator.adapter_handle_error_failed', { error: String(adapterErr) });
|
|
988
1401
|
}
|
|
989
1402
|
}
|
|
990
|
-
if (isTransientNetworkError(
|
|
1403
|
+
if (isTransientNetworkError(err.message) && retryCount < 3) {
|
|
991
1404
|
const backoffMs = Math.min(15000, 3000 * Math.pow(2, retryCount));
|
|
992
1405
|
const delaySec = Math.round(backoffMs / 1000);
|
|
993
1406
|
try {
|
|
@@ -996,7 +1409,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
996
1409
|
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
997
1410
|
}
|
|
998
1411
|
catch {
|
|
999
|
-
/* ignore
|
|
1412
|
+
/* ignore */
|
|
1000
1413
|
}
|
|
1001
1414
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1002
1415
|
try {
|
|
@@ -1007,137 +1420,64 @@ export class UnifiedChannelOrchestrator {
|
|
|
1007
1420
|
}
|
|
1008
1421
|
return;
|
|
1009
1422
|
}
|
|
1010
|
-
|
|
1011
|
-
let finalText = (res.response || '').trim();
|
|
1012
|
-
if (!finalText && activeTurn.accumulatedText) {
|
|
1013
|
-
finalText = activeTurn.accumulatedText.trim();
|
|
1014
|
-
}
|
|
1015
|
-
const isPureError = !finalText ||
|
|
1016
|
-
finalText === errorText ||
|
|
1017
|
-
finalText.startsWith('Process exited with code') ||
|
|
1018
|
-
finalText.startsWith('Verification Required') ||
|
|
1019
|
-
finalText.startsWith('Error:') ||
|
|
1020
|
-
finalText.length <= 20;
|
|
1021
|
-
if (!isPureError) {
|
|
1423
|
+
if (activeTurn.accumulatedText && activeTurn.accumulatedText.trim().length > 20) {
|
|
1022
1424
|
const foldedTools = buildFoldedToolsSummary({
|
|
1023
1425
|
stepIndex: activeTurn.stepIndex,
|
|
1024
1426
|
toolLines: activeTurn.toolLines,
|
|
1025
1427
|
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
1026
1428
|
startTime: activeTurn.startTime,
|
|
1027
|
-
usage: res.usage,
|
|
1028
1429
|
locale,
|
|
1029
1430
|
});
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const chunks = combineSummaryAndResponse(foldedTools, finalText + warningNote);
|
|
1431
|
+
const fullAnswer = activeTurn.accumulatedText.trim() +
|
|
1432
|
+
`\n\n⚠️ <i>(注意:任务执行中途异常中断: ${escapeHtml(err.message.slice(0, 120))})</i>`;
|
|
1433
|
+
const chunks = combineSummaryAndResponse(foldedTools, fullAnswer);
|
|
1034
1434
|
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1035
|
-
if (!activeTurn.suppressQueueDrain)
|
|
1036
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1037
1435
|
return;
|
|
1038
1436
|
}
|
|
1039
|
-
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌
|
|
1040
|
-
if (!activeTurn.suppressQueueDrain)
|
|
1041
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1042
|
-
return;
|
|
1437
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **执行异常**:\n\`\`\`\n${err.message}\n\`\`\``);
|
|
1043
1438
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1047
|
-
}
|
|
1048
|
-
catch (fatalErr) {
|
|
1049
|
-
this.logger.error('orchestrator.turn_result_unhandled', { error: String(fatalErr) });
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
execution.on('error', async (err) => {
|
|
1053
|
-
try {
|
|
1054
|
-
if (activeTurn.flushTimer)
|
|
1055
|
-
clearTimeout(activeTurn.flushTimer);
|
|
1056
|
-
if (activeTurn.typingTimer)
|
|
1057
|
-
clearInterval(activeTurn.typingTimer);
|
|
1058
|
-
this.activeTurns.delete(scopeId);
|
|
1059
|
-
this.store.removeActiveTurnPreview(turnKey);
|
|
1060
|
-
if (adapter.handleTurnError) {
|
|
1061
|
-
try {
|
|
1062
|
-
const handled = await adapter.handleTurnError({
|
|
1063
|
-
error: err.message,
|
|
1064
|
-
request: req,
|
|
1065
|
-
retryCount,
|
|
1066
|
-
retryTurn,
|
|
1067
|
-
sendMessage: (text) => this.sendMessage(scopeId, text),
|
|
1068
|
-
editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
|
|
1069
|
-
});
|
|
1070
|
-
if (handled)
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
catch (adapterErr) {
|
|
1074
|
-
this.logger.warn('orchestrator.adapter_handle_error_failed', { error: String(adapterErr) });
|
|
1075
|
-
}
|
|
1439
|
+
catch (fatalErr) {
|
|
1440
|
+
this.logger.error('orchestrator.turn_error_unhandled', { error: String(fatalErr) });
|
|
1076
1441
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const delaySec = Math.round(backoffMs / 1000);
|
|
1080
|
-
try {
|
|
1081
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
1082
|
-
? `🌐 **网络波动重试 (第 ${retryCount + 1}/3 次)**: 正在等待网络稳定 (${delaySec}s) 后自动恢复任务…`
|
|
1083
|
-
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
1084
|
-
}
|
|
1085
|
-
catch {
|
|
1086
|
-
/* ignore */
|
|
1087
|
-
}
|
|
1088
|
-
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1089
|
-
try {
|
|
1090
|
-
await retryTurn(retryCount + 1);
|
|
1091
|
-
}
|
|
1092
|
-
catch (retryErr) {
|
|
1093
|
-
this.logger.error('orchestrator.retry_turn_failed', { error: String(retryErr) });
|
|
1094
|
-
}
|
|
1095
|
-
return;
|
|
1442
|
+
finally {
|
|
1443
|
+
await finishTurn('ERROR', err.message).catch(error => this.logger.warn('orchestrator.finish_failed', { error: String(error) }));
|
|
1096
1444
|
}
|
|
1097
|
-
|
|
1098
|
-
const foldedTools = buildFoldedToolsSummary({
|
|
1099
|
-
stepIndex: activeTurn.stepIndex,
|
|
1100
|
-
toolLines: activeTurn.toolLines,
|
|
1101
|
-
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
1102
|
-
startTime: activeTurn.startTime,
|
|
1103
|
-
locale,
|
|
1104
|
-
});
|
|
1105
|
-
const fullAnswer = activeTurn.accumulatedText.trim() +
|
|
1106
|
-
`\n\n⚠️ <i>(注意:任务执行中途异常中断: ${escapeTelegramHtml(err.message.slice(0, 120))})</i>`;
|
|
1107
|
-
const chunks = combineSummaryAndResponse(foldedTools, fullAnswer);
|
|
1108
|
-
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1109
|
-
if (!activeTurn.suppressQueueDrain)
|
|
1110
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **执行异常**:\n\`\`\`\n${err.message}\n\`\`\``);
|
|
1114
|
-
if (!activeTurn.suppressQueueDrain)
|
|
1115
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1116
|
-
}
|
|
1117
|
-
catch (fatalErr) {
|
|
1118
|
-
this.logger.error('orchestrator.turn_error_unhandled', { error: String(fatalErr) });
|
|
1119
|
-
}
|
|
1445
|
+
});
|
|
1120
1446
|
});
|
|
1121
1447
|
}
|
|
1448
|
+
async supportedEfforts(scopeId) {
|
|
1449
|
+
const backend = this.getBackendDescriptorForScope(scopeId);
|
|
1450
|
+
const current = this.store.getChatSettings(scopeId)?.model;
|
|
1451
|
+
const models = await backend.adapter.listModels(scopeId);
|
|
1452
|
+
const selected = models.find(model => current ? model.id === current : model.isDefault);
|
|
1453
|
+
return selected?.supportedReasoningEfforts ?? backend.defaults?.supportedReasoningEfforts ?? ['low', 'medium', 'high'];
|
|
1454
|
+
}
|
|
1122
1455
|
async handleEffortCommand(scopeId, args, locale) {
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1456
|
+
const supported = await this.supportedEfforts(scopeId);
|
|
1457
|
+
const target = args.trim();
|
|
1458
|
+
if (target) {
|
|
1459
|
+
const effort = target === 'default' ? null : target;
|
|
1460
|
+
if (effort && !supported.includes(effort)) {
|
|
1461
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `当前模型不支持推理档位 \`${effort}\`。可用:default ${supported.join(' ')}` : `Unsupported reasoning effort \`${effort}\`. Available: default ${supported.join(' ')}`);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1125
1464
|
const settings = this.store.getChatSettings(scopeId);
|
|
1126
|
-
if (
|
|
1465
|
+
if (effort !== 'high' && settings?.serviceTier === 'boost')
|
|
1127
1466
|
this.store.setChatServiceTier(scopeId, null);
|
|
1128
|
-
|
|
1129
|
-
this.store.setChatEffort(scopeId, target);
|
|
1467
|
+
this.store.setChatEffort(scopeId, effort);
|
|
1130
1468
|
this.syncCurrentBackendSettings(scopeId);
|
|
1131
|
-
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${
|
|
1469
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${effort ?? 'default'}\`` : `✅ Reasoning effort set to: \`${effort ?? 'default'}\``);
|
|
1132
1470
|
}
|
|
1133
1471
|
else {
|
|
1134
|
-
const current = this.store.getChatSettings(scopeId)?.reasoningEffort ?? '
|
|
1135
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
1136
|
-
? `• **当前思考深度**: \`${current}\`\n\n用法: \`/effort <low|medium|high>\``
|
|
1137
|
-
: `• **Current effort**: \`${current}\`\n\nUsage: \`/effort <low|medium|high>\``);
|
|
1472
|
+
const current = this.store.getChatSettings(scopeId)?.reasoningEffort ?? 'default';
|
|
1473
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `当前思考深度: \`${current}\`\n可用:default ${supported.join(' ')}` : `Current effort: \`${current}\`\nAvailable: default ${supported.join(' ')}`);
|
|
1138
1474
|
}
|
|
1139
1475
|
}
|
|
1140
1476
|
async handleBoostCommand(scopeId, args, locale) {
|
|
1477
|
+
if (this.getBackendDescriptorForScope(scopeId).defaults?.boost === false) {
|
|
1478
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '当前后端不支持 Boost。' : 'Boost is not available on this backend.');
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1141
1481
|
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
1142
1482
|
const target = args.trim().toLowerCase();
|
|
1143
1483
|
let nextBoost = !isBoost;
|
|
@@ -1176,6 +1516,15 @@ export class UnifiedChannelOrchestrator {
|
|
|
1176
1516
|
}
|
|
1177
1517
|
}
|
|
1178
1518
|
async handleQueueCommand(event, args, locale) {
|
|
1519
|
+
const prompt = args.trim();
|
|
1520
|
+
if (prompt && prompt.toLowerCase() !== 'clear' && this.store.taskJournal.findReceipt(event, prompt))
|
|
1521
|
+
return;
|
|
1522
|
+
const task = prompt && prompt.toLowerCase() !== 'clear' ? this.captureTask(event, prompt, locale) : null;
|
|
1523
|
+
await this.scopeOperations.run(event.scopeId, () => this.handleQueueCommandNow(event, args, locale, task?.id));
|
|
1524
|
+
if (args.trim() && args.trim().toLowerCase() !== 'clear' && !this.stopped)
|
|
1525
|
+
await this.drainNextQueuedTurn(event, locale);
|
|
1526
|
+
}
|
|
1527
|
+
async handleQueueCommandNow(event, args, locale, taskId) {
|
|
1179
1528
|
const scopeId = event.scopeId;
|
|
1180
1529
|
if (!args.trim()) {
|
|
1181
1530
|
const count = this.store.countQueuedTurnInputs(scopeId);
|
|
@@ -1185,15 +1534,33 @@ export class UnifiedChannelOrchestrator {
|
|
|
1185
1534
|
return;
|
|
1186
1535
|
}
|
|
1187
1536
|
if (args.trim().toLowerCase() === 'clear') {
|
|
1188
|
-
const cleared = this.
|
|
1537
|
+
const cleared = this.cancelQueuedWork(scopeId);
|
|
1189
1538
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
1190
1539
|
? `🗑️ 已清空当前会话的 ${cleared} 条排队任务。`
|
|
1191
1540
|
: `🗑️ Cleared ${cleared} queued tasks.`);
|
|
1192
1541
|
return;
|
|
1193
1542
|
}
|
|
1194
|
-
|
|
1543
|
+
const binding = this.store.getBinding(scopeId);
|
|
1544
|
+
try {
|
|
1545
|
+
const task = taskId ? this.store.taskJournal.get(taskId) : null;
|
|
1546
|
+
const staged = event.attachments?.length ? await stageInboundAttachments(this.messaging, task?.request.cwd || binding?.cwd || this.config.defaultCwd, task?.request.threadId || binding?.threadId || 'default', event.attachments, this.logger) : [];
|
|
1547
|
+
if (staged.length !== event.attachments.length)
|
|
1548
|
+
throw new Error('Some attachments could not be saved; the task was not queued');
|
|
1549
|
+
await this.enqueuePromptTurn(event, args.trim(), locale, staged, taskId);
|
|
1550
|
+
}
|
|
1551
|
+
catch (error) {
|
|
1552
|
+
if (taskId)
|
|
1553
|
+
this.store.taskJournal.update(taskId, 'failed', { error: String(error) });
|
|
1554
|
+
await this.sendMessage(scopeId, `⚠️ ${String(error)}`);
|
|
1555
|
+
}
|
|
1195
1556
|
}
|
|
1196
1557
|
async handleSteerCommand(event, args, locale) {
|
|
1558
|
+
if (this.stopped || (args.trim() && this.store.taskJournal.findReceipt(event, args.trim())))
|
|
1559
|
+
return;
|
|
1560
|
+
const task = args.trim() ? this.captureTask(event, args.trim(), locale) : null;
|
|
1561
|
+
return this.scopeOperations.run(event.scopeId, () => this.handleSteerCommandNow(event, args, locale, task?.id));
|
|
1562
|
+
}
|
|
1563
|
+
async handleSteerCommandNow(event, args, locale, taskId) {
|
|
1197
1564
|
const scopeId = event.scopeId;
|
|
1198
1565
|
if (!args.trim()) {
|
|
1199
1566
|
await this.sendMessage(scopeId, locale === 'zh' ? '用法: `/steer <消息>` (立即中断当前思考,带入新指令)' : 'Usage: `/steer <message>`');
|
|
@@ -1201,36 +1568,23 @@ export class UnifiedChannelOrchestrator {
|
|
|
1201
1568
|
}
|
|
1202
1569
|
const active = this.activeTurns.get(scopeId);
|
|
1203
1570
|
if (active) {
|
|
1204
|
-
|
|
1205
|
-
active.suppressQueueDrain = true;
|
|
1206
|
-
active.execution.cancel();
|
|
1207
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
1208
|
-
await active.execution.waitForResult();
|
|
1209
|
-
if (active.typingTimer)
|
|
1210
|
-
clearInterval(active.typingTimer);
|
|
1211
|
-
if (active.flushTimer)
|
|
1212
|
-
clearTimeout(active.flushTimer);
|
|
1213
|
-
this.activeTurns.delete(scopeId);
|
|
1571
|
+
await this.cancelActiveTurn(scopeId);
|
|
1214
1572
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
1215
1573
|
? `⚡ 已中断前置任务,立即开始插话指令:\n> ${args.trim()}`
|
|
1216
1574
|
: `⚡ Interrupted active turn, executing steer instruction:\n> ${args.trim()}`);
|
|
1217
1575
|
}
|
|
1218
|
-
await this.
|
|
1576
|
+
await this.startPromptTurnNow(event, args.trim(), locale, { taskId });
|
|
1219
1577
|
}
|
|
1220
1578
|
async handleInterrupt(scopeId, locale) {
|
|
1579
|
+
return this.scopeOperations.run(scopeId, () => this.handleInterruptNow(scopeId, locale));
|
|
1580
|
+
}
|
|
1581
|
+
async handleInterruptNow(scopeId, locale) {
|
|
1221
1582
|
const active = this.activeTurns.get(scopeId);
|
|
1222
1583
|
const queuedCount = this.store.countQueuedTurnInputs(scopeId);
|
|
1223
1584
|
if (active) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
1228
|
-
await active.execution.waitForResult();
|
|
1229
|
-
if (active.flushTimer)
|
|
1230
|
-
clearTimeout(active.flushTimer);
|
|
1231
|
-
if (active.typingTimer)
|
|
1232
|
-
clearInterval(active.typingTimer);
|
|
1233
|
-
this.activeTurns.delete(scopeId);
|
|
1585
|
+
await this.cancelActiveTurn(scopeId);
|
|
1586
|
+
if (this.messaging.beginTaskPreview)
|
|
1587
|
+
return;
|
|
1234
1588
|
let extraMsg = '';
|
|
1235
1589
|
if (queuedCount > 0) {
|
|
1236
1590
|
extraMsg = locale === 'zh'
|
|
@@ -1240,14 +1594,21 @@ export class UnifiedChannelOrchestrator {
|
|
|
1240
1594
|
await this.sendMessage(scopeId, (locale === 'zh' ? '🛑 已发送中断请求。' : '🛑 Interrupt request sent.') + extraMsg);
|
|
1241
1595
|
}
|
|
1242
1596
|
else if (queuedCount > 0) {
|
|
1243
|
-
const cleared = this.
|
|
1597
|
+
const cleared = this.cancelQueuedWork(scopeId);
|
|
1244
1598
|
await this.sendMessage(scopeId, locale === 'zh' ? `🛑 当前无运行中任务,已清空 ${cleared} 条排队任务。` : `🛑 Cleared ${cleared} queued tasks.`);
|
|
1245
1599
|
}
|
|
1246
1600
|
else {
|
|
1247
1601
|
await this.sendMessage(scopeId, locale === 'zh' ? 'ℹ️ 当前没有正在执行或排队的任务。' : 'ℹ️ No active or queued tasks to interrupt.');
|
|
1248
1602
|
}
|
|
1249
1603
|
}
|
|
1250
|
-
async handleNewSession(scopeId, locale,
|
|
1604
|
+
async handleNewSession(scopeId, locale, cwdArg = '') {
|
|
1605
|
+
return this.scopeOperations.run(scopeId, () => this.handleNewSessionNow(scopeId, locale, cwdArg));
|
|
1606
|
+
}
|
|
1607
|
+
async handleNewSessionNow(scopeId, locale, targetCwdInput) {
|
|
1608
|
+
if (this.recoveryBlocksScope(scopeId)) {
|
|
1609
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '先用 /recover 处理待确认或待发送的任务,再新建会话。' : 'Resolve the pending task with /recover before creating a session.');
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1251
1612
|
const rawTarget = targetCwdInput?.trim();
|
|
1252
1613
|
const binding = this.store.getBinding(scopeId);
|
|
1253
1614
|
let cwd = binding?.cwd || this.config.defaultCwd;
|
|
@@ -1276,20 +1637,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1276
1637
|
cwd = resolved;
|
|
1277
1638
|
}
|
|
1278
1639
|
// Cancel queued turn inputs
|
|
1279
|
-
this.
|
|
1280
|
-
|
|
1281
|
-
if (active) {
|
|
1282
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
1283
|
-
active.suppressQueueDrain = true;
|
|
1284
|
-
active.execution.cancel();
|
|
1285
|
-
if (this.getAdapterForScope(scopeId).id === 'dsh')
|
|
1286
|
-
await active.execution.waitForResult();
|
|
1287
|
-
if (active.typingTimer)
|
|
1288
|
-
clearInterval(active.typingTimer);
|
|
1289
|
-
if (active.flushTimer)
|
|
1290
|
-
clearTimeout(active.flushTimer);
|
|
1291
|
-
this.activeTurns.delete(scopeId);
|
|
1292
|
-
}
|
|
1640
|
+
this.cancelQueuedWork(scopeId);
|
|
1641
|
+
await this.cancelActiveTurn(scopeId);
|
|
1293
1642
|
// Reset thread binding to empty/ready state with the target cwd
|
|
1294
1643
|
this.store.setBinding(scopeId, '', cwd);
|
|
1295
1644
|
this.syncCurrentBackendSettings(scopeId);
|
|
@@ -1304,13 +1653,14 @@ export class UnifiedChannelOrchestrator {
|
|
|
1304
1653
|
const isBusy = this.activeTurns.has(scopeId);
|
|
1305
1654
|
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1306
1655
|
const adapter = backendDesc.adapter;
|
|
1307
|
-
const
|
|
1656
|
+
const ui = this.backends.ui(backendDesc.id) ?? this.customUi;
|
|
1657
|
+
const customStatus = await ui?.renderCustomStatus?.(scopeId, locale);
|
|
1308
1658
|
const totalUsage = this.store.getCumulativeTokenUsage();
|
|
1309
1659
|
const tokenLine = formatTokenUsageSummary(totalUsage, locale);
|
|
1310
1660
|
const allUsages = this.store.getAllBackendTokenUsages();
|
|
1311
1661
|
const breakdown = formatBackendTokenUsageBreakdown(allUsages);
|
|
1312
|
-
const usageText = backendDesc.
|
|
1313
|
-
? (locale === 'zh' ? '• **Token 消耗**:
|
|
1662
|
+
const usageText = backendDesc.defaults?.tokenUsage === false
|
|
1663
|
+
? (locale === 'zh' ? '• **Token 消耗**: 当前后端未提供逐轮统计\n' : '• **Token usage**: Per-turn counts are not exposed by this backend\n')
|
|
1314
1664
|
: `${tokenLine}${breakdown}\n`;
|
|
1315
1665
|
const text = locale === 'zh'
|
|
1316
1666
|
? `📊 **${adapter.name} 运行状态**\n\n` +
|
|
@@ -1332,7 +1682,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
1332
1682
|
const keyboard = [
|
|
1333
1683
|
[
|
|
1334
1684
|
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1335
|
-
{ text: '🧠 切换模型', callback_data:
|
|
1685
|
+
{ text: '🧠 切换模型', callback_data: 'engine:setup:models' },
|
|
1336
1686
|
],
|
|
1337
1687
|
];
|
|
1338
1688
|
const allBackends = await this.listBackends();
|
|
@@ -1345,11 +1695,12 @@ export class UnifiedChannelOrchestrator {
|
|
|
1345
1695
|
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1346
1696
|
}
|
|
1347
1697
|
async sendSetupMenu(scopeId, locale, editMessageId) {
|
|
1348
|
-
|
|
1698
|
+
const ui = this.backends.ui(this.getBackendDescriptorForScope(scopeId).id) ?? this.customUi;
|
|
1699
|
+
if (await ui?.renderSetupMenu?.(scopeId, locale, editMessageId))
|
|
1349
1700
|
return;
|
|
1350
1701
|
const settings = this.store.getChatSettings(scopeId);
|
|
1351
1702
|
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
1352
|
-
const isBoost = settings?.serviceTier === 'boost';
|
|
1703
|
+
const isBoost = settings?.serviceTier === 'boost' && this.getBackendDescriptorForScope(scopeId).defaults?.boost !== false;
|
|
1353
1704
|
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1354
1705
|
const adapter = backendDesc.adapter;
|
|
1355
1706
|
const allBackends = await this.listBackends();
|
|
@@ -1360,14 +1711,14 @@ export class UnifiedChannelOrchestrator {
|
|
|
1360
1711
|
`• **当前引擎**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1361
1712
|
`• **当前模型**: \`${settings?.model || '默认'}\`\n` +
|
|
1362
1713
|
`• **思考深度**: \`${currentEffort || '默认'}\`\n` +
|
|
1363
|
-
`• **Boost 增强**: ${isBoost ? '🚀 已开启' : '⚪ 已关闭'}\n` +
|
|
1714
|
+
(backendDesc.defaults?.boost !== false ? `• **Boost 增强**: ${isBoost ? '🚀 已开启' : '⚪ 已关闭'}\n` : '') +
|
|
1364
1715
|
`• **运行中消息**: ${mode === 'steer' ? '⚡ 插话 (立即中断接管)' : '⏳ 排队 (完成后自动执行)'}\n\n` +
|
|
1365
1716
|
`请选择要配置的项目:`
|
|
1366
1717
|
: `⚙️ **${adapter.name} Setup Panel**\n\n` +
|
|
1367
1718
|
`• **Engine**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1368
1719
|
`• **Model**: \`${settings?.model || 'default'}\`\n` +
|
|
1369
1720
|
`• **Effort**: \`${currentEffort || 'default'}\`\n` +
|
|
1370
|
-
`• **Boost**: ${isBoost ? '🚀 Enabled' : '⚪ Disabled'}\n` +
|
|
1721
|
+
(backendDesc.defaults?.boost !== false ? `• **Boost**: ${isBoost ? '🚀 Enabled' : '⚪ Disabled'}\n` : '') +
|
|
1371
1722
|
`• **Active-Turn**: ${mode === 'steer' ? '⚡ Steer' : '⏳ Queue'}\n\n` +
|
|
1372
1723
|
`Select setting to configure:`;
|
|
1373
1724
|
const keyboard = [];
|
|
@@ -1391,13 +1742,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1391
1742
|
keyboard.push(modelButtons.slice(i, i + 2));
|
|
1392
1743
|
}
|
|
1393
1744
|
// 2. Reasoning effort choices (Codex style)
|
|
1394
|
-
const
|
|
1395
|
-
const
|
|
1396
|
-
const supportedEfforts = isAgy
|
|
1397
|
-
? ['low', 'medium', 'high']
|
|
1398
|
-
: isCodex
|
|
1399
|
-
? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1400
|
-
: ['low', 'medium', 'high'];
|
|
1745
|
+
const selectedModel = models.find(model => currentModel ? model.id === currentModel : model.isDefault);
|
|
1746
|
+
const supportedEfforts = selectedModel?.supportedReasoningEfforts ?? backendDesc.defaults?.supportedReasoningEfforts ?? ['low', 'medium', 'high'];
|
|
1401
1747
|
const effortButtons = [
|
|
1402
1748
|
{
|
|
1403
1749
|
text: `${!currentEffort || currentEffort === 'default' ? '• ' : ''}${locale === 'zh' ? '默认深度' : 'Default'}`,
|
|
@@ -1413,18 +1759,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
1413
1759
|
}
|
|
1414
1760
|
// 3. Boost & Active Mode row
|
|
1415
1761
|
keyboard.push([
|
|
1416
|
-
{
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1762
|
+
...(backendDesc.defaults?.boost !== false ? [{
|
|
1763
|
+
text: isBoost ? (locale === 'zh' ? '🚀 Boost: 开启' : '🚀 Boost: On') : (locale === 'zh' ? '⚪ Boost: 关闭' : '⚪ Boost: Off'),
|
|
1764
|
+
callback_data: 'engine:setup:boost',
|
|
1765
|
+
}] : []),
|
|
1420
1766
|
{
|
|
1421
1767
|
text: mode === 'steer' ? (locale === 'zh' ? '⚡ 插话模式' : '⚡ Steer') : (locale === 'zh' ? '⏳ 排队模式' : '⏳ Queue'),
|
|
1422
1768
|
callback_data: 'engine:setup:active_mode',
|
|
1423
1769
|
},
|
|
1424
1770
|
]);
|
|
1425
1771
|
// 4. Custom rows (Account & History)
|
|
1426
|
-
if (
|
|
1427
|
-
const customRows = await
|
|
1772
|
+
if (ui?.renderCustomSetupRows) {
|
|
1773
|
+
const customRows = await ui.renderCustomSetupRows(scopeId, locale);
|
|
1428
1774
|
keyboard.push(...customRows);
|
|
1429
1775
|
}
|
|
1430
1776
|
// 5. Session & Backend switcher row
|
|
@@ -1446,7 +1792,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1446
1792
|
}
|
|
1447
1793
|
}
|
|
1448
1794
|
async sendModelsMenu(scopeId, locale, editMessageId) {
|
|
1449
|
-
|
|
1795
|
+
const ui = this.backends.ui(this.getBackendDescriptorForScope(scopeId).id) ?? this.customUi;
|
|
1796
|
+
if (await ui?.renderModelsMenu?.(scopeId, locale, editMessageId))
|
|
1450
1797
|
return;
|
|
1451
1798
|
const settings = this.store.getChatSettings(scopeId);
|
|
1452
1799
|
const currentModel = settings?.model;
|
|
@@ -1516,7 +1863,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1516
1863
|
}
|
|
1517
1864
|
async sendBackendMenu(scopeId, locale, editMessageId) {
|
|
1518
1865
|
const backends = await this.listBackends();
|
|
1519
|
-
const
|
|
1866
|
+
const activeId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
1867
|
+
const activeBackend = this.backends.get(activeId) ?? { id: activeId, name: `${activeId} (${locale === 'zh' ? '不可用' : 'unavailable'})`, engineType: activeId, adapter: this.adapter };
|
|
1520
1868
|
const binding = this.store.getBinding(scopeId);
|
|
1521
1869
|
const lines = [
|
|
1522
1870
|
locale === 'zh' ? '🔌 **后端运行环境 (Backends & Engines)**' : '🔌 **Backends & Engines**',
|