@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.
@@ -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
- stdout: string;
246
- stderr: string;
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 stdout = normalizeTerminalOutput(view.stdout).trimEnd();
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(displayCommand, 600);
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 = "> Powered by [AgentBot](https://github.com/keyou/agent-bot)";
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 SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
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: "&#60;文件路径&#62;",
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 (!isPromptCommand(command)) await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
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 (!isPromptCommand(command)) await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
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(contextKey: string, command: string): Promise<void> {
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 startedAt = Date.now();
4516
- let latestOutput: ShellCommandOutputSnapshot = {
4517
- stdout: "",
4518
- stderr: "",
4519
- outputTruncated: false,
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
- ...latestOutput,
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
- result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
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
- if (elapsedTimer) clearInterval(elapsedTimer);
4564
- const failedCard = this.cardRenderer.renderShellCommandCard({
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 scheduler.flush({
4575
- ...runningState,
4576
- stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4577
- status: "failed",
4578
- elapsedMs: Date.now() - startedAt,
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.outbound.sendInteractiveCard(contextKey, failedCard);
4594
+ await this.shellCommandJobs.markPresented(created.id).catch(() => undefined);
4590
4595
  }
4591
4596
  throw error;
4592
4597
  }
4598
+ }
4593
4599
 
4594
- if (elapsedTimer) clearInterval(elapsedTimer);
4595
- const finalState: ShellCommandCardView = {
4596
- command,
4597
- cwd,
4598
- stdout: result.stdout,
4599
- stderr: result.stderr,
4600
- status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
4601
- exitCode: result.exitCode,
4602
- elapsedMs: Date.now() - startedAt,
4603
- outputTruncated: result.outputTruncated,
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
- if (scheduler) {
4606
- try {
4607
- await scheduler.flush(finalState);
4608
- } finally {
4609
- scheduler.dispose();
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
- } else {
4612
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
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 isPromptCommand(command: Command): boolean {
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 {