@keyou007/agent-bot 0.1.0

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 (212) hide show
  1. package/.env.example +8 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +675 -0
  4. package/README.md +187 -0
  5. package/README.zh.md +187 -0
  6. package/SECURITY.md +13 -0
  7. package/config.example.yaml +56 -0
  8. package/dist/acp/AcpJsonRpcConnection.js +155 -0
  9. package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
  10. package/dist/acp/AcpProcessManager.js +65 -0
  11. package/dist/acp/AcpProcessManager.js.map +1 -0
  12. package/dist/acp/AcpSessionManager.js +164 -0
  13. package/dist/acp/AcpSessionManager.js.map +1 -0
  14. package/dist/acp/acpTypes.js +10 -0
  15. package/dist/acp/acpTypes.js.map +1 -0
  16. package/dist/cli/FeishuAppConfiguration.js +349 -0
  17. package/dist/cli/FeishuAppConfiguration.js.map +1 -0
  18. package/dist/cli/FeishuAppRegistration.js +160 -0
  19. package/dist/cli/FeishuAppRegistration.js.map +1 -0
  20. package/dist/cli/Initializer.js +112 -0
  21. package/dist/cli/Initializer.js.map +1 -0
  22. package/dist/cli/LocalControlClient.js +47 -0
  23. package/dist/cli/LocalControlClient.js.map +1 -0
  24. package/dist/cli/LocalControlServer.js +66 -0
  25. package/dist/cli/LocalControlServer.js.map +1 -0
  26. package/dist/cli/SkillRegistry.js +126 -0
  27. package/dist/cli/SkillRegistry.js.map +1 -0
  28. package/dist/cli/controlProtocol.js +10 -0
  29. package/dist/cli/controlProtocol.js.map +1 -0
  30. package/dist/cli/taskChatRoute.js +23 -0
  31. package/dist/cli/taskChatRoute.js.map +1 -0
  32. package/dist/cli.js +616 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/codex/AppServerConnection.js +131 -0
  35. package/dist/codex/AppServerConnection.js.map +1 -0
  36. package/dist/codex/CodexEventMapper.js +296 -0
  37. package/dist/codex/CodexEventMapper.js.map +1 -0
  38. package/dist/codex/CodexLocalActivityDetector.js +124 -0
  39. package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
  40. package/dist/codex/CodexProcessManager.js +64 -0
  41. package/dist/codex/CodexProcessManager.js.map +1 -0
  42. package/dist/codex/CodexRuntime.js +825 -0
  43. package/dist/codex/CodexRuntime.js.map +1 -0
  44. package/dist/codex/ProjectlessWorkspace.js +74 -0
  45. package/dist/codex/ProjectlessWorkspace.js.map +1 -0
  46. package/dist/codex/protocol.js +10 -0
  47. package/dist/codex/protocol.js.map +1 -0
  48. package/dist/commands/CommandRouter.js +176 -0
  49. package/dist/commands/CommandRouter.js.map +1 -0
  50. package/dist/commands/commandTypes.js +2 -0
  51. package/dist/commands/commandTypes.js.map +1 -0
  52. package/dist/config/loadConfig.js +107 -0
  53. package/dist/config/loadConfig.js.map +1 -0
  54. package/dist/config/paths.js +32 -0
  55. package/dist/config/paths.js.map +1 -0
  56. package/dist/config/schema.js +38 -0
  57. package/dist/config/schema.js.map +1 -0
  58. package/dist/console/ConsoleConnector.js +39 -0
  59. package/dist/console/ConsoleConnector.js.map +1 -0
  60. package/dist/console/ConsoleTurnPresenter.js +60 -0
  61. package/dist/console/ConsoleTurnPresenter.js.map +1 -0
  62. package/dist/feishu/CardRenderer.js +972 -0
  63. package/dist/feishu/CardRenderer.js.map +1 -0
  64. package/dist/feishu/CardUpdateScheduler.js +148 -0
  65. package/dist/feishu/CardUpdateScheduler.js.map +1 -0
  66. package/dist/feishu/ConsoleFeishuClient.js +18 -0
  67. package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
  68. package/dist/feishu/FeishuConnector.js +257 -0
  69. package/dist/feishu/FeishuConnector.js.map +1 -0
  70. package/dist/feishu/FeishuMarkdown.js +37 -0
  71. package/dist/feishu/FeishuMarkdown.js.map +1 -0
  72. package/dist/feishu/FeishuMessageClient.js +459 -0
  73. package/dist/feishu/FeishuMessageClient.js.map +1 -0
  74. package/dist/feishu/FeishuTurnPresenter.js +425 -0
  75. package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
  76. package/dist/feishu/GroupAvatarGenerator.js +86 -0
  77. package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
  78. package/dist/feishu/LocalCardImage.js +13 -0
  79. package/dist/feishu/LocalCardImage.js.map +1 -0
  80. package/dist/feishu/LocalImageMarkdown.js +89 -0
  81. package/dist/feishu/LocalImageMarkdown.js.map +1 -0
  82. package/dist/feishu/contextKey.js +12 -0
  83. package/dist/feishu/contextKey.js.map +1 -0
  84. package/dist/feishu/transport.js +14 -0
  85. package/dist/feishu/transport.js.map +1 -0
  86. package/dist/feishu/types.js +2 -0
  87. package/dist/feishu/types.js.map +1 -0
  88. package/dist/index.js +213 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logging/logger.js +22 -0
  91. package/dist/logging/logger.js.map +1 -0
  92. package/dist/presentation/OutboundRouter.js +127 -0
  93. package/dist/presentation/OutboundRouter.js.map +1 -0
  94. package/dist/presentation/TurnStateReducer.js +307 -0
  95. package/dist/presentation/TurnStateReducer.js.map +1 -0
  96. package/dist/presentation/splitMarkdown.js +341 -0
  97. package/dist/presentation/splitMarkdown.js.map +1 -0
  98. package/dist/presentation/turnViewTypes.js +2 -0
  99. package/dist/presentation/turnViewTypes.js.map +1 -0
  100. package/dist/proxy/ProxySessionController.js +2819 -0
  101. package/dist/proxy/ProxySessionController.js.map +1 -0
  102. package/dist/runtime/AcpRuntimeAdapter.js +271 -0
  103. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
  104. package/dist/runtime/AgentRuntimeRegistry.js +18 -0
  105. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
  106. package/dist/runtime/agentEnvironment.js +11 -0
  107. package/dist/runtime/agentEnvironment.js.map +1 -0
  108. package/dist/runtime/types.js +2 -0
  109. package/dist/runtime/types.js.map +1 -0
  110. package/dist/startup/SessionMetadataHydrator.js +46 -0
  111. package/dist/startup/SessionMetadataHydrator.js.map +1 -0
  112. package/dist/startup/StartupNotifier.js +78 -0
  113. package/dist/startup/StartupNotifier.js.map +1 -0
  114. package/dist/startup/startFeishu.js +6 -0
  115. package/dist/startup/startFeishu.js.map +1 -0
  116. package/dist/state/StateStore.js +893 -0
  117. package/dist/state/StateStore.js.map +1 -0
  118. package/dist/state/migrations.js +129 -0
  119. package/dist/state/migrations.js.map +1 -0
  120. package/dist/supervision/SafeRestartNotifier.js +116 -0
  121. package/dist/supervision/SafeRestartNotifier.js.map +1 -0
  122. package/dist/supervision/SafeRestartScheduler.js +123 -0
  123. package/dist/supervision/SafeRestartScheduler.js.map +1 -0
  124. package/dist/supervision/replacementSupervisor.js +8 -0
  125. package/dist/supervision/replacementSupervisor.js.map +1 -0
  126. package/dist/supervision/restartPolicy.js +16 -0
  127. package/dist/supervision/restartPolicy.js.map +1 -0
  128. package/dist/supervisor.js +94 -0
  129. package/dist/supervisor.js.map +1 -0
  130. package/dist/utils/executeShellCommand.js +76 -0
  131. package/dist/utils/executeShellCommand.js.map +1 -0
  132. package/dist/utils/id.js +5 -0
  133. package/dist/utils/id.js.map +1 -0
  134. package/dist/utils/markdown.js +29 -0
  135. package/dist/utils/markdown.js.map +1 -0
  136. package/dist/utils/spawnCommand.js +21 -0
  137. package/dist/utils/spawnCommand.js.map +1 -0
  138. package/dist/utils/taskTitle.js +10 -0
  139. package/dist/utils/taskTitle.js.map +1 -0
  140. package/docs/technical-reference.md +294 -0
  141. package/docs/technical-reference.zh.md +294 -0
  142. package/npm-shrinkwrap.json +4525 -0
  143. package/package.json +87 -0
  144. package/skills/agent-bot/SKILL.md +141 -0
  145. package/skills/agent-bot/agents/openai.yaml +4 -0
  146. package/src/acp/AcpJsonRpcConnection.ts +212 -0
  147. package/src/acp/AcpProcessManager.ts +87 -0
  148. package/src/acp/AcpSessionManager.ts +227 -0
  149. package/src/acp/acpTypes.ts +99 -0
  150. package/src/cli/FeishuAppConfiguration.ts +473 -0
  151. package/src/cli/FeishuAppRegistration.ts +207 -0
  152. package/src/cli/Initializer.ts +159 -0
  153. package/src/cli/LocalControlClient.ts +48 -0
  154. package/src/cli/LocalControlServer.ts +67 -0
  155. package/src/cli/SkillRegistry.ts +151 -0
  156. package/src/cli/controlProtocol.ts +34 -0
  157. package/src/cli/taskChatRoute.ts +31 -0
  158. package/src/cli.ts +630 -0
  159. package/src/codex/AppServerConnection.ts +159 -0
  160. package/src/codex/CodexEventMapper.ts +351 -0
  161. package/src/codex/CodexLocalActivityDetector.ts +148 -0
  162. package/src/codex/CodexProcessManager.ts +63 -0
  163. package/src/codex/CodexRuntime.ts +1025 -0
  164. package/src/codex/ProjectlessWorkspace.ts +90 -0
  165. package/src/codex/protocol.ts +37 -0
  166. package/src/commands/CommandRouter.ts +179 -0
  167. package/src/commands/commandTypes.ts +28 -0
  168. package/src/config/loadConfig.ts +115 -0
  169. package/src/config/paths.ts +37 -0
  170. package/src/config/schema.ts +46 -0
  171. package/src/console/ConsoleConnector.ts +40 -0
  172. package/src/console/ConsoleTurnPresenter.ts +64 -0
  173. package/src/feishu/CardRenderer.ts +1157 -0
  174. package/src/feishu/CardUpdateScheduler.ts +165 -0
  175. package/src/feishu/ConsoleFeishuClient.ts +22 -0
  176. package/src/feishu/FeishuConnector.ts +277 -0
  177. package/src/feishu/FeishuMarkdown.ts +47 -0
  178. package/src/feishu/FeishuMessageClient.ts +649 -0
  179. package/src/feishu/FeishuTurnPresenter.ts +539 -0
  180. package/src/feishu/GroupAvatarGenerator.ts +102 -0
  181. package/src/feishu/LocalCardImage.ts +13 -0
  182. package/src/feishu/LocalImageMarkdown.ts +92 -0
  183. package/src/feishu/contextKey.ts +14 -0
  184. package/src/feishu/transport.ts +24 -0
  185. package/src/feishu/types.ts +83 -0
  186. package/src/index.ts +228 -0
  187. package/src/logging/logger.ts +28 -0
  188. package/src/presentation/OutboundRouter.ts +205 -0
  189. package/src/presentation/TurnStateReducer.ts +341 -0
  190. package/src/presentation/splitMarkdown.ts +358 -0
  191. package/src/presentation/turnViewTypes.ts +54 -0
  192. package/src/proxy/ProxySessionController.ts +3311 -0
  193. package/src/runtime/AcpRuntimeAdapter.ts +324 -0
  194. package/src/runtime/AgentRuntimeRegistry.ts +20 -0
  195. package/src/runtime/agentEnvironment.ts +14 -0
  196. package/src/runtime/types.ts +215 -0
  197. package/src/startup/SessionMetadataHydrator.ts +46 -0
  198. package/src/startup/StartupNotifier.ts +92 -0
  199. package/src/startup/startFeishu.ts +19 -0
  200. package/src/state/StateStore.ts +1249 -0
  201. package/src/state/migrations.ts +128 -0
  202. package/src/supervision/SafeRestartNotifier.ts +122 -0
  203. package/src/supervision/SafeRestartScheduler.ts +149 -0
  204. package/src/supervision/replacementSupervisor.ts +10 -0
  205. package/src/supervision/restartPolicy.ts +19 -0
  206. package/src/supervisor.ts +106 -0
  207. package/src/utils/executeShellCommand.ts +97 -0
  208. package/src/utils/id.ts +5 -0
  209. package/src/utils/markdown.ts +25 -0
  210. package/src/utils/spawnCommand.ts +35 -0
  211. package/src/utils/taskTitle.ts +9 -0
  212. package/tsconfig.json +19 -0
package/src/cli.ts ADDED
@@ -0,0 +1,630 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import qrcode from "qrcode-terminal";
5
+ import { loadConfig } from "./config/loadConfig.js";
6
+ import { sendControlRequest, isServerRunning } from "./cli/LocalControlClient.js";
7
+ import {
8
+ controlEndpoint,
9
+ type ControlResponse,
10
+ type TaskStatusControlData,
11
+ } from "./cli/controlProtocol.js";
12
+ import {
13
+ initializeAgentBot,
14
+ readFeishuCredentials,
15
+ writeFeishuCredentials,
16
+ type InitializationResult,
17
+ type InitializationStatus,
18
+ } from "./cli/Initializer.js";
19
+ import {
20
+ registerFeishuApp,
21
+ type FeishuAppCredentials,
22
+ type FeishuAppRegistrationChallenge,
23
+ } from "./cli/FeishuAppRegistration.js";
24
+ import {
25
+ ensureFeishuAppConfiguration,
26
+ type EnsureFeishuAppConfigurationResult,
27
+ type FeishuConfigurationChallenge,
28
+ } from "./cli/FeishuAppConfiguration.js";
29
+ import { resolveSystemSkillsRoot, SkillRegistry, type SkillRegistrationStatus } from "./cli/SkillRegistry.js";
30
+ import { taskChatRoute } from "./cli/taskChatRoute.js";
31
+ import { StateStore, type SessionRecord } from "./state/StateStore.js";
32
+
33
+ const args = process.argv.slice(2);
34
+
35
+ void main(args).catch((error: unknown) => {
36
+ process.stderr.write(`错误:${error instanceof Error ? error.message : String(error)}\n`);
37
+ process.exitCode = 1;
38
+ });
39
+
40
+ async function main(input: string[]): Promise<void> {
41
+ const parsed = parseGlobalOptions(input);
42
+ if (parsed.configPath) process.env.AGENT_BOT_CONFIG = parsed.configPath;
43
+ const [command, ...rest] = parsed.args;
44
+ if (!command || command === "help" || command === "--help" || command === "-h") {
45
+ printHelp();
46
+ return;
47
+ }
48
+ if (command === "init") {
49
+ await initCommand(rest, parsed.configPath);
50
+ return;
51
+ }
52
+ if (command === "console") {
53
+ await consoleCommand(rest);
54
+ return;
55
+ }
56
+ if (command === "server") {
57
+ await serverCommand(rest);
58
+ return;
59
+ }
60
+ if (command === "task" || command === "tasks") {
61
+ await taskCommand(rest);
62
+ return;
63
+ }
64
+ if (command === "skill" || command === "skills") {
65
+ skillsCommand(rest);
66
+ return;
67
+ }
68
+ throw new Error(`未知命令:${command}。使用 agent-bot --help 查看帮助。`);
69
+ }
70
+
71
+ type FeishuInitializationStatus = "created" | "existing" | "skipped";
72
+
73
+ interface InitCommandResult extends InitializationResult {
74
+ feishu: {
75
+ status: FeishuInitializationStatus;
76
+ appId?: string;
77
+ configuration?: EnsureFeishuAppConfigurationResult;
78
+ };
79
+ }
80
+
81
+ async function initCommand(input: string[], configPath?: string): Promise<void> {
82
+ const supported = new Set(["--json", "--skip-feishu", "--reconfigure-feishu"]);
83
+ const unsupported = input.filter((value) => !supported.has(value));
84
+ if (unsupported.length > 0) throw new Error(`init 不支持参数:${unsupported.join(" ")}`);
85
+ const json = input.includes("--json");
86
+ const skipFeishu = input.includes("--skip-feishu");
87
+ const reconfigureFeishu = input.includes("--reconfigure-feishu");
88
+ if (skipFeishu && reconfigureFeishu) {
89
+ throw new Error("--skip-feishu 和 --reconfigure-feishu 不能同时使用。");
90
+ }
91
+
92
+ const paths = initializeAgentBot({ configPath });
93
+ if (!json) printInitializationPaths(paths);
94
+
95
+ const existing = readFeishuCredentials(paths.env.path);
96
+ let feishu: InitCommandResult["feishu"];
97
+ if (skipFeishu) {
98
+ feishu = {
99
+ status: "skipped",
100
+ ...(existing.appId ? { appId: existing.appId } : {}),
101
+ };
102
+ } else {
103
+ const controller = new AbortController();
104
+ const onInterrupt = (): void => controller.abort(new Error("已取消初始化。"));
105
+ process.once("SIGINT", onInterrupt);
106
+ try {
107
+ let credentials: FeishuAppCredentials;
108
+ let status: Exclude<FeishuInitializationStatus, "skipped">;
109
+ if (existing.status === "configured" && !reconfigureFeishu) {
110
+ credentials = {
111
+ appId: existing.appId!,
112
+ appSecret: existing.appSecret!,
113
+ };
114
+ status = "existing";
115
+ } else {
116
+ if (existing.status === "incomplete" && !reconfigureFeishu) {
117
+ throw new Error(`检测到不完整的飞书凭证:请补全 ${paths.env.path},或使用 --reconfigure-feishu 重新创建应用。`);
118
+ }
119
+ credentials = await registerFeishuApp({
120
+ signal: controller.signal,
121
+ onVerification: (challenge) => printFeishuVerification(challenge, json),
122
+ });
123
+ writeFeishuCredentials(paths.env.path, credentials);
124
+ status = "created";
125
+ }
126
+
127
+ if (!json) process.stdout.write("\n正在检查飞书应用权限、事件和回调配置...\n");
128
+ const configuration = await ensureFeishuAppConfiguration(credentials, {
129
+ signal: controller.signal,
130
+ onVerification: (challenge) => printFeishuConfigurationVerification(challenge, json),
131
+ });
132
+ feishu = {
133
+ status,
134
+ appId: credentials.appId,
135
+ configuration,
136
+ };
137
+ } finally {
138
+ process.removeListener("SIGINT", onInterrupt);
139
+ }
140
+ }
141
+
142
+ const result: InitCommandResult = { ...paths, feishu };
143
+ if (json) printJson(result);
144
+ else printInitializationResult(result);
145
+ }
146
+
147
+ function skillsCommand(input: string[]): void {
148
+ const [action = "status", ...rest] = input;
149
+ const sourcePath = fileURLToPath(new URL("../skills/agent-bot/", import.meta.url));
150
+ const targetRoot = optionValue(rest, "--target") ?? resolveSystemSkillsRoot();
151
+ const registry = new SkillRegistry(sourcePath, targetRoot);
152
+ const json = rest.includes("--json");
153
+
154
+ if (action === "install" || action === "register") {
155
+ const result = registry.install();
156
+ if (json) printJson(result);
157
+ else process.stdout.write(result.updated
158
+ ? `已注册 Agent Bot Skill:${result.status.targetPath}\n`
159
+ : `Agent Bot Skill 已是最新版本:${result.status.targetPath}\n`);
160
+ return;
161
+ }
162
+ if (action === "uninstall" || action === "unregister") {
163
+ const removed = registry.uninstall();
164
+ if (json) printJson({ removed, targetPath: registry.targetPath });
165
+ else process.stdout.write(removed
166
+ ? `已反注册 Agent Bot Skill:${registry.targetPath}\n`
167
+ : `Agent Bot Skill 尚未注册:${registry.targetPath}\n`);
168
+ return;
169
+ }
170
+ if (action === "status") {
171
+ const status = registry.status();
172
+ if (json) printJson(status);
173
+ else printSkillStatus(status);
174
+ return;
175
+ }
176
+ if (action === "path") {
177
+ const paths = { sourcePath: registry.sourcePath, skillsRoot: registry.skillsRoot, targetPath: registry.targetPath };
178
+ if (json) printJson(paths);
179
+ else {
180
+ process.stdout.write(`内置 Skill:${paths.sourcePath}\n`);
181
+ process.stdout.write(`系统 Skills:${paths.skillsRoot}\n`);
182
+ process.stdout.write(`注册位置:${paths.targetPath}\n`);
183
+ }
184
+ return;
185
+ }
186
+ throw new Error(`未知 skills 命令:${action}`);
187
+ }
188
+
189
+ async function consoleCommand(input: string[]): Promise<void> {
190
+ const config = loadConfig();
191
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
192
+ const force = input.includes("--force");
193
+ if (!force && await isServerRunning(endpoint)) {
194
+ throw new Error("agent-bot server 正在运行。为避免争用同一任务状态,请先停止 server,或明确使用 --force。");
195
+ }
196
+ const entry = fileURLToPath(new URL("./index.js", import.meta.url));
197
+ const result = spawnSync(process.execPath, [entry], {
198
+ cwd: process.cwd(),
199
+ stdio: "inherit",
200
+ env: { ...process.env, AGENT_BOT_CONSOLE_ONLY: "1" },
201
+ });
202
+ if (result.error) throw result.error;
203
+ if (result.status && result.status !== 0) process.exitCode = result.status;
204
+ }
205
+
206
+ async function serverCommand(input: string[]): Promise<void> {
207
+ const [action = "status", ...rest] = input;
208
+ const config = loadConfig();
209
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
210
+ if (action === "status") {
211
+ try {
212
+ const response = await sendControlRequest(endpoint, { action: "health" }, 1_500);
213
+ ensureOk(response);
214
+ if (rest.includes("--json")) printJson(response.data);
215
+ else printServerStatus(response.data);
216
+ } catch {
217
+ if (rest.includes("--json")) printJson({ running: false });
218
+ else process.stdout.write("agent-bot server:未运行\n");
219
+ process.exitCode = 3;
220
+ }
221
+ return;
222
+ }
223
+ if (action === "start") {
224
+ if (await isServerRunning(endpoint)) {
225
+ process.stdout.write("agent-bot server 已在运行。\n");
226
+ return;
227
+ }
228
+ const entry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
229
+ const child = spawn(process.execPath, [entry], {
230
+ cwd: process.cwd(),
231
+ detached: true,
232
+ windowsHide: true,
233
+ stdio: "ignore",
234
+ env: { ...process.env, AGENT_BOT_RESTART_REASON: "通过 agent-bot CLI 启动" },
235
+ });
236
+ child.unref();
237
+ const running = await waitForServer(endpoint, 30_000);
238
+ if (!running) throw new Error("已启动 Supervisor,但 server 未在 30 秒内就绪。请检查日志。");
239
+ process.stdout.write("agent-bot server 已启动。\n");
240
+ return;
241
+ }
242
+ if (action === "stop") {
243
+ printResponse(await sendControlRequest(endpoint, { action: "server_stop" }));
244
+ return;
245
+ }
246
+ if (action === "restart") {
247
+ const immediate = rest.includes("--immediate") || rest.includes("--force");
248
+ const reason = optionValue(rest, "--reason")
249
+ ?? (immediate ? "通过 agent-bot CLI 立即重启" : "通过 agent-bot CLI 安全重启");
250
+ printResponse(await sendControlRequest(endpoint, {
251
+ action: "server_restart",
252
+ mode: immediate ? "immediate" : "safe",
253
+ reason,
254
+ }));
255
+ return;
256
+ }
257
+ throw new Error(`未知 server 命令:${action}`);
258
+ }
259
+
260
+ async function taskCommand(input: string[]): Promise<void> {
261
+ const [action = "list", ...rest] = input;
262
+ const config = loadConfig();
263
+ const store = new StateStore(config.storage.sqlitePath);
264
+ try {
265
+ const allSessions = store.listAllSessions();
266
+ if (action === "prompt" || action === "send") {
267
+ const [reference, ...promptParts] = rest;
268
+ if (!reference) throw new Error(`task ${action} 需要任务序号或任务 ID。`);
269
+ const text = promptParts.join(" ").trim();
270
+ if (!text) throw new Error(`task ${action} 需要要发送的 Prompt。`);
271
+ const session = resolveTask(allSessions, reference);
272
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
273
+ printResponse(await sendControlRequest(endpoint, {
274
+ action: "task_prompt",
275
+ localSessionId: session.localSessionId,
276
+ text,
277
+ }, 60_000));
278
+ return;
279
+ }
280
+ const sessions = filterSessions(allSessions, rest);
281
+ if (action === "list") {
282
+ if (rest.includes("--json")) printJson(sessions);
283
+ else printTaskList(sessions);
284
+ return;
285
+ }
286
+ const reference = rest.find((value) => !value.startsWith("--"));
287
+ if (!reference) throw new Error(`task ${action} 需要任务序号或任务 ID。`);
288
+ const session = resolveTask(sessions, reference);
289
+ if (action === "chat") {
290
+ const route = taskChatRoute(session);
291
+ if (rest.includes("--json")) printJson(route);
292
+ else process.stdout.write(`${route.chatId}\n`);
293
+ return;
294
+ }
295
+ if (action === "status") {
296
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
297
+ let live: TaskStatusControlData | undefined;
298
+ let response: ControlResponse | undefined;
299
+ try {
300
+ response = await sendControlRequest(endpoint, {
301
+ action: "task_status",
302
+ localSessionId: session.localSessionId,
303
+ });
304
+ } catch {
305
+ // The server can be stopped or still run an older control protocol
306
+ // while a safe restart is pending. Preserve the local read-only fallback.
307
+ }
308
+ if (response) {
309
+ ensureOk(response);
310
+ live = response.data as TaskStatusControlData;
311
+ }
312
+ const statusSession = live?.session ?? session;
313
+ const snapshot = live?.snapshot
314
+ ?? (statusSession.lastTurnId ? store.getTurnSnapshot(statusSession.lastTurnId) : undefined);
315
+ const result = { ...statusSession, snapshot, ...(live?.remote ? { remote: live.remote } : {}) };
316
+ if (rest.includes("--json")) printJson(result);
317
+ else printTaskStatus(statusSession, snapshot, live?.remote);
318
+ return;
319
+ }
320
+ const endpoint = controlEndpoint(config.storage.sqlitePath);
321
+ if (action === "stop") {
322
+ printResponse(await sendControlRequest(endpoint, { action: "task_stop", localSessionId: session.localSessionId }));
323
+ return;
324
+ }
325
+ if (action === "title") {
326
+ const referenceIndex = rest.indexOf(reference);
327
+ const titleArgs = rest.slice(referenceIndex + 1);
328
+ const optionIndex = titleArgs.findIndex((value) => value.startsWith("--"));
329
+ const title = titleArgs.slice(0, optionIndex < 0 ? undefined : optionIndex).join(" ").trim();
330
+ if (!title) throw new Error("task title 需要新标题。");
331
+ printResponse(await sendControlRequest(endpoint, {
332
+ action: "task_title",
333
+ localSessionId: session.localSessionId,
334
+ title,
335
+ }));
336
+ return;
337
+ }
338
+ throw new Error(`未知 task 命令:${action}`);
339
+ } finally {
340
+ store.close();
341
+ }
342
+ }
343
+
344
+ function parseGlobalOptions(input: string[]): { configPath?: string; args: string[] } {
345
+ const args = [...input];
346
+ const configIndex = args.indexOf("--config");
347
+ if (configIndex < 0) return { args };
348
+ const configPath = args[configIndex + 1];
349
+ if (!configPath) throw new Error("--config 需要配置文件路径。");
350
+ args.splice(configIndex, 2);
351
+ return { configPath, args };
352
+ }
353
+
354
+ function filterSessions(sessions: SessionRecord[], args: string[]): SessionRecord[] {
355
+ const context = optionValue(args, "--context");
356
+ const status = optionValue(args, "--status");
357
+ return sessions.filter((session) =>
358
+ (!context || session.contextKey === context)
359
+ && (!status || session.status === status || session.lastTurnStatus === status));
360
+ }
361
+
362
+ function resolveTask(sessions: SessionRecord[], reference: string): SessionRecord {
363
+ if (/^\d+$/.test(reference)) {
364
+ const index = Number(reference) - 1;
365
+ const session = sessions[index];
366
+ if (!session) throw new Error(`任务序号超出范围:${reference}`);
367
+ return session;
368
+ }
369
+ const exact = sessions.find((session) =>
370
+ session.localSessionId === reference || session.remoteSessionId === reference || session.acpSessionId === reference);
371
+ if (exact) return exact;
372
+ const matches = sessions.filter((session) =>
373
+ session.localSessionId.startsWith(reference)
374
+ || session.remoteSessionId?.startsWith(reference)
375
+ || session.acpSessionId?.startsWith(reference));
376
+ if (matches.length === 1) return matches[0]!;
377
+ if (matches.length > 1) throw new Error(`任务 ID 前缀不唯一:${reference}`);
378
+ throw new Error(`找不到任务:${reference}`);
379
+ }
380
+
381
+ function printTaskList(sessions: SessionRecord[]): void {
382
+ if (sessions.length === 0) {
383
+ process.stdout.write("没有任务。\n");
384
+ return;
385
+ }
386
+ for (const [index, session] of sessions.entries()) {
387
+ const id = session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId;
388
+ process.stdout.write(`${index + 1}. [${session.status}/${session.lastTurnStatus ?? "-"}] ${session.title ?? "未命名任务"}\n`);
389
+ process.stdout.write(` ${id} · ${session.contextKey} · ${session.updatedAt}\n`);
390
+ }
391
+ }
392
+
393
+ function printTaskStatus(session: SessionRecord, snapshot: unknown, remote?: TaskStatusControlData["remote"]): void {
394
+ process.stdout.write(`标题:${session.title ?? "未命名任务"}\n`);
395
+ process.stdout.write(`任务 ID:${session.remoteSessionId ?? session.acpSessionId ?? session.localSessionId}\n`);
396
+ process.stdout.write(`本地 ID:${session.localSessionId}\n`);
397
+ process.stdout.write(`上下文:${session.contextKey}\n`);
398
+ process.stdout.write(`状态:${session.status} / ${session.lastTurnStatus ?? "-"}\n`);
399
+ process.stdout.write(`目录:${session.cwd}\n`);
400
+ process.stdout.write(`最后轮次:${session.lastTurnId ?? "-"}\n`);
401
+ let displayedFinalResponse: string | undefined;
402
+ if (snapshot && typeof snapshot === "object") {
403
+ const state = snapshot as Record<string, unknown>;
404
+ if (typeof state.durationMs === "number") process.stdout.write(`耗时:${Math.round(state.durationMs / 1_000)}s\n`);
405
+ if (typeof state.totalTokens === "number") process.stdout.write(`Tokens:${state.totalTokens}\n`);
406
+ if (typeof state.totalToolCount === "number") process.stdout.write(`工具调用:${state.totalToolCount}\n`);
407
+ if (typeof state.finalResponse === "string" && state.finalResponse.trim()) displayedFinalResponse = state.finalResponse.trim();
408
+ }
409
+ if (!snapshot && remote?.lastTurnToolCount !== undefined) process.stdout.write(`工具调用:${remote.lastTurnToolCount}\n`);
410
+ displayedFinalResponse = remote?.finalResponse?.trim() || displayedFinalResponse;
411
+ if (displayedFinalResponse) process.stdout.write(`最终结果:\n${displayedFinalResponse}\n`);
412
+ }
413
+
414
+ function printServerStatus(data: unknown): void {
415
+ const value = data && typeof data === "object" ? data as Record<string, unknown> : {};
416
+ const activity = value.activity && typeof value.activity === "object"
417
+ ? value.activity as Record<string, unknown>
418
+ : {};
419
+ process.stdout.write("agent-bot server:运行中\n");
420
+ process.stdout.write(`PID:${value.pid ?? "-"}\n`);
421
+ process.stdout.write(`启动时间:${value.startedAt ?? "-"}\n`);
422
+ process.stdout.write(`Supervisor:${value.supervised ? "已启用" : "未启用"}\n`);
423
+ process.stdout.write(`运行任务:${activity.runningSessions ?? 0}\n`);
424
+ process.stdout.write(`待投递结果:${activity.pendingFinalDeliveries ?? 0}\n`);
425
+ process.stdout.write(`安全重启:${value.safeRestartScheduled ? `等待中(${value.safeRestartReason ?? "未注明原因"})` : "未安排"}\n`);
426
+ }
427
+
428
+ function printSkillStatus(status: SkillRegistrationStatus): void {
429
+ process.stdout.write(`Agent Bot Skill:${status.registered ? "已注册" : "未注册"}\n`);
430
+ process.stdout.write(`系统目录:${status.skillsRoot}\n`);
431
+ process.stdout.write(`注册位置:${status.targetPath}\n`);
432
+ if (status.registered) {
433
+ process.stdout.write(`归属:${status.managed ? "由 Agent Bot 管理" : "外部目录(不会覆盖或删除)"}\n`);
434
+ process.stdout.write(`版本:${status.upToDate ? "最新" : "需要更新"}\n`);
435
+ }
436
+ }
437
+
438
+ function printInitializationPaths(result: InitializationResult): void {
439
+ process.stdout.write("正在准备 Agent Bot 用户环境...\n");
440
+ process.stdout.write(`用户目录:${result.home.path}(${initializationStatusLabel(result.home.status)})\n`);
441
+ process.stdout.write(`配置文件:${result.config.path}(${initializationStatusLabel(result.config.status)})\n`);
442
+ process.stdout.write(`环境文件:${result.env.path}(${initializationStatusLabel(result.env.status)})\n`);
443
+ process.stdout.write(`数据目录:${result.data.path}(${initializationStatusLabel(result.data.status)})\n`);
444
+ process.stdout.write(`日志目录:${result.logs.path}(${initializationStatusLabel(result.logs.status)})\n`);
445
+ }
446
+
447
+ function printInitializationResult(result: InitCommandResult): void {
448
+ process.stdout.write("\nAgent Bot 初始化完成。\n");
449
+ if (result.feishu.status === "created") {
450
+ process.stdout.write(`飞书应用:已创建并保存凭证(${result.feishu.appId})\n`);
451
+ } else if (result.feishu.status === "existing") {
452
+ process.stdout.write(`飞书应用:已配置,未修改(${result.feishu.appId})\n`);
453
+ } else {
454
+ process.stdout.write("飞书应用:已跳过,可使用 Console 模式或稍后重新运行 init。\n");
455
+ }
456
+ if (result.feishu.configuration?.status === "updated") {
457
+ const added = result.feishu.configuration.added;
458
+ process.stdout.write(
459
+ `飞书配置:已补齐 ${added.scopes.length} 项权限、${added.events.length} 项事件、${added.callbacks.length} 项回调\n`,
460
+ );
461
+ } else if (result.feishu.configuration?.status === "partial") {
462
+ const configuration = result.feishu.configuration;
463
+ const addedCount =
464
+ configuration.added.scopes.length + configuration.added.events.length + configuration.added.callbacks.length;
465
+ if (addedCount > 0) {
466
+ process.stdout.write(
467
+ `飞书配置:核心能力已就绪,已补齐 ${configuration.added.scopes.length} 项权限、${configuration.added.events.length} 项事件、${configuration.added.callbacks.length} 项回调\n`,
468
+ );
469
+ } else {
470
+ process.stdout.write("飞书配置:核心能力已就绪\n");
471
+ }
472
+ printOptionalFeishuConfigurationWarnings(configuration.remaining);
473
+ } else if (result.feishu.configuration?.status === "ready") {
474
+ process.stdout.write("飞书配置:权限、事件和回调均已就绪\n");
475
+ }
476
+ process.stdout.write(`配置文件:${result.config.path}\n`);
477
+ process.stdout.write("下一步:运行 agent-bot server start。\n");
478
+ }
479
+
480
+ function printFeishuVerification(challenge: FeishuAppRegistrationChallenge, json: boolean): void {
481
+ process.stderr.write("\n请使用飞书扫码,或在浏览器中打开下面的链接创建机器人应用:\n\n");
482
+ process.stderr.write(`${challenge.verificationUrl}\n\n`);
483
+ if (!json) {
484
+ qrcode.generate(challenge.verificationUrl, { small: true }, (output) => {
485
+ process.stderr.write(`${output}\n`);
486
+ });
487
+ }
488
+ process.stderr.write(`链接约在 ${Math.ceil(challenge.expiresIn / 60)} 分钟后过期,正在等待确认...\n`);
489
+ }
490
+
491
+ function printFeishuConfigurationVerification(
492
+ challenge: FeishuConfigurationChallenge,
493
+ json: boolean,
494
+ ): void {
495
+ process.stderr.write("\n飞书应用还缺少以下配置:\n");
496
+ if (challenge.missing.scopes.length > 0) {
497
+ process.stderr.write(`权限:${challenge.missing.scopes.join(", ")}\n`);
498
+ }
499
+ if (challenge.missing.events.length > 0) {
500
+ process.stderr.write(`事件:${challenge.missing.events.join(", ")}\n`);
501
+ }
502
+ if (challenge.missing.callbacks.length > 0) {
503
+ process.stderr.write(`回调:${challenge.missing.callbacks.join(", ")}\n`);
504
+ }
505
+ process.stderr.write("\n请使用飞书扫码,或在浏览器中打开下面的链接补齐配置:\n\n");
506
+ process.stderr.write(`${challenge.verificationUrl}\n\n`);
507
+ if (!json) {
508
+ qrcode.generate(challenge.verificationUrl, { small: true }, (output) => {
509
+ process.stderr.write(`${output}\n`);
510
+ });
511
+ }
512
+ if (challenge.blocking) {
513
+ process.stderr.write("正在等待核心权限和消息事件发布生效...\n");
514
+ } else {
515
+ process.stderr.write("以上配置不影响基础消息收发;初始化将继续,可稍后打开链接授权。\n");
516
+ }
517
+ }
518
+
519
+ function printOptionalFeishuConfigurationWarnings(missing: FeishuConfigurationChallenge["missing"]): void {
520
+ process.stdout.write("提醒:以下非核心配置尚未生效,部分功能可能不可用。\n");
521
+ if (missing.scopes.length > 0) {
522
+ process.stdout.write(`未生效权限:${missing.scopes.join(", ")}\n`);
523
+ }
524
+ if (missing.events.length > 0) {
525
+ process.stdout.write(`未生效事件:${missing.events.join(", ")}\n`);
526
+ }
527
+ if (missing.callbacks.length > 0) {
528
+ process.stdout.write(`未生效回调:${missing.callbacks.join(", ")}\n`);
529
+ }
530
+ for (const feature of optionalFeishuFeatureWarnings(missing)) {
531
+ process.stdout.write(`可能缺失功能:${feature}\n`);
532
+ }
533
+ }
534
+
535
+ function optionalFeishuFeatureWarnings(missing: FeishuConfigurationChallenge["missing"]): string[] {
536
+ const scopes = new Set(missing.scopes);
537
+ const events = new Set(missing.events);
538
+ const callbacks = new Set(missing.callbacks);
539
+ const warnings: string[] = [];
540
+ if (scopes.has("im:chat:create")) {
541
+ warnings.push("无法使用 /newgroup 和 /forkgroup 创建飞书群");
542
+ }
543
+ if (scopes.has("im:chat:read") || events.has("im.chat.updated_v1")) {
544
+ warnings.push("修改飞书群名称时无法同步 Agent Bot 任务标题");
545
+ }
546
+ if (scopes.has("im:message.reactions:write_only")) {
547
+ warnings.push("无法用表情显示消息处理状态");
548
+ }
549
+ if (scopes.has("im:message:readonly")) {
550
+ warnings.push("可能无法读取用户消息中的图片");
551
+ }
552
+ if (scopes.has("im:resource")) {
553
+ warnings.push("可能无法上传图片、发送本地图片或设置群头像");
554
+ }
555
+ if (scopes.has("im:message:update")) {
556
+ warnings.push("可能无法更新已发送的进度卡片");
557
+ }
558
+ if (callbacks.has("card.action.trigger")) {
559
+ warnings.push("卡片按钮和交互操作不可用");
560
+ }
561
+ return warnings;
562
+ }
563
+
564
+ function initializationStatusLabel(status: InitializationStatus): string {
565
+ if (status === "created") return "已创建";
566
+ return "已存在,未修改";
567
+ }
568
+
569
+ function printResponse(response: ControlResponse): void {
570
+ ensureOk(response);
571
+ process.stdout.write(`${response.message ?? "操作成功。"}\n`);
572
+ }
573
+
574
+ function ensureOk(response: ControlResponse): void {
575
+ if (!response.ok) throw new Error(response.message ?? "Agent Bot 控制操作失败。");
576
+ }
577
+
578
+ function optionValue(args: string[], name: string): string | undefined {
579
+ const index = args.indexOf(name);
580
+ return index >= 0 ? args[index + 1] : undefined;
581
+ }
582
+
583
+ async function waitForServer(endpoint: string, timeoutMs: number): Promise<boolean> {
584
+ const deadline = Date.now() + timeoutMs;
585
+ while (Date.now() < deadline) {
586
+ if (await isServerRunning(endpoint)) return true;
587
+ await new Promise((resolve) => setTimeout(resolve, 200));
588
+ }
589
+ return false;
590
+ }
591
+
592
+ function printJson(value: unknown): void {
593
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
594
+ }
595
+
596
+ function printHelp(): void {
597
+ process.stdout.write(`Agent Bot 命令行工具
598
+
599
+ 用法:
600
+ agent-bot [--config <path>] init [--json] [--skip-feishu | --reconfigure-feishu]
601
+ agent-bot [--config <path>] console [--force]
602
+ agent-bot [--config <path>] server status [--json]
603
+ agent-bot [--config <path>] server start
604
+ agent-bot [--config <path>] server stop
605
+ agent-bot [--config <path>] server restart [--safe | --immediate] [--reason <text>]
606
+ agent-bot [--config <path>] task list [--context <key>] [--status <status>] [--json]
607
+ agent-bot [--config <path>] task chat <序号|任务ID> [--json]
608
+ agent-bot [--config <path>] task status <序号|任务ID> [--json]
609
+ agent-bot [--config <path>] task stop <序号|任务ID>
610
+ agent-bot [--config <path>] task title <序号|任务ID> <新标题>
611
+ agent-bot [--config <path>] task prompt <序号|任务ID> <prompt>
612
+ agent-bot skills status [--json] [--target <skills目录>]
613
+ agent-bot skills install|register [--json] [--target <skills目录>]
614
+ agent-bot skills uninstall|unregister [--json] [--target <skills目录>]
615
+ agent-bot skills path [--json] [--target <skills目录>]
616
+
617
+ 说明:
618
+ 默认用户目录是 ~/.agent-bot;配置为 ~/.agent-bot/config.yaml,环境变量为 ~/.agent-bot/.env。
619
+ 可用 AGENT_BOT_HOME 修改用户目录,或用 --config 指向某个配置文件。
620
+ init 创建用户目录和运行文件;创建或读取飞书凭证后会审计并补齐已发布的权限、事件和回调。
621
+ 仅核心消息能力缺失会阻塞初始化;非核心配置缺失只提醒受影响功能。
622
+ --skip-feishu 跳过飞书创建及配置审计并保留 Console 模式;--reconfigure-feishu 明确替换已有飞书凭证。
623
+ server restart 默认执行安全重启;等待全部任务完成、结果投递完成且连续 15 秒无新消息。
624
+ --immediate(或 --force)跳过空闲等待并立即重启 worker。
625
+ task 序号来自 task list 当前排序;任务管理操作通过运行中 server 执行。
626
+ task chat 从本地任务路由中读取飞书会话 ID;话题任务使用 --json 可同时查看 threadId。
627
+ task prompt 不切换任务;机器人先在原会话显示 Prompt,再提交任务,响应继续发送到该会话。
628
+ skills 默认注册到系统通用 ~/.agents/skills 目录;AGENT_BOT_SKILLS_DIR 可修改默认目录。
629
+ `);
630
+ }