@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -2
  3. package/README.zh.md +7 -2
  4. package/config.example.yaml +6 -0
  5. package/dist/commands/CommandRouter.js +7 -0
  6. package/dist/commands/CommandRouter.js.map +1 -1
  7. package/dist/config/loadConfig.js +4 -0
  8. package/dist/config/loadConfig.js.map +1 -1
  9. package/dist/config/schema.js +31 -0
  10. package/dist/config/schema.js.map +1 -1
  11. package/dist/feishu/CardRenderer.js +76 -9
  12. package/dist/feishu/CardRenderer.js.map +1 -1
  13. package/dist/feishu/FinalResponseBranding.js +1 -1
  14. package/dist/feishu/FinalResponseBranding.js.map +1 -1
  15. package/dist/feishu/GroupNameFormatter.js +193 -0
  16. package/dist/feishu/GroupNameFormatter.js.map +1 -0
  17. package/dist/proxy/ProxySessionController.js +197 -148
  18. package/dist/proxy/ProxySessionController.js.map +1 -1
  19. package/dist/shell/ShellCommandJobManager.js +264 -0
  20. package/dist/shell/ShellCommandJobManager.js.map +1 -0
  21. package/dist/shell/shellCommandRunner.js +157 -0
  22. package/dist/shell/shellCommandRunner.js.map +1 -0
  23. package/dist/utils/executeShellCommand.js +1 -1
  24. package/dist/utils/executeShellCommand.js.map +1 -1
  25. package/docs/technical-reference.md +19 -0
  26. package/docs/technical-reference.zh.md +19 -0
  27. package/npm-shrinkwrap.json +2 -2
  28. package/package.json +1 -1
  29. package/src/commands/CommandRouter.ts +6 -0
  30. package/src/commands/commandTypes.ts +1 -0
  31. package/src/config/loadConfig.ts +4 -0
  32. package/src/config/schema.ts +33 -0
  33. package/src/feishu/CardRenderer.ts +91 -17
  34. package/src/feishu/FinalResponseBranding.ts +2 -1
  35. package/src/feishu/GroupNameFormatter.ts +222 -0
  36. package/src/proxy/ProxySessionController.ts +238 -160
  37. package/src/shell/ShellCommandJobManager.ts +341 -0
  38. package/src/shell/shellCommandRunner.ts +163 -0
  39. 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 SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
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 (!isPromptCommand(command))
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 (!isPromptCommand(command))
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 ?? `新任务 (${formatGroupDateSuffix(new Date())})`;
2458
- const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup", false);
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", false);
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, includeTimestamp) {
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(agentName, boundProjectCwd, taskTitle, new Date(), includeTimestamp);
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 startedAt = Date.now();
3516
- let latestOutput = {
3517
- stdout: "",
3518
- stderr: "",
3519
- outputTruncated: false,
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
- ...latestOutput,
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
- result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
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
- if (elapsedTimer)
3557
- clearInterval(elapsedTimer);
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 scheduler.flush({
3569
- ...runningState,
3570
- stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
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.outbound.sendInteractiveCard(contextKey, failedCard);
3578
+ await this.shellCommandJobs.markPresented(created.id).catch(() => undefined);
3584
3579
  }
3585
3580
  throw error;
3586
3581
  }
3587
- if (elapsedTimer)
3588
- clearInterval(elapsedTimer);
3589
- const finalState = {
3590
- command,
3591
- cwd,
3592
- stdout: result.stdout,
3593
- stderr: result.stderr,
3594
- status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
3595
- exitCode: result.exitCode,
3596
- elapsedMs: Date.now() - startedAt,
3597
- outputTruncated: result.outputTruncated,
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
- if (scheduler) {
3600
- try {
3601
- await scheduler.flush(finalState);
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
- finally {
3604
- scheduler.dispose();
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
- else {
3608
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
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
- if (record.agentName.toLowerCase() !== parsed.agentName.toLowerCase())
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(parsed.title);
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 isPromptCommand(command) {
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)