@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.3

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 (80) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/README.md +48 -15
  3. package/README.zh.md +48 -15
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +21 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +94 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +661 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +16 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +266 -38
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/feishu/FeishuMessageClient.js +16 -0
  37. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  38. package/dist/feishu/FeishuTurnPresenter.js +42 -6
  39. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  40. package/dist/index.js +136 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/presentation/OutboundRouter.js +7 -0
  43. package/dist/presentation/OutboundRouter.js.map +1 -1
  44. package/dist/presentation/TurnStateReducer.js +21 -7
  45. package/dist/presentation/TurnStateReducer.js.map +1 -1
  46. package/dist/proxy/ProxySessionController.js +537 -13
  47. package/dist/proxy/ProxySessionController.js.map +1 -1
  48. package/dist/state/StateStore.js +53 -0
  49. package/dist/state/StateStore.js.map +1 -1
  50. package/docs/technical-reference.md +17 -7
  51. package/docs/technical-reference.zh.md +17 -7
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +2 -2
  54. package/skills/agent-bot/SKILL.md +109 -188
  55. package/src/cli/AutostartManager.ts +579 -0
  56. package/src/cli/FeishuAppConfiguration.ts +26 -7
  57. package/src/cli/Initializer.ts +57 -0
  58. package/src/cli/SelfUpdateRunner.ts +817 -0
  59. package/src/cli/SelfUpdater.ts +688 -0
  60. package/src/cli/autostartOutput.ts +75 -0
  61. package/src/cli/controlProtocol.ts +119 -1
  62. package/src/cli/help.ts +94 -18
  63. package/src/cli/taskGroupOptions.ts +27 -17
  64. package/src/cli/taskListOutput.ts +10 -0
  65. package/src/cli/taskTarget.ts +168 -0
  66. package/src/cli.ts +809 -65
  67. package/src/codex/CodexRuntime.ts +8 -2
  68. package/src/commands/CommandRouter.ts +14 -1
  69. package/src/commands/commandTypes.ts +2 -0
  70. package/src/feishu/CardRenderer.ts +313 -35
  71. package/src/feishu/FeishuMarkdown.ts +27 -1
  72. package/src/feishu/FeishuMessageClient.ts +33 -0
  73. package/src/feishu/FeishuTurnPresenter.ts +43 -6
  74. package/src/feishu/types.ts +1 -0
  75. package/src/index.ts +174 -2
  76. package/src/presentation/OutboundRouter.ts +7 -0
  77. package/src/presentation/TurnStateReducer.ts +21 -7
  78. package/src/proxy/ProxySessionController.ts +661 -16
  79. package/src/runtime/types.ts +1 -0
  80. package/src/state/StateStore.ts +56 -0
@@ -71,6 +71,12 @@ const HELP_COMMAND_SECTIONS = [
71
71
  requiresArgument: true,
72
72
  },
73
73
  { command: "/sessions", usage: "[关键词]", description: "查找本机任务" },
74
+ {
75
+ command: "/archive",
76
+ usage: "[序号或任务 ID]",
77
+ description: "归档当前或指定任务",
78
+ },
79
+ { command: "/dismiss", description: "归档当前任务并解散当前群" },
74
80
  {
75
81
  command: "/switch",
76
82
  usage: "[序号或任务 ID]",
@@ -268,7 +274,7 @@ export class ProxySessionController {
268
274
  if (isQueueIndependentCommand(command)) {
269
275
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
270
276
  try {
271
- if (command.type !== "forkgroup")
277
+ if (command.type !== "forkgroup" && command.type !== "dismiss")
272
278
  await this.ensureThreadFork(message);
273
279
  await this.execute(message.contextKey, command, message.messageId, replyTarget, localImagePaths, message.userId, message);
274
280
  if (!isPromptCommand(command))
@@ -413,6 +419,22 @@ export class ProxySessionController {
413
419
  await this.createFeishuGroupFromReference(contextKey, sessionId, scopedAction.userId);
414
420
  await this.refreshSessionsCardFromAction(scopedAction);
415
421
  }
422
+ else if (kind === "session_archive") {
423
+ const sessionId = String(scopedAction.value.sessionId ?? "");
424
+ await this.archiveSessionReference(contextKey, sessionId, {
425
+ announce: true,
426
+ source: "sessions_card",
427
+ });
428
+ await this.refreshSessionsCardFromAction(scopedAction);
429
+ }
430
+ else if (kind === "group_dismiss_keep") {
431
+ this.assertDismissGroupRequester(scopedAction);
432
+ await this.outbound.updateInteractiveCard(contextKey, requiredCardMessageId(scopedAction.messageId), this.cardRenderer.renderDismissGroupKept());
433
+ }
434
+ else if (kind === "group_dismiss_confirm") {
435
+ this.assertDismissGroupRequester(scopedAction);
436
+ await this.dismissGroupAndArchiveTask(contextKey, String(scopedAction.value.sessionId ?? ""), "command");
437
+ }
416
438
  else if (kind === "session_stop") {
417
439
  const sessionId = String(scopedAction.value.sessionId ?? "");
418
440
  await this.stopSessionReference(contextKey, sessionId);
@@ -557,6 +579,19 @@ export class ProxySessionController {
557
579
  await this.cancelSession(record);
558
580
  return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
559
581
  }
582
+ async controlArchiveTask(localSessionId) {
583
+ const record = this.requireControlSession(localSessionId);
584
+ return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
585
+ announce: false,
586
+ source: "cli",
587
+ });
588
+ }
589
+ async controlDismissTask(localSessionId) {
590
+ const record = this.store.getSession(localSessionId);
591
+ if (!record)
592
+ throw new Error(`Task not found: ${localSessionId}`);
593
+ return this.dismissGroupAndArchiveTask(this.controlSessionContextKey(record), localSessionId, "cli");
594
+ }
560
595
  async controlGetTaskStatus(localSessionId) {
561
596
  const record = this.store.getSession(localSessionId);
562
597
  if (!record)
@@ -706,6 +741,314 @@ export class ProxySessionController {
706
741
  task: forked.task,
707
742
  };
708
743
  }
744
+ async controlCreateTask(localSessionId, requestedTitle, requestedProjectCwd, forceProjectless = false, requestedAgentName) {
745
+ const source = this.requireControlSession(localSessionId);
746
+ const contextKey = this.controlSessionContextKey(source);
747
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
748
+ const agentName = requestedAgentName?.trim() || context.defaultAgent;
749
+ const agent = this.ensureAgent(agentName);
750
+ if (forceProjectless && agent.kind !== "app-server") {
751
+ throw new Error("task new --nodir is only available for App Server agents.");
752
+ }
753
+ const replyTarget = this.controlSessionReplyTarget(source);
754
+ const cwd = forceProjectless
755
+ ? undefined
756
+ : requestedProjectCwd === undefined
757
+ ? detectProjectlessWorkspace(source.cwd)
758
+ ? agent.kind === "app-server" ? undefined : createProjectlessWorkspace().cwd
759
+ : source.cwd
760
+ : resolveUserPath(requestedProjectCwd, source.cwd);
761
+ const settings = source.agentName === agentName
762
+ ? {
763
+ modelProvider: source.modelProvider,
764
+ model: source.model,
765
+ reasoningEffort: source.reasoningEffort,
766
+ permissionMode: source.permissionMode,
767
+ }
768
+ : {};
769
+ return this.outbound.withReplyTarget(contextKey, replyTarget, () => this.createSession(contextKey, agentName, cwd, true, false, undefined, undefined, requestedTitle, settings));
770
+ }
771
+ async controlForkTask(localSessionId) {
772
+ const source = this.requireControlSession(localSessionId);
773
+ const contextKey = this.controlSessionContextKey(source);
774
+ const replyTarget = this.controlSessionReplyTarget(source);
775
+ const plan = await this.prepareForkSession(contextKey, source.localSessionId);
776
+ const forked = await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
777
+ const result = await this.forkSessionIntoContext(contextKey, plan);
778
+ await this.outbound.sendText(contextKey, `已从指定任务${plan.forkedFromHistoricalTurn ? "最近已完成轮次" : ""}创建分支并切换到新任务:${result.session.title
779
+ ? `${result.session.title}(${result.session.remoteSessionId})`
780
+ : result.session.remoteSessionId}`);
781
+ return result;
782
+ });
783
+ return {
784
+ sourceLocalSessionId: source.localSessionId,
785
+ sourceTurnId: plan.lastTurnId,
786
+ task: this.store.getSession(forked.record.localSessionId) ?? forked.record,
787
+ };
788
+ }
789
+ async controlSwitchTask(localSessionId, targetLocalSessionId, previous = false) {
790
+ const anchor = this.requireControlSession(localSessionId);
791
+ const contextKey = this.controlSessionContextKey(anchor);
792
+ const replyTarget = this.controlSessionReplyTarget(anchor);
793
+ const target = previous ? undefined : targetLocalSessionId ?? anchor.localSessionId;
794
+ await this.outbound.withReplyTarget(contextKey, replyTarget, () => this.switchSession(contextKey, target));
795
+ return this.requireCurrentSession(contextKey);
796
+ }
797
+ async controlQueueTaskPrompt(localSessionId, text) {
798
+ const promptText = text.trim();
799
+ if (!promptText)
800
+ throw new Error("The queued Prompt cannot be empty.");
801
+ const record = this.requireControlSession(localSessionId);
802
+ const contextKey = this.controlSessionContextKey(record);
803
+ const replyTarget = this.controlSessionReplyTarget(record);
804
+ const queued = this.persistQueuedPrompt(record.localSessionId, contextKey, promptText, { replyTarget });
805
+ this.store.audit(contextKey, "queued_prompt_added", {
806
+ promptId: queued.promptId,
807
+ localSessionId: record.localSessionId,
808
+ source: "cli",
809
+ });
810
+ const queuedCount = this.store.countQueuedPrompts(record.localSessionId);
811
+ await this.scheduleNextQueuedPrompt(record.localSessionId);
812
+ return { promptId: queued.promptId, queued: queuedCount };
813
+ }
814
+ controlTaskAgent(localSessionId, agentName) {
815
+ const record = this.requireControlSession(localSessionId);
816
+ const contextKey = this.controlSessionContextKey(record);
817
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
818
+ if (agentName) {
819
+ this.ensureAgent(agentName);
820
+ this.store.setDefaultAgent(contextKey, agentName);
821
+ }
822
+ return {
823
+ current: agentName ?? context.defaultAgent,
824
+ agents: Object.entries(this.config.agents).map(([name, agent]) => ({ name, title: agent.title })),
825
+ };
826
+ }
827
+ async controlTaskSettings(localSessionId, setting, value) {
828
+ const record = this.requireControlSession(localSessionId);
829
+ const loaded = await this.loadSession(record);
830
+ if (setting && !value?.trim())
831
+ throw new Error(`task ${setting} requires a value.`);
832
+ const nextValue = value?.trim();
833
+ if (setting === "provider" && nextValue) {
834
+ await this.assertModelProvider(loaded, nextValue);
835
+ const models = await loaded.runtime.listModels();
836
+ const model = models.find((candidate) => candidate.id === loaded.session.model)
837
+ ?? models.find((candidate) => candidate.isDefault);
838
+ if (!model)
839
+ throw new Error("The current runtime has no model available for a Provider change.");
840
+ const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
841
+ ? loaded.session.reasoningEffort
842
+ : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
843
+ if (!effort)
844
+ throw new Error(`Model ${model.id} has no reasoning effort available for a Provider change.`);
845
+ if (!loaded.runtime.setExecutionSettings)
846
+ throw new Error("The current runtime does not support Provider settings.");
847
+ const updated = await loaded.runtime.setExecutionSettings(record.localSessionId, {
848
+ modelProvider: nextValue,
849
+ model: model.id,
850
+ reasoningEffort: effort,
851
+ permissionMode: loaded.session.permissionMode,
852
+ });
853
+ this.store.updateRuntimeSession(record.localSessionId, {
854
+ modelProvider: updated.modelProvider ?? nextValue,
855
+ model: updated.model ?? model.id,
856
+ reasoningEffort: updated.reasoningEffort ?? effort,
857
+ permissionMode: updated.permissionMode,
858
+ });
859
+ }
860
+ else if (setting === "model" && nextValue) {
861
+ const models = await loaded.runtime.listModels();
862
+ const selected = models.find((candidate) => candidate.id === nextValue);
863
+ if (!selected)
864
+ throw new Error(`Unknown model: ${nextValue}`);
865
+ const currentEffort = loaded.session.reasoningEffort;
866
+ const nextEffort = currentEffort && selected.supportedReasoningEfforts.some((candidate) => candidate.value === currentEffort)
867
+ ? currentEffort
868
+ : selected.defaultReasoningEffort;
869
+ await loaded.runtime.setModel(record.localSessionId, nextValue);
870
+ if (nextEffort && nextEffort !== currentEffort) {
871
+ await loaded.runtime.setReasoningEffort(record.localSessionId, nextEffort);
872
+ }
873
+ this.store.updateRuntimeSession(record.localSessionId, { model: nextValue, reasoningEffort: nextEffort });
874
+ }
875
+ else if (setting === "thinking" && nextValue) {
876
+ const models = await loaded.runtime.listModels();
877
+ const currentModel = models.find((candidate) => candidate.id === loaded.session.model)
878
+ ?? models.find((candidate) => candidate.isDefault);
879
+ if (!currentModel)
880
+ throw new Error("The current runtime has no model with configurable reasoning effort.");
881
+ if (!currentModel.supportedReasoningEfforts.some((candidate) => candidate.value === nextValue)) {
882
+ const supported = currentModel.supportedReasoningEfforts.map((candidate) => candidate.value).join(", ") || "none";
883
+ throw new Error(`Unsupported reasoning effort: ${nextValue}. Supported values: ${supported}.`);
884
+ }
885
+ await loaded.runtime.setReasoningEffort(record.localSessionId, nextValue);
886
+ this.store.updateRuntimeSession(record.localSessionId, { reasoningEffort: nextValue });
887
+ }
888
+ else if (setting === "permissions" && nextValue) {
889
+ if (nextValue !== "auto" && nextValue !== "confirm") {
890
+ throw new Error("task permissions accepts auto or confirm.");
891
+ }
892
+ await loaded.runtime.setPermissionMode(record.localSessionId, nextValue);
893
+ this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
894
+ }
895
+ const session = this.store.getSession(record.localSessionId) ?? record;
896
+ const models = await loaded.runtime.listModels();
897
+ const currentModel = models.find((candidate) => candidate.id === session.model)
898
+ ?? models.find((candidate) => candidate.isDefault);
899
+ const providers = loaded.runtime.listModelProviders
900
+ ? await loaded.runtime.listModelProviders()
901
+ : [];
902
+ return {
903
+ session,
904
+ providers,
905
+ models,
906
+ reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
907
+ permissionModes: ["auto", "confirm"],
908
+ };
909
+ }
910
+ async controlTaskGoal(localSessionId, action, objective) {
911
+ const record = this.requireControlSession(localSessionId);
912
+ const loaded = await this.loadSession(record);
913
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.getGoal || !loaded.runtime.setGoal || !loaded.runtime.clearGoal) {
914
+ throw new Error("The current Agent does not support Goal mode.");
915
+ }
916
+ if (action === "show")
917
+ return { goal: await loaded.runtime.getGoal(record.localSessionId) };
918
+ if (action === "clear")
919
+ return { cleared: await loaded.runtime.clearGoal(record.localSessionId) };
920
+ const current = await loaded.runtime.getGoal(record.localSessionId);
921
+ if (action === "pause" || action === "resume") {
922
+ if (!current)
923
+ throw new Error("The task has no Goal.");
924
+ return { goal: await loaded.runtime.setGoal(record.localSessionId, { status: action === "pause" ? "paused" : "active" }) };
925
+ }
926
+ const nextObjective = objective?.trim();
927
+ if (!nextObjective)
928
+ throw new Error(`task goal ${action} requires an objective.`);
929
+ validateGoalObjective(nextObjective);
930
+ if (action === "set" && current && current.status !== "complete") {
931
+ throw new Error("The task already has an unfinished Goal. Use task goal <task> edit <objective>, or clear it first.");
932
+ }
933
+ if (action === "edit" && !current)
934
+ throw new Error("The task has no Goal to edit.");
935
+ return {
936
+ goal: await loaded.runtime.setGoal(record.localSessionId, {
937
+ objective: nextObjective,
938
+ status: action === "edit" ? current.status : "active",
939
+ ...(action === "edit" ? { tokenBudget: current.tokenBudget } : {}),
940
+ }),
941
+ };
942
+ }
943
+ controlListTaskTurns(localSessionId) {
944
+ const session = this.requireControlSession(localSessionId);
945
+ if (!session.remoteSessionId || !this.isCodexSession(session)) {
946
+ throw new Error("The task is not an App Server task that can be reset.");
947
+ }
948
+ const contextKey = this.controlSessionContextKey(session);
949
+ const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
950
+ const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
951
+ return {
952
+ session,
953
+ turns: rows.flatMap((turn, index) => {
954
+ const snapshot = turnViewSnapshot(turn.snapshot);
955
+ if (!snapshot)
956
+ return [];
957
+ return [{
958
+ sequence: graph[index].sequence,
959
+ turnId: turn.turnId,
960
+ parentTurnId: turn.parentTurnId,
961
+ prompt: snapshot.prompt ?? snapshot.taskTitle,
962
+ startedAt: snapshot.startedAt,
963
+ completedAt: snapshot.completedAt,
964
+ current: session.lastTurnId === turn.turnId && session.lastTurnStatus === "completed",
965
+ }];
966
+ }),
967
+ };
968
+ }
969
+ async controlResetTaskToTurn(localSessionId, turnId) {
970
+ const record = this.requireControlSession(localSessionId);
971
+ const contextKey = this.controlSessionContextKey(record);
972
+ const previous = this.sessionResets.get(record.localSessionId) ?? Promise.resolve();
973
+ const reset = previous.catch(() => undefined).then(() => this.performSessionReset(contextKey, record, turnId, false));
974
+ this.sessionResets.set(record.localSessionId, reset);
975
+ try {
976
+ await reset;
977
+ }
978
+ finally {
979
+ if (this.sessionResets.get(record.localSessionId) === reset)
980
+ this.sessionResets.delete(record.localSessionId);
981
+ }
982
+ return this.store.getSession(record.localSessionId) ?? record;
983
+ }
984
+ controlTaskMute(localSessionId, enabled) {
985
+ const record = this.requireControlSession(localSessionId);
986
+ const contextKey = baseChatContextKey(this.controlSessionContextKey(record));
987
+ const chat = this.store.getChatContext(contextKey);
988
+ if (chat?.chatType !== "group")
989
+ throw new Error("task mute is only available for group chats.");
990
+ const next = enabled ?? Boolean(chat.requiresMention);
991
+ if (enabled !== undefined)
992
+ this.store.setChatRequiresMention(contextKey, enabled);
993
+ return { contextKey, enabled: next };
994
+ }
995
+ async controlRunTaskShell(localSessionId, command) {
996
+ const record = this.requireControlSession(localSessionId);
997
+ const normalized = command.trim();
998
+ if (!normalized)
999
+ throw new Error("task shell requires a command.");
1000
+ return { ...await this.shellCommandExecutor(normalized, record.cwd), cwd: record.cwd, command: normalized };
1001
+ }
1002
+ async controlListTaskDirectory(localSessionId, requestedDirectory, requestedPage = 0) {
1003
+ const record = this.requireControlSession(localSessionId);
1004
+ if (requestedDirectory === WINDOWS_DRIVES_DIRECTORY) {
1005
+ const drives = await this.windowsDriveLister();
1006
+ const totalPages = Math.max(1, Math.ceil(drives.length / DIRECTORY_PAGE_SIZE));
1007
+ const page = Math.min(Math.max(0, Math.trunc(requestedPage)), totalPages - 1);
1008
+ return {
1009
+ directory: WINDOWS_DRIVES_DIRECTORY,
1010
+ page,
1011
+ totalPages,
1012
+ totalEntries: drives.length,
1013
+ entries: drives.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE).map((drive) => ({
1014
+ name: windowsDriveDisplayName(drive),
1015
+ path: drive.root,
1016
+ kind: "drive",
1017
+ })),
1018
+ };
1019
+ }
1020
+ const directory = requestedDirectory === undefined
1021
+ ? path.resolve(record.cwd)
1022
+ : resolveUserPath(requestedDirectory, record.cwd);
1023
+ await this.assertBrowsableDirectory(directory);
1024
+ const entries = (await fs.readdir(directory, { withFileTypes: true }))
1025
+ .map((entry) => ({
1026
+ name: entry.name,
1027
+ path: path.join(directory, entry.name),
1028
+ kind: directoryBrowserEntryKind(entry),
1029
+ }))
1030
+ .sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
1031
+ || left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
1032
+ const totalPages = Math.max(1, Math.ceil(entries.length / DIRECTORY_PAGE_SIZE));
1033
+ const page = Math.min(Math.max(0, Math.trunc(requestedPage)), totalPages - 1);
1034
+ const parentDirectory = isWindowsDriveRoot(directory) ? WINDOWS_DRIVES_DIRECTORY : path.dirname(directory);
1035
+ return {
1036
+ directory,
1037
+ ...(parentDirectory !== directory ? { parentDirectory } : {}),
1038
+ page,
1039
+ totalPages,
1040
+ totalEntries: entries.length,
1041
+ entries: entries.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE),
1042
+ };
1043
+ }
1044
+ async controlSendTaskFile(localSessionId, requestedFilePath) {
1045
+ const record = this.requireControlSession(localSessionId);
1046
+ const filePath = resolveUserPath(requestedFilePath, record.cwd);
1047
+ await this.assertSendableFile(filePath);
1048
+ const contextKey = this.controlSessionContextKey(record);
1049
+ const replyTarget = this.controlSessionReplyTarget(record);
1050
+ return this.outbound.withReplyTarget(contextKey, replyTarget, () => this.outbound.sendFile(contextKey, filePath));
1051
+ }
709
1052
  cardActionContextKey(action) {
710
1053
  const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
711
1054
  if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey))
@@ -776,6 +1119,19 @@ export class ProxySessionController {
776
1119
  else
777
1120
  await this.openAgentSettings(contextKey);
778
1121
  return;
1122
+ case "archive": {
1123
+ const sessionId = command.sessionId ?? context.currentSessionId;
1124
+ if (!sessionId)
1125
+ throw new Error("当前会话没有可归档的任务。请使用 /sessions 查找任务。");
1126
+ await this.archiveSessionReference(contextKey, sessionId, {
1127
+ announce: true,
1128
+ source: "command",
1129
+ });
1130
+ return;
1131
+ }
1132
+ case "dismiss":
1133
+ await this.openDismissGroupCard(contextKey, userId);
1134
+ return;
779
1135
  case "status":
780
1136
  await this.status(contextKey, command.sessionId);
781
1137
  return;
@@ -2390,15 +2746,19 @@ export class ProxySessionController {
2390
2746
  if (current.localSessionId !== sessionId) {
2391
2747
  throw new Error("这张思考卡片不属于当前任务。请先切换回对应任务,再点击 Reset。");
2392
2748
  }
2749
+ await this.performSessionReset(contextKey, current, turnId, true);
2750
+ }
2751
+ async performSessionReset(contextKey, current, turnId, announce) {
2393
2752
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
2394
2753
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
2395
2754
  }
2396
2755
  const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
2397
- if (!snapshot || snapshot.sessionId !== sessionId || snapshot.status !== "completed") {
2756
+ if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
2398
2757
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
2399
2758
  }
2400
2759
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
2401
- await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
2760
+ if (announce)
2761
+ await this.outbound.sendText(contextKey, "当前任务已经处于本轮完成后的对话状态,无需 Reset。");
2402
2762
  return;
2403
2763
  }
2404
2764
  const runtime = this.runtimes.forAgent(current.agentName);
@@ -2413,7 +2773,7 @@ export class ProxySessionController {
2413
2773
  throw new Error("当前任务仍在执行。请等待任务完成或先停止任务,再点击 Reset。");
2414
2774
  }
2415
2775
  const origin = this.store.getTurnRuntimeOrigin(turnId);
2416
- if (origin && (origin.localSessionId !== sessionId || origin.agentName !== current.agentName)) {
2776
+ if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
2417
2777
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
2418
2778
  }
2419
2779
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -2445,13 +2805,15 @@ export class ProxySessionController {
2445
2805
  });
2446
2806
  const targetSummary = truncateText((snapshot.prompt ?? snapshot.taskTitle ?? "未记录对话内容").replace(/\s+/g, " ").trim(), 100) || "未记录对话内容";
2447
2807
  const targetTime = snapshot.completedAt ?? snapshot.startedAt;
2448
- await this.outbound.sendText(contextKey, [
2449
- "已将当前任务重置到:",
2450
- targetSummary,
2451
- `完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
2452
- `Turn ID:${turnId}`,
2453
- "后续对话将从该轮完成后的状态继续;本地文件没有回退。",
2454
- ].join("\n"));
2808
+ if (announce) {
2809
+ await this.outbound.sendText(contextKey, [
2810
+ "已将当前任务重置到:",
2811
+ targetSummary,
2812
+ `完成时间:${targetTime === undefined ? "未知" : formatResetTurnTime(targetTime)}`,
2813
+ `Turn ID:${turnId}`,
2814
+ "后续对话将从该轮完成后的状态继续;本地文件没有回退。",
2815
+ ].join("\n"));
2816
+ }
2455
2817
  }
2456
2818
  async resolveApproval(action) {
2457
2819
  const sessionId = String(action.value.sessionId ?? "");
@@ -3268,6 +3630,16 @@ export class ProxySessionController {
3268
3630
  contextKey,
3269
3631
  },
3270
3632
  });
3633
+ actions.push({
3634
+ text: "Archive",
3635
+ value: {
3636
+ action: "session_archive",
3637
+ sessionId: entry.reference,
3638
+ ...(searchTerm ? { searchTerm } : {}),
3639
+ page: String(page),
3640
+ contextKey,
3641
+ },
3642
+ });
3271
3643
  const title = truncateText(entry.title.replace(/\s+/gu, " ").trim() || "未命名任务", 56);
3272
3644
  return {
3273
3645
  project,
@@ -3306,7 +3678,7 @@ export class ProxySessionController {
3306
3678
  `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
3307
3679
  "",
3308
3680
  "> 项目行:**New** 新建任务,**NewGroup** 新建群。",
3309
- "> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态。",
3681
+ "> 任务详情:**Switch** 切换,**Stop** 停止,**Fork** / **ForkGroup** 创建分支,**Status** 查看状态,**Archive** 归档。",
3310
3682
  ], [
3311
3683
  ...(hasPrevious ? [{
3312
3684
  text: "Previous",
@@ -3471,6 +3843,138 @@ export class ProxySessionController {
3471
3843
  });
3472
3844
  await this.outbound.sendText(contextKey, `已向 Agent 发送 Interrupt 请求:${turnId}`);
3473
3845
  }
3846
+ async archiveSessionReference(contextKey, reference, options) {
3847
+ const taskId = this.resolveSessionReference(contextKey, reference);
3848
+ const direct = this.store.getSession(taskId);
3849
+ if (direct?.status === "closed")
3850
+ throw new Error(`任务已经归档:${direct.title ?? taskId}`);
3851
+ const local = direct ?? this.findStoredSessionByReference(taskId);
3852
+ if (local && (!local.remoteSessionId || !this.isCodexSession(local))) {
3853
+ throw new Error("当前 Agent 不支持归档任务。");
3854
+ }
3855
+ if (local && (local.status === "running"
3856
+ || Boolean(this.runtimes.forAgent(local.agentName).getSession(local.localSessionId)?.activeTurnId))) {
3857
+ throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
3858
+ }
3859
+ if (local && this.store.countQueuedPrompts(local.localSessionId) > 0) {
3860
+ throw new Error("任务仍有排队消息,无法归档。请先处理或取消排队消息。");
3861
+ }
3862
+ const remoteReference = local
3863
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
3864
+ : taskId;
3865
+ const { agentName, runtime, remote } = await this.resolveRemoteCodexSession(remoteReference);
3866
+ if (isRemoteSessionActive(remote)) {
3867
+ throw new Error("任务正在执行,无法归档。请先停止任务或等待本轮完成。");
3868
+ }
3869
+ if (!runtime.archiveRemoteSession) {
3870
+ throw new Error(`Agent ${agentName} 不支持归档任务。`);
3871
+ }
3872
+ const wasCurrent = Boolean(local && this.store.getUserContext(contextKey)?.currentSessionId === local.localSessionId);
3873
+ await runtime.archiveRemoteSession(remote.id);
3874
+ const title = local?.title ?? remote.title ?? remote.preview ?? remote.id;
3875
+ if (local) {
3876
+ this.store.archiveSession(local.localSessionId);
3877
+ this.outbound.unregisterSession(local.localSessionId);
3878
+ }
3879
+ this.store.audit(contextKey, "session_archived", {
3880
+ ...(local ? { localSessionId: local.localSessionId } : {}),
3881
+ remoteSessionId: remote.id,
3882
+ agentName,
3883
+ source: options.source,
3884
+ });
3885
+ if (options.announce) {
3886
+ await this.outbound.sendText(contextKey, wasCurrent
3887
+ ? `已归档任务:${title}\n当前会话已没有绑定任务,直接发送消息即可创建新任务。`
3888
+ : `已归档任务:${title}`);
3889
+ }
3890
+ return {
3891
+ localSessionId: local?.localSessionId ?? "",
3892
+ remoteSessionId: remote.id,
3893
+ title,
3894
+ };
3895
+ }
3896
+ async openDismissGroupCard(contextKey, userId) {
3897
+ const groupContextKey = this.dismissibleGroupContextKey(contextKey);
3898
+ if (!userId)
3899
+ throw new Error("无法识别当前用户,不能发起解散群操作。");
3900
+ const session = this.requireCurrentSession(groupContextKey);
3901
+ await this.outbound.sendInteractiveCard(groupContextKey, this.cardRenderer.renderDismissGroupConfirmation({
3902
+ contextKey: groupContextKey,
3903
+ sessionId: session.localSessionId,
3904
+ taskTitle: session.title ?? session.remoteSessionId ?? session.localSessionId,
3905
+ requestedBy: userId,
3906
+ }));
3907
+ }
3908
+ assertDismissGroupRequester(action) {
3909
+ const requestedBy = String(action.value.requestedBy ?? "");
3910
+ if (!requestedBy || !action.userId || action.userId !== requestedBy) {
3911
+ throw new Error("只有发起解散操作的用户可以点击这张卡片。");
3912
+ }
3913
+ }
3914
+ async dismissGroupAndArchiveTask(contextKey, localSessionId, source) {
3915
+ const groupContextKey = this.dismissibleGroupContextKey(contextKey);
3916
+ const session = this.store.getSessionForContext(localSessionId, groupContextKey);
3917
+ if (!session)
3918
+ throw new Error("解散群卡片已失效,请重新发送 /dismiss。");
3919
+ const currentSessionId = this.store.getUserContext(groupContextKey)?.currentSessionId;
3920
+ if (session.status !== "closed" && currentSessionId !== localSessionId) {
3921
+ throw new Error("当前任务已经切换,解散群卡片已失效,请重新发送 /dismiss。");
3922
+ }
3923
+ this.assertNoOtherActiveGroupTasks(groupContextKey, localSessionId);
3924
+ const archived = session.status === "closed"
3925
+ ? {
3926
+ localSessionId: session.localSessionId,
3927
+ remoteSessionId: session.remoteSessionId ?? session.acpSessionId ?? "",
3928
+ title: session.title ?? session.remoteSessionId ?? session.localSessionId,
3929
+ }
3930
+ : await this.archiveSessionReference(groupContextKey, localSessionId, {
3931
+ announce: false,
3932
+ source: "dismiss",
3933
+ });
3934
+ const chatId = groupContextKey.slice("chat_id:".length);
3935
+ try {
3936
+ await this.outbound.deleteGroup(groupContextKey, chatId);
3937
+ }
3938
+ catch (error) {
3939
+ const message = error instanceof Error ? error.message : String(error);
3940
+ throw new Error(`当前任务已归档,但解散群失败:${message}`);
3941
+ }
3942
+ const detachedSessionIds = this.store.removeChatContext(groupContextKey);
3943
+ for (const detachedSessionId of detachedSessionIds) {
3944
+ const linked = this.store.getSession(detachedSessionId);
3945
+ const routedContextKey = this.outbound.getSessionContextKey(detachedSessionId) ?? linked?.contextKey;
3946
+ if (routedContextKey && baseChatContextKey(routedContextKey) === groupContextKey) {
3947
+ this.outbound.unregisterSession(detachedSessionId);
3948
+ }
3949
+ }
3950
+ this.store.audit(groupContextKey, "group_dismissed", {
3951
+ chatId,
3952
+ localSessionId,
3953
+ remoteSessionId: archived.remoteSessionId,
3954
+ source,
3955
+ });
3956
+ return { ...archived, chatId };
3957
+ }
3958
+ assertNoOtherActiveGroupTasks(contextKey, dismissedSessionId) {
3959
+ const active = this.store.listSessionsForChat(contextKey).find((session) => (session.localSessionId !== dismissedSessionId
3960
+ && session.status !== "closed"
3961
+ && (session.status === "running"
3962
+ || Boolean(this.runtimes.forAgent(session.agentName).getSession(session.localSessionId)?.activeTurnId)
3963
+ || this.store.countQueuedPrompts(session.localSessionId) > 0)));
3964
+ if (active) {
3965
+ throw new Error(`群内任务仍在执行或有排队消息,暂时不能解散:${active.title ?? active.localSessionId}`);
3966
+ }
3967
+ }
3968
+ dismissibleGroupContextKey(contextKey) {
3969
+ if (isThreadContextKey(contextKey)) {
3970
+ throw new Error("/dismiss 不能在话题中使用,请回到群聊正文后重试。");
3971
+ }
3972
+ const groupContextKey = baseChatContextKey(contextKey);
3973
+ if (!groupContextKey.startsWith("chat_id:") || this.store.getChatContext(groupContextKey)?.chatType !== "group") {
3974
+ throw new Error("/dismiss 仅适用于普通群聊。");
3975
+ }
3976
+ return groupContextKey;
3977
+ }
3474
3978
  resolveSessionReference(contextKey, reference) {
3475
3979
  if (reference === undefined) {
3476
3980
  const previous = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).previousSessionId;
@@ -3770,6 +4274,26 @@ export class ProxySessionController {
3770
4274
  ? "已开启当前群的静音模式。之后只有 @ 机器人的消息会被处理;发送 @机器人 /mute off 可恢复自动响应。"
3771
4275
  : "已关闭当前群的静音模式。机器人将恢复自动响应群消息。");
3772
4276
  }
4277
+ requireControlSession(localSessionId) {
4278
+ const record = this.store.getSession(localSessionId);
4279
+ if (!record || record.status === "closed")
4280
+ throw new Error(`Task not found: ${localSessionId}`);
4281
+ return record;
4282
+ }
4283
+ controlSessionContextKey(record) {
4284
+ return this.outbound.getSessionContextKey(record.localSessionId)
4285
+ ?? (record.lastTurnId ? this.store.getTurnContextKey(record.lastTurnId) : undefined)
4286
+ ?? record.contextKey;
4287
+ }
4288
+ controlSessionReplyTarget(record) {
4289
+ const routed = this.outbound.getSessionReplyTarget(record.localSessionId);
4290
+ if (routed)
4291
+ return routed;
4292
+ const snapshot = record.lastTurnId
4293
+ ? turnViewSnapshot(this.store.getTurnSnapshot(record.lastTurnId))
4294
+ : undefined;
4295
+ return snapshot?.replyTarget;
4296
+ }
3773
4297
  ensureAgent(agentName) {
3774
4298
  const agent = this.config.agents[agentName];
3775
4299
  if (!agent)
@@ -4276,7 +4800,7 @@ function isBotOwnedActiveTurn(record, remote) {
4276
4800
  && record.lastTurnId === remote.lastTurnId;
4277
4801
  }
4278
4802
  function isQueueIndependentCommand(command) {
4279
- if (["stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type))
4803
+ if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "goal", "nosteer"].includes(command.type))
4280
4804
  return true;
4281
4805
  if (command.type === "agent")
4282
4806
  return command.agent === undefined;