mcp-optimizer 0.0.1-alpha.1
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/.github/workflows/publish.yml +34 -0
- package/README.md +31 -0
- package/bin/mcp-optimizer.js +16 -0
- package/dist/fix/fixer.js +19 -0
- package/dist/index.js +12 -0
- package/dist/mcpServer.js +351 -0
- package/dist/runner/lighthouseRunner.js +66 -0
- package/dist/server.js +79 -0
- package/docs/MCP.md +21 -0
- package/package.json +43 -0
- package/server.ts +600 -0
- package/src/fix/fixer.ts +18 -0
- package/src/index.ts +10 -0
- package/src/mcpServer.ts +335 -0
- package/src/runner/lighthouseRunner.ts +66 -0
- package/src/types/modelcontextprotocol__sdk.d.ts +4 -0
- package/tsconfig.json +13 -0
package/server.ts
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import express, { Request, Response } from "express";
|
|
5
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
7
|
+
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
8
|
+
|
|
9
|
+
import { YApiService } from "./services/yapi/api";
|
|
10
|
+
import { ProjectInfoCache } from "./services/yapi/cache";
|
|
11
|
+
import { Logger } from "./services/yapi/logger";
|
|
12
|
+
|
|
13
|
+
export class YapiMcpServer {
|
|
14
|
+
private readonly server: McpServer;
|
|
15
|
+
private readonly yapiService: YApiService;
|
|
16
|
+
private readonly projectInfoCache: ProjectInfoCache;
|
|
17
|
+
private readonly logger: Logger;
|
|
18
|
+
private sseTransport: SSEServerTransport | null = null;
|
|
19
|
+
private readonly isStdioMode: boolean;
|
|
20
|
+
|
|
21
|
+
constructor(yapiBaseUrl: string, yapiToken: string, yapiLogLevel: string = "info", yapiCacheTTL: number = 10) {
|
|
22
|
+
this.logger = new Logger("YapiMCP", yapiLogLevel);
|
|
23
|
+
this.yapiService = new YApiService(yapiBaseUrl, yapiToken, yapiLogLevel);
|
|
24
|
+
this.projectInfoCache = new ProjectInfoCache(yapiCacheTTL);
|
|
25
|
+
// 判断是否为stdio模式
|
|
26
|
+
this.isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");
|
|
27
|
+
|
|
28
|
+
this.logger.info(`YapiMcpServer初始化,日志级别: ${yapiLogLevel}, 缓存TTL: ${yapiCacheTTL}分钟`);
|
|
29
|
+
|
|
30
|
+
this.server = new McpServer({
|
|
31
|
+
name: "Yapi MCP Server",
|
|
32
|
+
version: "0.2.1",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.registerTools();
|
|
36
|
+
this.initializeCache();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async initializeCache(): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
// 检查缓存是否过期
|
|
42
|
+
if (this.projectInfoCache.isCacheExpired()) {
|
|
43
|
+
this.logger.info('缓存已过期,将异步更新缓存数据');
|
|
44
|
+
|
|
45
|
+
// 异步加载最新的项目信息,不阻塞初始化过程
|
|
46
|
+
this.asyncUpdateCache().catch(error => {
|
|
47
|
+
this.logger.error('异步更新缓存失败:', error);
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
// 从缓存加载数据
|
|
51
|
+
const cachedProjectInfo = this.projectInfoCache.loadFromCache();
|
|
52
|
+
|
|
53
|
+
// 如果缓存中有数据,直接使用
|
|
54
|
+
if (cachedProjectInfo.size > 0) {
|
|
55
|
+
// 将缓存数据设置到服务中
|
|
56
|
+
cachedProjectInfo.forEach((info, id) => {
|
|
57
|
+
this.yapiService.getProjectInfoCache().set(id, info);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.logger.info(`已从缓存加载 ${cachedProjectInfo.size} 个项目信息`);
|
|
61
|
+
} else {
|
|
62
|
+
// 缓存为空,异步更新
|
|
63
|
+
this.logger.info('缓存为空,将异步更新缓存数据');
|
|
64
|
+
this.asyncUpdateCache().catch(error => {
|
|
65
|
+
this.logger.error('异步更新缓存失败:', error);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.logger.error('加载或检查缓存时出错:', error);
|
|
71
|
+
|
|
72
|
+
// 出错时也尝试异步更新缓存
|
|
73
|
+
this.asyncUpdateCache().catch(err => {
|
|
74
|
+
this.logger.error('异步更新缓存失败:', err);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 异步更新缓存数据
|
|
81
|
+
* 该方法会在后台加载最新的项目信息和分类列表,并更新缓存
|
|
82
|
+
*/
|
|
83
|
+
private async asyncUpdateCache(): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
this.logger.debug('开始异步更新缓存数据');
|
|
86
|
+
|
|
87
|
+
// 加载最新的项目信息
|
|
88
|
+
await this.yapiService.loadAllProjectInfo();
|
|
89
|
+
this.logger.debug(`已加载 ${this.yapiService.getProjectInfoCache().size} 个项目信息`);
|
|
90
|
+
|
|
91
|
+
// 更新缓存
|
|
92
|
+
this.projectInfoCache.saveToCache(this.yapiService.getProjectInfoCache());
|
|
93
|
+
|
|
94
|
+
// 加载所有项目的分类列表
|
|
95
|
+
await this.yapiService.loadAllCategoryLists();
|
|
96
|
+
this.logger.debug('已加载所有项目的分类列表');
|
|
97
|
+
|
|
98
|
+
this.logger.info('缓存数据已成功更新');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.logger.error('更新缓存数据失败:', error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private registerTools(): void {
|
|
106
|
+
// 获取API接口详情
|
|
107
|
+
this.server.tool(
|
|
108
|
+
"yapi_get_api_desc",
|
|
109
|
+
"获取YApi中特定接口的详细信息",
|
|
110
|
+
{
|
|
111
|
+
projectId: z.string().describe("YApi项目ID;如连接/project/28/interface/api/66,则ID为28"),
|
|
112
|
+
apiId: z.string().describe("YApi接口的ID;如连接/project/1/interface/api/66,则ID为66")
|
|
113
|
+
},
|
|
114
|
+
async ({ projectId, apiId }) => {
|
|
115
|
+
try {
|
|
116
|
+
this.logger.info(`获取API接口: ${apiId}, 项目ID: ${projectId}`);
|
|
117
|
+
const apiInterface = await this.yapiService.getApiInterface(projectId, apiId);
|
|
118
|
+
this.logger.info(`成功获取API接口: ${apiInterface.title || apiId}`);
|
|
119
|
+
|
|
120
|
+
// 格式化返回数据,使其更易于阅读
|
|
121
|
+
const formattedResponse = {
|
|
122
|
+
基本信息: {
|
|
123
|
+
接口ID: apiInterface._id,
|
|
124
|
+
接口名称: apiInterface.title,
|
|
125
|
+
接口路径: apiInterface.path,
|
|
126
|
+
请求方式: apiInterface.method,
|
|
127
|
+
接口描述: apiInterface.desc
|
|
128
|
+
},
|
|
129
|
+
请求参数: {
|
|
130
|
+
URL参数: apiInterface.req_params,
|
|
131
|
+
查询参数: apiInterface.req_query,
|
|
132
|
+
请求头: apiInterface.req_headers,
|
|
133
|
+
请求体类型: apiInterface.req_body_type,
|
|
134
|
+
表单参数: apiInterface.req_body_form,
|
|
135
|
+
Json参数: apiInterface.req_body_other
|
|
136
|
+
},
|
|
137
|
+
响应信息: {
|
|
138
|
+
响应类型: apiInterface.res_body_type,
|
|
139
|
+
响应内容: apiInterface.res_body
|
|
140
|
+
},
|
|
141
|
+
其他信息: {
|
|
142
|
+
接口文档: apiInterface.markdown
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: JSON.stringify(formattedResponse, null, 2) }],
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.logger.error(`获取API接口 ${apiId} 时出错:`, error);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: `获取API接口出错: ${error}` }],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// 保存API接口
|
|
159
|
+
this.server.tool(
|
|
160
|
+
"yapi_save_api",
|
|
161
|
+
"新增或更新YApi中的接口信息",
|
|
162
|
+
{
|
|
163
|
+
projectId: z.string().describe("YApi项目ID"),
|
|
164
|
+
catid: z.string().describe("接口分类ID,新增接口时必填"),
|
|
165
|
+
id: z.string().optional().describe("接口ID,更新时必填,新增时不需要"),
|
|
166
|
+
title: z.string().describe("接口标题"),
|
|
167
|
+
path: z.string().describe("接口路径,如:/api/user"),
|
|
168
|
+
method: z.string().describe("请求方法,如:GET, POST, PUT, DELETE等"),
|
|
169
|
+
status: z.string().optional().describe("接口状态,done代表完成,undone代表未完成"),
|
|
170
|
+
tag: z.string().optional().describe("接口标签列表"),
|
|
171
|
+
req_params: z.string().optional().describe("路径参数,JSON格式数组,如:[{\"name\":\"id\",\"desc\":\"用户ID\"}]"),
|
|
172
|
+
req_query: z.string().optional().describe("查询参数,JSON格式数组,如:[{\"name\":\"page\",\"desc\":\"页码\",\"required\":\"1\"}]"),
|
|
173
|
+
req_headers: z.string().optional().describe("请求头参数,JSON格式数组,如:[{\"name\":\"Content-Type\",\"value\":\"application/json\"}]"),
|
|
174
|
+
req_body_type: z.string().optional().describe("请求体类型,如:form, json, file, raw"),
|
|
175
|
+
req_body_form: z.string().optional().describe("表单请求体,JSON格式数组"),
|
|
176
|
+
req_body_other: z.string().optional().describe("其他请求体(通常是JSON格式)"),
|
|
177
|
+
req_body_is_json_schema: z.boolean().optional().describe("是否开启JSON Schema,默认false"),
|
|
178
|
+
res_body_type: z.string().optional().describe("返回数据类型,如:json, raw"),
|
|
179
|
+
res_body: z.string().optional().describe("返回数据,如果res_body_is_json_schema为true则用json schema格式"),
|
|
180
|
+
res_body_is_json_schema: z.boolean().optional().describe("返回数据是否为JSON Schema,默认false"),
|
|
181
|
+
switch_notice: z.boolean().optional().describe("开启接口运行通知,默认true"),
|
|
182
|
+
api_opened: z.boolean().optional().describe("开启API文档页面,默认true"),
|
|
183
|
+
desc: z.string().optional().describe("接口描述"),
|
|
184
|
+
markdown: z.string().optional().describe("markdown格式的接口描述")
|
|
185
|
+
},
|
|
186
|
+
async ({
|
|
187
|
+
projectId,
|
|
188
|
+
catid,
|
|
189
|
+
id,
|
|
190
|
+
title,
|
|
191
|
+
path,
|
|
192
|
+
method,
|
|
193
|
+
status,
|
|
194
|
+
tag,
|
|
195
|
+
req_params,
|
|
196
|
+
req_query,
|
|
197
|
+
req_headers,
|
|
198
|
+
req_body_type,
|
|
199
|
+
req_body_form,
|
|
200
|
+
req_body_other,
|
|
201
|
+
req_body_is_json_schema,
|
|
202
|
+
res_body_type,
|
|
203
|
+
res_body,
|
|
204
|
+
res_body_is_json_schema,
|
|
205
|
+
switch_notice,
|
|
206
|
+
api_opened,
|
|
207
|
+
desc,
|
|
208
|
+
markdown
|
|
209
|
+
}) => {
|
|
210
|
+
try {
|
|
211
|
+
// 准备接口参数
|
|
212
|
+
const params = {
|
|
213
|
+
project_id: projectId,
|
|
214
|
+
catid,
|
|
215
|
+
title,
|
|
216
|
+
path,
|
|
217
|
+
method,
|
|
218
|
+
status: status || 'undone',
|
|
219
|
+
tag: tag ? JSON.parse(tag) : [],
|
|
220
|
+
desc: desc || "",
|
|
221
|
+
markdown: markdown || ""
|
|
222
|
+
} as any;
|
|
223
|
+
|
|
224
|
+
// 有ID则是更新,否则是新增
|
|
225
|
+
if (id) {
|
|
226
|
+
params.id = id;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 处理可选参数,将字符串JSON转为对象
|
|
230
|
+
if (req_params) {
|
|
231
|
+
try {
|
|
232
|
+
params.req_params = JSON.parse(req_params);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text: `路径参数JSON解析错误: ${e}` }],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (req_query) {
|
|
241
|
+
try {
|
|
242
|
+
params.req_query = JSON.parse(req_query);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: "text", text: `查询参数JSON解析错误: ${e}` }],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (req_headers) {
|
|
251
|
+
try {
|
|
252
|
+
params.req_headers = JSON.parse(req_headers);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: `请求头参数JSON解析错误: ${e}` }],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (req_body_type) {
|
|
261
|
+
params.req_body_type = req_body_type;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (req_body_form) {
|
|
265
|
+
try {
|
|
266
|
+
params.req_body_form = JSON.parse(req_body_form);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: `表单请求体JSON解析错误: ${e}` }],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (req_body_other) {
|
|
275
|
+
params.req_body_other = req_body_other;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (req_body_is_json_schema !== undefined) {
|
|
279
|
+
params.req_body_is_json_schema = req_body_is_json_schema;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (res_body_type) {
|
|
283
|
+
params.res_body_type = res_body_type;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (res_body) {
|
|
287
|
+
params.res_body = res_body;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (res_body_is_json_schema !== undefined) {
|
|
291
|
+
params.res_body_is_json_schema = res_body_is_json_schema;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (switch_notice !== undefined) {
|
|
295
|
+
params.switch_notice = switch_notice;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (api_opened !== undefined) {
|
|
299
|
+
params.api_opened = api_opened;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 调用API保存接口
|
|
303
|
+
const response = await this.yapiService.saveInterface(params);
|
|
304
|
+
|
|
305
|
+
// 返回保存结果
|
|
306
|
+
const resultApiId = response.data._id;
|
|
307
|
+
return {
|
|
308
|
+
content: [{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: `接口${id ? '更新' : '新增'}成功!\n接口ID: ${resultApiId}\n接口名称: ${title}\n请求方法: ${method}\n接口路径: ${path}`
|
|
311
|
+
}],
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.logger.error(`保存API接口时出错:`, error);
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: `保存API接口出错: ${error}` }],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// 搜索API接口
|
|
323
|
+
this.server.tool(
|
|
324
|
+
"yapi_search_apis",
|
|
325
|
+
"搜索YApi中的接口",
|
|
326
|
+
{
|
|
327
|
+
projectKeyword: z.string().optional().describe("项目关键字,用于过滤项目"),
|
|
328
|
+
nameKeyword: z.string().optional().describe("接口名称关键字"),
|
|
329
|
+
pathKeyword: z.string().optional().describe("接口路径关键字"),
|
|
330
|
+
tagKeyword: z.string().optional().describe("接口标签关键字"),
|
|
331
|
+
limit: z.number().optional().describe("返回结果数量限制,默认20")
|
|
332
|
+
},
|
|
333
|
+
async ({ projectKeyword, nameKeyword, pathKeyword, tagKeyword, limit }) => {
|
|
334
|
+
try {
|
|
335
|
+
const searchOptions = {
|
|
336
|
+
projectKeyword,
|
|
337
|
+
nameKeyword: nameKeyword ? nameKeyword.split(/[\s,]+/) : undefined,
|
|
338
|
+
pathKeyword: pathKeyword ? pathKeyword.split(/[\s,]+/) : undefined,
|
|
339
|
+
tagKeyword: tagKeyword ? tagKeyword.split(/[\s,]+/) : undefined,
|
|
340
|
+
limit: limit || 20
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
this.logger.info(`搜索API接口: ${JSON.stringify(searchOptions)}`);
|
|
344
|
+
const searchResults = await this.yapiService.searchApis(searchOptions);
|
|
345
|
+
|
|
346
|
+
// 按项目分组整理结果
|
|
347
|
+
const apisByProject: Record<string, {
|
|
348
|
+
projectName: string,
|
|
349
|
+
apis: Array<{
|
|
350
|
+
id: string,
|
|
351
|
+
title: string,
|
|
352
|
+
path: string,
|
|
353
|
+
method: string,
|
|
354
|
+
catName: string,
|
|
355
|
+
createTime: string,
|
|
356
|
+
updateTime: string
|
|
357
|
+
}>
|
|
358
|
+
}> = {};
|
|
359
|
+
|
|
360
|
+
// 格式化搜索结果
|
|
361
|
+
searchResults.list.forEach(api => {
|
|
362
|
+
const projectId = String(api.project_id);
|
|
363
|
+
const projectName = api.project_name || `未知项目(${projectId})`;
|
|
364
|
+
|
|
365
|
+
if (!apisByProject[projectId]) {
|
|
366
|
+
apisByProject[projectId] = {
|
|
367
|
+
projectName,
|
|
368
|
+
apis: []
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
apisByProject[projectId].apis.push({
|
|
373
|
+
id: api._id,
|
|
374
|
+
title: api.title,
|
|
375
|
+
path: api.path,
|
|
376
|
+
method: api.method,
|
|
377
|
+
catName: api.cat_name || '未知分类',
|
|
378
|
+
createTime: new Date(api.add_time).toLocaleString(),
|
|
379
|
+
updateTime: new Date(api.up_time).toLocaleString()
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// 构建响应内容
|
|
384
|
+
let responseContent = `共找到 ${searchResults.total} 个符合条件的接口(已限制显示 ${searchResults.list.length} 个)\n\n`;
|
|
385
|
+
|
|
386
|
+
// 添加搜索条件说明
|
|
387
|
+
responseContent += "搜索条件:\n";
|
|
388
|
+
if (projectKeyword) responseContent += `- 项目关键字: ${projectKeyword}\n`;
|
|
389
|
+
if (nameKeyword) responseContent += `- 接口名称关键字: ${nameKeyword}\n`;
|
|
390
|
+
if (pathKeyword) responseContent += `- API路径关键字: ${pathKeyword}\n`;
|
|
391
|
+
if (tagKeyword) responseContent += `- 标签关键字: ${tagKeyword}\n\n`;
|
|
392
|
+
|
|
393
|
+
// 按项目分组展示结果
|
|
394
|
+
Object.values(apisByProject).forEach(projectGroup => {
|
|
395
|
+
responseContent += `## 项目: ${projectGroup.projectName} (${projectGroup.apis.length}个接口)\n\n`;
|
|
396
|
+
|
|
397
|
+
if (projectGroup.apis.length <= 10) {
|
|
398
|
+
// 少量接口,展示详细信息
|
|
399
|
+
projectGroup.apis.forEach(api => {
|
|
400
|
+
responseContent += `### ${api.title} (${api.method} ${api.path})\n\n`;
|
|
401
|
+
responseContent += `- 接口ID: ${api.id}\n`;
|
|
402
|
+
responseContent += `- 所属分类: ${api.catName}\n`;
|
|
403
|
+
responseContent += `- 更新时间: ${api.updateTime}\n\n`;
|
|
404
|
+
});
|
|
405
|
+
} else {
|
|
406
|
+
// 大量接口,展示简洁表格
|
|
407
|
+
responseContent += "| 接口ID | 接口名称 | 请求方式 | 接口路径 | 所属分类 |\n";
|
|
408
|
+
responseContent += "| ------ | -------- | -------- | -------- | -------- |\n";
|
|
409
|
+
|
|
410
|
+
projectGroup.apis.forEach(api => {
|
|
411
|
+
responseContent += `| ${api.id} | ${api.title} | ${api.method} | ${api.path} | ${api.catName} |\n`;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
responseContent += "\n";
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// 添加使用提示
|
|
419
|
+
responseContent += "\n提示: 可以使用 `get_api_desc` 工具获取接口的详细信息,例如: `get_api_desc projectId=228 apiId=8570`";
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: "text", text: responseContent }],
|
|
423
|
+
};
|
|
424
|
+
} catch (error) {
|
|
425
|
+
this.logger.error(`搜索接口时出错:`, error);
|
|
426
|
+
let errorMsg = "搜索接口时发生错误";
|
|
427
|
+
|
|
428
|
+
if (error instanceof Error) {
|
|
429
|
+
errorMsg += `: ${error.message}`;
|
|
430
|
+
} else if (typeof error === 'object' && error !== null) {
|
|
431
|
+
errorMsg += `: ${JSON.stringify(error)}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: errorMsg }],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// 列出项目
|
|
442
|
+
this.server.tool(
|
|
443
|
+
"yapi_list_projects",
|
|
444
|
+
"列出YApi的项目ID(projectId)和项目名称",
|
|
445
|
+
{},
|
|
446
|
+
async () => {
|
|
447
|
+
try {
|
|
448
|
+
// 获取项目信息缓存
|
|
449
|
+
const projectInfoCache = this.yapiService.getProjectInfoCache();
|
|
450
|
+
|
|
451
|
+
if (projectInfoCache.size === 0) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: "没有找到任何项目信息,请检查配置的token是否正确" }],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 构建项目信息列表
|
|
458
|
+
const projectsList = Array.from(projectInfoCache.entries()).map(([id, info]) => ({
|
|
459
|
+
项目ID: id,
|
|
460
|
+
项目名称: info.name,
|
|
461
|
+
项目描述: info.desc || '无描述',
|
|
462
|
+
基础路径: info.basepath || '/',
|
|
463
|
+
项目分组ID: info.group_id
|
|
464
|
+
}));
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
content: [{
|
|
468
|
+
type: "text",
|
|
469
|
+
text: `已配置 ${projectInfoCache.size} 个YApi项目:\n\n${JSON.stringify(projectsList, null, 2)}`
|
|
470
|
+
}],
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
this.logger.error(`获取项目信息列表时出错:`, error);
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: `获取项目信息列表出错: ${error}` }],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
// 获取分类
|
|
482
|
+
this.server.tool(
|
|
483
|
+
"yapi_get_categories",
|
|
484
|
+
"获取YApi项目下的接口分类列表,以及每个分类下的接口信息",
|
|
485
|
+
{
|
|
486
|
+
projectId: z.string().describe("YApi项目ID")
|
|
487
|
+
},
|
|
488
|
+
async ({ projectId }) => {
|
|
489
|
+
try {
|
|
490
|
+
// 获取项目信息
|
|
491
|
+
const projectInfo = this.yapiService.getProjectInfoCache().get(projectId);
|
|
492
|
+
if (!projectInfo) {
|
|
493
|
+
return {
|
|
494
|
+
content: [{ type: "text", text: `未找到项目ID为 ${projectId} 的项目信息,请确认项目ID正确` }],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 获取项目下的分类列表
|
|
499
|
+
const categoryList = this.yapiService.getCategoryListCache().get(projectId);
|
|
500
|
+
|
|
501
|
+
if (!categoryList || categoryList.length === 0) {
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: `项目 "${projectInfo.name}" (ID: ${projectId}) 下没有找到任何接口分类` }],
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 构建包含接口列表的分类信息
|
|
508
|
+
const categoriesWithApisPromises = categoryList.map(async (cat) => {
|
|
509
|
+
// 获取分类下的接口列表
|
|
510
|
+
try {
|
|
511
|
+
const apis = await this.yapiService.getCategoryApis(projectId, cat._id);
|
|
512
|
+
|
|
513
|
+
// 将接口信息简化为所需字段
|
|
514
|
+
const simplifiedApis = apis?.map(api => ({
|
|
515
|
+
接口ID: api._id,
|
|
516
|
+
接口名称: api.title,
|
|
517
|
+
接口路径: api.path,
|
|
518
|
+
请求方法: api.method
|
|
519
|
+
})) || [];
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
分类ID: cat._id,
|
|
523
|
+
分类名称: cat.name,
|
|
524
|
+
分类描述: cat.desc || '无描述',
|
|
525
|
+
创建时间: new Date(cat.add_time).toLocaleString(),
|
|
526
|
+
更新时间: new Date(cat.up_time).toLocaleString(),
|
|
527
|
+
接口列表: simplifiedApis
|
|
528
|
+
};
|
|
529
|
+
} catch (error) {
|
|
530
|
+
this.logger.error(`获取分类 ${cat._id} 下的接口列表失败:`, error);
|
|
531
|
+
// 发生错误时仍然返回分类信息,但不包含接口列表
|
|
532
|
+
return {
|
|
533
|
+
分类ID: cat._id,
|
|
534
|
+
分类名称: cat.name,
|
|
535
|
+
分类描述: cat.desc || '无描述',
|
|
536
|
+
创建时间: new Date(cat.add_time).toLocaleString(),
|
|
537
|
+
更新时间: new Date(cat.up_time).toLocaleString(),
|
|
538
|
+
接口列表: [],
|
|
539
|
+
错误: `获取接口列表失败: ${error}`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// 等待所有分类的接口列表加载完成
|
|
545
|
+
const categoriesWithApis = await Promise.all(categoriesWithApisPromises);
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
content: [{
|
|
549
|
+
type: "text",
|
|
550
|
+
text: `项目 "${projectInfo.name}" (ID: ${projectId}) 下共有 ${categoryList.length} 个接口分类:\n\n${JSON.stringify(categoriesWithApis, null, 2)}`
|
|
551
|
+
}],
|
|
552
|
+
};
|
|
553
|
+
} catch (error) {
|
|
554
|
+
this.logger.error(`获取接口分类列表时出错:`, error);
|
|
555
|
+
return {
|
|
556
|
+
content: [{ type: "text", text: `获取接口分类列表出错: ${error}` }],
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async connect(transport: Transport): Promise<void> {
|
|
564
|
+
this.logger.info("连接到传输层...");
|
|
565
|
+
await this.server.connect(transport);
|
|
566
|
+
this.logger.info("服务器已连接,准备处理请求");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async startHttpServer(port: number): Promise<void> {
|
|
570
|
+
const app = express();
|
|
571
|
+
|
|
572
|
+
app.get("/sse", async (req: Request, res: Response) => {
|
|
573
|
+
this.logger.info("建立新的SSE连接");
|
|
574
|
+
this.sseTransport = new SSEServerTransport(
|
|
575
|
+
"/messages",
|
|
576
|
+
res as unknown as ServerResponse<IncomingMessage>,
|
|
577
|
+
);
|
|
578
|
+
await this.server.connect(this.sseTransport);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
app.post("/messages", async (req: Request, res: Response) => {
|
|
582
|
+
if (!this.sseTransport) {
|
|
583
|
+
// Express types 可能与实际使用不匹配,直接使用
|
|
584
|
+
// @ts-ignore
|
|
585
|
+
res.sendStatus(400);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
await this.sseTransport.handlePostMessage(
|
|
589
|
+
req as unknown as IncomingMessage,
|
|
590
|
+
res as unknown as ServerResponse<IncomingMessage>,
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
app.listen(port, () => {
|
|
595
|
+
this.logger.info(`HTTP服务器监听端口 ${port}`);
|
|
596
|
+
this.logger.info(`SSE端点: http://localhost:${port}/sse`);
|
|
597
|
+
this.logger.info(`消息端点: http://localhost:${port}/messages`);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
package/src/fix/fixer.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function autoFixFromReport(report: any, opts?: { onlyFailures?: boolean }): Promise<any> {
|
|
2
|
+
const performance = report.lhr?.categories?.performance || null;
|
|
3
|
+
const audits = report.lhr?.audits || {};
|
|
4
|
+
let failures: any = null;
|
|
5
|
+
if (opts?.onlyFailures) {
|
|
6
|
+
failures = Object.fromEntries(
|
|
7
|
+
Object.entries(audits).filter(([, v]: any) => {
|
|
8
|
+
const score = v && (v.score ?? v.scoreDisplayMode === 'notApplicable' ? 1 : v.score);
|
|
9
|
+
return typeof score === 'number' ? score < 1 : false;
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
suggestion: 'Review performance opportunities and apply targeted fixes',
|
|
15
|
+
performance,
|
|
16
|
+
failures,
|
|
17
|
+
};
|
|
18
|
+
}
|