@keyou007/agent-bot 0.1.9 → 0.1.11

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 (119) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +22 -10
  3. package/README.zh.md +22 -10
  4. package/assets/agent-bot-logo.png +0 -0
  5. package/dist/acp/AcpSessionManager.js +78 -46
  6. package/dist/acp/AcpSessionManager.js.map +1 -1
  7. package/dist/cli/FeishuAppConfiguration.js +84 -22
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/FeishuAppRegistration.js +12 -9
  10. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  11. package/dist/cli/Initializer.js +7 -6
  12. package/dist/cli/Initializer.js.map +1 -1
  13. package/dist/cli/LocalControlClient.js +3 -2
  14. package/dist/cli/LocalControlClient.js.map +1 -1
  15. package/dist/cli/LocalControlServer.js +2 -1
  16. package/dist/cli/LocalControlServer.js.map +1 -1
  17. package/dist/cli/OptionalAuthorizationSkip.js +27 -6
  18. package/dist/cli/OptionalAuthorizationSkip.js.map +1 -1
  19. package/dist/cli/ServerStarter.js +12 -3
  20. package/dist/cli/ServerStarter.js.map +1 -1
  21. package/dist/cli/SkillRegistry.js +5 -4
  22. package/dist/cli/SkillRegistry.js.map +1 -1
  23. package/dist/cli/VerificationOutput.js +2 -1
  24. package/dist/cli/VerificationOutput.js.map +1 -1
  25. package/dist/cli/help.js +48 -1
  26. package/dist/cli/help.js.map +1 -1
  27. package/dist/cli/i18n.js +27 -0
  28. package/dist/cli/i18n.js.map +1 -0
  29. package/dist/cli/initOptions.js +5 -4
  30. package/dist/cli/initOptions.js.map +1 -1
  31. package/dist/cli/packageVersion.js +2 -1
  32. package/dist/cli/packageVersion.js.map +1 -1
  33. package/dist/cli/profile.js +4 -3
  34. package/dist/cli/profile.js.map +1 -1
  35. package/dist/cli/serverStatus.js +15 -14
  36. package/dist/cli/serverStatus.js.map +1 -1
  37. package/dist/cli/taskChatRoute.js +3 -2
  38. package/dist/cli/taskChatRoute.js.map +1 -1
  39. package/dist/cli.js +165 -107
  40. package/dist/cli.js.map +1 -1
  41. package/dist/codex/AppServerConnection.js +15 -1
  42. package/dist/codex/AppServerConnection.js.map +1 -1
  43. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  44. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  45. package/dist/codex/CodexProcessManager.js +1 -0
  46. package/dist/codex/CodexProcessManager.js.map +1 -1
  47. package/dist/codex/CodexRuntime.js +77 -2
  48. package/dist/codex/CodexRuntime.js.map +1 -1
  49. package/dist/commands/CommandRouter.js +108 -47
  50. package/dist/commands/CommandRouter.js.map +1 -1
  51. package/dist/console/ConsoleTurnPresenter.js +1 -1
  52. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  53. package/dist/feishu/CardRenderer.js +345 -11
  54. package/dist/feishu/CardRenderer.js.map +1 -1
  55. package/dist/feishu/FeishuMessageClient.js +51 -5
  56. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  57. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  58. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  59. package/dist/index.js +18 -17
  60. package/dist/index.js.map +1 -1
  61. package/dist/proxy/ProxySessionController.js +595 -205
  62. package/dist/proxy/ProxySessionController.js.map +1 -1
  63. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  64. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  65. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  66. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  67. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  68. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  69. package/dist/startup/SessionMetadataHydrator.js +2 -2
  70. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  71. package/dist/startup/StartupNotifier.js +1 -0
  72. package/dist/startup/StartupNotifier.js.map +1 -1
  73. package/dist/state/StateStore.js +21 -15
  74. package/dist/state/StateStore.js.map +1 -1
  75. package/dist/supervision/SafeRestartScheduler.js +5 -0
  76. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  77. package/docs/technical-reference.md +27 -6
  78. package/docs/technical-reference.zh.md +27 -6
  79. package/npm-shrinkwrap.json +2 -2
  80. package/package.json +2 -1
  81. package/skills/agent-bot/SKILL.md +19 -9
  82. package/src/acp/AcpSessionManager.ts +73 -54
  83. package/src/cli/FeishuAppConfiguration.ts +130 -18
  84. package/src/cli/FeishuAppRegistration.ts +24 -9
  85. package/src/cli/Initializer.ts +25 -6
  86. package/src/cli/LocalControlClient.ts +9 -2
  87. package/src/cli/LocalControlServer.ts +2 -1
  88. package/src/cli/OptionalAuthorizationSkip.ts +51 -6
  89. package/src/cli/ServerStarter.ts +20 -3
  90. package/src/cli/SkillRegistry.ts +17 -4
  91. package/src/cli/VerificationOutput.ts +4 -1
  92. package/src/cli/help.ts +50 -1
  93. package/src/cli/i18n.ts +38 -0
  94. package/src/cli/initOptions.ts +17 -4
  95. package/src/cli/packageVersion.ts +5 -1
  96. package/src/cli/profile.ts +13 -3
  97. package/src/cli/serverStatus.ts +19 -14
  98. package/src/cli/taskChatRoute.ts +9 -2
  99. package/src/cli.ts +290 -103
  100. package/src/codex/AppServerConnection.ts +21 -1
  101. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  102. package/src/codex/CodexProcessManager.ts +1 -0
  103. package/src/codex/CodexRuntime.ts +91 -2
  104. package/src/commands/CommandRouter.ts +123 -46
  105. package/src/commands/commandTypes.ts +6 -11
  106. package/src/console/ConsoleTurnPresenter.ts +1 -1
  107. package/src/feishu/CardRenderer.ts +418 -11
  108. package/src/feishu/FeishuMessageClient.ts +66 -3
  109. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  110. package/src/index.ts +21 -22
  111. package/src/proxy/ProxySessionController.ts +712 -208
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
@@ -26,6 +26,21 @@ interface PendingRequest {
26
26
 
27
27
  const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
28
28
 
29
+ export class AppServerRequestError extends Error {
30
+ readonly data: unknown;
31
+
32
+ constructor(
33
+ readonly method: string,
34
+ readonly code: number,
35
+ readonly serverMessage: string,
36
+ data?: unknown,
37
+ ) {
38
+ super(`${method} failed: ${serverMessage}`);
39
+ this.name = "AppServerRequestError";
40
+ this.data = data;
41
+ }
42
+ }
43
+
29
44
  export class AppServerConnection {
30
45
  private nextId = 1;
31
46
  private closed = false;
@@ -131,7 +146,12 @@ export class AppServerConnection {
131
146
  this.pending.delete(message.id);
132
147
  if (pending.timeout) clearTimeout(pending.timeout);
133
148
  if ("error" in message) {
134
- pending.reject(new Error(`${pending.method} failed: ${message.error.message}`));
149
+ pending.reject(new AppServerRequestError(
150
+ pending.method,
151
+ message.error.code,
152
+ message.error.message,
153
+ message.error.data,
154
+ ));
135
155
  } else {
136
156
  pending.resolve(message.result);
137
157
  }
@@ -16,12 +16,14 @@ interface ThreadPathRow {
16
16
 
17
17
  interface ThreadSettingsRow {
18
18
  id: string;
19
+ model_provider?: string | null;
19
20
  model: string | null;
20
21
  reasoning_effort: string | null;
21
22
  approval_mode: string | null;
22
23
  }
23
24
 
24
25
  export interface CodexLocalThreadSettings {
26
+ modelProvider?: string;
25
27
  model?: string;
26
28
  reasoningEffort?: string;
27
29
  permissionMode?: PermissionMode;
@@ -79,10 +81,15 @@ export class CodexLocalActivityDetector {
79
81
  fileMustExist: true,
80
82
  });
81
83
  const placeholders = ids.map(() => "?").join(", ");
84
+ const columns = new Set(
85
+ (database.pragma("table_info(threads)") as Array<{ name: string }>).map((column) => column.name),
86
+ );
87
+ const providerColumn = columns.has("model_provider") ? ", model_provider" : "";
82
88
  const rows = database.prepare(
83
- `SELECT id, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
89
+ `SELECT id${providerColumn}, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
84
90
  ).all(...ids) as ThreadSettingsRow[];
85
91
  return new Map(rows.map((row) => [row.id, {
92
+ ...(row.model_provider?.trim() ? { modelProvider: row.model_provider.trim() } : {}),
86
93
  ...(row.model?.trim() ? { model: row.model.trim() } : {}),
87
94
  ...(row.reasoning_effort?.trim() ? { reasoningEffort: row.reasoning_effort.trim() } : {}),
88
95
  ...(row.approval_mode?.trim()
@@ -40,6 +40,7 @@ export class CodexProcessManager implements AppServerClientProvider {
40
40
  });
41
41
  await client.request("initialize", {
42
42
  clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
43
+ capabilities: { experimentalApi: true },
43
44
  });
44
45
  client.notify("initialized", {});
45
46
  return client;
@@ -5,6 +5,7 @@ import type {
5
5
  CreateRuntimeSessionInput,
6
6
  ForkRuntimeSessionInput,
7
7
  ModelOption,
8
+ ModelProviderOption,
8
9
  PermissionMode,
9
10
  RemoteSessionActivity,
10
11
  RemoteSessionPage,
@@ -13,12 +14,14 @@ import type {
13
14
  RuntimeGoal,
14
15
  RuntimeGoalUpdate,
15
16
  RuntimeEvent,
17
+ RuntimeExecutionSettings,
16
18
  RuntimePrompt,
17
19
  RuntimeSession,
18
20
  RuntimeSessionMetadata,
19
21
  } from "../runtime/types.js";
20
22
  import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
21
23
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
24
+ import { AppServerRequestError } from "./AppServerConnection.js";
22
25
  import { mapCodexNotification } from "./CodexEventMapper.js";
23
26
  import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
24
27
  import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
@@ -100,6 +103,7 @@ export class CodexRuntime implements AgentRuntime {
100
103
  const response = await client.request<ThreadResponse>("thread/start", {
101
104
  cwd: input.cwd,
102
105
  model: input.model,
106
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
103
107
  threadSource: "user",
104
108
  ...threadLifecycleParams(input.cwd),
105
109
  ...permissionParams(input.permissionMode),
@@ -122,8 +126,10 @@ export class CodexRuntime implements AgentRuntime {
122
126
  const client = await this.client();
123
127
  const response = await client.request<ThreadResponse>("thread/resume", {
124
128
  threadId: input.remoteSessionId,
129
+ excludeTurns: true,
125
130
  cwd: input.cwd,
126
131
  model: input.model,
132
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
127
133
  ...threadLifecycleParams(input.cwd),
128
134
  ...permissionParams(input.permissionMode),
129
135
  }, SESSION_REQUEST_TIMEOUT_MS);
@@ -135,15 +141,30 @@ export class CodexRuntime implements AgentRuntime {
135
141
 
136
142
  async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
137
143
  const client = await this.client();
138
- const response = await client.request<ThreadResponse>("thread/fork", {
144
+ const forkParams = {
139
145
  threadId: input.remoteSessionId,
140
146
  lastTurnId: input.lastTurnId,
141
147
  cwd: input.cwd,
142
148
  model: input.model,
149
+ ...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
143
150
  threadSource: "user",
144
151
  ...threadLifecycleParams(input.cwd),
145
152
  ...permissionParams(input.permissionMode),
146
- }, FORK_REQUEST_TIMEOUT_MS);
153
+ };
154
+ let response: ThreadResponse;
155
+ try {
156
+ response = await client.request<ThreadResponse>("thread/fork", {
157
+ ...forkParams,
158
+ excludeTurns: true,
159
+ }, FORK_REQUEST_TIMEOUT_MS);
160
+ } catch (error) {
161
+ if (!isUnsupportedExcludeTurnsError(error)) throw error;
162
+ this.logger.warn(
163
+ { error },
164
+ "Codex App Server does not support thread/fork excludeTurns; retrying without it.",
165
+ );
166
+ response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
167
+ }
147
168
  const requestedTitle = normalizeTaskTitle(input.title);
148
169
  if (requestedTitle) {
149
170
  await client.request("thread/name/set", {
@@ -375,6 +396,28 @@ export class CodexRuntime implements AgentRuntime {
375
396
  this.requireSession(sessionId).permissionMode = mode;
376
397
  }
377
398
 
399
+ async setExecutionSettings(
400
+ sessionId: string,
401
+ settings: RuntimeExecutionSettings,
402
+ ): Promise<RuntimeSession> {
403
+ const session = this.requireSession(sessionId);
404
+ const response = await (await this.client()).request<ThreadResponse>("thread/resume", {
405
+ threadId: session.remoteSessionId,
406
+ excludeTurns: true,
407
+ cwd: session.cwd,
408
+ modelProvider: settings.modelProvider,
409
+ model: settings.model,
410
+ ...threadLifecycleParams(session.cwd),
411
+ ...permissionParams(settings.permissionMode),
412
+ }, SESSION_REQUEST_TIMEOUT_MS);
413
+ session.modelProvider = response.modelProvider ?? settings.modelProvider;
414
+ session.model = response.model ?? settings.model;
415
+ session.reasoningEffort = settings.reasoningEffort;
416
+ session.permissionMode = settings.permissionMode;
417
+ session.needsResume = false;
418
+ return session;
419
+ }
420
+
378
421
  async respondToApproval(
379
422
  sessionId: string,
380
423
  requestId: string,
@@ -412,6 +455,33 @@ export class CodexRuntime implements AgentRuntime {
412
455
  }));
413
456
  }
414
457
 
458
+ async listModelProviders(): Promise<ModelProviderOption[]> {
459
+ const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
460
+ "config/read",
461
+ {},
462
+ CONTROL_REQUEST_TIMEOUT_MS,
463
+ );
464
+ const config = response.config ?? {};
465
+ const defaultProvider = stringValue(config.model_provider)?.trim();
466
+ const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
467
+ const providers = new Map<string, ModelProviderOption>();
468
+ for (const [id, value] of Object.entries(configuredProviders)) {
469
+ if (!id.trim()) continue;
470
+ const displayName = isRecord(value) ? stringValue(value.name)?.trim() : undefined;
471
+ providers.set(id, {
472
+ id,
473
+ ...(displayName ? { displayName } : {}),
474
+ ...(id === defaultProvider ? { isDefault: true } : {}),
475
+ });
476
+ }
477
+ if (defaultProvider && !providers.has(defaultProvider)) {
478
+ providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
479
+ }
480
+ return [...providers.values()].sort((left, right) =>
481
+ Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id),
482
+ );
483
+ }
484
+
415
485
  onEvent(listener: (event: RuntimeEvent) => void): () => void {
416
486
  this.listeners.add(listener);
417
487
  return () => this.listeners.delete(listener);
@@ -642,8 +712,10 @@ export class CodexRuntime implements AgentRuntime {
642
712
  if (!session.needsResume) return;
643
713
  await client.request("thread/resume", {
644
714
  threadId: session.remoteSessionId,
715
+ excludeTurns: true,
645
716
  cwd: session.cwd,
646
717
  model: session.model,
718
+ ...(session.modelProvider ? { modelProvider: session.modelProvider } : {}),
647
719
  ...threadLifecycleParams(session.cwd),
648
720
  ...permissionParams(session.permissionMode),
649
721
  }, SESSION_REQUEST_TIMEOUT_MS);
@@ -772,6 +844,7 @@ export class CodexRuntime implements AgentRuntime {
772
844
  title: normalizeTaskTitle(response.thread.name)
773
845
  ?? normalizeTaskTitle(response.thread.preview)
774
846
  ?? input.title,
847
+ modelProvider: input.modelProvider ?? response.modelProvider,
775
848
  model: input.model ?? response.model,
776
849
  reasoningEffort,
777
850
  permissionMode: input.permissionMode,
@@ -835,6 +908,7 @@ export class CodexRuntime implements AgentRuntime {
835
908
 
836
909
  interface ThreadResponse {
837
910
  thread: { id: string; name?: string | null; preview?: string };
911
+ modelProvider?: string;
838
912
  model?: string;
839
913
  reasoningEffort?: string | null;
840
914
  }
@@ -853,6 +927,7 @@ interface CodexThreadSnapshot {
853
927
  name?: string | null;
854
928
  preview?: string;
855
929
  cwd?: string;
930
+ modelProvider?: string;
856
931
  source?: unknown;
857
932
  createdAt?: number;
858
933
  updatedAt?: number;
@@ -933,6 +1008,19 @@ function uniqueStrings(values: string[]): string[] {
933
1008
  return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
934
1009
  }
935
1010
 
1011
+ function isUnsupportedExcludeTurnsError(error: unknown): boolean {
1012
+ if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
1013
+ const data = typeof error.data === "string"
1014
+ ? error.data
1015
+ : error.data === undefined
1016
+ ? ""
1017
+ : JSON.stringify(error.data);
1018
+ const details = `${error.serverMessage} ${data}`;
1019
+ if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
1020
+ return error.code === -32602
1021
+ || /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
1022
+ }
1023
+
936
1024
  function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
937
1025
  const lastTurn = thread.turns?.at(-1);
938
1026
  const lastCompletedTurn = [...(thread.turns ?? [])]
@@ -956,6 +1044,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
956
1044
  cwd: thread.cwd ?? "",
957
1045
  source: codexSourceLabel(thread.source),
958
1046
  status,
1047
+ modelProvider: thread.modelProvider,
959
1048
  createdAt: thread.createdAt,
960
1049
  updatedAt: thread.updatedAt,
961
1050
  recencyAt: thread.recencyAt ?? undefined,
@@ -1,5 +1,37 @@
1
1
  import type { Command } from "./commandTypes.js";
2
2
 
3
+ const COMMAND_NAMES = [
4
+ "agent",
5
+ "fork",
6
+ "forkgroup",
7
+ "goal",
8
+ "help",
9
+ "model",
10
+ "new",
11
+ "newgroup",
12
+ "nosteer",
13
+ "permissions",
14
+ "provider",
15
+ "queue",
16
+ "restart",
17
+ "sessions",
18
+ "status",
19
+ "stop",
20
+ "switch",
21
+ "thinking",
22
+ "title",
23
+ ] as const;
24
+
25
+ type CommandName = (typeof COMMAND_NAMES)[number];
26
+
27
+ const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
28
+ forkgroup: "fg",
29
+ newgroup: "ng",
30
+ nosteer: "ns",
31
+ };
32
+
33
+ const DISABLED_COMMAND_NAMES = new Set(["mode", "modes"]);
34
+
3
35
  export class CommandRouter {
4
36
  parse(text: string): Command {
5
37
  const trimmed = text.trim();
@@ -13,28 +45,20 @@ export class CommandRouter {
13
45
  }
14
46
 
15
47
  const [rawCommand, ...args] = splitArgs(trimmed);
16
- const command = rawCommand.slice(1).toLowerCase();
48
+ const command = resolveCommandName(rawCommand);
17
49
 
18
50
  switch (command) {
19
- case "agents":
20
- return { type: "agent" };
21
51
  case "new":
22
52
  return parseNewCommand(args);
23
53
  case "newgroup":
24
54
  return parseNewGroupCommand(args);
25
55
  case "forkgroup":
26
- return {
27
- type: "forkgroup",
28
- title: args.join(" ").trim() || undefined,
29
- };
56
+ return parseForkGroupCommand(args);
30
57
  case "fork":
31
- if (args.length > 1) throw new Error("/fork 只接受一个可选的任务序号或任务 ID。");
32
- return args[0] ? { type: "fork", sessionId: args[0] } : { type: "fork" };
58
+ return parseForkCommand(args);
33
59
  case "title":
34
60
  if (args.length === 0) throw new Error("请输入新标题,例如:/title 修复会话列表。");
35
61
  return { type: "title", title: args.join(" ") };
36
- case "ask":
37
- return { type: "ask", text: trimmed.slice(rawCommand.length).trim() };
38
62
  case "queue":
39
63
  case "nosteer": {
40
64
  const prompt = trimmed.slice(rawCommand.length).trim();
@@ -47,8 +71,6 @@ export class CommandRouter {
47
71
  return args[0] ? { type: "switch", sessionId: args[0] } : { type: "switch" };
48
72
  case "agent":
49
73
  return args[0] ? { type: "agent", agent: args[0] } : { type: "agent" };
50
- case "use":
51
- return requireArg(args[0], "agent name", (agent) => ({ type: "use", agent, cwd: args[1] }));
52
74
  case "stop":
53
75
  return { type: "stop" };
54
76
  case "status":
@@ -68,47 +90,88 @@ export class CommandRouter {
68
90
  return { type: "goal", action: "set", objective: args.join(" ") };
69
91
  }
70
92
  case "restart":
71
- return { type: "restart" };
72
- case "modes":
73
- return { type: "modes" };
74
- case "mode":
75
- return requireArg(args[0], "mode value", (value) => ({ type: "mode", value }));
93
+ if (args.length === 0) return { type: "restart" };
94
+ if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
95
+ throw new Error("/restart 只接受一个可选的 --force 参数。");
76
96
  case "model":
77
- return { type: "model", model: args[0] };
97
+ return settingsCommand("/model", args, { type: "model" });
98
+ case "provider":
99
+ return settingsCommand("/provider", args, { type: "provider" });
78
100
  case "thinking":
79
- return { type: "thinking", effort: args[0] };
80
- case "permissions": {
81
- const mode = args[0];
82
- if (mode !== undefined && mode !== "auto" && mode !== "confirm") {
83
- throw new Error("权限模式只能是 auto 或 confirm。");
84
- }
85
- return { type: "permissions", mode };
86
- }
101
+ return settingsCommand("/thinking", args, { type: "thinking" });
102
+ case "permissions":
103
+ return settingsCommand("/permissions", args, { type: "permissions" });
87
104
  case "help":
88
105
  return { type: "help" };
89
- default:
90
- throw new Error(`未知命令:${rawCommand}。发送 /help 查看可用命令。`);
91
106
  }
92
107
  }
93
108
  }
94
109
 
95
- function requireArg<T>(value: string | undefined, name: string, create: (value: string) => T): T {
96
- if (!value) {
97
- throw new Error(`Missing ${name}.`);
110
+ function resolveCommandName(rawCommand: string): CommandName {
111
+ const input = rawCommand.slice(1).toLowerCase();
112
+ if (!input) throw unknownCommand(rawCommand);
113
+ if (DISABLED_COMMAND_NAMES.has(input)) throw unknownCommand(rawCommand);
114
+
115
+ const exact = COMMAND_NAMES.find((command) => command === input);
116
+ if (exact) return exact;
117
+
118
+ const matches = COMMAND_NAMES.filter(
119
+ (command) => command.startsWith(input) || COMMAND_INITIALISMS[command] === input,
120
+ );
121
+ if (matches.length === 1) return matches[0]!;
122
+ if (matches.length > 1) {
123
+ throw new Error(
124
+ `命令前缀 ${rawCommand} 不唯一,可匹配:${matches.map((command) => `/${command}`).join("、")}。请输入更长的命令前缀。`,
125
+ );
98
126
  }
127
+ throw unknownCommand(rawCommand);
128
+ }
99
129
 
100
- return create(value);
130
+ function unknownCommand(rawCommand: string): Error {
131
+ return new Error(`未知命令:${rawCommand}。发送 /help 查看可用命令。`);
132
+ }
133
+
134
+ function settingsCommand<T extends Command>(name: string, args: string[], command: T): T {
135
+ if (args.length > 0) {
136
+ throw new Error(`${name} 不接受参数,请在设置卡片中完成选择。`);
137
+ }
138
+ return command;
101
139
  }
102
140
 
103
141
  function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
142
+ const options = parseNewTaskOptions("/new", args, "工作目录", "D:\\dev\\project");
143
+ return {
144
+ type: "new",
145
+ title: options.title,
146
+ cwd: options.cwd,
147
+ ...(options.projectless ? { projectless: true } : {}),
148
+ };
149
+ }
150
+
151
+ function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgroup" }> {
152
+ const options = parseNewTaskOptions("/newgroup", args, "项目目录", "~/dev/project");
153
+ return {
154
+ type: "newgroup",
155
+ title: options.title,
156
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
157
+ ...(options.projectless ? { projectless: true } : {}),
158
+ };
159
+ }
160
+
161
+ function parseNewTaskOptions(
162
+ commandName: "/new" | "/newgroup",
163
+ args: string[],
164
+ directoryLabel: string,
165
+ exampleDirectory: string,
166
+ ): { title?: string; cwd?: string; projectless: boolean } {
104
167
  const titleParts: string[] = [];
105
168
  let cwd: string | undefined;
106
169
  let projectless = false;
107
170
  for (let index = 0; index < args.length; index += 1) {
108
171
  const argument = args[index]!;
109
172
  if (argument === "--nodir") {
110
- if (projectless) throw new Error("/new 只能指定一次 --nodir。");
111
- if (cwd !== undefined) throw new Error("/new 的 --dir 和 --nodir 不能同时使用。");
173
+ if (projectless) throw new Error(`${commandName} 只能指定一次 --nodir。`);
174
+ if (cwd !== undefined) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
112
175
  projectless = true;
113
176
  continue;
114
177
  }
@@ -116,33 +179,47 @@ function parseNewCommand(args: string[]): Extract<Command, { type: "new" }> {
116
179
  titleParts.push(argument);
117
180
  continue;
118
181
  }
119
- if (projectless) throw new Error("/new 的 --dir 和 --nodir 不能同时使用。");
120
- if (cwd !== undefined) throw new Error("/new 只能指定一次 --dir。");
182
+ if (projectless) throw new Error(`${commandName} 的 --dir 和 --nodir 不能同时使用。`);
183
+ if (cwd !== undefined) throw new Error(`${commandName} 只能指定一次 --dir。`);
121
184
  const directory = args[index + 1];
122
- if (!directory || directory === "--dir") {
123
- throw new Error("请在 --dir 后指定工作目录,例如:/new 修复会话列表 --dir D:\\dev\\project。");
185
+ if (!directory || directory === "--dir" || directory === "--nodir") {
186
+ throw new Error(
187
+ `请在 --dir 后指定${directoryLabel},例如:${commandName} 修复会话列表 --dir ${exampleDirectory}。`,
188
+ );
124
189
  }
125
190
  cwd = directory;
126
191
  index += 1;
127
192
  }
128
193
  return {
129
- type: "new",
130
194
  title: titleParts.join(" ").trim() || undefined,
131
195
  cwd,
132
- ...(projectless ? { projectless: true } : {}),
196
+ projectless,
133
197
  };
134
198
  }
135
199
 
136
- function parseNewGroupCommand(args: string[]): Extract<Command, { type: "newgroup" }> {
137
- if (args.includes("--dir")) {
138
- throw new Error("/newgroup 只创建飞书群,不支持 --dir;请在新群中使用 /new --dir <cwd> 创建任务。");
139
- }
200
+ function parseForkCommand(args: string[]): Extract<Command, { type: "fork" }> {
201
+ rejectOptions("/fork", args);
202
+ if (args.length > 1) throw new Error("/fork 只接受一个可选的任务序号或任务 ID。");
140
203
  return {
141
- type: "newgroup",
204
+ type: "fork",
205
+ ...(args[0] ? { sessionId: args[0] } : {}),
206
+ };
207
+ }
208
+
209
+ function parseForkGroupCommand(args: string[]): Extract<Command, { type: "forkgroup" }> {
210
+ rejectOptions("/forkgroup", args);
211
+ return {
212
+ type: "forkgroup",
142
213
  title: args.join(" ").trim() || undefined,
143
214
  };
144
215
  }
145
216
 
217
+ function rejectOptions(command: string, args: string[]): void {
218
+ for (const argument of args) {
219
+ if (argument.startsWith("--")) throw new Error(`${command} 不支持参数:${argument}。`);
220
+ }
221
+ }
222
+
146
223
  function splitArgs(input: string): string[] {
147
224
  const args: string[] = [];
148
225
  let current = "";
@@ -1,28 +1,23 @@
1
- import type { PermissionMode } from "../runtime/types.js";
2
-
3
1
  export type Command =
4
2
  | { type: "shell"; command: string }
5
3
  | { type: "new"; title?: string; cwd?: string; projectless?: boolean }
6
- | { type: "newgroup"; title?: string }
4
+ | { type: "newgroup"; title?: string; cwd?: string; projectless?: boolean }
7
5
  | { type: "forkgroup"; title?: string }
8
6
  | { type: "fork"; sessionId?: string }
9
7
  | { type: "title"; title: string }
10
- | { type: "ask"; text: string }
11
8
  | { type: "nosteer"; text: string }
12
9
  | { type: "sessions"; searchTerm?: string }
13
10
  | { type: "switch"; sessionId?: string }
14
11
  | { type: "agent"; agent?: string }
15
- | { type: "use"; agent: string; cwd?: string }
16
12
  | { type: "stop" }
17
13
  | { type: "status"; sessionId?: string }
18
14
  | { type: "goal"; action: "show" }
19
15
  | { type: "goal"; action: "set" | "edit"; objective: string }
20
16
  | { type: "goal"; action: "pause" | "resume" | "clear" }
21
- | { type: "restart" }
22
- | { type: "modes" }
23
- | { type: "mode"; value: string }
24
- | { type: "model"; model?: string }
25
- | { type: "thinking"; effort?: string }
26
- | { type: "permissions"; mode?: PermissionMode }
17
+ | { type: "restart"; force?: boolean }
18
+ | { type: "model" }
19
+ | { type: "provider" }
20
+ | { type: "thinking" }
21
+ | { type: "permissions" }
27
22
  | { type: "help" }
28
23
  | { type: "prompt"; text: string };
@@ -39,7 +39,7 @@ export class ConsoleTurnPresenter implements TurnPresenter {
39
39
  } else if (event.type === "tool_started") {
40
40
  await this.outbound.sendText(contextKey, `> ${event.tool.title}`);
41
41
  } else if (event.type === "approval_requested") {
42
- await this.outbound.sendText(contextKey, `Approval required: ${event.request.title}. Use Feishu or /permissions auto.`);
42
+ await this.outbound.sendText(contextKey, `Approval required: ${event.request.title}. Use Feishu's Permission settings card.`);
43
43
  } else if (event.type === "turn_completed" && !this.delivered.has(event.turnId)) {
44
44
  this.delivered.add(event.turnId);
45
45
  await this.outbound.sendMarkdown(contextKey, event.finalResponse || "(completed without text)");