@foxden-app/foxclaw 0.10.0 → 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.
@@ -12,6 +12,7 @@ import { renderStreamPreviewContent, buildFoldedToolsSummary, combineSummaryAndR
12
12
  import { BRIDGE_SCOPE_TELEGRAM_PREFIX, BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope } from './bridge_scope.js';
13
13
  import { escapeTelegramHtml } from '../telegram/html.js';
14
14
  import { formatTokenUsageSummary, formatBackendTokenUsageBreakdown } from '../store/token_usage.js';
15
+ import { isTransientNetworkError } from '../telegram/api.js';
15
16
  export const STREAM_THROTTLE_MS = 700;
16
17
  export const TYPING_INTERVAL_MS = 4000;
17
18
  export class UnifiedChannelOrchestrator {
@@ -299,6 +300,16 @@ export class UnifiedChannelOrchestrator {
299
300
  defaultEngine: this.adapter.id,
300
301
  backends: Array.from(this.backends.keys()),
301
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
+ }
302
313
  // Crash recovery: check interrupted preview messages from before restart
303
314
  try {
304
315
  const backendIds = new Set(this.backends.keys());
@@ -312,9 +323,19 @@ export class UnifiedChannelOrchestrator {
312
323
  }
313
324
  return false;
314
325
  });
326
+ const recoveredScopes = new Set();
315
327
  for (const prev of activePreviews) {
316
328
  this.store.removeActiveTurnPreview(prev.turnId);
317
- this.editMessage(prev.scopeId, prev.messageId, '🔄 **Foxclaw 服务已重新加载/重启**\n\n检测到上一轮任务执行被服务重启中断,正在自动继续执行…').catch(() => { });
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(() => { });
318
339
  const fakeEvent = {
319
340
  scopeId: prev.scopeId,
320
341
  chatId: prev.scopeId,
@@ -327,8 +348,14 @@ export class UnifiedChannelOrchestrator {
327
348
  entities: [],
328
349
  replyToBot: false,
329
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.');
330
354
  const resumeTimer = setTimeout(() => {
331
- this.startPromptTurn(fakeEvent, '请继续完成上一步未完成的任务', 'zh').catch((err) => {
355
+ this.startPromptTurn(fakeEvent, resumePrompt, locale, {
356
+ reuseMessageId: prev.messageId,
357
+ forcedThreadId: prev.threadId || undefined,
358
+ }).catch((err) => {
332
359
  this.logger.warn('orchestrator.auto_resume_failed', {
333
360
  scopeId: prev.scopeId,
334
361
  error: String(err),
@@ -337,6 +364,32 @@ export class UnifiedChannelOrchestrator {
337
364
  }, 1500);
338
365
  resumeTimer?.unref?.();
339
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
+ }
340
393
  }
341
394
  catch (err) {
342
395
  this.logger.warn('orchestrator.restore_active_turns_error', { error: String(err) });
@@ -370,8 +423,36 @@ export class UnifiedChannelOrchestrator {
370
423
  return lastMsgId;
371
424
  }
372
425
  async editMessage(scopeId, messageId, text, keyboard) {
426
+ if (!messageId || messageId <= 0) {
427
+ await this.sendMessage(scopeId, text, keyboard);
428
+ return;
429
+ }
373
430
  await this.messaging.editRichMarkdown(scopeId, messageId, text, keyboard);
374
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
+ }
375
456
  async handleText(event) {
376
457
  const scopeId = event.scopeId;
377
458
  const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
@@ -423,7 +504,7 @@ export class UnifiedChannelOrchestrator {
423
504
  return;
424
505
  case 'model':
425
506
  if (argsString) {
426
- this.store.setChatSettings(scopeId, argsString, null);
507
+ this.store.setChatModel(scopeId, argsString === 'default' ? null : argsString);
427
508
  this.syncCurrentBackendSettings(scopeId);
428
509
  await this.sendMessage(scopeId, locale === 'zh' ? `✅ 模型已切换为: \`${argsString}\`` : `✅ Model switched to: \`${argsString}\``);
429
510
  }
@@ -500,7 +581,7 @@ export class UnifiedChannelOrchestrator {
500
581
  ? data.slice('engine:m:'.length)
501
582
  : decodeURIComponent(data.slice('setup:model:'.length));
502
583
  const model = rawModel === 'default' ? null : rawModel;
503
- this.store.setChatSettings(scopeId, model, null);
584
+ this.store.setChatModel(scopeId, model);
504
585
  this.syncCurrentBackendSettings(scopeId);
505
586
  await this.messaging.answerCallback(event.callbackQueryId, `Model: ${rawModel}`);
506
587
  await this.sendSetupMenu(scopeId, locale, messageId);
@@ -515,7 +596,7 @@ export class UnifiedChannelOrchestrator {
515
596
  if (targetEffort !== 'high' && settings?.serviceTier === 'boost') {
516
597
  this.store.setChatServiceTier(scopeId, null);
517
598
  }
518
- this.store.setChatSettings(scopeId, null, targetEffort);
599
+ this.store.setChatEffort(scopeId, targetEffort);
519
600
  this.syncCurrentBackendSettings(scopeId);
520
601
  await this.messaging.answerCallback(event.callbackQueryId, `Effort: ${rawEffort}`);
521
602
  await this.sendSetupMenu(scopeId, locale, messageId);
@@ -533,7 +614,7 @@ export class UnifiedChannelOrchestrator {
533
614
  const nextBoost = !isBoost;
534
615
  this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
535
616
  if (nextBoost) {
536
- this.store.setChatSettings(scopeId, null, 'high');
617
+ this.store.setChatEffort(scopeId, 'high');
537
618
  }
538
619
  this.syncCurrentBackendSettings(scopeId);
539
620
  await this.messaging.answerCallback(event.callbackQueryId, nextBoost ? (locale === 'zh' ? '🚀 Boost 模式已开启 (深度 High)' : '🚀 Boost Mode enabled (High)') : (locale === 'zh' ? '⚪ Boost 模式已关闭' : '⚪ Boost Mode disabled'));
@@ -567,11 +648,11 @@ export class UnifiedChannelOrchestrator {
567
648
  }
568
649
  }
569
650
  }
570
- async startPromptTurn(event, prompt, locale) {
651
+ async startPromptTurn(event, prompt, locale, options) {
571
652
  const scopeId = event.scopeId;
572
653
  const binding = this.store.getBinding(scopeId);
573
654
  const cwd = binding?.cwd || this.config.defaultCwd;
574
- const threadId = binding?.threadId || 'default';
655
+ const threadId = options?.forcedThreadId ?? binding?.threadId ?? 'default';
575
656
  let effectivePrompt = prompt;
576
657
  let stagedAttachments;
577
658
  if (event.attachments && event.attachments.length > 0) {
@@ -600,13 +681,13 @@ export class UnifiedChannelOrchestrator {
600
681
  await this.sendMessage(scopeId, locale === 'zh'
601
682
  ? `⚡ **已插话中断前置任务,立即开始新指令**:\n> ${effectivePrompt}`
602
683
  : `⚡ **Interrupted previous turn, executing new instruction**:\n> ${effectivePrompt}`);
603
- await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments);
684
+ await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, options);
604
685
  return;
605
686
  }
606
687
  await this.enqueuePromptTurn(event, effectivePrompt, locale);
607
688
  return;
608
689
  }
609
- await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments);
690
+ await this.executeTurn(event, effectivePrompt, locale, 0, stagedAttachments, options);
610
691
  }
611
692
  async enqueuePromptTurn(event, prompt, locale) {
612
693
  const scopeId = event.scopeId;
@@ -663,22 +744,41 @@ export class UnifiedChannelOrchestrator {
663
744
  this.logger.warn('orchestrator.drain_queue_failed', { error: String(err) });
664
745
  }
665
746
  }
666
- async executeTurn(event, prompt, locale, retryCount = 0, stagedAttachments) {
747
+ async executeTurn(event, prompt, locale, retryCount = 0, stagedAttachments, options) {
667
748
  const scopeId = event.scopeId;
668
749
  const binding = this.store.getBinding(scopeId);
669
750
  const settings = this.store.getChatSettings(scopeId);
670
751
  const adapter = this.getAdapterForScope(scopeId);
671
752
  const cwd = binding?.cwd || this.config.defaultCwd;
672
- const threadId = binding?.threadId || null;
753
+ const threadId = options?.forcedThreadId ?? binding?.threadId ?? null;
673
754
  const model = settings?.model || 'default';
674
755
  const isBoost = settings?.serviceTier === 'boost';
675
756
  const effort = isBoost ? 'high' : (settings?.reasoningEffort ?? 'high');
676
757
  const effectivePrompt = isBoost && !prompt.startsWith('[Boost Mode:')
677
758
  ? `[Boost Mode: Proceed with deep thinking, strategic planning, multiple perspectives, and rigorous verification.]\n\n${prompt}`
678
759
  : prompt;
679
- const initialMsgId = await this.sendMessage(scopeId, locale === 'zh'
680
- ? (isBoost ? `🚀 ${adapter.name} (Boost 模式) 正在深度思考中…` : `⏳ ${adapter.name} 正在思考中…`)
681
- : (isBoost ? `🚀 ${adapter.name} (Boost Mode) is thinking deeply…` : `⏳ ${adapter.name} is thinking…`));
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
+ }
682
782
  const turnKey = `${adapter.id}_${scopeId}_${Date.now()}`;
683
783
  try {
684
784
  this.store.saveActiveTurnPreview({
@@ -786,79 +886,47 @@ export class UnifiedChannelOrchestrator {
786
886
  }
787
887
  scheduleFlush();
788
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
+ });
789
902
  const retryTurn = async (nextRetryCount) => {
790
- await this.executeTurn(event, prompt, locale, nextRetryCount, stagedAttachments);
903
+ await this.executeTurn(event, prompt, locale, nextRetryCount, stagedAttachments, options);
791
904
  };
792
905
  execution.on('result', async (res) => {
793
- if (activeTurn.flushTimer)
794
- clearTimeout(activeTurn.flushTimer);
795
- if (activeTurn.typingTimer)
796
- clearInterval(activeTurn.typingTimer);
797
- this.activeTurns.delete(scopeId);
798
- this.store.removeActiveTurnPreview(turnKey);
799
- if (res.conversationId && (!binding?.threadId || binding.threadId !== res.conversationId)) {
800
- this.store.setBinding(scopeId, res.conversationId, cwd);
801
- this.syncCurrentBackendSettings(scopeId);
802
- }
803
- if (res.usage) {
804
- this.store.recordTokenUsage({
805
- inputTokens: res.usage.inputTokens,
806
- outputTokens: res.usage.outputTokens,
807
- cachedTokens: res.usage.cachedTokens,
808
- totalTokens: res.usage.totalTokens,
809
- }, adapter.id);
810
- }
811
- if (res.status === 'SUCCESS') {
812
- let finalText = (res.response || '').trim();
813
- if (!finalText && activeTurn.accumulatedText) {
814
- finalText = activeTurn.accumulatedText.trim();
815
- }
816
- const foldedTools = buildFoldedToolsSummary({
817
- stepIndex: activeTurn.stepIndex,
818
- toolLines: activeTurn.toolLines,
819
- toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
820
- startTime: activeTurn.startTime,
821
- usage: res.usage,
822
- locale,
823
- });
824
- const chunks = combineSummaryAndResponse(foldedTools, finalText || '(无输出 / No output)');
825
- if (chunks.length > 0) {
826
- await this.editMessage(scopeId, activeTurn.messageId, chunks[0]).catch(() => {
827
- return this.sendMessage(scopeId, chunks[0]);
828
- });
829
- for (let i = 1; i < chunks.length; i++) {
830
- await this.sendMessage(scopeId, chunks[i]);
831
- }
832
- }
833
- await this.drainNextQueuedTurn(event, locale);
834
- return;
835
- }
836
- if (res.status === 'ERROR') {
837
- const errorText = (res.error || res.response || res.error || 'Unknown error').trim();
838
- if (adapter.handleTurnError) {
839
- const handled = await adapter.handleTurnError({
840
- error: errorText,
841
- request: req,
842
- retryCount,
843
- retryTurn,
844
- sendMessage: (text) => this.sendMessage(scopeId, text),
845
- editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
846
- });
847
- if (handled)
848
- return;
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);
849
916
  }
850
- // If the agent actually produced substantive response text, do NOT mask it with an error box!
851
- let finalText = (res.response || '').trim();
852
- if (!finalText && activeTurn.accumulatedText) {
853
- finalText = activeTurn.accumulatedText.trim();
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);
854
924
  }
855
- const isPureError = !finalText ||
856
- finalText === errorText ||
857
- finalText.startsWith('Process exited with code') ||
858
- finalText.startsWith('Verification Required') ||
859
- finalText.startsWith('Error:') ||
860
- finalText.length <= 20;
861
- if (!isPureError) {
925
+ if (res.status === 'SUCCESS') {
926
+ let finalText = (res.response || '').trim();
927
+ if (!finalText && activeTurn.accumulatedText) {
928
+ finalText = activeTurn.accumulatedText.trim();
929
+ }
862
930
  const foldedTools = buildFoldedToolsSummary({
863
931
  stepIndex: activeTurn.stepIndex,
864
932
  toolLines: activeTurn.toolLines,
@@ -867,71 +935,155 @@ export class UnifiedChannelOrchestrator {
867
935
  usage: res.usage,
868
936
  locale,
869
937
  });
870
- const warningNote = errorText && errorText !== 'Unknown error' && errorText !== 'Antigravity execution failed'
871
- ? `\n\n⚠️ <i>(注意:任务结束时伴随提示: ${escapeTelegramHtml(errorText.slice(0, 120))})</i>`
872
- : '';
873
- const chunks = combineSummaryAndResponse(foldedTools, finalText + warningNote);
874
- if (chunks.length > 0) {
875
- await this.editMessage(scopeId, activeTurn.messageId, chunks[0]).catch(() => {
876
- return this.sendMessage(scopeId, chunks[0]);
877
- });
878
- for (let i = 1; i < chunks.length; i++) {
879
- await this.sendMessage(scopeId, chunks[i]);
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…`);
880
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;
881
981
  }
882
- await this.drainNextQueuedTurn(event, locale);
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(() => { });
883
1012
  return;
884
1013
  }
885
- await this.editMessage(scopeId, activeTurn.messageId, `❌ **${adapter.name} 错误**:\n\`\`\`\n${errorText}\n\`\`\``);
886
- await this.drainNextQueuedTurn(event, locale);
887
- return;
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) });
888
1019
  }
889
- await this.editMessage(scopeId, activeTurn.messageId, activeTurn.accumulatedText || '⚠️ 执行结束');
890
- await this.drainNextQueuedTurn(event, locale);
891
1020
  });
892
1021
  execution.on('error', async (err) => {
893
- if (activeTurn.flushTimer)
894
- clearTimeout(activeTurn.flushTimer);
895
- if (activeTurn.typingTimer)
896
- clearInterval(activeTurn.typingTimer);
897
- this.activeTurns.delete(scopeId);
898
- this.store.removeActiveTurnPreview(turnKey);
899
- if (adapter.handleTurnError) {
900
- const handled = await adapter.handleTurnError({
901
- error: err.message,
902
- request: req,
903
- retryCount,
904
- retryTurn,
905
- sendMessage: (text) => this.sendMessage(scopeId, text),
906
- editMessage: (messageId, text) => this.editMessage(scopeId, messageId, text),
907
- });
908
- if (handled)
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
+ }
909
1064
  return;
910
- }
911
- if (activeTurn.accumulatedText && activeTurn.accumulatedText.trim().length > 20) {
912
- const foldedTools = buildFoldedToolsSummary({
913
- stepIndex: activeTurn.stepIndex,
914
- toolLines: activeTurn.toolLines,
915
- toolCount: activeTurn.toolCount || activeTurn.toolLines.length,
916
- startTime: activeTurn.startTime,
917
- locale,
918
- });
919
- const fullAnswer = activeTurn.accumulatedText.trim() +
920
- `\n\n⚠️ <i>(注意:任务执行中途异常中断: ${escapeTelegramHtml(err.message.slice(0, 120))})</i>`;
921
- const chunks = combineSummaryAndResponse(foldedTools, fullAnswer);
922
- if (chunks.length > 0) {
923
- await this.editMessage(scopeId, activeTurn.messageId, chunks[0]).catch(() => {
924
- return this.sendMessage(scopeId, chunks[0]);
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,
925
1073
  });
926
- for (let i = 1; i < chunks.length; i++) {
927
- await this.sendMessage(scopeId, chunks[i]);
928
- }
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;
929
1080
  }
930
- await this.drainNextQueuedTurn(event, locale);
931
- return;
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) });
932
1086
  }
933
- await this.editMessage(scopeId, activeTurn.messageId, `❌ **执行异常**:\n\`\`\`\n${err.message}\n\`\`\``);
934
- await this.drainNextQueuedTurn(event, locale);
935
1087
  });
936
1088
  }
937
1089
  async handleEffortCommand(scopeId, args, locale) {
@@ -941,7 +1093,7 @@ export class UnifiedChannelOrchestrator {
941
1093
  if (target !== 'high' && settings?.serviceTier === 'boost') {
942
1094
  this.store.setChatServiceTier(scopeId, null);
943
1095
  }
944
- this.store.setChatSettings(scopeId, null, target);
1096
+ this.store.setChatEffort(scopeId, target);
945
1097
  this.syncCurrentBackendSettings(scopeId);
946
1098
  await this.sendMessage(scopeId, locale === 'zh' ? `✅ 思考深度已设置为: \`${target}\`` : `✅ Reasoning effort set to: \`${target}\``);
947
1099
  }
@@ -962,7 +1114,7 @@ export class UnifiedChannelOrchestrator {
962
1114
  nextBoost = false;
963
1115
  this.store.setChatServiceTier(scopeId, nextBoost ? 'boost' : null);
964
1116
  if (nextBoost) {
965
- this.store.setChatSettings(scopeId, null, 'high');
1117
+ this.store.setChatEffort(scopeId, 'high');
966
1118
  }
967
1119
  this.syncCurrentBackendSettings(scopeId);
968
1120
  const msg = locale === 'zh'