@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
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import pino, { type Logger } from "pino";
4
4
  import type { AppConfig } from "../config/schema.js";
5
+ import { DailyLogStream } from "./DailyLogStream.js";
5
6
 
6
7
  export function createLogger(config: AppConfig): Logger {
7
8
  const streams: pino.StreamEntry[] = [
@@ -13,7 +14,7 @@ export function createLogger(config: AppConfig): Logger {
13
14
  if (config.logging.path) {
14
15
  fs.mkdirSync(path.dirname(config.logging.path), { recursive: true });
15
16
  streams.push({
16
- stream: fs.createWriteStream(config.logging.path, { flags: "a" }),
17
+ stream: new DailyLogStream(config.logging.path),
17
18
  });
18
19
  }
19
20
 
@@ -24,8 +24,10 @@ import {
24
24
  type ResetHistoryCardEntry,
25
25
  type SessionTaskCardEntry,
26
26
  type SessionTaskCardGroup,
27
+ type ShellCommandCardView,
27
28
  type TaskListCardAction,
28
29
  } from "../feishu/CardRenderer.js";
30
+ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
29
31
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
30
32
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
31
33
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
@@ -54,10 +56,12 @@ import {
54
56
  type TurnAnchorRecord,
55
57
  } from "../state/StateStore.js";
56
58
  import { createId } from "../utils/id.js";
57
- import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
59
+ import { truncateMiddle, truncateText } from "../utils/markdown.js";
58
60
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
59
61
  import {
60
62
  executeShellCommand,
63
+ type ShellCommandOptions,
64
+ type ShellCommandOutputSnapshot,
61
65
  type ShellCommandResult,
62
66
  } from "../utils/executeShellCommand.js";
63
67
 
@@ -98,6 +102,7 @@ const RESET_HISTORY_PAGE_SIZE = 10;
98
102
  const MAX_LLM_TURN_RETRIES = 3;
99
103
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
100
104
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
105
+ const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
101
106
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
102
107
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
103
108
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -366,7 +371,11 @@ export interface ProxyLifecycle {
366
371
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
367
372
  }
368
373
 
369
- export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
374
+ export type ShellCommandExecutor = (
375
+ command: string,
376
+ cwd: string,
377
+ options?: ShellCommandOptions,
378
+ ) => Promise<ShellCommandResult>;
370
379
  export interface WindowsDriveInfo {
371
380
  root: string;
372
381
  label?: string;
@@ -686,7 +695,7 @@ export class ProxySessionController {
686
695
  if (isQueueIndependentCommand(command)) {
687
696
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
688
697
  try {
689
- if (command.type !== "forkgroup" && command.type !== "dismiss") await this.ensureThreadFork(message);
698
+ if (command.type === "prompt") await this.ensureThreadFork(message);
690
699
  await this.execute(
691
700
  message.contextKey,
692
701
  command,
@@ -709,7 +718,7 @@ export class ProxySessionController {
709
718
  const next = previous.catch(() => undefined).then(() =>
710
719
  this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
711
720
  try {
712
- if (command.type !== "forkgroup") await this.ensureThreadFork(message);
721
+ if (command.type === "prompt") await this.ensureThreadFork(message);
713
722
  await this.execute(
714
723
  message.contextKey,
715
724
  command,
@@ -812,6 +821,30 @@ export class ProxySessionController {
812
821
  const filePath = directoryFileActionPath(scopedAction.value.filePath);
813
822
  await this.assertSendableFile(filePath);
814
823
  await this.outbound.sendFile(contextKey, filePath);
824
+ } else if (kind === "directory_new_folder_prompt") {
825
+ const directory = directoryActionPath(scopedAction.value.directory);
826
+ await this.assertBrowsableDirectory(directory);
827
+ await this.outbound.updateInteractiveCard(
828
+ contextKey,
829
+ requiredCardMessageId(scopedAction.messageId),
830
+ this.cardRenderer.renderDirectoryNewFolderCard({
831
+ directory,
832
+ displayDirectory: abbreviateHomeDirectory(directory),
833
+ contextKey,
834
+ page: directoryPageValue(scopedAction.value.page),
835
+ }),
836
+ );
837
+ } else if (kind === "directory_new_folder_submit") {
838
+ await this.createDirectoryFromCard(contextKey, scopedAction);
839
+ } else if (kind === "directory_new_folder_cancel") {
840
+ await this.openDirectoryBrowser(
841
+ contextKey,
842
+ directoryActionPath(scopedAction.value.directory),
843
+ {
844
+ updateMessageId: requiredCardMessageId(scopedAction.messageId),
845
+ page: directoryPageValue(scopedAction.value.page),
846
+ },
847
+ );
815
848
  } else if (kind === "directory_new" || kind === "directory_new_group") {
816
849
  const directory = directoryActionPath(scopedAction.value.directory);
817
850
  await this.assertBrowsableDirectory(directory);
@@ -1488,8 +1521,7 @@ export class ProxySessionController {
1488
1521
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1489
1522
  throw new Error("The task is not an App Server task that can be reset.");
1490
1523
  }
1491
- const contextKey = this.controlSessionContextKey(session);
1492
- const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
1524
+ const rows = this.store.listTaskTurnGraph(session.localSessionId);
1493
1525
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1494
1526
  return {
1495
1527
  session,
@@ -1641,6 +1673,9 @@ export class ProxySessionController {
1641
1673
  incomingMessage?: IncomingMessage,
1642
1674
  ): Promise<void> {
1643
1675
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
1676
+ if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
1677
+ throw new Error(unboundThreadTaskMessage());
1678
+ }
1644
1679
  switch (command.type) {
1645
1680
  case "shell":
1646
1681
  await this.runShellCommand(contextKey, command.command);
@@ -3308,7 +3343,13 @@ export class ProxySessionController {
3308
3343
  const existing = runtime.getSession(record.localSessionId);
3309
3344
  if (existing) return { record, runtime, session: existing };
3310
3345
  const pending = this.sessionLoads.get(record.localSessionId);
3311
- if (pending) return pending;
3346
+ if (pending) {
3347
+ const loaded = await pending;
3348
+ return {
3349
+ ...loaded,
3350
+ record: { ...loaded.record, contextKey: record.contextKey },
3351
+ };
3352
+ }
3312
3353
 
3313
3354
  this.outbound.registerSession(
3314
3355
  record.localSessionId,
@@ -3377,7 +3418,11 @@ export class ProxySessionController {
3377
3418
  }
3378
3419
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3379
3420
  const saved = this.store.getSession(record.localSessionId) ?? record;
3380
- return { record: saved, runtime, session };
3421
+ return {
3422
+ record: { ...saved, contextKey: record.contextKey },
3423
+ runtime,
3424
+ session,
3425
+ };
3381
3426
  })();
3382
3427
  this.sessionLoads.set(record.localSessionId, loading);
3383
3428
  try {
@@ -3476,6 +3521,7 @@ export class ProxySessionController {
3476
3521
  }
3477
3522
 
3478
3523
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
3524
+ await this.refreshGoalCards(event.sessionId);
3479
3525
  const terminalStatus = event.type === "turn_completed"
3480
3526
  ? "completed"
3481
3527
  : event.type === "turn_cancelled"
@@ -3883,8 +3929,10 @@ export class ProxySessionController {
3883
3929
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3884
3930
  }
3885
3931
 
3886
- const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
3887
- if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
3932
+ const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3933
+ .find((turn) => turn.turnId === turnId);
3934
+ const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3935
+ if (!snapshot || snapshot.status !== "completed") {
3888
3936
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3889
3937
  }
3890
3938
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
@@ -3905,7 +3953,7 @@ export class ProxySessionController {
3905
3953
  }
3906
3954
 
3907
3955
  const origin = this.store.getTurnRuntimeOrigin(turnId);
3908
- if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
3956
+ if (origin && origin.agentName !== current.agentName) {
3909
3957
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
3910
3958
  }
3911
3959
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -4369,6 +4417,25 @@ export class ProxySessionController {
4369
4417
  notice?: string,
4370
4418
  record = this.currentSession(contextKey),
4371
4419
  ): Promise<void> {
4420
+ const card = this.renderGoalCard(goal, notice, record);
4421
+ if (!record) {
4422
+ await this.outbound.sendInteractiveCard(contextKey, card);
4423
+ return;
4424
+ }
4425
+ const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
4426
+ if (existing) {
4427
+ await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
4428
+ return;
4429
+ }
4430
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
4431
+ if (messageId) this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
4432
+ }
4433
+
4434
+ private renderGoalCard(
4435
+ goal: RuntimeGoal | undefined,
4436
+ notice?: string,
4437
+ record?: SessionRecord,
4438
+ ): Record<string, unknown> {
4372
4439
  const sections: CardSection[] = [];
4373
4440
  if (notice) sections.push({ lines: [cardText(notice)] });
4374
4441
  sections.push(goal
@@ -4390,24 +4457,138 @@ export class ProxySessionController {
4390
4457
  "**/goal edit &#60;新目标&#62;** 修改  **/goal clear** 清除",
4391
4458
  ],
4392
4459
  });
4393
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Goal", sections));
4460
+ const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
4461
+ return this.cardRenderer.renderSectionsCard(title, sections);
4462
+ }
4463
+
4464
+ private async refreshGoalCards(localSessionId: string): Promise<void> {
4465
+ const deliveries = this.store.listGoalCardDeliveries(localSessionId);
4466
+ if (deliveries.length === 0) return;
4467
+ const record = this.store.getSession(localSessionId);
4468
+ if (!record) return;
4469
+ const runtime = this.runtimes.forAgent(record.agentName);
4470
+ if (runtime.kind !== "codex" || !runtime.getGoal) return;
4471
+
4472
+ try {
4473
+ const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
4474
+ const goal = await runtime.getGoal(loaded.localSessionId);
4475
+ const card = this.renderGoalCard(goal, undefined, record);
4476
+ await Promise.all(deliveries.map(async (delivery) => {
4477
+ try {
4478
+ await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
4479
+ } catch (error) {
4480
+ this.logger.warn(
4481
+ { error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId },
4482
+ "Failed to refresh an Agent Goal card.",
4483
+ );
4484
+ }
4485
+ }));
4486
+ } catch (error) {
4487
+ this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
4488
+ }
4394
4489
  }
4395
4490
 
4396
4491
  private async runShellCommand(contextKey: string, command: string): Promise<void> {
4397
4492
  const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
4398
- const result = await this.shellCommandExecutor(command, cwd);
4399
- const outputParts = [
4400
- result.stdout.trimEnd(),
4401
- result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
4402
- ].filter(Boolean);
4403
- const output = truncateMiddle(outputParts.join("\n"), 6_000);
4404
- const status = result.timedOut
4405
- ? "已超时(120s)"
4406
- : `退出码 ${result.exitCode ?? "未知"}`;
4407
- await this.outbound.sendMarkdown(contextKey, [
4408
- codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
4409
- `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
4410
- ].join("\n"));
4493
+ const startedAt = Date.now();
4494
+ let latestOutput: ShellCommandOutputSnapshot = {
4495
+ stdout: "",
4496
+ stderr: "",
4497
+ outputTruncated: false,
4498
+ };
4499
+ let runningState: ShellCommandCardView = {
4500
+ command,
4501
+ cwd,
4502
+ ...latestOutput,
4503
+ status: "running",
4504
+ elapsedMs: 0,
4505
+ };
4506
+ const cardMessageId = await this.outbound.sendInteractiveCard(
4507
+ contextKey,
4508
+ this.cardRenderer.renderShellCommandCard(runningState),
4509
+ );
4510
+ const scheduler = cardMessageId
4511
+ ? new CardUpdateScheduler<ShellCommandCardView>({
4512
+ render: (state) => this.cardRenderer.renderShellCommandCard(state),
4513
+ write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
4514
+ onError: (error) => this.logger.warn(
4515
+ { error, contextKey, messageId: cardMessageId },
4516
+ "Failed to update a shell command card.",
4517
+ ),
4518
+ })
4519
+ : undefined;
4520
+ scheduler?.seed(runningState);
4521
+ const elapsedTimer = scheduler ? setInterval(() => {
4522
+ runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
4523
+ scheduler.update(runningState);
4524
+ }, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
4525
+ elapsedTimer?.unref?.();
4526
+
4527
+ const updateRunningOutput = (snapshot: ShellCommandOutputSnapshot): void => {
4528
+ latestOutput = snapshot;
4529
+ runningState = {
4530
+ ...runningState,
4531
+ ...snapshot,
4532
+ elapsedMs: Date.now() - startedAt,
4533
+ };
4534
+ scheduler?.update(runningState);
4535
+ };
4536
+
4537
+ let result: ShellCommandResult;
4538
+ try {
4539
+ result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
4540
+ } catch (error) {
4541
+ if (elapsedTimer) clearInterval(elapsedTimer);
4542
+ const failedCard = this.cardRenderer.renderShellCommandCard({
4543
+ command,
4544
+ cwd,
4545
+ ...latestOutput,
4546
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4547
+ status: "failed",
4548
+ elapsedMs: Date.now() - startedAt,
4549
+ });
4550
+ if (scheduler) {
4551
+ try {
4552
+ await scheduler.flush({
4553
+ ...runningState,
4554
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4555
+ status: "failed",
4556
+ elapsedMs: Date.now() - startedAt,
4557
+ });
4558
+ } catch (cardError) {
4559
+ this.logger.warn(
4560
+ { error: cardError, contextKey, messageId: cardMessageId },
4561
+ "Failed to finalize a shell command card.",
4562
+ );
4563
+ } finally {
4564
+ scheduler.dispose();
4565
+ }
4566
+ } else {
4567
+ await this.outbound.sendInteractiveCard(contextKey, failedCard);
4568
+ }
4569
+ throw error;
4570
+ }
4571
+
4572
+ if (elapsedTimer) clearInterval(elapsedTimer);
4573
+ const finalState: ShellCommandCardView = {
4574
+ command,
4575
+ cwd,
4576
+ stdout: result.stdout,
4577
+ stderr: result.stderr,
4578
+ status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
4579
+ exitCode: result.exitCode,
4580
+ elapsedMs: Date.now() - startedAt,
4581
+ outputTruncated: result.outputTruncated,
4582
+ };
4583
+ if (scheduler) {
4584
+ try {
4585
+ await scheduler.flush(finalState);
4586
+ } finally {
4587
+ scheduler.dispose();
4588
+ }
4589
+ } else {
4590
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
4591
+ }
4411
4592
  }
4412
4593
 
4413
4594
  private async setTitle(contextKey: string, title: string): Promise<void> {
@@ -4524,7 +4705,7 @@ export class ProxySessionController {
4524
4705
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4525
4706
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4526
4707
  }
4527
- const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
4708
+ const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
4528
4709
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
4529
4710
  turnId: turn.turnId,
4530
4711
  parentTurnId: turn.parentTurnId,
@@ -4666,7 +4847,6 @@ export class ProxySessionController {
4666
4847
  contextKey,
4667
4848
  },
4668
4849
  },
4669
- actions: directoryCreationActions(entry.path, contextKey),
4670
4850
  }
4671
4851
  : {
4672
4852
  name: entry.name,
@@ -4686,12 +4866,12 @@ export class ProxySessionController {
4686
4866
  const card = this.cardRenderer.renderDirectoryBrowserCard({
4687
4867
  directory: abbreviateHomeDirectory(directory),
4688
4868
  entries: visibleEntries,
4689
- currentActions: directoryCreationActions(directory, contextKey),
4869
+ currentActions: directoryCreationActions(directory, contextKey, page),
4690
4870
  navigationActions,
4691
4871
  footerLines: [
4692
4872
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
4693
4873
  "",
4694
- "> 点击目录名称进入,点击文件名称发送到当前会话;**New** 在该目录创建任务,**NewGroup** 在该目录创建新群和任务。",
4874
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部可新建目录、任务或群聊。",
4695
4875
  ],
4696
4876
  });
4697
4877
  if (options.updateMessageId) {
@@ -4723,7 +4903,6 @@ export class ProxySessionController {
4723
4903
  contextKey,
4724
4904
  },
4725
4905
  },
4726
- actions: directoryCreationActions(drive.root, contextKey),
4727
4906
  }));
4728
4907
  const card = this.cardRenderer.renderDirectoryBrowserCard({
4729
4908
  directory: "此电脑",
@@ -4738,7 +4917,7 @@ export class ProxySessionController {
4738
4917
  footerLines: [
4739
4918
  `第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
4740
4919
  "",
4741
- "> 点击磁盘名称进入;**New** 在磁盘根目录创建任务,**NewGroup** 在磁盘根目录创建新群和任务。",
4920
+ "> 点击磁盘名称进入。",
4742
4921
  ],
4743
4922
  });
4744
4923
  if (options.updateMessageId) {
@@ -4758,6 +4937,27 @@ export class ProxySessionController {
4758
4937
  if (!stats.isDirectory()) throw new Error(`这不是目录:${directory}`);
4759
4938
  }
4760
4939
 
4940
+ private async createDirectoryFromCard(contextKey: string, action: CardAction): Promise<void> {
4941
+ const directory = directoryActionPath(action.value.directory);
4942
+ await this.assertBrowsableDirectory(directory);
4943
+ const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
4944
+ const newDirectory = path.join(directory, folderName);
4945
+ try {
4946
+ await fs.mkdir(newDirectory);
4947
+ } catch (error) {
4948
+ const code = typeof error === "object" && error !== null && "code" in error
4949
+ ? String(error.code)
4950
+ : undefined;
4951
+ if (code === "EEXIST") throw new Error(`目录已存在:${newDirectory}`);
4952
+ throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
4953
+ }
4954
+ await this.openDirectoryBrowser(contextKey, directory, {
4955
+ updateMessageId: requiredCardMessageId(action.messageId),
4956
+ page: directoryPageValue(action.value.page),
4957
+ });
4958
+ await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
4959
+ }
4960
+
4761
4961
  private async assertSendableFile(filePath: string): Promise<void> {
4762
4962
  let stats;
4763
4963
  try {
@@ -5420,7 +5620,9 @@ export class ProxySessionController {
5420
5620
  let activeTurnId: string | undefined;
5421
5621
  let queued = 0;
5422
5622
  if (!current) {
5423
- taskLines.push("无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5623
+ taskLines.push(isThreadContextKey(contextKey)
5624
+ ? unboundThreadTaskMessage()
5625
+ : "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5424
5626
  } else {
5425
5627
  const agent = this.ensureAgent(current.agentName);
5426
5628
  const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
@@ -5696,7 +5898,11 @@ export class ProxySessionController {
5696
5898
 
5697
5899
  private requireCurrentSession(contextKey: string): SessionRecord {
5698
5900
  const record = this.currentSession(contextKey);
5699
- if (!record) throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
5901
+ if (!record) {
5902
+ throw new Error(isThreadContextKey(contextKey)
5903
+ ? unboundThreadTaskMessage()
5904
+ : "当前没有任务,直接发送一条消息即可自动创建。");
5905
+ }
5700
5906
  return record;
5701
5907
  }
5702
5908
 
@@ -6213,6 +6419,19 @@ function isQueueIndependentCommand(command: Command): boolean {
6213
6419
  return false;
6214
6420
  }
6215
6421
 
6422
+ function commandRequiresCurrentSession(command: Command): boolean {
6423
+ if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
6424
+ return true;
6425
+ }
6426
+ if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
6427
+ if (command.type === "goal") return command.action !== "set";
6428
+ return false;
6429
+ }
6430
+
6431
+ function unboundThreadTaskMessage(): string {
6432
+ return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
6433
+ }
6434
+
6216
6435
  function parseSessionPage(value: unknown): number {
6217
6436
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
6218
6437
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
@@ -6250,10 +6469,45 @@ function directoryFileActionPath(value: unknown): string {
6250
6469
  return path.resolve(value);
6251
6470
  }
6252
6471
 
6253
- function directoryCreationActions(directory: string, contextKey: string): TaskListCardAction[] {
6472
+ function cardFormValue(value: Record<string, unknown>, name: string): unknown {
6473
+ const formValue = value.formValue;
6474
+ return typeof formValue === "object" && formValue !== null
6475
+ ? (formValue as Record<string, unknown>)[name]
6476
+ : undefined;
6477
+ }
6478
+
6479
+ function directoryFolderName(value: unknown): string {
6480
+ if (typeof value !== "string" || !value.trim()) throw new Error("请输入目录名。");
6481
+ const name = value.trim();
6482
+ if (name === "." || name === "..") throw new Error("目录名不能是 . 或 ..。");
6483
+ if (name.length > 255) throw new Error("目录名不能超过 255 个字符。");
6484
+ if (/[<>:"/\\|?*\u0000-\u001F]/u.test(name)) {
6485
+ throw new Error("目录名不能包含路径分隔符或以下字符:< > : \" / \\ | ? *");
6486
+ }
6487
+ if (/[. ]$/u.test(name)) throw new Error("目录名不能以空格或句点结尾。");
6488
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu.test(name)) {
6489
+ throw new Error("该名称是系统保留名称,请使用其他目录名。");
6490
+ }
6491
+ return name;
6492
+ }
6493
+
6494
+ function directoryCreationActions(
6495
+ directory: string,
6496
+ contextKey: string,
6497
+ page: number,
6498
+ ): TaskListCardAction[] {
6254
6499
  return [
6255
6500
  {
6256
- text: "New",
6501
+ text: "NewFolder",
6502
+ value: {
6503
+ action: "directory_new_folder_prompt",
6504
+ directory,
6505
+ contextKey,
6506
+ page: String(page),
6507
+ },
6508
+ },
6509
+ {
6510
+ text: "NewTask",
6257
6511
  value: {
6258
6512
  action: "directory_new",
6259
6513
  directory,
@@ -6261,7 +6515,7 @@ function directoryCreationActions(directory: string, contextKey: string): TaskLi
6261
6515
  },
6262
6516
  },
6263
6517
  {
6264
- text: "NewGroup",
6518
+ text: "NewGroupTask",
6265
6519
  value: {
6266
6520
  action: "directory_new_group",
6267
6521
  directory,
@@ -6554,14 +6808,13 @@ const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
6554
6808
 
6555
6809
  function formatGroupProjectDirectory(projectCwd: string): string {
6556
6810
  const value = abbreviateHomeDirectory(projectCwd);
6557
- if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
6558
-
6559
6811
  const levels = value
6560
6812
  .replace(/[\\/]+$/, "")
6561
6813
  .split(/[\\/]+/)
6562
6814
  .filter(Boolean)
6563
6815
  .slice(-2);
6564
- return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
6816
+ const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
6817
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
6565
6818
  }
6566
6819
 
6567
6820
  function abbreviateHomeDirectory(value: string): string {