@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
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
7
7
|
import { resolveUserPath } from "../config/paths.js";
|
|
8
|
-
import type
|
|
8
|
+
import { DEFAULT_GROUP_NAME_FORMAT, type AppConfig } from "../config/schema.js";
|
|
9
9
|
import { CommandRouter } from "../commands/CommandRouter.js";
|
|
10
10
|
import type { Command } from "../commands/commandTypes.js";
|
|
11
11
|
import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
|
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
} from "../feishu/CardRenderer.js";
|
|
31
31
|
import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
|
|
32
32
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
33
|
+
import {
|
|
34
|
+
formatGroupNameDate,
|
|
35
|
+
formatNewGroupName,
|
|
36
|
+
parseTaskNameFromGroupName,
|
|
37
|
+
} from "../feishu/GroupNameFormatter.js";
|
|
33
38
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
34
39
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
35
40
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
@@ -47,6 +52,12 @@ import type {
|
|
|
47
52
|
RuntimePrompt,
|
|
48
53
|
RuntimeSession,
|
|
49
54
|
} from "../runtime/types.js";
|
|
55
|
+
import {
|
|
56
|
+
isActiveShellCommandJob,
|
|
57
|
+
ShellCommandJobManager,
|
|
58
|
+
type ShellCommandJobManagerLike,
|
|
59
|
+
type ShellCommandJobSnapshot,
|
|
60
|
+
} from "../shell/ShellCommandJobManager.js";
|
|
50
61
|
import {
|
|
51
62
|
StateStore,
|
|
52
63
|
type CardActionBinding,
|
|
@@ -63,7 +74,6 @@ import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
|
63
74
|
import {
|
|
64
75
|
executeShellCommand,
|
|
65
76
|
type ShellCommandOptions,
|
|
66
|
-
type ShellCommandOutputSnapshot,
|
|
67
77
|
type ShellCommandResult,
|
|
68
78
|
} from "../utils/executeShellCommand.js";
|
|
69
79
|
|
|
@@ -94,6 +104,12 @@ interface ForwardAttachmentReservation {
|
|
|
94
104
|
registry: Map<string, PendingForwardAttachment>;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
interface ShellCommandJobMonitor {
|
|
108
|
+
scheduler: CardUpdateScheduler<ShellCommandCardView>;
|
|
109
|
+
timer: NodeJS.Timeout;
|
|
110
|
+
refreshing: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
98
114
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
99
115
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -105,7 +121,8 @@ const RESET_HISTORY_PAGE_SIZE = 10;
|
|
|
105
121
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
106
122
|
const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
|
|
107
123
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
108
|
-
const
|
|
124
|
+
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
125
|
+
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
109
126
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
110
127
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
111
128
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -150,6 +167,12 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
150
167
|
usage: "[目录]",
|
|
151
168
|
description: "浏览、发送文件,并从选定目录创建任务或群",
|
|
152
169
|
},
|
|
170
|
+
{
|
|
171
|
+
command: "/file",
|
|
172
|
+
usage: "<文件路径>",
|
|
173
|
+
description: "将指定文件发送到当前飞书会话",
|
|
174
|
+
requiresArgument: true,
|
|
175
|
+
},
|
|
153
176
|
{
|
|
154
177
|
command: "/forkgroup",
|
|
155
178
|
usage: "[title]",
|
|
@@ -404,6 +427,7 @@ export class ProxySessionController {
|
|
|
404
427
|
private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
|
|
405
428
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
406
429
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
430
|
+
private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
|
|
407
431
|
private readonly unsubscribe: Array<() => void> = [];
|
|
408
432
|
private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
|
|
409
433
|
private startupRecovery?: Promise<void>;
|
|
@@ -418,6 +442,9 @@ export class ProxySessionController {
|
|
|
418
442
|
private readonly lifecycle?: ProxyLifecycle,
|
|
419
443
|
private readonly shellCommandExecutor: ShellCommandExecutor = executeShellCommand,
|
|
420
444
|
private readonly windowsDriveLister: WindowsDriveLister = listWindowsDriveRoots,
|
|
445
|
+
private readonly shellCommandJobs: ShellCommandJobManagerLike = new ShellCommandJobManager(
|
|
446
|
+
path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
|
|
447
|
+
),
|
|
421
448
|
) {
|
|
422
449
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
423
450
|
this.unsubscribe.push(
|
|
@@ -708,7 +735,9 @@ export class ProxySessionController {
|
|
|
708
735
|
message.userId,
|
|
709
736
|
message,
|
|
710
737
|
);
|
|
711
|
-
if (!
|
|
738
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
739
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
740
|
+
}
|
|
712
741
|
} catch (error) {
|
|
713
742
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
714
743
|
await this.sendError(message.contextKey, error);
|
|
@@ -731,7 +760,9 @@ export class ProxySessionController {
|
|
|
731
760
|
message.userId,
|
|
732
761
|
message,
|
|
733
762
|
);
|
|
734
|
-
if (!
|
|
763
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
764
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
765
|
+
}
|
|
735
766
|
} catch (error) {
|
|
736
767
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
737
768
|
await this.sendError(message.contextKey, error);
|
|
@@ -786,6 +817,11 @@ export class ProxySessionController {
|
|
|
786
817
|
} else if (kind === "turn_cancel") {
|
|
787
818
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
788
819
|
await this.cancelSession(this.requireSession(contextKey, sessionId));
|
|
820
|
+
} else if (kind === "shell_command_cancel") {
|
|
821
|
+
await this.cancelShellCommandJob(
|
|
822
|
+
contextKey,
|
|
823
|
+
String(scopedAction.value.jobId ?? ""),
|
|
824
|
+
);
|
|
789
825
|
} else if (kind === "turn_reset") {
|
|
790
826
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
791
827
|
await this.resetCurrentSessionToTurn(
|
|
@@ -1057,6 +1093,7 @@ export class ProxySessionController {
|
|
|
1057
1093
|
this.relatedReactionMessageIds.clear();
|
|
1058
1094
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1059
1095
|
this.recoveryRetryTimers.clear();
|
|
1096
|
+
for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
|
|
1060
1097
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
1061
1098
|
}
|
|
1062
1099
|
|
|
@@ -1700,11 +1737,14 @@ export class ProxySessionController {
|
|
|
1700
1737
|
}
|
|
1701
1738
|
switch (command.type) {
|
|
1702
1739
|
case "shell":
|
|
1703
|
-
await this.runShellCommand(contextKey, command.command);
|
|
1740
|
+
await this.runShellCommand(contextKey, command.command, messageId);
|
|
1704
1741
|
return;
|
|
1705
1742
|
case "dir":
|
|
1706
1743
|
await this.openDirectoryBrowser(contextKey, command.directory);
|
|
1707
1744
|
return;
|
|
1745
|
+
case "file":
|
|
1746
|
+
await this.sendCurrentTaskFile(contextKey, command.filePath);
|
|
1747
|
+
return;
|
|
1708
1748
|
case "new":
|
|
1709
1749
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1710
1750
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
@@ -1872,6 +1912,7 @@ export class ProxySessionController {
|
|
|
1872
1912
|
}
|
|
1873
1913
|
|
|
1874
1914
|
private async runStartupRecovery(): Promise<void> {
|
|
1915
|
+
await this.recoverShellCommandJobs();
|
|
1875
1916
|
this.backfillTurnAttemptsForUpgrade();
|
|
1876
1917
|
const attempts = this.store.listIncompleteTurnAttempts();
|
|
1877
1918
|
const recoveryCutoff = Date.now() - RECOVERY_ACTIVITY_WINDOW_MS;
|
|
@@ -3186,7 +3227,10 @@ export class ProxySessionController {
|
|
|
3186
3227
|
executionSettings: SessionExecutionSettings,
|
|
3187
3228
|
): Promise<CreatedFeishuTaskGroup> {
|
|
3188
3229
|
const explicitTitle = normalizeTaskTitle(requestedTitle);
|
|
3189
|
-
const taskTitle = explicitTitle ?? `新任务 (${
|
|
3230
|
+
const taskTitle = explicitTitle ?? `新任务 (${formatGroupNameDate(
|
|
3231
|
+
new Date(),
|
|
3232
|
+
this.config.feishu?.groupNameFormat?.dateFormat ?? DEFAULT_GROUP_NAME_FORMAT.dateFormat,
|
|
3233
|
+
)})`;
|
|
3190
3234
|
const group = await this.createFeishuGroupContext(
|
|
3191
3235
|
sourceContextKey,
|
|
3192
3236
|
agentName,
|
|
@@ -3194,7 +3238,6 @@ export class ProxySessionController {
|
|
|
3194
3238
|
userId,
|
|
3195
3239
|
boundProjectCwd,
|
|
3196
3240
|
"/newgroup",
|
|
3197
|
-
false,
|
|
3198
3241
|
);
|
|
3199
3242
|
|
|
3200
3243
|
let task: SessionRecord;
|
|
@@ -3295,7 +3338,6 @@ export class ProxySessionController {
|
|
|
3295
3338
|
userId,
|
|
3296
3339
|
boundProjectCwd,
|
|
3297
3340
|
"/forkgroup",
|
|
3298
|
-
false,
|
|
3299
3341
|
);
|
|
3300
3342
|
|
|
3301
3343
|
let forked: ForkSessionResult;
|
|
@@ -3338,12 +3380,17 @@ export class ProxySessionController {
|
|
|
3338
3380
|
userId: string | undefined,
|
|
3339
3381
|
boundProjectCwd: string | undefined,
|
|
3340
3382
|
commandName: "/newgroup" | "/forkgroup",
|
|
3341
|
-
includeTimestamp: boolean,
|
|
3342
3383
|
): Promise<CreatedFeishuGroupContext> {
|
|
3343
3384
|
if (!userId?.startsWith("ou_")) {
|
|
3344
3385
|
throw new Error(`${commandName} 只能由具有 open_id 的飞书用户消息触发。`);
|
|
3345
3386
|
}
|
|
3346
|
-
const groupName = formatNewGroupName(
|
|
3387
|
+
const groupName = formatNewGroupName({
|
|
3388
|
+
agentName,
|
|
3389
|
+
projectCwd: boundProjectCwd,
|
|
3390
|
+
taskName: taskTitle,
|
|
3391
|
+
date: new Date(),
|
|
3392
|
+
format: this.config.feishu?.groupNameFormat,
|
|
3393
|
+
});
|
|
3347
3394
|
const group = await this.outbound.createGroup(sourceContextKey, {
|
|
3348
3395
|
name: groupName,
|
|
3349
3396
|
userOpenId: userId,
|
|
@@ -4510,106 +4557,155 @@ export class ProxySessionController {
|
|
|
4510
4557
|
}
|
|
4511
4558
|
}
|
|
4512
4559
|
|
|
4513
|
-
private async runShellCommand(
|
|
4560
|
+
private async runShellCommand(
|
|
4561
|
+
contextKey: string,
|
|
4562
|
+
command: string,
|
|
4563
|
+
sourceMessageId?: string,
|
|
4564
|
+
): Promise<void> {
|
|
4514
4565
|
const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
|
|
4515
|
-
const
|
|
4516
|
-
let
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
};
|
|
4521
|
-
let runningState: ShellCommandCardView = {
|
|
4566
|
+
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
4567
|
+
let cardMessageId: string | undefined;
|
|
4568
|
+
const initialState: ShellCommandCardView = {
|
|
4569
|
+
jobId: created.id,
|
|
4570
|
+
contextKey,
|
|
4522
4571
|
command,
|
|
4523
4572
|
cwd,
|
|
4524
|
-
|
|
4573
|
+
output: "",
|
|
4574
|
+
outputTruncated: false,
|
|
4525
4575
|
status: "running",
|
|
4526
4576
|
elapsedMs: 0,
|
|
4527
4577
|
};
|
|
4528
|
-
const cardMessageId = await this.outbound.sendInteractiveCard(
|
|
4529
|
-
contextKey,
|
|
4530
|
-
this.cardRenderer.renderShellCommandCard(runningState),
|
|
4531
|
-
);
|
|
4532
|
-
const scheduler = cardMessageId
|
|
4533
|
-
? new CardUpdateScheduler<ShellCommandCardView>({
|
|
4534
|
-
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
4535
|
-
write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
|
|
4536
|
-
onError: (error) => this.logger.warn(
|
|
4537
|
-
{ error, contextKey, messageId: cardMessageId },
|
|
4538
|
-
"Failed to update a shell command card.",
|
|
4539
|
-
),
|
|
4540
|
-
})
|
|
4541
|
-
: undefined;
|
|
4542
|
-
scheduler?.seed(runningState);
|
|
4543
|
-
const elapsedTimer = scheduler ? setInterval(() => {
|
|
4544
|
-
runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
|
|
4545
|
-
scheduler.update(runningState);
|
|
4546
|
-
}, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
|
|
4547
|
-
elapsedTimer?.unref?.();
|
|
4548
|
-
|
|
4549
|
-
const updateRunningOutput = (snapshot: ShellCommandOutputSnapshot): void => {
|
|
4550
|
-
latestOutput = snapshot;
|
|
4551
|
-
runningState = {
|
|
4552
|
-
...runningState,
|
|
4553
|
-
...snapshot,
|
|
4554
|
-
elapsedMs: Date.now() - startedAt,
|
|
4555
|
-
};
|
|
4556
|
-
scheduler?.update(runningState);
|
|
4557
|
-
};
|
|
4558
|
-
|
|
4559
|
-
let result: ShellCommandResult;
|
|
4560
4578
|
try {
|
|
4561
|
-
|
|
4579
|
+
cardMessageId = await this.outbound.sendInteractiveCard(
|
|
4580
|
+
contextKey,
|
|
4581
|
+
this.cardRenderer.renderShellCommandCard(initialState),
|
|
4582
|
+
);
|
|
4583
|
+
if (!cardMessageId) throw new Error("Failed to create the shell command card.");
|
|
4584
|
+
const bound = await this.shellCommandJobs.bindCard(created.id, cardMessageId);
|
|
4585
|
+
await this.shellCommandJobs.startJob(created.id);
|
|
4586
|
+
this.monitorShellCommandJob(bound, initialState);
|
|
4562
4587
|
} catch (error) {
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
command,
|
|
4566
|
-
cwd,
|
|
4567
|
-
...latestOutput,
|
|
4568
|
-
stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
|
|
4569
|
-
status: "failed",
|
|
4570
|
-
elapsedMs: Date.now() - startedAt,
|
|
4571
|
-
});
|
|
4572
|
-
if (scheduler) {
|
|
4588
|
+
await this.shellCommandJobs.failJob(created.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
4589
|
+
if (cardMessageId) {
|
|
4573
4590
|
try {
|
|
4574
|
-
await
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4591
|
+
const failed = await this.shellCommandJobs.readJob(created.id);
|
|
4592
|
+
await this.outbound.updateInteractiveCard(
|
|
4593
|
+
contextKey,
|
|
4594
|
+
cardMessageId,
|
|
4595
|
+
this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)),
|
|
4596
|
+
);
|
|
4597
|
+
await this.shellCommandJobs.markPresented(created.id);
|
|
4580
4598
|
} catch (cardError) {
|
|
4581
4599
|
this.logger.warn(
|
|
4582
|
-
{ error: cardError, contextKey, messageId: cardMessageId },
|
|
4583
|
-
"Failed to finalize a shell command card.",
|
|
4600
|
+
{ error: cardError, jobId: created.id, contextKey, messageId: cardMessageId },
|
|
4601
|
+
"Failed to finalize a shell command card after the runner failed to start.",
|
|
4584
4602
|
);
|
|
4585
|
-
} finally {
|
|
4586
|
-
scheduler.dispose();
|
|
4587
4603
|
}
|
|
4588
4604
|
} else {
|
|
4589
|
-
await this.
|
|
4605
|
+
await this.shellCommandJobs.markPresented(created.id).catch(() => undefined);
|
|
4590
4606
|
}
|
|
4591
4607
|
throw error;
|
|
4592
4608
|
}
|
|
4609
|
+
}
|
|
4593
4610
|
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4611
|
+
private monitorShellCommandJob(
|
|
4612
|
+
job: ShellCommandJobSnapshot,
|
|
4613
|
+
seededState?: ShellCommandCardView,
|
|
4614
|
+
): void {
|
|
4615
|
+
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id)) return;
|
|
4616
|
+
const scheduler = new CardUpdateScheduler<ShellCommandCardView>({
|
|
4617
|
+
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
4618
|
+
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId!, card),
|
|
4619
|
+
onError: (error) => this.logger.warn(
|
|
4620
|
+
{ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId },
|
|
4621
|
+
"Failed to update a background shell command card.",
|
|
4622
|
+
),
|
|
4623
|
+
});
|
|
4624
|
+
if (seededState) scheduler.seed(seededState);
|
|
4625
|
+
const monitor: ShellCommandJobMonitor = {
|
|
4626
|
+
scheduler,
|
|
4627
|
+
refreshing: false,
|
|
4628
|
+
timer: setInterval(() => {
|
|
4629
|
+
void this.refreshShellCommandJob(job.id);
|
|
4630
|
+
}, SHELL_COMMAND_JOB_POLL_INTERVAL_MS),
|
|
4604
4631
|
};
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4632
|
+
monitor.timer.unref?.();
|
|
4633
|
+
this.shellCommandJobMonitors.set(job.id, monitor);
|
|
4634
|
+
void this.refreshShellCommandJob(job.id, seededState === undefined);
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
private async refreshShellCommandJob(jobId: string, critical = false): Promise<void> {
|
|
4638
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
4639
|
+
if (!monitor || monitor.refreshing) return;
|
|
4640
|
+
monitor.refreshing = true;
|
|
4641
|
+
try {
|
|
4642
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
4643
|
+
const view = shellCommandJobCardView(job);
|
|
4644
|
+
if (isActiveShellCommandJob(job.status)) {
|
|
4645
|
+
monitor.scheduler.update(view, critical ? "critical" : "normal");
|
|
4646
|
+
return;
|
|
4610
4647
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
4648
|
+
await monitor.scheduler.flush(view);
|
|
4649
|
+
if (job.sourceMessageId) {
|
|
4650
|
+
await this.finalizeStandaloneMessageReaction(
|
|
4651
|
+
job.sourceMessageId,
|
|
4652
|
+
job.status === "completed" ? "completed" : job.status === "cancelled" ? "cancelled" : "failed",
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
await this.shellCommandJobs.markPresented(jobId);
|
|
4656
|
+
this.stopShellCommandJobMonitor(jobId);
|
|
4657
|
+
} catch (error) {
|
|
4658
|
+
this.logger.warn({ error, jobId }, "Failed to refresh a background shell command job.");
|
|
4659
|
+
} finally {
|
|
4660
|
+
const current = this.shellCommandJobMonitors.get(jobId);
|
|
4661
|
+
if (current) current.refreshing = false;
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4665
|
+
private stopShellCommandJobMonitor(jobId: string): void {
|
|
4666
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
4667
|
+
if (!monitor) return;
|
|
4668
|
+
clearInterval(monitor.timer);
|
|
4669
|
+
monitor.scheduler.dispose();
|
|
4670
|
+
this.shellCommandJobMonitors.delete(jobId);
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
private async cancelShellCommandJob(contextKey: string, jobId: string): Promise<void> {
|
|
4674
|
+
if (!jobId) throw new Error("命令任务无效,请使用最新的命令卡片。");
|
|
4675
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
4676
|
+
if (job.contextKey !== contextKey) throw new Error("该命令不属于当前会话。");
|
|
4677
|
+
const requested = await this.shellCommandJobs.requestCancellation(jobId);
|
|
4678
|
+
if (!requested) {
|
|
4679
|
+
await this.outbound.sendText(contextKey, "该命令已经结束。");
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
if (!this.shellCommandJobMonitors.has(jobId)) this.monitorShellCommandJob(job);
|
|
4683
|
+
await this.refreshShellCommandJob(jobId, true);
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4686
|
+
private async recoverShellCommandJobs(): Promise<void> {
|
|
4687
|
+
const jobs = await this.shellCommandJobs.listRecoverableJobs();
|
|
4688
|
+
if (jobs.length > 0) {
|
|
4689
|
+
this.logger.warn(
|
|
4690
|
+
{ jobIds: jobs.map((job) => job.id) },
|
|
4691
|
+
"Recovering background shell command jobs after startup.",
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
for (const job of jobs) {
|
|
4695
|
+
if (!job.cardMessageId || !this.outbound.canRoute(job.contextKey)) continue;
|
|
4696
|
+
if (
|
|
4697
|
+
job.status === "starting"
|
|
4698
|
+
&& job.runnerPid === undefined
|
|
4699
|
+
&& Date.now() - job.updatedAt >= SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS
|
|
4700
|
+
) {
|
|
4701
|
+
try {
|
|
4702
|
+
await this.shellCommandJobs.startJob(job.id);
|
|
4703
|
+
} catch (error) {
|
|
4704
|
+
await this.shellCommandJobs.failJob(job.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
const refreshed = await this.shellCommandJobs.readJob(job.id);
|
|
4708
|
+
this.monitorShellCommandJob(refreshed);
|
|
4613
4709
|
}
|
|
4614
4710
|
}
|
|
4615
4711
|
|
|
@@ -4628,14 +4724,26 @@ export class ProxySessionController {
|
|
|
4628
4724
|
contextKey: string,
|
|
4629
4725
|
event: ChatUpdatedEvent,
|
|
4630
4726
|
): Promise<void> {
|
|
4631
|
-
const parsed = parseAgentGroupName(event.afterName);
|
|
4632
|
-
if (!parsed) return;
|
|
4633
4727
|
const context = this.store.getUserContext(contextKey);
|
|
4634
4728
|
if (!context?.currentSessionId) return;
|
|
4635
4729
|
const record = this.store.getSessionForContext(context.currentSessionId, contextKey);
|
|
4636
4730
|
if (!record || record.status === "closed") return;
|
|
4637
|
-
|
|
4638
|
-
const
|
|
4731
|
+
const nameFormat = this.config.feishu?.groupNameFormat ?? DEFAULT_GROUP_NAME_FORMAT;
|
|
4732
|
+
const projectCwd = detectProjectlessWorkspace(record.cwd) ? undefined : record.cwd;
|
|
4733
|
+
const templateTitle = parseTaskNameFromGroupName({
|
|
4734
|
+
agentName: record.agentName,
|
|
4735
|
+
groupName: event.afterName,
|
|
4736
|
+
projectCwd,
|
|
4737
|
+
format: nameFormat,
|
|
4738
|
+
});
|
|
4739
|
+
const legacy = parseAgentGroupName(event.afterName);
|
|
4740
|
+
const legacyTitle = legacy && record.agentName.toLowerCase() === legacy.agentName.toLowerCase()
|
|
4741
|
+
? legacy.title
|
|
4742
|
+
: undefined;
|
|
4743
|
+
if (isDefaultGroupNameFormat(nameFormat) && isLegacyGroupPrefixOnly(event.afterName)) return;
|
|
4744
|
+
const title = normalizeTaskTitle(isDefaultGroupNameFormat(nameFormat)
|
|
4745
|
+
? legacyTitle ?? templateTitle
|
|
4746
|
+
: templateTitle ?? legacyTitle);
|
|
4639
4747
|
if (!title || title === record.title) return;
|
|
4640
4748
|
|
|
4641
4749
|
const loaded = await this.loadSession(record);
|
|
@@ -4990,6 +5098,13 @@ export class ProxySessionController {
|
|
|
4990
5098
|
if (!stats.isFile()) throw new Error(`这不是普通文件:${filePath}`);
|
|
4991
5099
|
}
|
|
4992
5100
|
|
|
5101
|
+
private async sendCurrentTaskFile(contextKey: string, requestedFilePath: string): Promise<void> {
|
|
5102
|
+
const record = this.requireCurrentSession(contextKey);
|
|
5103
|
+
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
5104
|
+
await this.assertSendableFile(filePath);
|
|
5105
|
+
await this.outbound.sendFile(contextKey, filePath);
|
|
5106
|
+
}
|
|
5107
|
+
|
|
4993
5108
|
private async listSessions(
|
|
4994
5109
|
contextKey: string,
|
|
4995
5110
|
searchTerm?: string,
|
|
@@ -6505,14 +6620,29 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
6505
6620
|
}
|
|
6506
6621
|
|
|
6507
6622
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
6508
|
-
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
6623
|
+
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
6509
6624
|
if (command.type === "agent") return command.agent === undefined;
|
|
6510
6625
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
6511
6626
|
return false;
|
|
6512
6627
|
}
|
|
6513
6628
|
|
|
6629
|
+
function shellCommandJobCardView(job: ShellCommandJobSnapshot): ShellCommandCardView {
|
|
6630
|
+
const status = job.status === "starting" ? "running" : job.status;
|
|
6631
|
+
return {
|
|
6632
|
+
jobId: job.id,
|
|
6633
|
+
contextKey: job.contextKey,
|
|
6634
|
+
command: job.command,
|
|
6635
|
+
cwd: job.cwd,
|
|
6636
|
+
output: [job.output.trimEnd(), job.error].filter(Boolean).join("\n"),
|
|
6637
|
+
status,
|
|
6638
|
+
exitCode: job.exitCode,
|
|
6639
|
+
elapsedMs: Math.max(0, (job.completedAt ?? Date.now()) - job.startedAt),
|
|
6640
|
+
outputTruncated: job.outputTruncated,
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
6643
|
+
|
|
6514
6644
|
function commandRequiresCurrentSession(command: Command): boolean {
|
|
6515
|
-
if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
6645
|
+
if (["dismiss", "stop", "title", "turns", "file", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
6516
6646
|
return true;
|
|
6517
6647
|
}
|
|
6518
6648
|
if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
|
|
@@ -6696,8 +6826,8 @@ async function listWindowsDriveRoots(): Promise<WindowsDriveInfo[]> {
|
|
|
6696
6826
|
return drives;
|
|
6697
6827
|
}
|
|
6698
6828
|
|
|
6699
|
-
function
|
|
6700
|
-
return command.type === "prompt" || command.type === "nosteer";
|
|
6829
|
+
function commandDefersReactionFinalization(command: Command): boolean {
|
|
6830
|
+
return command.type === "prompt" || command.type === "nosteer" || command.type === "shell";
|
|
6701
6831
|
}
|
|
6702
6832
|
|
|
6703
6833
|
function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt {
|
|
@@ -6871,44 +7001,6 @@ function isRecoveryAttemptRecent(attempt: TurnAttemptRecord, cutoff: number): bo
|
|
|
6871
7001
|
return Number.isFinite(updatedAt) && updatedAt >= cutoff;
|
|
6872
7002
|
}
|
|
6873
7003
|
|
|
6874
|
-
function formatGroupDateSuffix(date: Date): string {
|
|
6875
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
6876
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
6877
|
-
return `${month}-${day}`;
|
|
6878
|
-
}
|
|
6879
|
-
|
|
6880
|
-
function formatNewGroupName(
|
|
6881
|
-
agentName: string,
|
|
6882
|
-
projectCwd: string | undefined,
|
|
6883
|
-
taskTitle: string,
|
|
6884
|
-
date: Date,
|
|
6885
|
-
includeTimestamp: boolean,
|
|
6886
|
-
): string {
|
|
6887
|
-
const prefix = `[${agentName}] `;
|
|
6888
|
-
const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
|
|
6889
|
-
const titlePrefix = `${prefix}${projectPrefix}`;
|
|
6890
|
-
const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
|
|
6891
|
-
const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
|
|
6892
|
-
- Array.from(titlePrefix).length
|
|
6893
|
-
- Array.from(timestampSuffix).length;
|
|
6894
|
-
const title = truncateTail(taskTitle, availableTitleLength);
|
|
6895
|
-
return truncateTail(`${titlePrefix}${title}${timestampSuffix}`, FEISHU_GROUP_NAME_MAX_LENGTH);
|
|
6896
|
-
}
|
|
6897
|
-
|
|
6898
|
-
const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
|
|
6899
|
-
const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
|
|
6900
|
-
|
|
6901
|
-
function formatGroupProjectDirectory(projectCwd: string): string {
|
|
6902
|
-
const value = abbreviateHomeDirectory(projectCwd);
|
|
6903
|
-
const levels = value
|
|
6904
|
-
.replace(/[\\/]+$/, "")
|
|
6905
|
-
.split(/[\\/]+/)
|
|
6906
|
-
.filter(Boolean)
|
|
6907
|
-
.slice(-2);
|
|
6908
|
-
const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
|
|
6909
|
-
return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
|
|
6910
|
-
}
|
|
6911
|
-
|
|
6912
7004
|
function abbreviateHomeDirectory(value: string): string {
|
|
6913
7005
|
const homeDirectory = os.homedir();
|
|
6914
7006
|
const valueIsWindows = isWindowsAbsolutePath(value);
|
|
@@ -6925,29 +7017,6 @@ function isWindowsAbsolutePath(value: string): boolean {
|
|
|
6925
7017
|
return /^[a-z]:[\\/]|^\\\\/iu.test(value);
|
|
6926
7018
|
}
|
|
6927
7019
|
|
|
6928
|
-
function fitTrailingPathLevels(levels: string[], maxLength: number, separator: string): string {
|
|
6929
|
-
if (levels.length === 0) return "";
|
|
6930
|
-
if (levels.length === 1) return truncatePathLevel(levels[0]!, maxLength);
|
|
6931
|
-
const parent = levels[0]!;
|
|
6932
|
-
const leaf = levels[1]!;
|
|
6933
|
-
const joined = `${parent}${separator}${leaf}`;
|
|
6934
|
-
if (Array.from(joined).length <= maxLength) return joined;
|
|
6935
|
-
if (Array.from(leaf).length <= maxLength) return leaf;
|
|
6936
|
-
return truncatePathLevel(leaf, maxLength);
|
|
6937
|
-
}
|
|
6938
|
-
|
|
6939
|
-
function truncatePathLevel(value: string, maxLength: number): string {
|
|
6940
|
-
return truncateTail(value, maxLength);
|
|
6941
|
-
}
|
|
6942
|
-
|
|
6943
|
-
function truncateTail(value: string, maxLength: number): string {
|
|
6944
|
-
const characters = Array.from(value);
|
|
6945
|
-
if (characters.length <= maxLength) return value;
|
|
6946
|
-
if (maxLength <= 0) return "";
|
|
6947
|
-
if (maxLength <= 3) return ".".repeat(maxLength);
|
|
6948
|
-
return `${characters.slice(0, maxLength - 3).join("")}...`;
|
|
6949
|
-
}
|
|
6950
|
-
|
|
6951
7020
|
function parseAgentGroupName(value: string): { agentName: string; title: string } | undefined {
|
|
6952
7021
|
const match = /^\[([^[\]]+)\]\s+(.+)$/.exec(value.trim());
|
|
6953
7022
|
const agentName = match?.[1]?.trim();
|
|
@@ -6958,6 +7027,15 @@ function parseAgentGroupName(value: string): { agentName: string; title: string
|
|
|
6958
7027
|
return agentName && title ? { agentName, title } : undefined;
|
|
6959
7028
|
}
|
|
6960
7029
|
|
|
7030
|
+
function isDefaultGroupNameFormat(format: AppConfig["feishu"]["groupNameFormat"]): boolean {
|
|
7031
|
+
return format.project === DEFAULT_GROUP_NAME_FORMAT.project
|
|
7032
|
+
&& format.projectless === DEFAULT_GROUP_NAME_FORMAT.projectless;
|
|
7033
|
+
}
|
|
7034
|
+
|
|
7035
|
+
function isLegacyGroupPrefixOnly(value: string): boolean {
|
|
7036
|
+
return /^\[[^[\]]+\]\s+\[[^[\]]+\]\s*$/u.test(value.trim());
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6961
7039
|
function validateGoalObjective(objective: string): void {
|
|
6962
7040
|
const length = Array.from(objective.trim()).length;
|
|
6963
7041
|
if (length === 0) throw new Error("Goal 不能为空。");
|