@keyou007/agent-bot 0.1.15-alpha.4 → 0.1.15-alpha.5
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 +7 -0
- package/README.md +4 -1
- package/README.zh.md +4 -1
- package/dist/commands/CommandRouter.js +7 -0
- package/dist/commands/CommandRouter.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/proxy/ProxySessionController.js +161 -83
- 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 +2 -0
- package/docs/technical-reference.zh.md +2 -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/feishu/CardRenderer.ts +91 -17
- package/src/feishu/FinalResponseBranding.ts +2 -1
- package/src/proxy/ProxySessionController.ts +196 -89
- package/src/shell/ShellCommandJobManager.ts +341 -0
- package/src/shell/shellCommandRunner.ts +163 -0
- package/src/utils/executeShellCommand.ts +1 -1
|
@@ -240,11 +240,12 @@ export interface DismissGroupCardView {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
export interface ShellCommandCardView {
|
|
243
|
+
jobId?: string;
|
|
244
|
+
contextKey?: string;
|
|
243
245
|
command: string;
|
|
244
246
|
cwd: string;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
status: "running" | "completed" | "failed" | "timed_out";
|
|
247
|
+
output: string;
|
|
248
|
+
status: "running" | "cancelling" | "completed" | "failed" | "cancelled" | "timed_out";
|
|
248
249
|
exitCode?: number | null;
|
|
249
250
|
elapsedMs: number;
|
|
250
251
|
outputTruncated: boolean;
|
|
@@ -258,12 +259,7 @@ export class CardRenderer {
|
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
renderShellCommandCard(view: ShellCommandCardView): Record<string, unknown> {
|
|
261
|
-
const
|
|
262
|
-
const stderr = normalizeTerminalOutput(view.stderr).trimEnd();
|
|
263
|
-
const rawOutput = [
|
|
264
|
-
stdout,
|
|
265
|
-
stderr ? `[stderr]\n${stderr}` : "",
|
|
266
|
-
].filter(Boolean).join("\n");
|
|
262
|
+
const rawOutput = normalizeTerminalOutput(view.output).trimEnd();
|
|
267
263
|
const displayOutput = truncateMiddle(
|
|
268
264
|
rawOutput,
|
|
269
265
|
6_000,
|
|
@@ -273,15 +269,32 @@ export class CardRenderer {
|
|
|
273
269
|
const output = displayOutput || (view.status === "running" ? "等待输出..." : "(无输出)");
|
|
274
270
|
const status = shellCommandStatus(view);
|
|
275
271
|
const outputWasTruncated = view.outputTruncated || rawOutput.length > 6_000;
|
|
272
|
+
const elements = [
|
|
273
|
+
markdown(codeBlock(`$ ${command}\n${output}`, 6_700)),
|
|
274
|
+
markdown([
|
|
275
|
+
`${inlineCode(view.cwd)} · ${status.label} · ${formatTurnDuration(view.elapsedMs)}`,
|
|
276
|
+
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
277
|
+
].join("\n")),
|
|
278
|
+
];
|
|
279
|
+
if (view.status === "running" && view.jobId && view.contextKey) {
|
|
280
|
+
elements.push({
|
|
281
|
+
tag: "button",
|
|
282
|
+
text: { tag: "plain_text", content: "Cancel" },
|
|
283
|
+
type: "danger",
|
|
284
|
+
size: "small",
|
|
285
|
+
behaviors: [{
|
|
286
|
+
type: "callback",
|
|
287
|
+
value: {
|
|
288
|
+
action: "shell_command_cancel",
|
|
289
|
+
jobId: view.jobId,
|
|
290
|
+
contextKey: view.contextKey,
|
|
291
|
+
},
|
|
292
|
+
}],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
276
295
|
return sectionCard(
|
|
277
296
|
status.title,
|
|
278
|
-
|
|
279
|
-
markdown(codeBlock(`$ ${command}\n${output}`, 6_700)),
|
|
280
|
-
markdown([
|
|
281
|
-
`${inlineCode(view.cwd)} · ${status.label} · ${formatTurnDuration(view.elapsedMs)}`,
|
|
282
|
-
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
283
|
-
].join("\n")),
|
|
284
|
-
],
|
|
297
|
+
elements,
|
|
285
298
|
status.template,
|
|
286
299
|
);
|
|
287
300
|
}
|
|
@@ -2052,11 +2065,66 @@ function renderToolDetails(tool: ToolState, projectCwd?: string): string {
|
|
|
2052
2065
|
const displayCommand = tool.kind === "command"
|
|
2053
2066
|
? unwrapShellCommand(normalizedCommand) ?? normalizedCommand
|
|
2054
2067
|
: normalizedCommand;
|
|
2055
|
-
const commandText = truncateText(
|
|
2068
|
+
const commandText = truncateText(
|
|
2069
|
+
tool.kind === "command" ? formatShellCommandForDisplay(displayCommand) : displayCommand,
|
|
2070
|
+
600,
|
|
2071
|
+
);
|
|
2056
2072
|
const resultText = result ? truncateToolResult(normalizeTerminalOutput(result).trim()) : undefined;
|
|
2057
2073
|
return codeBlock([`$ ${commandText}`, resultText].filter((part): part is string => part !== undefined).join("\n"), 2_003);
|
|
2058
2074
|
}
|
|
2059
2075
|
|
|
2076
|
+
function formatShellCommandForDisplay(command: string): string {
|
|
2077
|
+
const normalized = command.replace(/\r\n/g, "\n");
|
|
2078
|
+
let result = "";
|
|
2079
|
+
let quote: "'" | "\"" | undefined;
|
|
2080
|
+
|
|
2081
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
2082
|
+
const character = normalized[index] ?? "";
|
|
2083
|
+
|
|
2084
|
+
if (quote) {
|
|
2085
|
+
result += character;
|
|
2086
|
+
if ((character === "\\" || character === "`") && quote === "\"") {
|
|
2087
|
+
if (index + 1 < normalized.length) result += normalized[++index];
|
|
2088
|
+
} else if (character === quote) {
|
|
2089
|
+
quote = undefined;
|
|
2090
|
+
}
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (character === "'" || character === "\"") {
|
|
2095
|
+
quote = character;
|
|
2096
|
+
result += character;
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
if (character === "\\" || character === "`" || character === "^") {
|
|
2101
|
+
result += character;
|
|
2102
|
+
if (index + 1 < normalized.length) result += normalized[++index];
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const operator = readShellDisplayOperator(normalized, index);
|
|
2107
|
+
if (!operator) {
|
|
2108
|
+
result += character;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
result += operator;
|
|
2113
|
+
index += operator.length - 1;
|
|
2114
|
+
while (index + 1 < normalized.length && /\s/.test(normalized[index + 1] ?? "")) index += 1;
|
|
2115
|
+
if (index + 1 < normalized.length) result += " \\\n ";
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return result;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function readShellDisplayOperator(command: string, index: number): string | undefined {
|
|
2122
|
+
for (const operator of ["&&", "||", "|&", ";", "|"]) {
|
|
2123
|
+
if (command.startsWith(operator, index)) return operator;
|
|
2124
|
+
}
|
|
2125
|
+
return undefined;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2060
2128
|
function normalizeTerminalOutput(value: string): string {
|
|
2061
2129
|
const clean = stripAnsi(value).replace(/\r\n/g, "\n");
|
|
2062
2130
|
return clean
|
|
@@ -2167,6 +2235,12 @@ function shellCommandStatus(view: ShellCommandCardView): {
|
|
|
2167
2235
|
if (view.status === "running") {
|
|
2168
2236
|
return { title: "正在执行命令", label: "运行中", template: "blue" };
|
|
2169
2237
|
}
|
|
2238
|
+
if (view.status === "cancelling") {
|
|
2239
|
+
return { title: "正在取消命令", label: "正在取消", template: "orange" };
|
|
2240
|
+
}
|
|
2241
|
+
if (view.status === "cancelled") {
|
|
2242
|
+
return { title: "命令已取消", label: "已取消", template: "grey" };
|
|
2243
|
+
}
|
|
2170
2244
|
if (view.status === "timed_out") {
|
|
2171
2245
|
return { title: "命令执行超时", label: "已超时(120s)", template: "orange" };
|
|
2172
2246
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const FINAL_RESPONSE_BRANDING =
|
|
1
|
+
export const FINAL_RESPONSE_BRANDING =
|
|
2
|
+
"> Powered by [AgentBot](https://bytedance.larkoffice.com/docx/UTukdYBopojSivxRVeUcCXTtnid)";
|
|
2
3
|
export const FINAL_RESPONSE_BRANDING_BLOCK = `----\n\n${FINAL_RESPONSE_BRANDING}`;
|
|
3
4
|
|
|
4
5
|
export function splitFinalResponseBranding(markdown: string): {
|
|
@@ -47,6 +47,12 @@ import type {
|
|
|
47
47
|
RuntimePrompt,
|
|
48
48
|
RuntimeSession,
|
|
49
49
|
} from "../runtime/types.js";
|
|
50
|
+
import {
|
|
51
|
+
isActiveShellCommandJob,
|
|
52
|
+
ShellCommandJobManager,
|
|
53
|
+
type ShellCommandJobManagerLike,
|
|
54
|
+
type ShellCommandJobSnapshot,
|
|
55
|
+
} from "../shell/ShellCommandJobManager.js";
|
|
50
56
|
import {
|
|
51
57
|
StateStore,
|
|
52
58
|
type CardActionBinding,
|
|
@@ -63,7 +69,6 @@ import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
|
63
69
|
import {
|
|
64
70
|
executeShellCommand,
|
|
65
71
|
type ShellCommandOptions,
|
|
66
|
-
type ShellCommandOutputSnapshot,
|
|
67
72
|
type ShellCommandResult,
|
|
68
73
|
} from "../utils/executeShellCommand.js";
|
|
69
74
|
|
|
@@ -94,6 +99,12 @@ interface ForwardAttachmentReservation {
|
|
|
94
99
|
registry: Map<string, PendingForwardAttachment>;
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
interface ShellCommandJobMonitor {
|
|
103
|
+
scheduler: CardUpdateScheduler<ShellCommandCardView>;
|
|
104
|
+
timer: NodeJS.Timeout;
|
|
105
|
+
refreshing: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
98
109
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
99
110
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -105,7 +116,8 @@ const RESET_HISTORY_PAGE_SIZE = 10;
|
|
|
105
116
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
106
117
|
const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
|
|
107
118
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
108
|
-
const
|
|
119
|
+
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
120
|
+
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
109
121
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
110
122
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
111
123
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -150,6 +162,12 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
150
162
|
usage: "[目录]",
|
|
151
163
|
description: "浏览、发送文件,并从选定目录创建任务或群",
|
|
152
164
|
},
|
|
165
|
+
{
|
|
166
|
+
command: "/file",
|
|
167
|
+
usage: "<文件路径>",
|
|
168
|
+
description: "将指定文件发送到当前飞书会话",
|
|
169
|
+
requiresArgument: true,
|
|
170
|
+
},
|
|
153
171
|
{
|
|
154
172
|
command: "/forkgroup",
|
|
155
173
|
usage: "[title]",
|
|
@@ -404,6 +422,7 @@ export class ProxySessionController {
|
|
|
404
422
|
private readonly pendingMergedForwards = new Map<string, PendingForwardAttachment>();
|
|
405
423
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
406
424
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
425
|
+
private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
|
|
407
426
|
private readonly unsubscribe: Array<() => void> = [];
|
|
408
427
|
private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
|
|
409
428
|
private startupRecovery?: Promise<void>;
|
|
@@ -418,6 +437,9 @@ export class ProxySessionController {
|
|
|
418
437
|
private readonly lifecycle?: ProxyLifecycle,
|
|
419
438
|
private readonly shellCommandExecutor: ShellCommandExecutor = executeShellCommand,
|
|
420
439
|
private readonly windowsDriveLister: WindowsDriveLister = listWindowsDriveRoots,
|
|
440
|
+
private readonly shellCommandJobs: ShellCommandJobManagerLike = new ShellCommandJobManager(
|
|
441
|
+
path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
|
|
442
|
+
),
|
|
421
443
|
) {
|
|
422
444
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
423
445
|
this.unsubscribe.push(
|
|
@@ -708,7 +730,9 @@ export class ProxySessionController {
|
|
|
708
730
|
message.userId,
|
|
709
731
|
message,
|
|
710
732
|
);
|
|
711
|
-
if (!
|
|
733
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
734
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
735
|
+
}
|
|
712
736
|
} catch (error) {
|
|
713
737
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
714
738
|
await this.sendError(message.contextKey, error);
|
|
@@ -731,7 +755,9 @@ export class ProxySessionController {
|
|
|
731
755
|
message.userId,
|
|
732
756
|
message,
|
|
733
757
|
);
|
|
734
|
-
if (!
|
|
758
|
+
if (!commandDefersReactionFinalization(command)) {
|
|
759
|
+
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
760
|
+
}
|
|
735
761
|
} catch (error) {
|
|
736
762
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
737
763
|
await this.sendError(message.contextKey, error);
|
|
@@ -786,6 +812,11 @@ export class ProxySessionController {
|
|
|
786
812
|
} else if (kind === "turn_cancel") {
|
|
787
813
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
788
814
|
await this.cancelSession(this.requireSession(contextKey, sessionId));
|
|
815
|
+
} else if (kind === "shell_command_cancel") {
|
|
816
|
+
await this.cancelShellCommandJob(
|
|
817
|
+
contextKey,
|
|
818
|
+
String(scopedAction.value.jobId ?? ""),
|
|
819
|
+
);
|
|
789
820
|
} else if (kind === "turn_reset") {
|
|
790
821
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
791
822
|
await this.resetCurrentSessionToTurn(
|
|
@@ -1057,6 +1088,7 @@ export class ProxySessionController {
|
|
|
1057
1088
|
this.relatedReactionMessageIds.clear();
|
|
1058
1089
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1059
1090
|
this.recoveryRetryTimers.clear();
|
|
1091
|
+
for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
|
|
1060
1092
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
1061
1093
|
}
|
|
1062
1094
|
|
|
@@ -1700,11 +1732,14 @@ export class ProxySessionController {
|
|
|
1700
1732
|
}
|
|
1701
1733
|
switch (command.type) {
|
|
1702
1734
|
case "shell":
|
|
1703
|
-
await this.runShellCommand(contextKey, command.command);
|
|
1735
|
+
await this.runShellCommand(contextKey, command.command, messageId);
|
|
1704
1736
|
return;
|
|
1705
1737
|
case "dir":
|
|
1706
1738
|
await this.openDirectoryBrowser(contextKey, command.directory);
|
|
1707
1739
|
return;
|
|
1740
|
+
case "file":
|
|
1741
|
+
await this.sendCurrentTaskFile(contextKey, command.filePath);
|
|
1742
|
+
return;
|
|
1708
1743
|
case "new":
|
|
1709
1744
|
if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
|
|
1710
1745
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
@@ -1872,6 +1907,7 @@ export class ProxySessionController {
|
|
|
1872
1907
|
}
|
|
1873
1908
|
|
|
1874
1909
|
private async runStartupRecovery(): Promise<void> {
|
|
1910
|
+
await this.recoverShellCommandJobs();
|
|
1875
1911
|
this.backfillTurnAttemptsForUpgrade();
|
|
1876
1912
|
const attempts = this.store.listIncompleteTurnAttempts();
|
|
1877
1913
|
const recoveryCutoff = Date.now() - RECOVERY_ACTIVITY_WINDOW_MS;
|
|
@@ -4510,106 +4546,155 @@ export class ProxySessionController {
|
|
|
4510
4546
|
}
|
|
4511
4547
|
}
|
|
4512
4548
|
|
|
4513
|
-
private async runShellCommand(
|
|
4549
|
+
private async runShellCommand(
|
|
4550
|
+
contextKey: string,
|
|
4551
|
+
command: string,
|
|
4552
|
+
sourceMessageId?: string,
|
|
4553
|
+
): Promise<void> {
|
|
4514
4554
|
const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
|
|
4515
|
-
const
|
|
4516
|
-
let
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
};
|
|
4521
|
-
let runningState: ShellCommandCardView = {
|
|
4555
|
+
const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
|
|
4556
|
+
let cardMessageId: string | undefined;
|
|
4557
|
+
const initialState: ShellCommandCardView = {
|
|
4558
|
+
jobId: created.id,
|
|
4559
|
+
contextKey,
|
|
4522
4560
|
command,
|
|
4523
4561
|
cwd,
|
|
4524
|
-
|
|
4562
|
+
output: "",
|
|
4563
|
+
outputTruncated: false,
|
|
4525
4564
|
status: "running",
|
|
4526
4565
|
elapsedMs: 0,
|
|
4527
4566
|
};
|
|
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
4567
|
try {
|
|
4561
|
-
|
|
4568
|
+
cardMessageId = await this.outbound.sendInteractiveCard(
|
|
4569
|
+
contextKey,
|
|
4570
|
+
this.cardRenderer.renderShellCommandCard(initialState),
|
|
4571
|
+
);
|
|
4572
|
+
if (!cardMessageId) throw new Error("Failed to create the shell command card.");
|
|
4573
|
+
const bound = await this.shellCommandJobs.bindCard(created.id, cardMessageId);
|
|
4574
|
+
await this.shellCommandJobs.startJob(created.id);
|
|
4575
|
+
this.monitorShellCommandJob(bound, initialState);
|
|
4562
4576
|
} 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) {
|
|
4577
|
+
await this.shellCommandJobs.failJob(created.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
4578
|
+
if (cardMessageId) {
|
|
4573
4579
|
try {
|
|
4574
|
-
await
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
+
const failed = await this.shellCommandJobs.readJob(created.id);
|
|
4581
|
+
await this.outbound.updateInteractiveCard(
|
|
4582
|
+
contextKey,
|
|
4583
|
+
cardMessageId,
|
|
4584
|
+
this.cardRenderer.renderShellCommandCard(shellCommandJobCardView(failed)),
|
|
4585
|
+
);
|
|
4586
|
+
await this.shellCommandJobs.markPresented(created.id);
|
|
4580
4587
|
} catch (cardError) {
|
|
4581
4588
|
this.logger.warn(
|
|
4582
|
-
{ error: cardError, contextKey, messageId: cardMessageId },
|
|
4583
|
-
"Failed to finalize a shell command card.",
|
|
4589
|
+
{ error: cardError, jobId: created.id, contextKey, messageId: cardMessageId },
|
|
4590
|
+
"Failed to finalize a shell command card after the runner failed to start.",
|
|
4584
4591
|
);
|
|
4585
|
-
} finally {
|
|
4586
|
-
scheduler.dispose();
|
|
4587
4592
|
}
|
|
4588
4593
|
} else {
|
|
4589
|
-
await this.
|
|
4594
|
+
await this.shellCommandJobs.markPresented(created.id).catch(() => undefined);
|
|
4590
4595
|
}
|
|
4591
4596
|
throw error;
|
|
4592
4597
|
}
|
|
4598
|
+
}
|
|
4593
4599
|
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4600
|
+
private monitorShellCommandJob(
|
|
4601
|
+
job: ShellCommandJobSnapshot,
|
|
4602
|
+
seededState?: ShellCommandCardView,
|
|
4603
|
+
): void {
|
|
4604
|
+
if (!job.cardMessageId || this.shellCommandJobMonitors.has(job.id)) return;
|
|
4605
|
+
const scheduler = new CardUpdateScheduler<ShellCommandCardView>({
|
|
4606
|
+
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
4607
|
+
write: (card) => this.outbound.updateInteractiveCard(job.contextKey, job.cardMessageId!, card),
|
|
4608
|
+
onError: (error) => this.logger.warn(
|
|
4609
|
+
{ error, jobId: job.id, contextKey: job.contextKey, messageId: job.cardMessageId },
|
|
4610
|
+
"Failed to update a background shell command card.",
|
|
4611
|
+
),
|
|
4612
|
+
});
|
|
4613
|
+
if (seededState) scheduler.seed(seededState);
|
|
4614
|
+
const monitor: ShellCommandJobMonitor = {
|
|
4615
|
+
scheduler,
|
|
4616
|
+
refreshing: false,
|
|
4617
|
+
timer: setInterval(() => {
|
|
4618
|
+
void this.refreshShellCommandJob(job.id);
|
|
4619
|
+
}, SHELL_COMMAND_JOB_POLL_INTERVAL_MS),
|
|
4604
4620
|
};
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4621
|
+
monitor.timer.unref?.();
|
|
4622
|
+
this.shellCommandJobMonitors.set(job.id, monitor);
|
|
4623
|
+
void this.refreshShellCommandJob(job.id, seededState === undefined);
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
private async refreshShellCommandJob(jobId: string, critical = false): Promise<void> {
|
|
4627
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
4628
|
+
if (!monitor || monitor.refreshing) return;
|
|
4629
|
+
monitor.refreshing = true;
|
|
4630
|
+
try {
|
|
4631
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
4632
|
+
const view = shellCommandJobCardView(job);
|
|
4633
|
+
if (isActiveShellCommandJob(job.status)) {
|
|
4634
|
+
monitor.scheduler.update(view, critical ? "critical" : "normal");
|
|
4635
|
+
return;
|
|
4610
4636
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
4637
|
+
await monitor.scheduler.flush(view);
|
|
4638
|
+
if (job.sourceMessageId) {
|
|
4639
|
+
await this.finalizeStandaloneMessageReaction(
|
|
4640
|
+
job.sourceMessageId,
|
|
4641
|
+
job.status === "completed" ? "completed" : job.status === "cancelled" ? "cancelled" : "failed",
|
|
4642
|
+
);
|
|
4643
|
+
}
|
|
4644
|
+
await this.shellCommandJobs.markPresented(jobId);
|
|
4645
|
+
this.stopShellCommandJobMonitor(jobId);
|
|
4646
|
+
} catch (error) {
|
|
4647
|
+
this.logger.warn({ error, jobId }, "Failed to refresh a background shell command job.");
|
|
4648
|
+
} finally {
|
|
4649
|
+
const current = this.shellCommandJobMonitors.get(jobId);
|
|
4650
|
+
if (current) current.refreshing = false;
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
private stopShellCommandJobMonitor(jobId: string): void {
|
|
4655
|
+
const monitor = this.shellCommandJobMonitors.get(jobId);
|
|
4656
|
+
if (!monitor) return;
|
|
4657
|
+
clearInterval(monitor.timer);
|
|
4658
|
+
monitor.scheduler.dispose();
|
|
4659
|
+
this.shellCommandJobMonitors.delete(jobId);
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
private async cancelShellCommandJob(contextKey: string, jobId: string): Promise<void> {
|
|
4663
|
+
if (!jobId) throw new Error("命令任务无效,请使用最新的命令卡片。");
|
|
4664
|
+
const job = await this.shellCommandJobs.readJob(jobId);
|
|
4665
|
+
if (job.contextKey !== contextKey) throw new Error("该命令不属于当前会话。");
|
|
4666
|
+
const requested = await this.shellCommandJobs.requestCancellation(jobId);
|
|
4667
|
+
if (!requested) {
|
|
4668
|
+
await this.outbound.sendText(contextKey, "该命令已经结束。");
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
if (!this.shellCommandJobMonitors.has(jobId)) this.monitorShellCommandJob(job);
|
|
4672
|
+
await this.refreshShellCommandJob(jobId, true);
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
private async recoverShellCommandJobs(): Promise<void> {
|
|
4676
|
+
const jobs = await this.shellCommandJobs.listRecoverableJobs();
|
|
4677
|
+
if (jobs.length > 0) {
|
|
4678
|
+
this.logger.warn(
|
|
4679
|
+
{ jobIds: jobs.map((job) => job.id) },
|
|
4680
|
+
"Recovering background shell command jobs after startup.",
|
|
4681
|
+
);
|
|
4682
|
+
}
|
|
4683
|
+
for (const job of jobs) {
|
|
4684
|
+
if (!job.cardMessageId || !this.outbound.canRoute(job.contextKey)) continue;
|
|
4685
|
+
if (
|
|
4686
|
+
job.status === "starting"
|
|
4687
|
+
&& job.runnerPid === undefined
|
|
4688
|
+
&& Date.now() - job.updatedAt >= SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS
|
|
4689
|
+
) {
|
|
4690
|
+
try {
|
|
4691
|
+
await this.shellCommandJobs.startJob(job.id);
|
|
4692
|
+
} catch (error) {
|
|
4693
|
+
await this.shellCommandJobs.failJob(job.id, runtimeErrorMessage(error)).catch(() => undefined);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
const refreshed = await this.shellCommandJobs.readJob(job.id);
|
|
4697
|
+
this.monitorShellCommandJob(refreshed);
|
|
4613
4698
|
}
|
|
4614
4699
|
}
|
|
4615
4700
|
|
|
@@ -4990,6 +5075,13 @@ export class ProxySessionController {
|
|
|
4990
5075
|
if (!stats.isFile()) throw new Error(`这不是普通文件:${filePath}`);
|
|
4991
5076
|
}
|
|
4992
5077
|
|
|
5078
|
+
private async sendCurrentTaskFile(contextKey: string, requestedFilePath: string): Promise<void> {
|
|
5079
|
+
const record = this.requireCurrentSession(contextKey);
|
|
5080
|
+
const filePath = resolveUserPath(requestedFilePath, record.cwd);
|
|
5081
|
+
await this.assertSendableFile(filePath);
|
|
5082
|
+
await this.outbound.sendFile(contextKey, filePath);
|
|
5083
|
+
}
|
|
5084
|
+
|
|
4993
5085
|
private async listSessions(
|
|
4994
5086
|
contextKey: string,
|
|
4995
5087
|
searchTerm?: string,
|
|
@@ -6505,14 +6597,29 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
6505
6597
|
}
|
|
6506
6598
|
|
|
6507
6599
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
6508
|
-
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type)) return true;
|
|
6600
|
+
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
6509
6601
|
if (command.type === "agent") return command.agent === undefined;
|
|
6510
6602
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
6511
6603
|
return false;
|
|
6512
6604
|
}
|
|
6513
6605
|
|
|
6606
|
+
function shellCommandJobCardView(job: ShellCommandJobSnapshot): ShellCommandCardView {
|
|
6607
|
+
const status = job.status === "starting" ? "running" : job.status;
|
|
6608
|
+
return {
|
|
6609
|
+
jobId: job.id,
|
|
6610
|
+
contextKey: job.contextKey,
|
|
6611
|
+
command: job.command,
|
|
6612
|
+
cwd: job.cwd,
|
|
6613
|
+
output: [job.output.trimEnd(), job.error].filter(Boolean).join("\n"),
|
|
6614
|
+
status,
|
|
6615
|
+
exitCode: job.exitCode,
|
|
6616
|
+
elapsedMs: Math.max(0, (job.completedAt ?? Date.now()) - job.startedAt),
|
|
6617
|
+
outputTruncated: job.outputTruncated,
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6514
6621
|
function commandRequiresCurrentSession(command: Command): boolean {
|
|
6515
|
-
if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
6622
|
+
if (["dismiss", "stop", "title", "turns", "file", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
6516
6623
|
return true;
|
|
6517
6624
|
}
|
|
6518
6625
|
if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
|
|
@@ -6696,8 +6803,8 @@ async function listWindowsDriveRoots(): Promise<WindowsDriveInfo[]> {
|
|
|
6696
6803
|
return drives;
|
|
6697
6804
|
}
|
|
6698
6805
|
|
|
6699
|
-
function
|
|
6700
|
-
return command.type === "prompt" || command.type === "nosteer";
|
|
6806
|
+
function commandDefersReactionFinalization(command: Command): boolean {
|
|
6807
|
+
return command.type === "prompt" || command.type === "nosteer" || command.type === "shell";
|
|
6701
6808
|
}
|
|
6702
6809
|
|
|
6703
6810
|
function runtimePrompt(text: string, localImagePaths?: string[]): RuntimePrompt {
|