@foxden-app/foxclaw 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -1
- package/CHANGELOG.md +36 -0
- package/README.md +4 -3
- package/README_EN.md +4 -3
- 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 -106
- package/dist/antigravity/controller.js +2 -2083
- 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/config.d.ts +4 -0
- package/dist/config.js +41 -5
- 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 +20 -5
- 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 +57 -22
- package/dist/core/orchestrator.js +877 -477
- 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.d.ts +48 -0
- package/dist/dsh/adapter.js +205 -0
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/client.d.ts +46 -0
- package/dist/dsh/client.js +184 -0
- package/dist/dsh/permissions_plugin.d.ts +30 -0
- package/dist/dsh/permissions_plugin.js +21 -0
- package/dist/dsh/runtime.d.ts +20 -0
- package/dist/dsh/runtime.js +36 -0
- package/dist/dsh/ui.d.ts +38 -0
- package/dist/dsh/ui.js +342 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +12 -0
- package/dist/main.js +144 -72
- 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 +10 -4
- package/dist/store/database.js +13 -3
- 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 +2 -2
- package/docs/dsh.md +83 -0
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/dsh.md +83 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +7 -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,12 +20,18 @@ 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();
|
|
33
|
+
recoveryTimers = new Set();
|
|
34
|
+
ownsScope;
|
|
30
35
|
backendProvider;
|
|
31
36
|
get adapter() {
|
|
32
37
|
return this.getAdapterForBackend(this.defaultBackendId);
|
|
@@ -37,9 +42,11 @@ export class UnifiedChannelOrchestrator {
|
|
|
37
42
|
this.logger = options.logger;
|
|
38
43
|
this.bot = options.bot;
|
|
39
44
|
this.messaging = options.messaging;
|
|
40
|
-
this.
|
|
45
|
+
this.backends = new BackendRegistry(this);
|
|
46
|
+
this.serviceUi = options.serviceUi;
|
|
41
47
|
this.customUi = options.customUi;
|
|
42
48
|
this.backendProvider = options.backendProvider;
|
|
49
|
+
this.ownsScope = options.ownsScope ?? (() => true);
|
|
43
50
|
if (options.backends) {
|
|
44
51
|
for (const b of options.backends) {
|
|
45
52
|
this.backends.set(b.id, b);
|
|
@@ -72,30 +79,25 @@ export class UnifiedChannelOrchestrator {
|
|
|
72
79
|
}
|
|
73
80
|
const firstId = Array.from(this.backends.keys())[0] ?? 'default';
|
|
74
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`);
|
|
75
84
|
}
|
|
76
85
|
getAdapterForScope(scopeId) {
|
|
77
86
|
const activeBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
78
87
|
return this.getAdapterForBackend(activeBackendId);
|
|
79
88
|
}
|
|
80
89
|
getBackendDescriptorForScope(scopeId) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
id
|
|
85
|
-
|
|
86
|
-
engineType: activeBackendId,
|
|
87
|
-
adapter: this.adapter,
|
|
88
|
-
});
|
|
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;
|
|
89
95
|
}
|
|
90
96
|
getAdapterForBackend(backendId) {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!fallback) {
|
|
96
|
-
throw new Error(`No engine adapter available in orchestrator for backend '${backendId}'`);
|
|
97
|
-
}
|
|
98
|
-
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;
|
|
99
101
|
}
|
|
100
102
|
registerBackend(desc) {
|
|
101
103
|
this.backends.set(desc.id, desc);
|
|
@@ -109,9 +111,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
109
111
|
for (const b of dynamicList) {
|
|
110
112
|
list.push(b);
|
|
111
113
|
seen.add(b.id);
|
|
112
|
-
|
|
113
|
-
this.backends.set(b.id, b);
|
|
114
|
-
}
|
|
114
|
+
this.backends.set(b.id, b);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
catch (err) {
|
|
@@ -146,7 +146,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
scheduleStalePanelDeletion(scopeId, messageId) {
|
|
149
|
-
if (this.config.telegramPanelTtlMs <= 0 ||
|
|
149
|
+
if (this.config.telegramPanelTtlMs <= 0 || this.messaging.capabilities?.editableMessages === false) {
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
const key = `${scopeId}:${messageId}`;
|
|
@@ -166,6 +166,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
166
166
|
this.stalePanelDeleteTimers.set(key, timer);
|
|
167
167
|
}
|
|
168
168
|
async switchBackend(scopeId, targetBackendId, locale) {
|
|
169
|
+
return this.scopeOperations.run(scopeId, () => this.switchBackendNow(scopeId, targetBackendId, locale));
|
|
170
|
+
}
|
|
171
|
+
async switchBackendNow(scopeId, targetBackendId, locale) {
|
|
169
172
|
const currentBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
170
173
|
const targetDesc = await this.resolveBackendDescriptor(targetBackendId);
|
|
171
174
|
if (!targetDesc) {
|
|
@@ -173,6 +176,11 @@ export class UnifiedChannelOrchestrator {
|
|
|
173
176
|
? `未找到目标后端: ${targetBackendId}`
|
|
174
177
|
: `Target backend not found: ${targetBackendId}`);
|
|
175
178
|
}
|
|
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.');
|
|
183
|
+
}
|
|
176
184
|
if (targetDesc.onSelect) {
|
|
177
185
|
await targetDesc.onSelect(scopeId);
|
|
178
186
|
}
|
|
@@ -206,7 +214,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
206
214
|
else {
|
|
207
215
|
this.store.clearBinding(scopeId);
|
|
208
216
|
}
|
|
209
|
-
this.store.setChatSettings(scopeId, saved.model ?? null,
|
|
217
|
+
this.store.setChatSettings(scopeId, saved.model ?? null, null);
|
|
218
|
+
this.store.setChatEngineEffort(scopeId, saved.reasoningEffort ?? null);
|
|
210
219
|
if (saved.activeTurnMessageMode !== undefined && saved.activeTurnMessageMode !== null) {
|
|
211
220
|
this.store.setChatActiveTurnMessageMode(scopeId, saved.activeTurnMessageMode ?? null);
|
|
212
221
|
}
|
|
@@ -219,29 +228,29 @@ export class UnifiedChannelOrchestrator {
|
|
|
219
228
|
}
|
|
220
229
|
else {
|
|
221
230
|
this.store.clearBinding(scopeId);
|
|
222
|
-
|
|
223
|
-
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);
|
|
224
235
|
this.store.setChatServiceTier(scopeId, null);
|
|
225
236
|
}
|
|
226
|
-
const isCodex = targetDesc.engineType === 'codex';
|
|
227
237
|
const activeSettings = this.store.getChatSettings(scopeId);
|
|
228
238
|
const modelText = activeSettings?.model ? `\`${activeSettings.model}\`` : (locale === 'zh' ? '引擎默认' : 'default');
|
|
229
|
-
const effortText = activeSettings?.reasoningEffort ??
|
|
239
|
+
const effortText = activeSettings?.reasoningEffort ?? targetDesc.defaults?.reasoningEffort ?? 'default';
|
|
230
240
|
const modeText = activeSettings?.activeTurnMessageMode ?? 'queue';
|
|
231
241
|
const switchMsg = locale === 'zh'
|
|
232
242
|
? `🔄 **已切换至后端: ${targetDesc.name}** (\`${targetDesc.id}\`)\n\n` +
|
|
233
|
-
`• **引擎类型**: ${
|
|
234
|
-
|
|
243
|
+
`• **引擎类型**: ${targetDesc.name}\n` +
|
|
244
|
+
(targetDesc.account ? `• **绑定账号**: \`${targetDesc.account}\`\n` : '') +
|
|
235
245
|
`• **会话状态**: ${saved?.threadId ? `已恢复历史会话 (\`${saved.threadId.slice(0, 16)}…\`)\n• **工作目录**: \`${saved.cwd ?? this.config.defaultCwd}\`` : '新会话就绪 (发送消息将开启新会话)'}\n` +
|
|
236
246
|
`• **记忆配置**: 模型 ${modelText} | 思考深度 \`${effortText}\` | 插话模式 \`${modeText}\`\n` +
|
|
237
|
-
`•
|
|
238
|
-
`⚡ **当前 Telegram Bot 已完全切换为 ${isCodex ? 'OpenAI Codex' : 'Google Antigravity'} 交互人格与执行引擎!**`
|
|
247
|
+
`• **操作**: \`/setup\` · \`/models\` · \`/threads\``
|
|
239
248
|
: `🔄 **Switched to Backend: ${targetDesc.name}** (\`${targetDesc.id}\`)\n\n` +
|
|
240
|
-
`• **Engine**: ${
|
|
241
|
-
`• **Account**: \`${targetDesc.account
|
|
249
|
+
`• **Engine**: ${targetDesc.name}\n` +
|
|
250
|
+
(targetDesc.account ? `• **Account**: \`${targetDesc.account}\`\n` : '') +
|
|
242
251
|
`• **Thread**: ${saved?.threadId ? `Restored (\`${saved.threadId.slice(0, 16)}…\`)\n• **Directory**: \`${saved.cwd ?? this.config.defaultCwd}\`` : 'Ready (next prompt starts new thread)'}\n` +
|
|
243
252
|
`• **Restored Settings**: Model ${modelText} | Effort \`${effortText}\` | Message Mode \`${modeText}\`\n\n` +
|
|
244
|
-
|
|
253
|
+
`• **Controls**: \`/setup\` · \`/models\` · \`/threads\``;
|
|
245
254
|
try {
|
|
246
255
|
await this.sendMessage(scopeId, switchMsg);
|
|
247
256
|
}
|
|
@@ -249,11 +258,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
249
258
|
this.logger.warn('orchestrator.switch_announce_failed', { error: String(err) });
|
|
250
259
|
}
|
|
251
260
|
try {
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
const cmds = isCodex ? getTelegramCommands(locale) : getAntigravityTelegramCommands(locale);
|
|
255
|
-
await this.bot.setChatCommands(target.chatId, cmds);
|
|
256
|
-
}
|
|
261
|
+
if (targetDesc.commands)
|
|
262
|
+
await this.messaging.setScopeCommands?.(scopeId, targetDesc.commands(locale));
|
|
257
263
|
}
|
|
258
264
|
catch (err) {
|
|
259
265
|
this.logger.warn('orchestrator.set_chat_commands_failed', { error: String(err) });
|
|
@@ -271,15 +277,44 @@ export class UnifiedChannelOrchestrator {
|
|
|
271
277
|
cwd: saved?.cwd ?? null,
|
|
272
278
|
};
|
|
273
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();
|
|
274
303
|
registerInboundHandlers() {
|
|
275
|
-
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) => {
|
|
276
311
|
this.handleText(event).catch((err) => {
|
|
277
312
|
this.logger.error('orchestrator.inbound_text_error', {
|
|
278
313
|
error: err instanceof Error ? err.message : String(err),
|
|
279
314
|
});
|
|
280
315
|
});
|
|
281
316
|
});
|
|
282
|
-
this.
|
|
317
|
+
this.inboundListeners.listen(this.bot, 'callback', (event) => {
|
|
283
318
|
this.handleCallback(event).catch((err) => {
|
|
284
319
|
this.logger.error('orchestrator.inbound_callback_error', {
|
|
285
320
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -295,125 +330,266 @@ export class UnifiedChannelOrchestrator {
|
|
|
295
330
|
});
|
|
296
331
|
}
|
|
297
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
|
+
}
|
|
298
339
|
await this.bot.start();
|
|
299
340
|
this.logger.info('orchestrator.started', {
|
|
300
341
|
defaultEngine: this.adapter.id,
|
|
301
342
|
backends: Array.from(this.backends.keys()),
|
|
302
343
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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');
|
|
308
366
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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');
|
|
371
|
+
}
|
|
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) });
|
|
318
407
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
414
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const recoveredScopes = new Set();
|
|
327
|
-
for (const prev of activePreviews) {
|
|
328
|
-
this.store.removeActiveTurnPreview(prev.turnId);
|
|
329
|
-
recoveredScopes.add(prev.scopeId);
|
|
330
|
-
// Bind existing threadId immediately so auto-resume continues in the exact same thread
|
|
331
|
-
if (prev.threadId) {
|
|
332
|
-
const currentBinding = this.store.getBinding(prev.scopeId);
|
|
333
|
-
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) });
|
|
334
417
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
forcedThreadId: prev.threadId || undefined,
|
|
358
|
-
}).catch((err) => {
|
|
359
|
-
this.logger.warn('orchestrator.auto_resume_failed', {
|
|
360
|
-
scopeId: prev.scopeId,
|
|
361
|
-
error: String(err),
|
|
362
|
-
});
|
|
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
|
+
}
|
|
363
440
|
});
|
|
364
441
|
}, 1500);
|
|
365
|
-
|
|
442
|
+
this.recoveryTimers.add(timer);
|
|
443
|
+
timer.unref();
|
|
366
444
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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;
|
|
391
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) });
|
|
392
546
|
}
|
|
393
547
|
}
|
|
394
|
-
|
|
395
|
-
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');
|
|
396
557
|
}
|
|
397
558
|
}
|
|
398
559
|
async stop() {
|
|
399
|
-
this.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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();
|
|
566
|
+
for (const timer of this.stalePanelDeleteTimers.values())
|
|
567
|
+
clearTimeout(timer);
|
|
568
|
+
this.stalePanelDeleteTimers.clear();
|
|
569
|
+
for (const timer of this.recoveryTimers)
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
this.recoveryTimers.clear();
|
|
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();
|
|
408
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));
|
|
581
|
+
}
|
|
582
|
+
getPendingApprovals() { return this.backends.getPendingApprovals(); }
|
|
409
583
|
getActiveTurnsCount() {
|
|
410
584
|
return this.activeTurns.size;
|
|
411
585
|
}
|
|
586
|
+
ownsScopeId(scopeId) { return this.ownsScope(scopeId); }
|
|
587
|
+
hasExecutingTasks() { return this.activeTurns.size > 0 || !this.scopeOperations.isIdle() || this.settlements.size > 0; }
|
|
412
588
|
hasActiveTurn(scopeId) {
|
|
413
589
|
return this.activeTurns.has(scopeId);
|
|
414
590
|
}
|
|
415
591
|
async sendMessage(scopeId, text, keyboard) {
|
|
416
|
-
const chunks =
|
|
592
|
+
const chunks = chunkMessage(text, this.messaging.capabilities?.maxMessageLength);
|
|
417
593
|
let lastMsgId = 0;
|
|
418
594
|
for (let i = 0; i < chunks.length; i++) {
|
|
419
595
|
const isLast = i === chunks.length - 1;
|
|
@@ -427,64 +603,97 @@ export class UnifiedChannelOrchestrator {
|
|
|
427
603
|
await this.sendMessage(scopeId, text, keyboard);
|
|
428
604
|
return;
|
|
429
605
|
}
|
|
430
|
-
await this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard);
|
|
606
|
+
await this.messageOperations.run(`${scopeId}:${messageId}`, () => this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard));
|
|
431
607
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
446
628
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.
|
|
629
|
+
else
|
|
630
|
+
await this.sendMessage(task.event.scopeId, chunk);
|
|
631
|
+
task = this.store.taskJournal.update(task.id, 'delivery_pending', { deliveredChunks: index + 1 });
|
|
450
632
|
}
|
|
451
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);
|
|
643
|
+
}
|
|
452
644
|
async safeDeliverMessage(scopeId, messageId, text) {
|
|
453
|
-
|
|
454
|
-
await this.safeDeliverChunks(scopeId, messageId, chunks);
|
|
645
|
+
await this.safeDeliverChunks(scopeId, messageId, chunkMessage(text, this.messaging.capabilities?.maxMessageLength));
|
|
455
646
|
}
|
|
456
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
|
+
}
|
|
457
655
|
const scopeId = event.scopeId;
|
|
458
656
|
const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
|
|
459
|
-
const isDefaultTopic = isDefaultTelegramScope({
|
|
460
|
-
chatType: event.chatType,
|
|
461
|
-
allowedChatId: this.config.tgAllowedChatId ?? null,
|
|
462
|
-
allowedTopicId: this.config.tgAllowedTopicId ?? null,
|
|
463
|
-
topicId: event.topicId ?? null,
|
|
464
|
-
requireExplicitGroupAddressing: false,
|
|
465
|
-
});
|
|
466
657
|
const parsedCommand = parseCommand(event.text);
|
|
467
|
-
const addressing =
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
entities: event.entities,
|
|
471
|
-
command: parsedCommand,
|
|
472
|
-
botUsername: this.bot.username,
|
|
473
|
-
isDefaultTopic,
|
|
474
|
-
replyToBot: event.replyToBot,
|
|
475
|
-
});
|
|
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' });
|
|
476
661
|
if (addressing.kind === 'ignore')
|
|
477
662
|
return;
|
|
478
|
-
if (
|
|
479
|
-
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);
|
|
480
689
|
if (handled)
|
|
481
690
|
return;
|
|
482
691
|
}
|
|
483
692
|
if (addressing.kind === 'command') {
|
|
484
693
|
const commandName = addressing.command.name.toLowerCase();
|
|
485
694
|
const argsString = addressing.command.args.join(' ').trim();
|
|
486
|
-
if (
|
|
487
|
-
const handled = await
|
|
695
|
+
if (ui?.handleCustomCommand) {
|
|
696
|
+
const handled = await ui.handleCustomCommand(scopeId, commandName, argsString, locale, event);
|
|
488
697
|
if (handled)
|
|
489
698
|
return;
|
|
490
699
|
}
|
|
@@ -543,18 +752,35 @@ export class UnifiedChannelOrchestrator {
|
|
|
543
752
|
await this.handleNewSession(scopeId, locale, argsString);
|
|
544
753
|
return;
|
|
545
754
|
}
|
|
755
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `当前后端不支持 /${commandName}。发送 /help 查看可用操作。` : `This backend does not support /${commandName}. Send /help for available commands.`);
|
|
756
|
+
return;
|
|
546
757
|
}
|
|
547
758
|
if (addressing.kind === 'prompt') {
|
|
548
759
|
await this.startPromptTurn(event, addressing.text, locale);
|
|
549
760
|
}
|
|
550
761
|
}
|
|
551
762
|
async handleCallback(event) {
|
|
763
|
+
if (this.stopped || !this.ownsScope(event.scopeId))
|
|
764
|
+
return;
|
|
552
765
|
const scopeId = event.scopeId;
|
|
553
|
-
const locale = 'zh';
|
|
766
|
+
const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
|
|
554
767
|
const data = event.data || '';
|
|
555
768
|
const messageId = event.messageId;
|
|
556
|
-
if (
|
|
557
|
-
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);
|
|
558
784
|
if (handled)
|
|
559
785
|
return;
|
|
560
786
|
}
|
|
@@ -576,6 +802,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
576
802
|
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
577
803
|
return;
|
|
578
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
|
+
}
|
|
579
809
|
if (data.startsWith('engine:m:') || data.startsWith('setup:model:')) {
|
|
580
810
|
const rawModel = data.startsWith('engine:m:')
|
|
581
811
|
? data.slice('engine:m:'.length)
|
|
@@ -592,6 +822,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
592
822
|
? data.slice('engine:effort:'.length)
|
|
593
823
|
: data.slice('setup:effort:'.length);
|
|
594
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
|
+
}
|
|
595
829
|
const settings = this.store.getChatSettings(scopeId);
|
|
596
830
|
if (targetEffort !== 'high' && settings?.serviceTier === 'boost') {
|
|
597
831
|
this.store.setChatServiceTier(scopeId, null);
|
|
@@ -610,6 +844,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
610
844
|
return;
|
|
611
845
|
}
|
|
612
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
|
+
}
|
|
613
851
|
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
614
852
|
const nextBoost = !isBoost;
|
|
615
853
|
this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
|
|
@@ -648,67 +886,112 @@ export class UnifiedChannelOrchestrator {
|
|
|
648
886
|
}
|
|
649
887
|
}
|
|
650
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
|
+
}
|
|
651
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;
|
|
652
922
|
const scopeId = event.scopeId;
|
|
653
923
|
const binding = this.store.getBinding(scopeId);
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
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;
|
|
657
930
|
let stagedAttachments;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
stagedAttachments = staged;
|
|
664
|
-
effectivePrompt = buildAttachmentPrompt(prompt, staged);
|
|
665
|
-
}
|
|
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');
|
|
666
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;
|
|
667
944
|
}
|
|
668
|
-
|
|
945
|
+
const taskOptions = { ...options, taskId: task.id };
|
|
946
|
+
if (this.activeTurns.has(scopeId) || this.recoveryBlocksScope(scopeId)) {
|
|
669
947
|
const settings = this.store.getChatSettings(scopeId);
|
|
670
948
|
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
671
|
-
if (mode === 'steer') {
|
|
672
|
-
|
|
673
|
-
if (active) {
|
|
674
|
-
active.execution.cancel();
|
|
675
|
-
if (active.typingTimer)
|
|
676
|
-
clearInterval(active.typingTimer);
|
|
677
|
-
if (active.flushTimer)
|
|
678
|
-
clearTimeout(active.flushTimer);
|
|
679
|
-
this.activeTurns.delete(scopeId);
|
|
680
|
-
}
|
|
949
|
+
if (mode === 'steer' && !this.recoveryBlocksScope(scopeId)) {
|
|
950
|
+
await this.cancelActiveTurn(scopeId);
|
|
681
951
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
682
952
|
? `⚡ **已插话中断前置任务,立即开始新指令**:\n> ${effectivePrompt}`
|
|
683
953
|
: `⚡ **Interrupted previous turn, executing new instruction**:\n> ${effectivePrompt}`);
|
|
684
|
-
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments,
|
|
954
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, taskOptions);
|
|
685
955
|
return;
|
|
686
956
|
}
|
|
687
|
-
await this.enqueuePromptTurn(event, effectivePrompt, locale);
|
|
957
|
+
await this.enqueuePromptTurn(event, effectivePrompt, locale, stagedAttachments, task.id);
|
|
688
958
|
return;
|
|
689
959
|
}
|
|
690
|
-
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments,
|
|
960
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, taskOptions);
|
|
691
961
|
}
|
|
692
|
-
|
|
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) {
|
|
693
971
|
const scopeId = event.scopeId;
|
|
694
|
-
const
|
|
972
|
+
const journalTask = taskId ? this.store.taskJournal.get(taskId) : null;
|
|
973
|
+
const adapter = journalTask ? this.getAdapterForBackend(journalTask.backendId) : this.getAdapterForScope(scopeId);
|
|
695
974
|
const queueId = `${adapter.id}_q_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
696
975
|
const binding = this.store.getBinding(scopeId);
|
|
697
|
-
this.store.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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 } });
|
|
712
995
|
});
|
|
713
996
|
const queueCount = this.store.countQueuedTurnInputs(scopeId);
|
|
714
997
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
@@ -717,53 +1000,56 @@ export class UnifiedChannelOrchestrator {
|
|
|
717
1000
|
}
|
|
718
1001
|
async drainNextQueuedTurn(event, locale) {
|
|
719
1002
|
const scopeId = event.scopeId;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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(() => { });
|
|
730
1027
|
}
|
|
731
1028
|
}
|
|
732
|
-
|
|
733
|
-
/* ignore */
|
|
734
|
-
}
|
|
735
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
736
|
-
? `▶️ **开始执行排队任务**:\n> ${parsedInput}`
|
|
737
|
-
: `▶️ **Executing queued task**:\n> ${parsedInput}`);
|
|
738
|
-
void this.startPromptTurn({
|
|
739
|
-
...event,
|
|
740
|
-
text: parsedInput,
|
|
741
|
-
}, parsedInput, locale);
|
|
742
|
-
}
|
|
743
|
-
catch (err) {
|
|
744
|
-
this.logger.warn('orchestrator.drain_queue_failed', { error: String(err) });
|
|
745
|
-
}
|
|
1029
|
+
});
|
|
746
1030
|
}
|
|
747
1031
|
async executeTurn(event, prompt, locale, retryCount = 0, stagedAttachments, options) {
|
|
1032
|
+
if (this.stopped)
|
|
1033
|
+
throw new Error('Orchestrator is stopped');
|
|
748
1034
|
const scopeId = event.scopeId;
|
|
749
1035
|
const binding = this.store.getBinding(scopeId);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
? `[Boost Mode: Proceed with deep thinking, strategic planning, multiple perspectives, and rigorous verification.]\n\n${prompt}`
|
|
759
|
-
: 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';
|
|
760
1044
|
let initialMsgId = 0;
|
|
761
1045
|
if (options?.reuseMessageId) {
|
|
762
1046
|
initialMsgId = options.reuseMessageId;
|
|
763
1047
|
try {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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}` }]]);
|
|
767
1053
|
}
|
|
768
1054
|
catch (editErr) {
|
|
769
1055
|
this.logger.warn('orchestrator.reuse_message_failed', { error: String(editErr) });
|
|
@@ -771,15 +1057,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
771
1057
|
}
|
|
772
1058
|
else {
|
|
773
1059
|
try {
|
|
774
|
-
|
|
1060
|
+
const initialText = locale === 'zh'
|
|
775
1061
|
? (isBoost ? `🚀 ${adapter.name} (Boost 模式) 正在深度思考中…` : `⏳ ${adapter.name} 正在思考中…`)
|
|
776
|
-
: (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}` }]]);
|
|
777
1066
|
}
|
|
778
1067
|
catch (sendErr) {
|
|
779
1068
|
this.logger.warn('orchestrator.initial_message_failed', { error: String(sendErr) });
|
|
780
1069
|
}
|
|
781
1070
|
}
|
|
782
|
-
const turnKey = `${adapter.id}_${
|
|
1071
|
+
const turnKey = `${adapter.id}_${task.id}`;
|
|
783
1072
|
try {
|
|
784
1073
|
this.store.saveActiveTurnPreview({
|
|
785
1074
|
turnId: turnKey,
|
|
@@ -791,17 +1080,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
791
1080
|
catch {
|
|
792
1081
|
/* ignore */
|
|
793
1082
|
}
|
|
794
|
-
|
|
795
|
-
scopeId,
|
|
796
|
-
prompt: effectivePrompt,
|
|
797
|
-
stagedAttachments,
|
|
798
|
-
threadId,
|
|
799
|
-
cwd,
|
|
800
|
-
model,
|
|
801
|
-
effort,
|
|
802
|
-
serviceTier: isBoost ? 'boost' : (settings?.serviceTier ?? null),
|
|
803
|
-
locale,
|
|
804
|
-
};
|
|
1083
|
+
task = this.store.taskJournal.update(task.id, task.state, { request: req, previewMessageId: initialMsgId });
|
|
805
1084
|
if (adapter.preflightTurn) {
|
|
806
1085
|
try {
|
|
807
1086
|
await adapter.preflightTurn(req);
|
|
@@ -810,7 +1089,22 @@ export class UnifiedChannelOrchestrator {
|
|
|
810
1089
|
this.logger.warn('orchestrator.preflight_failed', { error: String(err) });
|
|
811
1090
|
}
|
|
812
1091
|
}
|
|
813
|
-
|
|
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
|
+
}
|
|
814
1108
|
const activeTurn = {
|
|
815
1109
|
scopeId,
|
|
816
1110
|
messageId: initialMsgId,
|
|
@@ -825,6 +1119,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
825
1119
|
toolCount: 0,
|
|
826
1120
|
currentTool: null,
|
|
827
1121
|
startTime: Date.now(),
|
|
1122
|
+
previewKey: turnKey,
|
|
1123
|
+
queueId: options?.queueId,
|
|
1124
|
+
taskId: task.id,
|
|
828
1125
|
};
|
|
829
1126
|
this.activeTurns.set(scopeId, activeTurn);
|
|
830
1127
|
this.messaging.sendTypingInScope(scopeId).catch(() => { });
|
|
@@ -833,13 +1130,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
833
1130
|
scheduleFlush();
|
|
834
1131
|
}, TYPING_INTERVAL_MS);
|
|
835
1132
|
activeTurn.typingTimer?.unref?.();
|
|
1133
|
+
const ownsTurn = () => !this.stopped && !activeTurn.suppressQueueDrain && this.activeTurns.get(scopeId) === activeTurn;
|
|
836
1134
|
const scheduleFlush = () => {
|
|
1135
|
+
if (!ownsTurn() || activeTurn.settling || this.messaging.capabilities?.editableMessages === false)
|
|
1136
|
+
return;
|
|
837
1137
|
if (activeTurn.flushTimer)
|
|
838
1138
|
return;
|
|
839
1139
|
const elapsed = Date.now() - activeTurn.lastFlushTime;
|
|
840
1140
|
const delay = Math.max(0, STREAM_THROTTLE_MS - elapsed);
|
|
841
1141
|
activeTurn.flushTimer = setTimeout(() => {
|
|
842
1142
|
activeTurn.flushTimer = null;
|
|
1143
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1144
|
+
return;
|
|
843
1145
|
activeTurn.lastFlushTime = Date.now();
|
|
844
1146
|
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - activeTurn.startTime) / 1000));
|
|
845
1147
|
const content = renderStreamPreviewContent({
|
|
@@ -852,31 +1154,49 @@ export class UnifiedChannelOrchestrator {
|
|
|
852
1154
|
currentTool: activeTurn.currentTool,
|
|
853
1155
|
elapsedSeconds,
|
|
854
1156
|
});
|
|
855
|
-
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(() => { });
|
|
856
1172
|
}, delay);
|
|
857
1173
|
activeTurn.flushTimer?.unref?.();
|
|
858
1174
|
};
|
|
859
1175
|
execution.on('delta', (delta) => {
|
|
1176
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1177
|
+
return;
|
|
860
1178
|
activeTurn.accumulatedText += delta;
|
|
861
1179
|
scheduleFlush();
|
|
862
1180
|
});
|
|
863
1181
|
execution.on('tool', (tool) => {
|
|
1182
|
+
if (!ownsTurn() || activeTurn.settling)
|
|
1183
|
+
return;
|
|
864
1184
|
if (tool.stepIndex && tool.stepIndex > 0) {
|
|
865
1185
|
activeTurn.stepIndex = tool.stepIndex;
|
|
866
1186
|
}
|
|
867
1187
|
const icon = tool.status === 'running' ? '⚙️' : tool.status === 'failed' ? '❌' : '✅';
|
|
868
|
-
const summaryText = tool.summary ? ` · ${
|
|
869
|
-
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}`;
|
|
870
1190
|
if (tool.status === 'running') {
|
|
871
1191
|
activeTurn.toolCount += 1;
|
|
872
|
-
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;
|
|
873
1193
|
activeTurn.toolLines.push(line);
|
|
874
1194
|
}
|
|
875
1195
|
else {
|
|
876
1196
|
if (activeTurn.currentTool && activeTurn.currentTool.startsWith(tool.name)) {
|
|
877
1197
|
activeTurn.currentTool = null;
|
|
878
1198
|
}
|
|
879
|
-
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>`));
|
|
880
1200
|
if (lastIdx !== -1) {
|
|
881
1201
|
activeTurn.toolLines[lastIdx] = line;
|
|
882
1202
|
}
|
|
@@ -887,7 +1207,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
887
1207
|
scheduleFlush();
|
|
888
1208
|
});
|
|
889
1209
|
execution.on('conversation', (convId) => {
|
|
890
|
-
if (!convId)
|
|
1210
|
+
if (!convId || !ownsTurn() || activeTurn.settling)
|
|
891
1211
|
return;
|
|
892
1212
|
activeTurn.threadId = convId;
|
|
893
1213
|
this.store.setBinding(scopeId, convId, cwd);
|
|
@@ -897,55 +1217,176 @@ export class UnifiedChannelOrchestrator {
|
|
|
897
1217
|
threadId: convId,
|
|
898
1218
|
messageId: activeTurn.messageId,
|
|
899
1219
|
});
|
|
1220
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { request: { ...req, threadId: convId } });
|
|
900
1221
|
this.syncCurrentBackendSettings(scopeId);
|
|
901
1222
|
});
|
|
902
1223
|
const retryTurn = async (nextRetryCount) => {
|
|
903
|
-
await this.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (activeTurn.flushTimer)
|
|
908
|
-
clearTimeout(activeTurn.flushTimer);
|
|
909
|
-
if (activeTurn.typingTimer)
|
|
910
|
-
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());
|
|
911
1228
|
this.activeTurns.delete(scopeId);
|
|
912
1229
|
this.store.removeActiveTurnPreview(turnKey);
|
|
913
|
-
|
|
914
|
-
this.
|
|
915
|
-
this.syncCurrentBackendSettings(scopeId);
|
|
1230
|
+
try {
|
|
1231
|
+
await this.executeTurn(event, prompt, locale, nextRetryCount, stagedAttachments, { ...options, reuseMessageId: activeTurn.messageId || undefined, taskId: activeTurn.taskId });
|
|
916
1232
|
}
|
|
917
|
-
|
|
918
|
-
this.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
cachedTokens: res.usage.cachedTokens,
|
|
922
|
-
totalTokens: res.usage.totalTokens,
|
|
923
|
-
}, adapter.id);
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
if (!this.stopped && !this.activeTurns.has(scopeId))
|
|
1235
|
+
this.activeTurns.set(scopeId, activeTurn);
|
|
1236
|
+
throw error;
|
|
924
1237
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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)
|
|
941
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) });
|
|
942
1369
|
}
|
|
943
|
-
|
|
944
|
-
|
|
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);
|
|
945
1386
|
if (adapter.handleTurnError) {
|
|
946
1387
|
try {
|
|
947
1388
|
const handled = await adapter.handleTurnError({
|
|
948
|
-
error:
|
|
1389
|
+
error: err.message,
|
|
949
1390
|
request: req,
|
|
950
1391
|
retryCount,
|
|
951
1392
|
retryTurn,
|
|
@@ -956,10 +1397,10 @@ export class UnifiedChannelOrchestrator {
|
|
|
956
1397
|
return;
|
|
957
1398
|
}
|
|
958
1399
|
catch (adapterErr) {
|
|
959
|
-
this.logger.warn('orchestrator.
|
|
1400
|
+
this.logger.warn('orchestrator.adapter_handle_error_failed', { error: String(adapterErr) });
|
|
960
1401
|
}
|
|
961
1402
|
}
|
|
962
|
-
if (isTransientNetworkError(
|
|
1403
|
+
if (isTransientNetworkError(err.message) && retryCount < 3) {
|
|
963
1404
|
const backoffMs = Math.min(15000, 3000 * Math.pow(2, retryCount));
|
|
964
1405
|
const delaySec = Math.round(backoffMs / 1000);
|
|
965
1406
|
try {
|
|
@@ -968,7 +1409,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
968
1409
|
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
969
1410
|
}
|
|
970
1411
|
catch {
|
|
971
|
-
/* ignore
|
|
1412
|
+
/* ignore */
|
|
972
1413
|
}
|
|
973
1414
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
974
1415
|
try {
|
|
@@ -979,132 +1420,64 @@ export class UnifiedChannelOrchestrator {
|
|
|
979
1420
|
}
|
|
980
1421
|
return;
|
|
981
1422
|
}
|
|
982
|
-
|
|
983
|
-
let finalText = (res.response || '').trim();
|
|
984
|
-
if (!finalText && activeTurn.accumulatedText) {
|
|
985
|
-
finalText = activeTurn.accumulatedText.trim();
|
|
986
|
-
}
|
|
987
|
-
const isPureError = !finalText ||
|
|
988
|
-
finalText === errorText ||
|
|
989
|
-
finalText.startsWith('Process exited with code') ||
|
|
990
|
-
finalText.startsWith('Verification Required') ||
|
|
991
|
-
finalText.startsWith('Error:') ||
|
|
992
|
-
finalText.length <= 20;
|
|
993
|
-
if (!isPureError) {
|
|
1423
|
+
if (activeTurn.accumulatedText && activeTurn.accumulatedText.trim().length > 20) {
|
|
994
1424
|
const foldedTools = buildFoldedToolsSummary({
|
|
995
1425
|
stepIndex: activeTurn.stepIndex,
|
|
996
1426
|
toolLines: activeTurn.toolLines,
|
|
997
1427
|
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
998
1428
|
startTime: activeTurn.startTime,
|
|
999
|
-
usage: res.usage,
|
|
1000
1429
|
locale,
|
|
1001
1430
|
});
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
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);
|
|
1006
1434
|
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1007
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1008
1435
|
return;
|
|
1009
1436
|
}
|
|
1010
|
-
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌
|
|
1011
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1012
|
-
return;
|
|
1437
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **执行异常**:\n\`\`\`\n${err.message}\n\`\`\``);
|
|
1013
1438
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
catch (fatalErr) {
|
|
1018
|
-
this.logger.error('orchestrator.turn_result_unhandled', { error: String(fatalErr) });
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
|
-
execution.on('error', async (err) => {
|
|
1022
|
-
try {
|
|
1023
|
-
if (activeTurn.flushTimer)
|
|
1024
|
-
clearTimeout(activeTurn.flushTimer);
|
|
1025
|
-
if (activeTurn.typingTimer)
|
|
1026
|
-
clearInterval(activeTurn.typingTimer);
|
|
1027
|
-
this.activeTurns.delete(scopeId);
|
|
1028
|
-
this.store.removeActiveTurnPreview(turnKey);
|
|
1029
|
-
if (adapter.handleTurnError) {
|
|
1030
|
-
try {
|
|
1031
|
-
const handled = await adapter.handleTurnError({
|
|
1032
|
-
error: err.message,
|
|
1033
|
-
request: req,
|
|
1034
|
-
retryCount,
|
|
1035
|
-
retryTurn,
|
|
1036
|
-
sendMessage: (text) => this.sendMessage(scopeId, text),
|
|
1037
|
-
editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
|
|
1038
|
-
});
|
|
1039
|
-
if (handled)
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
catch (adapterErr) {
|
|
1043
|
-
this.logger.warn('orchestrator.adapter_handle_error_failed', { error: String(adapterErr) });
|
|
1044
|
-
}
|
|
1439
|
+
catch (fatalErr) {
|
|
1440
|
+
this.logger.error('orchestrator.turn_error_unhandled', { error: String(fatalErr) });
|
|
1045
1441
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const delaySec = Math.round(backoffMs / 1000);
|
|
1049
|
-
try {
|
|
1050
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
1051
|
-
? `🌐 **网络波动重试 (第 ${retryCount + 1}/3 次)**: 正在等待网络稳定 (${delaySec}s) 后自动恢复任务…`
|
|
1052
|
-
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
1053
|
-
}
|
|
1054
|
-
catch {
|
|
1055
|
-
/* ignore */
|
|
1056
|
-
}
|
|
1057
|
-
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1058
|
-
try {
|
|
1059
|
-
await retryTurn(retryCount + 1);
|
|
1060
|
-
}
|
|
1061
|
-
catch (retryErr) {
|
|
1062
|
-
this.logger.error('orchestrator.retry_turn_failed', { error: String(retryErr) });
|
|
1063
|
-
}
|
|
1064
|
-
return;
|
|
1442
|
+
finally {
|
|
1443
|
+
await finishTurn('ERROR', err.message).catch(error => this.logger.warn('orchestrator.finish_failed', { error: String(error) }));
|
|
1065
1444
|
}
|
|
1066
|
-
|
|
1067
|
-
const foldedTools = buildFoldedToolsSummary({
|
|
1068
|
-
stepIndex: activeTurn.stepIndex,
|
|
1069
|
-
toolLines: activeTurn.toolLines,
|
|
1070
|
-
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
1071
|
-
startTime: activeTurn.startTime,
|
|
1072
|
-
locale,
|
|
1073
|
-
});
|
|
1074
|
-
const fullAnswer = activeTurn.accumulatedText.trim() +
|
|
1075
|
-
`\n\n⚠️ <i>(注意:任务执行中途异常中断: ${escapeTelegramHtml(err.message.slice(0, 120))})</i>`;
|
|
1076
|
-
const chunks = combineSummaryAndResponse(foldedTools, fullAnswer);
|
|
1077
|
-
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1078
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **执行异常**:\n\`\`\`\n${err.message}\n\`\`\``);
|
|
1082
|
-
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1083
|
-
}
|
|
1084
|
-
catch (fatalErr) {
|
|
1085
|
-
this.logger.error('orchestrator.turn_error_unhandled', { error: String(fatalErr) });
|
|
1086
|
-
}
|
|
1445
|
+
});
|
|
1087
1446
|
});
|
|
1088
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
|
+
}
|
|
1089
1455
|
async handleEffortCommand(scopeId, args, locale) {
|
|
1090
|
-
const
|
|
1091
|
-
|
|
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
|
+
}
|
|
1092
1464
|
const settings = this.store.getChatSettings(scopeId);
|
|
1093
|
-
if (
|
|
1465
|
+
if (effort !== 'high' && settings?.serviceTier === 'boost')
|
|
1094
1466
|
this.store.setChatServiceTier(scopeId, null);
|
|
1095
|
-
|
|
1096
|
-
this.store.setChatEffort(scopeId, target);
|
|
1467
|
+
this.store.setChatEffort(scopeId, effort);
|
|
1097
1468
|
this.syncCurrentBackendSettings(scopeId);
|
|
1098
|
-
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${
|
|
1469
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${effort ?? 'default'}\`` : `✅ Reasoning effort set to: \`${effort ?? 'default'}\``);
|
|
1099
1470
|
}
|
|
1100
1471
|
else {
|
|
1101
|
-
const current = this.store.getChatSettings(scopeId)?.reasoningEffort ?? '
|
|
1102
|
-
await this.sendMessage(scopeId, locale === 'zh'
|
|
1103
|
-
? `• **当前思考深度**: \`${current}\`\n\n用法: \`/effort <low|medium|high>\``
|
|
1104
|
-
: `• **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(' ')}`);
|
|
1105
1474
|
}
|
|
1106
1475
|
}
|
|
1107
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
|
+
}
|
|
1108
1481
|
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
1109
1482
|
const target = args.trim().toLowerCase();
|
|
1110
1483
|
let nextBoost = !isBoost;
|
|
@@ -1143,6 +1516,15 @@ export class UnifiedChannelOrchestrator {
|
|
|
1143
1516
|
}
|
|
1144
1517
|
}
|
|
1145
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) {
|
|
1146
1528
|
const scopeId = event.scopeId;
|
|
1147
1529
|
if (!args.trim()) {
|
|
1148
1530
|
const count = this.store.countQueuedTurnInputs(scopeId);
|
|
@@ -1152,15 +1534,33 @@ export class UnifiedChannelOrchestrator {
|
|
|
1152
1534
|
return;
|
|
1153
1535
|
}
|
|
1154
1536
|
if (args.trim().toLowerCase() === 'clear') {
|
|
1155
|
-
const cleared = this.
|
|
1537
|
+
const cleared = this.cancelQueuedWork(scopeId);
|
|
1156
1538
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
1157
1539
|
? `🗑️ 已清空当前会话的 ${cleared} 条排队任务。`
|
|
1158
1540
|
: `🗑️ Cleared ${cleared} queued tasks.`);
|
|
1159
1541
|
return;
|
|
1160
1542
|
}
|
|
1161
|
-
|
|
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
|
+
}
|
|
1162
1556
|
}
|
|
1163
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) {
|
|
1164
1564
|
const scopeId = event.scopeId;
|
|
1165
1565
|
if (!args.trim()) {
|
|
1166
1566
|
await this.sendMessage(scopeId, locale === 'zh' ? '用法: `/steer <消息>` (立即中断当前思考,带入新指令)' : 'Usage: `/steer <message>`');
|
|
@@ -1168,28 +1568,23 @@ export class UnifiedChannelOrchestrator {
|
|
|
1168
1568
|
}
|
|
1169
1569
|
const active = this.activeTurns.get(scopeId);
|
|
1170
1570
|
if (active) {
|
|
1171
|
-
|
|
1172
|
-
if (active.typingTimer)
|
|
1173
|
-
clearInterval(active.typingTimer);
|
|
1174
|
-
if (active.flushTimer)
|
|
1175
|
-
clearTimeout(active.flushTimer);
|
|
1176
|
-
this.activeTurns.delete(scopeId);
|
|
1571
|
+
await this.cancelActiveTurn(scopeId);
|
|
1177
1572
|
await this.sendMessage(scopeId, locale === 'zh'
|
|
1178
1573
|
? `⚡ 已中断前置任务,立即开始插话指令:\n> ${args.trim()}`
|
|
1179
1574
|
: `⚡ Interrupted active turn, executing steer instruction:\n> ${args.trim()}`);
|
|
1180
1575
|
}
|
|
1181
|
-
await this.
|
|
1576
|
+
await this.startPromptTurnNow(event, args.trim(), locale, { taskId });
|
|
1182
1577
|
}
|
|
1183
1578
|
async handleInterrupt(scopeId, locale) {
|
|
1579
|
+
return this.scopeOperations.run(scopeId, () => this.handleInterruptNow(scopeId, locale));
|
|
1580
|
+
}
|
|
1581
|
+
async handleInterruptNow(scopeId, locale) {
|
|
1184
1582
|
const active = this.activeTurns.get(scopeId);
|
|
1185
1583
|
const queuedCount = this.store.countQueuedTurnInputs(scopeId);
|
|
1186
1584
|
if (active) {
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
|
|
1190
|
-
if (active.typingTimer)
|
|
1191
|
-
clearInterval(active.typingTimer);
|
|
1192
|
-
this.activeTurns.delete(scopeId);
|
|
1585
|
+
await this.cancelActiveTurn(scopeId);
|
|
1586
|
+
if (this.messaging.beginTaskPreview)
|
|
1587
|
+
return;
|
|
1193
1588
|
let extraMsg = '';
|
|
1194
1589
|
if (queuedCount > 0) {
|
|
1195
1590
|
extraMsg = locale === 'zh'
|
|
@@ -1199,14 +1594,21 @@ export class UnifiedChannelOrchestrator {
|
|
|
1199
1594
|
await this.sendMessage(scopeId, (locale === 'zh' ? '🛑 已发送中断请求。' : '🛑 Interrupt request sent.') + extraMsg);
|
|
1200
1595
|
}
|
|
1201
1596
|
else if (queuedCount > 0) {
|
|
1202
|
-
const cleared = this.
|
|
1597
|
+
const cleared = this.cancelQueuedWork(scopeId);
|
|
1203
1598
|
await this.sendMessage(scopeId, locale === 'zh' ? `🛑 当前无运行中任务,已清空 ${cleared} 条排队任务。` : `🛑 Cleared ${cleared} queued tasks.`);
|
|
1204
1599
|
}
|
|
1205
1600
|
else {
|
|
1206
1601
|
await this.sendMessage(scopeId, locale === 'zh' ? 'ℹ️ 当前没有正在执行或排队的任务。' : 'ℹ️ No active or queued tasks to interrupt.');
|
|
1207
1602
|
}
|
|
1208
1603
|
}
|
|
1209
|
-
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
|
+
}
|
|
1210
1612
|
const rawTarget = targetCwdInput?.trim();
|
|
1211
1613
|
const binding = this.store.getBinding(scopeId);
|
|
1212
1614
|
let cwd = binding?.cwd || this.config.defaultCwd;
|
|
@@ -1235,16 +1637,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1235
1637
|
cwd = resolved;
|
|
1236
1638
|
}
|
|
1237
1639
|
// Cancel queued turn inputs
|
|
1238
|
-
this.
|
|
1239
|
-
|
|
1240
|
-
if (active) {
|
|
1241
|
-
active.execution.cancel();
|
|
1242
|
-
if (active.typingTimer)
|
|
1243
|
-
clearInterval(active.typingTimer);
|
|
1244
|
-
if (active.flushTimer)
|
|
1245
|
-
clearTimeout(active.flushTimer);
|
|
1246
|
-
this.activeTurns.delete(scopeId);
|
|
1247
|
-
}
|
|
1640
|
+
this.cancelQueuedWork(scopeId);
|
|
1641
|
+
await this.cancelActiveTurn(scopeId);
|
|
1248
1642
|
// Reset thread binding to empty/ready state with the target cwd
|
|
1249
1643
|
this.store.setBinding(scopeId, '', cwd);
|
|
1250
1644
|
this.syncCurrentBackendSettings(scopeId);
|
|
@@ -1259,17 +1653,21 @@ export class UnifiedChannelOrchestrator {
|
|
|
1259
1653
|
const isBusy = this.activeTurns.has(scopeId);
|
|
1260
1654
|
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1261
1655
|
const adapter = backendDesc.adapter;
|
|
1262
|
-
const
|
|
1656
|
+
const ui = this.backends.ui(backendDesc.id) ?? this.customUi;
|
|
1657
|
+
const customStatus = await ui?.renderCustomStatus?.(scopeId, locale);
|
|
1263
1658
|
const totalUsage = this.store.getCumulativeTokenUsage();
|
|
1264
1659
|
const tokenLine = formatTokenUsageSummary(totalUsage, locale);
|
|
1265
1660
|
const allUsages = this.store.getAllBackendTokenUsages();
|
|
1266
1661
|
const breakdown = formatBackendTokenUsageBreakdown(allUsages);
|
|
1662
|
+
const usageText = backendDesc.defaults?.tokenUsage === false
|
|
1663
|
+
? (locale === 'zh' ? '• **Token 消耗**: 当前后端未提供逐轮统计\n' : '• **Token usage**: Per-turn counts are not exposed by this backend\n')
|
|
1664
|
+
: `${tokenLine}${breakdown}\n`;
|
|
1267
1665
|
const text = locale === 'zh'
|
|
1268
1666
|
? `📊 **${adapter.name} 运行状态**\n\n` +
|
|
1269
1667
|
`• **当前引擎**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1270
1668
|
`• **状态**: ${isBusy ? '⚡ 正在执行任务' : '💤 空闲'}\n` +
|
|
1271
1669
|
`• **当前模型**: \`${settings?.model || '默认'}\`\n` +
|
|
1272
|
-
|
|
1670
|
+
usageText +
|
|
1273
1671
|
`• **绑定会话**: \`${binding?.threadId || '(新会话)'}\`\n` +
|
|
1274
1672
|
`• **工作目录**: \`${binding?.cwd || this.config.defaultCwd}\`` +
|
|
1275
1673
|
(customStatus ? `\n${customStatus}` : '')
|
|
@@ -1277,7 +1675,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
1277
1675
|
`• **Engine**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1278
1676
|
`• **State**: ${isBusy ? '⚡ Executing' : '💤 Idle'}\n` +
|
|
1279
1677
|
`• **Model**: \`${settings?.model || 'default'}\`\n` +
|
|
1280
|
-
|
|
1678
|
+
usageText +
|
|
1281
1679
|
`• **Thread**: \`${binding?.threadId || '(new)'}\`\n` +
|
|
1282
1680
|
`• **Directory**: \`${binding?.cwd || this.config.defaultCwd}\`` +
|
|
1283
1681
|
(customStatus ? `\n${customStatus}` : '');
|
|
@@ -1297,9 +1695,12 @@ export class UnifiedChannelOrchestrator {
|
|
|
1297
1695
|
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1298
1696
|
}
|
|
1299
1697
|
async sendSetupMenu(scopeId, locale, editMessageId) {
|
|
1698
|
+
const ui = this.backends.ui(this.getBackendDescriptorForScope(scopeId).id) ?? this.customUi;
|
|
1699
|
+
if (await ui?.renderSetupMenu?.(scopeId, locale, editMessageId))
|
|
1700
|
+
return;
|
|
1300
1701
|
const settings = this.store.getChatSettings(scopeId);
|
|
1301
1702
|
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
1302
|
-
const isBoost = settings?.serviceTier === 'boost';
|
|
1703
|
+
const isBoost = settings?.serviceTier === 'boost' && this.getBackendDescriptorForScope(scopeId).defaults?.boost !== false;
|
|
1303
1704
|
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1304
1705
|
const adapter = backendDesc.adapter;
|
|
1305
1706
|
const allBackends = await this.listBackends();
|
|
@@ -1310,19 +1711,19 @@ export class UnifiedChannelOrchestrator {
|
|
|
1310
1711
|
`• **当前引擎**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1311
1712
|
`• **当前模型**: \`${settings?.model || '默认'}\`\n` +
|
|
1312
1713
|
`• **思考深度**: \`${currentEffort || '默认'}\`\n` +
|
|
1313
|
-
`• **Boost 增强**: ${isBoost ? '🚀 已开启' : '⚪ 已关闭'}\n` +
|
|
1714
|
+
(backendDesc.defaults?.boost !== false ? `• **Boost 增强**: ${isBoost ? '🚀 已开启' : '⚪ 已关闭'}\n` : '') +
|
|
1314
1715
|
`• **运行中消息**: ${mode === 'steer' ? '⚡ 插话 (立即中断接管)' : '⏳ 排队 (完成后自动执行)'}\n\n` +
|
|
1315
1716
|
`请选择要配置的项目:`
|
|
1316
1717
|
: `⚙️ **${adapter.name} Setup Panel**\n\n` +
|
|
1317
1718
|
`• **Engine**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1318
1719
|
`• **Model**: \`${settings?.model || 'default'}\`\n` +
|
|
1319
1720
|
`• **Effort**: \`${currentEffort || 'default'}\`\n` +
|
|
1320
|
-
`• **Boost**: ${isBoost ? '🚀 Enabled' : '⚪ Disabled'}\n` +
|
|
1721
|
+
(backendDesc.defaults?.boost !== false ? `• **Boost**: ${isBoost ? '🚀 Enabled' : '⚪ Disabled'}\n` : '') +
|
|
1321
1722
|
`• **Active-Turn**: ${mode === 'steer' ? '⚡ Steer' : '⏳ Queue'}\n\n` +
|
|
1322
1723
|
`Select setting to configure:`;
|
|
1323
1724
|
const keyboard = [];
|
|
1324
1725
|
// 1. Model choices (Codex style)
|
|
1325
|
-
const models = await adapter.listModels();
|
|
1726
|
+
const models = await adapter.listModels(scopeId);
|
|
1326
1727
|
const modelButtons = [
|
|
1327
1728
|
{
|
|
1328
1729
|
text: `${!currentModel || currentModel === 'default' ? '• ' : ''}${locale === 'zh' ? '默认模型' : 'Default'}`,
|
|
@@ -1341,13 +1742,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1341
1742
|
keyboard.push(modelButtons.slice(i, i + 2));
|
|
1342
1743
|
}
|
|
1343
1744
|
// 2. Reasoning effort choices (Codex style)
|
|
1344
|
-
const
|
|
1345
|
-
const
|
|
1346
|
-
const supportedEfforts = isAgy
|
|
1347
|
-
? ['low', 'medium', 'high']
|
|
1348
|
-
: isCodex
|
|
1349
|
-
? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1350
|
-
: ['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'];
|
|
1351
1747
|
const effortButtons = [
|
|
1352
1748
|
{
|
|
1353
1749
|
text: `${!currentEffort || currentEffort === 'default' ? '• ' : ''}${locale === 'zh' ? '默认深度' : 'Default'}`,
|
|
@@ -1363,18 +1759,18 @@ export class UnifiedChannelOrchestrator {
|
|
|
1363
1759
|
}
|
|
1364
1760
|
// 3. Boost & Active Mode row
|
|
1365
1761
|
keyboard.push([
|
|
1366
|
-
{
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
+
}] : []),
|
|
1370
1766
|
{
|
|
1371
1767
|
text: mode === 'steer' ? (locale === 'zh' ? '⚡ 插话模式' : '⚡ Steer') : (locale === 'zh' ? '⏳ 排队模式' : '⏳ Queue'),
|
|
1372
1768
|
callback_data: 'engine:setup:active_mode',
|
|
1373
1769
|
},
|
|
1374
1770
|
]);
|
|
1375
1771
|
// 4. Custom rows (Account & History)
|
|
1376
|
-
if (
|
|
1377
|
-
const customRows = await
|
|
1772
|
+
if (ui?.renderCustomSetupRows) {
|
|
1773
|
+
const customRows = await ui.renderCustomSetupRows(scopeId, locale);
|
|
1378
1774
|
keyboard.push(...customRows);
|
|
1379
1775
|
}
|
|
1380
1776
|
// 5. Session & Backend switcher row
|
|
@@ -1396,10 +1792,13 @@ export class UnifiedChannelOrchestrator {
|
|
|
1396
1792
|
}
|
|
1397
1793
|
}
|
|
1398
1794
|
async sendModelsMenu(scopeId, locale, editMessageId) {
|
|
1795
|
+
const ui = this.backends.ui(this.getBackendDescriptorForScope(scopeId).id) ?? this.customUi;
|
|
1796
|
+
if (await ui?.renderModelsMenu?.(scopeId, locale, editMessageId))
|
|
1797
|
+
return;
|
|
1399
1798
|
const settings = this.store.getChatSettings(scopeId);
|
|
1400
1799
|
const currentModel = settings?.model;
|
|
1401
1800
|
const adapter = this.getAdapterForScope(scopeId);
|
|
1402
|
-
const models = await adapter.listModels();
|
|
1801
|
+
const models = await adapter.listModels(scopeId);
|
|
1403
1802
|
const keyboard = [];
|
|
1404
1803
|
for (let i = 0; i < models.length; i += 2) {
|
|
1405
1804
|
const row = [];
|
|
@@ -1464,7 +1863,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1464
1863
|
}
|
|
1465
1864
|
async sendBackendMenu(scopeId, locale, editMessageId) {
|
|
1466
1865
|
const backends = await this.listBackends();
|
|
1467
|
-
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 };
|
|
1468
1868
|
const binding = this.store.getBinding(scopeId);
|
|
1469
1869
|
const lines = [
|
|
1470
1870
|
locale === 'zh' ? '🔌 **后端运行环境 (Backends & Engines)**' : '🔌 **Backends & Engines**',
|