@keyou007/agent-bot 0.1.14 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +6 -4
  3. package/README.zh.md +6 -4
  4. package/dist/cli/help.js +6 -6
  5. package/dist/cli/initOptions.js +1 -4
  6. package/dist/cli/initOptions.js.map +1 -1
  7. package/dist/cli.js +4 -4
  8. package/dist/cli.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +40 -37
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuConnector.js +22 -7
  12. package/dist/feishu/FeishuConnector.js.map +1 -1
  13. package/dist/feishu/FeishuMessageClient.js +75 -0
  14. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  15. package/dist/feishu/FeishuTurnPresenter.js +23 -1
  16. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  17. package/dist/feishu/InboundText.js +13 -0
  18. package/dist/feishu/InboundText.js.map +1 -0
  19. package/dist/feishu/MergedForwardMessage.js +317 -0
  20. package/dist/feishu/MergedForwardMessage.js.map +1 -0
  21. package/dist/presentation/OutboundRouter.js +21 -0
  22. package/dist/presentation/OutboundRouter.js.map +1 -1
  23. package/dist/proxy/ProxySessionController.js +344 -22
  24. package/dist/proxy/ProxySessionController.js.map +1 -1
  25. package/dist/state/StateStore.js +35 -3
  26. package/dist/state/StateStore.js.map +1 -1
  27. package/dist/state/migrations.js +1 -0
  28. package/dist/state/migrations.js.map +1 -1
  29. package/npm-shrinkwrap.json +2 -2
  30. package/package.json +1 -1
  31. package/skills/agent-bot/SKILL.md +1 -1
  32. package/src/cli/help.ts +6 -6
  33. package/src/cli/initOptions.ts +1 -10
  34. package/src/cli.ts +4 -5
  35. package/src/feishu/CardRenderer.ts +41 -36
  36. package/src/feishu/FeishuConnector.ts +27 -7
  37. package/src/feishu/FeishuMessageClient.ts +110 -0
  38. package/src/feishu/FeishuTurnPresenter.ts +24 -3
  39. package/src/feishu/InboundText.ts +12 -0
  40. package/src/feishu/MergedForwardMessage.ts +372 -0
  41. package/src/feishu/types.ts +15 -0
  42. package/src/presentation/OutboundRouter.ts +28 -0
  43. package/src/proxy/ProxySessionController.ts +434 -20
  44. package/src/state/StateStore.ts +50 -2
  45. package/src/state/migrations.ts +1 -0
@@ -7,6 +7,7 @@ import { CommandRouter } from "../commands/CommandRouter.js";
7
7
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
8
8
  import { CardRenderer, } from "../feishu/CardRenderer.js";
9
9
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
10
+ import { normalizeFeishuPostText } from "../feishu/InboundText.js";
10
11
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
11
12
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
12
13
  import { createId } from "../utils/id.js";
@@ -23,6 +24,9 @@ const RESET_HISTORY_PAGE_SIZE = 10;
23
24
  const MAX_LLM_TURN_RETRIES = 3;
24
25
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
25
26
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
27
+ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
28
+ const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
29
+ const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
26
30
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
27
31
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
28
32
  const IMAGE_FILE_EXTENSIONS = new Set([
@@ -175,6 +179,9 @@ export class ProxySessionController {
175
179
  llmRetryingSessions = new Set();
176
180
  retriedFailureTurnIds = new Set();
177
181
  recoveryRetryTimers = new Map();
182
+ pendingMergedForwards = new Map();
183
+ pendingResourceForwards = new Map();
184
+ relatedReactionMessageIds = new Map();
178
185
  unsubscribe = [];
179
186
  recoveryActivityHeartbeat;
180
187
  startupRecovery;
@@ -203,6 +210,9 @@ export class ProxySessionController {
203
210
  this.recoveryActivityHeartbeat.unref?.();
204
211
  }
205
212
  async onMessage(message) {
213
+ let forwardedImages = [];
214
+ let forwardedFiles = [];
215
+ let coalescedForwardAttachment = false;
206
216
  if (!message.contextKey.startsWith("console:") && !allowsFeishuUser(this.config, message.userId)) {
207
217
  this.logger.debug({ messageId: message.messageId }, "Ignored a Feishu message from a non-owner before acknowledgement.");
208
218
  return;
@@ -217,6 +227,15 @@ export class ProxySessionController {
217
227
  // retries cannot add duplicate reactions.
218
228
  if (!this.store.claimInboundEvent(message.messageId, "message"))
219
229
  return;
230
+ const attachmentReservation = message.mergedForwardMessageId
231
+ ? undefined
232
+ : this.reserveForwardAttachment(message);
233
+ const pendingMergedForward = message.mergedForwardMessageId
234
+ ? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
235
+ : undefined;
236
+ const pendingResourceForward = !attachmentReservation && isStandaloneResourceMessage(message)
237
+ ? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
238
+ : undefined;
220
239
  try {
221
240
  const reactionId = await this.outbound.addReaction(message.contextKey, message.messageId, MESSAGE_RECEIVED_REACTION);
222
241
  if (reactionId) {
@@ -226,6 +245,13 @@ export class ProxySessionController {
226
245
  catch (error) {
227
246
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
228
247
  }
248
+ if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
249
+ this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
250
+ messageId: message.messageId,
251
+ parentMessageId: message.parentMessageId,
252
+ });
253
+ return;
254
+ }
229
255
  if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
230
256
  try {
231
257
  await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
@@ -239,19 +265,135 @@ export class ProxySessionController {
239
265
  this.store.recordChatContext(chatContextKey, message.chatType);
240
266
  this.store.markChatActive(chatContextKey);
241
267
  }
268
+ if (pendingResourceForward) {
269
+ const resourceMessage = message;
270
+ const attachment = await this.waitForForwardAttachment(message.messageId, pendingResourceForward, this.pendingResourceForwards);
271
+ if (attachment) {
272
+ coalescedForwardAttachment = true;
273
+ const instruction = normalizeFeishuPostText(attachment.text) || defaultResourcePrompt(resourceMessage);
274
+ forwardedImages = (resourceMessage.images ?? []).map((image) => ({
275
+ messageId: resourceMessage.messageId,
276
+ imageKey: image.imageKey,
277
+ }));
278
+ forwardedFiles = (resourceMessage.files ?? []).map((file) => ({
279
+ messageId: resourceMessage.messageId,
280
+ fileKey: file.fileKey,
281
+ fileName: file.fileName,
282
+ }));
283
+ message = {
284
+ ...attachment,
285
+ text: instruction,
286
+ displayText: instruction,
287
+ };
288
+ this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
289
+ }
290
+ }
291
+ if (message.mergedForwardMessageId) {
292
+ const mergedForwardMessage = message;
293
+ const mergedForwardResult = this.outbound.readMergedForward(message.contextKey, message.mergedForwardMessageId).then((content) => ({ content }), (error) => ({ error }));
294
+ const attachment = pendingMergedForward
295
+ ? await this.waitForForwardAttachment(message.messageId, pendingMergedForward, this.pendingMergedForwards)
296
+ : undefined;
297
+ if (attachment) {
298
+ coalescedForwardAttachment = true;
299
+ const instruction = normalizeFeishuPostText(attachment.text)
300
+ || ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
301
+ message = {
302
+ ...attachment,
303
+ text: instruction,
304
+ displayText: instruction,
305
+ };
306
+ this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
307
+ }
308
+ const result = await mergedForwardResult;
309
+ if ("error" in result) {
310
+ const replyTarget = message.replyInThread
311
+ ? { messageId: message.messageId, replyInThread: true }
312
+ : undefined;
313
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
314
+ const detail = result.error instanceof Error ? result.error.message : String(result.error);
315
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)));
316
+ return;
317
+ }
318
+ const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
319
+ forwardedImages = result.content.images;
320
+ forwardedFiles = result.content.files;
321
+ message = {
322
+ ...message,
323
+ displayText: instruction,
324
+ text: mergedForwardPrompt(instruction, result.content.text),
325
+ };
326
+ }
327
+ const referencedMessageId = coalescedForwardAttachment ? undefined : quotedMessageId(message);
328
+ if (referencedMessageId) {
329
+ try {
330
+ const referenced = await this.resolveReferencedMessage(message.contextKey, referencedMessageId);
331
+ const instruction = normalizeFeishuPostText(message.displayText ?? message.text)
332
+ || DEFAULT_REFERENCED_MESSAGE_INSTRUCTION;
333
+ forwardedImages = [...referenced.images, ...forwardedImages];
334
+ forwardedFiles = [...referenced.files, ...forwardedFiles];
335
+ message = {
336
+ ...message,
337
+ displayText: instruction,
338
+ text: referencedMessagePrompt(instruction, referenced.text),
339
+ };
340
+ }
341
+ catch (error) {
342
+ const replyTarget = message.replyInThread
343
+ ? { messageId: message.messageId, replyInThread: true }
344
+ : undefined;
345
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
346
+ const detail = error instanceof Error ? error.message : String(error);
347
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)));
348
+ return;
349
+ }
350
+ }
242
351
  const replyTarget = message.replyInThread
243
352
  ? { messageId: message.messageId, replyInThread: true }
244
353
  : undefined;
245
- const imageCount = message.images?.length ?? 0;
354
+ const imageReferences = deduplicateImageReferences([
355
+ ...forwardedImages,
356
+ ...(message.images ?? []).map((image) => ({ messageId: message.messageId, imageKey: image.imageKey })),
357
+ ]);
358
+ const fileReferences = deduplicateFileReferences([
359
+ ...forwardedFiles,
360
+ ...(message.files ?? []).map((file) => ({
361
+ messageId: message.messageId,
362
+ fileKey: file.fileKey,
363
+ fileName: file.fileName,
364
+ })),
365
+ ]);
366
+ const imageCount = imageReferences.length;
367
+ const fileCount = fileReferences.length;
246
368
  this.store.audit(message.contextKey, "incoming_message", {
247
369
  messageId: message.messageId,
248
370
  text: message.text,
249
371
  ...(imageCount > 0 ? { imageCount } : {}),
372
+ ...(fileCount > 0 ? { fileCount } : {}),
250
373
  });
251
374
  let localImagePaths;
252
375
  if (imageCount > 0) {
253
376
  try {
254
- localImagePaths = await Promise.all(message.images.map((image) => this.outbound.downloadImage(message.contextKey, message.messageId, image.imageKey)));
377
+ localImagePaths = await Promise.all(imageReferences.map((image) => this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
378
+ }
379
+ catch (error) {
380
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
381
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
382
+ return;
383
+ }
384
+ }
385
+ if (fileCount > 0) {
386
+ try {
387
+ const downloadedFiles = await Promise.all(fileReferences.map(async (file) => ({
388
+ fileName: file.fileName,
389
+ filePath: await this.outbound.downloadFile(message.contextKey, file.messageId, file.fileKey, file.fileName),
390
+ })));
391
+ const instruction = message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount);
392
+ message = {
393
+ ...message,
394
+ displayText: message.displayText ?? instruction,
395
+ text: appendDownloadedFiles(instruction, downloadedFiles),
396
+ };
255
397
  }
256
398
  catch (error) {
257
399
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
@@ -261,8 +403,9 @@ export class ProxySessionController {
261
403
  }
262
404
  let command;
263
405
  try {
264
- command = imageCount > 0 && !message.text.trimStart().startsWith("/")
265
- ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
406
+ const resourceCount = imageCount + fileCount;
407
+ command = resourceCount > 0 && !message.text.trimStart().startsWith("/")
408
+ ? { type: "prompt", text: message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount) }
266
409
  : this.router.parse(message.text);
267
410
  }
268
411
  catch (error) {
@@ -567,6 +710,13 @@ export class ProxySessionController {
567
710
  this.queuedPromptCards.clear();
568
711
  this.queuedPromptCardWrites.clear();
569
712
  this.sessionResets.clear();
713
+ for (const pending of this.pendingMergedForwards.values())
714
+ pending.resolveAttachment(undefined);
715
+ this.pendingMergedForwards.clear();
716
+ for (const pending of this.pendingResourceForwards.values())
717
+ pending.resolveAttachment(undefined);
718
+ this.pendingResourceForwards.clear();
719
+ this.relatedReactionMessageIds.clear();
570
720
  for (const timer of this.recoveryRetryTimers.values())
571
721
  clearTimeout(timer);
572
722
  this.recoveryRetryTimers.clear();
@@ -1102,7 +1252,7 @@ export class ProxySessionController {
1102
1252
  await this.setTitle(contextKey, command.title);
1103
1253
  return;
1104
1254
  case "prompt":
1105
- await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
1255
+ await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths, incomingMessage?.displayText);
1106
1256
  return;
1107
1257
  case "nosteer":
1108
1258
  await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
@@ -1513,22 +1663,23 @@ export class ProxySessionController {
1513
1663
  await this.finalizeTurnMessageReactions(turnId, status);
1514
1664
  }
1515
1665
  }
1516
- async prompt(contextKey, text, messageId, replyTarget, localImagePaths) {
1666
+ async prompt(contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
1517
1667
  if (!text.trim())
1518
1668
  throw new Error("请输入要交给 Agent 的内容。");
1519
1669
  let record = this.currentSession(contextKey);
1520
1670
  if (!record) {
1521
1671
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1522
- record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, true, text, replyTarget);
1672
+ record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, true, displayPrompt ?? text, replyTarget);
1523
1673
  }
1524
- await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
1674
+ await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
1525
1675
  }
1526
- async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths) {
1676
+ async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
1527
1677
  if (this.llmRetryingSessions.has(record.localSessionId)) {
1528
1678
  const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
1529
1679
  localImagePaths,
1530
1680
  messageId,
1531
1681
  replyTarget,
1682
+ displayPrompt,
1532
1683
  });
1533
1684
  this.store.audit(contextKey, "queued_prompt_added_during_llm_retry", {
1534
1685
  promptId: queued.promptId,
@@ -1540,7 +1691,7 @@ export class ProxySessionController {
1540
1691
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
1541
1692
  if (!configuredRuntime.getSession(record.localSessionId)) {
1542
1693
  this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
1543
- await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
1694
+ await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text);
1544
1695
  }
1545
1696
  let loaded;
1546
1697
  try {
@@ -1574,12 +1725,13 @@ export class ProxySessionController {
1574
1725
  localImagePaths,
1575
1726
  messageId,
1576
1727
  replyTarget,
1728
+ displayPrompt,
1577
1729
  });
1578
1730
  return;
1579
1731
  }
1580
1732
  try {
1581
1733
  await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
1582
- await this.presentSteerMessage(record.localSessionId, activeTurnId, text, messageId);
1734
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
1583
1735
  if (messageId)
1584
1736
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
1585
1737
  return;
@@ -1594,7 +1746,7 @@ export class ProxySessionController {
1594
1746
  this.logger.warn({ error: syncError, sessionId: record.localSessionId }, "Failed to synchronize after steering failure.");
1595
1747
  }
1596
1748
  if (!current?.activeTurnId) {
1597
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
1749
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
1598
1750
  if (messageId)
1599
1751
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
1600
1752
  return;
@@ -1602,7 +1754,7 @@ export class ProxySessionController {
1602
1754
  if (current.activeTurnId !== activeTurnId) {
1603
1755
  try {
1604
1756
  await loaded.runtime.steerTurn(record.localSessionId, current.activeTurnId, runtimePrompt(text, localImagePaths));
1605
- await this.presentSteerMessage(record.localSessionId, current.activeTurnId, text, messageId);
1757
+ await this.presentSteerMessage(record.localSessionId, current.activeTurnId, displayPrompt ?? text, messageId);
1606
1758
  if (messageId)
1607
1759
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
1608
1760
  return;
@@ -1615,11 +1767,12 @@ export class ProxySessionController {
1615
1767
  localImagePaths,
1616
1768
  messageId,
1617
1769
  replyTarget,
1770
+ displayPrompt,
1618
1771
  });
1619
1772
  return;
1620
1773
  }
1621
1774
  }
1622
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
1775
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
1623
1776
  if (messageId)
1624
1777
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
1625
1778
  }
@@ -1651,6 +1804,7 @@ export class ProxySessionController {
1651
1804
  localImagePaths: options.localImagePaths,
1652
1805
  messageId: options.messageId,
1653
1806
  replyMessageId: options.replyTarget?.messageId,
1807
+ displayPrompt: options.displayPrompt,
1654
1808
  });
1655
1809
  }
1656
1810
  async cancelQueuedPrompt(action) {
@@ -1711,7 +1865,7 @@ export class ProxySessionController {
1711
1865
  contextKey,
1712
1866
  prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
1713
1867
  id: prompt.promptId,
1714
- text: prompt.text,
1868
+ text: prompt.displayPrompt ?? prompt.text,
1715
1869
  })),
1716
1870
  });
1717
1871
  }
@@ -2128,7 +2282,7 @@ export class ProxySessionController {
2128
2282
  }
2129
2283
  async startTurn(loaded, text, replyTarget, localImagePaths, options = {}) {
2130
2284
  const currentRecord = this.store.getSession(loaded.record.localSessionId);
2131
- const title = currentRecord?.title ?? normalizeTaskTitle(text);
2285
+ const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
2132
2286
  if (!currentRecord?.title && title)
2133
2287
  this.store.updateRuntimeSession(loaded.record.localSessionId, { title });
2134
2288
  if (title)
@@ -2607,7 +2761,7 @@ export class ProxySessionController {
2607
2761
  const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
2608
2762
  loaded = { ...loaded, record };
2609
2763
  await this.refreshPromptQueueCards(sessionId);
2610
- const turnId = await this.startTurn(loaded, prompt.text, prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined, prompt.localImagePaths, { messageId: prompt.messageId });
2764
+ const turnId = await this.startTurn(loaded, prompt.text, prompt.replyMessageId ? { messageId: prompt.replyMessageId, replyInThread: true } : undefined, prompt.localImagePaths, { messageId: prompt.messageId, displayPrompt: prompt.displayPrompt });
2611
2765
  if (prompt.messageId)
2612
2766
  await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
2613
2767
  }
@@ -2633,13 +2787,17 @@ export class ProxySessionController {
2633
2787
  : this.store.findLatestCompletedTurnId(source.localSessionId, source.contextKey);
2634
2788
  }
2635
2789
  async finalizeStandaloneMessageReaction(messageId, status) {
2636
- const reaction = this.store.claimMessageReaction(messageId);
2637
- if (reaction)
2638
- await this.replaceMessageReaction(reaction, status);
2790
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
2791
+ const reaction = this.store.claimMessageReaction(relatedMessageId);
2792
+ if (reaction)
2793
+ await this.replaceMessageReaction(reaction, status);
2794
+ }
2639
2795
  }
2640
2796
  async bindMessageReactionToTurn(messageId, sessionId, turnId) {
2641
- this.store.bindMessageToTurn(messageId, sessionId, turnId);
2642
- this.store.bindMessageReaction(messageId, sessionId, turnId);
2797
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
2798
+ this.store.bindMessageToTurn(relatedMessageId, sessionId, turnId);
2799
+ this.store.bindMessageReaction(relatedMessageId, sessionId, turnId);
2800
+ }
2643
2801
  const session = this.store.getSession(sessionId);
2644
2802
  if (session?.lastTurnId !== turnId)
2645
2803
  return;
@@ -2647,6 +2805,90 @@ export class ProxySessionController {
2647
2805
  await this.finalizeTurnMessageReactions(turnId, session.lastTurnStatus);
2648
2806
  }
2649
2807
  }
2808
+ registerPendingForwardAttachment(message, registry) {
2809
+ let resolveAttachment;
2810
+ const attachmentPromise = new Promise((resolve) => {
2811
+ resolveAttachment = resolve;
2812
+ });
2813
+ const pending = {
2814
+ contextKey: message.contextKey,
2815
+ attachmentPromise,
2816
+ resolveAttachment,
2817
+ };
2818
+ registry.set(message.messageId, pending);
2819
+ return pending;
2820
+ }
2821
+ reserveForwardAttachment(message) {
2822
+ if (!message.parentMessageId)
2823
+ return undefined;
2824
+ const candidates = [
2825
+ { sourceKind: "merged_forward", registry: this.pendingMergedForwards },
2826
+ { sourceKind: "resource", registry: this.pendingResourceForwards },
2827
+ ];
2828
+ for (const candidate of candidates) {
2829
+ const pending = candidate.registry.get(message.parentMessageId);
2830
+ if (!pending || pending.contextKey !== message.contextKey || pending.attachmentMessageId)
2831
+ continue;
2832
+ pending.attachmentMessageId = message.messageId;
2833
+ return {
2834
+ sourceMessageId: message.parentMessageId,
2835
+ sourceKind: candidate.sourceKind,
2836
+ pending,
2837
+ registry: candidate.registry,
2838
+ };
2839
+ }
2840
+ return undefined;
2841
+ }
2842
+ completeForwardAttachment(message, reservation) {
2843
+ if (!message.parentMessageId)
2844
+ return false;
2845
+ if (reservation.sourceMessageId !== message.parentMessageId)
2846
+ return false;
2847
+ if (reservation.registry.get(message.parentMessageId) !== reservation.pending)
2848
+ return false;
2849
+ if (reservation.pending.attachmentMessageId !== message.messageId)
2850
+ return false;
2851
+ reservation.pending.resolveAttachment(message);
2852
+ return true;
2853
+ }
2854
+ async waitForForwardAttachment(messageId, pending, registry) {
2855
+ let timer;
2856
+ const timeout = new Promise((resolve) => {
2857
+ timer = setTimeout(() => resolve(undefined), FORWARD_ATTACHMENT_WINDOW_MS);
2858
+ timer.unref?.();
2859
+ });
2860
+ try {
2861
+ return await Promise.race([pending.attachmentPromise, timeout]);
2862
+ }
2863
+ finally {
2864
+ if (timer)
2865
+ clearTimeout(timer);
2866
+ if (registry.get(messageId) === pending) {
2867
+ registry.delete(messageId);
2868
+ }
2869
+ }
2870
+ }
2871
+ async resolveReferencedMessage(contextKey, messageId) {
2872
+ const local = this.store.findAgentBotTurnMessageById(messageId);
2873
+ if (local) {
2874
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(local.turnId));
2875
+ const text = snapshot ? renderLocalTurnReference(snapshot, local.messageKind) : undefined;
2876
+ if (text) {
2877
+ return {
2878
+ text,
2879
+ messageType: local.messageKind === "final" ? "agent_bot_reply" : "agent_bot_progress",
2880
+ images: [],
2881
+ files: [],
2882
+ };
2883
+ }
2884
+ }
2885
+ return this.outbound.readReferencedMessage(contextKey, messageId);
2886
+ }
2887
+ takeRelatedReactionMessageIds(messageId) {
2888
+ const related = this.relatedReactionMessageIds.get(messageId) ?? [];
2889
+ this.relatedReactionMessageIds.delete(messageId);
2890
+ return [...new Set([messageId, ...related])];
2891
+ }
2650
2892
  async finalizeTurnMessageReactions(turnId, status) {
2651
2893
  for (const reaction of this.store.claimMessageReactionsForTurn(turnId)) {
2652
2894
  await this.replaceMessageReaction(reaction, status);
@@ -4941,6 +5183,86 @@ function isPromptCommand(command) {
4941
5183
  function runtimePrompt(text, localImagePaths) {
4942
5184
  return localImagePaths?.length ? { text, localImagePaths } : text;
4943
5185
  }
5186
+ function mergedForwardPrompt(instruction, transcript) {
5187
+ return `${instruction}\n\n参考聊天记录:\n${transcript}`;
5188
+ }
5189
+ function referencedMessagePrompt(instruction, referencedMessage) {
5190
+ return `${instruction}\n\n引用消息:\n${referencedMessage}`;
5191
+ }
5192
+ function quotedMessageId(message) {
5193
+ const messageId = message.parentMessageId?.trim();
5194
+ if (!messageId || message.contextKey.startsWith("console:"))
5195
+ return undefined;
5196
+ if (message.mergedForwardMessageId)
5197
+ return undefined;
5198
+ const text = message.text.trimStart();
5199
+ if (text.startsWith("/") || text.startsWith("!"))
5200
+ return undefined;
5201
+ if (message.threadContext && message.rootMessageId === messageId)
5202
+ return undefined;
5203
+ return messageId;
5204
+ }
5205
+ function renderLocalTurnReference(snapshot, messageKind) {
5206
+ if (messageKind === "final") {
5207
+ const response = snapshot.finalResponse?.trim() || snapshot.assistantText?.trim();
5208
+ return response
5209
+ ? `[消息类型:AgentBot 回复]\n${truncateMiddle(response, 48_000)}`
5210
+ : undefined;
5211
+ }
5212
+ const activityText = (snapshot.activities ?? [])
5213
+ .filter((activity) => activity.kind === "assistant" || activity.kind === "reasoning")
5214
+ .map((activity) => activity.text.trim())
5215
+ .filter(Boolean)
5216
+ .join("\n\n");
5217
+ const content = [
5218
+ snapshot.prompt?.trim() ? `用户请求:\n${snapshot.prompt.trim()}` : "",
5219
+ activityText,
5220
+ snapshot.progressText?.trim() || "",
5221
+ snapshot.finalResponse?.trim() || snapshot.assistantText?.trim() || "",
5222
+ snapshot.error?.trim() ? `错误:${snapshot.error.trim()}` : "",
5223
+ ].filter(Boolean).join("\n\n");
5224
+ return content
5225
+ ? `[消息类型:AgentBot 思考卡片]\n${truncateMiddle(content, 48_000)}`
5226
+ : undefined;
5227
+ }
5228
+ function appendDownloadedFiles(prompt, files) {
5229
+ return `${prompt}\n\n参考文件(已下载到本地):\n${files.map((file, index) => `[文件 ${index + 1}:${file.fileName}] ${file.filePath}`).join("\n")}`;
5230
+ }
5231
+ function defaultResourcePrompt(message) {
5232
+ return defaultResourcePromptFromCounts(message.images?.length ?? 0, message.files?.length ?? 0);
5233
+ }
5234
+ function defaultResourcePromptFromCounts(imageCount, fileCount) {
5235
+ if (imageCount > 0 && fileCount > 0)
5236
+ return "请查看附带的图片和文件";
5237
+ if (fileCount > 0)
5238
+ return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
5239
+ return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
5240
+ }
5241
+ function isStandaloneResourceMessage(message) {
5242
+ return !message.mergedForwardMessageId
5243
+ && message.text.trim().length === 0
5244
+ && ((message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
5245
+ }
5246
+ function deduplicateImageReferences(images) {
5247
+ const seen = new Set();
5248
+ return images.filter((image) => {
5249
+ const key = `${image.messageId}:${image.imageKey}`;
5250
+ if (seen.has(key))
5251
+ return false;
5252
+ seen.add(key);
5253
+ return true;
5254
+ });
5255
+ }
5256
+ function deduplicateFileReferences(files) {
5257
+ const seen = new Set();
5258
+ return files.filter((file) => {
5259
+ const key = `${file.messageId}:${file.fileKey}`;
5260
+ if (seen.has(key))
5261
+ return false;
5262
+ seen.add(key);
5263
+ return true;
5264
+ });
5265
+ }
4944
5266
  function recoveryCardPrompt(prompt) {
4945
5267
  return `恢复重启前的任务:${prompt}`;
4946
5268
  }