@keyou007/agent-bot 0.1.15-alpha.4 → 0.1.15-alpha.6
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 +12 -0
- package/README.md +7 -2
- package/README.zh.md +7 -2
- package/config.example.yaml +6 -0
- package/dist/commands/CommandRouter.js +7 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +31 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +76 -9
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +1 -1
- package/dist/feishu/FinalResponseBranding.js.map +1 -1
- package/dist/feishu/GroupNameFormatter.js +193 -0
- package/dist/feishu/GroupNameFormatter.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +197 -148
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/shell/ShellCommandJobManager.js +264 -0
- package/dist/shell/ShellCommandJobManager.js.map +1 -0
- package/dist/shell/shellCommandRunner.js +157 -0
- package/dist/shell/shellCommandRunner.js.map +1 -0
- package/dist/utils/executeShellCommand.js +1 -1
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +19 -0
- package/docs/technical-reference.zh.md +19 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/commands/CommandRouter.ts +6 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +33 -0
- package/src/feishu/CardRenderer.ts +91 -17
- package/src/feishu/FinalResponseBranding.ts +2 -1
- package/src/feishu/GroupNameFormatter.ts +222 -0
- package/src/proxy/ProxySessionController.ts +238 -160
- package/src/shell/ShellCommandJobManager.ts +341 -0
- package/src/shell/shellCommandRunner.ts +163 -0
- package/src/utils/executeShellCommand.ts +1 -1
|
@@ -4,14 +4,17 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
6
6
|
import { resolveUserPath } from "../config/paths.js";
|
|
7
|
+
import { DEFAULT_GROUP_NAME_FORMAT } from "../config/schema.js";
|
|
7
8
|
import { CommandRouter } from "../commands/CommandRouter.js";
|
|
8
9
|
import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
|
|
9
10
|
import { CardRenderer, } from "../feishu/CardRenderer.js";
|
|
10
11
|
import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
|
|
11
12
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
13
|
+
import { formatGroupNameDate, formatNewGroupName, parseTaskNameFromGroupName, } from "../feishu/GroupNameFormatter.js";
|
|
12
14
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
13
15
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
14
16
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
17
|
+
import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
|
|
15
18
|
import { createId } from "../utils/id.js";
|
|
16
19
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
17
20
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
@@ -27,7 +30,8 @@ const RESET_HISTORY_PAGE_SIZE = 10;
|
|
|
27
30
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
28
31
|
const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
|
|
29
32
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
30
|
-
const
|
|
33
|
+
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
34
|
+
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
31
35
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
32
36
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
33
37
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -61,6 +65,12 @@ const HELP_COMMAND_SECTIONS = [
|
|
|
61
65
|
usage: "[目录]",
|
|
62
66
|
description: "浏览、发送文件,并从选定目录创建任务或群",
|
|
63
67
|
},
|
|
68
|
+
{
|
|
69
|
+
command: "/file",
|
|
70
|
+
usage: "<文件路径>",
|
|
71
|
+
description: "将指定文件发送到当前飞书会话",
|
|
72
|
+
requiresArgument: true,
|
|
73
|
+
},
|
|
64
74
|
{
|
|
65
75
|
command: "/forkgroup",
|
|
66
76
|
usage: "[title]",
|
|
@@ -170,6 +180,7 @@ export class ProxySessionController {
|
|
|
170
180
|
lifecycle;
|
|
171
181
|
shellCommandExecutor;
|
|
172
182
|
windowsDriveLister;
|
|
183
|
+
shellCommandJobs;
|
|
173
184
|
router = new CommandRouter();
|
|
174
185
|
cardRenderer = new CardRenderer();
|
|
175
186
|
messageQueues = new Map();
|
|
@@ -186,11 +197,12 @@ export class ProxySessionController {
|
|
|
186
197
|
pendingMergedForwards = new Map();
|
|
187
198
|
pendingResourceForwards = new Map();
|
|
188
199
|
relatedReactionMessageIds = new Map();
|
|
200
|
+
shellCommandJobMonitors = new Map();
|
|
189
201
|
unsubscribe = [];
|
|
190
202
|
recoveryActivityHeartbeat;
|
|
191
203
|
startupRecovery;
|
|
192
204
|
queuedPromptsRestored = false;
|
|
193
|
-
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots) {
|
|
205
|
+
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"))) {
|
|
194
206
|
this.config = config;
|
|
195
207
|
this.store = store;
|
|
196
208
|
this.runtimes = runtimes;
|
|
@@ -199,6 +211,7 @@ export class ProxySessionController {
|
|
|
199
211
|
this.lifecycle = lifecycle;
|
|
200
212
|
this.shellCommandExecutor = shellCommandExecutor;
|
|
201
213
|
this.windowsDriveLister = windowsDriveLister;
|
|
214
|
+
this.shellCommandJobs = shellCommandJobs;
|
|
202
215
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
203
216
|
this.unsubscribe.push(runtime.onEvent((event) => {
|
|
204
217
|
void this.handleRuntimeEvent(event).catch((error) => {
|
|
@@ -424,8 +437,9 @@ export class ProxySessionController {
|
|
|
424
437
|
if (command.type === "prompt")
|
|
425
438
|
await this.ensureThreadFork(message);
|
|
426
439
|
await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
|
|
427
|
-
if (!
|
|
440
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
428
441
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
442
|
+
}
|
|
429
443
|
}
|
|
430
444
|
catch (error) {
|
|
431
445
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
@@ -440,8 +454,9 @@ export class ProxySessionController {
|
|
|
440
454
|
if (command.type === "prompt")
|
|
441
455
|
await this.ensureThreadFork(message);
|
|
442
456
|
await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
|
|
443
|
-
if (!
|
|
457
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
444
458
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
459
|
+
}
|
|
445
460
|
}
|
|
446
461
|
catch (error) {
|
|
447
462
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
@@ -493,6 +508,9 @@ export class ProxySessionController {
|
|
|
493
508
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
494
509
|
await this.cancelSession(this.requireSession(contextKey, sessionId));
|
|
495
510
|
}
|
|
511
|
+
else if (kind === "shell_command_cancel") {
|
|
512
|
+
await this.cancelShellCommandJob(contextKey, String(scopedAction.value.jobId ?? ""));
|
|
513
|
+
}
|
|
496
514
|
else if (kind === "turn_reset") {
|
|
497
515
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
498
516
|
await this.resetCurrentSessionToTurn(contextKey, sessionId, String(scopedAction.value.turnId ?? ""));
|
|
@@ -753,6 +771,8 @@ export class ProxySessionController {
|
|
|
753
771
|
for (const timer of this.recoveryRetryTimers.values())
|
|
754
772
|
clearTimeout(timer);
|
|
755
773
|
this.recoveryRetryTimers.clear();
|
|
774
|
+
for (const [jobId] of this.shellCommandJobMonitors)
|
|
775
|
+
this.stopShellCommandJobMonitor(jobId);
|
|
756
776
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
757
777
|
}
|
|
758
778
|
async controlStopTask(localSessionId) {
|
|
@@ -1269,11 +1289,14 @@ export class ProxySessionController {
|
|
|
1269
1289
|
}
|
|
1270
1290
|
switch (command.type) {
|
|
1271
1291
|
case "shell":
|
|
1272
|
-
await this.runShellCommand(contextKey, command.command);
|
|
1292
|
+
await this.runShellCommand(contextKey, command.command, messageId);
|
|
1273
1293
|
return;
|
|
1274
1294
|
case "dir":
|
|
1275
1295
|
await this.openDirectoryBrowser(contextKey, command.directory);
|
|
1276
1296
|
return;
|
|
1297
|
+
case "file":
|
|
1298
|
+
await this.sendCurrentTaskFile(contextKey, command.filePath);
|
|
1299
|
+
return;
|
|
1277
1300
|
case "new":
|
|
1278
1301
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1279
1302
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
@@ -1416,6 +1439,7 @@ export class ProxySessionController {
|
|
|
1416
1439
|
return recovery;
|
|
1417
1440
|
}
|
|
1418
1441
|
async runStartupRecovery() {
|
|
1442
|
+
await this.recoverShellCommandJobs();
|
|
1419
1443
|
this.backfillTurnAttemptsForUpgrade();
|
|
1420
1444
|
const attempts = this.store.listIncompleteTurnAttempts();
|
|
1421
1445
|
const recoveryCutoff = Date.now() - RECOVERY_ACTIVITY_WINDOW_MS;
|
|
@@ -2454,8 +2478,8 @@ export class ProxySessionController {
|
|
|
2454
2478
|
}
|
|
2455
2479
|
async createFeishuGroupWithTask(sourceContextKey, agentName, requestedTitle, userId, boundProjectCwd, executionSettings) {
|
|
2456
2480
|
const explicitTitle = normalizeTaskTitle(requestedTitle);
|
|
2457
|
-
const taskTitle = explicitTitle ?? `新任务 (${
|
|
2458
|
-
const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup"
|
|
2481
|
+
const taskTitle = explicitTitle ?? `新任务 (${formatGroupNameDate(new Date(), this.config.feishu?.groupNameFormat?.dateFormat ?? DEFAULT_GROUP_NAME_FORMAT.dateFormat)})`;
|
|
2482
|
+
const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup");
|
|
2459
2483
|
let task;
|
|
2460
2484
|
try {
|
|
2461
2485
|
task = await this.createSession(group.contextKey, agentName, boundProjectCwd, false, false, undefined, undefined, taskTitle, executionSettings);
|
|
@@ -2501,7 +2525,7 @@ export class ProxySessionController {
|
|
|
2501
2525
|
async forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userId, sourceSummary) {
|
|
2502
2526
|
const { plan } = prepared;
|
|
2503
2527
|
const boundProjectCwd = detectProjectlessWorkspace(plan.cwd) ? undefined : plan.cwd;
|
|
2504
|
-
const group = await this.createFeishuGroupContext(sourceContextKey, plan.agentName, plan.forkTitle, userId, boundProjectCwd, "/forkgroup"
|
|
2528
|
+
const group = await this.createFeishuGroupContext(sourceContextKey, plan.agentName, plan.forkTitle, userId, boundProjectCwd, "/forkgroup");
|
|
2505
2529
|
let forked;
|
|
2506
2530
|
try {
|
|
2507
2531
|
forked = await this.forkSessionIntoContext(group.contextKey, plan);
|
|
@@ -2525,11 +2549,17 @@ export class ProxySessionController {
|
|
|
2525
2549
|
await this.outbound.sendText(sourceContextKey, `已将${sourceSummary} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`);
|
|
2526
2550
|
return { group, task: forked.record };
|
|
2527
2551
|
}
|
|
2528
|
-
async createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, commandName
|
|
2552
|
+
async createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, commandName) {
|
|
2529
2553
|
if (!userId?.startsWith("ou_")) {
|
|
2530
2554
|
throw new Error(`${commandName} 只能由具有 open_id 的飞书用户消息触发。`);
|
|
2531
2555
|
}
|
|
2532
|
-
const groupName = formatNewGroupName(
|
|
2556
|
+
const groupName = formatNewGroupName({
|
|
2557
|
+
agentName,
|
|
2558
|
+
projectCwd: boundProjectCwd,
|
|
2559
|
+
taskName: taskTitle,
|
|
2560
|
+
date: new Date(),
|
|
2561
|
+
format: this.config.feishu?.groupNameFormat,
|
|
2562
|
+
});
|
|
2533
2563
|
const group = await this.outbound.createGroup(sourceContextKey, {
|
|
2534
2564
|
name: groupName,
|
|
2535
2565
|
userOpenId: userId,
|
|
@@ -3510,102 +3540,138 @@ export class ProxySessionController {
|
|
|
3510
3540
|
this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
|
|
3511
3541
|
}
|
|
3512
3542
|
}
|
|
3513
|
-
async runShellCommand(contextKey, command) {
|
|
3543
|
+
async runShellCommand(contextKey, command, sourceMessageId) {
|
|
3514
3544
|
const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
|
|
3515
|
-
const
|
|
3516
|
-
let
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
};
|
|
3521
|
-
let runningState = {
|
|
3545
|
+
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
3546
|
+
let cardMessageId;
|
|
3547
|
+
const initialState = {
|
|
3548
|
+
jobId: created.id,
|
|
3549
|
+
contextKey,
|
|
3522
3550
|
command,
|
|
3523
3551
|
cwd,
|
|
3524
|
-
|
|
3552
|
+
output: "",
|
|
3553
|
+
outputTruncated: false,
|
|
3525
3554
|
status: "running",
|
|
3526
3555
|
elapsedMs: 0,
|
|
3527
3556
|
};
|
|
3528
|
-
const cardMessageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(runningState));
|
|
3529
|
-
const scheduler = cardMessageId
|
|
3530
|
-
? new CardUpdateScheduler({
|
|
3531
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
3532
|
-
write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
|
|
3533
|
-
onError: (error) => this.logger.warn({ error, contextKey, messageId: cardMessageId }, "Failed to update a shell command card."),
|
|
3534
|
-
})
|
|
3535
|
-
: undefined;
|
|
3536
|
-
scheduler?.seed(runningState);
|
|
3537
|
-
const elapsedTimer = scheduler ? setInterval(() => {
|
|
3538
|
-
runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
|
|
3539
|
-
scheduler.update(runningState);
|
|
3540
|
-
}, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
|
|
3541
|
-
elapsedTimer?.unref?.();
|
|
3542
|
-
const updateRunningOutput = (snapshot) => {
|
|
3543
|
-
latestOutput = snapshot;
|
|
3544
|
-
runningState = {
|
|
3545
|
-
...runningState,
|
|
3546
|
-
...snapshot,
|
|
3547
|
-
elapsedMs: Date.now() - startedAt,
|
|
3548
|
-
};
|
|
3549
|
-
scheduler?.update(runningState);
|
|
3550
|
-
};
|
|
3551
|
-
let result;
|
|
3552
3557
|
try {
|
|
3553
|
-
|
|
3558
|
+
cardMessageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(initialState));
|
|
3559
|
+
if (!cardMessageId)
|
|
3560
|
+
throw new Error("Failed to create the shell command card.");
|
|
3561
|
+
const bound = await this.shellCommandJobs.bindCard(created.id, cardMessageId);
|
|
3562
|
+
await this.shellCommandJobs.startJob(created.id);
|
|
3563
|
+
this.monitorShellCommandJob(bound, initialState);
|
|
3554
3564
|
}
|
|
3555
3565
|
catch (error) {
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
const failedCard = this.cardRenderer.renderShellCommandCard({
|
|
3559
|
-
command,
|
|
3560
|
-
cwd,
|
|
3561
|
-
...latestOutput,
|
|
3562
|
-
stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
|
|
3563
|
-
status: "failed",
|
|
3564
|
-
elapsedMs: Date.now() - startedAt,
|
|
3565
|
-
});
|
|
3566
|
-
if (scheduler) {
|
|
3566
|
+
await this.shellCommandJobs.failJob(created.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
3567
|
+
if (cardMessageId) {
|
|
3567
3568
|
try {
|
|
3568
|
-
await
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
status: "failed",
|
|
3572
|
-
elapsedMs: Date.now() - startedAt,
|
|
3573
|
-
});
|
|
3569
|
+
const failed = await this.shellCommandJobs.readJob(created.id);
|
|
3570
|
+
await this.outbound.updateInteractiveCard(contextKey, cardMessageId, this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)));
|
|
3571
|
+
await this.shellCommandJobs.markPresented(created.id);
|
|
3574
3572
|
}
|
|
3575
3573
|
catch (cardError) {
|
|
3576
|
-
this.logger.warn({ error: cardError, contextKey, messageId: cardMessageId }, "Failed to finalize a shell command card.");
|
|
3577
|
-
}
|
|
3578
|
-
finally {
|
|
3579
|
-
scheduler.dispose();
|
|
3574
|
+
this.logger.warn({ error: cardError, jobId: created.id, contextKey, messageId: cardMessageId }, "Failed to finalize a shell command card after the runner failed to start.");
|
|
3580
3575
|
}
|
|
3581
3576
|
}
|
|
3582
3577
|
else {
|
|
3583
|
-
await this.
|
|
3578
|
+
await this.shellCommandJobs.markPresented(created.id).catch(() => undefined);
|
|
3584
3579
|
}
|
|
3585
3580
|
throw error;
|
|
3586
3581
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3582
|
+
}
|
|
3583
|
+
monitorShellCommandJob(job, seededState) {
|
|
3584
|
+
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id))
|
|
3585
|
+
return;
|
|
3586
|
+
const scheduler = new CardUpdateScheduler({
|
|
3587
|
+
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
3588
|
+
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId, card),
|
|
3589
|
+
onError: (error) => this.logger.warn({ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId }, "Failed to update a background shell command card."),
|
|
3590
|
+
});
|
|
3591
|
+
if (seededState)
|
|
3592
|
+
scheduler.seed(seededState);
|
|
3593
|
+
const monitor = {
|
|
3594
|
+
scheduler,
|
|
3595
|
+
refreshing: false,
|
|
3596
|
+
timer: setInterval(() => {
|
|
3597
|
+
void this.refreshShellCommandJob(job.id);
|
|
3598
|
+
}, SHELL_COMMAND_JOB_POLL_INTERVAL_MS),
|
|
3598
3599
|
};
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3600
|
+
monitor.timer.unref?.();
|
|
3601
|
+
this.shellCommandJobMonitors.set(job.id, monitor);
|
|
3602
|
+
void this.refreshShellCommandJob(job.id, seededState === undefined);
|
|
3603
|
+
}
|
|
3604
|
+
async refreshShellCommandJob(jobId, critical = false) {
|
|
3605
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
3606
|
+
if (!monitor || monitor.refreshing)
|
|
3607
|
+
return;
|
|
3608
|
+
monitor.refreshing = true;
|
|
3609
|
+
try {
|
|
3610
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
3611
|
+
const view = shellCommandJobCardView(job);
|
|
3612
|
+
if (isActiveShellCommandJob(job.status)) {
|
|
3613
|
+
monitor.scheduler.update(view, critical ? "critical" : "normal");
|
|
3614
|
+
return;
|
|
3602
3615
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3616
|
+
await monitor.scheduler.flush(view);
|
|
3617
|
+
if (job.sourceMessageId) {
|
|
3618
|
+
await this.finalizeStandaloneMessageReaction(job.sourceMessageId, job.status === "completed" ? "completed" : job.status === "cancelled" ? "cancelled" : "failed");
|
|
3605
3619
|
}
|
|
3620
|
+
await this.shellCommandJobs.markPresented(jobId);
|
|
3621
|
+
this.stopShellCommandJobMonitor(jobId);
|
|
3606
3622
|
}
|
|
3607
|
-
|
|
3608
|
-
|
|
3623
|
+
catch (error) {
|
|
3624
|
+
this.logger.warn({ error, jobId }, "Failed to refresh a background shell command job.");
|
|
3625
|
+
}
|
|
3626
|
+
finally {
|
|
3627
|
+
const current = this.shellCommandJobMonitors.get(jobId);
|
|
3628
|
+
if (current)
|
|
3629
|
+
current.refreshing = false;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
stopShellCommandJobMonitor(jobId) {
|
|
3633
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
3634
|
+
if (!monitor)
|
|
3635
|
+
return;
|
|
3636
|
+
clearInterval(monitor.timer);
|
|
3637
|
+
monitor.scheduler.dispose();
|
|
3638
|
+
this.shellCommandJobMonitors.delete(jobId);
|
|
3639
|
+
}
|
|
3640
|
+
async cancelShellCommandJob(contextKey, jobId) {
|
|
3641
|
+
if (!jobId)
|
|
3642
|
+
throw new Error("命令任务无效,请使用最新的命令卡片。");
|
|
3643
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
3644
|
+
if (job.contextKey !== contextKey)
|
|
3645
|
+
throw new Error("该命令不属于当前会话。");
|
|
3646
|
+
const requested = await this.shellCommandJobs.requestCancellation(jobId);
|
|
3647
|
+
if (!requested) {
|
|
3648
|
+
await this.outbound.sendText(contextKey, "该命令已经结束。");
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
if (!this.shellCommandJobMonitors.has(jobId))
|
|
3652
|
+
this.monitorShellCommandJob(job);
|
|
3653
|
+
await this.refreshShellCommandJob(jobId, true);
|
|
3654
|
+
}
|
|
3655
|
+
async recoverShellCommandJobs() {
|
|
3656
|
+
const jobs = await this.shellCommandJobs.listRecoverableJobs();
|
|
3657
|
+
if (jobs.length > 0) {
|
|
3658
|
+
this.logger.warn({ jobIds: jobs.map((job) => job.id) }, "Recovering background shell command jobs after startup.");
|
|
3659
|
+
}
|
|
3660
|
+
for (const job of jobs) {
|
|
3661
|
+
if (!job.cardMessageId || !this.outbound.canRoute(job.contextKey))
|
|
3662
|
+
continue;
|
|
3663
|
+
if (job.status === "starting"
|
|
3664
|
+
&& job.runnerPid === undefined
|
|
3665
|
+
&& Date.now() - job.updatedAt >= SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS) {
|
|
3666
|
+
try {
|
|
3667
|
+
await this.shellCommandJobs.startJob(job.id);
|
|
3668
|
+
}
|
|
3669
|
+
catch (error) {
|
|
3670
|
+
await this.shellCommandJobs.failJob(job.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
const refreshed = await this.shellCommandJobs.readJob(job.id);
|
|
3674
|
+
this.monitorShellCommandJob(refreshed);
|
|
3609
3675
|
}
|
|
3610
3676
|
}
|
|
3611
3677
|
async setTitle(contextKey, title) {
|
|
@@ -3622,18 +3688,29 @@ export class ProxySessionController {
|
|
|
3622
3688
|
await this.outbound.sendText(contextKey, `已将当前任务标题修改为:${normalizedTitle}`);
|
|
3623
3689
|
}
|
|
3624
3690
|
async syncTaskTitleFromGroupName(contextKey, event) {
|
|
3625
|
-
const parsed = parseAgentGroupName(event.afterName);
|
|
3626
|
-
if (!parsed)
|
|
3627
|
-
return;
|
|
3628
3691
|
const context = this.store.getUserContext(contextKey);
|
|
3629
3692
|
if (!context?.currentSessionId)
|
|
3630
3693
|
return;
|
|
3631
3694
|
const record = this.store.getSessionForContext(context.currentSessionId, contextKey);
|
|
3632
3695
|
if (!record || record.status === "closed")
|
|
3633
3696
|
return;
|
|
3634
|
-
|
|
3697
|
+
const nameFormat = this.config.feishu?.groupNameFormat ?? DEFAULT_GROUP_NAME_FORMAT;
|
|
3698
|
+
const projectCwd = detectProjectlessWorkspace(record.cwd) ? undefined : record.cwd;
|
|
3699
|
+
const templateTitle = parseTaskNameFromGroupName({
|
|
3700
|
+
agentName: record.agentName,
|
|
3701
|
+
groupName: event.afterName,
|
|
3702
|
+
projectCwd,
|
|
3703
|
+
format: nameFormat,
|
|
3704
|
+
});
|
|
3705
|
+
const legacy = parseAgentGroupName(event.afterName);
|
|
3706
|
+
const legacyTitle = legacy && record.agentName.toLowerCase() === legacy.agentName.toLowerCase()
|
|
3707
|
+
? legacy.title
|
|
3708
|
+
: undefined;
|
|
3709
|
+
if (isDefaultGroupNameFormat(nameFormat) && isLegacyGroupPrefixOnly(event.afterName))
|
|
3635
3710
|
return;
|
|
3636
|
-
const title = normalizeTaskTitle(
|
|
3711
|
+
const title = normalizeTaskTitle(isDefaultGroupNameFormat(nameFormat)
|
|
3712
|
+
? legacyTitle ?? templateTitle
|
|
3713
|
+
: templateTitle ?? legacyTitle);
|
|
3637
3714
|
if (!title || title === record.title)
|
|
3638
3715
|
return;
|
|
3639
3716
|
const loaded = await this.loadSession(record);
|
|
@@ -3969,6 +4046,12 @@ export class ProxySessionController {
|
|
|
3969
4046
|
if (!stats.isFile())
|
|
3970
4047
|
throw new Error(`这不是普通文件:${filePath}`);
|
|
3971
4048
|
}
|
|
4049
|
+
async sendCurrentTaskFile(contextKey, requestedFilePath) {
|
|
4050
|
+
const record = this.requireCurrentSession(contextKey);
|
|
4051
|
+
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
4052
|
+
await this.assertSendableFile(filePath);
|
|
4053
|
+
await this.outbound.sendFile(contextKey, filePath);
|
|
4054
|
+
}
|
|
3972
4055
|
async listSessions(contextKey, searchTerm, options = {}) {
|
|
3973
4056
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
|
|
3974
4057
|
const normalizedSearch = searchTerm?.trim().toLowerCase();
|
|
@@ -5304,7 +5387,7 @@ function isBotOwnedActiveTurn(record, remote) {
|
|
|
5304
5387
|
&& record.lastTurnId === remote.lastTurnId;
|
|
5305
5388
|
}
|
|
5306
5389
|
function isQueueIndependentCommand(command) {
|
|
5307
|
-
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type))
|
|
5390
|
+
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
|
|
5308
5391
|
return true;
|
|
5309
5392
|
if (command.type === "agent")
|
|
5310
5393
|
return command.agent === undefined;
|
|
@@ -5312,8 +5395,22 @@ function isQueueIndependentCommand(command) {
|
|
|
5312
5395
|
return true;
|
|
5313
5396
|
return false;
|
|
5314
5397
|
}
|
|
5398
|
+
function shellCommandJobCardView(job) {
|
|
5399
|
+
const status = job.status === "starting" ? "running" : job.status;
|
|
5400
|
+
return {
|
|
5401
|
+
jobId: job.id,
|
|
5402
|
+
contextKey: job.contextKey,
|
|
5403
|
+
command: job.command,
|
|
5404
|
+
cwd: job.cwd,
|
|
5405
|
+
output: [job.output.trimEnd(), job.error].filter(Boolean).join("\n"),
|
|
5406
|
+
status,
|
|
5407
|
+
exitCode: job.exitCode,
|
|
5408
|
+
elapsedMs: Math.max(0, (job.completedAt ?? Date.now()) - job.startedAt),
|
|
5409
|
+
outputTruncated: job.outputTruncated,
|
|
5410
|
+
};
|
|
5411
|
+
}
|
|
5315
5412
|
function commandRequiresCurrentSession(command) {
|
|
5316
|
-
if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
5413
|
+
if (["dismiss", "stop", "title", "turns", "file", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
5317
5414
|
return true;
|
|
5318
5415
|
}
|
|
5319
5416
|
if (command.type === "archive" || command.type === "fork")
|
|
@@ -5485,8 +5582,8 @@ async function listWindowsDriveRoots() {
|
|
|
5485
5582
|
throw new Error("没有检测到可用的 Windows 磁盘。");
|
|
5486
5583
|
return drives;
|
|
5487
5584
|
}
|
|
5488
|
-
function
|
|
5489
|
-
return command.type === "prompt" || command.type === "nosteer";
|
|
5585
|
+
function commandDefersReactionFinalization(command) {
|
|
5586
|
+
return command.type === "prompt" || command.type === "nosteer" || command.type === "shell";
|
|
5490
5587
|
}
|
|
5491
5588
|
function runtimePrompt(text, localImagePaths) {
|
|
5492
5589
|
return localImagePaths?.length ? { text, localImagePaths } : text;
|
|
@@ -5636,34 +5733,6 @@ function isRecoveryAttemptRecent(attempt, cutoff) {
|
|
|
5636
5733
|
const updatedAt = Date.parse(attempt.updatedAt);
|
|
5637
5734
|
return Number.isFinite(updatedAt) && updatedAt >= cutoff;
|
|
5638
5735
|
}
|
|
5639
|
-
function formatGroupDateSuffix(date) {
|
|
5640
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
5641
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
5642
|
-
return `${month}-${day}`;
|
|
5643
|
-
}
|
|
5644
|
-
function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimestamp) {
|
|
5645
|
-
const prefix = `[${agentName}] `;
|
|
5646
|
-
const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
|
|
5647
|
-
const titlePrefix = `${prefix}${projectPrefix}`;
|
|
5648
|
-
const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
|
|
5649
|
-
const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
|
|
5650
|
-
- Array.from(titlePrefix).length
|
|
5651
|
-
- Array.from(timestampSuffix).length;
|
|
5652
|
-
const title = truncateTail(taskTitle, availableTitleLength);
|
|
5653
|
-
return truncateTail(`${titlePrefix}${title}${timestampSuffix}`, FEISHU_GROUP_NAME_MAX_LENGTH);
|
|
5654
|
-
}
|
|
5655
|
-
const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
|
|
5656
|
-
const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
|
|
5657
|
-
function formatGroupProjectDirectory(projectCwd) {
|
|
5658
|
-
const value = abbreviateHomeDirectory(projectCwd);
|
|
5659
|
-
const levels = value
|
|
5660
|
-
.replace(/[\\/]+$/, "")
|
|
5661
|
-
.split(/[\\/]+/)
|
|
5662
|
-
.filter(Boolean)
|
|
5663
|
-
.slice(-2);
|
|
5664
|
-
const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
|
|
5665
|
-
return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
|
|
5666
|
-
}
|
|
5667
5736
|
function abbreviateHomeDirectory(value) {
|
|
5668
5737
|
const homeDirectory = os.homedir();
|
|
5669
5738
|
const valueIsWindows = isWindowsAbsolutePath(value);
|
|
@@ -5681,33 +5750,6 @@ function abbreviateHomeDirectory(value) {
|
|
|
5681
5750
|
function isWindowsAbsolutePath(value) {
|
|
5682
5751
|
return /^[a-z]:[\\/]|^\\\\/iu.test(value);
|
|
5683
5752
|
}
|
|
5684
|
-
function fitTrailingPathLevels(levels, maxLength, separator) {
|
|
5685
|
-
if (levels.length === 0)
|
|
5686
|
-
return "";
|
|
5687
|
-
if (levels.length === 1)
|
|
5688
|
-
return truncatePathLevel(levels[0], maxLength);
|
|
5689
|
-
const parent = levels[0];
|
|
5690
|
-
const leaf = levels[1];
|
|
5691
|
-
const joined = `${parent}${separator}${leaf}`;
|
|
5692
|
-
if (Array.from(joined).length <= maxLength)
|
|
5693
|
-
return joined;
|
|
5694
|
-
if (Array.from(leaf).length <= maxLength)
|
|
5695
|
-
return leaf;
|
|
5696
|
-
return truncatePathLevel(leaf, maxLength);
|
|
5697
|
-
}
|
|
5698
|
-
function truncatePathLevel(value, maxLength) {
|
|
5699
|
-
return truncateTail(value, maxLength);
|
|
5700
|
-
}
|
|
5701
|
-
function truncateTail(value, maxLength) {
|
|
5702
|
-
const characters = Array.from(value);
|
|
5703
|
-
if (characters.length <= maxLength)
|
|
5704
|
-
return value;
|
|
5705
|
-
if (maxLength <= 0)
|
|
5706
|
-
return "";
|
|
5707
|
-
if (maxLength <= 3)
|
|
5708
|
-
return ".".repeat(maxLength);
|
|
5709
|
-
return `${characters.slice(0, maxLength - 3).join("")}...`;
|
|
5710
|
-
}
|
|
5711
5753
|
function parseAgentGroupName(value) {
|
|
5712
5754
|
const match = /^\[([^[\]]+)\]\s+(.+)$/.exec(value.trim());
|
|
5713
5755
|
const agentName = match?.[1]?.trim();
|
|
@@ -5718,6 +5760,13 @@ function parseAgentGroupName(value) {
|
|
|
5718
5760
|
const title = projectPrefix ? projectPrefix[2]?.trim() : remainder;
|
|
5719
5761
|
return agentName && title ? { agentName, title } : undefined;
|
|
5720
5762
|
}
|
|
5763
|
+
function isDefaultGroupNameFormat(format) {
|
|
5764
|
+
return format.project === DEFAULT_GROUP_NAME_FORMAT.project
|
|
5765
|
+
&& format.projectless === DEFAULT_GROUP_NAME_FORMAT.projectless;
|
|
5766
|
+
}
|
|
5767
|
+
function isLegacyGroupPrefixOnly(value) {
|
|
5768
|
+
return /^\[[^[\]]+\]\s+\[[^[\]]+\]\s*$/u.test(value.trim());
|
|
5769
|
+
}
|
|
5721
5770
|
function validateGoalObjective(objective) {
|
|
5722
5771
|
const length = Array.from(objective.trim()).length;
|
|
5723
5772
|
if (length === 0)
|