@keyou007/agent-bot 0.1.14-alpha.3 → 0.1.15-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +6 -4
- package/README.zh.md +6 -4
- 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 +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +40 -37
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +22 -7
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +75 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +23 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- 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/presentation/OutboundRouter.js +21 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +344 -22
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +35 -3
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +1 -0
- package/dist/state/migrations.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/cli/help.ts +6 -6
- package/src/cli/initOptions.ts +1 -10
- package/src/cli.ts +4 -5
- package/src/feishu/CardRenderer.ts +41 -36
- package/src/feishu/FeishuConnector.ts +27 -7
- package/src/feishu/FeishuMessageClient.ts +110 -0
- package/src/feishu/FeishuTurnPresenter.ts +24 -3
- package/src/feishu/InboundText.ts +12 -0
- package/src/feishu/MergedForwardMessage.ts +372 -0
- package/src/feishu/types.ts +15 -0
- package/src/presentation/OutboundRouter.ts +28 -0
- package/src/proxy/ProxySessionController.ts +434 -20
- package/src/state/StateStore.ts +50 -2
- package/src/state/migrations.ts +1 -0
|
@@ -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,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
type TaskListCardAction,
|
|
27
28
|
} from "../feishu/CardRenderer.js";
|
|
28
29
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
30
|
+
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
29
31
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
30
32
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
31
33
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
@@ -72,6 +74,20 @@ interface StartTurnOptions {
|
|
|
72
74
|
preserveAttemptOnFailure?: boolean;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
interface PendingForwardAttachment {
|
|
78
|
+
contextKey: string;
|
|
79
|
+
attachmentMessageId?: string;
|
|
80
|
+
attachmentPromise: Promise<IncomingMessage | undefined>;
|
|
81
|
+
resolveAttachment: (message: IncomingMessage | undefined) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ForwardAttachmentReservation {
|
|
85
|
+
sourceMessageId: string;
|
|
86
|
+
sourceKind: "merged_forward" | "resource";
|
|
87
|
+
pending: PendingForwardAttachment;
|
|
88
|
+
registry: Map<string, PendingForwardAttachment>;
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
76
92
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
77
93
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -82,6 +98,9 @@ const RESET_HISTORY_PAGE_SIZE = 10;
|
|
|
82
98
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
83
99
|
const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
|
|
84
100
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
101
|
+
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
102
|
+
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
103
|
+
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
85
104
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
86
105
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
87
106
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
@@ -370,6 +389,9 @@ export class ProxySessionController {
|
|
|
370
389
|
private readonly llmRetryingSessions = new Set<string>();
|
|
371
390
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
372
391
|
private readonly recoveryRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
392
|
+
private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
|
|
393
|
+
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
394
|
+
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
373
395
|
private readonly unsubscribe: Array<() => void> = [];
|
|
374
396
|
private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
|
|
375
397
|
private startupRecovery?: Promise<void>;
|
|
@@ -406,6 +428,9 @@ export class ProxySessionController {
|
|
|
406
428
|
}
|
|
407
429
|
|
|
408
430
|
async onMessage(message: IncomingMessage): Promise<void> {
|
|
431
|
+
let forwardedImages: Array<{ messageId: string; imageKey: string }> = [];
|
|
432
|
+
let forwardedFiles: Array<{ messageId: string; fileKey: string; fileName: string }> = [];
|
|
433
|
+
let coalescedForwardAttachment = false;
|
|
409
434
|
if (!message.contextKey.startsWith("console:") && !allowsFeishuUser(this.config, message.userId)) {
|
|
410
435
|
this.logger.debug(
|
|
411
436
|
{ messageId: message.messageId },
|
|
@@ -427,6 +452,15 @@ export class ProxySessionController {
|
|
|
427
452
|
// For accepted messages, claim durable deduplication before acknowledgement so event
|
|
428
453
|
// retries cannot add duplicate reactions.
|
|
429
454
|
if (!this.store.claimInboundEvent(message.messageId, "message")) return;
|
|
455
|
+
const attachmentReservation = message.mergedForwardMessageId
|
|
456
|
+
? undefined
|
|
457
|
+
: this.reserveForwardAttachment(message);
|
|
458
|
+
const pendingMergedForward = message.mergedForwardMessageId
|
|
459
|
+
? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
|
|
460
|
+
: undefined;
|
|
461
|
+
const pendingResourceForward = !attachmentReservation && isStandaloneResourceMessage(message)
|
|
462
|
+
? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
|
|
463
|
+
: undefined;
|
|
430
464
|
try {
|
|
431
465
|
const reactionId = await this.outbound.addReaction(
|
|
432
466
|
message.contextKey,
|
|
@@ -442,6 +476,13 @@ export class ProxySessionController {
|
|
|
442
476
|
"Failed to acknowledge the incoming Feishu message with a reaction.",
|
|
443
477
|
);
|
|
444
478
|
}
|
|
479
|
+
if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
|
|
480
|
+
this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
|
|
481
|
+
messageId: message.messageId,
|
|
482
|
+
parentMessageId: message.parentMessageId,
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
445
486
|
if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
|
|
446
487
|
try {
|
|
447
488
|
await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
|
|
@@ -457,20 +498,165 @@ export class ProxySessionController {
|
|
|
457
498
|
this.store.recordChatContext(chatContextKey, message.chatType);
|
|
458
499
|
this.store.markChatActive(chatContextKey);
|
|
459
500
|
}
|
|
501
|
+
if (pendingResourceForward) {
|
|
502
|
+
const resourceMessage = message;
|
|
503
|
+
const attachment = await this.waitForForwardAttachment(
|
|
504
|
+
message.messageId,
|
|
505
|
+
pendingResourceForward,
|
|
506
|
+
this.pendingResourceForwards,
|
|
507
|
+
);
|
|
508
|
+
if (attachment) {
|
|
509
|
+
coalescedForwardAttachment = true;
|
|
510
|
+
const instruction = normalizeFeishuPostText(attachment.text) || defaultResourcePrompt(resourceMessage);
|
|
511
|
+
forwardedImages = (resourceMessage.images ?? []).map((image) => ({
|
|
512
|
+
messageId: resourceMessage.messageId,
|
|
513
|
+
imageKey: image.imageKey,
|
|
514
|
+
}));
|
|
515
|
+
forwardedFiles = (resourceMessage.files ?? []).map((file) => ({
|
|
516
|
+
messageId: resourceMessage.messageId,
|
|
517
|
+
fileKey: file.fileKey,
|
|
518
|
+
fileName: file.fileName,
|
|
519
|
+
}));
|
|
520
|
+
message = {
|
|
521
|
+
...attachment,
|
|
522
|
+
text: instruction,
|
|
523
|
+
displayText: instruction,
|
|
524
|
+
};
|
|
525
|
+
this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (message.mergedForwardMessageId) {
|
|
529
|
+
const mergedForwardMessage = message;
|
|
530
|
+
const mergedForwardResult = this.outbound.readMergedForward(
|
|
531
|
+
message.contextKey,
|
|
532
|
+
message.mergedForwardMessageId,
|
|
533
|
+
).then(
|
|
534
|
+
(content) => ({ content } as const),
|
|
535
|
+
(error: unknown) => ({ error } as const),
|
|
536
|
+
);
|
|
537
|
+
const attachment = pendingMergedForward
|
|
538
|
+
? await this.waitForForwardAttachment(
|
|
539
|
+
message.messageId,
|
|
540
|
+
pendingMergedForward,
|
|
541
|
+
this.pendingMergedForwards,
|
|
542
|
+
)
|
|
543
|
+
: undefined;
|
|
544
|
+
if (attachment) {
|
|
545
|
+
coalescedForwardAttachment = true;
|
|
546
|
+
const instruction = normalizeFeishuPostText(attachment.text)
|
|
547
|
+
|| ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
|
|
548
|
+
message = {
|
|
549
|
+
...attachment,
|
|
550
|
+
text: instruction,
|
|
551
|
+
displayText: instruction,
|
|
552
|
+
};
|
|
553
|
+
this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
|
|
554
|
+
}
|
|
555
|
+
const result = await mergedForwardResult;
|
|
556
|
+
if ("error" in result) {
|
|
557
|
+
const replyTarget = message.replyInThread
|
|
558
|
+
? { messageId: message.messageId, replyInThread: true as const }
|
|
559
|
+
: undefined;
|
|
560
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
561
|
+
const detail = result.error instanceof Error ? result.error.message : String(result.error);
|
|
562
|
+
await this.outbound.withReplyTarget(
|
|
563
|
+
message.contextKey,
|
|
564
|
+
replyTarget,
|
|
565
|
+
() => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)),
|
|
566
|
+
);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
|
|
570
|
+
forwardedImages = result.content.images;
|
|
571
|
+
forwardedFiles = result.content.files;
|
|
572
|
+
message = {
|
|
573
|
+
...message,
|
|
574
|
+
displayText: instruction,
|
|
575
|
+
text: mergedForwardPrompt(instruction, result.content.text),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const referencedMessageId = coalescedForwardAttachment ? undefined : quotedMessageId(message);
|
|
579
|
+
if (referencedMessageId) {
|
|
580
|
+
try {
|
|
581
|
+
const referenced = await this.resolveReferencedMessage(message.contextKey, referencedMessageId);
|
|
582
|
+
const instruction = normalizeFeishuPostText(message.displayText ?? message.text)
|
|
583
|
+
|| DEFAULT_REFERENCED_MESSAGE_INSTRUCTION;
|
|
584
|
+
forwardedImages = [...referenced.images, ...forwardedImages];
|
|
585
|
+
forwardedFiles = [...referenced.files, ...forwardedFiles];
|
|
586
|
+
message = {
|
|
587
|
+
...message,
|
|
588
|
+
displayText: instruction,
|
|
589
|
+
text: referencedMessagePrompt(instruction, referenced.text),
|
|
590
|
+
};
|
|
591
|
+
} catch (error) {
|
|
592
|
+
const replyTarget = message.replyInThread
|
|
593
|
+
? { messageId: message.messageId, replyInThread: true as const }
|
|
594
|
+
: undefined;
|
|
595
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
596
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
597
|
+
await this.outbound.withReplyTarget(
|
|
598
|
+
message.contextKey,
|
|
599
|
+
replyTarget,
|
|
600
|
+
() => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)),
|
|
601
|
+
);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
460
605
|
const replyTarget = message.replyInThread
|
|
461
606
|
? { messageId: message.messageId, replyInThread: true as const }
|
|
462
607
|
: undefined;
|
|
463
|
-
const
|
|
608
|
+
const imageReferences = deduplicateImageReferences([
|
|
609
|
+
...forwardedImages,
|
|
610
|
+
...(message.images ?? []).map((image) => ({ messageId: message.messageId, imageKey: image.imageKey })),
|
|
611
|
+
]);
|
|
612
|
+
const fileReferences = deduplicateFileReferences([
|
|
613
|
+
...forwardedFiles,
|
|
614
|
+
...(message.files ?? []).map((file) => ({
|
|
615
|
+
messageId: message.messageId,
|
|
616
|
+
fileKey: file.fileKey,
|
|
617
|
+
fileName: file.fileName,
|
|
618
|
+
})),
|
|
619
|
+
]);
|
|
620
|
+
const imageCount = imageReferences.length;
|
|
621
|
+
const fileCount = fileReferences.length;
|
|
464
622
|
this.store.audit(message.contextKey, "incoming_message", {
|
|
465
623
|
messageId: message.messageId,
|
|
466
624
|
text: message.text,
|
|
467
625
|
...(imageCount > 0 ? { imageCount } : {}),
|
|
626
|
+
...(fileCount > 0 ? { fileCount } : {}),
|
|
468
627
|
});
|
|
469
628
|
let localImagePaths: string[] | undefined;
|
|
470
629
|
if (imageCount > 0) {
|
|
471
630
|
try {
|
|
472
|
-
localImagePaths = await Promise.all(
|
|
473
|
-
this.outbound.downloadImage(message.contextKey,
|
|
631
|
+
localImagePaths = await Promise.all(imageReferences.map((image) =>
|
|
632
|
+
this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
|
|
633
|
+
} catch (error) {
|
|
634
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
635
|
+
await this.outbound.withReplyTarget(
|
|
636
|
+
message.contextKey,
|
|
637
|
+
replyTarget,
|
|
638
|
+
() => this.sendError(message.contextKey, error),
|
|
639
|
+
);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (fileCount > 0) {
|
|
644
|
+
try {
|
|
645
|
+
const downloadedFiles = await Promise.all(fileReferences.map(async (file) => ({
|
|
646
|
+
fileName: file.fileName,
|
|
647
|
+
filePath: await this.outbound.downloadFile(
|
|
648
|
+
message.contextKey,
|
|
649
|
+
file.messageId,
|
|
650
|
+
file.fileKey,
|
|
651
|
+
file.fileName,
|
|
652
|
+
),
|
|
653
|
+
})));
|
|
654
|
+
const instruction = message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount);
|
|
655
|
+
message = {
|
|
656
|
+
...message,
|
|
657
|
+
displayText: message.displayText ?? instruction,
|
|
658
|
+
text: appendDownloadedFiles(instruction, downloadedFiles),
|
|
659
|
+
};
|
|
474
660
|
} catch (error) {
|
|
475
661
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
476
662
|
await this.outbound.withReplyTarget(
|
|
@@ -483,8 +669,9 @@ export class ProxySessionController {
|
|
|
483
669
|
}
|
|
484
670
|
let command: Command;
|
|
485
671
|
try {
|
|
486
|
-
|
|
487
|
-
|
|
672
|
+
const resourceCount = imageCount + fileCount;
|
|
673
|
+
command = resourceCount > 0 && !message.text.trimStart().startsWith("/")
|
|
674
|
+
? { type: "prompt", text: message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount) }
|
|
488
675
|
: this.router.parse(message.text);
|
|
489
676
|
} catch (error) {
|
|
490
677
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
@@ -818,6 +1005,11 @@ export class ProxySessionController {
|
|
|
818
1005
|
this.queuedPromptCards.clear();
|
|
819
1006
|
this.queuedPromptCardWrites.clear();
|
|
820
1007
|
this.sessionResets.clear();
|
|
1008
|
+
for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
|
|
1009
|
+
this.pendingMergedForwards.clear();
|
|
1010
|
+
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
1011
|
+
this.pendingResourceForwards.clear();
|
|
1012
|
+
this.relatedReactionMessageIds.clear();
|
|
821
1013
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
822
1014
|
this.recoveryRetryTimers.clear();
|
|
823
1015
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
@@ -1499,7 +1691,14 @@ export class ProxySessionController {
|
|
|
1499
1691
|
await this.setTitle(contextKey, command.title);
|
|
1500
1692
|
return;
|
|
1501
1693
|
case "prompt":
|
|
1502
|
-
await this.prompt(
|
|
1694
|
+
await this.prompt(
|
|
1695
|
+
contextKey,
|
|
1696
|
+
command.text,
|
|
1697
|
+
messageId,
|
|
1698
|
+
replyTarget,
|
|
1699
|
+
localImagePaths,
|
|
1700
|
+
incomingMessage?.displayText,
|
|
1701
|
+
);
|
|
1503
1702
|
return;
|
|
1504
1703
|
case "nosteer":
|
|
1505
1704
|
await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
|
|
@@ -1986,6 +2185,7 @@ export class ProxySessionController {
|
|
|
1986
2185
|
messageId?: string,
|
|
1987
2186
|
replyTarget?: MessageReplyTarget,
|
|
1988
2187
|
localImagePaths?: string[],
|
|
2188
|
+
displayPrompt?: string,
|
|
1989
2189
|
): Promise<void> {
|
|
1990
2190
|
if (!text.trim()) throw new Error("请输入要交给 Agent 的内容。");
|
|
1991
2191
|
let record = this.currentSession(contextKey);
|
|
@@ -1997,11 +2197,11 @@ export class ProxySessionController {
|
|
|
1997
2197
|
this.inheritedNewTaskCwd(contextKey),
|
|
1998
2198
|
false,
|
|
1999
2199
|
true,
|
|
2000
|
-
text,
|
|
2200
|
+
displayPrompt ?? text,
|
|
2001
2201
|
replyTarget,
|
|
2002
2202
|
);
|
|
2003
2203
|
}
|
|
2004
|
-
await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
|
|
2204
|
+
await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
|
|
2005
2205
|
}
|
|
2006
2206
|
|
|
2007
2207
|
private async promptSession(
|
|
@@ -2011,12 +2211,14 @@ export class ProxySessionController {
|
|
|
2011
2211
|
messageId?: string,
|
|
2012
2212
|
replyTarget?: MessageReplyTarget,
|
|
2013
2213
|
localImagePaths?: string[],
|
|
2214
|
+
displayPrompt?: string,
|
|
2014
2215
|
): Promise<void> {
|
|
2015
2216
|
if (this.llmRetryingSessions.has(record.localSessionId)) {
|
|
2016
2217
|
const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
|
|
2017
2218
|
localImagePaths,
|
|
2018
2219
|
messageId,
|
|
2019
2220
|
replyTarget,
|
|
2221
|
+
displayPrompt,
|
|
2020
2222
|
});
|
|
2021
2223
|
this.store.audit(contextKey, "queued_prompt_added_during_llm_retry", {
|
|
2022
2224
|
promptId: queued.promptId,
|
|
@@ -2034,7 +2236,13 @@ export class ProxySessionController {
|
|
|
2034
2236
|
record.cwd,
|
|
2035
2237
|
this.agentLabel(record.agentName),
|
|
2036
2238
|
);
|
|
2037
|
-
await this.outbound.startPendingTurn(
|
|
2239
|
+
await this.outbound.startPendingTurn(
|
|
2240
|
+
record.localSessionId,
|
|
2241
|
+
contextKey,
|
|
2242
|
+
record.title,
|
|
2243
|
+
replyTarget,
|
|
2244
|
+
displayPrompt ?? text,
|
|
2245
|
+
);
|
|
2038
2246
|
}
|
|
2039
2247
|
let loaded: LoadedSession;
|
|
2040
2248
|
try {
|
|
@@ -2065,12 +2273,13 @@ export class ProxySessionController {
|
|
|
2065
2273
|
localImagePaths,
|
|
2066
2274
|
messageId,
|
|
2067
2275
|
replyTarget,
|
|
2276
|
+
displayPrompt,
|
|
2068
2277
|
});
|
|
2069
2278
|
return;
|
|
2070
2279
|
}
|
|
2071
2280
|
try {
|
|
2072
2281
|
await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
|
|
2073
|
-
await this.presentSteerMessage(record.localSessionId, activeTurnId, text, messageId);
|
|
2282
|
+
await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
|
|
2074
2283
|
if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
|
|
2075
2284
|
return;
|
|
2076
2285
|
} catch (error) {
|
|
@@ -2082,7 +2291,7 @@ export class ProxySessionController {
|
|
|
2082
2291
|
this.logger.warn({ error: syncError, sessionId: record.localSessionId }, "Failed to synchronize after steering failure.");
|
|
2083
2292
|
}
|
|
2084
2293
|
if (!current?.activeTurnId) {
|
|
2085
|
-
const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
|
|
2294
|
+
const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
|
|
2086
2295
|
if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
|
|
2087
2296
|
return;
|
|
2088
2297
|
}
|
|
@@ -2093,7 +2302,12 @@ export class ProxySessionController {
|
|
|
2093
2302
|
current.activeTurnId,
|
|
2094
2303
|
runtimePrompt(text, localImagePaths),
|
|
2095
2304
|
);
|
|
2096
|
-
await this.presentSteerMessage(
|
|
2305
|
+
await this.presentSteerMessage(
|
|
2306
|
+
record.localSessionId,
|
|
2307
|
+
current.activeTurnId,
|
|
2308
|
+
displayPrompt ?? text,
|
|
2309
|
+
messageId,
|
|
2310
|
+
);
|
|
2097
2311
|
if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
|
|
2098
2312
|
return;
|
|
2099
2313
|
} catch (retryError) {
|
|
@@ -2107,11 +2321,12 @@ export class ProxySessionController {
|
|
|
2107
2321
|
localImagePaths,
|
|
2108
2322
|
messageId,
|
|
2109
2323
|
replyTarget,
|
|
2324
|
+
displayPrompt,
|
|
2110
2325
|
});
|
|
2111
2326
|
return;
|
|
2112
2327
|
}
|
|
2113
2328
|
}
|
|
2114
|
-
const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
|
|
2329
|
+
const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
|
|
2115
2330
|
if (messageId) await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
|
|
2116
2331
|
}
|
|
2117
2332
|
|
|
@@ -2156,6 +2371,7 @@ export class ProxySessionController {
|
|
|
2156
2371
|
localImagePaths?: string[];
|
|
2157
2372
|
messageId?: string;
|
|
2158
2373
|
replyTarget?: MessageReplyTarget;
|
|
2374
|
+
displayPrompt?: string;
|
|
2159
2375
|
} = {},
|
|
2160
2376
|
): QueuedPromptRecord {
|
|
2161
2377
|
return this.store.enqueuePrompt({
|
|
@@ -2166,6 +2382,7 @@ export class ProxySessionController {
|
|
|
2166
2382
|
localImagePaths: options.localImagePaths,
|
|
2167
2383
|
messageId: options.messageId,
|
|
2168
2384
|
replyMessageId: options.replyTarget?.messageId,
|
|
2385
|
+
displayPrompt: options.displayPrompt,
|
|
2169
2386
|
});
|
|
2170
2387
|
}
|
|
2171
2388
|
|
|
@@ -2227,7 +2444,7 @@ export class ProxySessionController {
|
|
|
2227
2444
|
contextKey,
|
|
2228
2445
|
prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
|
|
2229
2446
|
id: prompt.promptId,
|
|
2230
|
-
text: prompt.text,
|
|
2447
|
+
text: prompt.displayPrompt ?? prompt.text,
|
|
2231
2448
|
})),
|
|
2232
2449
|
});
|
|
2233
2450
|
}
|
|
@@ -2729,7 +2946,7 @@ export class ProxySessionController {
|
|
|
2729
2946
|
options: StartTurnOptions = {},
|
|
2730
2947
|
): Promise<string> {
|
|
2731
2948
|
const currentRecord = this.store.getSession(loaded.record.localSessionId);
|
|
2732
|
-
const title = currentRecord?.title ?? normalizeTaskTitle(text);
|
|
2949
|
+
const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
|
|
2733
2950
|
if (!currentRecord?.title && title) this.store.updateRuntimeSession(loaded.record.localSessionId, { title });
|
|
2734
2951
|
if (title) this.outbound.updateSessionTitle(loaded.record.localSessionId, title);
|
|
2735
2952
|
const attemptId = options.attemptId ?? createId("attempt");
|
|
@@ -3392,7 +3609,7 @@ export class ProxySessionController {
|
|
|
3392
3609
|
prompt.text,
|
|
3393
3610
|
prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined,
|
|
3394
3611
|
prompt.localImagePaths,
|
|
3395
|
-
{ messageId: prompt.messageId },
|
|
3612
|
+
{ messageId: prompt.messageId, displayPrompt: prompt.displayPrompt },
|
|
3396
3613
|
);
|
|
3397
3614
|
if (prompt.messageId) await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
|
|
3398
3615
|
} catch (error) {
|
|
@@ -3418,13 +3635,17 @@ export class ProxySessionController {
|
|
|
3418
3635
|
messageId: string,
|
|
3419
3636
|
status: "completed" | "failed" | "cancelled",
|
|
3420
3637
|
): Promise<void> {
|
|
3421
|
-
const
|
|
3422
|
-
|
|
3638
|
+
for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
|
|
3639
|
+
const reaction = this.store.claimMessageReaction(relatedMessageId);
|
|
3640
|
+
if (reaction) await this.replaceMessageReaction(reaction, status);
|
|
3641
|
+
}
|
|
3423
3642
|
}
|
|
3424
3643
|
|
|
3425
3644
|
private async bindMessageReactionToTurn(messageId: string, sessionId: string, turnId: string): Promise<void> {
|
|
3426
|
-
this.
|
|
3427
|
-
|
|
3645
|
+
for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
|
|
3646
|
+
this.store.bindMessageToTurn(relatedMessageId, sessionId, turnId);
|
|
3647
|
+
this.store.bindMessageReaction(relatedMessageId, sessionId, turnId);
|
|
3648
|
+
}
|
|
3428
3649
|
const session = this.store.getSession(sessionId);
|
|
3429
3650
|
if (session?.lastTurnId !== turnId) return;
|
|
3430
3651
|
if (session.lastTurnStatus === "completed" || session.lastTurnStatus === "failed" || session.lastTurnStatus === "cancelled") {
|
|
@@ -3432,6 +3653,101 @@ export class ProxySessionController {
|
|
|
3432
3653
|
}
|
|
3433
3654
|
}
|
|
3434
3655
|
|
|
3656
|
+
private registerPendingForwardAttachment(
|
|
3657
|
+
message: IncomingMessage,
|
|
3658
|
+
registry: Map<string, PendingForwardAttachment>,
|
|
3659
|
+
): PendingForwardAttachment {
|
|
3660
|
+
let resolveAttachment!: (message: IncomingMessage | undefined) => void;
|
|
3661
|
+
const attachmentPromise = new Promise<IncomingMessage | undefined>((resolve) => {
|
|
3662
|
+
resolveAttachment = resolve;
|
|
3663
|
+
});
|
|
3664
|
+
const pending = {
|
|
3665
|
+
contextKey: message.contextKey,
|
|
3666
|
+
attachmentPromise,
|
|
3667
|
+
resolveAttachment,
|
|
3668
|
+
};
|
|
3669
|
+
registry.set(message.messageId, pending);
|
|
3670
|
+
return pending;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
private reserveForwardAttachment(message: IncomingMessage): ForwardAttachmentReservation | undefined {
|
|
3674
|
+
if (!message.parentMessageId) return undefined;
|
|
3675
|
+
const candidates = [
|
|
3676
|
+
{ sourceKind: "merged_forward" as const, registry: this.pendingMergedForwards },
|
|
3677
|
+
{ sourceKind: "resource" as const, registry: this.pendingResourceForwards },
|
|
3678
|
+
];
|
|
3679
|
+
for (const candidate of candidates) {
|
|
3680
|
+
const pending = candidate.registry.get(message.parentMessageId);
|
|
3681
|
+
if (!pending || pending.contextKey !== message.contextKey || pending.attachmentMessageId) continue;
|
|
3682
|
+
pending.attachmentMessageId = message.messageId;
|
|
3683
|
+
return {
|
|
3684
|
+
sourceMessageId: message.parentMessageId,
|
|
3685
|
+
sourceKind: candidate.sourceKind,
|
|
3686
|
+
pending,
|
|
3687
|
+
registry: candidate.registry,
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
return undefined;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
private completeForwardAttachment(
|
|
3694
|
+
message: IncomingMessage,
|
|
3695
|
+
reservation: ForwardAttachmentReservation,
|
|
3696
|
+
): boolean {
|
|
3697
|
+
if (!message.parentMessageId) return false;
|
|
3698
|
+
if (reservation.sourceMessageId !== message.parentMessageId) return false;
|
|
3699
|
+
if (reservation.registry.get(message.parentMessageId) !== reservation.pending) return false;
|
|
3700
|
+
if (reservation.pending.attachmentMessageId !== message.messageId) return false;
|
|
3701
|
+
reservation.pending.resolveAttachment(message);
|
|
3702
|
+
return true;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
private async waitForForwardAttachment(
|
|
3706
|
+
messageId: string,
|
|
3707
|
+
pending: PendingForwardAttachment,
|
|
3708
|
+
registry: Map<string, PendingForwardAttachment>,
|
|
3709
|
+
): Promise<IncomingMessage | undefined> {
|
|
3710
|
+
let timer: NodeJS.Timeout | undefined;
|
|
3711
|
+
const timeout = new Promise<undefined>((resolve) => {
|
|
3712
|
+
timer = setTimeout(() => resolve(undefined), FORWARD_ATTACHMENT_WINDOW_MS);
|
|
3713
|
+
timer.unref?.();
|
|
3714
|
+
});
|
|
3715
|
+
try {
|
|
3716
|
+
return await Promise.race([pending.attachmentPromise, timeout]);
|
|
3717
|
+
} finally {
|
|
3718
|
+
if (timer) clearTimeout(timer);
|
|
3719
|
+
if (registry.get(messageId) === pending) {
|
|
3720
|
+
registry.delete(messageId);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
private async resolveReferencedMessage(
|
|
3726
|
+
contextKey: string,
|
|
3727
|
+
messageId: string,
|
|
3728
|
+
): Promise<ReferencedMessageContent> {
|
|
3729
|
+
const local = this.store.findAgentBotTurnMessageById(messageId);
|
|
3730
|
+
if (local) {
|
|
3731
|
+
const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(local.turnId));
|
|
3732
|
+
const text = snapshot ? renderLocalTurnReference(snapshot, local.messageKind) : undefined;
|
|
3733
|
+
if (text) {
|
|
3734
|
+
return {
|
|
3735
|
+
text,
|
|
3736
|
+
messageType: local.messageKind === "final" ? "agent_bot_reply" : "agent_bot_progress",
|
|
3737
|
+
images: [],
|
|
3738
|
+
files: [],
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
return this.outbound.readReferencedMessage(contextKey, messageId);
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
private takeRelatedReactionMessageIds(messageId: string): string[] {
|
|
3746
|
+
const related = this.relatedReactionMessageIds.get(messageId) ?? [];
|
|
3747
|
+
this.relatedReactionMessageIds.delete(messageId);
|
|
3748
|
+
return [...new Set([messageId, ...related])];
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3435
3751
|
private async finalizeTurnMessageReactions(
|
|
3436
3752
|
turnId: string,
|
|
3437
3753
|
status: "completed" | "failed" | "cancelled",
|
|
@@ -6042,6 +6358,104 @@ function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt
|
|
|
6042
6358
|
return localImagePaths?.length ? { text, localImagePaths } : text;
|
|
6043
6359
|
}
|
|
6044
6360
|
|
|
6361
|
+
function mergedForwardPrompt(
|
|
6362
|
+
instruction: string,
|
|
6363
|
+
transcript: string,
|
|
6364
|
+
): string {
|
|
6365
|
+
return `${instruction}\n\n参考聊天记录:\n${transcript}`;
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
function referencedMessagePrompt(instruction: string, referencedMessage: string): string {
|
|
6369
|
+
return `${instruction}\n\n引用消息:\n${referencedMessage}`;
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
function quotedMessageId(message: IncomingMessage): string | undefined {
|
|
6373
|
+
const messageId = message.parentMessageId?.trim();
|
|
6374
|
+
if (!messageId || message.contextKey.startsWith("console:")) return undefined;
|
|
6375
|
+
if (message.mergedForwardMessageId) return undefined;
|
|
6376
|
+
const text = message.text.trimStart();
|
|
6377
|
+
if (text.startsWith("/") || text.startsWith("!")) return undefined;
|
|
6378
|
+
if (message.threadContext && message.rootMessageId === messageId) return undefined;
|
|
6379
|
+
return messageId;
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
function renderLocalTurnReference(
|
|
6383
|
+
snapshot: TurnViewState,
|
|
6384
|
+
messageKind: "progress" | "final",
|
|
6385
|
+
): string | undefined {
|
|
6386
|
+
if (messageKind === "final") {
|
|
6387
|
+
const response = snapshot.finalResponse?.trim() || snapshot.assistantText?.trim();
|
|
6388
|
+
return response
|
|
6389
|
+
? `[消息类型:AgentBot 回复]\n${truncateMiddle(response, 48_000)}`
|
|
6390
|
+
: undefined;
|
|
6391
|
+
}
|
|
6392
|
+
|
|
6393
|
+
const activityText = (snapshot.activities ?? [])
|
|
6394
|
+
.filter((activity): activity is Extract<TurnActivity, { kind: "assistant" | "reasoning" }> =>
|
|
6395
|
+
activity.kind === "assistant" || activity.kind === "reasoning")
|
|
6396
|
+
.map((activity) => activity.text.trim())
|
|
6397
|
+
.filter(Boolean)
|
|
6398
|
+
.join("\n\n");
|
|
6399
|
+
const content = [
|
|
6400
|
+
snapshot.prompt?.trim() ? `用户请求:\n${snapshot.prompt.trim()}` : "",
|
|
6401
|
+
activityText,
|
|
6402
|
+
snapshot.progressText?.trim() || "",
|
|
6403
|
+
snapshot.finalResponse?.trim() || snapshot.assistantText?.trim() || "",
|
|
6404
|
+
snapshot.error?.trim() ? `错误:${snapshot.error.trim()}` : "",
|
|
6405
|
+
].filter(Boolean).join("\n\n");
|
|
6406
|
+
return content
|
|
6407
|
+
? `[消息类型:AgentBot 思考卡片]\n${truncateMiddle(content, 48_000)}`
|
|
6408
|
+
: undefined;
|
|
6409
|
+
}
|
|
6410
|
+
|
|
6411
|
+
function appendDownloadedFiles(
|
|
6412
|
+
prompt: string,
|
|
6413
|
+
files: Array<{ fileName: string; filePath: string }>,
|
|
6414
|
+
): string {
|
|
6415
|
+
return `${prompt}\n\n参考文件(已下载到本地):\n${files.map((file, index) =>
|
|
6416
|
+
`[文件 ${index + 1}:${file.fileName}] ${file.filePath}`).join("\n")}`;
|
|
6417
|
+
}
|
|
6418
|
+
|
|
6419
|
+
function defaultResourcePrompt(message: IncomingMessage): string {
|
|
6420
|
+
return defaultResourcePromptFromCounts(message.images?.length ?? 0, message.files?.length ?? 0);
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
function defaultResourcePromptFromCounts(imageCount: number, fileCount: number): string {
|
|
6424
|
+
if (imageCount > 0 && fileCount > 0) return "请查看附带的图片和文件";
|
|
6425
|
+
if (fileCount > 0) return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
|
|
6426
|
+
return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
function isStandaloneResourceMessage(message: IncomingMessage): boolean {
|
|
6430
|
+
return !message.mergedForwardMessageId
|
|
6431
|
+
&& message.text.trim().length === 0
|
|
6432
|
+
&& ((message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
6433
|
+
}
|
|
6434
|
+
|
|
6435
|
+
function deduplicateImageReferences(
|
|
6436
|
+
images: Array<{ messageId: string; imageKey: string }>,
|
|
6437
|
+
): Array<{ messageId: string; imageKey: string }> {
|
|
6438
|
+
const seen = new Set<string>();
|
|
6439
|
+
return images.filter((image) => {
|
|
6440
|
+
const key = `${image.messageId}:${image.imageKey}`;
|
|
6441
|
+
if (seen.has(key)) return false;
|
|
6442
|
+
seen.add(key);
|
|
6443
|
+
return true;
|
|
6444
|
+
});
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
function deduplicateFileReferences(
|
|
6448
|
+
files: Array<{ messageId: string; fileKey: string; fileName: string }>,
|
|
6449
|
+
): Array<{ messageId: string; fileKey: string; fileName: string }> {
|
|
6450
|
+
const seen = new Set<string>();
|
|
6451
|
+
return files.filter((file) => {
|
|
6452
|
+
const key = `${file.messageId}:${file.fileKey}`;
|
|
6453
|
+
if (seen.has(key)) return false;
|
|
6454
|
+
seen.add(key);
|
|
6455
|
+
return true;
|
|
6456
|
+
});
|
|
6457
|
+
}
|
|
6458
|
+
|
|
6045
6459
|
function recoveryCardPrompt(prompt: string): string {
|
|
6046
6460
|
return `恢复重启前的任务:${prompt}`;
|
|
6047
6461
|
}
|