@xiaozhi-client/cli 1.9.4-beta.5

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/fix-imports.js +32 -0
  4. package/package.json +26 -0
  5. package/project.json +75 -0
  6. package/src/Constants.ts +105 -0
  7. package/src/Container.ts +212 -0
  8. package/src/Types.ts +79 -0
  9. package/src/commands/CommandHandlerFactory.ts +98 -0
  10. package/src/commands/ConfigCommandHandler.ts +279 -0
  11. package/src/commands/EndpointCommandHandler.ts +158 -0
  12. package/src/commands/McpCommandHandler.ts +778 -0
  13. package/src/commands/ProjectCommandHandler.ts +254 -0
  14. package/src/commands/ServiceCommandHandler.ts +182 -0
  15. package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
  16. package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
  17. package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
  18. package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
  19. package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
  20. package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
  21. package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
  22. package/src/commands/index.ts +351 -0
  23. package/src/errors/ErrorHandlers.ts +141 -0
  24. package/src/errors/ErrorMessages.ts +121 -0
  25. package/src/errors/__tests__/index.test.ts +186 -0
  26. package/src/errors/index.ts +163 -0
  27. package/src/global.d.ts +19 -0
  28. package/src/index.ts +53 -0
  29. package/src/interfaces/Command.ts +128 -0
  30. package/src/interfaces/CommandTypes.ts +95 -0
  31. package/src/interfaces/Config.ts +25 -0
  32. package/src/interfaces/Service.ts +99 -0
  33. package/src/services/DaemonManager.ts +318 -0
  34. package/src/services/ProcessManager.ts +235 -0
  35. package/src/services/ServiceManager.ts +319 -0
  36. package/src/services/TemplateManager.ts +382 -0
  37. package/src/services/__tests__/DaemonManager.test.ts +378 -0
  38. package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
  39. package/src/services/__tests__/ProcessManager.test.ts +296 -0
  40. package/src/services/__tests__/ServiceManager.test.ts +774 -0
  41. package/src/services/__tests__/TemplateManager.test.ts +337 -0
  42. package/src/types/backend.d.ts +48 -0
  43. package/src/utils/FileUtils.ts +320 -0
  44. package/src/utils/FormatUtils.ts +198 -0
  45. package/src/utils/PathUtils.ts +255 -0
  46. package/src/utils/PlatformUtils.ts +217 -0
  47. package/src/utils/Validation.ts +274 -0
  48. package/src/utils/VersionUtils.ts +141 -0
  49. package/src/utils/__tests__/FileUtils.test.ts +728 -0
  50. package/src/utils/__tests__/FormatUtils.test.ts +243 -0
  51. package/src/utils/__tests__/PathUtils.test.ts +1165 -0
  52. package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
  53. package/src/utils/__tests__/Validation.test.ts +560 -0
  54. package/src/utils/__tests__/VersionUtils.test.ts +410 -0
  55. package/tsconfig.json +32 -0
  56. package/tsconfig.tsbuildinfo +1 -0
  57. package/tsup.config.ts +107 -0
  58. package/vitest.config.ts +97 -0
@@ -0,0 +1,778 @@
1
+ /**
2
+ * MCP管理命令处理器
3
+ */
4
+
5
+ import { configManager } from "@xiaozhi-client/config";
6
+ import chalk from "chalk";
7
+ import Table from "cli-table3";
8
+ import consola from "consola";
9
+ import ora from "ora";
10
+ import type { SubCommand } from "../interfaces/Command";
11
+ import { BaseCommandHandler } from "../interfaces/Command";
12
+ import type {
13
+ CallOptions,
14
+ CommandArguments,
15
+ CommandOptions,
16
+ ListOptions,
17
+ } from "../interfaces/CommandTypes";
18
+ import { isLocalMCPServerConfig } from "../interfaces/CommandTypes";
19
+ import { ProcessManagerImpl } from "../services/ProcessManager";
20
+
21
+ // 工具调用结果接口
22
+ interface ToolCallResult {
23
+ content: Array<{
24
+ type: string;
25
+ text: string;
26
+ }>;
27
+ isError?: boolean;
28
+ }
29
+
30
+ /**
31
+ * MCP管理命令处理器
32
+ */
33
+ export class McpCommandHandler extends BaseCommandHandler {
34
+ private processManager: ProcessManagerImpl;
35
+ private baseUrl: string;
36
+
37
+ constructor(...args: ConstructorParameters<typeof BaseCommandHandler>) {
38
+ super(...args);
39
+ this.processManager = new ProcessManagerImpl();
40
+
41
+ // 获取 Web 服务器的端口
42
+ try {
43
+ const webPort = configManager.getWebUIPort() ?? 9999;
44
+ this.baseUrl = `http://localhost:${webPort}`;
45
+ } catch {
46
+ this.baseUrl = "http://localhost:9999";
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 中文字符正则表达式
52
+ *
53
+ * Unicode 范围说明:
54
+ * - \u4e00-\u9fff: CJK 统一汉字(基本汉字)
55
+ * - \u3400-\u4dbf: CJK 扩展 A(扩展汉字)
56
+ * - \uff00-\uffef: 全角字符和半角片假名(包括中文标点符号)
57
+ *
58
+ * 注意:此范围可能不完全覆盖所有中日韩字符(如 CJK 扩展 B-F 等),
59
+ * 但已覆盖绝大多数常用中文场景。
60
+ */
61
+ private static readonly CHINESE_CHAR_REGEX =
62
+ /[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef]/;
63
+
64
+ /**
65
+ * 计算字符串的显示宽度(中文字符占2个宽度,英文字符占1个宽度)
66
+ */
67
+ private static getDisplayWidth(str: string): number {
68
+ let width = 0;
69
+ for (const char of str) {
70
+ // 判断是否为中文字符(包括中文标点符号)
71
+ if (McpCommandHandler.CHINESE_CHAR_REGEX.test(char)) {
72
+ width += 2;
73
+ } else {
74
+ width += 1;
75
+ }
76
+ }
77
+ return width;
78
+ }
79
+
80
+ /**
81
+ * 截断字符串到指定的显示宽度
82
+ */
83
+ private static truncateToWidth(str: string, maxWidth: number): string {
84
+ if (McpCommandHandler.getDisplayWidth(str) <= maxWidth) {
85
+ return str;
86
+ }
87
+
88
+ // 如果最大宽度小于等于省略号的宽度,返回空字符串
89
+ if (maxWidth <= 3) {
90
+ return "";
91
+ }
92
+
93
+ let result = "";
94
+ let currentWidth = 0;
95
+ let hasAddedChar = false;
96
+
97
+ for (const char of str) {
98
+ const charWidth = McpCommandHandler.CHINESE_CHAR_REGEX.test(char) ? 2 : 1;
99
+
100
+ // 如果加上当前字符会超出限制
101
+ if (currentWidth + charWidth > maxWidth - 3) {
102
+ // 如果还没有添加任何字符,说明连一个字符都放不下,返回空字符串
103
+ if (!hasAddedChar) {
104
+ return "";
105
+ }
106
+ // 否则添加省略号并退出
107
+ result += "...";
108
+ break;
109
+ }
110
+
111
+ result += char;
112
+ currentWidth += charWidth;
113
+ hasAddedChar = true;
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * 解析 JSON 参数
121
+ * @param argsString JSON 字符串
122
+ * @returns 解析后的参数对象
123
+ */
124
+ private static parseJsonArgs(argsString: string): Record<string, unknown> {
125
+ try {
126
+ return JSON.parse(argsString);
127
+ } catch (error) {
128
+ throw new Error(
129
+ `参数格式错误,请使用有效的 JSON 格式。错误详情: ${
130
+ error instanceof Error ? error.message : String(error)
131
+ }`
132
+ );
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 格式化工具调用结果输出
138
+ * @param result 工具调用结果
139
+ * @returns 格式化后的字符串
140
+ */
141
+ private static formatToolCallResult(result: ToolCallResult): string {
142
+ return JSON.stringify(result);
143
+ }
144
+
145
+ override name = "mcp";
146
+ override description = "MCP 服务和工具管理";
147
+
148
+ override subcommands: SubCommand[] = [
149
+ {
150
+ name: "list",
151
+ description: "列出 MCP 服务",
152
+ options: [{ flags: "--tools", description: "显示所有服务的工具列表" }],
153
+ execute: async (args: CommandArguments, options: CommandOptions) => {
154
+ await this.handleList(options as ListOptions);
155
+ },
156
+ },
157
+ {
158
+ name: "server",
159
+ description: "管理指定的 MCP 服务",
160
+ execute: async (args: CommandArguments, options: CommandOptions) => {
161
+ this.validateArgs(args, 1);
162
+ await this.handleServer(args[0]);
163
+ },
164
+ },
165
+ {
166
+ name: "tool",
167
+ description: "启用或禁用指定服务的工具",
168
+ execute: async (args: CommandArguments, options: CommandOptions) => {
169
+ this.validateArgs(args, 3);
170
+ const [serverName, toolName, action] = args;
171
+
172
+ if (action !== "enable" && action !== "disable") {
173
+ console.error(chalk.red("错误: 操作必须是 'enable' 或 'disable'"));
174
+ process.exit(1);
175
+ }
176
+
177
+ const enabled = action === "enable";
178
+ await this.handleTool(serverName, toolName, enabled);
179
+ },
180
+ },
181
+ {
182
+ name: "call",
183
+ description: "调用指定服务的工具",
184
+ options: [
185
+ {
186
+ flags: "--args <json>",
187
+ description: "工具参数 (JSON 格式)",
188
+ defaultValue: "{}",
189
+ },
190
+ ],
191
+ execute: async (args: CommandArguments, options: CommandOptions) => {
192
+ this.validateArgs(args, 2);
193
+ const [serviceName, toolName] = args;
194
+ await this.handleCall(
195
+ serviceName,
196
+ toolName,
197
+ (options as CallOptions).args ?? "{}"
198
+ );
199
+ },
200
+ },
201
+ ];
202
+
203
+ /**
204
+ * 主命令执行(显示帮助)
205
+ */
206
+ async execute(
207
+ args: CommandArguments,
208
+ options: CommandOptions
209
+ ): Promise<void> {
210
+ console.log("MCP 服务和工具管理命令。使用 --help 查看可用的子命令。");
211
+ }
212
+
213
+ /**
214
+ * 处理列出服务命令
215
+ */
216
+ private async handleList(options: ListOptions): Promise<void> {
217
+ try {
218
+ await this.handleListInternal(options);
219
+ } catch (error) {
220
+ this.handleError(error as Error);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * 处理服务管理命令
226
+ */
227
+ private async handleServer(serverName: string): Promise<void> {
228
+ try {
229
+ await this.handleServerInternal(serverName);
230
+ } catch (error) {
231
+ this.handleError(error as Error);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 处理工具管理命令
237
+ */
238
+ private async handleTool(
239
+ serverName: string,
240
+ toolName: string,
241
+ enabled: boolean
242
+ ): Promise<void> {
243
+ try {
244
+ await this.handleToolInternal(serverName, toolName, enabled);
245
+ } catch (error) {
246
+ this.handleError(error as Error);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * 验证服务状态
252
+ * @private
253
+ */
254
+ private async validateServiceStatus(): Promise<void> {
255
+ // 检查进程级别的服务状态
256
+ const processStatus = this.processManager.getServiceStatus();
257
+ if (!processStatus.running) {
258
+ throw new Error(
259
+ "xiaozhi 服务未启动。请先运行 'xiaozhi start' 或 'xiaozhi start -d' 启动服务。"
260
+ );
261
+ }
262
+
263
+ // 检查 Web 服务器是否可访问
264
+ try {
265
+ const response = await fetch(`${this.baseUrl}/api/status`, {
266
+ method: "GET",
267
+ signal: AbortSignal.timeout(5000), // 5秒超时
268
+ });
269
+
270
+ if (!response.ok) {
271
+ throw new Error(`Web 服务器响应错误: ${response.status}`);
272
+ }
273
+ } catch (error: unknown) {
274
+ // 超时单独提示
275
+ if (error instanceof Error && error.name === "AbortError") {
276
+ throw new Error("连接 xiaozhi 服务超时。请检查服务是否正常运行。");
277
+ }
278
+
279
+ // 已知的 Error 实例,区分网络错误与其他错误
280
+ if (error instanceof Error) {
281
+ const isNetworkError =
282
+ error instanceof TypeError &&
283
+ /fetch|network|failed/i.test(error.message);
284
+
285
+ if (isNetworkError) {
286
+ throw new Error(
287
+ `无法连接到 xiaozhi 服务(网络请求失败)。请检查网络连接或服务地址是否正确。原始错误: ${error.message}`
288
+ );
289
+ }
290
+
291
+ throw new Error(
292
+ `无法连接到 xiaozhi 服务。请检查服务状态。原始错误: ${error.message}`
293
+ );
294
+ }
295
+
296
+ // 非 Error 对象的兜底处理,避免出现 [object Object]
297
+ let detail: string;
298
+ try {
299
+ detail = JSON.stringify(error);
300
+ } catch {
301
+ detail = String(error);
302
+ }
303
+
304
+ throw new Error(
305
+ `无法连接到 xiaozhi 服务。请检查服务状态。错误详情: ${detail}`
306
+ );
307
+ }
308
+ }
309
+
310
+ /**
311
+ * 调用 MCP 工具的内部实现
312
+ * @param serviceName 服务名称
313
+ * @param toolName 工具名称
314
+ * @param args 工具参数
315
+ * @returns 工具调用结果
316
+ */
317
+ private async callToolInternal(
318
+ serviceName: string,
319
+ toolName: string,
320
+ args: Record<string, unknown>
321
+ ): Promise<ToolCallResult> {
322
+ // 1. 检查服务状态
323
+ await this.validateServiceStatus();
324
+
325
+ // 2. 通过 HTTP API 调用工具
326
+ try {
327
+ const response = await fetch(`${this.baseUrl}/api/tools/call`, {
328
+ method: "POST",
329
+ headers: {
330
+ "Content-Type": "application/json",
331
+ },
332
+ body: JSON.stringify({
333
+ serviceName,
334
+ toolName,
335
+ args,
336
+ }),
337
+ });
338
+
339
+ if (!response.ok) {
340
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
341
+ try {
342
+ const errorData = await response.json();
343
+ const detailedMessage =
344
+ errorData?.error?.message ?? errorData?.message;
345
+ if (typeof detailedMessage === "string" && detailedMessage.trim()) {
346
+ errorMessage = detailedMessage;
347
+ }
348
+ } catch {
349
+ // 响应体不是 JSON 时,保留默认的 HTTP 错误信息
350
+ }
351
+ throw new Error(errorMessage);
352
+ }
353
+
354
+ const responseData = await response.json();
355
+
356
+ if (!responseData.success) {
357
+ throw new Error(responseData.error?.message || "工具调用失败");
358
+ }
359
+
360
+ return responseData.data;
361
+ } catch (error) {
362
+ consola.error(
363
+ `工具调用失败: ${serviceName}/${toolName}`,
364
+ error instanceof Error ? error.message : String(error)
365
+ );
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * 处理工具调用命令
372
+ */
373
+ private async handleCall(
374
+ serviceName: string,
375
+ toolName: string,
376
+ argsString: string
377
+ ): Promise<void> {
378
+ try {
379
+ // 解析参数
380
+ const args = McpCommandHandler.parseJsonArgs(argsString);
381
+
382
+ // 调用工具
383
+ const result = await this.callToolInternal(serviceName, toolName, args);
384
+
385
+ console.log(McpCommandHandler.formatToolCallResult(result));
386
+ } catch (error) {
387
+ console.log(`工具调用失败: ${serviceName}/${toolName}`);
388
+ console.error(chalk.red("错误:"), (error as Error).message);
389
+
390
+ // 提供有用的提示
391
+ const errorMessage = (error as Error).message;
392
+ if (errorMessage.includes("服务未启动")) {
393
+ console.log();
394
+ console.log(chalk.yellow("💡 请先启动服务:"));
395
+ console.log(chalk.gray(" xiaozhi start # 前台启动"));
396
+ console.log(chalk.gray(" xiaozhi start -d # 后台启动"));
397
+ } else if (errorMessage.includes("参数格式错误")) {
398
+ console.log();
399
+ console.log(chalk.yellow("💡 正确格式示例:"));
400
+ console.log(
401
+ chalk.gray(
402
+ ` xiaozhi mcp call ${serviceName} ${toolName} --args '{"param": "value"}'`
403
+ )
404
+ );
405
+ }
406
+
407
+ // 测试环境:通过抛出错误让测试可以捕获并断言
408
+ if (process.env.NODE_ENV === "test") {
409
+ throw new Error("process.exit called");
410
+ }
411
+
412
+ process.exit(1);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * 列出所有 MCP 服务
418
+ */
419
+ private async handleListInternal(
420
+ options: { tools?: boolean } = {}
421
+ ): Promise<void> {
422
+ const spinner = ora("获取 MCP 服务列表...").start();
423
+
424
+ try {
425
+ const mcpServers = configManager.getMcpServers();
426
+ const serverNames = Object.keys(mcpServers);
427
+
428
+ // 检查是否有 customMCP 工具
429
+ const customMCPTools = configManager.getCustomMCPTools();
430
+ const hasCustomMCP = customMCPTools.length > 0;
431
+
432
+ // 计算总服务数(包括 customMCP)
433
+ const totalServices = serverNames.length + (hasCustomMCP ? 1 : 0);
434
+
435
+ if (totalServices === 0) {
436
+ spinner.warn("未配置任何 MCP 服务或 customMCP 工具");
437
+ console.log(
438
+ chalk.yellow(
439
+ "💡 提示: 使用 'xiaozhi config' 命令配置 MCP 服务或在 xiaozhi.config.json 中配置 customMCP 工具"
440
+ )
441
+ );
442
+ return;
443
+ }
444
+
445
+ spinner.succeed(
446
+ `找到 ${totalServices} 个 MCP 服务${hasCustomMCP ? " (包括 customMCP)" : ""}`
447
+ );
448
+
449
+ if (options.tools) {
450
+ // 显示所有服务的工具列表
451
+ console.log();
452
+ console.log(chalk.bold("MCP 服务工具列表:"));
453
+ console.log();
454
+
455
+ // 计算所有工具名称的最大长度,用于动态设置列宽
456
+ let maxToolNameWidth = 8; // 默认最小宽度
457
+ const allToolNames: string[] = [];
458
+
459
+ // 添加标准 MCP 服务的工具名称
460
+ for (const serverName of serverNames) {
461
+ const toolsConfig = configManager.getServerToolsConfig(serverName);
462
+ const toolNames = Object.keys(toolsConfig);
463
+ allToolNames.push(...toolNames);
464
+ }
465
+
466
+ // 添加 customMCP 工具名称
467
+ if (hasCustomMCP) {
468
+ const customToolNames = customMCPTools.map((tool) => tool.name);
469
+ allToolNames.push(...customToolNames);
470
+ }
471
+
472
+ // 计算最长工具名称的显示宽度
473
+ for (const toolName of allToolNames) {
474
+ const width = McpCommandHandler.getDisplayWidth(toolName);
475
+ if (width > maxToolNameWidth) {
476
+ maxToolNameWidth = width;
477
+ }
478
+ }
479
+
480
+ // 确保工具名称列宽度至少为10,最多为30
481
+ maxToolNameWidth = Math.max(10, Math.min(maxToolNameWidth + 2, 30));
482
+
483
+ // 使用 cli-table3 创建表格
484
+ const table = new Table({
485
+ head: [
486
+ chalk.bold("MCP"),
487
+ chalk.bold("工具名称"),
488
+ chalk.bold("状态"),
489
+ chalk.bold("描述"),
490
+ ],
491
+ colWidths: [15, maxToolNameWidth, 8, 40], // MCP | 工具名称 | 状态 | 描述
492
+ wordWrap: true,
493
+ style: {
494
+ head: [],
495
+ border: [],
496
+ },
497
+ });
498
+
499
+ // 首先添加 customMCP 工具(如果存在)
500
+ if (hasCustomMCP) {
501
+ for (const customTool of customMCPTools) {
502
+ const description = McpCommandHandler.truncateToWidth(
503
+ customTool.description || "",
504
+ 32
505
+ );
506
+
507
+ table.push([
508
+ "customMCP",
509
+ customTool.name,
510
+ chalk.green("启用"), // customMCP 工具默认启用
511
+ description,
512
+ ]);
513
+ }
514
+ }
515
+
516
+ // 然后添加标准 MCP 服务的工具
517
+ for (const serverName of serverNames) {
518
+ const toolsConfig = configManager.getServerToolsConfig(serverName);
519
+ const toolNames = Object.keys(toolsConfig);
520
+
521
+ if (toolNames.length === 0) {
522
+ // 服务没有工具时显示提示信息
523
+ table.push([
524
+ chalk.gray(serverName),
525
+ chalk.gray("-"),
526
+ chalk.gray("-"),
527
+ chalk.gray("暂未识别到相关工具"),
528
+ ]);
529
+ } else {
530
+ // 添加服务分隔行(如果表格不为空)
531
+ if (table.length > 0) {
532
+ table.push([{ colSpan: 4, content: "" }]);
533
+ }
534
+
535
+ for (const toolName of toolNames) {
536
+ const toolConfig = toolsConfig[toolName];
537
+ const status = toolConfig.enable
538
+ ? chalk.green("启用")
539
+ : chalk.red("禁用");
540
+
541
+ // 截断描述到最大32个字符宽度(约16个中文字符)
542
+ const description = McpCommandHandler.truncateToWidth(
543
+ toolConfig.description || "",
544
+ 32
545
+ );
546
+
547
+ // 只显示工具名称,不包含服务名前缀
548
+ table.push([serverName, toolName, status, description]);
549
+ }
550
+ }
551
+ }
552
+
553
+ console.log(table.toString());
554
+ } else {
555
+ // 只显示服务列表
556
+ console.log();
557
+ console.log(chalk.bold("MCP 服务列表:"));
558
+ console.log();
559
+
560
+ // 首先显示 customMCP 服务(如果存在)
561
+ if (hasCustomMCP) {
562
+ console.log(`${chalk.cyan("•")} ${chalk.bold("customMCP")}`);
563
+ console.log(` 类型: ${chalk.gray("自定义 MCP 工具")}`);
564
+ console.log(` 配置: ${chalk.gray("xiaozhi.config.json")}`);
565
+ console.log(
566
+ ` 工具: ${chalk.green(customMCPTools.length)} 启用 / ${chalk.yellow(
567
+ customMCPTools.length
568
+ )} 总计`
569
+ );
570
+ console.log();
571
+ }
572
+
573
+ // 然后显示标准 MCP 服务
574
+ for (const serverName of serverNames) {
575
+ const serverConfig = mcpServers[serverName];
576
+ const toolsConfig = configManager.getServerToolsConfig(serverName);
577
+ const toolCount = Object.keys(toolsConfig).length;
578
+ const enabledCount = Object.values(toolsConfig).filter(
579
+ (t) => t.enable !== false
580
+ ).length;
581
+
582
+ console.log(`${chalk.cyan("•")} ${chalk.bold(serverName)}`);
583
+
584
+ // 检查服务类型并显示相应信息
585
+ if ("url" in serverConfig) {
586
+ // URL 类型的服务(SSE 或 Streamable HTTP)
587
+ if ("type" in serverConfig && serverConfig.type === "sse") {
588
+ console.log(` 类型: ${chalk.gray("SSE")}`);
589
+ } else {
590
+ console.log(` 类型: ${chalk.gray("Streamable HTTP")}`);
591
+ }
592
+ console.log(` URL: ${chalk.gray(serverConfig.url)}`);
593
+ } else if (isLocalMCPServerConfig(serverConfig)) {
594
+ // 本地服务
595
+ console.log(
596
+ ` 命令: ${chalk.gray(serverConfig.command)} ${chalk.gray(
597
+ serverConfig.args.join(" ")
598
+ )}`
599
+ );
600
+ }
601
+ if (toolCount > 0) {
602
+ console.log(
603
+ ` 工具: ${chalk.green(enabledCount)} 启用 / ${chalk.yellow(
604
+ toolCount
605
+ )} 总计`
606
+ );
607
+ } else {
608
+ console.log(` 工具: ${chalk.gray("未扫描 (请先启动服务)")}`);
609
+ }
610
+ console.log();
611
+ }
612
+ }
613
+
614
+ console.log(chalk.gray("💡 提示:"));
615
+ console.log(
616
+ chalk.gray(" - 使用 'xiaozhi mcp list --tools' 查看所有工具")
617
+ );
618
+ console.log(
619
+ chalk.gray(" - 使用 'xiaozhi mcp <服务名> list' 查看指定服务的工具")
620
+ );
621
+ console.log(
622
+ chalk.gray(
623
+ " - 使用 'xiaozhi mcp <服务名> <工具名> enable/disable' 启用/禁用工具"
624
+ )
625
+ );
626
+ } catch (error) {
627
+ spinner.fail("获取 MCP 服务列表失败");
628
+ console.error(
629
+ chalk.red(
630
+ `错误: ${error instanceof Error ? error.message : String(error)}`
631
+ )
632
+ );
633
+ process.exit(1);
634
+ }
635
+ }
636
+
637
+ /**
638
+ * 列出指定服务的工具
639
+ */
640
+ private async handleServerInternal(serverName: string): Promise<void> {
641
+ const spinner = ora(`获取 ${serverName} 服务的工具列表...`).start();
642
+
643
+ try {
644
+ const mcpServers = configManager.getMcpServers();
645
+
646
+ if (!mcpServers[serverName]) {
647
+ spinner.fail(`服务 '${serverName}' 不存在`);
648
+ console.log(
649
+ chalk.yellow("💡 提示: 使用 'xiaozhi mcp list' 查看所有可用服务")
650
+ );
651
+ return;
652
+ }
653
+
654
+ const toolsConfig = configManager.getServerToolsConfig(serverName);
655
+ const toolNames = Object.keys(toolsConfig);
656
+
657
+ if (toolNames.length === 0) {
658
+ spinner.warn(`服务 '${serverName}' 暂无工具信息`);
659
+ console.log(chalk.yellow("💡 提示: 请先启动服务以扫描工具列表"));
660
+ return;
661
+ }
662
+
663
+ spinner.succeed(`服务 '${serverName}' 共有 ${toolNames.length} 个工具`);
664
+
665
+ console.log();
666
+ console.log(chalk.bold(`${serverName} 服务工具列表:`));
667
+ console.log();
668
+
669
+ // 使用 cli-table3 创建表格
670
+ const table = new Table({
671
+ head: [chalk.bold("工具名称"), chalk.bold("状态"), chalk.bold("描述")],
672
+ colWidths: [30, 8, 50], // 工具名称 | 状态 | 描述
673
+ wordWrap: true,
674
+ style: {
675
+ head: [],
676
+ border: [],
677
+ },
678
+ });
679
+
680
+ for (const toolName of toolNames) {
681
+ const toolConfig = toolsConfig[toolName];
682
+ const status = toolConfig.enable
683
+ ? chalk.green("启用")
684
+ : chalk.red("禁用");
685
+
686
+ // 截断描述到最大40个字符宽度(约20个中文字符)
687
+ const description = McpCommandHandler.truncateToWidth(
688
+ toolConfig.description || "",
689
+ 40
690
+ );
691
+
692
+ table.push([toolName, status, description]);
693
+ }
694
+
695
+ console.log(table.toString());
696
+
697
+ console.log();
698
+ console.log(chalk.gray("💡 提示:"));
699
+ console.log(
700
+ chalk.gray(
701
+ ` - 使用 'xiaozhi mcp ${serverName} <工具名> enable' 启用工具`
702
+ )
703
+ );
704
+ console.log(
705
+ chalk.gray(
706
+ ` - 使用 'xiaozhi mcp ${serverName} <工具名> disable' 禁用工具`
707
+ )
708
+ );
709
+ } catch (error) {
710
+ spinner.fail("获取工具列表失败");
711
+ console.error(
712
+ chalk.red(
713
+ `错误: ${error instanceof Error ? error.message : String(error)}`
714
+ )
715
+ );
716
+ process.exit(1);
717
+ }
718
+ }
719
+
720
+ /**
721
+ * 启用或禁用工具
722
+ */
723
+ private async handleToolInternal(
724
+ serverName: string,
725
+ toolName: string,
726
+ enabled: boolean
727
+ ): Promise<void> {
728
+ const action = enabled ? "启用" : "禁用";
729
+ const spinner = ora(`${action}工具 ${serverName}/${toolName}...`).start();
730
+
731
+ try {
732
+ const mcpServers = configManager.getMcpServers();
733
+
734
+ if (!mcpServers[serverName]) {
735
+ spinner.fail(`服务 '${serverName}' 不存在`);
736
+ console.log(
737
+ chalk.yellow("💡 提示: 使用 'xiaozhi mcp list' 查看所有可用服务")
738
+ );
739
+ return;
740
+ }
741
+
742
+ const toolsConfig = configManager.getServerToolsConfig(serverName);
743
+
744
+ if (!toolsConfig[toolName]) {
745
+ spinner.fail(`工具 '${toolName}' 在服务 '${serverName}' 中不存在`);
746
+ console.log(
747
+ chalk.yellow(
748
+ `💡 提示: 使用 'xiaozhi mcp ${serverName} list' 查看该服务的所有工具`
749
+ )
750
+ );
751
+ return;
752
+ }
753
+
754
+ // 更新工具状态
755
+ configManager.setToolEnabled(
756
+ serverName,
757
+ toolName,
758
+ enabled,
759
+ toolsConfig[toolName].description
760
+ );
761
+
762
+ spinner.succeed(
763
+ `成功${action}工具 ${chalk.cyan(serverName)}/${chalk.cyan(toolName)}`
764
+ );
765
+
766
+ console.log();
767
+ console.log(chalk.gray("💡 提示: 工具状态更改将在下次启动服务时生效"));
768
+ } catch (error) {
769
+ spinner.fail(`${action}工具失败`);
770
+ console.error(
771
+ chalk.red(
772
+ `错误: ${error instanceof Error ? error.message : String(error)}`
773
+ )
774
+ );
775
+ process.exit(1);
776
+ }
777
+ }
778
+ }