@keyou007/agent-bot 0.1.15-alpha.0 → 0.1.15-alpha.2

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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +23 -20
  3. package/README.zh.md +23 -20
  4. package/config.example.yaml +1 -0
  5. package/dist/cli/FeishuAppConfiguration.js +42 -3
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +114 -16
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuConnector.js +21 -6
  12. package/dist/feishu/FeishuConnector.js.map +1 -1
  13. package/dist/feishu/FeishuMarkdown.js +58 -6
  14. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  15. package/dist/feishu/FeishuMessageClient.js +33 -4
  16. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  17. package/dist/feishu/FeishuTurnPresenter.js +7 -6
  18. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  19. package/dist/feishu/FinalResponseBranding.js +15 -0
  20. package/dist/feishu/FinalResponseBranding.js.map +1 -0
  21. package/dist/logging/DailyLogStream.js +73 -0
  22. package/dist/logging/DailyLogStream.js.map +1 -0
  23. package/dist/logging/logger.js +2 -1
  24. package/dist/logging/logger.js.map +1 -1
  25. package/dist/proxy/ProxySessionController.js +270 -40
  26. package/dist/proxy/ProxySessionController.js.map +1 -1
  27. package/dist/state/StateStore.js +126 -0
  28. package/dist/state/StateStore.js.map +1 -1
  29. package/dist/state/migrations.js +11 -0
  30. package/dist/state/migrations.js.map +1 -1
  31. package/dist/supervision/SupervisorDiagnostics.js +37 -21
  32. package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
  33. package/dist/supervisor.js +11 -4
  34. package/dist/supervisor.js.map +1 -1
  35. package/dist/utils/executeShellCommand.js +83 -16
  36. package/dist/utils/executeShellCommand.js.map +1 -1
  37. package/docs/technical-reference.md +9 -8
  38. package/docs/technical-reference.zh.md +9 -8
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
  41. package/src/cli/FeishuAppConfiguration.ts +76 -3
  42. package/src/cli.ts +6 -1
  43. package/src/feishu/CardRenderer.ts +150 -17
  44. package/src/feishu/FeishuConnector.ts +32 -7
  45. package/src/feishu/FeishuMarkdown.ts +64 -5
  46. package/src/feishu/FeishuMessageClient.ts +41 -12
  47. package/src/feishu/FeishuTurnPresenter.ts +9 -6
  48. package/src/feishu/FinalResponseBranding.ts +18 -0
  49. package/src/logging/DailyLogStream.ts +88 -0
  50. package/src/logging/logger.ts +2 -1
  51. package/src/proxy/ProxySessionController.ts +292 -39
  52. package/src/state/StateStore.ts +180 -0
  53. package/src/state/migrations.ts +11 -0
  54. package/src/supervision/SupervisorDiagnostics.ts +41 -25
  55. package/src/supervisor.ts +10 -4
  56. package/src/utils/executeShellCommand.ts +103 -15
@@ -6,12 +6,13 @@ import { resolveUserPath } from "../config/paths.js";
6
6
  import { CommandRouter } from "../commands/CommandRouter.js";
7
7
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
8
8
  import { CardRenderer, } from "../feishu/CardRenderer.js";
9
+ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
9
10
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
10
11
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
11
12
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
12
13
  import { buildTurnGraphRows } from "../presentation/turnGraph.js";
13
14
  import { createId } from "../utils/id.js";
14
- import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
15
+ import { truncateMiddle, truncateText } from "../utils/markdown.js";
15
16
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
16
17
  import { executeShellCommand, } from "../utils/executeShellCommand.js";
17
18
  const MESSAGE_RECEIVED_REACTION = "OnIt";
@@ -24,6 +25,7 @@ const RESET_HISTORY_PAGE_SIZE = 10;
24
25
  const MAX_LLM_TURN_RETRIES = 3;
25
26
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
26
27
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
28
+ const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
27
29
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
28
30
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
29
31
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -417,7 +419,7 @@ export class ProxySessionController {
417
419
  if (isQueueIndependentCommand(command)) {
418
420
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
419
421
  try {
420
- if (command.type !== "forkgroup" && command.type !== "dismiss")
422
+ if (command.type === "prompt")
421
423
  await this.ensureThreadFork(message);
422
424
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
423
425
  if (!isPromptCommand(command))
@@ -433,7 +435,7 @@ export class ProxySessionController {
433
435
  const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
434
436
  const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
435
437
  try {
436
- if (command.type !== "forkgroup")
438
+ if (command.type === "prompt")
437
439
  await this.ensureThreadFork(message);
438
440
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
439
441
  if (!isPromptCommand(command))
@@ -524,6 +526,25 @@ export class ProxySessionController {
524
526
  await this.assertSendableFile(filePath);
525
527
  await this.outbound.sendFile(contextKey, filePath);
526
528
  }
529
+ else if (kind === "directory_new_folder_prompt") {
530
+ const directory = directoryActionPath(scopedAction.value.directory);
531
+ await this.assertBrowsableDirectory(directory);
532
+ await this.outbound.updateInteractiveCard(contextKey, requiredCardMessageId(scopedAction.messageId), this.cardRenderer.renderDirectoryNewFolderCard({
533
+ directory,
534
+ displayDirectory: abbreviateHomeDirectory(directory),
535
+ contextKey,
536
+ page: directoryPageValue(scopedAction.value.page),
537
+ }));
538
+ }
539
+ else if (kind === "directory_new_folder_submit") {
540
+ await this.createDirectoryFromCard(contextKey, scopedAction);
541
+ }
542
+ else if (kind === "directory_new_folder_cancel") {
543
+ await this.openDirectoryBrowser(contextKey, directoryActionPath(scopedAction.value.directory), {
544
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
545
+ page: directoryPageValue(scopedAction.value.page),
546
+ });
547
+ }
527
548
  else if (kind === "directory_new" || kind === "directory_new_group") {
528
549
  const directory = directoryActionPath(scopedAction.value.directory);
529
550
  await this.assertBrowsableDirectory(directory);
@@ -1095,8 +1116,7 @@ export class ProxySessionController {
1095
1116
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1096
1117
  throw new Error("The task is not an App Server task that can be reset.");
1097
1118
  }
1098
- const contextKey = this.controlSessionContextKey(session);
1099
- const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
1119
+ const rows = this.store.listTaskTurnGraph(session.localSessionId);
1100
1120
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1101
1121
  return {
1102
1122
  session,
@@ -1219,6 +1239,9 @@ export class ProxySessionController {
1219
1239
  }
1220
1240
  async execute(contextKey, command, messageId, replyTarget, localImagePaths, userId, incomingMessage) {
1221
1241
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1242
+ if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
1243
+ throw new Error(unboundThreadTaskMessage());
1244
+ }
1222
1245
  switch (command.type) {
1223
1246
  case "shell":
1224
1247
  await this.runShellCommand(contextKey, command.command);
@@ -2501,8 +2524,13 @@ export class ProxySessionController {
2501
2524
  if (existing)
2502
2525
  return { record, runtime, session: existing };
2503
2526
  const pending = this.sessionLoads.get(record.localSessionId);
2504
- if (pending)
2505
- return pending;
2527
+ if (pending) {
2528
+ const loaded = await pending;
2529
+ return {
2530
+ ...loaded,
2531
+ record: { ...loaded.record, contextKey: record.contextKey },
2532
+ };
2533
+ }
2506
2534
  this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
2507
2535
  const loading = (async () => {
2508
2536
  if (record.lastTurnId) {
@@ -2566,7 +2594,11 @@ export class ProxySessionController {
2566
2594
  }
2567
2595
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
2568
2596
  const saved = this.store.getSession(record.localSessionId) ?? record;
2569
- return { record: saved, runtime, session };
2597
+ return {
2598
+ record: { ...saved, contextKey: record.contextKey },
2599
+ runtime,
2600
+ session,
2601
+ };
2570
2602
  })();
2571
2603
  this.sessionLoads.set(record.localSessionId, loading);
2572
2604
  try {
@@ -2653,6 +2685,7 @@ export class ProxySessionController {
2653
2685
  presentationError = error;
2654
2686
  }
2655
2687
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
2688
+ await this.refreshGoalCards(event.sessionId);
2656
2689
  const terminalStatus = event.type === "turn_completed"
2657
2690
  ? "completed"
2658
2691
  : event.type === "turn_cancelled"
@@ -2994,8 +3027,10 @@ export class ProxySessionController {
2994
3027
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
2995
3028
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2996
3029
  }
2997
- const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
2998
- if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
3030
+ const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3031
+ .find((turn) => turn.turnId === turnId);
3032
+ const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3033
+ if (!snapshot || snapshot.status !== "completed") {
2999
3034
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3000
3035
  }
3001
3036
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
@@ -3015,7 +3050,7 @@ export class ProxySessionController {
3015
3050
  throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
3016
3051
  }
3017
3052
  const origin = this.store.getTurnRuntimeOrigin(turnId);
3018
- if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
3053
+ if (origin && origin.agentName !== current.agentName) {
3019
3054
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
3020
3055
  }
3021
3056
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -3383,6 +3418,21 @@ export class ProxySessionController {
3383
3418
  await this.sendGoalCard(contextKey, goal, command.action === "edit" ? "Goal 已更新。" : "Goal 已启动,Agent 会持续执行直到完成、暂停或遇到阻塞。", record);
3384
3419
  }
3385
3420
  async sendGoalCard(contextKey, goal, notice, record = this.currentSession(contextKey)) {
3421
+ const card = this.renderGoalCard(goal, notice, record);
3422
+ if (!record) {
3423
+ await this.outbound.sendInteractiveCard(contextKey, card);
3424
+ return;
3425
+ }
3426
+ const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
3427
+ if (existing) {
3428
+ await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
3429
+ return;
3430
+ }
3431
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
3432
+ if (messageId)
3433
+ this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
3434
+ }
3435
+ renderGoalCard(goal, notice, record) {
3386
3436
  const sections = [];
3387
3437
  if (notice)
3388
3438
  sections.push({ lines: [cardText(notice)] });
@@ -3405,23 +3455,133 @@ export class ProxySessionController {
3405
3455
  "**/goal edit <新目标>** 修改  **/goal clear** 清除",
3406
3456
  ],
3407
3457
  });
3408
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Goal", sections));
3458
+ const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
3459
+ return this.cardRenderer.renderSectionsCard(title, sections);
3460
+ }
3461
+ async refreshGoalCards(localSessionId) {
3462
+ const deliveries = this.store.listGoalCardDeliveries(localSessionId);
3463
+ if (deliveries.length === 0)
3464
+ return;
3465
+ const record = this.store.getSession(localSessionId);
3466
+ if (!record)
3467
+ return;
3468
+ const runtime = this.runtimes.forAgent(record.agentName);
3469
+ if (runtime.kind !== "codex" || !runtime.getGoal)
3470
+ return;
3471
+ try {
3472
+ const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
3473
+ const goal = await runtime.getGoal(loaded.localSessionId);
3474
+ const card = this.renderGoalCard(goal, undefined, record);
3475
+ await Promise.all(deliveries.map(async (delivery) => {
3476
+ try {
3477
+ await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
3478
+ }
3479
+ catch (error) {
3480
+ this.logger.warn({ error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId }, "Failed to refresh an Agent Goal card.");
3481
+ }
3482
+ }));
3483
+ }
3484
+ catch (error) {
3485
+ this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
3486
+ }
3409
3487
  }
3410
3488
  async runShellCommand(contextKey, command) {
3411
3489
  const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
3412
- const result = await this.shellCommandExecutor(command, cwd);
3413
- const outputParts = [
3414
- result.stdout.trimEnd(),
3415
- result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
3416
- ].filter(Boolean);
3417
- const output = truncateMiddle(outputParts.join("\n"), 6_000);
3418
- const status = result.timedOut
3419
- ? "已超时(120s)"
3420
- : `退出码 ${result.exitCode ?? "未知"}`;
3421
- await this.outbound.sendMarkdown(contextKey, [
3422
- codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
3423
- `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
3424
- ].join("\n"));
3490
+ const startedAt = Date.now();
3491
+ let latestOutput = {
3492
+ stdout: "",
3493
+ stderr: "",
3494
+ outputTruncated: false,
3495
+ };
3496
+ let runningState = {
3497
+ command,
3498
+ cwd,
3499
+ ...latestOutput,
3500
+ status: "running",
3501
+ elapsedMs: 0,
3502
+ };
3503
+ const cardMessageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(runningState));
3504
+ const scheduler = cardMessageId
3505
+ ? new CardUpdateScheduler({
3506
+ render: (state) => this.cardRenderer.renderShellCommandCard(state),
3507
+ write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
3508
+ onError: (error) => this.logger.warn({ error, contextKey, messageId: cardMessageId }, "Failed to update a shell command card."),
3509
+ })
3510
+ : undefined;
3511
+ scheduler?.seed(runningState);
3512
+ const elapsedTimer = scheduler ? setInterval(() => {
3513
+ runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
3514
+ scheduler.update(runningState);
3515
+ }, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
3516
+ elapsedTimer?.unref?.();
3517
+ const updateRunningOutput = (snapshot) => {
3518
+ latestOutput = snapshot;
3519
+ runningState = {
3520
+ ...runningState,
3521
+ ...snapshot,
3522
+ elapsedMs: Date.now() - startedAt,
3523
+ };
3524
+ scheduler?.update(runningState);
3525
+ };
3526
+ let result;
3527
+ try {
3528
+ result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
3529
+ }
3530
+ catch (error) {
3531
+ if (elapsedTimer)
3532
+ clearInterval(elapsedTimer);
3533
+ const failedCard = this.cardRenderer.renderShellCommandCard({
3534
+ command,
3535
+ cwd,
3536
+ ...latestOutput,
3537
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
3538
+ status: "failed",
3539
+ elapsedMs: Date.now() - startedAt,
3540
+ });
3541
+ if (scheduler) {
3542
+ try {
3543
+ await scheduler.flush({
3544
+ ...runningState,
3545
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
3546
+ status: "failed",
3547
+ elapsedMs: Date.now() - startedAt,
3548
+ });
3549
+ }
3550
+ catch (cardError) {
3551
+ this.logger.warn({ error: cardError, contextKey, messageId: cardMessageId }, "Failed to finalize a shell command card.");
3552
+ }
3553
+ finally {
3554
+ scheduler.dispose();
3555
+ }
3556
+ }
3557
+ else {
3558
+ await this.outbound.sendInteractiveCard(contextKey, failedCard);
3559
+ }
3560
+ throw error;
3561
+ }
3562
+ if (elapsedTimer)
3563
+ clearInterval(elapsedTimer);
3564
+ const finalState = {
3565
+ command,
3566
+ cwd,
3567
+ stdout: result.stdout,
3568
+ stderr: result.stderr,
3569
+ status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
3570
+ exitCode: result.exitCode,
3571
+ elapsedMs: Date.now() - startedAt,
3572
+ outputTruncated: result.outputTruncated,
3573
+ };
3574
+ if (scheduler) {
3575
+ try {
3576
+ await scheduler.flush(finalState);
3577
+ }
3578
+ finally {
3579
+ scheduler.dispose();
3580
+ }
3581
+ }
3582
+ else {
3583
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
3584
+ }
3425
3585
  }
3426
3586
  async setTitle(contextKey, title) {
3427
3587
  const normalizedTitle = normalizeTaskTitle(title);
@@ -3532,7 +3692,7 @@ export class ProxySessionController {
3532
3692
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
3533
3693
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3534
3694
  }
3535
- const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
3695
+ const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
3536
3696
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
3537
3697
  turnId: turn.turnId,
3538
3698
  parentTurnId: turn.parentTurnId,
@@ -3668,7 +3828,6 @@ export class ProxySessionController {
3668
3828
  contextKey,
3669
3829
  },
3670
3830
  },
3671
- actions: directoryCreationActions(entry.path, contextKey),
3672
3831
  }
3673
3832
  : {
3674
3833
  name: entry.name,
@@ -3688,12 +3847,12 @@ export class ProxySessionController {
3688
3847
  const card = this.cardRenderer.renderDirectoryBrowserCard({
3689
3848
  directory: abbreviateHomeDirectory(directory),
3690
3849
  entries: visibleEntries,
3691
- currentActions: directoryCreationActions(directory, contextKey),
3850
+ currentActions: directoryCreationActions(directory, contextKey, page),
3692
3851
  navigationActions,
3693
3852
  footerLines: [
3694
3853
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
3695
3854
  "",
3696
- "> 点击目录名称进入,点击文件名称发送到当前会话;**New** 在该目录创建任务,**NewGroup** 在该目录创建新群和任务。",
3855
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部可新建目录、任务或群聊。",
3697
3856
  ],
3698
3857
  });
3699
3858
  if (options.updateMessageId) {
@@ -3722,7 +3881,6 @@ export class ProxySessionController {
3722
3881
  contextKey,
3723
3882
  },
3724
3883
  },
3725
- actions: directoryCreationActions(drive.root, contextKey),
3726
3884
  }));
3727
3885
  const card = this.cardRenderer.renderDirectoryBrowserCard({
3728
3886
  directory: "此电脑",
@@ -3732,7 +3890,7 @@ export class ProxySessionController {
3732
3890
  footerLines: [
3733
3891
  `第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
3734
3892
  "",
3735
- "> 点击磁盘名称进入;**New** 在磁盘根目录创建任务,**NewGroup** 在磁盘根目录创建新群和任务。",
3893
+ "> 点击磁盘名称进入。",
3736
3894
  ],
3737
3895
  });
3738
3896
  if (options.updateMessageId) {
@@ -3753,6 +3911,28 @@ export class ProxySessionController {
3753
3911
  if (!stats.isDirectory())
3754
3912
  throw new Error(`这不是目录:${directory}`);
3755
3913
  }
3914
+ async createDirectoryFromCard(contextKey, action) {
3915
+ const directory = directoryActionPath(action.value.directory);
3916
+ await this.assertBrowsableDirectory(directory);
3917
+ const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
3918
+ const newDirectory = path.join(directory, folderName);
3919
+ try {
3920
+ await fs.mkdir(newDirectory);
3921
+ }
3922
+ catch (error) {
3923
+ const code = typeof error === "object" && error !== null && "code" in error
3924
+ ? String(error.code)
3925
+ : undefined;
3926
+ if (code === "EEXIST")
3927
+ throw new Error(`目录已存在:${newDirectory}`);
3928
+ throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
3929
+ }
3930
+ await this.openDirectoryBrowser(contextKey, directory, {
3931
+ updateMessageId: requiredCardMessageId(action.messageId),
3932
+ page: directoryPageValue(action.value.page),
3933
+ });
3934
+ await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
3935
+ }
3756
3936
  async assertSendableFile(filePath) {
3757
3937
  let stats;
3758
3938
  try {
@@ -4350,7 +4530,9 @@ export class ProxySessionController {
4350
4530
  let activeTurnId;
4351
4531
  let queued = 0;
4352
4532
  if (!current) {
4353
- taskLines.push("无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
4533
+ taskLines.push(isThreadContextKey(contextKey)
4534
+ ? unboundThreadTaskMessage()
4535
+ : "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
4354
4536
  }
4355
4537
  else {
4356
4538
  const agent = this.ensureAgent(current.agentName);
@@ -4581,8 +4763,11 @@ export class ProxySessionController {
4581
4763
  }
4582
4764
  requireCurrentSession(contextKey) {
4583
4765
  const record = this.currentSession(contextKey);
4584
- if (!record)
4585
- throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
4766
+ if (!record) {
4767
+ throw new Error(isThreadContextKey(contextKey)
4768
+ ? unboundThreadTaskMessage()
4769
+ : "当前没有任务,直接发送一条消息即可自动创建。");
4770
+ }
4586
4771
  return record;
4587
4772
  }
4588
4773
  requireSession(contextKey, sessionId) {
@@ -5050,6 +5235,19 @@ function isQueueIndependentCommand(command) {
5050
5235
  return true;
5051
5236
  return false;
5052
5237
  }
5238
+ function commandRequiresCurrentSession(command) {
5239
+ if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
5240
+ return true;
5241
+ }
5242
+ if (command.type === "archive" || command.type === "fork")
5243
+ return command.sessionId === undefined;
5244
+ if (command.type === "goal")
5245
+ return command.action !== "set";
5246
+ return false;
5247
+ }
5248
+ function unboundThreadTaskMessage() {
5249
+ return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
5250
+ }
5053
5251
  function parseSessionPage(value) {
5054
5252
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
5055
5253
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
@@ -5085,10 +5283,43 @@ function directoryFileActionPath(value) {
5085
5283
  }
5086
5284
  return path.resolve(value);
5087
5285
  }
5088
- function directoryCreationActions(directory, contextKey) {
5286
+ function cardFormValue(value, name) {
5287
+ const formValue = value.formValue;
5288
+ return typeof formValue === "object" && formValue !== null
5289
+ ? formValue[name]
5290
+ : undefined;
5291
+ }
5292
+ function directoryFolderName(value) {
5293
+ if (typeof value !== "string" || !value.trim())
5294
+ throw new Error("请输入目录名。");
5295
+ const name = value.trim();
5296
+ if (name === "." || name === "..")
5297
+ throw new Error("目录名不能是 . 或 ..。");
5298
+ if (name.length > 255)
5299
+ throw new Error("目录名不能超过 255 个字符。");
5300
+ if (/[<>:"/\\|?*\u0000-\u001F]/u.test(name)) {
5301
+ throw new Error("目录名不能包含路径分隔符或以下字符:< > : \" / \\ | ? *");
5302
+ }
5303
+ if (/[. ]$/u.test(name))
5304
+ throw new Error("目录名不能以空格或句点结尾。");
5305
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu.test(name)) {
5306
+ throw new Error("该名称是系统保留名称,请使用其他目录名。");
5307
+ }
5308
+ return name;
5309
+ }
5310
+ function directoryCreationActions(directory, contextKey, page) {
5089
5311
  return [
5090
5312
  {
5091
- text: "New",
5313
+ text: "NewFolder",
5314
+ value: {
5315
+ action: "directory_new_folder_prompt",
5316
+ directory,
5317
+ contextKey,
5318
+ page: String(page),
5319
+ },
5320
+ },
5321
+ {
5322
+ text: "NewTask",
5092
5323
  value: {
5093
5324
  action: "directory_new",
5094
5325
  directory,
@@ -5096,7 +5327,7 @@ function directoryCreationActions(directory, contextKey) {
5096
5327
  },
5097
5328
  },
5098
5329
  {
5099
- text: "NewGroup",
5330
+ text: "NewGroupTask",
5100
5331
  value: {
5101
5332
  action: "directory_new_group",
5102
5333
  directory,
@@ -5348,14 +5579,13 @@ const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
5348
5579
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
5349
5580
  function formatGroupProjectDirectory(projectCwd) {
5350
5581
  const value = abbreviateHomeDirectory(projectCwd);
5351
- if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
5352
- return `[${value}]`;
5353
5582
  const levels = value
5354
5583
  .replace(/[\\/]+$/, "")
5355
5584
  .split(/[\\/]+/)
5356
5585
  .filter(Boolean)
5357
5586
  .slice(-2);
5358
- return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
5587
+ const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
5588
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
5359
5589
  }
5360
5590
  function abbreviateHomeDirectory(value) {
5361
5591
  const homeDirectory = os.homedir();