@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.
- package/CHANGELOG.md +11 -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
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
2637
|
-
|
|
2638
|
-
|
|
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.
|
|
2642
|
-
|
|
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
|
}
|