@ai-setting/roy-agent-cli 1.0.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 (158) hide show
  1. package/README.md +126 -0
  2. package/dist/bin/roy.js +127297 -0
  3. package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
  4. package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
  5. package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
  6. package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
  7. package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
  8. package/package.json +91 -0
  9. package/src/bin/roy.ts +12 -0
  10. package/src/cli.ts +101 -0
  11. package/src/commands/act.ts +480 -0
  12. package/src/commands/commands-add.ts +110 -0
  13. package/src/commands/commands-dirs.ts +70 -0
  14. package/src/commands/commands-info.ts +90 -0
  15. package/src/commands/commands-list.ts +161 -0
  16. package/src/commands/commands-remove.ts +147 -0
  17. package/src/commands/commands.ts +55 -0
  18. package/src/commands/config/config-service.test.ts +449 -0
  19. package/src/commands/config/config-service.ts +312 -0
  20. package/src/commands/config/deep-merge.test.ts +168 -0
  21. package/src/commands/config/deep-merge.ts +63 -0
  22. package/src/commands/config/export.ts +97 -0
  23. package/src/commands/config/filter-history-e2e.test.ts +141 -0
  24. package/src/commands/config/import-preserve-refs.test.ts +212 -0
  25. package/src/commands/config/import.ts +119 -0
  26. package/src/commands/config/index.ts +35 -0
  27. package/src/commands/config/list.ts +281 -0
  28. package/src/commands/config/roy-config-e2e.test.ts +297 -0
  29. package/src/commands/config/types.ts +54 -0
  30. package/src/commands/debug/index.ts +38 -0
  31. package/src/commands/debug/log.test.ts +233 -0
  32. package/src/commands/debug/log.ts +123 -0
  33. package/src/commands/debug/span.test.ts +297 -0
  34. package/src/commands/debug/span.ts +211 -0
  35. package/src/commands/debug/trace.test.ts +254 -0
  36. package/src/commands/debug/trace.ts +140 -0
  37. package/src/commands/eventsource/add.ts +133 -0
  38. package/src/commands/eventsource/index.ts +48 -0
  39. package/src/commands/eventsource/list.ts +194 -0
  40. package/src/commands/eventsource/remove.ts +95 -0
  41. package/src/commands/eventsource/start.ts +103 -0
  42. package/src/commands/eventsource/status.ts +185 -0
  43. package/src/commands/eventsource/stop.ts +89 -0
  44. package/src/commands/index.ts +22 -0
  45. package/src/commands/input-handler.test.ts +76 -0
  46. package/src/commands/input-handler.ts +43 -0
  47. package/src/commands/interactive-esc.test.ts +254 -0
  48. package/src/commands/interactive.shutdown.test.ts +122 -0
  49. package/src/commands/interactive.test.ts +221 -0
  50. package/src/commands/interactive.ts +1015 -0
  51. package/src/commands/lsp/check.ts +92 -0
  52. package/src/commands/lsp/index.ts +32 -0
  53. package/src/commands/lsp/install.ts +126 -0
  54. package/src/commands/lsp/list.ts +64 -0
  55. package/src/commands/mcp/index.ts +27 -0
  56. package/src/commands/mcp/list.ts +116 -0
  57. package/src/commands/mcp/reload.ts +70 -0
  58. package/src/commands/mcp/tools.ts +121 -0
  59. package/src/commands/memory/extract-e2e.test.ts +388 -0
  60. package/src/commands/memory/index.ts +11 -0
  61. package/src/commands/memory/memory-simplified.test.ts +58 -0
  62. package/src/commands/memory/memory.ts +25 -0
  63. package/src/commands/memory/organize.ts +300 -0
  64. package/src/commands/memory/recall.test.ts +120 -0
  65. package/src/commands/memory/recall.ts +88 -0
  66. package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
  67. package/src/commands/memory/record-prompt-component.test.ts +343 -0
  68. package/src/commands/memory/record.test.ts +92 -0
  69. package/src/commands/memory/record.ts +332 -0
  70. package/src/commands/plugin.test.ts +292 -0
  71. package/src/commands/plugin.ts +267 -0
  72. package/src/commands/sessions/active.ts +96 -0
  73. package/src/commands/sessions/add-message.ts +96 -0
  74. package/src/commands/sessions/checkpoints.ts +154 -0
  75. package/src/commands/sessions/compact.test.ts +215 -0
  76. package/src/commands/sessions/compact.ts +269 -0
  77. package/src/commands/sessions/delete.ts +236 -0
  78. package/src/commands/sessions/get.ts +165 -0
  79. package/src/commands/sessions/grep.ts +233 -0
  80. package/src/commands/sessions/index.ts +95 -0
  81. package/src/commands/sessions/list.ts +210 -0
  82. package/src/commands/sessions/messages.test.ts +333 -0
  83. package/src/commands/sessions/messages.ts +248 -0
  84. package/src/commands/sessions/mock.ts +194 -0
  85. package/src/commands/sessions/new.ts +82 -0
  86. package/src/commands/sessions/rename.ts +98 -0
  87. package/src/commands/shared/event-handler.ts +213 -0
  88. package/src/commands/shared/event-message-formatter.ts +295 -0
  89. package/src/commands/shared/index.ts +11 -0
  90. package/src/commands/shared/query-executor.test.ts +434 -0
  91. package/src/commands/shared/query-executor.ts +324 -0
  92. package/src/commands/shared/repl-engine.test.ts +354 -0
  93. package/src/commands/shared/session-manager.test.ts +212 -0
  94. package/src/commands/shared/session-manager.ts +114 -0
  95. package/src/commands/skills/get.ts +90 -0
  96. package/src/commands/skills/index.ts +39 -0
  97. package/src/commands/skills/list.ts +129 -0
  98. package/src/commands/skills/reload.ts +59 -0
  99. package/src/commands/skills/search.ts +132 -0
  100. package/src/commands/skills/show-config.ts +93 -0
  101. package/src/commands/tasks/complete.ts +92 -0
  102. package/src/commands/tasks/create.ts +118 -0
  103. package/src/commands/tasks/delete.ts +86 -0
  104. package/src/commands/tasks/get.ts +116 -0
  105. package/src/commands/tasks/index.ts +53 -0
  106. package/src/commands/tasks/list.ts +140 -0
  107. package/src/commands/tasks/operations.ts +120 -0
  108. package/src/commands/tasks/update.ts +122 -0
  109. package/src/commands/tools/exec-tool.ts +128 -0
  110. package/src/commands/tools/get.ts +114 -0
  111. package/src/commands/tools/index.ts +35 -0
  112. package/src/commands/tools/list.ts +107 -0
  113. package/src/commands/tools/shared/index.ts +7 -0
  114. package/src/commands/tools/shared/schema-helper.ts +111 -0
  115. package/src/commands/workflow/commands/add.ts +315 -0
  116. package/src/commands/workflow/commands/get.ts +193 -0
  117. package/src/commands/workflow/commands/list.ts +137 -0
  118. package/src/commands/workflow/commands/nodes.ts +528 -0
  119. package/src/commands/workflow/commands/remove.ts +94 -0
  120. package/src/commands/workflow/commands/run.ts +398 -0
  121. package/src/commands/workflow/commands/status.ts +147 -0
  122. package/src/commands/workflow/commands/stop.ts +91 -0
  123. package/src/commands/workflow/commands/update.ts +130 -0
  124. package/src/commands/workflow/commands/validate.ts +139 -0
  125. package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
  126. package/src/commands/workflow/index.ts +65 -0
  127. package/src/commands/workflow/renderers.ts +358 -0
  128. package/src/commands/workflow/validators/index.ts +8 -0
  129. package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
  130. package/src/commands/workflow/validators/node-validator.ts +125 -0
  131. package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
  132. package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
  133. package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
  134. package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
  135. package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
  136. package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
  137. package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
  138. package/src/commands/workflow/validators/types.ts +78 -0
  139. package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
  140. package/src/commands/workflow/validators/workflow-validator.ts +320 -0
  141. package/src/index.ts +19 -0
  142. package/src/plugin/apply.ts +103 -0
  143. package/src/plugin/discover.ts +219 -0
  144. package/src/plugin/index.ts +45 -0
  145. package/src/plugin/registry.ts +272 -0
  146. package/src/plugin/types.ts +165 -0
  147. package/src/services/context-handler.service.test.ts +501 -0
  148. package/src/services/context-handler.service.ts +372 -0
  149. package/src/services/environment.service.commands-prompt.test.ts +167 -0
  150. package/src/services/environment.service.ts +656 -0
  151. package/src/services/output.service.test.ts +92 -0
  152. package/src/services/output.service.ts +122 -0
  153. package/src/services/quiet-mode.service.test.ts +114 -0
  154. package/src/services/quiet-mode.service.ts +81 -0
  155. package/src/services/stream-output.service.test.ts +214 -0
  156. package/src/services/stream-output.service.ts +323 -0
  157. package/src/util/which.test.ts +101 -0
  158. package/src/util/which.ts +55 -0
@@ -0,0 +1,1015 @@
1
+ /**
2
+ * @fileoverview Interactive Command
3
+ *
4
+ * 交互式多轮对话命令 - 复用 act 逻辑
5
+ *
6
+ * 提供 REPL 风格的多轮对话体验
7
+ *
8
+ * 特性:
9
+ * - 用户输入与 Assistant 输出颜色区分
10
+ * - Esc 按键:中断流式输出 + 恢复输入(执行中有效,空闲时无操作)
11
+ * - Ctrl+C:任何时候都退出程序
12
+ * - /abort 命令:中断流式输出(与 Esc 等效)
13
+ */
14
+
15
+ import { CommandModule } from "yargs";
16
+ import * as readline from "readline";
17
+ import chalk from "chalk";
18
+ import { BaseEnvironment } from "@ai-setting/roy-agent-core";
19
+ import { EnvironmentService } from "../services/environment.service";
20
+ import { OutputService } from "../services/output.service";
21
+ import { SessionManager } from "./shared/session-manager";
22
+ import { QueryExecutor } from "./shared/query-executor";
23
+ import { EventHandler } from "./shared/event-handler";
24
+ import { EventMessageFormatter } from "./shared/event-message-formatter";
25
+ import { CliQuietModeService } from "../services/quiet-mode.service";
26
+ import {
27
+ COLORS,
28
+ abortStream,
29
+ resetStreamAbort
30
+ } from "../services/stream-output.service";
31
+ import { createLogger, getTracerProvider } from "@ai-setting/roy-agent-core";
32
+ import { InputHandler } from "./input-handler";
33
+
34
+ // 创建 cli 模块日志器
35
+ const logger = createLogger("cli:interactive");
36
+
37
+ export interface InteractiveOptions {
38
+ session?: string;
39
+ continue?: boolean;
40
+ config?: string;
41
+ reasoning?: boolean;
42
+ toolCalls?: boolean;
43
+ toolResults?: boolean;
44
+ eventSource?: string; // 启动时一并启动的事件源 ID
45
+ plugins?: string[]; // 启用的 coder 插件列表,如 ["lsp", "linter", "typecheck"]
46
+ }
47
+
48
+ // yargs 参数别名映射
49
+ export interface ParsedInteractiveOptions {
50
+ session?: string;
51
+ continue?: boolean;
52
+ config?: string;
53
+ reasoning?: boolean;
54
+ toolCalls?: boolean;
55
+ toolResults?: boolean;
56
+ eventSource?: string;
57
+ plugins?: string[];
58
+ }
59
+
60
+ interface REPLStatus {
61
+ sessionId: string;
62
+ sessionTitle: string;
63
+ messageCount: number;
64
+ tokenCount: number;
65
+ }
66
+
67
+ interface REPLOptions {
68
+ sessionId: string;
69
+ sessionTitle: string;
70
+ onExecute: (message: string, traceId?: string) => Promise<string>;
71
+ onStatus: () => REPLStatus | Promise<REPLStatus>;
72
+ onSwitchSession: (sessionId: string) => Promise<{ sessionId: string; sessionTitle: string }>;
73
+ onCompact: () => Promise<void>;
74
+ onShutdown: () => Promise<void>; // shutdown 回调,用于清理资源
75
+ }
76
+
77
+ // 用户输入提示符(带颜色)
78
+ const USER_PROMPT = COLORS.userInput("❯ ");
79
+
80
+ /**
81
+ * REPL - 简化的 REPL 实现
82
+ */
83
+ export class REPL {
84
+ private rl: readline.Interface;
85
+ private options: REPLOptions;
86
+ private isExecuting = false;
87
+ private commandRegistry: Map<string, () => Promise<void>> = new Map();
88
+ private isShuttingDown = false;
89
+ private env: BaseEnvironment | null = null;
90
+ private inputHandler: InputHandler;
91
+
92
+ constructor(options: REPLOptions, env?: BaseEnvironment) {
93
+ this.options = options;
94
+ this.env = env ?? null;
95
+ this.inputHandler = new InputHandler();
96
+
97
+ this.rl = readline.createInterface({
98
+ input: process.stdin,
99
+ output: process.stdout,
100
+ prompt: USER_PROMPT,
101
+ historySize: 100,
102
+ });
103
+
104
+ // 注册默认命令
105
+ this.registerDefaultCommands();
106
+ }
107
+
108
+ /**
109
+ * 设置环境引用(用于获取 AgentComponent 等组件)
110
+ */
111
+ setEnvironment(env: BaseEnvironment): void {
112
+ this.env = env;
113
+ }
114
+
115
+ /**
116
+ * 执行优雅关闭
117
+ * 调用 onShutdown 回调并关闭 readline
118
+ */
119
+ async shutdown(): Promise<void> {
120
+ if (this.isShuttingDown) {
121
+ return;
122
+ }
123
+ this.isShuttingDown = true;
124
+
125
+ // 关闭 readline(不再接受输入)
126
+ this.rl.close();
127
+
128
+ // 调用 shutdown 回调执行清理
129
+ await this.options.onShutdown();
130
+ }
131
+
132
+ private registerDefaultCommands(): void {
133
+ this.commandRegistry.set("exit", async () => {
134
+ await this.shutdown();
135
+ process.exit(0);
136
+ });
137
+ this.commandRegistry.set("quit", async () => {
138
+ await this.shutdown();
139
+ process.exit(0);
140
+ });
141
+ this.commandRegistry.set("q", async () => {
142
+ await this.shutdown();
143
+ process.exit(0);
144
+ });
145
+
146
+ this.commandRegistry.set("help", async () => this.printHelp());
147
+ this.commandRegistry.set("h", async () => this.printHelp());
148
+
149
+ this.commandRegistry.set("status", async () => this.printStatus());
150
+ this.commandRegistry.set("st", async () => this.printStatus());
151
+
152
+ this.commandRegistry.set("clear", async () => {
153
+ console.clear();
154
+ this.printHeader();
155
+ });
156
+ this.commandRegistry.set("cls", async () => {
157
+ console.clear();
158
+ this.printHeader();
159
+ });
160
+
161
+ this.commandRegistry.set("sessions", async () => {
162
+ this.printSessionsHelp();
163
+ });
164
+ this.commandRegistry.set("s", async () => this.printSessionsHelp());
165
+
166
+ this.commandRegistry.set("compact", async () => {
167
+ await this.options.onCompact();
168
+ });
169
+ this.commandRegistry.set("c", async () => {
170
+ await this.options.onCompact();
171
+ });
172
+
173
+ // 中断当前请求
174
+ this.commandRegistry.set("abort", async () => {
175
+ if (this.isExecuting) {
176
+ console.log(`\n${COLORS.system("[正在中断...]")}`);
177
+
178
+ // 1. 中断底层 LLM 调用
179
+ if (this.env) {
180
+ const agentComponent = this.env.getComponent("agent") as any;
181
+ if (agentComponent?.abort) {
182
+ agentComponent.abort("default");
183
+ logger.debug("[/abort] AgentComponent.abort('default') called");
184
+ }
185
+ }
186
+
187
+ // 2. 中断流式输出显示
188
+ abortStream();
189
+
190
+ // 3. 重置 isExecuting 状态
191
+ this.isExecuting = false;
192
+
193
+ console.log(`${COLORS.system("[已中断,可以继续输入]")}`);
194
+ } else {
195
+ console.log(`\n${COLORS.system("[没有正在执行的请求]")}`);
196
+ }
197
+ });
198
+ }
199
+
200
+ async start(): Promise<void> {
201
+ this.printHeader();
202
+
203
+ // 设置 Esc 按键监听
204
+ readline.emitKeypressEvents(process.stdin);
205
+
206
+ if (process.stdin.isTTY) {
207
+ process.stdin.setRawMode?.(true);
208
+ }
209
+
210
+ // 监听 keypress 事件
211
+ process.stdin.on("keypress", (str: string, key: readline.Key) => {
212
+ // Alt+Enter: 结束多行输入
213
+ if (key.meta && key.name === "return") {
214
+ // 清除当前行
215
+ readline.moveCursor(process.stdout, 0, -1);
216
+ readline.clearLine(process.stdout, 0);
217
+ this.finishInput();
218
+ return;
219
+ }
220
+
221
+ // Esc 按键: key.name === "escape"
222
+ if (key.name === "escape") {
223
+ this.handleEscKey();
224
+ }
225
+ });
226
+
227
+ // 返回 Promise,等待 REPL 真正关闭才 resolve
228
+ return new Promise<void>((resolve) => {
229
+ this.rl.on("line", async (line) => {
230
+ await this.handleLine(line.trim());
231
+ });
232
+
233
+ this.rl.on("close", () => {
234
+ console.log("\n再见!👋\n");
235
+ resolve();
236
+ });
237
+ });
238
+ }
239
+
240
+ private async handleLine(line: string): Promise<void> {
241
+ // 快捷命令(以 / 开头)- 只有第一行才检测,后续行即使以 / 开头也作为普通文本
242
+ if (line.startsWith("/") && this.inputHandler.getBuffer() === "") {
243
+ await this.handleCommand(line.slice(1));
244
+ this.inputHandler.reset();
245
+ this.updatePrompt();
246
+ return;
247
+ }
248
+
249
+ // 累积输入到 buffer(不管是否以 / 开头)
250
+ this.inputHandler.pushLine(line);
251
+ this.updatePrompt();
252
+ }
253
+
254
+ /**
255
+ * 更新 readline 提示符
256
+ */
257
+ private updatePrompt(): void {
258
+ const prompt = this.inputHandler.getPrompt();
259
+ this.rl.setPrompt(prompt);
260
+ this.rl.prompt(true);
261
+ }
262
+
263
+ /**
264
+ * 结束输入,提交 buffer 内容
265
+ */
266
+ private finishInput(): void {
267
+ const fullContent = this.inputHandler.getBuffer();
268
+ if (!fullContent) {
269
+ // 空输入,忽略
270
+ return;
271
+ }
272
+
273
+ // 显示用户输入(带颜色)
274
+ console.log(COLORS.userInput(`❯ ${fullContent.replace(/\n/g, "\n" + COLORS.userInput("❯ "))}`));
275
+
276
+ // 重置 buffer
277
+ this.inputHandler.reset();
278
+
279
+ // 处理消息
280
+ this.handleMessage(fullContent);
281
+ this.updatePrompt();
282
+ }
283
+
284
+ private async handleCommand(input: string): Promise<void> {
285
+ const [cmd] = input.split(/\s+/);
286
+ const command = this.commandRegistry.get(cmd.toLowerCase());
287
+
288
+ if (command) {
289
+ await command();
290
+ } else {
291
+ console.log(`\n${COLORS.error("❓ 未知命令: /" + cmd + "}")}`);
292
+ this.printHelp();
293
+ }
294
+ }
295
+
296
+ private async handleMessage(message: string): Promise<void> {
297
+ if (this.isExecuting) {
298
+ console.log(`\n${COLORS.system("⏳ 上一次请求尚未完成,请稍候...")}\n`);
299
+ return;
300
+ }
301
+
302
+ await this.executeInternal(message, { showPrompt: false });
303
+ }
304
+
305
+ /**
306
+ * 处理事件消息(来自 EventHandler)
307
+ * 显示为用户输入,并执行 agent
308
+ */
309
+ async handleEventMessage(message: string): Promise<void> {
310
+ // 显示事件消息(带颜色)
311
+ console.log(`\n${COLORS.system("[通知] ❯ " + message.replace(/\n/g, "\n" + COLORS.system("[通知] ❯ ")))}\n`);
312
+ await this.executeInternal(message, { showPrompt: true });
313
+ }
314
+
315
+ /**
316
+ * 检查是否正在执行
317
+ */
318
+ isIdle(): boolean {
319
+ return !this.isExecuting;
320
+ }
321
+
322
+ /**
323
+ * 执行消息并显示结果(内部实现)
324
+ */
325
+ private async executeInternal(
326
+ message: string,
327
+ options: { showPrompt: boolean }
328
+ ): Promise<void> {
329
+ this.isExecuting = true;
330
+
331
+ // 重置 abort 信号
332
+ resetStreamAbort();
333
+
334
+ // 使用 OTelTracer 创建根 span
335
+ // 关键:在调用 onExecute 之前设置全局 context,这样装饰器创建的 span 也能继承 traceId
336
+ const provider = getTracerProvider();
337
+ const tracer = provider.getTracer("roy-tracer");
338
+
339
+ // 创建根 span(会自动设置 currentContext)
340
+ // 必须在设置全局 context 之前创建,这样全局 context 才能用正确的 spanId
341
+ const rootSpan = tracer.startSpan("interactive.execute");
342
+ const traceId = rootSpan.spanContext.traceId;
343
+
344
+ // 设置全局 context,让其他 tracer 实例也能共享这个 traceId
345
+ // 必须在调用 onExecute 之前设置,这样装饰器创建的 span 才会继承这个 traceId
346
+ // 使用根 span 的 spanContext 作为全局 context
347
+ provider.setGlobalContext(rootSpan.spanContext);
348
+
349
+ // 显示 trace id 给用户
350
+ console.log(`${COLORS.system("[Trace] " + traceId)}`);
351
+
352
+ // 显示思考中提示
353
+ console.log(`${COLORS.progress("[thinking...]")}`);
354
+
355
+ try {
356
+ // 传递 trace id 给 onExecute
357
+ await this.options.onExecute(message, traceId);
358
+ // 注意:流式输出已在 QueryExecutor.subscribeToEvents -> StreamOutputService 中处理
359
+ // 不需要再次打印返回值,避免重复输出
360
+ } catch (error) {
361
+ console.error(`\n${COLORS.error("❌ 执行失败: " + error)}\n`);
362
+ // 标记 span 为错误
363
+ rootSpan.setAttribute("error", String(error));
364
+ } finally {
365
+ // 清理全局 context(必须在结束 span 之前)
366
+ provider.setGlobalContext(undefined);
367
+
368
+ // 结束根 span(会保存到 SQLite 并恢复父上下文)
369
+ rootSpan.end();
370
+
371
+ this.isExecuting = false;
372
+ // 如果是事件消息,需要恢复 prompt
373
+ if (options.showPrompt) {
374
+ this.rl.prompt(true);
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * 处理 SIGINT 信号(Ctrl+C)
381
+ *
382
+ * 行为:
383
+ * - 多行输入中:取消当前输入,重置状态
384
+ * - 正在执行:退出程序
385
+ * - 空闲状态:退出程序
386
+ *
387
+ * 与 Esc 的区别:
388
+ * - Esc:中断 LLM 调用 + 停止流式输出 + 恢复 prompt(继续执行)
389
+ * - Ctrl+C:退出程序,或取消多行输入
390
+ */
391
+ handleSigint(): void {
392
+ if (this.isShuttingDown) {
393
+ return;
394
+ }
395
+
396
+ // 多行输入状态下,取消输入而不是退出
397
+ if (this.inputHandler.getPrompt() !== USER_PROMPT) {
398
+ console.log("\n\n" + COLORS.system("[已取消多行输入]"));
399
+ this.inputHandler.reset();
400
+ this.updatePrompt();
401
+ return;
402
+ }
403
+
404
+ // 正在执行时,退出程序
405
+ if (this.isExecuting) {
406
+ console.log("\n\n收到中断信号,正在清理...");
407
+ this.shutdown()
408
+ .then(() => process.exit(0))
409
+ .catch(() => process.exit(0));
410
+ return;
411
+ }
412
+
413
+ // 空闲状态,退出程序
414
+ console.log("\n\n再见!👋\n");
415
+ this.shutdown()
416
+ .then(() => process.exit(0))
417
+ .catch(() => process.exit(0));
418
+ }
419
+
420
+ /**
421
+ * 处理 Esc 按键 - 中断流式输出
422
+ *
423
+ * Esc 行为:
424
+ * - 正在执行时:中断底层 LLM 调用 + 停止流式输出 + 恢复 prompt
425
+ * - 空闲时:不做任何操作
426
+ *
427
+ * 与 Ctrl+C 的区别:
428
+ * - Esc 只在执行时有效,空闲时不退出
429
+ * - Ctrl+C 在空闲时会退出程序
430
+ */
431
+ handleEscKey(): void {
432
+ if (this.isShuttingDown) {
433
+ return;
434
+ }
435
+
436
+ if (this.isExecuting) {
437
+ // 1. 中断底层 LLM 调用
438
+ if (this.env) {
439
+ const agentComponent = this.env.getComponent("agent") as any;
440
+ if (agentComponent?.abort) {
441
+ agentComponent.abort("default");
442
+ logger.debug("[Esc] AgentComponent.abort('default') called");
443
+ }
444
+ }
445
+
446
+ // 2. 停止流式输出
447
+ abortStream();
448
+
449
+ // 3. 重置状态
450
+ this.isExecuting = false;
451
+
452
+ // 4. 恢复 prompt
453
+ this.rl.prompt(true);
454
+ }
455
+ // 空闲时不做任何操作
456
+ }
457
+
458
+ private printHeader(): void {
459
+ console.log(chalk.bold("\n╔══════════════════════════════════════════════════╗"));
460
+ console.log(chalk.bold("║ ") + chalk.green.bold("roy-agent") + chalk.bold(" Interactive Mode") + chalk.bold(" ║"));
461
+ console.log(chalk.bold("╚══════════════════════════════════════════════════╝\n"));
462
+ console.log(`${chalk.dim("会话:")} ${this.options.sessionTitle}`);
463
+ console.log(`${chalk.dim("输入 ")} ${COLORS.userInput("/help")} ${chalk.dim("查看可用命令")}\n`);
464
+ }
465
+
466
+ private printHelp(): void {
467
+ console.log(`
468
+ ${chalk.bold("╭")}${chalk.dim("─".repeat(49))}${chalk.bold("╮")}
469
+ ${chalk.bold("│")} ${chalk.bold("快捷命令")} ${chalk.bold("│")}
470
+ ${chalk.bold("├")}${chalk.dim("─".repeat(49))}${chalk.bold("├")}
471
+ ${chalk.bold("│")} ${COLORS.userInput("/help")}, ${COLORS.userInput("/h")} 显示帮助 ${chalk.bold("│")}
472
+ ${chalk.bold("│")} ${COLORS.userInput("/status")}, ${COLORS.userInput("/st")} 显示状态 ${chalk.bold("│")}
473
+ ${chalk.bold("│")} ${COLORS.userInput("/sessions")}, ${COLORS.userInput("/s")} 切换会话 ${chalk.bold("│")}
474
+ ${chalk.bold("│")} ${COLORS.userInput("/compact")}, ${COLORS.userInput("/c")} 压缩上下文 ${chalk.bold("│")}
475
+ ${chalk.bold("│")} ${COLORS.userInput("/abort")} 中断当前流式输出 ${chalk.bold("│")}
476
+ ${chalk.bold("│")} ${COLORS.userInput("/clear")}, ${COLORS.userInput("/cls")} 清屏 ${chalk.bold("│")}
477
+ ${chalk.bold("│")} ${COLORS.userInput("/exit")}, ${COLORS.userInput("/q")} 退出 ${chalk.bold("│")}
478
+ ${chalk.bold("├")}${chalk.dim("─".repeat(49))}${chalk.bold("├")}
479
+ ${chalk.bold("│")} ${chalk.bold("多行输入")} ${chalk.bold("│")}
480
+ ${chalk.bold("├")}${chalk.dim("─".repeat(49))}${chalk.bold("├")}
481
+ ${chalk.bold("│")} 输入多行内容后,Alt+Enter 结束输入。 ${chalk.bold("│")}
482
+ ${chalk.bold("│")} Ctrl+C 可取消多行输入。 ${chalk.bold("│")}
483
+ ${chalk.bold("╰")}${chalk.dim("─".repeat(49))}${chalk.bold("╯")}
484
+ `);
485
+ }
486
+
487
+ private async printStatus(): Promise<void> {
488
+ const status = await this.options.onStatus();
489
+ console.log(`
490
+ ${chalk.bold("╭")}${chalk.dim("─".repeat(49))}${chalk.bold("╮")}
491
+ ${chalk.bold("│")} ${chalk.bold("会话状态")} ${chalk.bold("│")}
492
+ ${chalk.bold("├")}${chalk.dim("─".repeat(49))}${chalk.bold("├")}
493
+ ${chalk.bold("│")} ${chalk.dim("会话:")} ${status.sessionTitle.padEnd(35)} ${chalk.bold("│")}
494
+ ${chalk.bold("│")} ${chalk.dim("ID:")} ${status.sessionId.padEnd(37)} ${chalk.bold("│")}
495
+ ${chalk.bold("│")} ${chalk.dim("消息:")} ${String(status.messageCount).padEnd(34)} ${chalk.bold("│")}
496
+ ${chalk.bold("│")} ${chalk.dim("Token:")} ${String(status.tokenCount).padEnd(33)} ${chalk.bold("│")}
497
+ ${chalk.bold("╰")}${chalk.dim("─".repeat(49))}${chalk.bold("╯")}
498
+ `);
499
+ }
500
+
501
+ private printSessionsHelp(): void {
502
+ console.log(`
503
+ ${chalk.bold("╭")}${chalk.dim("─".repeat(49))}${chalk.bold("╮")}
504
+ ${chalk.bold("│")} ${chalk.bold("切换会话")} ${chalk.bold("│")}
505
+ ${chalk.bold("├")}${chalk.dim("─".repeat(49))}${chalk.bold("├")}
506
+ ${chalk.bold("│")} 退出当前交互模式后,使用以下命令切换会话: ${chalk.bold("│")}
507
+ ${chalk.bold("│")} ${chalk.bold("│")}
508
+ ${chalk.bold("│")} ${COLORS.userInput("roy sessions list")} 列出所有会话 ${chalk.bold("│")}
509
+ ${chalk.bold("│")} ${COLORS.userInput("roy sessions use <id>")} 切换到指定会话 ${chalk.bold("│")}
510
+ ${chalk.bold("│")} ${chalk.bold("│")}
511
+ ${chalk.bold("│")} 或直接指定会话启动: ${chalk.bold("│")}
512
+ ${chalk.bold("│")} ${COLORS.userInput("roy interactive -s <session-id>")} ${chalk.bold("│")}
513
+ ${chalk.bold("╰")}${chalk.dim("─".repeat(49))}${chalk.bold("╯")}
514
+ `);
515
+ }
516
+ }
517
+
518
+ /**
519
+ * InteractiveCommand - 交互式命令
520
+ *
521
+ * 核心复用:
522
+ * - SessionManager: Session 管理
523
+ * - QueryExecutor: 查询执行
524
+ * - REPL: 交互循环
525
+ */
526
+ export const InteractiveCommand: CommandModule<object, InteractiveOptions> = {
527
+ command: "interactive",
528
+ aliases: ["i", "repl"],
529
+ describe: "交互式模式 - 多轮对话交互",
530
+
531
+ builder: (yargs) =>
532
+ yargs
533
+ .option("session", {
534
+ alias: "s",
535
+ describe: "指定会话 ID",
536
+ type: "string",
537
+ })
538
+ .option("continue", {
539
+ alias: "c",
540
+ describe: "继续上次会话",
541
+ type: "boolean",
542
+ default: false,
543
+ })
544
+ .option("config", {
545
+ alias: "C",
546
+ describe: "配置文件路径",
547
+ type: "string",
548
+ })
549
+ .option("reasoning", {
550
+ alias: "r",
551
+ describe: "显示 AI 思考过程",
552
+ type: "boolean",
553
+ default: false,
554
+ })
555
+ .option("tool-calls", {
556
+ describe: "显示工具调用",
557
+ type: "boolean",
558
+ default: false,
559
+ })
560
+ .option("tool-results", {
561
+ describe: "显示工具执行结果",
562
+ type: "boolean",
563
+ default: false,
564
+ })
565
+ .option("event-source", {
566
+ alias: "es",
567
+ describe: "启动时一并启动的事件源 ID",
568
+ type: "string",
569
+ })
570
+ .option("plugin", {
571
+ alias: "p",
572
+ describe: "启用 plugin (lsp, code-check, reminder, memory, task-tag)",
573
+ type: "array",
574
+ string: true,
575
+ }),
576
+
577
+ async handler(args) {
578
+ // ========== 1. 初始化(复用 act) ==========
579
+ CliQuietModeService.getInstance().setQuiet(true);
580
+ const output = new OutputService();
581
+ const envService = new EnvironmentService(output);
582
+
583
+ // ========== 1.5 Plugin 参数解析(与 act.ts 保持一致)==========
584
+ // coder-harness 插件名称集合
585
+ const CODER_HARNESS_PLUGINS = new Set(["lsp", "code-check", "reminder"]);
586
+
587
+ // 确保 plugins 是数组(yargs 在单个值时可能返回字符串)
588
+ const rawPlugins = args.plugin;
589
+ const pluginNames: string[] = Array.isArray(rawPlugins)
590
+ ? rawPlugins
591
+ : rawPlugins
592
+ ? [rawPlugins]
593
+ : [];
594
+
595
+ // 分离 coder-harness 插件和 component plugins
596
+ const coderPluginNames: string[] = [];
597
+ const componentPluginNames: string[] = [];
598
+
599
+ for (const plugin of pluginNames) {
600
+ const name = plugin.split(":")[0];
601
+ if (CODER_HARNESS_PLUGINS.has(name)) {
602
+ coderPluginNames.push(plugin);
603
+ } else {
604
+ componentPluginNames.push(plugin);
605
+ }
606
+ }
607
+
608
+ try {
609
+ // 创建环境(传入 componentPluginNames 以加载 task-tag 等插件)
610
+ await envService.create({
611
+ configPath: args.config,
612
+ plugins: componentPluginNames,
613
+ });
614
+ const env = envService.getEnvironment();
615
+ if (!env) {
616
+ output.error("Failed to create environment");
617
+ process.exit(1);
618
+ }
619
+ const sessionComponent = envService.getSession();
620
+ if (!sessionComponent) {
621
+ output.error("Failed to create session component");
622
+ process.exit(1);
623
+ }
624
+
625
+ // 初始化 SummaryAgent(复用 act 逻辑)
626
+ const llmComponent = envService.getLLM();
627
+ const promptComponent = env.getComponent("prompt") as any;
628
+
629
+ // 初始化流式输出服务(导入 COLORS 和 abort 函数)
630
+ const { COLORS: STREAM_COLORS, resetStreamAbort: resetStream } = await import("../services/stream-output.service");
631
+
632
+ // ========== 2. Session 管理(复用 SessionManager) ==========
633
+ const sessionManager = new SessionManager({
634
+ sessionComponent,
635
+ output,
636
+ quiet: true,
637
+ });
638
+
639
+ const sessionResult = await sessionManager.init(args.session, args.continue);
640
+ const { sessionId, sessionTitle } = sessionResult;
641
+
642
+ if (llmComponent && promptComponent) {
643
+ sessionComponent.setSummaryComponents(promptComponent, llmComponent);
644
+ }
645
+
646
+ // ========== 3. Query 执行器(复用 QueryExecutor) ==========
647
+ const queryExecutor = new QueryExecutor({
648
+ env,
649
+ sessionComponent,
650
+ output,
651
+ quiet: true,
652
+ });
653
+
654
+ // 初始化流式输出配置(设置 context window 信息)
655
+ queryExecutor.initStreamOutput(llmComponent);
656
+
657
+ // ========== 3.5 Coder-harness 插件加载(lsp/linter/typecheck)==========
658
+ // 延迟加载 coder-harness 以避免影响 CLI 基础功能
659
+ let pluginAdapterDispose: (() => Promise<void>) | null = null;
660
+ let lspManagerDispose: (() => Promise<void>) | null = null;
661
+ let memoryPluginDispose: (() => Promise<void>) | null = null;
662
+ // MemoryPlugin 实例(用于 shutdown 时访问)
663
+ let memoryPluginInstance: any = null;
664
+
665
+ // Coder-harness 插件加载(lsp/linter/typecheck)
666
+ if (coderPluginNames.length > 0) {
667
+ try {
668
+ // 使用 globalHookManager(与 act.ts 保持一致)
669
+ const { globalHookManager } = await import("@ai-setting/roy-agent-core");
670
+ const { createPluginHookAdapter, createGlobalLSPManager } = await import("@ai-setting/roy-agent-coder-harness");
671
+
672
+ const adapter = createPluginHookAdapter(globalHookManager as any, {
673
+ enableLSP: coderPluginNames.includes("lsp"),
674
+ enableCodeCheck: coderPluginNames.includes("code-check"),
675
+ enableReminder: coderPluginNames.includes("reminder"),
676
+ });
677
+
678
+ // 初始化插件(启动 LSP 预加载等)- 与 act.ts 保持一致
679
+ await adapter.initialize();
680
+
681
+ pluginAdapterDispose = () => adapter.dispose();
682
+
683
+ // 过滤有效的插件名称
684
+ const enabledPlugins = coderPluginNames.filter(
685
+ (p) => CODER_HARNESS_PLUGINS.has(p.split(":")[0])
686
+ );
687
+ if (enabledPlugins.length > 0) {
688
+ console.log(`已启用插件: ${enabledPlugins.join(", ")}`);
689
+ output.success(`✅ 已启用插件: ${enabledPlugins.join(", ")}`);
690
+ }
691
+
692
+ // ========== 3.6 Memory Plugin 加载 ==========
693
+ // MemoryPlugin 已在 EnvironmentService 中创建并传入 MemoryComponent
694
+ // 这里只需要注册到 AgentComponent 的 Hook 系统
695
+ if (coderPluginNames.includes("memory")) {
696
+ try {
697
+ // 从 EnvironmentService 获取已创建的 MemoryPlugin 实例
698
+ const memoryPluginInstance = (envService as any).getMemoryPlugin?.();
699
+
700
+ if (memoryPluginInstance) {
701
+ // 注册到 AgentComponent(使用 default agent)
702
+ const agentComponent = env.getComponent("agent") as any;
703
+ if (agentComponent?.registerPlugin) {
704
+ // 确保 default agent 存在(可能在 queryExecutor.execute 之前调用)
705
+ if (!agentComponent.getAgent("default")) {
706
+ agentComponent.registerAgent("default", { type: "primary" });
707
+ }
708
+
709
+ // 创建符合 Plugin 接口的对象
710
+ const pluginWrapper = {
711
+ name: memoryPluginInstance.name,
712
+ version: "1.0.0",
713
+ hooks: memoryPluginInstance.hooks,
714
+ execute: async (ctx: any) => {
715
+ await memoryPluginInstance.execute(ctx);
716
+ return { continue: true };
717
+ },
718
+ };
719
+ agentComponent.registerPlugin("default", pluginWrapper);
720
+ output.success("✅ Memory 插件已注册到 Agent");
721
+ }
722
+
723
+ // 设置 dispose 函数
724
+ memoryPluginDispose = async () => {
725
+ if (memoryPluginInstance) {
726
+ memoryPluginInstance.prepareShutdown();
727
+ if (memoryPluginInstance.hasPendingTasks()) {
728
+ console.log("💾 正在保存记忆,请稍候...");
729
+ await memoryPluginInstance.waitForCompletion();
730
+ }
731
+ await memoryPluginInstance.dispose();
732
+ console.log("💾 记忆已保存完毕");
733
+ }
734
+ };
735
+ } else {
736
+ output.warn("⚠️ Memory 插件未在 EnvironmentService 中初始化");
737
+ }
738
+ } catch (error) {
739
+ output.warn(`⚠️ Memory 插件加载失败: ${error}`);
740
+ }
741
+ }
742
+
743
+ // ========== 3.7 LSP Plugin 加载 ==========
744
+ // 如果启用了 LSP 插件,预热 LSP 服务器
745
+ if (coderPluginNames.includes("lsp")) {
746
+ try {
747
+ // 加载 LSP 配置
748
+ const { createLSPConfigLoader } = await import("@ai-setting/roy-agent-coder-harness");
749
+ const configLoader = createLSPConfigLoader();
750
+ const lspConfig = configLoader.load();
751
+
752
+ output.info("正在预热 LSP 服务器...");
753
+ if (lspConfig.preload) {
754
+ output.info("📋 使用配置文件中的预加载设置");
755
+ }
756
+ const lspManager = createGlobalLSPManager({
757
+ idleTimeout: lspConfig.idleTimeout ?? 300000,
758
+ maxConnections: lspConfig.maxConnections ?? 10,
759
+ autoDownload: lspConfig.autoInstall ?? false,
760
+ preloadLanguages: lspConfig.preloadLanguages,
761
+ });
762
+ // 注册 shutdown handler 确保进程退出时清理
763
+ lspManager.registerShutdownHandler();
764
+ // 保存 dispose 函数以便在退出时调用
765
+ lspManagerDispose = () => lspManager.dispose();
766
+ // 预热所有服务器(异步,不阻塞)
767
+ if (lspConfig.preload || coderPluginNames.includes("lsp")) {
768
+ lspManager.prewarm().then(() => {
769
+ output.success("✅ LSP 服务器预热完成");
770
+ }).catch((err: Error) => {
771
+ output.warn(`⚠ LSP 预热部分失败: ${err.message}`);
772
+ });
773
+ }
774
+ } catch (error) {
775
+ output.warn(`⚠ LSP 预热失败: ${error}`);
776
+ }
777
+ }
778
+ } catch (error) {
779
+ output.error(`❌ 加载 coder-harness 插件失败: ${error}`);
780
+ }
781
+ }
782
+
783
+ // ========== 4. REPL 循环(新增) ==========
784
+ // 传入 env 引用,以便在 Ctrl+C 时可以获取 AgentComponent 进行中断
785
+ const repl = new REPL({
786
+ sessionId,
787
+ sessionTitle,
788
+ onExecute: async (message: string, traceId?: string) => {
789
+ // 每次执行前重置 abort 信号
790
+ resetStreamAbort();
791
+ return queryExecutor.execute(message, sessionId, {
792
+ showReasoning: args.reasoning,
793
+ showToolCalls: args.toolCalls,
794
+ showToolResults: args.toolResults,
795
+ }, traceId);
796
+ },
797
+ onStatus: async (): Promise<REPLStatus> => {
798
+ // 从 session 获取真实统计
799
+ const session = await sessionComponent.get(sessionId);
800
+ return {
801
+ sessionId,
802
+ sessionTitle,
803
+ messageCount: session?.messageCount ?? 0,
804
+ tokenCount: 0, // tokenCount 需要从消息中累加,暂不实现
805
+ };
806
+ },
807
+ onSwitchSession: async (newSessionId: string) => {
808
+ const result = await sessionManager.switchSession(newSessionId);
809
+ queryExecutor.dispose();
810
+ return result;
811
+ },
812
+ onCompact: async () => {
813
+ output.info("🔄 正在压缩上下文...");
814
+ const result = await queryExecutor.compact(sessionId);
815
+ if (result.success) {
816
+ output.success(`✓ 压缩完成,清理了 ${result.deletedMessageCount} 条消息`);
817
+ } else {
818
+ output.error(`❌ 压缩失败: ${result.error}`);
819
+ }
820
+ },
821
+ onShutdown: async () => {
822
+ // 0. 清理 MemoryPlugin(等待待处理任务完成)
823
+ if (memoryPluginDispose) {
824
+ if (memoryPluginInstance && memoryPluginInstance.hasPendingTasks()) {
825
+ console.log("💾 正在保存记忆,请稍候...");
826
+ await memoryPluginInstance.waitForCompletion();
827
+ }
828
+ await memoryPluginDispose();
829
+ console.log("💾 记忆已保存完毕");
830
+ }
831
+
832
+ // 1. 清理 LSP Manager(关闭所有 LSP 连接和进程)
833
+ if (lspManagerDispose) {
834
+ await lspManagerDispose();
835
+ }
836
+
837
+ // 2. 清理 PluginAdapter
838
+ if (pluginAdapterDispose) {
839
+ await pluginAdapterDispose();
840
+ }
841
+
842
+ // 2. 停止 EventHandler
843
+ eventHandler.stop();
844
+
845
+ // 2. 停止 QueryExecutor(取消环境事件订阅)
846
+ queryExecutor.dispose();
847
+
848
+ // 3. 停止所有运行中的事件源
849
+ const eventSourceComponent = env.getComponent("event-source") as any;
850
+ if (eventSourceComponent) {
851
+ const sources = eventSourceComponent.list?.();
852
+ if (sources && sources.length > 0) {
853
+ for (const source of sources) {
854
+ const status = eventSourceComponent.getStatus?.(source.id);
855
+ logger.debug(`[Shutdown] 检查事件源: ${source.id}, 状态: ${status}`);
856
+
857
+ // 停止任何非 stopped 状态的事件源(包括 running 和 error)
858
+ if (status !== "stopped" && status !== "created") {
859
+ try {
860
+ await eventSourceComponent.stopSource(source.id);
861
+ output.info(`✓ 已停止事件源: ${source.name} (${status})`);
862
+ } catch (error) {
863
+ output.warn(`⚠️ 停止事件源 ${source.name} 失败: ${error}`);
864
+ }
865
+ }
866
+ }
867
+ }
868
+ }
869
+
870
+ // 4. 销毁环境
871
+ await envService.dispose();
872
+ },
873
+ }, env);
874
+
875
+ // ========== 5. EventHandler(事件驱动自动响应) ==========
876
+ const eventHandler = new EventHandler({
877
+ env,
878
+ eventTypes: ["task.*", "event-source.event.*"], // 只监听任务事件和事件源的实际消息事件
879
+ formatter: new EventMessageFormatter({ prefix: "[通知]" }),
880
+ isIdle: () => repl.isIdle(),
881
+ onEvent: async (message: string) => {
882
+ await repl.handleEventMessage(message);
883
+ },
884
+ });
885
+
886
+ // 启动事件监听
887
+ const stopEventHandler = eventHandler.start();
888
+
889
+ // ========== 6. EventSource 启动(必须成功) ==========
890
+ const eventSourceId = args.eventSource;
891
+ let eventSourceStarted = false;
892
+
893
+ if (eventSourceId) {
894
+ const eventSourceComponent = env.getComponent("event-source") as any;
895
+
896
+ if (!eventSourceComponent || typeof eventSourceComponent.startSource !== "function") {
897
+ output.error(`❌ 未找到事件源组件或 startSource 方法`);
898
+ output.info(`请确保 EventSourceComponent 已正确注册`);
899
+ process.exit(1);
900
+ }
901
+
902
+ // 支持前缀匹配
903
+ const sources = eventSourceComponent.list();
904
+ const matchedSource = sources.find(
905
+ (s: any) => s.id === eventSourceId || s.id.startsWith(eventSourceId)
906
+ );
907
+
908
+ if (!matchedSource) {
909
+ output.error(`❌ 事件源不存在: ${eventSourceId}`);
910
+ output.info(`可用的事件源:`);
911
+ for (const s of sources) {
912
+ output.info(` - ${s.id}: ${s.name} (${s.type})`);
913
+ }
914
+ process.exit(1);
915
+ }
916
+
917
+ const actualId = matchedSource.id;
918
+ const status = eventSourceComponent.getStatus(actualId);
919
+
920
+ if (status === "running") {
921
+ output.info(`📡 事件源 ${matchedSource.name} 已在运行中`);
922
+ } else {
923
+ try {
924
+ await eventSourceComponent.startSource(actualId);
925
+
926
+ // 验证启动成功:检查状态是否为 running
927
+ const verifyStatus = eventSourceComponent.getStatus(actualId);
928
+ if (verifyStatus !== "running") {
929
+ output.error(`❌ 事件源启动后状态异常: ${verifyStatus}`);
930
+ output.info(`请检查日志确认后台进程是否正常运行`);
931
+ process.exit(1);
932
+ }
933
+
934
+ eventSourceStarted = true;
935
+ output.success(`📡 已启动事件源: ${matchedSource.name} (${verifyStatus})`);
936
+ } catch (error) {
937
+ output.error(`❌ 启动事件源失败: ${error}`);
938
+ output.info(`常见问题:`);
939
+ output.info(` - lark-cli 事件源:确保已安装并登录 lark-cli`);
940
+ output.info(` - websocket 事件源:确保目标服务器可达`);
941
+ output.info(` - timer 事件源:检查配置是否正确`);
942
+ process.exit(1);
943
+ }
944
+ }
945
+ }
946
+
947
+ // ========== 7. SIGINT 处理器(Ctrl+C)==========
948
+ // 不再使用 process.on("SIGINT"),改用 REPL.handleSigint()
949
+ // 需要在启动 REPL 之前设置信号处理
950
+ let sigintHandler: NodeJS.SignalsListener;
951
+
952
+ // 创建一个可以被移除的 handler
953
+ sigintHandler = () => {
954
+ repl.handleSigint();
955
+ };
956
+
957
+ process.on("SIGINT", sigintHandler);
958
+
959
+ // 启动 REPL
960
+ await repl.start();
961
+
962
+ // 移除 SIGINT 处理器(正常退出时)
963
+ process.removeListener("SIGINT", sigintHandler);
964
+
965
+ // ========== 清理(正常退出) ==========
966
+ // 注意:exit 命令会调用 shutdown,然后 process.exit
967
+ // 正常退出时(REPL 自然关闭),需要手动清理
968
+ console.log("正在清理资源...");
969
+
970
+ // 0. 清理 LSP Manager(关闭所有 LSP 连接和进程)
971
+ if (lspManagerDispose) {
972
+ await lspManagerDispose();
973
+ }
974
+
975
+ // 1. 清理 PluginAdapter
976
+ if (pluginAdapterDispose) {
977
+ await pluginAdapterDispose();
978
+ }
979
+
980
+ // 2. 停止 EventHandler
981
+ eventHandler.stop();
982
+
983
+ // 停止 QueryExecutor
984
+ queryExecutor.dispose();
985
+
986
+ // 停止所有运行中的事件源
987
+ const esComponent = env.getComponent("event-source") as any;
988
+ if (esComponent) {
989
+ const sources = esComponent.list?.();
990
+ if (sources) {
991
+ for (const source of sources) {
992
+ const status = esComponent.getStatus?.(source.id);
993
+ if (status === "running") {
994
+ try {
995
+ await esComponent.stopSource(source.id);
996
+ output.info(`✓ 已停止事件源: ${source.name}`);
997
+ } catch (error) {
998
+ output.warn(`⚠️ 停止事件源 ${source.name} 失败: ${error}`);
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ // 销毁环境
1006
+ await envService.dispose();
1007
+
1008
+ } catch (error) {
1009
+ output.error(`启动交互模式失败: ${error}`);
1010
+ throw error;
1011
+ } finally {
1012
+ await envService.dispose();
1013
+ }
1014
+ },
1015
+ };