@keyou007/agent-bot 0.1.14 → 0.1.15-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +29 -24
  3. package/README.zh.md +29 -24
  4. package/config.example.yaml +1 -0
  5. package/dist/cli/FeishuAppConfiguration.js +42 -3
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli/help.js +6 -6
  8. package/dist/cli/initOptions.js +1 -4
  9. package/dist/cli/initOptions.js.map +1 -1
  10. package/dist/cli.js +8 -5
  11. package/dist/cli.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +91 -45
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/feishu/FeishuConnector.js +39 -12
  15. package/dist/feishu/FeishuConnector.js.map +1 -1
  16. package/dist/feishu/FeishuMarkdown.js +58 -6
  17. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  18. package/dist/feishu/FeishuMessageClient.js +108 -4
  19. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  20. package/dist/feishu/FeishuTurnPresenter.js +24 -1
  21. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  22. package/dist/feishu/FinalResponseBranding.js +15 -0
  23. package/dist/feishu/FinalResponseBranding.js.map +1 -0
  24. package/dist/feishu/InboundText.js +13 -0
  25. package/dist/feishu/InboundText.js.map +1 -0
  26. package/dist/feishu/MergedForwardMessage.js +317 -0
  27. package/dist/feishu/MergedForwardMessage.js.map +1 -0
  28. package/dist/logging/DailyLogStream.js +73 -0
  29. package/dist/logging/DailyLogStream.js.map +1 -0
  30. package/dist/logging/logger.js +2 -1
  31. package/dist/logging/logger.js.map +1 -1
  32. package/dist/presentation/OutboundRouter.js +21 -0
  33. package/dist/presentation/OutboundRouter.js.map +1 -1
  34. package/dist/proxy/ProxySessionController.js +536 -58
  35. package/dist/proxy/ProxySessionController.js.map +1 -1
  36. package/dist/state/StateStore.js +161 -3
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/state/migrations.js +12 -0
  39. package/dist/state/migrations.js.map +1 -1
  40. package/dist/supervision/SupervisorDiagnostics.js +37 -21
  41. package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
  42. package/dist/supervisor.js +11 -4
  43. package/dist/supervisor.js.map +1 -1
  44. package/dist/utils/executeShellCommand.js +83 -16
  45. package/dist/utils/executeShellCommand.js.map +1 -1
  46. package/docs/technical-reference.md +9 -8
  47. package/docs/technical-reference.zh.md +9 -8
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
  50. package/skills/agent-bot/SKILL.md +1 -1
  51. package/src/cli/FeishuAppConfiguration.ts +76 -3
  52. package/src/cli/help.ts +6 -6
  53. package/src/cli/initOptions.ts +1 -10
  54. package/src/cli.ts +10 -6
  55. package/src/feishu/CardRenderer.ts +116 -45
  56. package/src/feishu/FeishuConnector.ts +55 -13
  57. package/src/feishu/FeishuMarkdown.ts +64 -5
  58. package/src/feishu/FeishuMessageClient.ts +151 -12
  59. package/src/feishu/FeishuTurnPresenter.ts +28 -4
  60. package/src/feishu/FinalResponseBranding.ts +18 -0
  61. package/src/feishu/InboundText.ts +12 -0
  62. package/src/feishu/MergedForwardMessage.ts +372 -0
  63. package/src/feishu/types.ts +15 -0
  64. package/src/logging/DailyLogStream.ts +88 -0
  65. package/src/logging/logger.ts +2 -1
  66. package/src/presentation/OutboundRouter.ts +28 -0
  67. package/src/proxy/ProxySessionController.ts +642 -55
  68. package/src/state/StateStore.ts +230 -2
  69. package/src/state/migrations.ts +12 -0
  70. package/src/supervision/SupervisorDiagnostics.ts +41 -25
  71. package/src/supervisor.ts +10 -4
  72. package/src/utils/executeShellCommand.ts +103 -15
@@ -13,6 +13,7 @@ import type {
13
13
  ChatUpdatedEvent,
14
14
  IncomingMessage,
15
15
  MessageReplyTarget,
16
+ ReferencedMessageContent,
16
17
  } from "../feishu/types.js";
17
18
  import {
18
19
  CardRenderer,
@@ -23,9 +24,12 @@ import {
23
24
  type ResetHistoryCardEntry,
24
25
  type SessionTaskCardEntry,
25
26
  type SessionTaskCardGroup,
27
+ type ShellCommandCardView,
26
28
  type TaskListCardAction,
27
29
  } from "../feishu/CardRenderer.js";
30
+ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
28
31
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
32
+ import { normalizeFeishuPostText } from "../feishu/InboundText.js";
29
33
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
30
34
  import type { OutboundRouter } from "../presentation/OutboundRouter.js";
31
35
  import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
@@ -52,10 +56,12 @@ import {
52
56
  type TurnAnchorRecord,
53
57
  } from "../state/StateStore.js";
54
58
  import { createId } from "../utils/id.js";
55
- import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
59
+ import { truncateMiddle, truncateText } from "../utils/markdown.js";
56
60
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
57
61
  import {
58
62
  executeShellCommand,
63
+ type ShellCommandOptions,
64
+ type ShellCommandOutputSnapshot,
59
65
  type ShellCommandResult,
60
66
  } from "../utils/executeShellCommand.js";
61
67
 
@@ -72,6 +78,20 @@ interface StartTurnOptions {
72
78
  preserveAttemptOnFailure?: boolean;
73
79
  }
74
80
 
81
+ interface PendingForwardAttachment {
82
+ contextKey: string;
83
+ attachmentMessageId?: string;
84
+ attachmentPromise: Promise<IncomingMessage | undefined>;
85
+ resolveAttachment: (message: IncomingMessage | undefined) => void;
86
+ }
87
+
88
+ interface ForwardAttachmentReservation {
89
+ sourceMessageId: string;
90
+ sourceKind: "merged_forward" | "resource";
91
+ pending: PendingForwardAttachment;
92
+ registry: Map<string, PendingForwardAttachment>;
93
+ }
94
+
75
95
  const MESSAGE_RECEIVED_REACTION = "OnIt";
76
96
  const MESSAGE_COMPLETED_REACTION = "DONE";
77
97
  const MESSAGE_FAILED_REACTION = "ERROR";
@@ -82,6 +102,10 @@ const RESET_HISTORY_PAGE_SIZE = 10;
82
102
  const MAX_LLM_TURN_RETRIES = 3;
83
103
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
84
104
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
105
+ const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
106
+ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
107
+ const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
108
+ const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
85
109
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
86
110
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
87
111
  const IMAGE_FILE_EXTENSIONS = new Set([
@@ -347,7 +371,11 @@ export interface ProxyLifecycle {
347
371
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
348
372
  }
349
373
 
350
- export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
374
+ export type ShellCommandExecutor = (
375
+ command: string,
376
+ cwd: string,
377
+ options?: ShellCommandOptions,
378
+ ) => Promise<ShellCommandResult>;
351
379
  export interface WindowsDriveInfo {
352
380
  root: string;
353
381
  label?: string;
@@ -370,6 +398,9 @@ export class ProxySessionController {
370
398
  private readonly llmRetryingSessions = new Set<string>();
371
399
  private readonly retriedFailureTurnIds = new Set<string>();
372
400
  private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
401
+ private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
402
+ private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
403
+ private readonly relatedReactionMessageIds = new Map<string, string[]>();
373
404
  private readonly unsubscribe: Array<() => void> = [];
374
405
  private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
375
406
  private startupRecovery?: Promise<void>;
@@ -406,6 +437,9 @@ export class ProxySessionController {
406
437
  }
407
438
 
408
439
  async onMessage(message: IncomingMessage): Promise<void> {
440
+ let forwardedImages: Array<{ messageId: string; imageKey: string }> = [];
441
+ let forwardedFiles: Array<{ messageId: string; fileKey: string; fileName: string }> = [];
442
+ let coalescedForwardAttachment = false;
409
443
  if (!message.contextKey.startsWith("console:") && !allowsFeishuUser(this.config, message.userId)) {
410
444
  this.logger.debug(
411
445
  { messageId: message.messageId },
@@ -427,6 +461,15 @@ export class ProxySessionController {
427
461
  // For accepted messages, claim durable deduplication before acknowledgement so event
428
462
  // retries cannot add duplicate reactions.
429
463
  if (!this.store.claimInboundEvent(message.messageId, "message")) return;
464
+ const attachmentReservation = message.mergedForwardMessageId
465
+ ? undefined
466
+ : this.reserveForwardAttachment(message);
467
+ const pendingMergedForward = message.mergedForwardMessageId
468
+ ? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
469
+ : undefined;
470
+ const pendingResourceForward = !attachmentReservation && isStandaloneResourceMessage(message)
471
+ ? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
472
+ : undefined;
430
473
  try {
431
474
  const reactionId = await this.outbound.addReaction(
432
475
  message.contextKey,
@@ -442,6 +485,13 @@ export class ProxySessionController {
442
485
  "Failed to acknowledge the incoming Feishu message with a reaction.",
443
486
  );
444
487
  }
488
+ if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
489
+ this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
490
+ messageId: message.messageId,
491
+ parentMessageId: message.parentMessageId,
492
+ });
493
+ return;
494
+ }
445
495
  if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
446
496
  try {
447
497
  await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
@@ -457,20 +507,165 @@ export class ProxySessionController {
457
507
  this.store.recordChatContext(chatContextKey, message.chatType);
458
508
  this.store.markChatActive(chatContextKey);
459
509
  }
510
+ if (pendingResourceForward) {
511
+ const resourceMessage = message;
512
+ const attachment = await this.waitForForwardAttachment(
513
+ message.messageId,
514
+ pendingResourceForward,
515
+ this.pendingResourceForwards,
516
+ );
517
+ if (attachment) {
518
+ coalescedForwardAttachment = true;
519
+ const instruction = normalizeFeishuPostText(attachment.text) || defaultResourcePrompt(resourceMessage);
520
+ forwardedImages = (resourceMessage.images ?? []).map((image) => ({
521
+ messageId: resourceMessage.messageId,
522
+ imageKey: image.imageKey,
523
+ }));
524
+ forwardedFiles = (resourceMessage.files ?? []).map((file) => ({
525
+ messageId: resourceMessage.messageId,
526
+ fileKey: file.fileKey,
527
+ fileName: file.fileName,
528
+ }));
529
+ message = {
530
+ ...attachment,
531
+ text: instruction,
532
+ displayText: instruction,
533
+ };
534
+ this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
535
+ }
536
+ }
537
+ if (message.mergedForwardMessageId) {
538
+ const mergedForwardMessage = message;
539
+ const mergedForwardResult = this.outbound.readMergedForward(
540
+ message.contextKey,
541
+ message.mergedForwardMessageId,
542
+ ).then(
543
+ (content) => ({ content } as const),
544
+ (error: unknown) => ({ error } as const),
545
+ );
546
+ const attachment = pendingMergedForward
547
+ ? await this.waitForForwardAttachment(
548
+ message.messageId,
549
+ pendingMergedForward,
550
+ this.pendingMergedForwards,
551
+ )
552
+ : undefined;
553
+ if (attachment) {
554
+ coalescedForwardAttachment = true;
555
+ const instruction = normalizeFeishuPostText(attachment.text)
556
+ || ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
557
+ message = {
558
+ ...attachment,
559
+ text: instruction,
560
+ displayText: instruction,
561
+ };
562
+ this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
563
+ }
564
+ const result = await mergedForwardResult;
565
+ if ("error" in result) {
566
+ const replyTarget = message.replyInThread
567
+ ? { messageId: message.messageId, replyInThread: true as const }
568
+ : undefined;
569
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
570
+ const detail = result.error instanceof Error ? result.error.message : String(result.error);
571
+ await this.outbound.withReplyTarget(
572
+ message.contextKey,
573
+ replyTarget,
574
+ () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)),
575
+ );
576
+ return;
577
+ }
578
+ const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
579
+ forwardedImages = result.content.images;
580
+ forwardedFiles = result.content.files;
581
+ message = {
582
+ ...message,
583
+ displayText: instruction,
584
+ text: mergedForwardPrompt(instruction, result.content.text),
585
+ };
586
+ }
587
+ const referencedMessageId = coalescedForwardAttachment ? undefined : quotedMessageId(message);
588
+ if (referencedMessageId) {
589
+ try {
590
+ const referenced = await this.resolveReferencedMessage(message.contextKey, referencedMessageId);
591
+ const instruction = normalizeFeishuPostText(message.displayText ?? message.text)
592
+ || DEFAULT_REFERENCED_MESSAGE_INSTRUCTION;
593
+ forwardedImages = [...referenced.images, ...forwardedImages];
594
+ forwardedFiles = [...referenced.files, ...forwardedFiles];
595
+ message = {
596
+ ...message,
597
+ displayText: instruction,
598
+ text: referencedMessagePrompt(instruction, referenced.text),
599
+ };
600
+ } catch (error) {
601
+ const replyTarget = message.replyInThread
602
+ ? { messageId: message.messageId, replyInThread: true as const }
603
+ : undefined;
604
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
605
+ const detail = error instanceof Error ? error.message : String(error);
606
+ await this.outbound.withReplyTarget(
607
+ message.contextKey,
608
+ replyTarget,
609
+ () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)),
610
+ );
611
+ return;
612
+ }
613
+ }
460
614
  const replyTarget = message.replyInThread
461
615
  ? { messageId: message.messageId, replyInThread: true as const }
462
616
  : undefined;
463
- const imageCount = message.images?.length ?? 0;
617
+ const imageReferences = deduplicateImageReferences([
618
+ ...forwardedImages,
619
+ ...(message.images ?? []).map((image) => ({ messageId: message.messageId, imageKey: image.imageKey })),
620
+ ]);
621
+ const fileReferences = deduplicateFileReferences([
622
+ ...forwardedFiles,
623
+ ...(message.files ?? []).map((file) => ({
624
+ messageId: message.messageId,
625
+ fileKey: file.fileKey,
626
+ fileName: file.fileName,
627
+ })),
628
+ ]);
629
+ const imageCount = imageReferences.length;
630
+ const fileCount = fileReferences.length;
464
631
  this.store.audit(message.contextKey, "incoming_message", {
465
632
  messageId: message.messageId,
466
633
  text: message.text,
467
634
  ...(imageCount > 0 ? { imageCount } : {}),
635
+ ...(fileCount > 0 ? { fileCount } : {}),
468
636
  });
469
637
  let localImagePaths: string[] | undefined;
470
638
  if (imageCount > 0) {
471
639
  try {
472
- localImagePaths = await Promise.all(message.images!.map((image) =>
473
- this.outbound.downloadImage(message.contextKey, message.messageId, image.imageKey)));
640
+ localImagePaths = await Promise.all(imageReferences.map((image) =>
641
+ this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
642
+ } catch (error) {
643
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
644
+ await this.outbound.withReplyTarget(
645
+ message.contextKey,
646
+ replyTarget,
647
+ () => this.sendError(message.contextKey, error),
648
+ );
649
+ return;
650
+ }
651
+ }
652
+ if (fileCount > 0) {
653
+ try {
654
+ const downloadedFiles = await Promise.all(fileReferences.map(async (file) => ({
655
+ fileName: file.fileName,
656
+ filePath: await this.outbound.downloadFile(
657
+ message.contextKey,
658
+ file.messageId,
659
+ file.fileKey,
660
+ file.fileName,
661
+ ),
662
+ })));
663
+ const instruction = message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount);
664
+ message = {
665
+ ...message,
666
+ displayText: message.displayText ?? instruction,
667
+ text: appendDownloadedFiles(instruction, downloadedFiles),
668
+ };
474
669
  } catch (error) {
475
670
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
476
671
  await this.outbound.withReplyTarget(
@@ -483,8 +678,9 @@ export class ProxySessionController {
483
678
  }
484
679
  let command: Command;
485
680
  try {
486
- command = imageCount > 0 && !message.text.trimStart().startsWith("/")
487
- ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
681
+ const resourceCount = imageCount + fileCount;
682
+ command = resourceCount > 0 && !message.text.trimStart().startsWith("/")
683
+ ? { type: "prompt", text: message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount) }
488
684
  : this.router.parse(message.text);
489
685
  } catch (error) {
490
686
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
@@ -499,7 +695,7 @@ export class ProxySessionController {
499
695
  if (isQueueIndependentCommand(command)) {
500
696
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
501
697
  try {
502
- if (command.type !== "forkgroup" && command.type !== "dismiss") await this.ensureThreadFork(message);
698
+ if (command.type === "prompt") await this.ensureThreadFork(message);
503
699
  await this.execute(
504
700
  message.contextKey,
505
701
  command,
@@ -522,7 +718,7 @@ export class ProxySessionController {
522
718
  const next = previous.catch(() => undefined).then(() =>
523
719
  this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
524
720
  try {
525
- if (command.type !== "forkgroup") await this.ensureThreadFork(message);
721
+ if (command.type === "prompt") await this.ensureThreadFork(message);
526
722
  await this.execute(
527
723
  message.contextKey,
528
724
  command,
@@ -818,6 +1014,11 @@ export class ProxySessionController {
818
1014
  this.queuedPromptCards.clear();
819
1015
  this.queuedPromptCardWrites.clear();
820
1016
  this.sessionResets.clear();
1017
+ for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
1018
+ this.pendingMergedForwards.clear();
1019
+ for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
1020
+ this.pendingResourceForwards.clear();
1021
+ this.relatedReactionMessageIds.clear();
821
1022
  for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
822
1023
  this.recoveryRetryTimers.clear();
823
1024
  clearInterval(this.recoveryActivityHeartbeat);
@@ -1296,8 +1497,7 @@ export class ProxySessionController {
1296
1497
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1297
1498
  throw new Error("The task is not an App Server task that can be reset.");
1298
1499
  }
1299
- const contextKey = this.controlSessionContextKey(session);
1300
- const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
1500
+ const rows = this.store.listTaskTurnGraph(session.localSessionId);
1301
1501
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1302
1502
  return {
1303
1503
  session,
@@ -1449,6 +1649,9 @@ export class ProxySessionController {
1449
1649
  incomingMessage?: IncomingMessage,
1450
1650
  ): Promise<void> {
1451
1651
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
1652
+ if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
1653
+ throw new Error(unboundThreadTaskMessage());
1654
+ }
1452
1655
  switch (command.type) {
1453
1656
  case "shell":
1454
1657
  await this.runShellCommand(contextKey, command.command);
@@ -1499,7 +1702,14 @@ export class ProxySessionController {
1499
1702
  await this.setTitle(contextKey, command.title);
1500
1703
  return;
1501
1704
  case "prompt":
1502
- await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
1705
+ await this.prompt(
1706
+ contextKey,
1707
+ command.text,
1708
+ messageId,
1709
+ replyTarget,
1710
+ localImagePaths,
1711
+ incomingMessage?.displayText,
1712
+ );
1503
1713
  return;
1504
1714
  case "nosteer":
1505
1715
  await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
@@ -1986,6 +2196,7 @@ export class ProxySessionController {
1986
2196
  messageId?: string,
1987
2197
  replyTarget?: MessageReplyTarget,
1988
2198
  localImagePaths?: string[],
2199
+ displayPrompt?: string,
1989
2200
  ): Promise<void> {
1990
2201
  if (!text.trim()) throw new Error("请输入要交给 Agent 的内容。");
1991
2202
  let record = this.currentSession(contextKey);
@@ -1997,11 +2208,11 @@ export class ProxySessionController {
1997
2208
  this.inheritedNewTaskCwd(contextKey),
1998
2209
  false,
1999
2210
  true,
2000
- text,
2211
+ displayPrompt ?? text,
2001
2212
  replyTarget,
2002
2213
  );
2003
2214
  }
2004
- await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
2215
+ await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
2005
2216
  }
2006
2217
 
2007
2218
  private async promptSession(
@@ -2011,12 +2222,14 @@ export class ProxySessionController {
2011
2222
  messageId?: string,
2012
2223
  replyTarget?: MessageReplyTarget,
2013
2224
  localImagePaths?: string[],
2225
+ displayPrompt?: string,
2014
2226
  ): Promise<void> {
2015
2227
  if (this.llmRetryingSessions.has(record.localSessionId)) {
2016
2228
  const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
2017
2229
  localImagePaths,
2018
2230
  messageId,
2019
2231
  replyTarget,
2232
+ displayPrompt,
2020
2233
  });
2021
2234
  this.store.audit(contextKey, "queued_prompt_added_during_llm_retry", {
2022
2235
  promptId: queued.promptId,
@@ -2034,7 +2247,13 @@ export class ProxySessionController {
2034
2247
  record.cwd,
2035
2248
  this.agentLabel(record.agentName),
2036
2249
  );
2037
- await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
2250
+ await this.outbound.startPendingTurn(
2251
+ record.localSessionId,
2252
+ contextKey,
2253
+ record.title,
2254
+ replyTarget,
2255
+ displayPrompt ?? text,
2256
+ );
2038
2257
  }
2039
2258
  let loaded: LoadedSession;
2040
2259
  try {
@@ -2065,12 +2284,13 @@ export class ProxySessionController {
2065
2284
  localImagePaths,
2066
2285
  messageId,
2067
2286
  replyTarget,
2287
+ displayPrompt,
2068
2288
  });
2069
2289
  return;
2070
2290
  }
2071
2291
  try {
2072
2292
  await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
2073
- await this.presentSteerMessage(record.localSessionId, activeTurnId, text, messageId);
2293
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
2074
2294
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
2075
2295
  return;
2076
2296
  } catch (error) {
@@ -2082,7 +2302,7 @@ export class ProxySessionController {
2082
2302
  this.logger.warn({ error: syncError, sessionId: record.localSessionId }, "Failed to synchronize after steering failure.");
2083
2303
  }
2084
2304
  if (!current?.activeTurnId) {
2085
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
2305
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
2086
2306
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
2087
2307
  return;
2088
2308
  }
@@ -2093,7 +2313,12 @@ export class ProxySessionController {
2093
2313
  current.activeTurnId,
2094
2314
  runtimePrompt(text, localImagePaths),
2095
2315
  );
2096
- await this.presentSteerMessage(record.localSessionId, current.activeTurnId, text, messageId);
2316
+ await this.presentSteerMessage(
2317
+ record.localSessionId,
2318
+ current.activeTurnId,
2319
+ displayPrompt ?? text,
2320
+ messageId,
2321
+ );
2097
2322
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
2098
2323
  return;
2099
2324
  } catch (retryError) {
@@ -2107,11 +2332,12 @@ export class ProxySessionController {
2107
2332
  localImagePaths,
2108
2333
  messageId,
2109
2334
  replyTarget,
2335
+ displayPrompt,
2110
2336
  });
2111
2337
  return;
2112
2338
  }
2113
2339
  }
2114
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
2340
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
2115
2341
  if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
2116
2342
  }
2117
2343
 
@@ -2156,6 +2382,7 @@ export class ProxySessionController {
2156
2382
  localImagePaths?: string[];
2157
2383
  messageId?: string;
2158
2384
  replyTarget?: MessageReplyTarget;
2385
+ displayPrompt?: string;
2159
2386
  } = {},
2160
2387
  ): QueuedPromptRecord {
2161
2388
  return this.store.enqueuePrompt({
@@ -2166,6 +2393,7 @@ export class ProxySessionController {
2166
2393
  localImagePaths: options.localImagePaths,
2167
2394
  messageId: options.messageId,
2168
2395
  replyMessageId: options.replyTarget?.messageId,
2396
+ displayPrompt: options.displayPrompt,
2169
2397
  });
2170
2398
  }
2171
2399
 
@@ -2227,7 +2455,7 @@ export class ProxySessionController {
2227
2455
  contextKey,
2228
2456
  prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
2229
2457
  id: prompt.promptId,
2230
- text: prompt.text,
2458
+ text: prompt.displayPrompt ?? prompt.text,
2231
2459
  })),
2232
2460
  });
2233
2461
  }
@@ -2729,7 +2957,7 @@ export class ProxySessionController {
2729
2957
  options: StartTurnOptions = {},
2730
2958
  ): Promise<string> {
2731
2959
  const currentRecord = this.store.getSession(loaded.record.localSessionId);
2732
- const title = currentRecord?.title ?? normalizeTaskTitle(text);
2960
+ const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
2733
2961
  if (!currentRecord?.title && title) this.store.updateRuntimeSession(loaded.record.localSessionId, { title });
2734
2962
  if (title) this.outbound.updateSessionTitle(loaded.record.localSessionId, title);
2735
2963
  const attemptId = options.attemptId ?? createId("attempt");
@@ -3091,7 +3319,13 @@ export class ProxySessionController {
3091
3319
  const existing = runtime.getSession(record.localSessionId);
3092
3320
  if (existing) return { record, runtime, session: existing };
3093
3321
  const pending = this.sessionLoads.get(record.localSessionId);
3094
- if (pending) return pending;
3322
+ if (pending) {
3323
+ const loaded = await pending;
3324
+ return {
3325
+ ...loaded,
3326
+ record: { ...loaded.record, contextKey: record.contextKey },
3327
+ };
3328
+ }
3095
3329
 
3096
3330
  this.outbound.registerSession(
3097
3331
  record.localSessionId,
@@ -3160,7 +3394,11 @@ export class ProxySessionController {
3160
3394
  }
3161
3395
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3162
3396
  const saved = this.store.getSession(record.localSessionId) ?? record;
3163
- return { record: saved, runtime, session };
3397
+ return {
3398
+ record: { ...saved, contextKey: record.contextKey },
3399
+ runtime,
3400
+ session,
3401
+ };
3164
3402
  })();
3165
3403
  this.sessionLoads.set(record.localSessionId, loading);
3166
3404
  try {
@@ -3259,6 +3497,7 @@ export class ProxySessionController {
3259
3497
  }
3260
3498
 
3261
3499
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
3500
+ await this.refreshGoalCards(event.sessionId);
3262
3501
  const terminalStatus = event.type === "turn_completed"
3263
3502
  ? "completed"
3264
3503
  : event.type === "turn_cancelled"
@@ -3392,7 +3631,7 @@ export class ProxySessionController {
3392
3631
  prompt.text,
3393
3632
  prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined,
3394
3633
  prompt.localImagePaths,
3395
- { messageId: prompt.messageId },
3634
+ { messageId: prompt.messageId, displayPrompt: prompt.displayPrompt },
3396
3635
  );
3397
3636
  if (prompt.messageId) await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
3398
3637
  } catch (error) {
@@ -3418,13 +3657,17 @@ export class ProxySessionController {
3418
3657
  messageId: string,
3419
3658
  status: "completed" | "failed" | "cancelled",
3420
3659
  ): Promise<void> {
3421
- const reaction = this.store.claimMessageReaction(messageId);
3422
- if (reaction) await this.replaceMessageReaction(reaction, status);
3660
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
3661
+ const reaction = this.store.claimMessageReaction(relatedMessageId);
3662
+ if (reaction) await this.replaceMessageReaction(reaction, status);
3663
+ }
3423
3664
  }
3424
3665
 
3425
3666
  private async bindMessageReactionToTurn(messageId: string, sessionId: string, turnId: string): Promise<void> {
3426
- this.store.bindMessageToTurn(messageId, sessionId, turnId);
3427
- this.store.bindMessageReaction(messageId, sessionId, turnId);
3667
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
3668
+ this.store.bindMessageToTurn(relatedMessageId, sessionId, turnId);
3669
+ this.store.bindMessageReaction(relatedMessageId, sessionId, turnId);
3670
+ }
3428
3671
  const session = this.store.getSession(sessionId);
3429
3672
  if (session?.lastTurnId !== turnId) return;
3430
3673
  if (session.lastTurnStatus === "completed" || session.lastTurnStatus === "failed" || session.lastTurnStatus === "cancelled") {
@@ -3432,6 +3675,101 @@ export class ProxySessionController {
3432
3675
  }
3433
3676
  }
3434
3677
 
3678
+ private registerPendingForwardAttachment(
3679
+ message: IncomingMessage,
3680
+ registry: Map<string, PendingForwardAttachment>,
3681
+ ): PendingForwardAttachment {
3682
+ let resolveAttachment!: (message: IncomingMessage | undefined) => void;
3683
+ const attachmentPromise = new Promise<IncomingMessage | undefined>((resolve) => {
3684
+ resolveAttachment = resolve;
3685
+ });
3686
+ const pending = {
3687
+ contextKey: message.contextKey,
3688
+ attachmentPromise,
3689
+ resolveAttachment,
3690
+ };
3691
+ registry.set(message.messageId, pending);
3692
+ return pending;
3693
+ }
3694
+
3695
+ private reserveForwardAttachment(message: IncomingMessage): ForwardAttachmentReservation | undefined {
3696
+ if (!message.parentMessageId) return undefined;
3697
+ const candidates = [
3698
+ { sourceKind: "merged_forward" as const, registry: this.pendingMergedForwards },
3699
+ { sourceKind: "resource" as const, registry: this.pendingResourceForwards },
3700
+ ];
3701
+ for (const candidate of candidates) {
3702
+ const pending = candidate.registry.get(message.parentMessageId);
3703
+ if (!pending || pending.contextKey !== message.contextKey || pending.attachmentMessageId) continue;
3704
+ pending.attachmentMessageId = message.messageId;
3705
+ return {
3706
+ sourceMessageId: message.parentMessageId,
3707
+ sourceKind: candidate.sourceKind,
3708
+ pending,
3709
+ registry: candidate.registry,
3710
+ };
3711
+ }
3712
+ return undefined;
3713
+ }
3714
+
3715
+ private completeForwardAttachment(
3716
+ message: IncomingMessage,
3717
+ reservation: ForwardAttachmentReservation,
3718
+ ): boolean {
3719
+ if (!message.parentMessageId) return false;
3720
+ if (reservation.sourceMessageId !== message.parentMessageId) return false;
3721
+ if (reservation.registry.get(message.parentMessageId) !== reservation.pending) return false;
3722
+ if (reservation.pending.attachmentMessageId !== message.messageId) return false;
3723
+ reservation.pending.resolveAttachment(message);
3724
+ return true;
3725
+ }
3726
+
3727
+ private async waitForForwardAttachment(
3728
+ messageId: string,
3729
+ pending: PendingForwardAttachment,
3730
+ registry: Map<string, PendingForwardAttachment>,
3731
+ ): Promise<IncomingMessage | undefined> {
3732
+ let timer: NodeJS.Timeout | undefined;
3733
+ const timeout = new Promise<undefined>((resolve) => {
3734
+ timer = setTimeout(() => resolve(undefined), FORWARD_ATTACHMENT_WINDOW_MS);
3735
+ timer.unref?.();
3736
+ });
3737
+ try {
3738
+ return await Promise.race([pending.attachmentPromise, timeout]);
3739
+ } finally {
3740
+ if (timer) clearTimeout(timer);
3741
+ if (registry.get(messageId) === pending) {
3742
+ registry.delete(messageId);
3743
+ }
3744
+ }
3745
+ }
3746
+
3747
+ private async resolveReferencedMessage(
3748
+ contextKey: string,
3749
+ messageId: string,
3750
+ ): Promise<ReferencedMessageContent> {
3751
+ const local = this.store.findAgentBotTurnMessageById(messageId);
3752
+ if (local) {
3753
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(local.turnId));
3754
+ const text = snapshot ? renderLocalTurnReference(snapshot, local.messageKind) : undefined;
3755
+ if (text) {
3756
+ return {
3757
+ text,
3758
+ messageType: local.messageKind === "final" ? "agent_bot_reply" : "agent_bot_progress",
3759
+ images: [],
3760
+ files: [],
3761
+ };
3762
+ }
3763
+ }
3764
+ return this.outbound.readReferencedMessage(contextKey, messageId);
3765
+ }
3766
+
3767
+ private takeRelatedReactionMessageIds(messageId: string): string[] {
3768
+ const related = this.relatedReactionMessageIds.get(messageId) ?? [];
3769
+ this.relatedReactionMessageIds.delete(messageId);
3770
+ return [...new Set([messageId, ...related])];
3771
+ }
3772
+
3435
3773
  private async finalizeTurnMessageReactions(
3436
3774
  turnId: string,
3437
3775
  status: "completed" | "failed" | "cancelled",
@@ -3567,8 +3905,10 @@ export class ProxySessionController {
3567
3905
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3568
3906
  }
3569
3907
 
3570
- const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
3571
- if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
3908
+ const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3909
+ .find((turn) => turn.turnId === turnId);
3910
+ const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3911
+ if (!snapshot || snapshot.status !== "completed") {
3572
3912
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3573
3913
  }
3574
3914
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
@@ -3589,7 +3929,7 @@ export class ProxySessionController {
3589
3929
  }
3590
3930
 
3591
3931
  const origin = this.store.getTurnRuntimeOrigin(turnId);
3592
- if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
3932
+ if (origin && origin.agentName !== current.agentName) {
3593
3933
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
3594
3934
  }
3595
3935
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -4053,6 +4393,25 @@ export class ProxySessionController {
4053
4393
  notice?: string,
4054
4394
  record = this.currentSession(contextKey),
4055
4395
  ): Promise<void> {
4396
+ const card = this.renderGoalCard(goal, notice, record);
4397
+ if (!record) {
4398
+ await this.outbound.sendInteractiveCard(contextKey, card);
4399
+ return;
4400
+ }
4401
+ const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
4402
+ if (existing) {
4403
+ await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
4404
+ return;
4405
+ }
4406
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
4407
+ if (messageId) this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
4408
+ }
4409
+
4410
+ private renderGoalCard(
4411
+ goal: RuntimeGoal | undefined,
4412
+ notice?: string,
4413
+ record?: SessionRecord,
4414
+ ): Record<string, unknown> {
4056
4415
  const sections: CardSection[] = [];
4057
4416
  if (notice) sections.push({ lines: [cardText(notice)] });
4058
4417
  sections.push(goal
@@ -4074,24 +4433,138 @@ export class ProxySessionController {
4074
4433
  "**/goal edit &#60;新目标&#62;** 修改  **/goal clear** 清除",
4075
4434
  ],
4076
4435
  });
4077
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Goal", sections));
4436
+ const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
4437
+ return this.cardRenderer.renderSectionsCard(title, sections);
4438
+ }
4439
+
4440
+ private async refreshGoalCards(localSessionId: string): Promise<void> {
4441
+ const deliveries = this.store.listGoalCardDeliveries(localSessionId);
4442
+ if (deliveries.length === 0) return;
4443
+ const record = this.store.getSession(localSessionId);
4444
+ if (!record) return;
4445
+ const runtime = this.runtimes.forAgent(record.agentName);
4446
+ if (runtime.kind !== "codex" || !runtime.getGoal) return;
4447
+
4448
+ try {
4449
+ const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
4450
+ const goal = await runtime.getGoal(loaded.localSessionId);
4451
+ const card = this.renderGoalCard(goal, undefined, record);
4452
+ await Promise.all(deliveries.map(async (delivery) => {
4453
+ try {
4454
+ await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
4455
+ } catch (error) {
4456
+ this.logger.warn(
4457
+ { error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId },
4458
+ "Failed to refresh an Agent Goal card.",
4459
+ );
4460
+ }
4461
+ }));
4462
+ } catch (error) {
4463
+ this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
4464
+ }
4078
4465
  }
4079
4466
 
4080
4467
  private async runShellCommand(contextKey: string, command: string): Promise<void> {
4081
4468
  const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
4082
- const result = await this.shellCommandExecutor(command, cwd);
4083
- const outputParts = [
4084
- result.stdout.trimEnd(),
4085
- result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
4086
- ].filter(Boolean);
4087
- const output = truncateMiddle(outputParts.join("\n"), 6_000);
4088
- const status = result.timedOut
4089
- ? "已超时(120s)"
4090
- : `退出码 ${result.exitCode ?? "未知"}`;
4091
- await this.outbound.sendMarkdown(contextKey, [
4092
- codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
4093
- `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
4094
- ].join("\n"));
4469
+ const startedAt = Date.now();
4470
+ let latestOutput: ShellCommandOutputSnapshot = {
4471
+ stdout: "",
4472
+ stderr: "",
4473
+ outputTruncated: false,
4474
+ };
4475
+ let runningState: ShellCommandCardView = {
4476
+ command,
4477
+ cwd,
4478
+ ...latestOutput,
4479
+ status: "running",
4480
+ elapsedMs: 0,
4481
+ };
4482
+ const cardMessageId = await this.outbound.sendInteractiveCard(
4483
+ contextKey,
4484
+ this.cardRenderer.renderShellCommandCard(runningState),
4485
+ );
4486
+ const scheduler = cardMessageId
4487
+ ? new CardUpdateScheduler<ShellCommandCardView>({
4488
+ render: (state) => this.cardRenderer.renderShellCommandCard(state),
4489
+ write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
4490
+ onError: (error) => this.logger.warn(
4491
+ { error, contextKey, messageId: cardMessageId },
4492
+ "Failed to update a shell command card.",
4493
+ ),
4494
+ })
4495
+ : undefined;
4496
+ scheduler?.seed(runningState);
4497
+ const elapsedTimer = scheduler ? setInterval(() => {
4498
+ runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
4499
+ scheduler.update(runningState);
4500
+ }, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
4501
+ elapsedTimer?.unref?.();
4502
+
4503
+ const updateRunningOutput = (snapshot: ShellCommandOutputSnapshot): void => {
4504
+ latestOutput = snapshot;
4505
+ runningState = {
4506
+ ...runningState,
4507
+ ...snapshot,
4508
+ elapsedMs: Date.now() - startedAt,
4509
+ };
4510
+ scheduler?.update(runningState);
4511
+ };
4512
+
4513
+ let result: ShellCommandResult;
4514
+ try {
4515
+ result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
4516
+ } catch (error) {
4517
+ if (elapsedTimer) clearInterval(elapsedTimer);
4518
+ const failedCard = this.cardRenderer.renderShellCommandCard({
4519
+ command,
4520
+ cwd,
4521
+ ...latestOutput,
4522
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4523
+ status: "failed",
4524
+ elapsedMs: Date.now() - startedAt,
4525
+ });
4526
+ if (scheduler) {
4527
+ try {
4528
+ await scheduler.flush({
4529
+ ...runningState,
4530
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4531
+ status: "failed",
4532
+ elapsedMs: Date.now() - startedAt,
4533
+ });
4534
+ } catch (cardError) {
4535
+ this.logger.warn(
4536
+ { error: cardError, contextKey, messageId: cardMessageId },
4537
+ "Failed to finalize a shell command card.",
4538
+ );
4539
+ } finally {
4540
+ scheduler.dispose();
4541
+ }
4542
+ } else {
4543
+ await this.outbound.sendInteractiveCard(contextKey, failedCard);
4544
+ }
4545
+ throw error;
4546
+ }
4547
+
4548
+ if (elapsedTimer) clearInterval(elapsedTimer);
4549
+ const finalState: ShellCommandCardView = {
4550
+ command,
4551
+ cwd,
4552
+ stdout: result.stdout,
4553
+ stderr: result.stderr,
4554
+ status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
4555
+ exitCode: result.exitCode,
4556
+ elapsedMs: Date.now() - startedAt,
4557
+ outputTruncated: result.outputTruncated,
4558
+ };
4559
+ if (scheduler) {
4560
+ try {
4561
+ await scheduler.flush(finalState);
4562
+ } finally {
4563
+ scheduler.dispose();
4564
+ }
4565
+ } else {
4566
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
4567
+ }
4095
4568
  }
4096
4569
 
4097
4570
  private async setTitle(contextKey: string, title: string): Promise<void> {
@@ -4208,7 +4681,7 @@ export class ProxySessionController {
4208
4681
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4209
4682
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4210
4683
  }
4211
- const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
4684
+ const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
4212
4685
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
4213
4686
  turnId: turn.turnId,
4214
4687
  parentTurnId: turn.parentTurnId,
@@ -4350,7 +4823,6 @@ export class ProxySessionController {
4350
4823
  contextKey,
4351
4824
  },
4352
4825
  },
4353
- actions: directoryCreationActions(entry.path, contextKey),
4354
4826
  }
4355
4827
  : {
4356
4828
  name: entry.name,
@@ -4375,7 +4847,7 @@ export class ProxySessionController {
4375
4847
  footerLines: [
4376
4848
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
4377
4849
  "",
4378
- "> 点击目录名称进入,点击文件名称发送到当前会话;**New** 在该目录创建任务,**NewGroup** 在该目录创建新群和任务。",
4850
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部的 **New** 和 **NewGroup** 使用当前目录。",
4379
4851
  ],
4380
4852
  });
4381
4853
  if (options.updateMessageId) {
@@ -4407,7 +4879,6 @@ export class ProxySessionController {
4407
4879
  contextKey,
4408
4880
  },
4409
4881
  },
4410
- actions: directoryCreationActions(drive.root, contextKey),
4411
4882
  }));
4412
4883
  const card = this.cardRenderer.renderDirectoryBrowserCard({
4413
4884
  directory: "此电脑",
@@ -4422,7 +4893,7 @@ export class ProxySessionController {
4422
4893
  footerLines: [
4423
4894
  `第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
4424
4895
  "",
4425
- "> 点击磁盘名称进入;**New** 在磁盘根目录创建任务,**NewGroup** 在磁盘根目录创建新群和任务。",
4896
+ "> 点击磁盘名称进入。",
4426
4897
  ],
4427
4898
  });
4428
4899
  if (options.updateMessageId) {
@@ -5104,7 +5575,9 @@ export class ProxySessionController {
5104
5575
  let activeTurnId: string | undefined;
5105
5576
  let queued = 0;
5106
5577
  if (!current) {
5107
- taskLines.push("无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5578
+ taskLines.push(isThreadContextKey(contextKey)
5579
+ ? unboundThreadTaskMessage()
5580
+ : "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5108
5581
  } else {
5109
5582
  const agent = this.ensureAgent(current.agentName);
5110
5583
  const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
@@ -5380,7 +5853,11 @@ export class ProxySessionController {
5380
5853
 
5381
5854
  private requireCurrentSession(contextKey: string): SessionRecord {
5382
5855
  const record = this.currentSession(contextKey);
5383
- if (!record) throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
5856
+ if (!record) {
5857
+ throw new Error(isThreadContextKey(contextKey)
5858
+ ? unboundThreadTaskMessage()
5859
+ : "当前没有任务,直接发送一条消息即可自动创建。");
5860
+ }
5384
5861
  return record;
5385
5862
  }
5386
5863
 
@@ -5897,6 +6374,19 @@ function isQueueIndependentCommand(command: Command): boolean {
5897
6374
  return false;
5898
6375
  }
5899
6376
 
6377
+ function commandRequiresCurrentSession(command: Command): boolean {
6378
+ if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
6379
+ return true;
6380
+ }
6381
+ if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
6382
+ if (command.type === "goal") return command.action !== "set";
6383
+ return false;
6384
+ }
6385
+
6386
+ function unboundThreadTaskMessage(): string {
6387
+ return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
6388
+ }
6389
+
5900
6390
  function parseSessionPage(value: unknown): number {
5901
6391
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
5902
6392
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
@@ -6042,6 +6532,104 @@ function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt
6042
6532
  return localImagePaths?.length ? { text, localImagePaths } : text;
6043
6533
  }
6044
6534
 
6535
+ function mergedForwardPrompt(
6536
+ instruction: string,
6537
+ transcript: string,
6538
+ ): string {
6539
+ return `${instruction}\n\n参考聊天记录:\n${transcript}`;
6540
+ }
6541
+
6542
+ function referencedMessagePrompt(instruction: string, referencedMessage: string): string {
6543
+ return `${instruction}\n\n引用消息:\n${referencedMessage}`;
6544
+ }
6545
+
6546
+ function quotedMessageId(message: IncomingMessage): string | undefined {
6547
+ const messageId = message.parentMessageId?.trim();
6548
+ if (!messageId || message.contextKey.startsWith("console:")) return undefined;
6549
+ if (message.mergedForwardMessageId) return undefined;
6550
+ const text = message.text.trimStart();
6551
+ if (text.startsWith("/") || text.startsWith("!")) return undefined;
6552
+ if (message.threadContext && message.rootMessageId === messageId) return undefined;
6553
+ return messageId;
6554
+ }
6555
+
6556
+ function renderLocalTurnReference(
6557
+ snapshot: TurnViewState,
6558
+ messageKind: "progress" | "final",
6559
+ ): string | undefined {
6560
+ if (messageKind === "final") {
6561
+ const response = snapshot.finalResponse?.trim() || snapshot.assistantText?.trim();
6562
+ return response
6563
+ ? `[消息类型:AgentBot 回复]\n${truncateMiddle(response, 48_000)}`
6564
+ : undefined;
6565
+ }
6566
+
6567
+ const activityText = (snapshot.activities ?? [])
6568
+ .filter((activity): activity is Extract<TurnActivity, { kind: "assistant" | "reasoning" }> =>
6569
+ activity.kind === "assistant" || activity.kind === "reasoning")
6570
+ .map((activity) => activity.text.trim())
6571
+ .filter(Boolean)
6572
+ .join("\n\n");
6573
+ const content = [
6574
+ snapshot.prompt?.trim() ? `用户请求:\n${snapshot.prompt.trim()}` : "",
6575
+ activityText,
6576
+ snapshot.progressText?.trim() || "",
6577
+ snapshot.finalResponse?.trim() || snapshot.assistantText?.trim() || "",
6578
+ snapshot.error?.trim() ? `错误:${snapshot.error.trim()}` : "",
6579
+ ].filter(Boolean).join("\n\n");
6580
+ return content
6581
+ ? `[消息类型:AgentBot 思考卡片]\n${truncateMiddle(content, 48_000)}`
6582
+ : undefined;
6583
+ }
6584
+
6585
+ function appendDownloadedFiles(
6586
+ prompt: string,
6587
+ files: Array<{ fileName: string; filePath: string }>,
6588
+ ): string {
6589
+ return `${prompt}\n\n参考文件(已下载到本地):\n${files.map((file, index) =>
6590
+ `[文件 ${index + 1}:${file.fileName}] ${file.filePath}`).join("\n")}`;
6591
+ }
6592
+
6593
+ function defaultResourcePrompt(message: IncomingMessage): string {
6594
+ return defaultResourcePromptFromCounts(message.images?.length ?? 0, message.files?.length ?? 0);
6595
+ }
6596
+
6597
+ function defaultResourcePromptFromCounts(imageCount: number, fileCount: number): string {
6598
+ if (imageCount > 0 && fileCount > 0) return "请查看附带的图片和文件";
6599
+ if (fileCount > 0) return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
6600
+ return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
6601
+ }
6602
+
6603
+ function isStandaloneResourceMessage(message: IncomingMessage): boolean {
6604
+ return !message.mergedForwardMessageId
6605
+ && message.text.trim().length === 0
6606
+ && ((message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
6607
+ }
6608
+
6609
+ function deduplicateImageReferences(
6610
+ images: Array<{ messageId: string; imageKey: string }>,
6611
+ ): Array<{ messageId: string; imageKey: string }> {
6612
+ const seen = new Set<string>();
6613
+ return images.filter((image) => {
6614
+ const key = `${image.messageId}:${image.imageKey}`;
6615
+ if (seen.has(key)) return false;
6616
+ seen.add(key);
6617
+ return true;
6618
+ });
6619
+ }
6620
+
6621
+ function deduplicateFileReferences(
6622
+ files: Array<{ messageId: string; fileKey: string; fileName: string }>,
6623
+ ): Array<{ messageId: string; fileKey: string; fileName: string }> {
6624
+ const seen = new Set<string>();
6625
+ return files.filter((file) => {
6626
+ const key = `${file.messageId}:${file.fileKey}`;
6627
+ if (seen.has(key)) return false;
6628
+ seen.add(key);
6629
+ return true;
6630
+ });
6631
+ }
6632
+
6045
6633
  function recoveryCardPrompt(prompt: string): string {
6046
6634
  return `恢复重启前的任务:${prompt}`;
6047
6635
  }
@@ -6140,14 +6728,13 @@ const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
6140
6728
 
6141
6729
  function formatGroupProjectDirectory(projectCwd: string): string {
6142
6730
  const value = abbreviateHomeDirectory(projectCwd);
6143
- if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
6144
-
6145
6731
  const levels = value
6146
6732
  .replace(/[\\/]+$/, "")
6147
6733
  .split(/[\\/]+/)
6148
6734
  .filter(Boolean)
6149
6735
  .slice(-2);
6150
- return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
6736
+ const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
6737
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
6151
6738
  }
6152
6739
 
6153
6740
  function abbreviateHomeDirectory(value: string): string {