@foxden-app/foxclaw 0.7.3 → 0.11.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 +22 -8
- package/CHANGELOG.md +134 -0
- package/README.md +21 -1
- package/README_EN.md +21 -1
- package/dist/antigravity/adapter.d.ts +18 -0
- package/dist/antigravity/adapter.js +394 -0
- package/dist/antigravity/auth.d.ts +126 -0
- package/dist/antigravity/auth.js +933 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +244 -0
- package/dist/antigravity/controller.d.ts +101 -0
- package/dist/antigravity/controller.js +2007 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +102 -0
- package/dist/antigravity/events.js +143 -0
- package/dist/antigravity/runtime.d.ts +49 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/antigravity/subagents.d.ts +66 -0
- package/dist/antigravity/subagents.js +459 -0
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
- package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
- package/dist/codex_app/adapter.d.ts +20 -0
- package/dist/codex_app/adapter.js +213 -0
- package/dist/codex_app/client.d.ts +1 -1
- package/dist/codex_app/client.js +12 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +65 -16
- package/dist/controller/controller.d.ts +12 -5
- package/dist/controller/controller.js +109 -8
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +77 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +106 -0
- package/dist/core/orchestrator.js +1556 -0
- package/dist/core/stream_preview.d.ts +29 -0
- package/dist/core/stream_preview.js +146 -0
- package/dist/core/turn_queue.d.ts +19 -0
- package/dist/core/turn_queue.js +41 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +49 -0
- package/dist/main.js +375 -446
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +169 -0
- package/dist/opencode/controller.js +2 -2
- package/dist/store/database.d.ts +44 -0
- package/dist/store/database.js +275 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.d.ts +1 -0
- package/dist/telegram/api.js +80 -3
- package/dist/telegram/bot_home.js +5 -4
- package/dist/telegram/gateway.d.ts +4 -0
- package/dist/telegram/gateway.js +11 -0
- package/dist/types.d.ts +3 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +95 -7
- package/dist/voice/tts.js +4 -2
- package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
- package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { parseCommand } from '../controller/commands.js';
|
|
5
|
+
import { getTelegramCommands, getAntigravityTelegramCommands } from '../i18n.js';
|
|
6
|
+
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
7
|
+
import { chunkTelegramMessage } from '../telegram/text.js';
|
|
8
|
+
import { stageInboundAttachments } from './attachments.js';
|
|
9
|
+
import { buildAttachmentPrompt } from '../telegram/media.js';
|
|
10
|
+
import { TurnQueueManager } from './turn_queue.js';
|
|
11
|
+
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
|
+
import { formatTokenUsageSummary, formatBackendTokenUsageBreakdown } from '../store/token_usage.js';
|
|
15
|
+
import { isTransientNetworkError } from '../telegram/api.js';
|
|
16
|
+
export const STREAM_THROTTLE_MS = 700;
|
|
17
|
+
export const TYPING_INTERVAL_MS = 4000;
|
|
18
|
+
export class UnifiedChannelOrchestrator {
|
|
19
|
+
config;
|
|
20
|
+
store;
|
|
21
|
+
logger;
|
|
22
|
+
bot;
|
|
23
|
+
messaging;
|
|
24
|
+
queueManager;
|
|
25
|
+
customUi;
|
|
26
|
+
backends = new Map();
|
|
27
|
+
defaultBackendId;
|
|
28
|
+
activeTurns = new Map();
|
|
29
|
+
stalePanelDeleteTimers = new Map();
|
|
30
|
+
backendProvider;
|
|
31
|
+
get adapter() {
|
|
32
|
+
return this.getAdapterForBackend(this.defaultBackendId);
|
|
33
|
+
}
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.config = options.config;
|
|
36
|
+
this.store = options.store;
|
|
37
|
+
this.logger = options.logger;
|
|
38
|
+
this.bot = options.bot;
|
|
39
|
+
this.messaging = options.messaging;
|
|
40
|
+
this.queueManager = new TurnQueueManager();
|
|
41
|
+
this.customUi = options.customUi;
|
|
42
|
+
this.backendProvider = options.backendProvider;
|
|
43
|
+
if (options.backends) {
|
|
44
|
+
for (const b of options.backends) {
|
|
45
|
+
this.backends.set(b.id, b);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (options.adapters) {
|
|
49
|
+
if (options.adapters instanceof Map) {
|
|
50
|
+
for (const [id, ad] of options.adapters) {
|
|
51
|
+
if (!this.backends.has(id)) {
|
|
52
|
+
this.backends.set(id, { id, name: ad.name, engineType: id, adapter: ad });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (Array.isArray(options.adapters)) {
|
|
57
|
+
for (const ad of options.adapters) {
|
|
58
|
+
if (!this.backends.has(ad.id)) {
|
|
59
|
+
this.backends.set(ad.id, { id: ad.id, name: ad.name, engineType: ad.id, adapter: ad });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (options.adapter && !this.backends.has(options.adapter.id)) {
|
|
65
|
+
this.backends.set(options.adapter.id, {
|
|
66
|
+
id: options.adapter.id,
|
|
67
|
+
name: options.adapter.name,
|
|
68
|
+
engineType: options.adapter.id,
|
|
69
|
+
adapter: options.adapter,
|
|
70
|
+
isDefault: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const firstId = Array.from(this.backends.keys())[0] ?? 'default';
|
|
74
|
+
this.defaultBackendId = options.defaultBackendId ?? options.adapter?.id ?? firstId;
|
|
75
|
+
}
|
|
76
|
+
getAdapterForScope(scopeId) {
|
|
77
|
+
const activeBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
78
|
+
return this.getAdapterForBackend(activeBackendId);
|
|
79
|
+
}
|
|
80
|
+
getBackendDescriptorForScope(scopeId) {
|
|
81
|
+
const activeBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
82
|
+
return (this.backends.get(activeBackendId) ??
|
|
83
|
+
this.backends.get(this.defaultBackendId) ?? {
|
|
84
|
+
id: activeBackendId,
|
|
85
|
+
name: activeBackendId,
|
|
86
|
+
engineType: activeBackendId,
|
|
87
|
+
adapter: this.adapter,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
getAdapterForBackend(backendId) {
|
|
91
|
+
const desc = this.backends.get(backendId);
|
|
92
|
+
if (desc)
|
|
93
|
+
return desc.adapter;
|
|
94
|
+
const fallback = this.backends.get(this.defaultBackendId) ?? Array.from(this.backends.values())[0];
|
|
95
|
+
if (!fallback) {
|
|
96
|
+
throw new Error(`No engine adapter available in orchestrator for backend '${backendId}'`);
|
|
97
|
+
}
|
|
98
|
+
return fallback.adapter;
|
|
99
|
+
}
|
|
100
|
+
registerBackend(desc) {
|
|
101
|
+
this.backends.set(desc.id, desc);
|
|
102
|
+
}
|
|
103
|
+
async listBackends() {
|
|
104
|
+
const list = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
if (this.backendProvider) {
|
|
107
|
+
try {
|
|
108
|
+
const dynamicList = await this.backendProvider();
|
|
109
|
+
for (const b of dynamicList) {
|
|
110
|
+
list.push(b);
|
|
111
|
+
seen.add(b.id);
|
|
112
|
+
if (!this.backends.has(b.id)) {
|
|
113
|
+
this.backends.set(b.id, b);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
this.logger.warn('orchestrator.backend_provider_failed', { error: String(err) });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const b of this.backends.values()) {
|
|
122
|
+
if (!seen.has(b.id)) {
|
|
123
|
+
list.push(b);
|
|
124
|
+
seen.add(b.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return list;
|
|
128
|
+
}
|
|
129
|
+
async resolveBackendDescriptor(backendId) {
|
|
130
|
+
const cached = this.backends.get(backendId);
|
|
131
|
+
if (cached)
|
|
132
|
+
return cached;
|
|
133
|
+
const all = await this.listBackends();
|
|
134
|
+
return all.find((b) => b.id === backendId) ?? null;
|
|
135
|
+
}
|
|
136
|
+
syncCurrentBackendSettings(scopeId) {
|
|
137
|
+
const activeBackend = this.getBackendDescriptorForScope(scopeId);
|
|
138
|
+
const binding = this.store.getBinding(scopeId);
|
|
139
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
140
|
+
this.store.setScopeBackendBinding(scopeId, activeBackend.id, binding?.threadId ?? '', binding?.cwd ?? null, {
|
|
141
|
+
model: settings?.model ?? null,
|
|
142
|
+
reasoningEffort: settings?.reasoningEffort ?? null,
|
|
143
|
+
activeTurnMessageMode: settings?.activeTurnMessageMode ?? null,
|
|
144
|
+
serviceTier: settings?.serviceTier ?? null,
|
|
145
|
+
accessPreset: settings?.accessPreset ?? null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
scheduleStalePanelDeletion(scopeId, messageId) {
|
|
149
|
+
if (this.config.telegramPanelTtlMs <= 0 || scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const key = `${scopeId}:${messageId}`;
|
|
153
|
+
const existing = this.stalePanelDeleteTimers.get(key);
|
|
154
|
+
if (existing) {
|
|
155
|
+
clearTimeout(existing);
|
|
156
|
+
}
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
this.stalePanelDeleteTimers.delete(key);
|
|
159
|
+
if (typeof this.messaging?.deleteMessage === 'function') {
|
|
160
|
+
void this.messaging.deleteMessage(scopeId, messageId).catch((error) => {
|
|
161
|
+
this.logger.debug('telegram.stale_panel_delete_failed', { scopeId, messageId, error: String(error) });
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}, this.config.telegramPanelTtlMs);
|
|
165
|
+
timer.unref();
|
|
166
|
+
this.stalePanelDeleteTimers.set(key, timer);
|
|
167
|
+
}
|
|
168
|
+
async switchBackend(scopeId, targetBackendId, locale) {
|
|
169
|
+
const currentBackendId = this.store.getActiveBackend(scopeId) || this.defaultBackendId;
|
|
170
|
+
const targetDesc = await this.resolveBackendDescriptor(targetBackendId);
|
|
171
|
+
if (!targetDesc) {
|
|
172
|
+
throw new Error(locale === 'zh'
|
|
173
|
+
? `未找到目标后端: ${targetBackendId}`
|
|
174
|
+
: `Target backend not found: ${targetBackendId}`);
|
|
175
|
+
}
|
|
176
|
+
if (targetDesc.onSelect) {
|
|
177
|
+
await targetDesc.onSelect(scopeId);
|
|
178
|
+
}
|
|
179
|
+
if (!this.backends.has(targetDesc.id)) {
|
|
180
|
+
this.backends.set(targetDesc.id, targetDesc);
|
|
181
|
+
}
|
|
182
|
+
if (currentBackendId === targetBackendId) {
|
|
183
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
184
|
+
return {
|
|
185
|
+
previousBackendId: currentBackendId,
|
|
186
|
+
newBackendId: targetBackendId,
|
|
187
|
+
restoredThreadId: currentBinding?.threadId ?? null,
|
|
188
|
+
cwd: currentBinding?.cwd ?? null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const currentBinding = this.store.getBinding(scopeId);
|
|
192
|
+
const currentSettings = this.store.getChatSettings(scopeId);
|
|
193
|
+
this.store.setScopeBackendBinding(scopeId, currentBackendId, currentBinding?.threadId ?? '', currentBinding?.cwd ?? null, {
|
|
194
|
+
model: currentSettings?.model ?? null,
|
|
195
|
+
reasoningEffort: currentSettings?.reasoningEffort ?? null,
|
|
196
|
+
activeTurnMessageMode: currentSettings?.activeTurnMessageMode ?? null,
|
|
197
|
+
serviceTier: currentSettings?.serviceTier ?? null,
|
|
198
|
+
accessPreset: currentSettings?.accessPreset ?? null,
|
|
199
|
+
});
|
|
200
|
+
this.store.setActiveBackend(scopeId, targetBackendId);
|
|
201
|
+
const saved = this.store.getScopeBackendBinding(scopeId, targetBackendId);
|
|
202
|
+
if (saved) {
|
|
203
|
+
if (saved.threadId) {
|
|
204
|
+
this.store.setBinding(scopeId, saved.threadId, saved.cwd ?? this.config.defaultCwd);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.store.clearBinding(scopeId);
|
|
208
|
+
}
|
|
209
|
+
this.store.setChatSettings(scopeId, saved.model ?? null, saved.reasoningEffort ?? null);
|
|
210
|
+
if (saved.activeTurnMessageMode !== undefined && saved.activeTurnMessageMode !== null) {
|
|
211
|
+
this.store.setChatActiveTurnMessageMode(scopeId, saved.activeTurnMessageMode ?? null);
|
|
212
|
+
}
|
|
213
|
+
if (saved.serviceTier !== undefined) {
|
|
214
|
+
this.store.setChatServiceTier(scopeId, saved.serviceTier ?? null);
|
|
215
|
+
}
|
|
216
|
+
if (saved.accessPreset !== undefined) {
|
|
217
|
+
this.store.setChatAccessPreset(scopeId, saved.accessPreset ?? null);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
this.store.clearBinding(scopeId);
|
|
222
|
+
const defaultEffort = targetDesc.engineType === 'antigravity' ? 'high' : 'medium';
|
|
223
|
+
this.store.setChatSettings(scopeId, null, defaultEffort);
|
|
224
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
225
|
+
}
|
|
226
|
+
const isCodex = targetDesc.engineType === 'codex';
|
|
227
|
+
const activeSettings = this.store.getChatSettings(scopeId);
|
|
228
|
+
const modelText = activeSettings?.model ? `\`${activeSettings.model}\`` : (locale === 'zh' ? '引擎默认' : 'default');
|
|
229
|
+
const effortText = activeSettings?.reasoningEffort ?? (isCodex ? 'medium' : 'high');
|
|
230
|
+
const modeText = activeSettings?.activeTurnMessageMode ?? 'queue';
|
|
231
|
+
const switchMsg = locale === 'zh'
|
|
232
|
+
? `🔄 **已切换至后端: ${targetDesc.name}** (\`${targetDesc.id}\`)\n\n` +
|
|
233
|
+
`• **引擎类型**: ${isCodex ? 'OpenAI Codex (App Server)' : 'Google Antigravity (AGY)'}\n` +
|
|
234
|
+
`• **绑定账号**: \`${targetDesc.account || '默认账号'}\`${targetDesc.details ? ` (${targetDesc.details})` : ''}\n` +
|
|
235
|
+
`• **会话状态**: ${saved?.threadId ? `已恢复历史会话 (\`${saved.threadId.slice(0, 16)}…\`)\n• **工作目录**: \`${saved.cwd ?? this.config.defaultCwd}\`` : '新会话就绪 (发送消息将开启新会话)'}\n` +
|
|
236
|
+
`• **记忆配置**: 模型 ${modelText} | 思考深度 \`${effortText}\` | 插话模式 \`${modeText}\`\n` +
|
|
237
|
+
`• **专属指令**: \`/models\` (${isCodex ? 'Codex模型' : 'Gemini模型'}), \`/auth\` (${isCodex ? 'OpenAI账号池' : 'Google授权'}), \`/threads\` (${isCodex ? 'Codex历史' : 'AGY历史'})\n\n` +
|
|
238
|
+
`⚡ **当前 Telegram Bot 已完全切换为 ${isCodex ? 'OpenAI Codex' : 'Google Antigravity'} 交互人格与执行引擎!**`
|
|
239
|
+
: `🔄 **Switched to Backend: ${targetDesc.name}** (\`${targetDesc.id}\`)\n\n` +
|
|
240
|
+
`• **Engine**: ${isCodex ? 'OpenAI Codex (App Server)' : 'Google Antigravity (AGY)'}\n` +
|
|
241
|
+
`• **Account**: \`${targetDesc.account || 'default'}\`${targetDesc.details ? ` (${targetDesc.details})` : ''}\n` +
|
|
242
|
+
`• **Thread**: ${saved?.threadId ? `Restored (\`${saved.threadId.slice(0, 16)}…\`)\n• **Directory**: \`${saved.cwd ?? this.config.defaultCwd}\`` : 'Ready (next prompt starts new thread)'}\n` +
|
|
243
|
+
`• **Restored Settings**: Model ${modelText} | Effort \`${effortText}\` | Message Mode \`${modeText}\`\n\n` +
|
|
244
|
+
`⚡ **Telegram Bot is now fully operated by ${isCodex ? 'OpenAI Codex' : 'Google Antigravity'}!**`;
|
|
245
|
+
try {
|
|
246
|
+
await this.sendMessage(scopeId, switchMsg);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
this.logger.warn('orchestrator.switch_announce_failed', { error: String(err) });
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
if (scopeId.startsWith(BRIDGE_SCOPE_TELEGRAM_PREFIX)) {
|
|
253
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
254
|
+
const cmds = isCodex ? getTelegramCommands(locale) : getAntigravityTelegramCommands(locale);
|
|
255
|
+
await this.bot.setChatCommands(target.chatId, cmds);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
this.logger.warn('orchestrator.set_chat_commands_failed', { error: String(err) });
|
|
260
|
+
}
|
|
261
|
+
this.logger.info('orchestrator.backend_switched', {
|
|
262
|
+
scopeId,
|
|
263
|
+
from: currentBackendId,
|
|
264
|
+
to: targetBackendId,
|
|
265
|
+
restoredThreadId: saved?.threadId ?? null,
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
previousBackendId: currentBackendId,
|
|
269
|
+
newBackendId: targetBackendId,
|
|
270
|
+
restoredThreadId: saved?.threadId ?? null,
|
|
271
|
+
cwd: saved?.cwd ?? null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
registerInboundHandlers() {
|
|
275
|
+
this.bot.on('text', (event) => {
|
|
276
|
+
this.handleText(event).catch((err) => {
|
|
277
|
+
this.logger.error('orchestrator.inbound_text_error', {
|
|
278
|
+
error: err instanceof Error ? err.message : String(err),
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
this.bot.on('callback', (event) => {
|
|
283
|
+
this.handleCallback(event).catch((err) => {
|
|
284
|
+
this.logger.error('orchestrator.inbound_callback_error', {
|
|
285
|
+
error: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
dispatchInboundLikeTelegramText(event) {
|
|
291
|
+
this.handleText(event).catch((err) => {
|
|
292
|
+
this.logger.error('orchestrator.inbound_text_error', {
|
|
293
|
+
error: err instanceof Error ? err.message : String(err),
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async start() {
|
|
298
|
+
await this.bot.start();
|
|
299
|
+
this.logger.info('orchestrator.started', {
|
|
300
|
+
defaultEngine: this.adapter.id,
|
|
301
|
+
backends: Array.from(this.backends.keys()),
|
|
302
|
+
});
|
|
303
|
+
// Requeue any interrupted turns from previous crash/restart
|
|
304
|
+
try {
|
|
305
|
+
const requeued = this.store.requeueInterruptedQueuedTurnInputs();
|
|
306
|
+
if (requeued > 0) {
|
|
307
|
+
this.logger.info('orchestrator.requeued_interrupted_turns', { count: requeued });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
this.logger.warn('orchestrator.requeue_interrupted_error', { error: String(err) });
|
|
312
|
+
}
|
|
313
|
+
// Crash recovery: check interrupted preview messages from before restart
|
|
314
|
+
try {
|
|
315
|
+
const backendIds = new Set(this.backends.keys());
|
|
316
|
+
for (const desc of this.backends.values()) {
|
|
317
|
+
backendIds.add(desc.adapter.id);
|
|
318
|
+
}
|
|
319
|
+
const activePreviews = this.store.listActiveTurnPreviews().filter((p) => {
|
|
320
|
+
for (const bId of backendIds) {
|
|
321
|
+
if (p.turnId.startsWith(`${bId}_`))
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
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);
|
|
334
|
+
}
|
|
335
|
+
const locale = this.store.getChatSettings(prev.scopeId)?.locale || 'zh';
|
|
336
|
+
this.editMessage(prev.scopeId, prev.messageId, locale === 'zh'
|
|
337
|
+
? '🔄 **[Foxclaw 服务已重新加载/重启]**\n\n检测到上一轮任务执行被服务重启中断,正在自动继续执行…'
|
|
338
|
+
: '🔄 **[FoxClaw Service Reloaded / Restarted]**\n\nInterrupted turn detected. Resuming automatically…').catch(() => { });
|
|
339
|
+
const fakeEvent = {
|
|
340
|
+
scopeId: prev.scopeId,
|
|
341
|
+
chatId: prev.scopeId,
|
|
342
|
+
topicId: null,
|
|
343
|
+
chatType: 'private',
|
|
344
|
+
userId: 'system',
|
|
345
|
+
messageId: prev.messageId,
|
|
346
|
+
text: '继续',
|
|
347
|
+
attachments: [],
|
|
348
|
+
entities: [],
|
|
349
|
+
replyToBot: false,
|
|
350
|
+
};
|
|
351
|
+
const resumePrompt = prev.threadId
|
|
352
|
+
? (locale === 'zh' ? 'FoxClaw 服务重启中断,请在当前会话中从中断处继续完成刚才的工作。' : 'FoxClaw restarted while previous turn was running. Please resume and complete the work from where it left off.')
|
|
353
|
+
: (locale === 'zh' ? '请继续完成上一步未完成的任务。' : 'Please continue and complete the unfinished task from previous step.');
|
|
354
|
+
const resumeTimer = setTimeout(() => {
|
|
355
|
+
this.startPromptTurn(fakeEvent, resumePrompt, locale, {
|
|
356
|
+
reuseMessageId: prev.messageId,
|
|
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
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}, 1500);
|
|
365
|
+
resumeTimer?.unref?.();
|
|
366
|
+
}
|
|
367
|
+
// Check for queued turns on scopes without active previews
|
|
368
|
+
const queuedInputs = this.store.listQueuedTurnInputs();
|
|
369
|
+
for (const q of queuedInputs) {
|
|
370
|
+
if (!recoveredScopes.has(q.scopeId) && !this.activeTurns.has(q.scopeId)) {
|
|
371
|
+
recoveredScopes.add(q.scopeId);
|
|
372
|
+
const locale = this.store.getChatSettings(q.scopeId)?.locale || 'zh';
|
|
373
|
+
const fakeEvent = {
|
|
374
|
+
scopeId: q.scopeId,
|
|
375
|
+
chatId: q.chatId,
|
|
376
|
+
topicId: q.topicId ?? null,
|
|
377
|
+
chatType: q.chatType || 'private',
|
|
378
|
+
userId: 'system',
|
|
379
|
+
messageId: q.messageId ?? 0,
|
|
380
|
+
text: q.sourceSummary,
|
|
381
|
+
attachments: [],
|
|
382
|
+
entities: [],
|
|
383
|
+
replyToBot: false,
|
|
384
|
+
};
|
|
385
|
+
const drainTimer = setTimeout(() => {
|
|
386
|
+
this.drainNextQueuedTurn(fakeEvent, locale).catch((err) => {
|
|
387
|
+
this.logger.warn('orchestrator.auto_drain_failed', { scopeId: q.scopeId, error: String(err) });
|
|
388
|
+
});
|
|
389
|
+
}, 2000);
|
|
390
|
+
drainTimer?.unref?.();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
this.logger.warn('orchestrator.restore_active_turns_error', { error: String(err) });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async stop() {
|
|
399
|
+
this.bot.stop();
|
|
400
|
+
for (const [scopeId, turn] of this.activeTurns.entries()) {
|
|
401
|
+
if (turn.flushTimer)
|
|
402
|
+
clearTimeout(turn.flushTimer);
|
|
403
|
+
if (turn.typingTimer)
|
|
404
|
+
clearInterval(turn.typingTimer);
|
|
405
|
+
turn.execution.cancel();
|
|
406
|
+
this.activeTurns.delete(scopeId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
getActiveTurnsCount() {
|
|
410
|
+
return this.activeTurns.size;
|
|
411
|
+
}
|
|
412
|
+
hasActiveTurn(scopeId) {
|
|
413
|
+
return this.activeTurns.has(scopeId);
|
|
414
|
+
}
|
|
415
|
+
async sendMessage(scopeId, text, keyboard) {
|
|
416
|
+
const chunks = chunkTelegramMessage(text);
|
|
417
|
+
let lastMsgId = 0;
|
|
418
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
419
|
+
const isLast = i === chunks.length - 1;
|
|
420
|
+
const kb = isLast ? keyboard : undefined;
|
|
421
|
+
lastMsgId = await this.messaging.sendRichMarkdown(scopeId, chunks[i], kb);
|
|
422
|
+
}
|
|
423
|
+
return lastMsgId;
|
|
424
|
+
}
|
|
425
|
+
async editMessage(scopeId, messageId, text, keyboard) {
|
|
426
|
+
if (!messageId || messageId <= 0) {
|
|
427
|
+
await this.sendMessage(scopeId, text, keyboard);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard);
|
|
431
|
+
}
|
|
432
|
+
async safeDeliverChunks(scopeId, messageId, chunks) {
|
|
433
|
+
if (!chunks || chunks.length === 0)
|
|
434
|
+
return;
|
|
435
|
+
try {
|
|
436
|
+
if (messageId > 0) {
|
|
437
|
+
await this.editMessage(scopeId, messageId, chunks[0]).catch(async () => {
|
|
438
|
+
await this.sendMessage(scopeId, chunks[0]).catch(() => { });
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
await this.sendMessage(scopeId, chunks[0]).catch(() => { });
|
|
443
|
+
}
|
|
444
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
445
|
+
await this.sendMessage(scopeId, chunks[i]).catch(() => { });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
this.logger.warn('orchestrator.safe_deliver_chunks_failed', { error: String(err) });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async safeDeliverMessage(scopeId, messageId, text) {
|
|
453
|
+
const chunks = chunkTelegramMessage(text);
|
|
454
|
+
await this.safeDeliverChunks(scopeId, messageId, chunks);
|
|
455
|
+
}
|
|
456
|
+
async handleText(event) {
|
|
457
|
+
const scopeId = event.scopeId;
|
|
458
|
+
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
|
+
const parsedCommand = parseCommand(event.text);
|
|
467
|
+
const addressing = resolveTelegramAddressing({
|
|
468
|
+
text: event.text,
|
|
469
|
+
attachmentsCount: event.attachments?.length ?? 0,
|
|
470
|
+
entities: event.entities,
|
|
471
|
+
command: parsedCommand,
|
|
472
|
+
botUsername: this.bot.username,
|
|
473
|
+
isDefaultTopic,
|
|
474
|
+
replyToBot: event.replyToBot,
|
|
475
|
+
});
|
|
476
|
+
if (addressing.kind === 'ignore')
|
|
477
|
+
return;
|
|
478
|
+
if (this.customUi?.handleCustomInbound) {
|
|
479
|
+
const handled = await this.customUi.handleCustomInbound(event, locale);
|
|
480
|
+
if (handled)
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (addressing.kind === 'command') {
|
|
484
|
+
const commandName = addressing.command.name.toLowerCase();
|
|
485
|
+
const argsString = addressing.command.args.join(' ').trim();
|
|
486
|
+
if (this.customUi?.handleCustomCommand) {
|
|
487
|
+
const handled = await this.customUi.handleCustomCommand(scopeId, commandName, argsString, locale, event);
|
|
488
|
+
if (handled)
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
switch (commandName) {
|
|
492
|
+
case 'start':
|
|
493
|
+
case 'help':
|
|
494
|
+
await this.sendHelp(scopeId, locale);
|
|
495
|
+
return;
|
|
496
|
+
case 'status':
|
|
497
|
+
await this.sendStatus(scopeId, locale);
|
|
498
|
+
return;
|
|
499
|
+
case 'setup':
|
|
500
|
+
await this.sendSetupMenu(scopeId, locale);
|
|
501
|
+
return;
|
|
502
|
+
case 'models':
|
|
503
|
+
await this.sendModelsMenu(scopeId, locale);
|
|
504
|
+
return;
|
|
505
|
+
case 'model':
|
|
506
|
+
if (argsString) {
|
|
507
|
+
this.store.setChatModel(scopeId, argsString === 'default' ? null : argsString);
|
|
508
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
509
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 模型已切换为: \`${argsString}\`` : `✅ Model switched to: \`${argsString}\``);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
await this.sendModelsMenu(scopeId, locale);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
case 'backend':
|
|
516
|
+
case 'backends':
|
|
517
|
+
case 'engine':
|
|
518
|
+
case 'engines':
|
|
519
|
+
await this.handleBackendCommand(scopeId, argsString, locale);
|
|
520
|
+
return;
|
|
521
|
+
case 'effort':
|
|
522
|
+
await this.handleEffortCommand(scopeId, argsString, locale);
|
|
523
|
+
return;
|
|
524
|
+
case 'boost':
|
|
525
|
+
await this.handleBoostCommand(scopeId, argsString, locale);
|
|
526
|
+
return;
|
|
527
|
+
case 'active':
|
|
528
|
+
await this.handleActiveCommand(scopeId, argsString, locale);
|
|
529
|
+
return;
|
|
530
|
+
case 'queue':
|
|
531
|
+
await this.handleQueueCommand(event, argsString, locale);
|
|
532
|
+
return;
|
|
533
|
+
case 'steer':
|
|
534
|
+
await this.handleSteerCommand(event, argsString, locale);
|
|
535
|
+
return;
|
|
536
|
+
case 'interrupt':
|
|
537
|
+
case 'stop':
|
|
538
|
+
case 'abort':
|
|
539
|
+
await this.handleInterrupt(scopeId, locale);
|
|
540
|
+
return;
|
|
541
|
+
case 'clear':
|
|
542
|
+
case 'new':
|
|
543
|
+
await this.handleNewSession(scopeId, locale, argsString);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (addressing.kind === 'prompt') {
|
|
548
|
+
await this.startPromptTurn(event, addressing.text, locale);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async handleCallback(event) {
|
|
552
|
+
const scopeId = event.scopeId;
|
|
553
|
+
const locale = 'zh';
|
|
554
|
+
const data = event.data || '';
|
|
555
|
+
const messageId = event.messageId;
|
|
556
|
+
if (this.customUi?.handleCustomCallback) {
|
|
557
|
+
const handled = await this.customUi.handleCustomCallback(scopeId, data, locale, messageId, event);
|
|
558
|
+
if (handled)
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (data.startsWith('engine:backend:')) {
|
|
562
|
+
const targetId = data.slice('engine:backend:'.length);
|
|
563
|
+
try {
|
|
564
|
+
const targetDesc = await this.resolveBackendDescriptor(targetId);
|
|
565
|
+
if (targetDesc) {
|
|
566
|
+
await this.switchBackend(scopeId, targetId, locale);
|
|
567
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? `已切换到: ${targetDesc.name}` : `Switched to: ${targetDesc.name}`);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? '未找到对应后端服务' : 'Backend not found');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh' ? `切换失败: ${String(err)}` : `Switch failed: ${String(err)}`);
|
|
575
|
+
}
|
|
576
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (data.startsWith('engine:m:') || data.startsWith('setup:model:')) {
|
|
580
|
+
const rawModel = data.startsWith('engine:m:')
|
|
581
|
+
? data.slice('engine:m:'.length)
|
|
582
|
+
: decodeURIComponent(data.slice('setup:model:'.length));
|
|
583
|
+
const model = rawModel === 'default' ? null : rawModel;
|
|
584
|
+
this.store.setChatModel(scopeId, model);
|
|
585
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
586
|
+
await this.messaging.answerCallback(event.callbackQueryId, `Model: ${rawModel}`);
|
|
587
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (data.startsWith('engine:effort:') || data.startsWith('setup:effort:')) {
|
|
591
|
+
const rawEffort = data.startsWith('engine:effort:')
|
|
592
|
+
? data.slice('engine:effort:'.length)
|
|
593
|
+
: data.slice('setup:effort:'.length);
|
|
594
|
+
const targetEffort = rawEffort === 'default' ? null : rawEffort;
|
|
595
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
596
|
+
if (targetEffort !== 'high' && settings?.serviceTier === 'boost') {
|
|
597
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
598
|
+
}
|
|
599
|
+
this.store.setChatEffort(scopeId, targetEffort);
|
|
600
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
601
|
+
await this.messaging.answerCallback(event.callbackQueryId, `Effort: ${rawEffort}`);
|
|
602
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (data.startsWith('engine:setup:')) {
|
|
606
|
+
const sub = data.slice('engine:setup:'.length);
|
|
607
|
+
if (sub === 'main') {
|
|
608
|
+
await this.messaging.answerCallback(event.callbackQueryId, '');
|
|
609
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (sub === 'boost') {
|
|
613
|
+
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
614
|
+
const nextBoost = !isBoost;
|
|
615
|
+
this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
|
|
616
|
+
if (nextBoost) {
|
|
617
|
+
this.store.setChatEffort(scopeId, 'high');
|
|
618
|
+
}
|
|
619
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
620
|
+
await this.messaging.answerCallback(event.callbackQueryId, nextBoost ? (locale === 'zh' ? '🚀 Boost 模式已开启 (深度 High)' : '🚀 Boost Mode enabled (High)') : (locale === 'zh' ? '⚪ Boost 模式已关闭' : '⚪ Boost Mode disabled'));
|
|
621
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (sub === 'models') {
|
|
625
|
+
await this.messaging.answerCallback(event.callbackQueryId, '');
|
|
626
|
+
await this.sendModelsMenu(scopeId, locale, messageId);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (sub === 'active_mode') {
|
|
630
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
631
|
+
const current = settings?.activeTurnMessageMode ?? 'queue';
|
|
632
|
+
const next = current === 'steer' ? 'queue' : 'steer';
|
|
633
|
+
this.store.setChatActiveTurnMessageMode(scopeId, next);
|
|
634
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
635
|
+
await this.messaging.answerCallback(event.callbackQueryId, next === 'steer' ? '已切换为:⚡ 插话模式' : '已切换为:⏳ 排队模式');
|
|
636
|
+
await this.sendSetupMenu(scopeId, locale, messageId);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (sub === 'backend') {
|
|
640
|
+
await this.messaging.answerCallback(event.callbackQueryId, '');
|
|
641
|
+
await this.sendBackendMenu(scopeId, locale, messageId);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (sub === 'new') {
|
|
645
|
+
await this.messaging.answerCallback(event.callbackQueryId, '已创建新会话');
|
|
646
|
+
await this.handleNewSession(scopeId, locale);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async startPromptTurn(event, prompt, locale, options) {
|
|
652
|
+
const scopeId = event.scopeId;
|
|
653
|
+
const binding = this.store.getBinding(scopeId);
|
|
654
|
+
const cwd = binding?.cwd || this.config.defaultCwd;
|
|
655
|
+
const threadId = options?.forcedThreadId ?? binding?.threadId ?? 'default';
|
|
656
|
+
let effectivePrompt = prompt;
|
|
657
|
+
let stagedAttachments;
|
|
658
|
+
if (event.attachments && event.attachments.length > 0) {
|
|
659
|
+
const nonDocJson = event.attachments.filter((a) => !(a.kind === 'document' && (a.fileName?.endsWith('.json') || a.mimeType?.includes('json'))));
|
|
660
|
+
if (nonDocJson.length > 0) {
|
|
661
|
+
const staged = await stageInboundAttachments(this.messaging, cwd, threadId, nonDocJson, this.logger);
|
|
662
|
+
if (staged.length > 0) {
|
|
663
|
+
stagedAttachments = staged;
|
|
664
|
+
effectivePrompt = buildAttachmentPrompt(prompt, staged);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (this.activeTurns.has(scopeId)) {
|
|
669
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
670
|
+
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
671
|
+
if (mode === 'steer') {
|
|
672
|
+
const active = this.activeTurns.get(scopeId);
|
|
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
|
+
}
|
|
681
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
682
|
+
? `⚡ **已插话中断前置任务,立即开始新指令**:\n> ${effectivePrompt}`
|
|
683
|
+
: `⚡ **Interrupted previous turn, executing new instruction**:\n> ${effectivePrompt}`);
|
|
684
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, options);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
await this.enqueuePromptTurn(event, effectivePrompt, locale);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, options);
|
|
691
|
+
}
|
|
692
|
+
async enqueuePromptTurn(event, prompt, locale) {
|
|
693
|
+
const scopeId = event.scopeId;
|
|
694
|
+
const adapter = this.getAdapterForScope(scopeId);
|
|
695
|
+
const queueId = `${adapter.id}_q_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
696
|
+
const binding = this.store.getBinding(scopeId);
|
|
697
|
+
this.store.saveQueuedTurnInput({
|
|
698
|
+
queueId,
|
|
699
|
+
scopeId,
|
|
700
|
+
chatId: String(event.chatId),
|
|
701
|
+
chatType: event.chatType,
|
|
702
|
+
topicId: event.topicId ?? null,
|
|
703
|
+
threadId: binding?.threadId || '',
|
|
704
|
+
inputJson: JSON.stringify([{ type: 'text', text: prompt }]),
|
|
705
|
+
sourceSummary: prompt,
|
|
706
|
+
messageId: event.messageId ?? null,
|
|
707
|
+
status: 'queued',
|
|
708
|
+
error: null,
|
|
709
|
+
createdAt: Date.now(),
|
|
710
|
+
updatedAt: Date.now(),
|
|
711
|
+
resolvedAt: null,
|
|
712
|
+
});
|
|
713
|
+
const queueCount = this.store.countQueuedTurnInputs(scopeId);
|
|
714
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
715
|
+
? `📥 **已加入排队队列** (当前队列: ${queueCount} 条)\n当前任务完成后将自动按序执行:\n> ${prompt}\n\n*提示:发送 /interrupt 可中断当前任务,或在 /setup 中切换为“插话”模式。*`
|
|
716
|
+
: `📥 **Queued** (${queueCount} in queue)\nWill execute automatically once current turn finishes:\n> ${prompt}\n\n*Tip: send /interrupt to stop, or switch to "Steer" in /setup.*`);
|
|
717
|
+
}
|
|
718
|
+
async drainNextQueuedTurn(event, locale) {
|
|
719
|
+
const scopeId = event.scopeId;
|
|
720
|
+
try {
|
|
721
|
+
const nextQueued = this.store.peekQueuedTurnInput(scopeId);
|
|
722
|
+
if (!nextQueued)
|
|
723
|
+
return;
|
|
724
|
+
this.store.updateQueuedTurnInputStatus(nextQueued.queueId, 'completed');
|
|
725
|
+
let parsedInput = nextQueued.sourceSummary;
|
|
726
|
+
try {
|
|
727
|
+
const arr = JSON.parse(nextQueued.inputJson);
|
|
728
|
+
if (Array.isArray(arr) && arr[0]?.text) {
|
|
729
|
+
parsedInput = arr[0].text;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
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
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async executeTurn(event, prompt, locale, retryCount = 0, stagedAttachments, options) {
|
|
748
|
+
const scopeId = event.scopeId;
|
|
749
|
+
const binding = this.store.getBinding(scopeId);
|
|
750
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
751
|
+
const adapter = this.getAdapterForScope(scopeId);
|
|
752
|
+
const cwd = binding?.cwd || this.config.defaultCwd;
|
|
753
|
+
const threadId = options?.forcedThreadId ?? binding?.threadId ?? null;
|
|
754
|
+
const model = settings?.model || 'default';
|
|
755
|
+
const isBoost = settings?.serviceTier === 'boost';
|
|
756
|
+
const effort = isBoost ? 'high' : (settings?.reasoningEffort ?? 'high');
|
|
757
|
+
const effectivePrompt = isBoost && !prompt.startsWith('[Boost Mode:')
|
|
758
|
+
? `[Boost Mode: Proceed with deep thinking, strategic planning, multiple perspectives, and rigorous verification.]\n\n${prompt}`
|
|
759
|
+
: prompt;
|
|
760
|
+
let initialMsgId = 0;
|
|
761
|
+
if (options?.reuseMessageId) {
|
|
762
|
+
initialMsgId = options.reuseMessageId;
|
|
763
|
+
try {
|
|
764
|
+
await this.editMessage(scopeId, initialMsgId, locale === 'zh'
|
|
765
|
+
? `🔄 **[重启恢复] ${adapter.name} 正在继续执行…**`
|
|
766
|
+
: `🔄 **[Resuming after restart] ${adapter.name} is continuing…**`);
|
|
767
|
+
}
|
|
768
|
+
catch (editErr) {
|
|
769
|
+
this.logger.warn('orchestrator.reuse_message_failed', { error: String(editErr) });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
try {
|
|
774
|
+
initialMsgId = await this.sendMessage(scopeId, locale === 'zh'
|
|
775
|
+
? (isBoost ? `🚀 ${adapter.name} (Boost 模式) 正在深度思考中…` : `⏳ ${adapter.name} 正在思考中…`)
|
|
776
|
+
: (isBoost ? `🚀 ${adapter.name} (Boost Mode) is thinking deeply…` : `⏳ ${adapter.name} is thinking…`));
|
|
777
|
+
}
|
|
778
|
+
catch (sendErr) {
|
|
779
|
+
this.logger.warn('orchestrator.initial_message_failed', { error: String(sendErr) });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const turnKey = `${adapter.id}_${scopeId}_${Date.now()}`;
|
|
783
|
+
try {
|
|
784
|
+
this.store.saveActiveTurnPreview({
|
|
785
|
+
turnId: turnKey,
|
|
786
|
+
scopeId,
|
|
787
|
+
threadId: threadId || '',
|
|
788
|
+
messageId: initialMsgId,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
/* ignore */
|
|
793
|
+
}
|
|
794
|
+
const req = {
|
|
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
|
+
};
|
|
805
|
+
if (adapter.preflightTurn) {
|
|
806
|
+
try {
|
|
807
|
+
await adapter.preflightTurn(req);
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
this.logger.warn('orchestrator.preflight_failed', { error: String(err) });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const execution = adapter.executeTurn(req);
|
|
814
|
+
const activeTurn = {
|
|
815
|
+
scopeId,
|
|
816
|
+
messageId: initialMsgId,
|
|
817
|
+
threadId,
|
|
818
|
+
execution,
|
|
819
|
+
accumulatedText: '',
|
|
820
|
+
toolLines: [],
|
|
821
|
+
flushTimer: null,
|
|
822
|
+
lastFlushTime: Date.now(),
|
|
823
|
+
typingTimer: null,
|
|
824
|
+
stepIndex: 1,
|
|
825
|
+
toolCount: 0,
|
|
826
|
+
currentTool: null,
|
|
827
|
+
startTime: Date.now(),
|
|
828
|
+
};
|
|
829
|
+
this.activeTurns.set(scopeId, activeTurn);
|
|
830
|
+
this.messaging.sendTypingInScope(scopeId).catch(() => { });
|
|
831
|
+
activeTurn.typingTimer = setInterval(() => {
|
|
832
|
+
this.messaging.sendTypingInScope(scopeId).catch(() => { });
|
|
833
|
+
scheduleFlush();
|
|
834
|
+
}, TYPING_INTERVAL_MS);
|
|
835
|
+
activeTurn.typingTimer?.unref?.();
|
|
836
|
+
const scheduleFlush = () => {
|
|
837
|
+
if (activeTurn.flushTimer)
|
|
838
|
+
return;
|
|
839
|
+
const elapsed = Date.now() - activeTurn.lastFlushTime;
|
|
840
|
+
const delay = Math.max(0, STREAM_THROTTLE_MS - elapsed);
|
|
841
|
+
activeTurn.flushTimer = setTimeout(() => {
|
|
842
|
+
activeTurn.flushTimer = null;
|
|
843
|
+
activeTurn.lastFlushTime = Date.now();
|
|
844
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - activeTurn.startTime) / 1000));
|
|
845
|
+
const content = renderStreamPreviewContent({
|
|
846
|
+
toolLines: activeTurn.toolLines,
|
|
847
|
+
accumulatedText: activeTurn.accumulatedText,
|
|
848
|
+
isBoost,
|
|
849
|
+
engineName: adapter.name,
|
|
850
|
+
stepIndex: activeTurn.stepIndex,
|
|
851
|
+
toolCount: activeTurn.toolCount,
|
|
852
|
+
currentTool: activeTurn.currentTool,
|
|
853
|
+
elapsedSeconds,
|
|
854
|
+
});
|
|
855
|
+
this.editMessage(activeTurn.scopeId, activeTurn.messageId, content).catch(() => { });
|
|
856
|
+
}, delay);
|
|
857
|
+
activeTurn.flushTimer?.unref?.();
|
|
858
|
+
};
|
|
859
|
+
execution.on('delta', (delta) => {
|
|
860
|
+
activeTurn.accumulatedText += delta;
|
|
861
|
+
scheduleFlush();
|
|
862
|
+
});
|
|
863
|
+
execution.on('tool', (tool) => {
|
|
864
|
+
if (tool.stepIndex && tool.stepIndex > 0) {
|
|
865
|
+
activeTurn.stepIndex = tool.stepIndex;
|
|
866
|
+
}
|
|
867
|
+
const icon = tool.status === 'running' ? '⚙️' : tool.status === 'failed' ? '❌' : '✅';
|
|
868
|
+
const summaryText = tool.summary ? ` · ${escapeTelegramHtml(tool.summary)}` : '';
|
|
869
|
+
const line = `${icon} <code>${escapeTelegramHtml(tool.name)}</code>${summaryText}`;
|
|
870
|
+
if (tool.status === 'running') {
|
|
871
|
+
activeTurn.toolCount += 1;
|
|
872
|
+
activeTurn.currentTool = tool.summary ? `${tool.name} (${tool.summary})` : tool.name;
|
|
873
|
+
activeTurn.toolLines.push(line);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
if (activeTurn.currentTool && activeTurn.currentTool.startsWith(tool.name)) {
|
|
877
|
+
activeTurn.currentTool = null;
|
|
878
|
+
}
|
|
879
|
+
const lastIdx = activeTurn.toolLines.findLastIndex((l) => l.includes(`<code>${escapeTelegramHtml(tool.name)}</code>`));
|
|
880
|
+
if (lastIdx !== -1) {
|
|
881
|
+
activeTurn.toolLines[lastIdx] = line;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
activeTurn.toolLines.push(line);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
scheduleFlush();
|
|
888
|
+
});
|
|
889
|
+
execution.on('conversation', (convId) => {
|
|
890
|
+
if (!convId)
|
|
891
|
+
return;
|
|
892
|
+
activeTurn.threadId = convId;
|
|
893
|
+
this.store.setBinding(scopeId, convId, cwd);
|
|
894
|
+
this.store.saveActiveTurnPreview({
|
|
895
|
+
turnId: turnKey,
|
|
896
|
+
scopeId,
|
|
897
|
+
threadId: convId,
|
|
898
|
+
messageId: activeTurn.messageId,
|
|
899
|
+
});
|
|
900
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
901
|
+
});
|
|
902
|
+
const retryTurn = async (nextRetryCount) => {
|
|
903
|
+
await this.executeTurn(event, prompt, locale, nextRetryCount, stagedAttachments, options);
|
|
904
|
+
};
|
|
905
|
+
execution.on('result', async (res) => {
|
|
906
|
+
try {
|
|
907
|
+
if (activeTurn.flushTimer)
|
|
908
|
+
clearTimeout(activeTurn.flushTimer);
|
|
909
|
+
if (activeTurn.typingTimer)
|
|
910
|
+
clearInterval(activeTurn.typingTimer);
|
|
911
|
+
this.activeTurns.delete(scopeId);
|
|
912
|
+
this.store.removeActiveTurnPreview(turnKey);
|
|
913
|
+
if (res.conversationId && (!binding?.threadId || binding.threadId !== res.conversationId)) {
|
|
914
|
+
this.store.setBinding(scopeId, res.conversationId, cwd);
|
|
915
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
916
|
+
}
|
|
917
|
+
if (res.usage) {
|
|
918
|
+
this.store.recordTokenUsage({
|
|
919
|
+
inputTokens: res.usage.inputTokens,
|
|
920
|
+
outputTokens: res.usage.outputTokens,
|
|
921
|
+
cachedTokens: res.usage.cachedTokens,
|
|
922
|
+
totalTokens: res.usage.totalTokens,
|
|
923
|
+
}, adapter.id);
|
|
924
|
+
}
|
|
925
|
+
if (res.status === 'SUCCESS') {
|
|
926
|
+
let finalText = (res.response || '').trim();
|
|
927
|
+
if (!finalText && activeTurn.accumulatedText) {
|
|
928
|
+
finalText = activeTurn.accumulatedText.trim();
|
|
929
|
+
}
|
|
930
|
+
const foldedTools = buildFoldedToolsSummary({
|
|
931
|
+
stepIndex: activeTurn.stepIndex,
|
|
932
|
+
toolLines: activeTurn.toolLines,
|
|
933
|
+
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
934
|
+
startTime: activeTurn.startTime,
|
|
935
|
+
usage: res.usage,
|
|
936
|
+
locale,
|
|
937
|
+
});
|
|
938
|
+
const chunks = combineSummaryAndResponse(foldedTools, finalText || '(无输出 / No output)');
|
|
939
|
+
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
940
|
+
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (res.status === 'ERROR') {
|
|
944
|
+
const errorText = (res.error || res.response || res.error || 'Unknown error').trim();
|
|
945
|
+
if (adapter.handleTurnError) {
|
|
946
|
+
try {
|
|
947
|
+
const handled = await adapter.handleTurnError({
|
|
948
|
+
error: errorText,
|
|
949
|
+
request: req,
|
|
950
|
+
retryCount,
|
|
951
|
+
retryTurn,
|
|
952
|
+
sendMessage: (text) => this.sendMessage(scopeId, text),
|
|
953
|
+
editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
|
|
954
|
+
});
|
|
955
|
+
if (handled)
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
catch (adapterErr) {
|
|
959
|
+
this.logger.warn('orchestrator.adapter_handle_turn_error_failed', { error: String(adapterErr) });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (isTransientNetworkError(errorText) && retryCount < 3) {
|
|
963
|
+
const backoffMs = Math.min(15000, 3000 * Math.pow(2, retryCount));
|
|
964
|
+
const delaySec = Math.round(backoffMs / 1000);
|
|
965
|
+
try {
|
|
966
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
967
|
+
? `🌐 **网络波动重试 (第 ${retryCount + 1}/3 次)**: 正在等待网络稳定 (${delaySec}s) 后自动恢复任务…`
|
|
968
|
+
: `🌐 **Network Retry (${retryCount + 1}/3)**: Waiting for connection (${delaySec}s) to resume task…`);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
/* ignore messaging error during network glitch */
|
|
972
|
+
}
|
|
973
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
974
|
+
try {
|
|
975
|
+
await retryTurn(retryCount + 1);
|
|
976
|
+
}
|
|
977
|
+
catch (retryErr) {
|
|
978
|
+
this.logger.error('orchestrator.retry_turn_failed', { error: String(retryErr) });
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
// If the agent actually produced substantive response text, do NOT mask it with an error box!
|
|
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) {
|
|
994
|
+
const foldedTools = buildFoldedToolsSummary({
|
|
995
|
+
stepIndex: activeTurn.stepIndex,
|
|
996
|
+
toolLines: activeTurn.toolLines,
|
|
997
|
+
toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
|
|
998
|
+
startTime: activeTurn.startTime,
|
|
999
|
+
usage: res.usage,
|
|
1000
|
+
locale,
|
|
1001
|
+
});
|
|
1002
|
+
const warningNote = errorText && errorText !== 'Unknown error' && errorText !== 'Antigravity execution failed'
|
|
1003
|
+
? `\n\n⚠️ <i>(注意:任务结束时伴随提示: ${escapeTelegramHtml(errorText.slice(0, 120))})</i>`
|
|
1004
|
+
: '';
|
|
1005
|
+
const chunks = combineSummaryAndResponse(foldedTools, finalText + warningNote);
|
|
1006
|
+
await this.safeDeliverChunks(scopeId, activeTurn.messageId, chunks);
|
|
1007
|
+
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, `❌ **${adapter.name} 错误**:\n\`\`\`\n${errorText}\n\`\`\``);
|
|
1011
|
+
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
await this.safeDeliverMessage(scopeId, activeTurn.messageId, activeTurn.accumulatedText || '⚠️ 执行结束');
|
|
1015
|
+
await this.drainNextQueuedTurn(event, locale).catch(() => { });
|
|
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
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (isTransientNetworkError(err.message) && retryCount < 3) {
|
|
1047
|
+
const backoffMs = Math.min(15000, 3000 * Math.pow(2, retryCount));
|
|
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;
|
|
1065
|
+
}
|
|
1066
|
+
if (activeTurn.accumulatedText && activeTurn.accumulatedText.trim().length > 20) {
|
|
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
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
async handleEffortCommand(scopeId, args, locale) {
|
|
1090
|
+
const target = args.trim().toLowerCase();
|
|
1091
|
+
if (target === 'low' || target === 'medium' || target === 'high') {
|
|
1092
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
1093
|
+
if (target !== 'high' && settings?.serviceTier === 'boost') {
|
|
1094
|
+
this.store.setChatServiceTier(scopeId, null);
|
|
1095
|
+
}
|
|
1096
|
+
this.store.setChatEffort(scopeId, target);
|
|
1097
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
1098
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${target}\`` : `✅ Reasoning effort set to: \`${target}\``);
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
const current = this.store.getChatSettings(scopeId)?.reasoningEffort ?? 'high';
|
|
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>\``);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async handleBoostCommand(scopeId, args, locale) {
|
|
1108
|
+
const isBoost = this.store.getChatSettings(scopeId)?.serviceTier === 'boost';
|
|
1109
|
+
const target = args.trim().toLowerCase();
|
|
1110
|
+
let nextBoost = !isBoost;
|
|
1111
|
+
if (target === 'on' || target === '1' || target === 'true')
|
|
1112
|
+
nextBoost = true;
|
|
1113
|
+
if (target === 'off' || target === '0' || target === 'false')
|
|
1114
|
+
nextBoost = false;
|
|
1115
|
+
this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
|
|
1116
|
+
if (nextBoost) {
|
|
1117
|
+
this.store.setChatEffort(scopeId, 'high');
|
|
1118
|
+
}
|
|
1119
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
1120
|
+
const msg = locale === 'zh'
|
|
1121
|
+
? (nextBoost
|
|
1122
|
+
? '🚀 已开启 **Boost 增强模式**!\n• 思考深度锁定为 `high`\n• 注入深度规划、多视角审视与交叉验证指令\n• 适用于复杂编程、长推理任务与疑难排错'
|
|
1123
|
+
: '⚪ 已关闭 Boost 模式。')
|
|
1124
|
+
: (nextBoost
|
|
1125
|
+
? '🚀 **Boost Mode** enabled!\n• Reasoning effort locked to `high`\n• Deep planning, multi-perspective analysis & rigorous verification active'
|
|
1126
|
+
: '⚪ Boost mode disabled.');
|
|
1127
|
+
await this.sendMessage(scopeId, msg);
|
|
1128
|
+
}
|
|
1129
|
+
async handleActiveCommand(scopeId, args, locale) {
|
|
1130
|
+
const target = args.trim().toLowerCase();
|
|
1131
|
+
if (target === 'steer' || target === 'queue') {
|
|
1132
|
+
this.store.setChatActiveTurnMessageMode(scopeId, target);
|
|
1133
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
1134
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1135
|
+
? `✅ 运行中消息模式已设置为: **${target === 'steer' ? '插话 ⚡ (立即中断接管)' : '排队 ⏳ (完成后自动执行)'}**`
|
|
1136
|
+
: `✅ Active-turn message mode set to: **${target}**`);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
const current = this.store.getChatSettings(scopeId)?.activeTurnMessageMode ?? 'queue';
|
|
1140
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1141
|
+
? `• **当前运行中消息模式**: ${current === 'steer' ? '⚡ 插话 (立即中断接管)' : '⏳ 排队 (完成后自动执行)'}\n\n用法: \`/active <steer|queue>\` 或在 \`/setup\` 面板中一键切换。`
|
|
1142
|
+
: `• **Current mode**: ${current}\n\nUsage: \`/active <steer|queue>\` or toggle in \`/setup\`.`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async handleQueueCommand(event, args, locale) {
|
|
1146
|
+
const scopeId = event.scopeId;
|
|
1147
|
+
if (!args.trim()) {
|
|
1148
|
+
const count = this.store.countQueuedTurnInputs(scopeId);
|
|
1149
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1150
|
+
? `📥 当前队列中共有 ${count} 条待执行任务。\n用法: \`/queue <消息>\` 追加排队,或 \`/queue clear\` 清空队列。`
|
|
1151
|
+
: `📥 Total ${count} queued tasks.\nUsage: \`/queue <message>\` or \`/queue clear\`.`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (args.trim().toLowerCase() === 'clear') {
|
|
1155
|
+
const cleared = this.store.cancelQueuedTurnInputs(scopeId);
|
|
1156
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1157
|
+
? `🗑️ 已清空当前会话的 ${cleared} 条排队任务。`
|
|
1158
|
+
: `🗑️ Cleared ${cleared} queued tasks.`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
await this.enqueuePromptTurn(event, args.trim(), locale);
|
|
1162
|
+
}
|
|
1163
|
+
async handleSteerCommand(event, args, locale) {
|
|
1164
|
+
const scopeId = event.scopeId;
|
|
1165
|
+
if (!args.trim()) {
|
|
1166
|
+
await this.sendMessage(scopeId, locale === 'zh' ? '用法: `/steer <消息>` (立即中断当前思考,带入新指令)' : 'Usage: `/steer <message>`');
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const active = this.activeTurns.get(scopeId);
|
|
1170
|
+
if (active) {
|
|
1171
|
+
active.execution.cancel();
|
|
1172
|
+
if (active.typingTimer)
|
|
1173
|
+
clearInterval(active.typingTimer);
|
|
1174
|
+
if (active.flushTimer)
|
|
1175
|
+
clearTimeout(active.flushTimer);
|
|
1176
|
+
this.activeTurns.delete(scopeId);
|
|
1177
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1178
|
+
? `⚡ 已中断前置任务,立即开始插话指令:\n> ${args.trim()}`
|
|
1179
|
+
: `⚡ Interrupted active turn, executing steer instruction:\n> ${args.trim()}`);
|
|
1180
|
+
}
|
|
1181
|
+
await this.executeTurn(event, args.trim(), locale);
|
|
1182
|
+
}
|
|
1183
|
+
async handleInterrupt(scopeId, locale) {
|
|
1184
|
+
const active = this.activeTurns.get(scopeId);
|
|
1185
|
+
const queuedCount = this.store.countQueuedTurnInputs(scopeId);
|
|
1186
|
+
if (active) {
|
|
1187
|
+
active.execution.cancel();
|
|
1188
|
+
if (active.flushTimer)
|
|
1189
|
+
clearTimeout(active.flushTimer);
|
|
1190
|
+
if (active.typingTimer)
|
|
1191
|
+
clearInterval(active.typingTimer);
|
|
1192
|
+
this.activeTurns.delete(scopeId);
|
|
1193
|
+
let extraMsg = '';
|
|
1194
|
+
if (queuedCount > 0) {
|
|
1195
|
+
extraMsg = locale === 'zh'
|
|
1196
|
+
? `\nℹ️ 队列中尚有 ${queuedCount} 条任务,如需一并清空请发送 \`/queue clear\`。`
|
|
1197
|
+
: `\nℹ️ ${queuedCount} tasks in queue. Send \`/queue clear\` to remove.`;
|
|
1198
|
+
}
|
|
1199
|
+
await this.sendMessage(scopeId, (locale === 'zh' ? '🛑 已发送中断请求。' : '🛑 Interrupt request sent.') + extraMsg);
|
|
1200
|
+
}
|
|
1201
|
+
else if (queuedCount > 0) {
|
|
1202
|
+
const cleared = this.store.cancelQueuedTurnInputs(scopeId);
|
|
1203
|
+
await this.sendMessage(scopeId, locale === 'zh' ? `🛑 当前无运行中任务,已清空 ${cleared} 条排队任务。` : `🛑 Cleared ${cleared} queued tasks.`);
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
await this.sendMessage(scopeId, locale === 'zh' ? 'ℹ️ 当前没有正在执行或排队的任务。' : 'ℹ️ No active or queued tasks to interrupt.');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async handleNewSession(scopeId, locale, targetCwdInput) {
|
|
1210
|
+
const rawTarget = targetCwdInput?.trim();
|
|
1211
|
+
const binding = this.store.getBinding(scopeId);
|
|
1212
|
+
let cwd = binding?.cwd || this.config.defaultCwd;
|
|
1213
|
+
if (rawTarget) {
|
|
1214
|
+
let resolved = rawTarget;
|
|
1215
|
+
if (resolved.startsWith('~')) {
|
|
1216
|
+
resolved = path.join(os.homedir(), resolved.slice(1));
|
|
1217
|
+
}
|
|
1218
|
+
else if (!path.isAbsolute(resolved)) {
|
|
1219
|
+
resolved = path.resolve(cwd, resolved);
|
|
1220
|
+
}
|
|
1221
|
+
resolved = path.normalize(resolved);
|
|
1222
|
+
if (!fs.existsSync(resolved)) {
|
|
1223
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1224
|
+
? `❌ **指定的工作目录不存在**:\`${resolved}\`\n请检查路径是否正确。`
|
|
1225
|
+
: `❌ **Directory does not exist**: \`${resolved}\``);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const stat = fs.statSync(resolved);
|
|
1229
|
+
if (!stat.isDirectory()) {
|
|
1230
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1231
|
+
? `❌ **指定的路径不是一个目录**:\`${resolved}\``
|
|
1232
|
+
: `❌ **Path is not a directory**: \`${resolved}\``);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
cwd = resolved;
|
|
1236
|
+
}
|
|
1237
|
+
// Cancel queued turn inputs
|
|
1238
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
1239
|
+
const active = this.activeTurns.get(scopeId);
|
|
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
|
+
}
|
|
1248
|
+
// Reset thread binding to empty/ready state with the target cwd
|
|
1249
|
+
this.store.setBinding(scopeId, '', cwd);
|
|
1250
|
+
this.syncCurrentBackendSettings(scopeId);
|
|
1251
|
+
const adapter = this.getAdapterForScope(scopeId);
|
|
1252
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1253
|
+
? `✨ **已开启全新会话**\n• 当前引擎: **${adapter.name}**\n• 工作目录: \`${cwd}\`\n\n发送任意消息开启新任务。`
|
|
1254
|
+
: `✨ **New Session Created**\n• Engine: **${adapter.name}**\n• Directory: \`${cwd}\`\n\nSend a message to start.`);
|
|
1255
|
+
}
|
|
1256
|
+
async sendStatus(scopeId, locale) {
|
|
1257
|
+
const binding = this.store.getBinding(scopeId);
|
|
1258
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
1259
|
+
const isBusy = this.activeTurns.has(scopeId);
|
|
1260
|
+
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1261
|
+
const adapter = backendDesc.adapter;
|
|
1262
|
+
const customStatus = this.customUi?.renderCustomStatus ? await this.customUi.renderCustomStatus(scopeId, locale) : '';
|
|
1263
|
+
const totalUsage = this.store.getCumulativeTokenUsage();
|
|
1264
|
+
const tokenLine = formatTokenUsageSummary(totalUsage, locale);
|
|
1265
|
+
const allUsages = this.store.getAllBackendTokenUsages();
|
|
1266
|
+
const breakdown = formatBackendTokenUsageBreakdown(allUsages);
|
|
1267
|
+
const text = locale === 'zh'
|
|
1268
|
+
? `📊 **${adapter.name} 运行状态**\n\n` +
|
|
1269
|
+
`• **当前引擎**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1270
|
+
`• **状态**: ${isBusy ? '⚡ 正在执行任务' : '💤 空闲'}\n` +
|
|
1271
|
+
`• **当前模型**: \`${settings?.model || '默认'}\`\n` +
|
|
1272
|
+
`${tokenLine}${breakdown}\n` +
|
|
1273
|
+
`• **绑定会话**: \`${binding?.threadId || '(新会话)'}\`\n` +
|
|
1274
|
+
`• **工作目录**: \`${binding?.cwd || this.config.defaultCwd}\`` +
|
|
1275
|
+
(customStatus ? `\n${customStatus}` : '')
|
|
1276
|
+
: `📊 **${adapter.name} Status**\n\n` +
|
|
1277
|
+
`• **Engine**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1278
|
+
`• **State**: ${isBusy ? '⚡ Executing' : '💤 Idle'}\n` +
|
|
1279
|
+
`• **Model**: \`${settings?.model || 'default'}\`\n` +
|
|
1280
|
+
`${tokenLine}${breakdown}\n` +
|
|
1281
|
+
`• **Thread**: \`${binding?.threadId || '(new)'}\`\n` +
|
|
1282
|
+
`• **Directory**: \`${binding?.cwd || this.config.defaultCwd}\`` +
|
|
1283
|
+
(customStatus ? `\n${customStatus}` : '');
|
|
1284
|
+
const keyboard = [
|
|
1285
|
+
[
|
|
1286
|
+
{ text: '⚙️ 控制面板', callback_data: 'engine:setup:main' },
|
|
1287
|
+
{ text: '🧠 切换模型', callback_data: 'engine:setup:models' },
|
|
1288
|
+
],
|
|
1289
|
+
];
|
|
1290
|
+
const allBackends = await this.listBackends();
|
|
1291
|
+
if (allBackends.length > 1 || this.backendProvider) {
|
|
1292
|
+
keyboard.push([
|
|
1293
|
+
{ text: `🔌 切换后端 (${allBackends.length})`, callback_data: 'engine:setup:backend' },
|
|
1294
|
+
]);
|
|
1295
|
+
}
|
|
1296
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1297
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1298
|
+
}
|
|
1299
|
+
async sendSetupMenu(scopeId, locale, editMessageId) {
|
|
1300
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
1301
|
+
const mode = settings?.activeTurnMessageMode ?? 'queue';
|
|
1302
|
+
const isBoost = settings?.serviceTier === 'boost';
|
|
1303
|
+
const backendDesc = this.getBackendDescriptorForScope(scopeId);
|
|
1304
|
+
const adapter = backendDesc.adapter;
|
|
1305
|
+
const allBackends = await this.listBackends();
|
|
1306
|
+
const currentModel = settings?.model;
|
|
1307
|
+
const currentEffort = isBoost ? 'high' : (settings?.reasoningEffort ?? null);
|
|
1308
|
+
const text = locale === 'zh'
|
|
1309
|
+
? `⚙️ **${adapter.name} 控制面板**\n\n` +
|
|
1310
|
+
`• **当前引擎**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1311
|
+
`• **当前模型**: \`${settings?.model || '默认'}\`\n` +
|
|
1312
|
+
`• **思考深度**: \`${currentEffort || '默认'}\`\n` +
|
|
1313
|
+
`• **Boost 增强**: ${isBoost ? '🚀 已开启' : '⚪ 已关闭'}\n` +
|
|
1314
|
+
`• **运行中消息**: ${mode === 'steer' ? '⚡ 插话 (立即中断接管)' : '⏳ 排队 (完成后自动执行)'}\n\n` +
|
|
1315
|
+
`请选择要配置的项目:`
|
|
1316
|
+
: `⚙️ **${adapter.name} Setup Panel**\n\n` +
|
|
1317
|
+
`• **Engine**: \`${backendDesc.name}\` (\`${backendDesc.id}\`)${backendDesc.account ? ` · \`${backendDesc.account}\`` : ''}\n` +
|
|
1318
|
+
`• **Model**: \`${settings?.model || 'default'}\`\n` +
|
|
1319
|
+
`• **Effort**: \`${currentEffort || 'default'}\`\n` +
|
|
1320
|
+
`• **Boost**: ${isBoost ? '🚀 Enabled' : '⚪ Disabled'}\n` +
|
|
1321
|
+
`• **Active-Turn**: ${mode === 'steer' ? '⚡ Steer' : '⏳ Queue'}\n\n` +
|
|
1322
|
+
`Select setting to configure:`;
|
|
1323
|
+
const keyboard = [];
|
|
1324
|
+
// 1. Model choices (Codex style)
|
|
1325
|
+
const models = await adapter.listModels();
|
|
1326
|
+
const modelButtons = [
|
|
1327
|
+
{
|
|
1328
|
+
text: `${!currentModel || currentModel === 'default' ? '• ' : ''}${locale === 'zh' ? '默认模型' : 'Default'}`,
|
|
1329
|
+
callback_data: 'engine:m:default',
|
|
1330
|
+
},
|
|
1331
|
+
];
|
|
1332
|
+
for (const m of models.slice(0, 5)) {
|
|
1333
|
+
const isAct = currentModel === m.id || (!currentModel && m.isDefault);
|
|
1334
|
+
const label = m.name.length > 14 ? `${m.name.slice(0, 13)}…` : m.name;
|
|
1335
|
+
modelButtons.push({
|
|
1336
|
+
text: `${isAct ? '• ' : ''}${label}`,
|
|
1337
|
+
callback_data: `engine:m:${m.id}`,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
for (let i = 0; i < modelButtons.length; i += 2) {
|
|
1341
|
+
keyboard.push(modelButtons.slice(i, i + 2));
|
|
1342
|
+
}
|
|
1343
|
+
// 2. Reasoning effort choices (Codex style)
|
|
1344
|
+
const isAgy = backendDesc.engineType === 'antigravity' || backendDesc.id === 'antigravity';
|
|
1345
|
+
const isCodex = backendDesc.engineType === 'codex' || backendDesc.id === 'codex';
|
|
1346
|
+
const supportedEfforts = isAgy
|
|
1347
|
+
? ['low', 'medium', 'high']
|
|
1348
|
+
: isCodex
|
|
1349
|
+
? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1350
|
+
: ['low', 'medium', 'high'];
|
|
1351
|
+
const effortButtons = [
|
|
1352
|
+
{
|
|
1353
|
+
text: `${!currentEffort || currentEffort === 'default' ? '• ' : ''}${locale === 'zh' ? '默认深度' : 'Default'}`,
|
|
1354
|
+
callback_data: 'engine:effort:default',
|
|
1355
|
+
},
|
|
1356
|
+
...supportedEfforts.map((eff) => ({
|
|
1357
|
+
text: `${currentEffort === eff ? '• ' : ''}${eff}`,
|
|
1358
|
+
callback_data: `engine:effort:${eff}`,
|
|
1359
|
+
})),
|
|
1360
|
+
];
|
|
1361
|
+
for (let i = 0; i < effortButtons.length; i += 3) {
|
|
1362
|
+
keyboard.push(effortButtons.slice(i, i + 3));
|
|
1363
|
+
}
|
|
1364
|
+
// 3. Boost & Active Mode row
|
|
1365
|
+
keyboard.push([
|
|
1366
|
+
{
|
|
1367
|
+
text: isBoost ? (locale === 'zh' ? '🚀 Boost: 开启' : '🚀 Boost: On') : (locale === 'zh' ? '⚪ Boost: 关闭' : '⚪ Boost: Off'),
|
|
1368
|
+
callback_data: 'engine:setup:boost',
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
text: mode === 'steer' ? (locale === 'zh' ? '⚡ 插话模式' : '⚡ Steer') : (locale === 'zh' ? '⏳ 排队模式' : '⏳ Queue'),
|
|
1372
|
+
callback_data: 'engine:setup:active_mode',
|
|
1373
|
+
},
|
|
1374
|
+
]);
|
|
1375
|
+
// 4. Custom rows (Account & History)
|
|
1376
|
+
if (this.customUi?.renderCustomSetupRows) {
|
|
1377
|
+
const customRows = await this.customUi.renderCustomSetupRows(scopeId, locale);
|
|
1378
|
+
keyboard.push(...customRows);
|
|
1379
|
+
}
|
|
1380
|
+
// 5. Session & Backend switcher row
|
|
1381
|
+
keyboard.push([
|
|
1382
|
+
{ text: '✨ 新建会话', callback_data: 'engine:setup:new' },
|
|
1383
|
+
{ text: `🔌 切换后端运行环境 (${allBackends.length})`, callback_data: 'engine:setup:backend' },
|
|
1384
|
+
]);
|
|
1385
|
+
// 6. Refresh row
|
|
1386
|
+
keyboard.push([
|
|
1387
|
+
{ text: '🔄 刷新面板', callback_data: 'engine:setup:main' },
|
|
1388
|
+
]);
|
|
1389
|
+
if (editMessageId) {
|
|
1390
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1391
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1395
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
async sendModelsMenu(scopeId, locale, editMessageId) {
|
|
1399
|
+
const settings = this.store.getChatSettings(scopeId);
|
|
1400
|
+
const currentModel = settings?.model;
|
|
1401
|
+
const adapter = this.getAdapterForScope(scopeId);
|
|
1402
|
+
const models = await adapter.listModels();
|
|
1403
|
+
const keyboard = [];
|
|
1404
|
+
for (let i = 0; i < models.length; i += 2) {
|
|
1405
|
+
const row = [];
|
|
1406
|
+
const m1 = models[i];
|
|
1407
|
+
const isM1Active = m1.id === currentModel || (!currentModel && m1.isDefault);
|
|
1408
|
+
row.push({
|
|
1409
|
+
text: `${isM1Active ? '✅ ' : ''}${m1.name}`,
|
|
1410
|
+
callback_data: `engine:m:${m1.id}`,
|
|
1411
|
+
});
|
|
1412
|
+
if (i + 1 < models.length) {
|
|
1413
|
+
const m2 = models[i + 1];
|
|
1414
|
+
const isM2Active = m2.id === currentModel || (!currentModel && m2.isDefault);
|
|
1415
|
+
row.push({
|
|
1416
|
+
text: `${isM2Active ? '✅ ' : ''}${m2.name}`,
|
|
1417
|
+
callback_data: `engine:m:${m2.id}`,
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
keyboard.push(row);
|
|
1421
|
+
}
|
|
1422
|
+
keyboard.push([{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }]);
|
|
1423
|
+
const title = locale === 'zh'
|
|
1424
|
+
? `🎯 **选择 ${adapter.name} 模型**`
|
|
1425
|
+
: `🎯 **Select ${adapter.name} Model**`;
|
|
1426
|
+
if (editMessageId) {
|
|
1427
|
+
await this.editMessage(scopeId, editMessageId, title, keyboard);
|
|
1428
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
const msgId = await this.sendMessage(scopeId, title, keyboard);
|
|
1432
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async sendHelp(scopeId, locale) {
|
|
1436
|
+
const adapter = this.getAdapterForScope(scopeId);
|
|
1437
|
+
const text = locale === 'zh'
|
|
1438
|
+
? `🦊 **FoxClaw — ${adapter.name} 使用指南**\n\n` +
|
|
1439
|
+
`• 直接发送消息即可向 AI 发起提问或任务\n` +
|
|
1440
|
+
`• 发送照片、文件等多媒体,AI 可自动下载落盘并分析\n\n` +
|
|
1441
|
+
`**常用命令:**\n` +
|
|
1442
|
+
`/setup — 控制面板(切换模型、插话/排队模式管理)\n` +
|
|
1443
|
+
`/backend — 切换当前后端引擎与账号(如 Google Antigravity / OpenAI Codex / OpenCode)\n` +
|
|
1444
|
+
`/models — 快速切换 AI 模型\n` +
|
|
1445
|
+
`/threads — 查看与切换会话历史\n` +
|
|
1446
|
+
`/status — 查看当前运行状态与会话信息\n` +
|
|
1447
|
+
`/interrupt — 中断当前正在运行的任务\n` +
|
|
1448
|
+
`/new — 清空历史并新建会话\n` +
|
|
1449
|
+
`/help — 显示本帮助手册\n`
|
|
1450
|
+
: `🦊 **FoxClaw — ${adapter.name} Quick Guide**\n\n` +
|
|
1451
|
+
`• Send any prompt to chat or run tasks\n` +
|
|
1452
|
+
`• Send photos or documents for AI inspection\n\n` +
|
|
1453
|
+
`**Commands:**\n` +
|
|
1454
|
+
`/setup — Control panel\n` +
|
|
1455
|
+
`/backend — Switch active backend engine or account\n` +
|
|
1456
|
+
`/models — Switch model\n` +
|
|
1457
|
+
`/threads — List and switch conversations\n` +
|
|
1458
|
+
`/status — Check runtime status\n` +
|
|
1459
|
+
`/interrupt — Stop active turn\n` +
|
|
1460
|
+
`/new — Start fresh session\n` +
|
|
1461
|
+
`/help — Show this help\n`;
|
|
1462
|
+
const msgId = await this.sendMessage(scopeId, text);
|
|
1463
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1464
|
+
}
|
|
1465
|
+
async sendBackendMenu(scopeId, locale, editMessageId) {
|
|
1466
|
+
const backends = await this.listBackends();
|
|
1467
|
+
const activeBackend = this.getBackendDescriptorForScope(scopeId);
|
|
1468
|
+
const binding = this.store.getBinding(scopeId);
|
|
1469
|
+
const lines = [
|
|
1470
|
+
locale === 'zh' ? '🔌 **后端运行环境 (Backends & Engines)**' : '🔌 **Backends & Engines**',
|
|
1471
|
+
'',
|
|
1472
|
+
locale === 'zh'
|
|
1473
|
+
? `• **当前活跃**: ● **${activeBackend.name}** (\`${activeBackend.id}\`)`
|
|
1474
|
+
: `• **Active**: ● **${activeBackend.name}** (\`${activeBackend.id}\`)`,
|
|
1475
|
+
];
|
|
1476
|
+
if (activeBackend.account) {
|
|
1477
|
+
lines.push(locale === 'zh' ? `• **绑定账号**: \`${activeBackend.account}\`` : `• **Account**: \`${activeBackend.account}\``);
|
|
1478
|
+
}
|
|
1479
|
+
if (activeBackend.details) {
|
|
1480
|
+
lines.push(locale === 'zh' ? `• **状态详情**: ${activeBackend.details}` : `• **Details**: ${activeBackend.details}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (binding?.threadId) {
|
|
1483
|
+
lines.push(locale === 'zh'
|
|
1484
|
+
? `• **当前会话**: \`${binding.threadId.slice(0, 16)}…\` (\`${binding.cwd ?? this.config.defaultCwd}\`)`
|
|
1485
|
+
: `• **Thread**: \`${binding.threadId.slice(0, 16)}…\` (\`${binding.cwd ?? this.config.defaultCwd}\`)`);
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
lines.push(locale === 'zh'
|
|
1489
|
+
? `• **会话状态**: 未绑定 (发送新消息将开启新会话)`
|
|
1490
|
+
: `• **Thread**: None (next prompt starts new thread)`);
|
|
1491
|
+
}
|
|
1492
|
+
lines.push('', '───────────────────', locale === 'zh' ? '**可用后端列表**:' : '**Available Backends**:');
|
|
1493
|
+
const keyboard = [];
|
|
1494
|
+
backends.forEach((b, idx) => {
|
|
1495
|
+
const isCurrent = b.id === activeBackend.id;
|
|
1496
|
+
const marker = isCurrent ? '●' : '○';
|
|
1497
|
+
const badge = isCurrent ? (locale === 'zh' ? ' [当前活跃]' : ' [Active]') : '';
|
|
1498
|
+
const num = idx + 1;
|
|
1499
|
+
lines.push(`${marker} **${num}.** ${b.name}${badge}`);
|
|
1500
|
+
if (b.details || b.account) {
|
|
1501
|
+
lines.push(` ${b.details || b.account}`);
|
|
1502
|
+
}
|
|
1503
|
+
keyboard.push([
|
|
1504
|
+
{
|
|
1505
|
+
text: `${marker} ${num}. ${b.name.length > 24 ? `${b.name.slice(0, 23)}…` : b.name}`,
|
|
1506
|
+
callback_data: `engine:backend:${b.id}`,
|
|
1507
|
+
},
|
|
1508
|
+
]);
|
|
1509
|
+
});
|
|
1510
|
+
keyboard.push([{ text: '◀️ 返回控制面板', callback_data: 'engine:setup:main' }]);
|
|
1511
|
+
const text = lines.join('\n');
|
|
1512
|
+
if (editMessageId) {
|
|
1513
|
+
await this.editMessage(scopeId, editMessageId, text, keyboard);
|
|
1514
|
+
this.scheduleStalePanelDeletion(scopeId, editMessageId);
|
|
1515
|
+
}
|
|
1516
|
+
else {
|
|
1517
|
+
const msgId = await this.sendMessage(scopeId, text, keyboard);
|
|
1518
|
+
this.scheduleStalePanelDeletion(scopeId, msgId);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async handleBackendCommand(scopeId, argsString, locale) {
|
|
1522
|
+
if (!argsString) {
|
|
1523
|
+
await this.sendBackendMenu(scopeId, locale);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const backends = await this.listBackends();
|
|
1527
|
+
let targetId = argsString.trim().toLowerCase();
|
|
1528
|
+
if (targetId.includes('反重力') || targetId === 'agy' || targetId === 'gemini') {
|
|
1529
|
+
targetId = 'antigravity';
|
|
1530
|
+
}
|
|
1531
|
+
else if (targetId.includes('codex') || targetId.includes('openai')) {
|
|
1532
|
+
targetId = 'codex';
|
|
1533
|
+
}
|
|
1534
|
+
// Check if target is a number index
|
|
1535
|
+
const num = parseInt(targetId, 10);
|
|
1536
|
+
if (!isNaN(num) && num >= 1 && num <= backends.length) {
|
|
1537
|
+
targetId = backends[num - 1].id;
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
// Find matching by id or name
|
|
1541
|
+
const found = backends.find((b) => b.id.toLowerCase() === targetId || b.name.toLowerCase().includes(targetId));
|
|
1542
|
+
if (found) {
|
|
1543
|
+
targetId = found.id;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const targetDesc = await this.resolveBackendDescriptor(targetId);
|
|
1547
|
+
if (!targetDesc) {
|
|
1548
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
1549
|
+
? `❌ 未找到后端: \`${argsString}\`。可用列表: ${backends.map((b) => `\`${b.id}\``).join(', ')}`
|
|
1550
|
+
: `❌ Backend not found: \`${argsString}\`. Available: ${backends.map((b) => `\`${b.id}\``).join(', ')}`);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
await this.switchBackend(scopeId, targetId, locale);
|
|
1554
|
+
await this.sendSetupMenu(scopeId, locale);
|
|
1555
|
+
}
|
|
1556
|
+
}
|