@keyou007/agent-bot 0.1.14 → 0.1.15-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +29 -24
- package/README.zh.md +29 -24
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/help.js +6 -6
- package/dist/cli/initOptions.js +1 -4
- package/dist/cli/initOptions.js.map +1 -1
- package/dist/cli.js +8 -5
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +91 -45
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +39 -12
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +108 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +24 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/feishu/InboundText.js +13 -0
- package/dist/feishu/InboundText.js.map +1 -0
- package/dist/feishu/MergedForwardMessage.js +317 -0
- package/dist/feishu/MergedForwardMessage.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +21 -0
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +536 -58
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +161 -3
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +12 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli/help.ts +6 -6
- package/src/cli/initOptions.ts +1 -10
- package/src/cli.ts +10 -6
- package/src/feishu/CardRenderer.ts +116 -45
- package/src/feishu/FeishuConnector.ts +55 -13
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +151 -12
- package/src/feishu/FeishuTurnPresenter.ts +28 -4
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/feishu/InboundText.ts +12 -0
- package/src/feishu/MergedForwardMessage.ts +372 -0
- package/src/feishu/types.ts +15 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/presentation/OutboundRouter.ts +28 -0
- package/src/proxy/ProxySessionController.ts +642 -55
- package/src/state/StateStore.ts +230 -2
- package/src/state/migrations.ts +12 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- package/src/utils/executeShellCommand.ts +103 -15
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
2637
|
-
|
|
2638
|
-
|
|
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.
|
|
2642
|
-
|
|
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
|
|
2756
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
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.
|
|
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
|
-
">
|
|
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
|
-
">
|
|
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(
|
|
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
|
-
|
|
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();
|