@zzp123/mcp-zentao 1.15.0 → 1.17.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.
@@ -0,0 +1,916 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { ZentaoAPI } from './api/zentaoApi.js';
6
+ import { loadConfig, saveConfig } from './config.js';
7
+ // 解析命令行参数
8
+ const args = process.argv.slice(2);
9
+ let configData = null;
10
+ // 查找 --config 参数
11
+ const configIndex = args.indexOf('--config');
12
+ if (configIndex !== -1 && configIndex + 1 < args.length) {
13
+ try {
14
+ // 获取 --config 后面的 JSON 字符串并解析
15
+ const jsonStr = args[configIndex + 1];
16
+ configData = JSON.parse(jsonStr);
17
+ console.log('成功解析配置数据:', configData);
18
+ // 如果配置数据中包含 config 对象,则保存配置
19
+ if (configData.config) {
20
+ console.log('正在保存配置...');
21
+ saveConfig(configData.config);
22
+ }
23
+ }
24
+ catch (error) {
25
+ console.error('配置解析失败:', error);
26
+ process.exit(1);
27
+ }
28
+ }
29
+ // Create an MCP server
30
+ const server = new McpServer({
31
+ name: "Zentao API (测试工程师)",
32
+ version: "1.17.0"
33
+ });
34
+ // Initialize ZentaoAPI instance
35
+ let zentaoApi = null;
36
+ export default async function main(params) {
37
+ console.log('接收到的参数:', params);
38
+ // 如果传入了配置信息,就保存它
39
+ if (params.config) {
40
+ console.log('保存新的配置信息...');
41
+ saveConfig(params.config);
42
+ }
43
+ }
44
+ // Add Zentao configuration tool
45
+ server.tool("initZentao", {}, async ({}) => {
46
+ let config;
47
+ // 尝试从配置文件加载配置
48
+ const savedConfig = loadConfig();
49
+ if (!savedConfig) {
50
+ throw new Error("No configuration found. Please provide complete Zentao configuration.");
51
+ }
52
+ config = savedConfig;
53
+ zentaoApi = new ZentaoAPI(config);
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
56
+ };
57
+ });
58
+ server.tool("getMyTasks", {
59
+ status: z.enum(['wait', 'doing', 'done', 'all']).optional()
60
+ }, async ({ status }) => {
61
+ if (!zentaoApi)
62
+ throw new Error("Please initialize Zentao API first");
63
+ const tasks = await zentaoApi.getMyTasks(status);
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }]
66
+ };
67
+ });
68
+ server.tool("getTaskDetail", {
69
+ taskId: z.number()
70
+ }, async ({ taskId }) => {
71
+ if (!zentaoApi)
72
+ throw new Error("Please initialize Zentao API first");
73
+ const task = await zentaoApi.getTaskDetail(taskId);
74
+ return {
75
+ content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
76
+ };
77
+ });
78
+ server.tool("getMyBugs", "获取Bug列表 - 支持多种状态筛选(指派给我、由我创建、未关闭等)和分支筛选", {
79
+ status: z.enum([
80
+ 'active', // 激活状态
81
+ 'resolved', // 已解决
82
+ 'closed', // 已关闭
83
+ 'all', // 所有状态
84
+ 'assigntome', // 指派给我的
85
+ 'openedbyme', // 由我创建
86
+ 'resolvedbyme', // 由我解决
87
+ 'assignedbyme', // 由我指派
88
+ 'assigntonull', // 未指派
89
+ 'unconfirmed', // 未确认
90
+ 'unclosed', // 未关闭
91
+ 'unresolved', // 未解决(激活状态)
92
+ 'toclosed', // 待关闭(已解决)
93
+ 'postponedbugs', // 延期的Bug
94
+ 'longlifebugs', // 长期未处理的Bug
95
+ 'overduebugs', // 已过期的Bug
96
+ 'review', // 待我审核
97
+ 'feedback', // 用户反馈
98
+ 'needconfirm', // 需求变更需确认
99
+ 'bysearch' // 自定义搜索
100
+ ]).optional().describe("Bug状态筛选,默认返回所有状态"),
101
+ productId: z.number().optional().describe("产品ID,默认使用第二个产品"),
102
+ page: z.number().optional().describe("页码,从1开始,默认为1"),
103
+ limit: z.number().optional().describe("每页数量,默认20,最大100"),
104
+ branch: z.string().optional().describe("分支筛选:'all'(所有分支,默认)、'0'(主干分支)、分支ID"),
105
+ order: z.string().optional().describe("排序方式,如 id_desc(ID降序), pri_desc(优先级降序), openedDate_desc(创建时间降序)等,默认id_desc"),
106
+ fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(基本字段)、'default'(默认字段,推荐)、'full'(完整字段),默认'default'")
107
+ }, async ({ status, productId, page, limit, branch, order, fields }) => {
108
+ if (!zentaoApi)
109
+ throw new Error("Please initialize Zentao API first");
110
+ const result = await zentaoApi.getMyBugs(status, productId, page, limit, branch, order, fields);
111
+ // 返回分页信息和数据
112
+ const summary = {
113
+ summary: `当前第 ${result.page} 页,共 ${result.total} 个Bug,本页显示 ${result.bugs.length} 个`,
114
+ pagination: {
115
+ page: result.page,
116
+ limit: result.limit,
117
+ total: result.total,
118
+ totalPages: Math.ceil(result.total / result.limit)
119
+ },
120
+ bugs: result.bugs
121
+ };
122
+ return {
123
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
124
+ };
125
+ });
126
+ server.tool("getProductBugs", "获取指定产品的Bug列表 - 支持多种状态筛选和分支筛选", {
127
+ productId: z.number().describe("产品ID(必填)"),
128
+ page: z.number().optional().describe("页码,从1开始,默认为1"),
129
+ limit: z.number().optional().describe("每页数量,默认20,最大100"),
130
+ status: z.enum([
131
+ 'active', // 激活状态
132
+ 'resolved', // 已解决
133
+ 'closed', // 已关闭
134
+ 'all', // 所有状态
135
+ 'assigntome', // 指派给我的
136
+ 'openedbyme', // 由我创建
137
+ 'resolvedbyme', // 由我解决
138
+ 'assignedbyme', // 由我指派
139
+ 'assigntonull', // 未指派
140
+ 'unconfirmed', // 未确认
141
+ 'unclosed', // 未关闭
142
+ 'unresolved', // 未解决(激活状态)
143
+ 'toclosed', // 待关闭(已解决)
144
+ 'postponedbugs', // 延期的Bug
145
+ 'longlifebugs', // 长期未处理的Bug
146
+ 'overduebugs', // 已过期的Bug
147
+ 'review', // 待我审核
148
+ 'feedback', // 用户反馈
149
+ 'needconfirm', // 需求变更需确认
150
+ 'bysearch' // 自定义搜索
151
+ ]).optional().describe("Bug状态筛选,默认返回所有状态"),
152
+ branch: z.string().optional().describe("分支筛选:'all'(所有分支,默认)、'0'(主干分支)、分支ID"),
153
+ order: z.string().optional().describe("排序方式,如 id_desc(ID降序), pri_desc(优先级降序), openedDate_desc(创建时间降序)等,默认id_desc"),
154
+ fields: z.enum(['basic', 'default', 'full']).optional().describe("返回字段级别:'basic'(基本字段)、'default'(默认字段,推荐)、'full'(完整字段),默认'default'")
155
+ }, async ({ productId, page, limit, status, branch, order, fields }) => {
156
+ if (!zentaoApi)
157
+ throw new Error("Please initialize Zentao API first");
158
+ const result = await zentaoApi.getProductBugs(productId, page, limit, status, branch, order, fields);
159
+ // 返回分页信息和数据
160
+ const summary = {
161
+ summary: `当前第 ${result.page} 页,共 ${result.total} 个Bug,本页显示 ${result.bugs.length} 个`,
162
+ pagination: {
163
+ page: result.page,
164
+ limit: result.limit,
165
+ total: result.total,
166
+ totalPages: Math.ceil(result.total / result.limit)
167
+ },
168
+ bugs: result.bugs
169
+ };
170
+ return {
171
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
172
+ };
173
+ });
174
+ server.tool("getBugDetail", {
175
+ bugId: z.number(),
176
+ fields: z.enum(['basic', 'detail', 'full']).optional().describe(`
177
+ 字段级别:
178
+ - basic: 基本信息(id, title, status, severity, pri等核心字段)
179
+ - detail: 详细信息(包含steps步骤,但不包含files/cases/linkBugs等关联数据)
180
+ - full: 完整信息(包含所有字段,默认值)
181
+ `.trim())
182
+ }, async ({ bugId, fields }) => {
183
+ if (!zentaoApi)
184
+ throw new Error("Please initialize Zentao API first");
185
+ const bug = await zentaoApi.getBugDetail(bugId, fields);
186
+ // 添加说明信息
187
+ const result = {
188
+ fieldLevel: fields || 'full',
189
+ bug: bug
190
+ };
191
+ // 根据字段级别添加提示
192
+ if (fields === 'basic') {
193
+ result.note = '仅显示基本信息,如需完整内容请使用 fields=detail 或 fields=full';
194
+ }
195
+ else if (fields === 'detail') {
196
+ result.note = '显示详细信息但不包含关联数据(files/cases/linkBugs),如需完整内容请使用 fields=full';
197
+ }
198
+ return {
199
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
200
+ };
201
+ });
202
+ server.tool("resolveBug", {
203
+ bugId: z.number(),
204
+ resolution: z.enum(['fixed', 'bydesign', 'duplicate', 'external', 'notrepro', 'postponed', 'willnotfix']),
205
+ resolvedBuild: z.string().optional(),
206
+ assignedTo: z.string().optional(),
207
+ comment: z.string().optional(),
208
+ duplicateBug: z.number().optional()
209
+ }, async ({ bugId, resolution, resolvedBuild, assignedTo, comment, duplicateBug }) => {
210
+ if (!zentaoApi)
211
+ throw new Error("Please initialize Zentao API first");
212
+ const resolveBugRequest = {
213
+ resolution,
214
+ resolvedBuild,
215
+ assignedTo,
216
+ comment,
217
+ duplicateBug
218
+ };
219
+ const bug = await zentaoApi.resolveBug(bugId, resolveBugRequest);
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify(bug, null, 2) }]
222
+ };
223
+ });
224
+ server.tool("getStoryDetail", "获取需求详情 - 支持软件需求(story)和用户需求(requirement),自动识别类型", {
225
+ storyId: z.number().describe("需求ID(可以是story或requirement类型)"),
226
+ fields: z.enum(['basic', 'detail', 'full']).optional().describe(`
227
+ 字段级别:
228
+ - basic: 基本信息(id, title, status, stage, pri等核心字段)
229
+ - detail: 详细信息(包含spec和verify,但不包含executions/tasks/stages/children等关联数据)
230
+ - full: 完整信息(包含所有字段,默认值)
231
+ `.trim())
232
+ }, async ({ storyId, fields }) => {
233
+ if (!zentaoApi)
234
+ throw new Error("Please initialize Zentao API first");
235
+ const story = await zentaoApi.getStoryDetail(storyId, fields);
236
+ // 添加说明信息
237
+ const result = {
238
+ fieldLevel: fields || 'full',
239
+ story: story
240
+ };
241
+ // 根据字段级别添加提示
242
+ if (fields === 'basic') {
243
+ result.note = '仅显示基本信息,如需完整内容请使用 fields=detail 或 fields=full';
244
+ }
245
+ else if (fields === 'detail') {
246
+ result.note = '显示详细信息但不包含关联数据(executions/tasks/stages/children),如需完整内容请使用 fields=full';
247
+ }
248
+ return {
249
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
250
+ };
251
+ });
252
+ server.tool("createBug", {
253
+ productId: z.number(),
254
+ title: z.string(),
255
+ severity: z.number(),
256
+ pri: z.number(),
257
+ type: z.string(),
258
+ branch: z.number().optional(),
259
+ module: z.number().optional(),
260
+ execution: z.number().optional(),
261
+ keywords: z.string().optional(),
262
+ os: z.string().optional(),
263
+ browser: z.string().optional(),
264
+ steps: z.string().optional(),
265
+ task: z.number().optional(),
266
+ story: z.number().optional(),
267
+ deadline: z.string().optional(),
268
+ openedBuild: z.array(z.string()).optional().default(["trunk"])
269
+ }, async ({ productId, title, severity, pri, type, branch, module, execution, keywords, os, browser, steps, task, story, deadline, openedBuild }) => {
270
+ if (!zentaoApi)
271
+ throw new Error("Please initialize Zentao API first");
272
+ // 如果未提供 openedBuild,使用默认值 ["trunk"] 表示主干版本
273
+ const bugData = {
274
+ title,
275
+ severity,
276
+ pri,
277
+ type,
278
+ branch,
279
+ module,
280
+ execution,
281
+ keywords,
282
+ os,
283
+ browser,
284
+ steps,
285
+ task,
286
+ story,
287
+ deadline,
288
+ openedBuild: openedBuild || ["trunk"]
289
+ };
290
+ const bug = await zentaoApi.createBug(productId, bugData);
291
+ return {
292
+ content: [{ type: "text", text: JSON.stringify(bug, null, 2) }]
293
+ };
294
+ });
295
+ server.tool("getProductStories", "获取产品需求列表 - 包含软件需求(story)和用户需求(requirement),默认只返回核心字段(id, 标题, 指派人, 创建人)", {
296
+ productId: z.number(),
297
+ page: z.number().optional().describe("页码,从1开始,默认为1"),
298
+ limit: z.number().optional().describe("每页数量,默认20,最大100"),
299
+ fields: z.enum(['basic', 'default', 'full']).optional().describe("字段级别:\n- basic: 基本信息(id, title, assignedTo, openedBy)\n- default: 常用信息(包含basic + status, stage, pri等)\n- full: 完整信息(所有字段)")
300
+ }, async ({ productId, page, limit, fields }) => {
301
+ if (!zentaoApi)
302
+ throw new Error("Please initialize Zentao API first");
303
+ const result = await zentaoApi.getProductStories(productId, page, limit, fields);
304
+ // 返回分页信息和数据
305
+ const summary = {
306
+ summary: `当前第 ${result.page} 页,共 ${result.total} 条需求,本页显示 ${result.stories.length} 条`,
307
+ pagination: {
308
+ page: result.page,
309
+ limit: result.limit,
310
+ total: result.total,
311
+ totalPages: Math.ceil(result.total / result.limit)
312
+ },
313
+ stories: result.stories
314
+ };
315
+ return {
316
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
317
+ };
318
+ });
319
+ server.tool("updateBug", {
320
+ bugId: z.number(),
321
+ update: z.object({
322
+ branch: z.number().optional(), module: z.number().optional(), execution: z.number().optional(),
323
+ title: z.string().optional(), keywords: z.string().optional(), severity: z.number().optional(),
324
+ pri: z.number().optional(), type: z.string().optional(), os: z.string().optional(),
325
+ browser: z.string().optional(), steps: z.string().optional(), assignedTo: z.string().optional(),
326
+ deadline: z.string().optional()
327
+ })
328
+ }, async ({ bugId, update }) => {
329
+ if (!zentaoApi)
330
+ throw new Error("Please initialize Zentao API first");
331
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateBug(bugId, update), null, 2) }] };
332
+ });
333
+ server.tool("deleteBug", { bugId: z.number() }, async ({ bugId }) => {
334
+ if (!zentaoApi)
335
+ throw new Error("Please initialize Zentao API first");
336
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteBug(bugId), null, 2) }] };
337
+ });
338
+ server.tool("getProductTestCases", { productId: z.number() }, async ({ productId }) => {
339
+ if (!zentaoApi)
340
+ throw new Error("Please initialize Zentao API first");
341
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getProductTestCases(productId), null, 2) }] };
342
+ });
343
+ server.tool("createTestCase", {
344
+ productId: z.number(),
345
+ branch: z.number().optional(),
346
+ module: z.number().optional(),
347
+ story: z.number().optional(),
348
+ title: z.string(),
349
+ type: z.string(),
350
+ stage: z.string().optional(),
351
+ precondition: z.string().optional(),
352
+ pri: z.number().optional(),
353
+ steps: z.array(z.object({ desc: z.string(), expect: z.string() })),
354
+ keywords: z.string().optional()
355
+ }, async ({ productId, ...params }) => {
356
+ if (!zentaoApi)
357
+ throw new Error("Please initialize Zentao API first");
358
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createTestCase(productId, params), null, 2) }] };
359
+ });
360
+ server.tool("getTestCaseDetail", { testCaseId: z.number() }, async ({ testCaseId }) => {
361
+ if (!zentaoApi)
362
+ throw new Error("Please initialize Zentao API first");
363
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getTestCaseDetail(testCaseId), null, 2) }] };
364
+ });
365
+ server.tool("updateTestCase", {
366
+ testCaseId: z.number(),
367
+ branch: z.number().optional(),
368
+ module: z.number().optional(),
369
+ story: z.number().optional(),
370
+ title: z.string().optional(),
371
+ type: z.string().optional(),
372
+ stage: z.string().optional(),
373
+ precondition: z.string().optional(),
374
+ pri: z.number().optional(),
375
+ steps: z.array(z.object({ desc: z.string(), expect: z.string() })).optional(),
376
+ keywords: z.string().optional()
377
+ }, async ({ testCaseId, ...params }) => {
378
+ if (!zentaoApi)
379
+ throw new Error("Please initialize Zentao API first");
380
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateTestCase(testCaseId, params), null, 2) }] };
381
+ });
382
+ server.tool("deleteTestCase", { testCaseId: z.number() }, async ({ testCaseId }) => {
383
+ if (!zentaoApi)
384
+ throw new Error("Please initialize Zentao API first");
385
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteTestCase(testCaseId), null, 2) }] };
386
+ });
387
+ server.tool("createBuild", {
388
+ projectId: z.number(),
389
+ execution: z.number(),
390
+ product: z.number(),
391
+ branch: z.number().optional(),
392
+ name: z.string(),
393
+ builder: z.string(),
394
+ date: z.string().optional(),
395
+ scmPath: z.string().optional(),
396
+ filePath: z.string().optional(),
397
+ desc: z.string().optional()
398
+ }, async ({ projectId, ...params }) => {
399
+ if (!zentaoApi)
400
+ throw new Error("Please initialize Zentao API first");
401
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createBuild(projectId, params), null, 2) }] };
402
+ });
403
+ server.tool("getBuildDetail", { buildId: z.number() }, async ({ buildId }) => {
404
+ if (!zentaoApi)
405
+ throw new Error("Please initialize Zentao API first");
406
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getBuildDetail(buildId), null, 2) }] };
407
+ });
408
+ server.tool("updateBuild", {
409
+ buildId: z.number(),
410
+ name: z.string().optional(),
411
+ builder: z.string().optional(),
412
+ date: z.string().optional(),
413
+ scmPath: z.string().optional(),
414
+ filePath: z.string().optional(),
415
+ desc: z.string().optional()
416
+ }, async ({ buildId, ...params }) => {
417
+ if (!zentaoApi)
418
+ throw new Error("Please initialize Zentao API first");
419
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateBuild(buildId, params), null, 2) }] };
420
+ });
421
+ server.tool("deleteBuild", { buildId: z.number() }, async ({ buildId }) => {
422
+ if (!zentaoApi)
423
+ throw new Error("Please initialize Zentao API first");
424
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteBuild(buildId), null, 2) }] };
425
+ });
426
+ server.tool("getTickets", {
427
+ browseType: z.string().optional(),
428
+ param: z.string().optional(),
429
+ orderBy: z.string().optional(),
430
+ page: z.number().optional(),
431
+ limit: z.number().optional()
432
+ }, async (params) => {
433
+ if (!zentaoApi)
434
+ throw new Error("Please initialize Zentao API first");
435
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getTickets(params), null, 2) }] };
436
+ });
437
+ server.tool("getTicketDetail", { ticketId: z.number() }, async ({ ticketId }) => {
438
+ if (!zentaoApi)
439
+ throw new Error("Please initialize Zentao API first");
440
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.getTicketDetail(ticketId), null, 2) }] };
441
+ });
442
+ server.tool("createTicket", {
443
+ product: z.number(),
444
+ module: z.number(),
445
+ title: z.string(),
446
+ type: z.string().optional(),
447
+ desc: z.string().optional()
448
+ }, async (params) => {
449
+ if (!zentaoApi)
450
+ throw new Error("Please initialize Zentao API first");
451
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.createTicket(params), null, 2) }] };
452
+ });
453
+ server.tool("updateTicket", {
454
+ ticketId: z.number(),
455
+ product: z.number().optional(),
456
+ module: z.number().optional(),
457
+ title: z.string().optional(),
458
+ type: z.string().optional(),
459
+ desc: z.string().optional()
460
+ }, async ({ ticketId, ...params }) => {
461
+ if (!zentaoApi)
462
+ throw new Error("Please initialize Zentao API first");
463
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.updateTicket(ticketId, params), null, 2) }] };
464
+ });
465
+ server.tool("deleteTicket", { ticketId: z.number() }, async ({ ticketId }) => {
466
+ if (!zentaoApi)
467
+ throw new Error("Please initialize Zentao API first");
468
+ return { content: [{ type: "text", text: JSON.stringify(await zentaoApi.deleteTicket(ticketId), null, 2) }] };
469
+ });
470
+ server.tool("getModules", {
471
+ type: z.enum(['story', 'task', 'bug', 'case', 'feedback', 'product']),
472
+ id: z.number(),
473
+ fields: z.string().optional()
474
+ }, async ({ type, id, fields }) => {
475
+ if (!zentaoApi)
476
+ throw new Error("Please initialize Zentao API first");
477
+ const response = await zentaoApi.getModules(type, id, fields);
478
+ return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
479
+ });
480
+ server.tool("uploadFile", {
481
+ filePath: z.string().optional(),
482
+ base64Data: z.string().optional(),
483
+ filename: z.string().optional(),
484
+ uid: z.string().optional()
485
+ }, async ({ filePath, base64Data, filename, uid }) => {
486
+ if (!zentaoApi)
487
+ throw new Error("Please initialize Zentao API first");
488
+ const fs = await import('fs');
489
+ const path = await import('path');
490
+ let fileBuffer;
491
+ let finalFilename;
492
+ let savedPath;
493
+ // 如果提供了 base64 数据(复制的图片)
494
+ if (base64Data) {
495
+ // 创建 img 文件夹(如果不存在)
496
+ const imgDir = path.join(process.cwd(), 'img');
497
+ if (!fs.existsSync(imgDir)) {
498
+ fs.mkdirSync(imgDir, { recursive: true });
499
+ }
500
+ // 处理 base64 数据
501
+ const matches = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);
502
+ if (matches) {
503
+ const ext = matches[1];
504
+ const data = matches[2];
505
+ fileBuffer = Buffer.from(data, 'base64');
506
+ finalFilename = filename || `image_${Date.now()}.${ext}`;
507
+ }
508
+ else {
509
+ // 直接的 base64 数据,没有 data URL 前缀
510
+ fileBuffer = Buffer.from(base64Data, 'base64');
511
+ finalFilename = filename || `image_${Date.now()}.png`;
512
+ }
513
+ // 保存到 img 文件夹
514
+ savedPath = path.join(imgDir, finalFilename);
515
+ fs.writeFileSync(savedPath, fileBuffer);
516
+ }
517
+ // 如果提供了文件路径
518
+ else if (filePath) {
519
+ fileBuffer = fs.readFileSync(filePath);
520
+ finalFilename = path.basename(filePath);
521
+ }
522
+ else {
523
+ throw new Error("必须提供 filePath 或 base64Data 参数");
524
+ }
525
+ const result = await zentaoApi.uploadFile({
526
+ file: fileBuffer,
527
+ filename: finalFilename,
528
+ uid
529
+ });
530
+ // 生成禅道需要的 HTML 格式
531
+ const fileId = result.id;
532
+ const baseUrl = zentaoApi.getConfig().url;
533
+ const imageUrl = `${baseUrl}/zentao/entao/api.php?m=file&f=read&t=png&fileID=${fileId}`;
534
+ const imageHtml = `<p><img onload="setImageSize(this,0)" src="${imageUrl}" alt="${finalFilename}" /></p>`;
535
+ const response = {
536
+ upload: result,
537
+ fileId: fileId,
538
+ imageUrl: imageUrl,
539
+ imageHtml: imageHtml,
540
+ tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
541
+ };
542
+ if (savedPath) {
543
+ response.savedPath = savedPath;
544
+ }
545
+ return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
546
+ });
547
+ server.tool("uploadImageFromClipboard", "上传剪贴板图片到禅道 - 自动获取剪贴板中的图片并上传", {
548
+ filename: z.string().optional().describe("自定义文件名,默认为 clipboard_时间戳.png"),
549
+ uid: z.string().optional().describe("禅道文件UID,用于关联需求或Bug")
550
+ }, async ({ filename, uid }) => {
551
+ if (!zentaoApi) {
552
+ return {
553
+ content: [{
554
+ type: "text",
555
+ text: JSON.stringify({ error: "请先初始化禅道API配置" }, null, 2)
556
+ }],
557
+ isError: true
558
+ };
559
+ }
560
+ const fs = await import('fs');
561
+ const path = await import('path');
562
+ const { execSync } = await import('child_process');
563
+ try {
564
+ console.log('[uploadImageFromClipboard] 开始执行...');
565
+ // 读取系统剪贴板图片
566
+ let fileBuffer;
567
+ let finalFilename;
568
+ // Windows: 使用 PowerShell 脚本读取剪贴板
569
+ if (process.platform === 'win32') {
570
+ console.log('[uploadImageFromClipboard] Windows平台,使用PowerShell脚本读取剪贴板...');
571
+ // 内嵌 PowerShell 脚本,避免依赖外部文件
572
+ const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); $base64 = [Convert]::ToBase64String($bytes); Write-Output $base64 } else { Write-Output 'NoImage' }`;
573
+ try {
574
+ // 使用 Base64 编码 PowerShell 脚本以避免转义问题
575
+ const psScriptBase64 = Buffer.from(psScript, 'utf16le').toString('base64');
576
+ const result = execSync(`powershell -ExecutionPolicy Bypass -EncodedCommand ${psScriptBase64}`, {
577
+ encoding: 'utf8',
578
+ maxBuffer: 10 * 1024 * 1024,
579
+ stdio: ['pipe', 'pipe', 'pipe']
580
+ }).trim();
581
+ if (!result || result.includes('NoImage') || result.includes('Error')) {
582
+ console.error('[uploadImageFromClipboard] 剪贴板中没有图片');
583
+ return {
584
+ content: [{
585
+ type: "text",
586
+ text: JSON.stringify({
587
+ error: "剪贴板中没有图片",
588
+ tip: "请先复制图片,不要粘贴到输入框,直接告诉我'上传'"
589
+ }, null, 2)
590
+ }],
591
+ isError: true
592
+ };
593
+ }
594
+ fileBuffer = Buffer.from(result, 'base64');
595
+ finalFilename = filename || `clipboard_${Date.now()}.png`;
596
+ console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
597
+ }
598
+ catch (err) {
599
+ console.error('[uploadImageFromClipboard] Windows读取剪贴板失败:', err);
600
+ return {
601
+ content: [{
602
+ type: "text",
603
+ text: JSON.stringify({
604
+ error: "读取剪贴板失败",
605
+ details: err.message,
606
+ tip: "请确保已复制图片到剪贴板"
607
+ }, null, 2)
608
+ }],
609
+ isError: true
610
+ };
611
+ }
612
+ }
613
+ // macOS: 使用 pngpaste
614
+ else if (process.platform === 'darwin') {
615
+ console.log('[uploadImageFromClipboard] macOS平台,使用pngpaste读取剪贴板...');
616
+ const tempFile = path.join(process.cwd(), '.temp_clipboard.png');
617
+ try {
618
+ execSync(`pngpaste "${tempFile}"`);
619
+ fileBuffer = fs.readFileSync(tempFile);
620
+ fs.unlinkSync(tempFile);
621
+ finalFilename = filename || `clipboard_${Date.now()}.png`;
622
+ console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
623
+ }
624
+ catch (err) {
625
+ console.error('[uploadImageFromClipboard] macOS读取剪贴板失败:', err);
626
+ return {
627
+ content: [{
628
+ type: "text",
629
+ text: JSON.stringify({
630
+ error: "剪贴板中没有图片或pngpaste未安装",
631
+ tip: "请先安装 pngpaste: brew install pngpaste"
632
+ }, null, 2)
633
+ }],
634
+ isError: true
635
+ };
636
+ }
637
+ }
638
+ // Linux: 使用 xclip
639
+ else {
640
+ console.log('[uploadImageFromClipboard] Linux平台,使用xclip读取剪贴板...');
641
+ try {
642
+ const buffer = execSync('xclip -selection clipboard -t image/png -o', {
643
+ maxBuffer: 10 * 1024 * 1024
644
+ });
645
+ fileBuffer = buffer;
646
+ finalFilename = filename || `clipboard_${Date.now()}.png`;
647
+ console.log(`[uploadImageFromClipboard] 已读取剪贴板图片,大小: ${fileBuffer.length} bytes`);
648
+ }
649
+ catch (err) {
650
+ console.error('[uploadImageFromClipboard] Linux读取剪贴板失败:', err);
651
+ return {
652
+ content: [{
653
+ type: "text",
654
+ text: JSON.stringify({
655
+ error: "剪贴板中没有图片或xclip未安装",
656
+ tip: "请先安装 xclip: sudo apt-get install xclip"
657
+ }, null, 2)
658
+ }],
659
+ isError: true
660
+ };
661
+ }
662
+ }
663
+ // 创建 img 文件夹
664
+ const imgDir = path.join(process.cwd(), 'img');
665
+ if (!fs.existsSync(imgDir)) {
666
+ console.log(`[uploadImageFromClipboard] 创建目录: ${imgDir}`);
667
+ fs.mkdirSync(imgDir, { recursive: true });
668
+ }
669
+ // 保存到 img 文件夹
670
+ const savedPath = path.join(imgDir, finalFilename);
671
+ fs.writeFileSync(savedPath, fileBuffer);
672
+ console.log(`[uploadImageFromClipboard] 图片已保存到: ${savedPath}`);
673
+ // 上传到禅道
674
+ console.log('[uploadImageFromClipboard] 开始上传到禅道...');
675
+ const uploadResult = await zentaoApi.uploadFile({
676
+ file: fileBuffer,
677
+ filename: finalFilename,
678
+ uid
679
+ });
680
+ console.log('[uploadImageFromClipboard] 上传成功,结果:', uploadResult);
681
+ // 生成禅道需要的 HTML 格式
682
+ const fileId = uploadResult.id;
683
+ const baseUrl = zentaoApi.getConfig().url;
684
+ const imageUrl = `${baseUrl}/zentao/file-read-${fileId}.png`;
685
+ const imageHtml = `<p><img onload="setImageSize(this,0)" src="${imageUrl}" alt="${finalFilename}" /></p>`;
686
+ const response = {
687
+ success: true,
688
+ upload: uploadResult,
689
+ savedPath: savedPath,
690
+ filename: finalFilename,
691
+ fileSize: fileBuffer.length,
692
+ fileId: fileId,
693
+ imageUrl: imageUrl,
694
+ imageHtml: imageHtml,
695
+ message: `图片已保存到本地并上传到禅道`,
696
+ tip: `更新 Bug 描述时,请使用 imageHtml 字段中的 HTML 代码。图片会被包裹在 <p> 标签中以确保正确显示。`
697
+ };
698
+ console.log('[uploadImageFromClipboard] 返回结果:', response);
699
+ return {
700
+ content: [{
701
+ type: "text",
702
+ text: JSON.stringify(response, null, 2)
703
+ }]
704
+ };
705
+ }
706
+ catch (error) {
707
+ console.error('[uploadImageFromClipboard] 发生错误:', error);
708
+ const errorResponse = {
709
+ success: false,
710
+ error: error.message || String(error),
711
+ stack: error.stack,
712
+ tip: "请检查:\n1. 确保已复制图片到剪贴板\n2. 不要粘贴到输入框,直接说'上传剪贴板图片'\n3. 或使用命令行: upload.bat"
713
+ };
714
+ return {
715
+ content: [{
716
+ type: "text",
717
+ text: JSON.stringify(errorResponse, null, 2)
718
+ }],
719
+ isError: true
720
+ };
721
+ }
722
+ });
723
+ server.tool("downloadFile", {
724
+ fileId: z.number(),
725
+ savePath: z.string()
726
+ }, async ({ fileId, savePath }) => {
727
+ if (!zentaoApi)
728
+ throw new Error("Please initialize Zentao API first");
729
+ const fs = await import('fs');
730
+ const fileBuffer = await zentaoApi.downloadFile(fileId);
731
+ fs.writeFileSync(savePath, fileBuffer);
732
+ return { content: [{ type: "text", text: `文件已下载到: ${savePath}` }] };
733
+ });
734
+ server.tool("getComments", "获取评论列表 - 获取指定对象的所有评论", {
735
+ objectType: z.enum([
736
+ 'story', // 软件需求
737
+ 'requirement', // 用户需求
738
+ 'task', // 任务
739
+ 'bug', // Bug
740
+ 'testcase', // 测试用例
741
+ 'testtask', // 测试单
742
+ 'todo', // 待办
743
+ 'doc', // 文档
744
+ 'execution', // 执行/迭代
745
+ 'project', // 项目
746
+ 'doctemplate' // 文档模板
747
+ ]).describe("对象类型"),
748
+ objectID: z.number().describe("对象ID")
749
+ }, async ({ objectType, objectID }) => {
750
+ if (!zentaoApi)
751
+ throw new Error("Please initialize Zentao API first");
752
+ const comments = await zentaoApi.getComments(objectType, objectID);
753
+ return {
754
+ content: [{
755
+ type: "text",
756
+ text: JSON.stringify({
757
+ total: comments.length,
758
+ objectType,
759
+ objectID,
760
+ comments
761
+ }, null, 2)
762
+ }]
763
+ };
764
+ });
765
+ server.tool("getCommentDetail", "获取单条评论详情", {
766
+ commentId: z.number().describe("评论ID")
767
+ }, async ({ commentId }) => {
768
+ if (!zentaoApi)
769
+ throw new Error("Please initialize Zentao API first");
770
+ const comment = await zentaoApi.getCommentDetail(commentId);
771
+ return {
772
+ content: [{
773
+ type: "text",
774
+ text: JSON.stringify(comment, null, 2)
775
+ }]
776
+ };
777
+ });
778
+ server.tool("addComment", "添加评论 - 为需求、任务、Bug等对象添加评论", {
779
+ objectType: z.enum([
780
+ 'story', // 软件需求
781
+ 'requirement', // 用户需求
782
+ 'task', // 任务
783
+ 'bug', // Bug
784
+ 'testcase', // 测试用例
785
+ 'testtask', // 测试单
786
+ 'todo', // 待办
787
+ 'doc', // 文档
788
+ 'execution', // 执行/迭代
789
+ 'project', // 项目
790
+ 'doctemplate' // 文档模板
791
+ ]).describe("对象类型"),
792
+ objectID: z.number().describe("对象ID"),
793
+ comment: z.string().describe("评论内容,支持HTML格式"),
794
+ uid: z.string().optional().describe("唯一标识符(可选)")
795
+ }, async ({ objectType, objectID, comment, uid }) => {
796
+ if (!zentaoApi)
797
+ throw new Error("Please initialize Zentao API first");
798
+ const result = await zentaoApi.addComment({
799
+ objectType,
800
+ objectID,
801
+ comment,
802
+ uid
803
+ });
804
+ return {
805
+ content: [{
806
+ type: "text",
807
+ text: JSON.stringify({
808
+ message: '评论添加成功',
809
+ comment: result
810
+ }, null, 2)
811
+ }]
812
+ };
813
+ });
814
+ server.tool("updateComment", "更新评论 - 只能更新自己的评论", {
815
+ commentId: z.number().describe("评论ID"),
816
+ comment: z.string().describe("更新后的评论内容,支持HTML格式"),
817
+ uid: z.string().optional().describe("唯一标识符(可选)")
818
+ }, async ({ commentId, comment, uid }) => {
819
+ if (!zentaoApi)
820
+ throw new Error("Please initialize Zentao API first");
821
+ const result = await zentaoApi.updateComment(commentId, {
822
+ comment,
823
+ uid
824
+ });
825
+ return {
826
+ content: [{
827
+ type: "text",
828
+ text: JSON.stringify({
829
+ message: '评论更新成功',
830
+ comment: result
831
+ }, null, 2)
832
+ }]
833
+ };
834
+ });
835
+ server.tool("deleteComment", "删除评论 - 只能删除自己的评论,管理员可以删除所有评论", {
836
+ commentId: z.number().describe("评论ID")
837
+ }, async ({ commentId }) => {
838
+ if (!zentaoApi)
839
+ throw new Error("Please initialize Zentao API first");
840
+ const result = await zentaoApi.deleteComment(commentId);
841
+ return {
842
+ content: [{
843
+ type: "text",
844
+ text: JSON.stringify({
845
+ status: 'success',
846
+ message: result.message || '评论删除成功'
847
+ }, null, 2)
848
+ }]
849
+ };
850
+ });
851
+ server.tool("addStoryComment", "为需求添加评论 - 专用工具,支持软件需求(story)和用户需求(requirement)", {
852
+ storyId: z.number().describe("需求ID"),
853
+ comment: z.string().describe("评论内容,支持HTML格式")
854
+ }, async ({ storyId, comment }) => {
855
+ if (!zentaoApi)
856
+ throw new Error("Please initialize Zentao API first");
857
+ const result = await zentaoApi.addComment({
858
+ objectType: 'story',
859
+ objectID: storyId,
860
+ comment
861
+ });
862
+ return {
863
+ content: [{
864
+ type: "text",
865
+ text: JSON.stringify({
866
+ message: `已为需求 #${storyId} 添加评论`,
867
+ comment: result
868
+ }, null, 2)
869
+ }]
870
+ };
871
+ });
872
+ server.tool("addTaskComment", "为任务添加评论", {
873
+ taskId: z.number().describe("任务ID"),
874
+ comment: z.string().describe("评论内容,支持HTML格式")
875
+ }, async ({ taskId, comment }) => {
876
+ if (!zentaoApi)
877
+ throw new Error("Please initialize Zentao API first");
878
+ const result = await zentaoApi.addComment({
879
+ objectType: 'task',
880
+ objectID: taskId,
881
+ comment
882
+ });
883
+ return {
884
+ content: [{
885
+ type: "text",
886
+ text: JSON.stringify({
887
+ message: `已为任务 #${taskId} 添加评论`,
888
+ comment: result
889
+ }, null, 2)
890
+ }]
891
+ };
892
+ });
893
+ server.tool("addBugComment", "为Bug添加评论", {
894
+ bugId: z.number().describe("Bug ID"),
895
+ comment: z.string().describe("评论内容,支持HTML格式")
896
+ }, async ({ bugId, comment }) => {
897
+ if (!zentaoApi)
898
+ throw new Error("Please initialize Zentao API first");
899
+ const result = await zentaoApi.addComment({
900
+ objectType: 'bug',
901
+ objectID: bugId,
902
+ comment
903
+ });
904
+ return {
905
+ content: [{
906
+ type: "text",
907
+ text: JSON.stringify({
908
+ message: `已为Bug #${bugId} 添加评论`,
909
+ comment: result
910
+ }, null, 2)
911
+ }]
912
+ };
913
+ });
914
+ // Start receiving messages on stdin and sending messages on stdout
915
+ const transport = new StdioServerTransport();
916
+ await server.connect(transport).catch(console.error);