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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +29 -24
  3. package/README.zh.md +29 -24
  4. package/config.example.yaml +1 -0
  5. package/dist/cli/FeishuAppConfiguration.js +42 -3
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli/help.js +6 -6
  8. package/dist/cli/initOptions.js +1 -4
  9. package/dist/cli/initOptions.js.map +1 -1
  10. package/dist/cli.js +8 -5
  11. package/dist/cli.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +91 -45
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/feishu/FeishuConnector.js +39 -12
  15. package/dist/feishu/FeishuConnector.js.map +1 -1
  16. package/dist/feishu/FeishuMarkdown.js +58 -6
  17. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  18. package/dist/feishu/FeishuMessageClient.js +108 -4
  19. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  20. package/dist/feishu/FeishuTurnPresenter.js +24 -1
  21. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  22. package/dist/feishu/FinalResponseBranding.js +15 -0
  23. package/dist/feishu/FinalResponseBranding.js.map +1 -0
  24. package/dist/feishu/InboundText.js +13 -0
  25. package/dist/feishu/InboundText.js.map +1 -0
  26. package/dist/feishu/MergedForwardMessage.js +317 -0
  27. package/dist/feishu/MergedForwardMessage.js.map +1 -0
  28. package/dist/logging/DailyLogStream.js +73 -0
  29. package/dist/logging/DailyLogStream.js.map +1 -0
  30. package/dist/logging/logger.js +2 -1
  31. package/dist/logging/logger.js.map +1 -1
  32. package/dist/presentation/OutboundRouter.js +21 -0
  33. package/dist/presentation/OutboundRouter.js.map +1 -1
  34. package/dist/proxy/ProxySessionController.js +536 -58
  35. package/dist/proxy/ProxySessionController.js.map +1 -1
  36. package/dist/state/StateStore.js +161 -3
  37. package/dist/state/StateStore.js.map +1 -1
  38. package/dist/state/migrations.js +12 -0
  39. package/dist/state/migrations.js.map +1 -1
  40. package/dist/supervision/SupervisorDiagnostics.js +37 -21
  41. package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
  42. package/dist/supervisor.js +11 -4
  43. package/dist/supervisor.js.map +1 -1
  44. package/dist/utils/executeShellCommand.js +83 -16
  45. package/dist/utils/executeShellCommand.js.map +1 -1
  46. package/docs/technical-reference.md +9 -8
  47. package/docs/technical-reference.zh.md +9 -8
  48. package/npm-shrinkwrap.json +2 -2
  49. package/package.json +1 -1
  50. package/skills/agent-bot/SKILL.md +1 -1
  51. package/src/cli/FeishuAppConfiguration.ts +76 -3
  52. package/src/cli/help.ts +6 -6
  53. package/src/cli/initOptions.ts +1 -10
  54. package/src/cli.ts +10 -6
  55. package/src/feishu/CardRenderer.ts +116 -45
  56. package/src/feishu/FeishuConnector.ts +55 -13
  57. package/src/feishu/FeishuMarkdown.ts +64 -5
  58. package/src/feishu/FeishuMessageClient.ts +151 -12
  59. package/src/feishu/FeishuTurnPresenter.ts +28 -4
  60. package/src/feishu/FinalResponseBranding.ts +18 -0
  61. package/src/feishu/InboundText.ts +12 -0
  62. package/src/feishu/MergedForwardMessage.ts +372 -0
  63. package/src/feishu/types.ts +15 -0
  64. package/src/logging/DailyLogStream.ts +88 -0
  65. package/src/logging/logger.ts +2 -1
  66. package/src/presentation/OutboundRouter.ts +28 -0
  67. package/src/proxy/ProxySessionController.ts +642 -55
  68. package/src/state/StateStore.ts +230 -2
  69. package/src/state/migrations.ts +12 -0
  70. package/src/supervision/SupervisorDiagnostics.ts +41 -25
  71. package/src/supervisor.ts +10 -4
  72. package/src/utils/executeShellCommand.ts +103 -15
@@ -6,11 +6,13 @@ import { resolveUserPath } from "../config/paths.js";
6
6
  import { CommandRouter } from "../commands/CommandRouter.js";
7
7
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
8
8
  import { CardRenderer, } from "../feishu/CardRenderer.js";
9
+ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
9
10
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
11
+ import { normalizeFeishuPostText } from "../feishu/InboundText.js";
10
12
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
11
13
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
12
14
  import { createId } from "../utils/id.js";
13
- import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
15
+ import { truncateMiddle, truncateText } from "../utils/markdown.js";
14
16
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
15
17
  import { executeShellCommand, } from "../utils/executeShellCommand.js";
16
18
  const MESSAGE_RECEIVED_REACTION = "OnIt";
@@ -23,6 +25,10 @@ const RESET_HISTORY_PAGE_SIZE = 10;
23
25
  const MAX_LLM_TURN_RETRIES = 3;
24
26
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
25
27
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
28
+ const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
29
+ const FORWARD_ATTACHMENT_WINDOW_MS = 800;
30
+ const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
31
+ const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
26
32
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
27
33
  const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
28
34
  const IMAGE_FILE_EXTENSIONS = new Set([
@@ -175,6 +181,9 @@ export class ProxySessionController {
175
181
  llmRetryingSessions = new Set();
176
182
  retriedFailureTurnIds = new Set();
177
183
  recoveryRetryTimers = new Map();
184
+ pendingMergedForwards = new Map();
185
+ pendingResourceForwards = new Map();
186
+ relatedReactionMessageIds = new Map();
178
187
  unsubscribe = [];
179
188
  recoveryActivityHeartbeat;
180
189
  startupRecovery;
@@ -203,6 +212,9 @@ export class ProxySessionController {
203
212
  this.recoveryActivityHeartbeat.unref?.();
204
213
  }
205
214
  async onMessage(message) {
215
+ let forwardedImages = [];
216
+ let forwardedFiles = [];
217
+ let coalescedForwardAttachment = false;
206
218
  if (!message.contextKey.startsWith("console:") && !allowsFeishuUser(this.config, message.userId)) {
207
219
  this.logger.debug({ messageId: message.messageId }, "Ignored a Feishu message from a non-owner before acknowledgement.");
208
220
  return;
@@ -217,6 +229,15 @@ export class ProxySessionController {
217
229
  // retries cannot add duplicate reactions.
218
230
  if (!this.store.claimInboundEvent(message.messageId, "message"))
219
231
  return;
232
+ const attachmentReservation = message.mergedForwardMessageId
233
+ ? undefined
234
+ : this.reserveForwardAttachment(message);
235
+ const pendingMergedForward = message.mergedForwardMessageId
236
+ ? this.registerPendingForwardAttachment(message, this.pendingMergedForwards)
237
+ : undefined;
238
+ const pendingResourceForward = !attachmentReservation && isStandaloneResourceMessage(message)
239
+ ? this.registerPendingForwardAttachment(message, this.pendingResourceForwards)
240
+ : undefined;
220
241
  try {
221
242
  const reactionId = await this.outbound.addReaction(message.contextKey, message.messageId, MESSAGE_RECEIVED_REACTION);
222
243
  if (reactionId) {
@@ -226,6 +247,13 @@ export class ProxySessionController {
226
247
  catch (error) {
227
248
  this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
228
249
  }
250
+ if (attachmentReservation && this.completeForwardAttachment(message, attachmentReservation)) {
251
+ this.store.audit(message.contextKey, `${attachmentReservation.sourceKind}_attachment_coalesced`, {
252
+ messageId: message.messageId,
253
+ parentMessageId: message.parentMessageId,
254
+ });
255
+ return;
256
+ }
229
257
  if (message.chatType === "p2p" && message.userId?.startsWith("ou_")) {
230
258
  try {
231
259
  await this.lifecycle?.rememberFeishuUserOpenId?.(message.userId);
@@ -239,19 +267,135 @@ export class ProxySessionController {
239
267
  this.store.recordChatContext(chatContextKey, message.chatType);
240
268
  this.store.markChatActive(chatContextKey);
241
269
  }
270
+ if (pendingResourceForward) {
271
+ const resourceMessage = message;
272
+ const attachment = await this.waitForForwardAttachment(message.messageId, pendingResourceForward, this.pendingResourceForwards);
273
+ if (attachment) {
274
+ coalescedForwardAttachment = true;
275
+ const instruction = normalizeFeishuPostText(attachment.text) || defaultResourcePrompt(resourceMessage);
276
+ forwardedImages = (resourceMessage.images ?? []).map((image) => ({
277
+ messageId: resourceMessage.messageId,
278
+ imageKey: image.imageKey,
279
+ }));
280
+ forwardedFiles = (resourceMessage.files ?? []).map((file) => ({
281
+ messageId: resourceMessage.messageId,
282
+ fileKey: file.fileKey,
283
+ fileName: file.fileName,
284
+ }));
285
+ message = {
286
+ ...attachment,
287
+ text: instruction,
288
+ displayText: instruction,
289
+ };
290
+ this.relatedReactionMessageIds.set(message.messageId, [resourceMessage.messageId]);
291
+ }
292
+ }
293
+ if (message.mergedForwardMessageId) {
294
+ const mergedForwardMessage = message;
295
+ const mergedForwardResult = this.outbound.readMergedForward(message.contextKey, message.mergedForwardMessageId).then((content) => ({ content }), (error) => ({ error }));
296
+ const attachment = pendingMergedForward
297
+ ? await this.waitForForwardAttachment(message.messageId, pendingMergedForward, this.pendingMergedForwards)
298
+ : undefined;
299
+ if (attachment) {
300
+ coalescedForwardAttachment = true;
301
+ const instruction = normalizeFeishuPostText(attachment.text)
302
+ || ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
303
+ message = {
304
+ ...attachment,
305
+ text: instruction,
306
+ displayText: instruction,
307
+ };
308
+ this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
309
+ }
310
+ const result = await mergedForwardResult;
311
+ if ("error" in result) {
312
+ const replyTarget = message.replyInThread
313
+ ? { messageId: message.messageId, replyInThread: true }
314
+ : undefined;
315
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
316
+ const detail = result.error instanceof Error ? result.error.message : String(result.error);
317
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)));
318
+ return;
319
+ }
320
+ const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
321
+ forwardedImages = result.content.images;
322
+ forwardedFiles = result.content.files;
323
+ message = {
324
+ ...message,
325
+ displayText: instruction,
326
+ text: mergedForwardPrompt(instruction, result.content.text),
327
+ };
328
+ }
329
+ const referencedMessageId = coalescedForwardAttachment ? undefined : quotedMessageId(message);
330
+ if (referencedMessageId) {
331
+ try {
332
+ const referenced = await this.resolveReferencedMessage(message.contextKey, referencedMessageId);
333
+ const instruction = normalizeFeishuPostText(message.displayText ?? message.text)
334
+ || DEFAULT_REFERENCED_MESSAGE_INSTRUCTION;
335
+ forwardedImages = [...referenced.images, ...forwardedImages];
336
+ forwardedFiles = [...referenced.files, ...forwardedFiles];
337
+ message = {
338
+ ...message,
339
+ displayText: instruction,
340
+ text: referencedMessagePrompt(instruction, referenced.text),
341
+ };
342
+ }
343
+ catch (error) {
344
+ const replyTarget = message.replyInThread
345
+ ? { messageId: message.messageId, replyInThread: true }
346
+ : undefined;
347
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
348
+ const detail = error instanceof Error ? error.message : String(error);
349
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)));
350
+ return;
351
+ }
352
+ }
242
353
  const replyTarget = message.replyInThread
243
354
  ? { messageId: message.messageId, replyInThread: true }
244
355
  : undefined;
245
- const imageCount = message.images?.length ?? 0;
356
+ const imageReferences = deduplicateImageReferences([
357
+ ...forwardedImages,
358
+ ...(message.images ?? []).map((image) => ({ messageId: message.messageId, imageKey: image.imageKey })),
359
+ ]);
360
+ const fileReferences = deduplicateFileReferences([
361
+ ...forwardedFiles,
362
+ ...(message.files ?? []).map((file) => ({
363
+ messageId: message.messageId,
364
+ fileKey: file.fileKey,
365
+ fileName: file.fileName,
366
+ })),
367
+ ]);
368
+ const imageCount = imageReferences.length;
369
+ const fileCount = fileReferences.length;
246
370
  this.store.audit(message.contextKey, "incoming_message", {
247
371
  messageId: message.messageId,
248
372
  text: message.text,
249
373
  ...(imageCount > 0 ? { imageCount } : {}),
374
+ ...(fileCount > 0 ? { fileCount } : {}),
250
375
  });
251
376
  let localImagePaths;
252
377
  if (imageCount > 0) {
253
378
  try {
254
- localImagePaths = await Promise.all(message.images.map((image) => this.outbound.downloadImage(message.contextKey, message.messageId, image.imageKey)));
379
+ localImagePaths = await Promise.all(imageReferences.map((image) => this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
380
+ }
381
+ catch (error) {
382
+ await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
383
+ await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error));
384
+ return;
385
+ }
386
+ }
387
+ if (fileCount > 0) {
388
+ try {
389
+ const downloadedFiles = await Promise.all(fileReferences.map(async (file) => ({
390
+ fileName: file.fileName,
391
+ filePath: await this.outbound.downloadFile(message.contextKey, file.messageId, file.fileKey, file.fileName),
392
+ })));
393
+ const instruction = message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount);
394
+ message = {
395
+ ...message,
396
+ displayText: message.displayText ?? instruction,
397
+ text: appendDownloadedFiles(instruction, downloadedFiles),
398
+ };
255
399
  }
256
400
  catch (error) {
257
401
  await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
@@ -261,8 +405,9 @@ export class ProxySessionController {
261
405
  }
262
406
  let command;
263
407
  try {
264
- command = imageCount > 0 && !message.text.trimStart().startsWith("/")
265
- ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
408
+ const resourceCount = imageCount + fileCount;
409
+ command = resourceCount > 0 && !message.text.trimStart().startsWith("/")
410
+ ? { type: "prompt", text: message.text.trim() || defaultResourcePromptFromCounts(imageCount, fileCount) }
266
411
  : this.router.parse(message.text);
267
412
  }
268
413
  catch (error) {
@@ -274,7 +419,7 @@ export class ProxySessionController {
274
419
  if (isQueueIndependentCommand(command)) {
275
420
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
276
421
  try {
277
- if (command.type !== "forkgroup" && command.type !== "dismiss")
422
+ if (command.type === "prompt")
278
423
  await this.ensureThreadFork(message);
279
424
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
280
425
  if (!isPromptCommand(command))
@@ -290,7 +435,7 @@ export class ProxySessionController {
290
435
  const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
291
436
  const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
292
437
  try {
293
- if (command.type !== "forkgroup")
438
+ if (command.type === "prompt")
294
439
  await this.ensureThreadFork(message);
295
440
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
296
441
  if (!isPromptCommand(command))
@@ -567,6 +712,13 @@ export class ProxySessionController {
567
712
  this.queuedPromptCards.clear();
568
713
  this.queuedPromptCardWrites.clear();
569
714
  this.sessionResets.clear();
715
+ for (const pending of this.pendingMergedForwards.values())
716
+ pending.resolveAttachment(undefined);
717
+ this.pendingMergedForwards.clear();
718
+ for (const pending of this.pendingResourceForwards.values())
719
+ pending.resolveAttachment(undefined);
720
+ this.pendingResourceForwards.clear();
721
+ this.relatedReactionMessageIds.clear();
570
722
  for (const timer of this.recoveryRetryTimers.values())
571
723
  clearTimeout(timer);
572
724
  this.recoveryRetryTimers.clear();
@@ -945,8 +1097,7 @@ export class ProxySessionController {
945
1097
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
946
1098
  throw new Error("The task is not an App Server task that can be reset.");
947
1099
  }
948
- const contextKey = this.controlSessionContextKey(session);
949
- const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
1100
+ const rows = this.store.listTaskTurnGraph(session.localSessionId);
950
1101
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
951
1102
  return {
952
1103
  session,
@@ -1069,6 +1220,9 @@ export class ProxySessionController {
1069
1220
  }
1070
1221
  async execute(contextKey, command, messageId, replyTarget, localImagePaths, userId, incomingMessage) {
1071
1222
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1223
+ if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
1224
+ throw new Error(unboundThreadTaskMessage());
1225
+ }
1072
1226
  switch (command.type) {
1073
1227
  case "shell":
1074
1228
  await this.runShellCommand(contextKey, command.command);
@@ -1102,7 +1256,7 @@ export class ProxySessionController {
1102
1256
  await this.setTitle(contextKey, command.title);
1103
1257
  return;
1104
1258
  case "prompt":
1105
- await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
1259
+ await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths, incomingMessage?.displayText);
1106
1260
  return;
1107
1261
  case "nosteer":
1108
1262
  await this.enqueueNoSteerPrompt(contextKey, command.text, messageId, replyTarget);
@@ -1513,22 +1667,23 @@ export class ProxySessionController {
1513
1667
  await this.finalizeTurnMessageReactions(turnId, status);
1514
1668
  }
1515
1669
  }
1516
- async prompt(contextKey, text, messageId, replyTarget, localImagePaths) {
1670
+ async prompt(contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
1517
1671
  if (!text.trim())
1518
1672
  throw new Error("请输入要交给 Agent 的内容。");
1519
1673
  let record = this.currentSession(contextKey);
1520
1674
  if (!record) {
1521
1675
  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);
1676
+ record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, true, displayPrompt ?? text, replyTarget);
1523
1677
  }
1524
- await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
1678
+ await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt);
1525
1679
  }
1526
- async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths) {
1680
+ async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths, displayPrompt) {
1527
1681
  if (this.llmRetryingSessions.has(record.localSessionId)) {
1528
1682
  const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, text, {
1529
1683
  localImagePaths,
1530
1684
  messageId,
1531
1685
  replyTarget,
1686
+ displayPrompt,
1532
1687
  });
1533
1688
  this.store.audit(contextKey, "queued_prompt_added_during_llm_retry", {
1534
1689
  promptId: queued.promptId,
@@ -1540,7 +1695,7 @@ export class ProxySessionController {
1540
1695
  const configuredRuntime = this.runtimes.forAgent(record.agentName);
1541
1696
  if (!configuredRuntime.getSession(record.localSessionId)) {
1542
1697
  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);
1698
+ await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, displayPrompt ?? text);
1544
1699
  }
1545
1700
  let loaded;
1546
1701
  try {
@@ -1574,12 +1729,13 @@ export class ProxySessionController {
1574
1729
  localImagePaths,
1575
1730
  messageId,
1576
1731
  replyTarget,
1732
+ displayPrompt,
1577
1733
  });
1578
1734
  return;
1579
1735
  }
1580
1736
  try {
1581
1737
  await loaded.runtime.steerTurn(record.localSessionId, activeTurnId, runtimePrompt(text, localImagePaths));
1582
- await this.presentSteerMessage(record.localSessionId, activeTurnId, text, messageId);
1738
+ await this.presentSteerMessage(record.localSessionId, activeTurnId, displayPrompt ?? text, messageId);
1583
1739
  if (messageId)
1584
1740
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, activeTurnId);
1585
1741
  return;
@@ -1594,7 +1750,7 @@ export class ProxySessionController {
1594
1750
  this.logger.warn({ error: syncError, sessionId: record.localSessionId }, "Failed to synchronize after steering failure.");
1595
1751
  }
1596
1752
  if (!current?.activeTurnId) {
1597
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
1753
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
1598
1754
  if (messageId)
1599
1755
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
1600
1756
  return;
@@ -1602,7 +1758,7 @@ export class ProxySessionController {
1602
1758
  if (current.activeTurnId !== activeTurnId) {
1603
1759
  try {
1604
1760
  await loaded.runtime.steerTurn(record.localSessionId, current.activeTurnId, runtimePrompt(text, localImagePaths));
1605
- await this.presentSteerMessage(record.localSessionId, current.activeTurnId, text, messageId);
1761
+ await this.presentSteerMessage(record.localSessionId, current.activeTurnId, displayPrompt ?? text, messageId);
1606
1762
  if (messageId)
1607
1763
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, current.activeTurnId);
1608
1764
  return;
@@ -1615,11 +1771,12 @@ export class ProxySessionController {
1615
1771
  localImagePaths,
1616
1772
  messageId,
1617
1773
  replyTarget,
1774
+ displayPrompt,
1618
1775
  });
1619
1776
  return;
1620
1777
  }
1621
1778
  }
1622
- const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId });
1779
+ const turnId = await this.startTurn(loaded, text, replyTarget, localImagePaths, { messageId, displayPrompt });
1623
1780
  if (messageId)
1624
1781
  await this.bindMessageReactionToTurn(messageId, record.localSessionId, turnId);
1625
1782
  }
@@ -1651,6 +1808,7 @@ export class ProxySessionController {
1651
1808
  localImagePaths: options.localImagePaths,
1652
1809
  messageId: options.messageId,
1653
1810
  replyMessageId: options.replyTarget?.messageId,
1811
+ displayPrompt: options.displayPrompt,
1654
1812
  });
1655
1813
  }
1656
1814
  async cancelQueuedPrompt(action) {
@@ -1711,7 +1869,7 @@ export class ProxySessionController {
1711
1869
  contextKey,
1712
1870
  prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
1713
1871
  id: prompt.promptId,
1714
- text: prompt.text,
1872
+ text: prompt.displayPrompt ?? prompt.text,
1715
1873
  })),
1716
1874
  });
1717
1875
  }
@@ -2128,7 +2286,7 @@ export class ProxySessionController {
2128
2286
  }
2129
2287
  async startTurn(loaded, text, replyTarget, localImagePaths, options = {}) {
2130
2288
  const currentRecord = this.store.getSession(loaded.record.localSessionId);
2131
- const title = currentRecord?.title ?? normalizeTaskTitle(text);
2289
+ const title = currentRecord?.title ?? normalizeTaskTitle(options.displayPrompt ?? text);
2132
2290
  if (!currentRecord?.title && title)
2133
2291
  this.store.updateRuntimeSession(loaded.record.localSessionId, { title });
2134
2292
  if (title)
@@ -2347,8 +2505,13 @@ export class ProxySessionController {
2347
2505
  if (existing)
2348
2506
  return { record, runtime, session: existing };
2349
2507
  const pending = this.sessionLoads.get(record.localSessionId);
2350
- if (pending)
2351
- return pending;
2508
+ if (pending) {
2509
+ const loaded = await pending;
2510
+ return {
2511
+ ...loaded,
2512
+ record: { ...loaded.record, contextKey: record.contextKey },
2513
+ };
2514
+ }
2352
2515
  this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
2353
2516
  const loading = (async () => {
2354
2517
  if (record.lastTurnId) {
@@ -2412,7 +2575,11 @@ export class ProxySessionController {
2412
2575
  }
2413
2576
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
2414
2577
  const saved = this.store.getSession(record.localSessionId) ?? record;
2415
- return { record: saved, runtime, session };
2578
+ return {
2579
+ record: { ...saved, contextKey: record.contextKey },
2580
+ runtime,
2581
+ session,
2582
+ };
2416
2583
  })();
2417
2584
  this.sessionLoads.set(record.localSessionId, loading);
2418
2585
  try {
@@ -2499,6 +2666,7 @@ export class ProxySessionController {
2499
2666
  presentationError = error;
2500
2667
  }
2501
2668
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
2669
+ await this.refreshGoalCards(event.sessionId);
2502
2670
  const terminalStatus = event.type === "turn_completed"
2503
2671
  ? "completed"
2504
2672
  : event.type === "turn_cancelled"
@@ -2607,7 +2775,7 @@ export class ProxySessionController {
2607
2775
  const record = this.store.getSessionForContext(sessionId, prompt.contextKey) ?? baseRecord;
2608
2776
  loaded = { ...loaded, record };
2609
2777
  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 });
2778
+ 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
2779
  if (prompt.messageId)
2612
2780
  await this.bindMessageReactionToTurn(prompt.messageId, sessionId, turnId);
2613
2781
  }
@@ -2633,13 +2801,17 @@ export class ProxySessionController {
2633
2801
  : this.store.findLatestCompletedTurnId(source.localSessionId, source.contextKey);
2634
2802
  }
2635
2803
  async finalizeStandaloneMessageReaction(messageId, status) {
2636
- const reaction = this.store.claimMessageReaction(messageId);
2637
- if (reaction)
2638
- await this.replaceMessageReaction(reaction, status);
2804
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
2805
+ const reaction = this.store.claimMessageReaction(relatedMessageId);
2806
+ if (reaction)
2807
+ await this.replaceMessageReaction(reaction, status);
2808
+ }
2639
2809
  }
2640
2810
  async bindMessageReactionToTurn(messageId, sessionId, turnId) {
2641
- this.store.bindMessageToTurn(messageId, sessionId, turnId);
2642
- this.store.bindMessageReaction(messageId, sessionId, turnId);
2811
+ for (const relatedMessageId of this.takeRelatedReactionMessageIds(messageId)) {
2812
+ this.store.bindMessageToTurn(relatedMessageId, sessionId, turnId);
2813
+ this.store.bindMessageReaction(relatedMessageId, sessionId, turnId);
2814
+ }
2643
2815
  const session = this.store.getSession(sessionId);
2644
2816
  if (session?.lastTurnId !== turnId)
2645
2817
  return;
@@ -2647,6 +2819,90 @@ export class ProxySessionController {
2647
2819
  await this.finalizeTurnMessageReactions(turnId, session.lastTurnStatus);
2648
2820
  }
2649
2821
  }
2822
+ registerPendingForwardAttachment(message, registry) {
2823
+ let resolveAttachment;
2824
+ const attachmentPromise = new Promise((resolve) => {
2825
+ resolveAttachment = resolve;
2826
+ });
2827
+ const pending = {
2828
+ contextKey: message.contextKey,
2829
+ attachmentPromise,
2830
+ resolveAttachment,
2831
+ };
2832
+ registry.set(message.messageId, pending);
2833
+ return pending;
2834
+ }
2835
+ reserveForwardAttachment(message) {
2836
+ if (!message.parentMessageId)
2837
+ return undefined;
2838
+ const candidates = [
2839
+ { sourceKind: "merged_forward", registry: this.pendingMergedForwards },
2840
+ { sourceKind: "resource", registry: this.pendingResourceForwards },
2841
+ ];
2842
+ for (const candidate of candidates) {
2843
+ const pending = candidate.registry.get(message.parentMessageId);
2844
+ if (!pending || pending.contextKey !== message.contextKey || pending.attachmentMessageId)
2845
+ continue;
2846
+ pending.attachmentMessageId = message.messageId;
2847
+ return {
2848
+ sourceMessageId: message.parentMessageId,
2849
+ sourceKind: candidate.sourceKind,
2850
+ pending,
2851
+ registry: candidate.registry,
2852
+ };
2853
+ }
2854
+ return undefined;
2855
+ }
2856
+ completeForwardAttachment(message, reservation) {
2857
+ if (!message.parentMessageId)
2858
+ return false;
2859
+ if (reservation.sourceMessageId !== message.parentMessageId)
2860
+ return false;
2861
+ if (reservation.registry.get(message.parentMessageId) !== reservation.pending)
2862
+ return false;
2863
+ if (reservation.pending.attachmentMessageId !== message.messageId)
2864
+ return false;
2865
+ reservation.pending.resolveAttachment(message);
2866
+ return true;
2867
+ }
2868
+ async waitForForwardAttachment(messageId, pending, registry) {
2869
+ let timer;
2870
+ const timeout = new Promise((resolve) => {
2871
+ timer = setTimeout(() => resolve(undefined), FORWARD_ATTACHMENT_WINDOW_MS);
2872
+ timer.unref?.();
2873
+ });
2874
+ try {
2875
+ return await Promise.race([pending.attachmentPromise, timeout]);
2876
+ }
2877
+ finally {
2878
+ if (timer)
2879
+ clearTimeout(timer);
2880
+ if (registry.get(messageId) === pending) {
2881
+ registry.delete(messageId);
2882
+ }
2883
+ }
2884
+ }
2885
+ async resolveReferencedMessage(contextKey, messageId) {
2886
+ const local = this.store.findAgentBotTurnMessageById(messageId);
2887
+ if (local) {
2888
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(local.turnId));
2889
+ const text = snapshot ? renderLocalTurnReference(snapshot, local.messageKind) : undefined;
2890
+ if (text) {
2891
+ return {
2892
+ text,
2893
+ messageType: local.messageKind === "final" ? "agent_bot_reply" : "agent_bot_progress",
2894
+ images: [],
2895
+ files: [],
2896
+ };
2897
+ }
2898
+ }
2899
+ return this.outbound.readReferencedMessage(contextKey, messageId);
2900
+ }
2901
+ takeRelatedReactionMessageIds(messageId) {
2902
+ const related = this.relatedReactionMessageIds.get(messageId) ?? [];
2903
+ this.relatedReactionMessageIds.delete(messageId);
2904
+ return [...new Set([messageId, ...related])];
2905
+ }
2650
2906
  async finalizeTurnMessageReactions(turnId, status) {
2651
2907
  for (const reaction of this.store.claimMessageReactionsForTurn(turnId)) {
2652
2908
  await this.replaceMessageReaction(reaction, status);
@@ -2752,8 +3008,10 @@ export class ProxySessionController {
2752
3008
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
2753
3009
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2754
3010
  }
2755
- const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
2756
- if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
3011
+ const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3012
+ .find((turn) => turn.turnId === turnId);
3013
+ const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3014
+ if (!snapshot || snapshot.status !== "completed") {
2757
3015
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
2758
3016
  }
2759
3017
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
@@ -2773,7 +3031,7 @@ export class ProxySessionController {
2773
3031
  throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
2774
3032
  }
2775
3033
  const origin = this.store.getTurnRuntimeOrigin(turnId);
2776
- if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
3034
+ if (origin && origin.agentName !== current.agentName) {
2777
3035
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
2778
3036
  }
2779
3037
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -3141,6 +3399,21 @@ export class ProxySessionController {
3141
3399
  await this.sendGoalCard(contextKey, goal, command.action === "edit" ? "Goal 已更新。" : "Goal 已启动,Agent 会持续执行直到完成、暂停或遇到阻塞。", record);
3142
3400
  }
3143
3401
  async sendGoalCard(contextKey, goal, notice, record = this.currentSession(contextKey)) {
3402
+ const card = this.renderGoalCard(goal, notice, record);
3403
+ if (!record) {
3404
+ await this.outbound.sendInteractiveCard(contextKey, card);
3405
+ return;
3406
+ }
3407
+ const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
3408
+ if (existing) {
3409
+ await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
3410
+ return;
3411
+ }
3412
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
3413
+ if (messageId)
3414
+ this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
3415
+ }
3416
+ renderGoalCard(goal, notice, record) {
3144
3417
  const sections = [];
3145
3418
  if (notice)
3146
3419
  sections.push({ lines: [cardText(notice)] });
@@ -3163,23 +3436,133 @@ export class ProxySessionController {
3163
3436
  "**/goal edit <新目标>** 修改  **/goal clear** 清除",
3164
3437
  ],
3165
3438
  });
3166
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Goal", sections));
3439
+ const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
3440
+ return this.cardRenderer.renderSectionsCard(title, sections);
3441
+ }
3442
+ async refreshGoalCards(localSessionId) {
3443
+ const deliveries = this.store.listGoalCardDeliveries(localSessionId);
3444
+ if (deliveries.length === 0)
3445
+ return;
3446
+ const record = this.store.getSession(localSessionId);
3447
+ if (!record)
3448
+ return;
3449
+ const runtime = this.runtimes.forAgent(record.agentName);
3450
+ if (runtime.kind !== "codex" || !runtime.getGoal)
3451
+ return;
3452
+ try {
3453
+ const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
3454
+ const goal = await runtime.getGoal(loaded.localSessionId);
3455
+ const card = this.renderGoalCard(goal, undefined, record);
3456
+ await Promise.all(deliveries.map(async (delivery) => {
3457
+ try {
3458
+ await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
3459
+ }
3460
+ catch (error) {
3461
+ this.logger.warn({ error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId }, "Failed to refresh an Agent Goal card.");
3462
+ }
3463
+ }));
3464
+ }
3465
+ catch (error) {
3466
+ this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
3467
+ }
3167
3468
  }
3168
3469
  async runShellCommand(contextKey, command) {
3169
3470
  const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
3170
- const result = await this.shellCommandExecutor(command, cwd);
3171
- const outputParts = [
3172
- result.stdout.trimEnd(),
3173
- result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
3174
- ].filter(Boolean);
3175
- const output = truncateMiddle(outputParts.join("\n"), 6_000);
3176
- const status = result.timedOut
3177
- ? "已超时(120s)"
3178
- : `退出码 ${result.exitCode ?? "未知"}`;
3179
- await this.outbound.sendMarkdown(contextKey, [
3180
- codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
3181
- `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
3182
- ].join("\n"));
3471
+ const startedAt = Date.now();
3472
+ let latestOutput = {
3473
+ stdout: "",
3474
+ stderr: "",
3475
+ outputTruncated: false,
3476
+ };
3477
+ let runningState = {
3478
+ command,
3479
+ cwd,
3480
+ ...latestOutput,
3481
+ status: "running",
3482
+ elapsedMs: 0,
3483
+ };
3484
+ const cardMessageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(runningState));
3485
+ const scheduler = cardMessageId
3486
+ ? new CardUpdateScheduler({
3487
+ render: (state) => this.cardRenderer.renderShellCommandCard(state),
3488
+ write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
3489
+ onError: (error) => this.logger.warn({ error, contextKey, messageId: cardMessageId }, "Failed to update a shell command card."),
3490
+ })
3491
+ : undefined;
3492
+ scheduler?.seed(runningState);
3493
+ const elapsedTimer = scheduler ? setInterval(() => {
3494
+ runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
3495
+ scheduler.update(runningState);
3496
+ }, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
3497
+ elapsedTimer?.unref?.();
3498
+ const updateRunningOutput = (snapshot) => {
3499
+ latestOutput = snapshot;
3500
+ runningState = {
3501
+ ...runningState,
3502
+ ...snapshot,
3503
+ elapsedMs: Date.now() - startedAt,
3504
+ };
3505
+ scheduler?.update(runningState);
3506
+ };
3507
+ let result;
3508
+ try {
3509
+ result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
3510
+ }
3511
+ catch (error) {
3512
+ if (elapsedTimer)
3513
+ clearInterval(elapsedTimer);
3514
+ const failedCard = this.cardRenderer.renderShellCommandCard({
3515
+ command,
3516
+ cwd,
3517
+ ...latestOutput,
3518
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
3519
+ status: "failed",
3520
+ elapsedMs: Date.now() - startedAt,
3521
+ });
3522
+ if (scheduler) {
3523
+ try {
3524
+ await scheduler.flush({
3525
+ ...runningState,
3526
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
3527
+ status: "failed",
3528
+ elapsedMs: Date.now() - startedAt,
3529
+ });
3530
+ }
3531
+ catch (cardError) {
3532
+ this.logger.warn({ error: cardError, contextKey, messageId: cardMessageId }, "Failed to finalize a shell command card.");
3533
+ }
3534
+ finally {
3535
+ scheduler.dispose();
3536
+ }
3537
+ }
3538
+ else {
3539
+ await this.outbound.sendInteractiveCard(contextKey, failedCard);
3540
+ }
3541
+ throw error;
3542
+ }
3543
+ if (elapsedTimer)
3544
+ clearInterval(elapsedTimer);
3545
+ const finalState = {
3546
+ command,
3547
+ cwd,
3548
+ stdout: result.stdout,
3549
+ stderr: result.stderr,
3550
+ status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
3551
+ exitCode: result.exitCode,
3552
+ elapsedMs: Date.now() - startedAt,
3553
+ outputTruncated: result.outputTruncated,
3554
+ };
3555
+ if (scheduler) {
3556
+ try {
3557
+ await scheduler.flush(finalState);
3558
+ }
3559
+ finally {
3560
+ scheduler.dispose();
3561
+ }
3562
+ }
3563
+ else {
3564
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
3565
+ }
3183
3566
  }
3184
3567
  async setTitle(contextKey, title) {
3185
3568
  const normalizedTitle = normalizeTaskTitle(title);
@@ -3290,7 +3673,7 @@ export class ProxySessionController {
3290
3673
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3291
3674
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3292
3675
  }
3293
- const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
3676
+ const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
3294
3677
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
3295
3678
  turnId: turn.turnId,
3296
3679
  parentTurnId: turn.parentTurnId,
@@ -3426,7 +3809,6 @@ export class ProxySessionController {
3426
3809
  contextKey,
3427
3810
  },
3428
3811
  },
3429
- actions: directoryCreationActions(entry.path, contextKey),
3430
3812
  }
3431
3813
  : {
3432
3814
  name: entry.name,
@@ -3451,7 +3833,7 @@ export class ProxySessionController {
3451
3833
  footerLines: [
3452
3834
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
3453
3835
  "",
3454
- "> 点击目录名称进入,点击文件名称发送到当前会话;**New** 在该目录创建任务,**NewGroup** 在该目录创建新群和任务。",
3836
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部的 **New** 和 **NewGroup** 使用当前目录。",
3455
3837
  ],
3456
3838
  });
3457
3839
  if (options.updateMessageId) {
@@ -3480,7 +3862,6 @@ export class ProxySessionController {
3480
3862
  contextKey,
3481
3863
  },
3482
3864
  },
3483
- actions: directoryCreationActions(drive.root, contextKey),
3484
3865
  }));
3485
3866
  const card = this.cardRenderer.renderDirectoryBrowserCard({
3486
3867
  directory: "此电脑",
@@ -3490,7 +3871,7 @@ export class ProxySessionController {
3490
3871
  footerLines: [
3491
3872
  `第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
3492
3873
  "",
3493
- "> 点击磁盘名称进入;**New** 在磁盘根目录创建任务,**NewGroup** 在磁盘根目录创建新群和任务。",
3874
+ "> 点击磁盘名称进入。",
3494
3875
  ],
3495
3876
  });
3496
3877
  if (options.updateMessageId) {
@@ -4108,7 +4489,9 @@ export class ProxySessionController {
4108
4489
  let activeTurnId;
4109
4490
  let queued = 0;
4110
4491
  if (!current) {
4111
- taskLines.push("无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
4492
+ taskLines.push(isThreadContextKey(contextKey)
4493
+ ? unboundThreadTaskMessage()
4494
+ : "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
4112
4495
  }
4113
4496
  else {
4114
4497
  const agent = this.ensureAgent(current.agentName);
@@ -4339,8 +4722,11 @@ export class ProxySessionController {
4339
4722
  }
4340
4723
  requireCurrentSession(contextKey) {
4341
4724
  const record = this.currentSession(contextKey);
4342
- if (!record)
4343
- throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
4725
+ if (!record) {
4726
+ throw new Error(isThreadContextKey(contextKey)
4727
+ ? unboundThreadTaskMessage()
4728
+ : "当前没有任务,直接发送一条消息即可自动创建。");
4729
+ }
4344
4730
  return record;
4345
4731
  }
4346
4732
  requireSession(contextKey, sessionId) {
@@ -4808,6 +5194,19 @@ function isQueueIndependentCommand(command) {
4808
5194
  return true;
4809
5195
  return false;
4810
5196
  }
5197
+ function commandRequiresCurrentSession(command) {
5198
+ if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
5199
+ return true;
5200
+ }
5201
+ if (command.type === "archive" || command.type === "fork")
5202
+ return command.sessionId === undefined;
5203
+ if (command.type === "goal")
5204
+ return command.action !== "set";
5205
+ return false;
5206
+ }
5207
+ function unboundThreadTaskMessage() {
5208
+ return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
5209
+ }
4811
5210
  function parseSessionPage(value) {
4812
5211
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
4813
5212
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
@@ -4941,6 +5340,86 @@ function isPromptCommand(command) {
4941
5340
  function runtimePrompt(text, localImagePaths) {
4942
5341
  return localImagePaths?.length ? { text, localImagePaths } : text;
4943
5342
  }
5343
+ function mergedForwardPrompt(instruction, transcript) {
5344
+ return `${instruction}\n\n参考聊天记录:\n${transcript}`;
5345
+ }
5346
+ function referencedMessagePrompt(instruction, referencedMessage) {
5347
+ return `${instruction}\n\n引用消息:\n${referencedMessage}`;
5348
+ }
5349
+ function quotedMessageId(message) {
5350
+ const messageId = message.parentMessageId?.trim();
5351
+ if (!messageId || message.contextKey.startsWith("console:"))
5352
+ return undefined;
5353
+ if (message.mergedForwardMessageId)
5354
+ return undefined;
5355
+ const text = message.text.trimStart();
5356
+ if (text.startsWith("/") || text.startsWith("!"))
5357
+ return undefined;
5358
+ if (message.threadContext && message.rootMessageId === messageId)
5359
+ return undefined;
5360
+ return messageId;
5361
+ }
5362
+ function renderLocalTurnReference(snapshot, messageKind) {
5363
+ if (messageKind === "final") {
5364
+ const response = snapshot.finalResponse?.trim() || snapshot.assistantText?.trim();
5365
+ return response
5366
+ ? `[消息类型:AgentBot 回复]\n${truncateMiddle(response, 48_000)}`
5367
+ : undefined;
5368
+ }
5369
+ const activityText = (snapshot.activities ?? [])
5370
+ .filter((activity) => activity.kind === "assistant" || activity.kind === "reasoning")
5371
+ .map((activity) => activity.text.trim())
5372
+ .filter(Boolean)
5373
+ .join("\n\n");
5374
+ const content = [
5375
+ snapshot.prompt?.trim() ? `用户请求:\n${snapshot.prompt.trim()}` : "",
5376
+ activityText,
5377
+ snapshot.progressText?.trim() || "",
5378
+ snapshot.finalResponse?.trim() || snapshot.assistantText?.trim() || "",
5379
+ snapshot.error?.trim() ? `错误:${snapshot.error.trim()}` : "",
5380
+ ].filter(Boolean).join("\n\n");
5381
+ return content
5382
+ ? `[消息类型:AgentBot 思考卡片]\n${truncateMiddle(content, 48_000)}`
5383
+ : undefined;
5384
+ }
5385
+ function appendDownloadedFiles(prompt, files) {
5386
+ return `${prompt}\n\n参考文件(已下载到本地):\n${files.map((file, index) => `[文件 ${index + 1}:${file.fileName}] ${file.filePath}`).join("\n")}`;
5387
+ }
5388
+ function defaultResourcePrompt(message) {
5389
+ return defaultResourcePromptFromCounts(message.images?.length ?? 0, message.files?.length ?? 0);
5390
+ }
5391
+ function defaultResourcePromptFromCounts(imageCount, fileCount) {
5392
+ if (imageCount > 0 && fileCount > 0)
5393
+ return "请查看附带的图片和文件";
5394
+ if (fileCount > 0)
5395
+ return fileCount === 1 ? "请查看这个文件" : "请查看这些文件";
5396
+ return imageCount === 1 ? "请查看这张图片" : "请查看这些图片";
5397
+ }
5398
+ function isStandaloneResourceMessage(message) {
5399
+ return !message.mergedForwardMessageId
5400
+ && message.text.trim().length === 0
5401
+ && ((message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
5402
+ }
5403
+ function deduplicateImageReferences(images) {
5404
+ const seen = new Set();
5405
+ return images.filter((image) => {
5406
+ const key = `${image.messageId}:${image.imageKey}`;
5407
+ if (seen.has(key))
5408
+ return false;
5409
+ seen.add(key);
5410
+ return true;
5411
+ });
5412
+ }
5413
+ function deduplicateFileReferences(files) {
5414
+ const seen = new Set();
5415
+ return files.filter((file) => {
5416
+ const key = `${file.messageId}:${file.fileKey}`;
5417
+ if (seen.has(key))
5418
+ return false;
5419
+ seen.add(key);
5420
+ return true;
5421
+ });
5422
+ }
4944
5423
  function recoveryCardPrompt(prompt) {
4945
5424
  return `恢复重启前的任务:${prompt}`;
4946
5425
  }
@@ -5026,14 +5505,13 @@ const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
5026
5505
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
5027
5506
  function formatGroupProjectDirectory(projectCwd) {
5028
5507
  const value = abbreviateHomeDirectory(projectCwd);
5029
- if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
5030
- return `[${value}]`;
5031
5508
  const levels = value
5032
5509
  .replace(/[\\/]+$/, "")
5033
5510
  .split(/[\\/]+/)
5034
5511
  .filter(Boolean)
5035
5512
  .slice(-2);
5036
- return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
5513
+ const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
5514
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
5037
5515
  }
5038
5516
  function abbreviateHomeDirectory(value) {
5039
5517
  const homeDirectory = os.homedir();