@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.
- package/CHANGELOG.md +21 -0
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/help.js +6 -6
- package/dist/cli/initOptions.js +1 -4
- package/dist/cli/initOptions.js.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +91 -45
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +39 -12
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +108 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +24 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/feishu/InboundText.js +13 -0
- package/dist/feishu/InboundText.js.map +1 -0
- package/dist/feishu/MergedForwardMessage.js +317 -0
- package/dist/feishu/MergedForwardMessage.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +21 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +536 -58
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +161 -3
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli/help.ts +6 -6
- package/src/cli/initOptions.ts +1 -10
- package/src/cli.ts +10 -6
- package/src/feishu/CardRenderer.ts +116 -45
- package/src/feishu/FeishuConnector.ts +55 -13
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +151 -12
- package/src/feishu/FeishuTurnPresenter.ts +28 -4
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/feishu/InboundText.ts +12 -0
- package/src/feishu/MergedForwardMessage.ts +372 -0
- package/src/feishu/types.ts +15 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/presentation/OutboundRouter.ts +28 -0
- package/src/proxy/ProxySessionController.ts +642 -55
- package/src/state/StateStore.ts +230 -2
- package/src/state/migrations.ts +12 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- 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 {
|
|
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 = (
|
|
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
|
|
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(
|
|
473
|
-
this.outbound.downloadImage(message.contextKey,
|
|
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
|
-
|
|
487
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|
|
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 {
|
|
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
|
|
3422
|
-
|
|
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.
|
|
3427
|
-
|
|
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
|
|
3571
|
-
|
|
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 &&
|
|
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 <新目标>** 修改 **/goal clear** 清除",
|
|
4075
4434
|
],
|
|
4076
4435
|
});
|
|
4077
|
-
|
|
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
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
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.
|
|
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
|
-
">
|
|
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
|
-
">
|
|
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(
|
|
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)
|
|
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
|
-
|
|
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 {
|