@keyou007/agent-bot 0.1.18-alpha.3 → 0.1.19-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 +10 -0
- package/dist/codex/CodexRuntime.js +8 -3
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +344 -0
- package/dist/codex/ThreadWriterProcess.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +1 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +83 -0
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +13 -3
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/errorLogValue.js +18 -0
- package/dist/logging/errorLogValue.js.map +1 -0
- package/dist/logging/logger.js +4 -0
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +2 -2
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +329 -15
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +1 -1
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/codex/CodexRuntime.ts +8 -2
- package/src/codex/ThreadWriterProcess.ts +385 -0
- package/src/console/ConsoleTurnPresenter.ts +2 -1
- package/src/feishu/CardRenderer.ts +109 -0
- package/src/feishu/FeishuTurnPresenter.ts +16 -3
- package/src/index.ts +2 -2
- package/src/logging/errorLogValue.ts +26 -0
- package/src/logging/logger.ts +4 -0
- package/src/presentation/OutboundRouter.ts +15 -3
- package/src/proxy/ProxySessionController.ts +459 -24
- package/src/runtime/types.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +1 -1
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
6
|
+
import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
|
|
6
7
|
import { resolveUserPath } from "../config/paths.js";
|
|
7
8
|
import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
|
|
8
9
|
import { CommandRouter } from "../commands/CommandRouter.js";
|
|
@@ -14,12 +15,21 @@ import { formatGroupNameDate, formatNewGroupName, parseTaskNameFromGroupName, }
|
|
|
14
15
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
15
16
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
16
17
|
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
18
|
+
import { errorLogValue } from "../logging/errorLogValue.js";
|
|
17
19
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
18
20
|
import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
|
|
19
21
|
import { createId } from "../utils/id.js";
|
|
20
22
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
21
23
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
22
24
|
import { executeShellCommand, } from "../utils/executeShellCommand.js";
|
|
25
|
+
class PresentedRequestError extends Error {
|
|
26
|
+
originalError;
|
|
27
|
+
constructor(originalError) {
|
|
28
|
+
super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
|
|
29
|
+
this.originalError = originalError;
|
|
30
|
+
this.name = "PresentedRequestError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
23
33
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
24
34
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
25
35
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -178,6 +188,7 @@ export class ProxySessionController {
|
|
|
178
188
|
shellCommandExecutor;
|
|
179
189
|
windowsDriveLister;
|
|
180
190
|
shellCommandJobs;
|
|
191
|
+
threadWriterProcesses;
|
|
181
192
|
router = new CommandRouter();
|
|
182
193
|
cardRenderer = new CardRenderer();
|
|
183
194
|
messageQueues = new Map();
|
|
@@ -201,7 +212,7 @@ export class ProxySessionController {
|
|
|
201
212
|
recoveryActivityHeartbeat;
|
|
202
213
|
startupRecovery;
|
|
203
214
|
queuedPromptsRestored = false;
|
|
204
|
-
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"))) {
|
|
215
|
+
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs")), threadWriterProcesses = new SystemThreadWriterProcessController()) {
|
|
205
216
|
this.config = config;
|
|
206
217
|
this.store = store;
|
|
207
218
|
this.runtimes = runtimes;
|
|
@@ -211,6 +222,7 @@ export class ProxySessionController {
|
|
|
211
222
|
this.shellCommandExecutor = shellCommandExecutor;
|
|
212
223
|
this.windowsDriveLister = windowsDriveLister;
|
|
213
224
|
this.shellCommandJobs = shellCommandJobs;
|
|
225
|
+
this.threadWriterProcesses = threadWriterProcesses;
|
|
214
226
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
215
227
|
this.unsubscribe.push(runtime.onEvent((event) => {
|
|
216
228
|
void this.handleRuntimeEvent(event).catch((error) => {
|
|
@@ -328,7 +340,11 @@ export class ProxySessionController {
|
|
|
328
340
|
: undefined;
|
|
329
341
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
330
342
|
const detail = result.error instanceof Error ? result.error.message : String(result.error);
|
|
331
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)
|
|
343
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
|
|
344
|
+
requestId: message.messageId,
|
|
345
|
+
messageId: message.messageId,
|
|
346
|
+
phase: "merged_forward_read",
|
|
347
|
+
}));
|
|
332
348
|
return;
|
|
333
349
|
}
|
|
334
350
|
const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
|
|
@@ -360,7 +376,11 @@ export class ProxySessionController {
|
|
|
360
376
|
: undefined;
|
|
361
377
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
362
378
|
const detail = error instanceof Error ? error.message : String(error);
|
|
363
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)
|
|
379
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
|
|
380
|
+
requestId: message.messageId,
|
|
381
|
+
messageId: message.messageId,
|
|
382
|
+
phase: "referenced_message_read",
|
|
383
|
+
}));
|
|
364
384
|
return;
|
|
365
385
|
}
|
|
366
386
|
}
|
|
@@ -394,7 +414,11 @@ export class ProxySessionController {
|
|
|
394
414
|
}
|
|
395
415
|
catch (error) {
|
|
396
416
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
397
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
417
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
418
|
+
requestId: message.messageId,
|
|
419
|
+
messageId: message.messageId,
|
|
420
|
+
phase: "image_download",
|
|
421
|
+
}));
|
|
398
422
|
return;
|
|
399
423
|
}
|
|
400
424
|
}
|
|
@@ -413,7 +437,11 @@ export class ProxySessionController {
|
|
|
413
437
|
}
|
|
414
438
|
catch (error) {
|
|
415
439
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
416
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
440
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
441
|
+
requestId: message.messageId,
|
|
442
|
+
messageId: message.messageId,
|
|
443
|
+
phase: "file_download",
|
|
444
|
+
}));
|
|
417
445
|
return;
|
|
418
446
|
}
|
|
419
447
|
}
|
|
@@ -426,9 +454,21 @@ export class ProxySessionController {
|
|
|
426
454
|
}
|
|
427
455
|
catch (error) {
|
|
428
456
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
429
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
457
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
458
|
+
requestId: message.messageId,
|
|
459
|
+
messageId: message.messageId,
|
|
460
|
+
phase: "command_parse",
|
|
461
|
+
}));
|
|
430
462
|
return;
|
|
431
463
|
}
|
|
464
|
+
if (command.type !== "prompt") {
|
|
465
|
+
this.logger.info({
|
|
466
|
+
requestId: message.messageId,
|
|
467
|
+
messageId: message.messageId,
|
|
468
|
+
contextKey: message.contextKey,
|
|
469
|
+
command: command.type,
|
|
470
|
+
}, "Processing Agent Bot command.");
|
|
471
|
+
}
|
|
432
472
|
// Operational and read-only commands must remain available even if a prompt operation is slow.
|
|
433
473
|
if (isQueueIndependentCommand(command)) {
|
|
434
474
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
@@ -442,7 +482,12 @@ export class ProxySessionController {
|
|
|
442
482
|
}
|
|
443
483
|
catch (error) {
|
|
444
484
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
445
|
-
await this.sendError(message.contextKey, error
|
|
485
|
+
await this.sendError(message.contextKey, error, {
|
|
486
|
+
requestId: message.messageId,
|
|
487
|
+
messageId: message.messageId,
|
|
488
|
+
command: command.type,
|
|
489
|
+
phase: "command_execute",
|
|
490
|
+
});
|
|
446
491
|
}
|
|
447
492
|
});
|
|
448
493
|
return;
|
|
@@ -459,7 +504,12 @@ export class ProxySessionController {
|
|
|
459
504
|
}
|
|
460
505
|
catch (error) {
|
|
461
506
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
462
|
-
await this.sendError(message.contextKey, error
|
|
507
|
+
await this.sendError(message.contextKey, error, {
|
|
508
|
+
requestId: message.messageId,
|
|
509
|
+
messageId: message.messageId,
|
|
510
|
+
command: command.type,
|
|
511
|
+
phase: "command_execute",
|
|
512
|
+
});
|
|
463
513
|
}
|
|
464
514
|
}));
|
|
465
515
|
this.messageQueues.set(message.contextKey, next);
|
|
@@ -479,7 +529,11 @@ export class ProxySessionController {
|
|
|
479
529
|
resolvedAction = this.resolveCardActionBinding(action);
|
|
480
530
|
}
|
|
481
531
|
catch (error) {
|
|
482
|
-
await this.sendError(action.contextKey, error
|
|
532
|
+
await this.sendError(action.contextKey, error, {
|
|
533
|
+
requestId: action.actionId,
|
|
534
|
+
actionId: action.actionId,
|
|
535
|
+
phase: "card_action_resolve",
|
|
536
|
+
});
|
|
483
537
|
return;
|
|
484
538
|
}
|
|
485
539
|
const contextKey = this.cardActionContextKey(resolvedAction);
|
|
@@ -510,6 +564,18 @@ export class ProxySessionController {
|
|
|
510
564
|
else if (kind === "shell_command_cancel") {
|
|
511
565
|
await this.cancelShellCommandJob(contextKey, String(scopedAction.value.jobId ?? ""));
|
|
512
566
|
}
|
|
567
|
+
else if (kind === "thread_writer_close") {
|
|
568
|
+
await this.closeThreadWriter(scopedAction, contextKey);
|
|
569
|
+
}
|
|
570
|
+
else if (kind === "thread_writer_fork") {
|
|
571
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
572
|
+
const threadId = String(scopedAction.value.threadId ?? "");
|
|
573
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
574
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
575
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
576
|
+
}
|
|
577
|
+
await this.forkSessionReference(contextKey, sessionId);
|
|
578
|
+
}
|
|
513
579
|
else if (kind === "turn_reset") {
|
|
514
580
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
515
581
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -734,7 +800,12 @@ export class ProxySessionController {
|
|
|
734
800
|
}
|
|
735
801
|
}
|
|
736
802
|
catch (error) {
|
|
737
|
-
await this.sendError(contextKey, error
|
|
803
|
+
await this.sendError(contextKey, error, {
|
|
804
|
+
requestId: scopedAction.actionId,
|
|
805
|
+
actionId: scopedAction.actionId,
|
|
806
|
+
command: String(scopedAction.value.action ?? "unknown"),
|
|
807
|
+
phase: "card_action_execute",
|
|
808
|
+
});
|
|
738
809
|
}
|
|
739
810
|
});
|
|
740
811
|
}
|
|
@@ -1809,6 +1880,9 @@ export class ProxySessionController {
|
|
|
1809
1880
|
loaded = await this.loadSession(record);
|
|
1810
1881
|
}
|
|
1811
1882
|
catch (error) {
|
|
1883
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
1884
|
+
throw new PresentedRequestError(error);
|
|
1885
|
+
}
|
|
1812
1886
|
await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
|
|
1813
1887
|
throw error;
|
|
1814
1888
|
}
|
|
@@ -2900,6 +2974,8 @@ export class ProxySessionController {
|
|
|
2900
2974
|
});
|
|
2901
2975
|
}
|
|
2902
2976
|
catch (error) {
|
|
2977
|
+
if (activeWriterThreadId(error))
|
|
2978
|
+
throw error;
|
|
2903
2979
|
if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
|
|
2904
2980
|
throw error;
|
|
2905
2981
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
@@ -5262,14 +5338,26 @@ export class ProxySessionController {
|
|
|
5262
5338
|
description: command.description,
|
|
5263
5339
|
})),
|
|
5264
5340
|
}));
|
|
5265
|
-
|
|
5341
|
+
const introLines = [
|
|
5266
5342
|
"直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
|
|
5267
5343
|
"点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
|
|
5268
5344
|
"> 命令前缀示例:/sess 等同于 /sessions。",
|
|
5269
5345
|
"> 命令缩写示例:/fg 等同于 /forkgroup。",
|
|
5270
5346
|
"前缀或缩写匹配多个命令时,需要输入更长的形式。",
|
|
5271
5347
|
"**[参数]** 可选 **<参数>** 必填",
|
|
5272
|
-
]
|
|
5348
|
+
];
|
|
5349
|
+
try {
|
|
5350
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard("Agent Bot 使用帮助", introLines, sections));
|
|
5351
|
+
}
|
|
5352
|
+
catch (cardError) {
|
|
5353
|
+
this.logger.warn({ error: cardError, contextKey, command: "help", phase: "help_card_send" }, "Failed to send the help card; falling back to plain text.");
|
|
5354
|
+
try {
|
|
5355
|
+
await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
|
|
5356
|
+
}
|
|
5357
|
+
catch (textError) {
|
|
5358
|
+
throw new AggregateError([cardError, textError], "帮助卡片和纯文本帮助均发送失败。");
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5273
5361
|
}
|
|
5274
5362
|
async setGroupMute(contextKey, enabled) {
|
|
5275
5363
|
const chatContextKey = baseChatContextKey(contextKey);
|
|
@@ -5375,10 +5463,236 @@ export class ProxySessionController {
|
|
|
5375
5463
|
throw new Error(`找不到任务:${sessionId}`);
|
|
5376
5464
|
return record;
|
|
5377
5465
|
}
|
|
5378
|
-
async
|
|
5379
|
-
|
|
5380
|
-
|
|
5466
|
+
async presentThreadWriterConflict(record, contextKey, error, replacePendingTurn) {
|
|
5467
|
+
if (contextKey.startsWith("console:"))
|
|
5468
|
+
return false;
|
|
5469
|
+
const threadId = activeWriterThreadId(error);
|
|
5470
|
+
if (!threadId || record.remoteSessionId !== threadId)
|
|
5471
|
+
return false;
|
|
5472
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5473
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5474
|
+
let owners = [];
|
|
5475
|
+
if (lockPath) {
|
|
5476
|
+
try {
|
|
5477
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5478
|
+
}
|
|
5479
|
+
catch (inspectionError) {
|
|
5480
|
+
this.logger.warn({ error: inspectionError, threadId, lockPath }, "Failed to inspect the process holding a thread writer lock.");
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
5484
|
+
const applicationIdle = owner && lockPath
|
|
5485
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
|
|
5486
|
+
: undefined;
|
|
5487
|
+
const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, "occupied", lockPath
|
|
5488
|
+
? undefined
|
|
5489
|
+
: "无法识别占用进程。", applicationIdle));
|
|
5490
|
+
if (replacePendingTurn) {
|
|
5491
|
+
const replaced = await this.outbound.failPendingTurn(record.localSessionId, "任务被占用。", card);
|
|
5492
|
+
if (!replaced)
|
|
5493
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5494
|
+
}
|
|
5495
|
+
else {
|
|
5496
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5497
|
+
}
|
|
5498
|
+
return true;
|
|
5499
|
+
}
|
|
5500
|
+
threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle) {
|
|
5501
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
5502
|
+
return {
|
|
5503
|
+
status,
|
|
5504
|
+
contextKey,
|
|
5505
|
+
sessionId: record.localSessionId,
|
|
5506
|
+
threadId,
|
|
5507
|
+
taskTitle: record.title,
|
|
5508
|
+
notice: notice ?? (owners.length > 1
|
|
5509
|
+
? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
|
|
5510
|
+
: undefined),
|
|
5511
|
+
applicationIdle,
|
|
5512
|
+
...(owner ? {
|
|
5513
|
+
owner: {
|
|
5514
|
+
displayName: owner.displayName,
|
|
5515
|
+
writerPid: owner.writerPid,
|
|
5516
|
+
writerProcessName: owner.writerProcessName,
|
|
5517
|
+
writerStartedAt: owner.writerStartedAt,
|
|
5518
|
+
applicationPid: owner.applicationPid,
|
|
5519
|
+
applicationProcessName: owner.applicationProcessName,
|
|
5520
|
+
applicationStartedAt: owner.applicationStartedAt,
|
|
5521
|
+
canClose: owner.canClose,
|
|
5522
|
+
},
|
|
5523
|
+
} : {}),
|
|
5524
|
+
};
|
|
5525
|
+
}
|
|
5526
|
+
async closeThreadWriter(action, contextKey) {
|
|
5527
|
+
const messageId = requiredCardMessageId(action.messageId);
|
|
5528
|
+
const sessionId = String(action.value.sessionId ?? "");
|
|
5529
|
+
const threadId = String(action.value.threadId ?? "");
|
|
5530
|
+
const expectedWriterPid = positiveActionPid(action.value.writerPid);
|
|
5531
|
+
const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
|
|
5532
|
+
const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
|
|
5533
|
+
const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
|
|
5534
|
+
const force = action.value.force === true || action.value.force === "true";
|
|
5535
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
5536
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
5537
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
5538
|
+
}
|
|
5539
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5540
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5541
|
+
if (!lockPath)
|
|
5542
|
+
throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
5543
|
+
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5544
|
+
const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
|
|
5545
|
+
if (!expectedOwner) {
|
|
5546
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
5547
|
+
const applicationIdle = currentOwner
|
|
5548
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
5549
|
+
: undefined;
|
|
5550
|
+
await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, owners.length === 0 ? "released" : "occupied", owners.length === 0
|
|
5551
|
+
? "请重新发送消息。"
|
|
5552
|
+
: "占用进程已变化,请重试。", applicationIdle)));
|
|
5553
|
+
return;
|
|
5554
|
+
}
|
|
5555
|
+
if (!expectedOwner.canClose)
|
|
5556
|
+
throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
|
|
5557
|
+
try {
|
|
5558
|
+
await this.threadWriterProcesses.close(expectedOwner, force);
|
|
5559
|
+
}
|
|
5560
|
+
catch (error) {
|
|
5561
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5562
|
+
if (owners.some((owner) => threadWriterMatches(owner, expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt)))
|
|
5563
|
+
throw error;
|
|
5564
|
+
}
|
|
5565
|
+
owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
|
|
5566
|
+
const sameOwnerRemains = owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? ""));
|
|
5567
|
+
if (sameOwnerRemains && force) {
|
|
5568
|
+
throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
|
|
5569
|
+
}
|
|
5570
|
+
const status = owners.length === 0
|
|
5571
|
+
? "released"
|
|
5572
|
+
: sameOwnerRemains ? "force_required" : "occupied";
|
|
5573
|
+
const notice = owners.length === 0
|
|
5574
|
+
? "请重新发送消息。"
|
|
5575
|
+
: sameOwnerRemains
|
|
5576
|
+
? "普通关闭失败,可强制关闭。"
|
|
5577
|
+
: "占用进程已变化,请重试。";
|
|
5578
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
5579
|
+
const applicationIdle = currentOwner
|
|
5580
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
5581
|
+
: undefined;
|
|
5582
|
+
await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle)));
|
|
5583
|
+
}
|
|
5584
|
+
async inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner) {
|
|
5585
|
+
if (!owner.applicationStartedAt
|
|
5586
|
+
|| !this.threadWriterProcesses.inspectApplicationThreadIds
|
|
5587
|
+
|| !runtime.inspectRemoteSessionActivity)
|
|
5588
|
+
return undefined;
|
|
5589
|
+
try {
|
|
5590
|
+
const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
|
|
5591
|
+
if (!ownedThreadIds.includes(threadId))
|
|
5592
|
+
return undefined;
|
|
5593
|
+
const activities = await Promise.all(ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity(ownedThreadId)));
|
|
5594
|
+
return activities.every((activity) => !activity.active);
|
|
5595
|
+
}
|
|
5596
|
+
catch (error) {
|
|
5597
|
+
this.logger.warn({ error, threadId, applicationPid: owner.applicationPid }, "Failed to inspect thread-writer application activity.");
|
|
5598
|
+
return undefined;
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
async waitForThreadWriterChange(lockPath, expectedOwner) {
|
|
5602
|
+
const deadline = Date.now() + 5_000;
|
|
5603
|
+
let owners = [];
|
|
5604
|
+
do {
|
|
5605
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5606
|
+
if (!owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? "")))
|
|
5607
|
+
return owners;
|
|
5608
|
+
await delay(250);
|
|
5609
|
+
} while (Date.now() < deadline);
|
|
5610
|
+
return owners;
|
|
5611
|
+
}
|
|
5612
|
+
async sendError(contextKey, error, details = {}) {
|
|
5613
|
+
if (error instanceof PresentedRequestError) {
|
|
5614
|
+
this.logger.warn({ error: errorLogValue(error.originalError), contextKey, ...details }, "Request failed after a dedicated error card was presented.");
|
|
5615
|
+
return;
|
|
5616
|
+
}
|
|
5617
|
+
const record = this.currentSession(contextKey);
|
|
5618
|
+
if (record) {
|
|
5619
|
+
try {
|
|
5620
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, false))
|
|
5621
|
+
return;
|
|
5622
|
+
}
|
|
5623
|
+
catch (presentationError) {
|
|
5624
|
+
this.logger.warn({ error: presentationError, originalError: errorLogValue(error), contextKey, ...details }, "Failed to present the thread writer conflict card.");
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
this.logger.warn({ error, contextKey, ...details }, "Request failed.");
|
|
5628
|
+
try {
|
|
5629
|
+
await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
|
|
5630
|
+
}
|
|
5631
|
+
catch (deliveryError) {
|
|
5632
|
+
this.logger.error({
|
|
5633
|
+
error: deliveryError,
|
|
5634
|
+
originalError: errorLogValue(error),
|
|
5635
|
+
contextKey,
|
|
5636
|
+
...details,
|
|
5637
|
+
phase: "error_response_send",
|
|
5638
|
+
}, "Failed to deliver the request failure to the user.");
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
function activeWriterThreadId(error) {
|
|
5643
|
+
const visited = new Set();
|
|
5644
|
+
let current = error;
|
|
5645
|
+
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
5646
|
+
visited.add(current);
|
|
5647
|
+
const message = current instanceof Error
|
|
5648
|
+
? current.message
|
|
5649
|
+
: typeof current === "string" ? current : undefined;
|
|
5650
|
+
const match = message?.match(/thread(?:\/resume failed:\s*thread)?\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+already has an active writer/iu);
|
|
5651
|
+
if (match?.[1])
|
|
5652
|
+
return match[1];
|
|
5653
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
5654
|
+
}
|
|
5655
|
+
return undefined;
|
|
5656
|
+
}
|
|
5657
|
+
function positiveActionPid(value) {
|
|
5658
|
+
const pid = typeof value === "number" ? value : Number(value);
|
|
5659
|
+
if (!Number.isSafeInteger(pid) || pid <= 4) {
|
|
5660
|
+
throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
|
|
5381
5661
|
}
|
|
5662
|
+
return pid;
|
|
5663
|
+
}
|
|
5664
|
+
function threadWriterMatches(owner, writerPid, writerStartedAt, applicationPid, applicationStartedAt) {
|
|
5665
|
+
return Boolean(writerStartedAt)
|
|
5666
|
+
&& Boolean(applicationStartedAt)
|
|
5667
|
+
&& owner.writerPid === writerPid
|
|
5668
|
+
&& owner.writerStartedAt === writerStartedAt
|
|
5669
|
+
&& owner.applicationPid === applicationPid
|
|
5670
|
+
&& owner.applicationStartedAt === applicationStartedAt;
|
|
5671
|
+
}
|
|
5672
|
+
function delay(milliseconds) {
|
|
5673
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
5674
|
+
}
|
|
5675
|
+
function renderPlainHelp(introLines, sections) {
|
|
5676
|
+
return [
|
|
5677
|
+
"Agent Bot 使用帮助",
|
|
5678
|
+
"",
|
|
5679
|
+
...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
|
|
5680
|
+
...sections.flatMap((section) => [
|
|
5681
|
+
"",
|
|
5682
|
+
section.title,
|
|
5683
|
+
...section.commands.map((command) => [
|
|
5684
|
+
command.text,
|
|
5685
|
+
command.usage ? decodeHelpEntities(command.usage) : "",
|
|
5686
|
+
`- ${command.description}`,
|
|
5687
|
+
].filter(Boolean).join(" ")),
|
|
5688
|
+
]),
|
|
5689
|
+
].join("\n");
|
|
5690
|
+
}
|
|
5691
|
+
function decodeHelpEntities(value) {
|
|
5692
|
+
return value
|
|
5693
|
+
.replaceAll("<", "<")
|
|
5694
|
+
.replaceAll(">", ">")
|
|
5695
|
+
.replaceAll("&", "&");
|
|
5382
5696
|
}
|
|
5383
5697
|
function threadForkAnchorMessageIds(message) {
|
|
5384
5698
|
return [...new Set([message.rootMessageId, message.parentMessageId]
|