@xiaozhi-client/cli 1.9.4-beta.10
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.
- package/README.md +98 -0
- package/package.json +31 -0
- package/project.json +75 -0
- package/src/Constants.ts +105 -0
- package/src/Container.ts +212 -0
- package/src/Types.ts +79 -0
- package/src/commands/CommandHandlerFactory.ts +98 -0
- package/src/commands/ConfigCommandHandler.ts +279 -0
- package/src/commands/EndpointCommandHandler.ts +158 -0
- package/src/commands/McpCommandHandler.ts +778 -0
- package/src/commands/ProjectCommandHandler.ts +254 -0
- package/src/commands/ServiceCommandHandler.ts +182 -0
- package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
- package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
- package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
- package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
- package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
- package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
- package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
- package/src/commands/index.ts +351 -0
- package/src/errors/ErrorHandlers.ts +141 -0
- package/src/errors/ErrorMessages.ts +121 -0
- package/src/errors/__tests__/index.test.ts +186 -0
- package/src/errors/index.ts +163 -0
- package/src/global.d.ts +19 -0
- package/src/index.ts +53 -0
- package/src/interfaces/Command.ts +128 -0
- package/src/interfaces/CommandTypes.ts +95 -0
- package/src/interfaces/Config.ts +25 -0
- package/src/interfaces/Service.ts +99 -0
- package/src/services/DaemonManager.ts +318 -0
- package/src/services/ProcessManager.ts +235 -0
- package/src/services/ServiceManager.ts +319 -0
- package/src/services/TemplateManager.ts +382 -0
- package/src/services/__tests__/DaemonManager.test.ts +378 -0
- package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
- package/src/services/__tests__/ProcessManager.test.ts +296 -0
- package/src/services/__tests__/ServiceManager.test.ts +774 -0
- package/src/services/__tests__/TemplateManager.test.ts +337 -0
- package/src/types/backend.d.ts +48 -0
- package/src/utils/FileUtils.ts +320 -0
- package/src/utils/FormatUtils.ts +198 -0
- package/src/utils/PathUtils.ts +255 -0
- package/src/utils/PlatformUtils.ts +217 -0
- package/src/utils/Validation.ts +274 -0
- package/src/utils/VersionUtils.ts +141 -0
- package/src/utils/__tests__/FileUtils.test.ts +728 -0
- package/src/utils/__tests__/FormatUtils.test.ts +243 -0
- package/src/utils/__tests__/PathUtils.test.ts +1165 -0
- package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
- package/src/utils/__tests__/Validation.test.ts +560 -0
- package/src/utils/__tests__/VersionUtils.test.ts +410 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +100 -0
- 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
|
+
}
|