@xiaozhi-client/config 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.
package/src/adapter.ts ADDED
@@ -0,0 +1,432 @@
1
+ /**
2
+ * 配置适配器
3
+ * 将旧的配置格式转换为新的 MCPServiceConfig 格式,确保向后兼容性
4
+ */
5
+
6
+ import { isAbsolute, resolve } from "node:path";
7
+ import type {
8
+ LocalMCPServerConfig,
9
+ MCPServerConfig,
10
+ SSEMCPServerConfig,
11
+ StreamableHTTPMCPServerConfig,
12
+ } from "./manager.js";
13
+
14
+ // 从外部导入 MCP 类型(这些类型将在运行时从 backend 包解析)
15
+ // 为了避免循环依赖,这里使用动态导入的方式
16
+ // 在实际使用时,adapter 将作为 config 包的一部分被使用
17
+
18
+ /**
19
+ * 配置验证错误类
20
+ */
21
+ export class ConfigValidationError extends Error {
22
+ constructor(
23
+ message: string,
24
+ public readonly configName?: string
25
+ ) {
26
+ super(message);
27
+ this.name = "ConfigValidationError";
28
+ }
29
+ }
30
+
31
+ // 定义简化的 MCP 传输类型(与 core/mcp/types 保持一致)
32
+ export enum MCPTransportType {
33
+ STDIO = "stdio",
34
+ SSE = "sse",
35
+ STREAMABLE_HTTP = "streamable-http",
36
+ }
37
+
38
+ // 定义简化的 MCPServiceConfig 接口
39
+ export interface MCPServiceConfig {
40
+ name: string;
41
+ type: MCPTransportType;
42
+ command?: string;
43
+ args?: string[];
44
+ env?: Record<string, string>;
45
+ url?: string;
46
+ timeout?: number;
47
+ headers?: Record<string, string>;
48
+ modelScopeAuth?: boolean;
49
+ }
50
+
51
+ /**
52
+ * URL 类型推断函数
53
+ * 与 MCPService 的推断逻辑保持一致
54
+ * 基于 URL 路径末尾推断传输类型
55
+ */
56
+ function inferTransportTypeFromUrl(url: string): MCPTransportType {
57
+ try {
58
+ const parsedUrl = new URL(url);
59
+ const pathname = parsedUrl.pathname;
60
+
61
+ // 检查路径末尾
62
+ if (pathname.endsWith("/sse")) {
63
+ return MCPTransportType.SSE;
64
+ }
65
+ if (pathname.endsWith("/mcp")) {
66
+ return MCPTransportType.STREAMABLE_HTTP;
67
+ }
68
+
69
+ // 默认类型
70
+ return MCPTransportType.STREAMABLE_HTTP;
71
+ } catch (error) {
72
+ // URL 解析失败时使用默认类型
73
+ return MCPTransportType.STREAMABLE_HTTP;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 将旧的 MCPServerConfig 转换为新的 MCPServiceConfig
79
+ */
80
+ export function convertLegacyToNew(
81
+ serviceName: string,
82
+ legacyConfig: MCPServerConfig
83
+ ): MCPServiceConfig {
84
+ console.log("转换配置", { serviceName, legacyConfig });
85
+
86
+ try {
87
+ // 验证输入参数
88
+ if (!serviceName || typeof serviceName !== "string") {
89
+ throw new ConfigValidationError("服务名称必须是非空字符串");
90
+ }
91
+
92
+ if (!legacyConfig || typeof legacyConfig !== "object") {
93
+ throw new ConfigValidationError("配置对象不能为空", serviceName);
94
+ }
95
+
96
+ // 根据配置类型进行转换
97
+ const newConfig = convertByConfigType(serviceName, legacyConfig);
98
+
99
+ // 验证转换后的配置
100
+ validateNewConfig(newConfig);
101
+
102
+ console.log("配置转换成功", { serviceName, type: newConfig.type });
103
+ return newConfig;
104
+ } catch (error) {
105
+ console.error("配置转换失败", { serviceName, error });
106
+ throw error instanceof ConfigValidationError
107
+ ? error
108
+ : new ConfigValidationError(
109
+ `配置转换失败: ${error instanceof Error ? error.message : String(error)}`,
110
+ serviceName
111
+ );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * 根据配置类型进行转换
117
+ */
118
+ function convertByConfigType(
119
+ serviceName: string,
120
+ legacyConfig: MCPServerConfig
121
+ ): MCPServiceConfig {
122
+ // 检查是否为本地 stdio 配置(最高优先级)
123
+ if (isLocalConfig(legacyConfig)) {
124
+ return convertLocalConfig(serviceName, legacyConfig);
125
+ }
126
+
127
+ // 检查是否有显式指定的类型
128
+ if ("type" in legacyConfig) {
129
+ switch (legacyConfig.type) {
130
+ case "sse":
131
+ return convertSSEConfig(serviceName, legacyConfig);
132
+ case "streamable-http":
133
+ return convertStreamableHTTPConfig(serviceName, legacyConfig);
134
+ default:
135
+ throw new ConfigValidationError(
136
+ `不支持的传输类型: ${legacyConfig.type}`,
137
+ serviceName
138
+ );
139
+ }
140
+ }
141
+
142
+ // 检查是否为网络配置(自动推断类型)
143
+ if ("url" in legacyConfig) {
144
+ // 如果 URL 是 undefined 或 null,抛出错误
145
+ if (legacyConfig.url === undefined || legacyConfig.url === null) {
146
+ throw new ConfigValidationError(
147
+ "网络配置必须包含有效的 url 字段",
148
+ serviceName
149
+ );
150
+ }
151
+
152
+ // 先推断类型,然后根据推断的类型选择正确的转换函数
153
+ const inferredType = inferTransportTypeFromUrl(legacyConfig.url || "");
154
+
155
+ if (inferredType === MCPTransportType.SSE) {
156
+ // 为SSE类型添加显式type字段
157
+ const sseConfig = { ...legacyConfig, type: "sse" as const };
158
+ return convertSSEConfig(serviceName, sseConfig);
159
+ }
160
+ // 为STREAMABLE_HTTP类型添加显式type字段
161
+ const httpConfig = { ...legacyConfig, type: "streamable-http" as const };
162
+ return convertStreamableHTTPConfig(serviceName, httpConfig);
163
+ }
164
+
165
+ throw new ConfigValidationError("无法识别的配置类型", serviceName);
166
+ }
167
+
168
+ /** * 转换本地 stdio 配置 */
169
+ function convertLocalConfig(
170
+ serviceName: string,
171
+ config: LocalMCPServerConfig
172
+ ): MCPServiceConfig {
173
+ if (!config.command) {
174
+ throw new ConfigValidationError(
175
+ "本地配置必须包含 command 字段",
176
+ serviceName
177
+ );
178
+ }
179
+
180
+ // 获取用户的工作目录(优先使用环境变量,否则使用当前工作目录)
181
+ const workingDir = process.env.XIAOZHI_CONFIG_DIR || process.cwd();
182
+
183
+ // 解析 args 中的相对路径
184
+ const resolvedArgs = (config.args || []).map((arg) => {
185
+ // 检查是否为相对路径(以 ./ 开头或不以 / 开头且包含文件扩展名)
186
+ if (isRelativePath(arg)) {
187
+ const resolvedPath = resolve(workingDir, arg);
188
+ console.log("解析相对路径", { arg, resolvedPath });
189
+ return resolvedPath;
190
+ }
191
+ return arg;
192
+ });
193
+
194
+ return {
195
+ name: serviceName,
196
+ type: MCPTransportType.STDIO,
197
+ command: config.command,
198
+ args: resolvedArgs,
199
+ env: config.env, // 传递环境变量
200
+ timeout: 30000,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * 转换 SSE 配置
206
+ */
207
+ function convertSSEConfig(
208
+ serviceName: string,
209
+ config: SSEMCPServerConfig
210
+ ): MCPServiceConfig {
211
+ if (config.url === undefined || config.url === null) {
212
+ throw new ConfigValidationError("SSE 配置必须包含 url 字段", serviceName);
213
+ }
214
+
215
+ // 优先使用显式指定的类型,如果没有则进行推断
216
+ const inferredType =
217
+ config.type === "sse"
218
+ ? MCPTransportType.SSE
219
+ : inferTransportTypeFromUrl(config.url || "");
220
+ const isModelScope = config.url ? isModelScopeURL(config.url) : false;
221
+
222
+ const baseConfig: MCPServiceConfig = {
223
+ name: serviceName,
224
+ type: inferredType,
225
+ url: config.url,
226
+ timeout: 30000,
227
+ headers: config.headers,
228
+ };
229
+
230
+ // 如果是 ModelScope 服务,添加特殊配置
231
+ if (isModelScope) {
232
+ baseConfig.modelScopeAuth = true;
233
+ }
234
+
235
+ console.log("SSE配置转换", {
236
+ serviceName,
237
+ url: config.url,
238
+ inferredType,
239
+ isModelScope,
240
+ });
241
+
242
+ return baseConfig;
243
+ }
244
+
245
+ /**
246
+ * 转换 Streamable HTTP 配置
247
+ */
248
+ function convertStreamableHTTPConfig(
249
+ serviceName: string,
250
+ config: StreamableHTTPMCPServerConfig
251
+ ): MCPServiceConfig {
252
+ // 检查 URL 是否存在
253
+ if (config.url === undefined || config.url === null) {
254
+ throw new ConfigValidationError(
255
+ "STREAMABLE_HTTP 配置必须包含 url 字段",
256
+ serviceName
257
+ );
258
+ }
259
+
260
+ const url = config.url || "";
261
+
262
+ return {
263
+ name: serviceName,
264
+ type: MCPTransportType.STREAMABLE_HTTP,
265
+ url,
266
+ timeout: 30000,
267
+ headers: config.headers,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * 批量转换配置
273
+ */
274
+ export function convertLegacyConfigBatch(
275
+ legacyConfigs: Record<string, MCPServerConfig>
276
+ ): Record<string, MCPServiceConfig> {
277
+ const newConfigs: Record<string, MCPServiceConfig> = {};
278
+ const errors: Array<{ serviceName: string; error: Error }> = [];
279
+
280
+ for (const [serviceName, legacyConfig] of Object.entries(legacyConfigs)) {
281
+ try {
282
+ newConfigs[serviceName] = convertLegacyToNew(serviceName, legacyConfig);
283
+ } catch (error) {
284
+ errors.push({
285
+ serviceName,
286
+ error: error instanceof Error ? error : new Error(String(error)),
287
+ });
288
+ }
289
+ }
290
+
291
+ if (errors.length > 0) {
292
+ const errorMessages = errors
293
+ .map(({ serviceName, error }) => `${serviceName}: ${error.message}`)
294
+ .join("; ");
295
+ throw new ConfigValidationError(`批量配置转换失败: ${errorMessages}`);
296
+ }
297
+
298
+ console.log("批量配置转换成功", { count: Object.keys(newConfigs).length });
299
+ return newConfigs;
300
+ }
301
+
302
+ /**
303
+ * 检查是否为相对路径
304
+ */
305
+ function isRelativePath(path: string): boolean {
306
+ // 使用 Node.js 的 path.isAbsolute() 来正确检测绝对路径
307
+ // 这个方法能够正确处理 Windows、macOS、Linux 三个平台的路径格式
308
+ if (isAbsolute(path)) {
309
+ return false; // 绝对路径不是相对路径
310
+ }
311
+
312
+ // 检查是否为相对路径的条件:
313
+ // 1. 以 ./ 或 ../ 开头
314
+ // 2. 包含常见的脚本文件扩展名(且不是绝对路径)
315
+ if (path.startsWith("./") || path.startsWith("../")) {
316
+ return true;
317
+ }
318
+
319
+ // 如果包含文件扩展名且不是绝对路径,也认为是相对路径
320
+ if (/\.(js|py|ts|mjs|cjs)$/i.test(path)) {
321
+ return true;
322
+ }
323
+
324
+ return false;
325
+ }
326
+
327
+ /**
328
+ * 检查是否为本地配置
329
+ */
330
+ function isLocalConfig(
331
+ config: MCPServerConfig
332
+ ): config is LocalMCPServerConfig {
333
+ return "command" in config && typeof config.command === "string";
334
+ }
335
+
336
+ /**
337
+ * 检查是否为 ModelScope URL
338
+ * 使用 URL hostname 检查而非简单的字符串包含检查,防止安全绕过
339
+ */
340
+ export function isModelScopeURL(url: string): boolean {
341
+ try {
342
+ const parsedUrl = new URL(url);
343
+ const hostname = parsedUrl.hostname.toLowerCase();
344
+ return (
345
+ hostname.endsWith(".modelscope.net") ||
346
+ hostname.endsWith(".modelscope.cn") ||
347
+ hostname === "modelscope.net" ||
348
+ hostname === "modelscope.cn"
349
+ );
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * 验证新配置格式
357
+ */
358
+ function validateNewConfig(config: MCPServiceConfig): void {
359
+ if (!config.name || typeof config.name !== "string") {
360
+ throw new ConfigValidationError("配置必须包含有效的 name 字段");
361
+ }
362
+
363
+ if (config.type && !Object.values(MCPTransportType).includes(config.type)) {
364
+ throw new ConfigValidationError(`无效的传输类型: ${config.type}`);
365
+ }
366
+
367
+ // 根据传输类型验证必需字段
368
+ if (!config.type) {
369
+ throw new ConfigValidationError("传输类型未指定,请检查配置或启用自动推断");
370
+ }
371
+
372
+ switch (config.type) {
373
+ case MCPTransportType.STDIO:
374
+ if (!config.command) {
375
+ throw new ConfigValidationError("STDIO 配置必须包含 command 字段");
376
+ }
377
+ break;
378
+
379
+ case MCPTransportType.SSE:
380
+ // SSE 配置必须有 URL(即使是空字符串也会被推断为 STREAMABLE_HTTP)
381
+ if (config.url === undefined || config.url === null) {
382
+ throw new ConfigValidationError("SSE 配置必须包含 url 字段");
383
+ }
384
+ break;
385
+
386
+ case MCPTransportType.STREAMABLE_HTTP:
387
+ // STREAMABLE_HTTP 配置允许空 URL,会在后续处理中设置默认值
388
+ // 只有当 URL 完全不存在时才报错
389
+ if (config.url === undefined || config.url === null) {
390
+ throw new ConfigValidationError(
391
+ "STREAMABLE_HTTP 配置必须包含 url 字段"
392
+ );
393
+ }
394
+ break;
395
+
396
+ default:
397
+ throw new ConfigValidationError(`不支持的传输类型: ${config.type}`);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * 获取配置类型描述
403
+ */
404
+ export function getConfigTypeDescription(config: MCPServerConfig): string {
405
+ if (isLocalConfig(config)) {
406
+ return `本地进程 (${config.command})`;
407
+ }
408
+
409
+ if ("url" in config) {
410
+ // 检查是否为显式 streamable-http 配置
411
+ if ("type" in config && config.type === "streamable-http") {
412
+ return `Streamable HTTP (${config.url})`;
413
+ }
414
+
415
+ // 检查是否为显式 sse 配置
416
+ if ("type" in config && config.type === "sse") {
417
+ const isModelScope = isModelScopeURL(config.url);
418
+ return `SSE${isModelScope ? " (ModelScope)" : ""} (${config.url})`;
419
+ }
420
+
421
+ // 对于只有 url 的配置,根据路径推断类型
422
+ const inferredType = inferTransportTypeFromUrl(config.url);
423
+ const isModelScope = isModelScopeURL(config.url);
424
+
425
+ if (inferredType === MCPTransportType.SSE) {
426
+ return `SSE${isModelScope ? " (ModelScope)" : ""} (${config.url})`;
427
+ }
428
+ return `Streamable HTTP (${config.url})`;
429
+ }
430
+
431
+ return "未知类型";
432
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./manager.js";
2
+ export * from "./adapter.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * JSON5 注释保留适配器
3
+ * 使用 comment-json 实现 JSON5/JSONC 注释保留功能
4
+ *
5
+ * 注意:为了使用 comment-json 保留注释,JSON5 配置文件的键需要带引号。
6
+ * 这与 JSON5 标准语法允许不带引号的键略有不同,但能实现注释保留功能。
7
+ */
8
+ import * as commentJson from "comment-json";
9
+
10
+ /**
11
+ * JSON5 写入器适配器接口
12
+ * 保持与 json5-writer 兼容的 API
13
+ */
14
+ export interface Json5WriterAdapter {
15
+ write(data: unknown): void;
16
+ toSource(): string;
17
+ }
18
+
19
+ /**
20
+ * 创建 JSON5 写入器适配器
21
+ * @param content 原始 JSON5 内容字符串
22
+ * @returns Json5WriterAdapter 实例
23
+ */
24
+ export function createJson5Writer(content: string): Json5WriterAdapter {
25
+ // 使用 comment-json 解析原始内容
26
+ // comment-json 会保留注释信息在返回的对象中
27
+ const parsedData = commentJson.parse(content) as Record<string, unknown>;
28
+
29
+ return {
30
+ write(data: unknown): void {
31
+ // 通过 Object.assign 合并新数据
32
+ if (parsedData && typeof parsedData === "object" && data) {
33
+ Object.assign(parsedData, data);
34
+ }
35
+ },
36
+
37
+ toSource(): string {
38
+ // 使用 comment-json 序列化,保留注释和格式
39
+ return commentJson.stringify(parsedData, null, 2);
40
+ },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * 解析 JSON5 内容(带注释保留)
46
+ * @param content JSON5 内容字符串
47
+ * @returns 解析后的对象
48
+ */
49
+ export function parseJson5(content: string): unknown {
50
+ // 使用 comment-json 解析,支持注释保留
51
+ return commentJson.parse(content);
52
+ }