@maxenlin/mcp-zentao-11-3 1.0.1 → 1.0.3

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 CHANGED
@@ -15,27 +15,7 @@
15
15
 
16
16
  ## 📦 安装
17
17
 
18
- ### 方法 1:使用 npx(推荐)
19
-
20
- 在支持 MCP 的 IDE/工具配置文件中添加(以 Cursor IDE 为例):
21
-
22
- ```json
23
- {
24
- "mcpServers": {
25
- "zentao-11-3": {
26
- "command": "npx",
27
- "args": ["-y", "@maxenlin/mcp-zentao-11-3"],
28
- "env": {
29
- "ZENTAO_URL": "http://your-zentao-url/zentao",
30
- "ZENTAO_USERNAME": "your-username",
31
- "ZENTAO_PASSWORD": "your-password"
32
- }
33
- }
34
- }
35
- }
36
- ```
37
-
38
- ### 方法 2:本地安装
18
+ ### 方法 1:本地安装
39
19
 
40
20
  ```bash
41
21
  npm install -g @maxenlin/mcp-zentao-11-3
@@ -59,18 +39,21 @@ npm install -g @maxenlin/mcp-zentao-11-3
59
39
  }
60
40
  ```
61
41
 
62
- ## ⚙️ 配置
42
+ **配置说明:**
43
+ - `ZENTAO_URL`: 禅道服务器地址(必须包含 `/zentao` 路径)
44
+ - `ZENTAO_USERNAME`: 禅道用户名
45
+ - `ZENTAO_PASSWORD`: 禅道密码
63
46
 
64
- ### 方法 1:在 Cursor 配置中直接设置(推荐)
47
+ ### 方法 2:使用 npx
65
48
 
66
- 在支持 MCP 的 IDE/工具的配置文件中,直接在 `env` 字段中填入配置(以 Cursor IDE 为例):
49
+ 在支持 MCP 的 IDE/工具配置文件中添加(以 Cursor IDE 为例):
67
50
 
68
51
  ```json
69
52
  {
70
53
  "mcpServers": {
71
54
  "zentao-11-3": {
72
- "command": "mcp-zentao-11-3",
73
- "args": [],
55
+ "command": "npx",
56
+ "args": ["-y", "@maxenlin/mcp-zentao-11-3"],
74
57
  "env": {
75
58
  "ZENTAO_URL": "http://your-zentao-url/zentao",
76
59
  "ZENTAO_USERNAME": "your-username",
@@ -86,33 +69,6 @@ npm install -g @maxenlin/mcp-zentao-11-3
86
69
  - `ZENTAO_USERNAME`: 禅道用户名
87
70
  - `ZENTAO_PASSWORD`: 禅道密码
88
71
 
89
- ### 方法 2:使用配置文件(备选)
90
-
91
- 如果不想在 Cursor 配置中直接填写密码,可以创建配置文件:
92
-
93
- **Windows:**
94
- ```
95
- C:\Users\你的用户名\.zentao\config.json
96
- ```
97
-
98
- **macOS/Linux:**
99
- ```
100
- ~/.zentao/config.json
101
- ```
102
-
103
- 配置文件内容:
104
-
105
- ```json
106
- {
107
- "url": "http://your-zentao-url/zentao",
108
- "username": "your-username",
109
- "password": "your-password",
110
- "apiVersion": "legacy"
111
- }
112
- ```
113
-
114
- **注意:** 环境变量配置优先于配置文件。如果同时设置了环境变量和配置文件,将使用环境变量中的配置。
115
-
116
72
  ## 🚀 使用
117
73
 
118
74
  配置完成后,重启您的 IDE/工具即可使用。
@@ -176,70 +132,84 @@ C:\Users\你的用户名\.zentao\config.json
176
132
  ## 📋 可用工具
177
133
 
178
134
  ### 配置管理
135
+
179
136
  - `initZentao` - 初始化禅道连接
180
137
  - `getConfig` - 查看配置信息
181
138
 
182
139
  ### 任务管理
140
+
183
141
  - `getMyTasks` - 获取我的任务列表
184
142
  - `getTaskDetail` - 获取任务详情
185
143
  - `updateTask` - 更新任务
186
144
  - `finishTask` - 完成任务
187
145
 
188
146
  ### Bug 管理
147
+
189
148
  - `getMyBugs` - 获取我的Bug列表
190
149
  - `getBugDetail` - 获取Bug详情
191
150
  - `resolveBug` - 解决Bug
192
151
 
193
152
  ### 产品管理
153
+
194
154
  - `getProducts` - 获取产品列表
195
155
 
196
156
  ### 需求管理
157
+
197
158
  - `getProductStories` - 获取产品的需求列表
198
159
  - `getStoryDetail` - 获取需求详情
199
160
  - `searchStories` - 搜索需求
200
161
  - `searchStoriesByProductName` - 按产品名称搜索需求
201
162
 
202
163
  ### 测试用例管理
164
+
203
165
  - `getProductTestCases` - 获取产品的测试用例
204
166
  - `getTestCaseDetail` - 获取测试用例详情
205
167
  - `createTestCase` - 创建测试用例
206
168
  - `getStoryTestCases` - 获取需求的测试用例
207
169
 
208
170
  ### 测试单管理
171
+
209
172
  - `getTestTasks` - 获取测试单列表
210
173
  - `getTestTaskDetail` - 获取测试单详情
211
174
  - `getTestTaskResults` - 获取测试单的测试结果
212
175
  - `runTestCase` - 执行测试用例
213
176
 
214
177
  ### 关联关系查询
178
+
215
179
  - `getStoryRelatedBugs` - 获取需求关联的 Bug 列表
216
180
  - `getBugRelatedStory` - 获取 Bug 关联的需求
217
181
 
218
182
  ### 批量操作
183
+
219
184
  - `batchUpdateTasks` - 批量更新任务
220
185
  - `batchResolveBugs` - 批量解决 Bug
221
186
 
222
187
  ### 数据统计
188
+
223
189
  - `getMyTaskStatistics` - 获取我的任务统计信息
224
190
  - `getMyBugStatistics` - 获取我的 Bug 统计信息
225
191
 
226
192
  ### AI 编程辅助功能
193
+
227
194
  - `getDevelopmentContext` - 获取需求/Bug 的完整开发上下文(包含关联信息)
228
195
  - `generateStorySummary` - 生成需求摘要(支持 JSON/Markdown/文本格式)
229
196
  - `generateBugSummary` - 生成 Bug 摘要(支持 JSON/Markdown/文本格式)
230
197
  - `formatTaskAsMarkdown` - 将任务格式化为 Markdown
231
198
 
232
199
  ### 智能分析功能
200
+
233
201
  - `analyzeStoryComplexity` - 分析需求复杂度(评分、工时估算、优先级建议)
234
202
  - `analyzeBugPriority` - 分析 Bug 优先级(评分、优先级建议)
235
203
  - `analyzeTaskWorkload` - 分析任务工作量(工时估算、难度评估)
236
204
 
237
205
  ### 代码生成提示
206
+
238
207
  - `generateCodePromptFromStory` - 根据需求生成代码框架提示
239
208
  - `generateTestPromptFromBug` - 根据 Bug 生成测试用例提示
240
209
  - `generateCodeReviewChecklist` - 生成代码审查检查清单
241
210
 
242
211
  ### 根据需求/Bug创建任务
212
+
243
213
  - `createTaskFromStory` - 根据需求创建任务(提供手动操作指南)
244
214
  - `createTaskFromBug` - 根据Bug创建修复任务(提供手动操作指南)
245
215
 
@@ -251,4 +221,3 @@ MIT
251
221
 
252
222
  - [禅道开源版 GitHub](https://github.com/easysoft/zentaopms) - 禅道官方 GitHub 仓库
253
223
  - [禅道官网](https://www.zentao.net/)
254
-
package/dist/index.js CHANGED
@@ -9,6 +9,59 @@ import { ZentaoError, ErrorCode, createError } from './errors.js';
9
9
  import { formatStoryAsMarkdown, formatBugAsMarkdown, formatTaskAsMarkdown, generateStorySummary, generateBugSummary } from './utils/formatter.js';
10
10
  import { analyzeStoryComplexity, analyzeBugPriority, analyzeTaskWorkload } from './utils/analyzer.js';
11
11
  import { suggestNextActionsForStory, suggestNextActionsForBug, suggestNextActionsForTask, formatSuggestionsAsMarkdown } from './utils/suggestions.js';
12
+ /**
13
+ * 解析模块链接,提取产品ID和模块ID
14
+ * 支持的链接格式:
15
+ * - product-browse-{productId}--byModule-{moduleId}.html - 产品模块(需求)
16
+ * - testtask-cases-{taskId}-byModule-{moduleId}.html - 测试任务用例模块
17
+ * - testcase-browse-{productId}-byModule-{moduleId}.html - 产品用例模块
18
+ * - bug-browse-{productId}--byModule-{moduleId}.html - Bug模块
19
+ *
20
+ * @param url 链接URL
21
+ * @returns 解析结果 { type: 'story' | 'testcase' | 'bug', productId: number, moduleId: number, taskId?: number }
22
+ */
23
+ function parseModuleUrl(url) {
24
+ // 移除协议和域名,只保留路径部分
25
+ const path = url.replace(/^https?:\/\/[^\/]+/, '');
26
+ // 匹配 product-browse-{productId}--byModule-{moduleId}
27
+ const productModuleMatch = path.match(/product-browse-(\d+)--byModule-(\d+)/);
28
+ if (productModuleMatch) {
29
+ return {
30
+ type: 'story',
31
+ productId: parseInt(productModuleMatch[1]),
32
+ moduleId: parseInt(productModuleMatch[2])
33
+ };
34
+ }
35
+ // 匹配 testtask-cases-{taskId}-byModule-{moduleId}
36
+ const testtaskModuleMatch = path.match(/testtask-cases-(\d+)-byModule-(\d+)/);
37
+ if (testtaskModuleMatch) {
38
+ return {
39
+ type: 'testcase',
40
+ productId: 0, // 测试任务用例需要从任务详情获取产品ID
41
+ moduleId: parseInt(testtaskModuleMatch[2]),
42
+ taskId: parseInt(testtaskModuleMatch[1])
43
+ };
44
+ }
45
+ // 匹配 testcase-browse-{productId}-byModule-{moduleId}
46
+ const testcaseModuleMatch = path.match(/testcase-browse-(\d+)-byModule-(\d+)/);
47
+ if (testcaseModuleMatch) {
48
+ return {
49
+ type: 'testcase',
50
+ productId: parseInt(testcaseModuleMatch[1]),
51
+ moduleId: parseInt(testcaseModuleMatch[2])
52
+ };
53
+ }
54
+ // 匹配 bug-browse-{productId}--byModule-{moduleId}
55
+ const bugModuleMatch = path.match(/bug-browse-(\d+)--byModule-(\d+)/);
56
+ if (bugModuleMatch) {
57
+ return {
58
+ type: 'bug',
59
+ productId: parseInt(bugModuleMatch[1]),
60
+ moduleId: parseInt(bugModuleMatch[2])
61
+ };
62
+ }
63
+ return null;
64
+ }
12
65
  /**
13
66
  * 解析自然语言时间表达式
14
67
  * 支持:今年、今年1月、最近3个月、今天、昨天、上个月等
@@ -240,17 +293,49 @@ server.tool("getBugDetail", {
240
293
  result.hasImages = result.images.length > 0;
241
294
  result.hasFiles = result.fileIds.length > 0;
242
295
  }
243
- // 下载图片(并行)
296
+ // 下载图片(并行,带超时控制)
297
+ let downloadedImages = [];
298
+ let imageDownloadStats = { total: 0, success: 0, failed: 0 };
244
299
  if (shouldDownloadImages && result.images.length > 0) {
245
- result.downloadedImages = await downloadImages(zentaoApi, result.images, true);
300
+ // 去重图片 URL,避免重复下载
301
+ const uniqueImageUrls = Array.from(new Set(result.images));
302
+ imageDownloadStats.total = uniqueImageUrls.length;
303
+ try {
304
+ // 使用超时控制,即使部分图片超时/失败也能继续
305
+ downloadedImages = await downloadImages(zentaoApi, uniqueImageUrls, true, 15000);
306
+ imageDownloadStats.success = downloadedImages.filter(img => img.success).length;
307
+ imageDownloadStats.failed = downloadedImages.filter(img => !img.success).length;
308
+ }
309
+ catch (error) {
310
+ // 即使下载过程出错,也继续返回结果(可能部分图片已下载成功)
311
+ imageDownloadStats.failed = imageDownloadStats.total;
312
+ console.warn(`图片下载过程中出现错误,但继续返回已成功下载的图片:`, error);
313
+ }
246
314
  }
315
+ // 构建返回的 JSON(移除原始图片 URL,避免 Cursor 循环读取)
316
+ const imageInfo = result.images.length > 0
317
+ ? `已找到 ${result.images.length} 张图片,成功下载 ${imageDownloadStats.success} 张${imageDownloadStats.failed > 0 ? `,${imageDownloadStats.failed} 张下载失败或超时` : ''}`
318
+ : [];
319
+ const jsonResult = {
320
+ ...result,
321
+ images: imageInfo,
322
+ downloadedImages: downloadedImages.map((img, idx) => ({
323
+ success: img.success,
324
+ size: img.size,
325
+ mimeType: img.mimeType,
326
+ // 不包含 base64 和 URL,避免 JSON 过大和循环读取
327
+ index: idx + 1,
328
+ error: img.success ? undefined : img.error
329
+ }))
330
+ };
247
331
  // 构建返回内容,包含文本和图片
248
332
  const content = [
249
- { type: "text", text: JSON.stringify(result, null, 2) }
333
+ { type: "text", text: JSON.stringify(jsonResult, null, 2) }
250
334
  ];
251
335
  // 添加图片内容(使用MCP协议的image类型)
252
- if (result.downloadedImages && result.downloadedImages.length > 0) {
253
- const imageContent = buildImageContent(result.downloadedImages, 'bug', bugId);
336
+ // 只添加成功下载的图片,失败的图片不会阻塞流程
337
+ if (downloadedImages && downloadedImages.length > 0) {
338
+ const imageContent = buildImageContent(downloadedImages, 'bug', bugId);
254
339
  content.push(...imageContent);
255
340
  }
256
341
  return {
@@ -364,11 +449,12 @@ server.tool("resolveBug", {
364
449
  // Add getProductStories tool
365
450
  server.tool("getProductStories", {
366
451
  productId: z.number(),
367
- status: z.enum(['draft', 'active', 'closed', 'changed', 'all']).optional()
368
- }, async ({ productId, status }) => {
452
+ status: z.enum(['draft', 'active', 'closed', 'changed', 'all']).optional(),
453
+ moduleId: z.number().optional()
454
+ }, async ({ productId, status, moduleId }) => {
369
455
  await ensureInitialized();
370
456
  try {
371
- const stories = await zentaoApi.getProductStories(productId, status);
457
+ const stories = await zentaoApi.getProductStories(productId, status, moduleId);
372
458
  return {
373
459
  content: [{ type: "text", text: JSON.stringify(stories, null, 2) }]
374
460
  };
@@ -402,20 +488,52 @@ server.tool("getStoryDetail", {
402
488
  result.hasImages = result.images.length > 0;
403
489
  result.hasFiles = result.fileIds.length > 0;
404
490
  }
405
- // 下载图片(并行)
491
+ // 下载图片(并行,带超时控制)
492
+ let downloadedImages = [];
493
+ let imageDownloadStats = { total: 0, success: 0, failed: 0 };
406
494
  if (shouldDownloadImages && result.images.length > 0) {
407
- result.downloadedImages = await downloadImages(zentaoApi, result.images, true);
495
+ // 去重图片 URL,避免重复下载
496
+ const uniqueImageUrls = Array.from(new Set(result.images));
497
+ imageDownloadStats.total = uniqueImageUrls.length;
498
+ try {
499
+ // 使用超时控制,即使部分图片超时/失败也能继续
500
+ downloadedImages = await downloadImages(zentaoApi, uniqueImageUrls, true, 15000);
501
+ imageDownloadStats.success = downloadedImages.filter(img => img.success).length;
502
+ imageDownloadStats.failed = downloadedImages.filter(img => !img.success).length;
503
+ }
504
+ catch (error) {
505
+ // 即使下载过程出错,也继续返回结果(可能部分图片已下载成功)
506
+ imageDownloadStats.failed = imageDownloadStats.total;
507
+ console.warn(`图片下载过程中出现错误,但继续返回已成功下载的图片:`, error);
508
+ }
408
509
  }
510
+ // 构建返回的 JSON(移除原始图片 URL,避免 Cursor 循环读取)
511
+ const imageInfo = result.images.length > 0
512
+ ? `已找到 ${result.images.length} 张图片,成功下载 ${imageDownloadStats.success} 张${imageDownloadStats.failed > 0 ? `,${imageDownloadStats.failed} 张下载失败或超时` : ''}`
513
+ : [];
514
+ const jsonResult = {
515
+ ...result,
516
+ images: imageInfo,
517
+ downloadedImages: downloadedImages.map((img, idx) => ({
518
+ success: img.success,
519
+ size: img.size,
520
+ mimeType: img.mimeType,
521
+ // 不包含 base64 和 URL,避免 JSON 过大和循环读取
522
+ index: idx + 1,
523
+ error: img.success ? undefined : img.error
524
+ }))
525
+ };
409
526
  // 构建返回内容,包含文本和图片
410
527
  const content = [
411
- { type: "text", text: JSON.stringify(result, null, 2) }
528
+ { type: "text", text: JSON.stringify(jsonResult, null, 2) }
412
529
  ];
413
530
  // 添加图片内容(使用MCP协议的image类型)
414
- if (result.downloadedImages && result.downloadedImages.length > 0) {
415
- const imageContent = buildImageContent(result.downloadedImages, 'story', storyId);
531
+ // 只添加成功下载的图片,失败的图片不会阻塞流程
532
+ if (downloadedImages && downloadedImages.length > 0) {
533
+ const imageContent = buildImageContent(downloadedImages, 'story', storyId);
416
534
  content.push(...imageContent);
417
535
  }
418
- // 添加下一步建议
536
+ // 添加下一步建议(即使图片下载失败,也继续提供建议)
419
537
  const relatedBugs = await zentaoApi.getStoryRelatedBugs(storyId).catch(() => []);
420
538
  const testCases = await zentaoApi.getStoryTestCases(storyId).catch(() => []);
421
539
  const suggestions = suggestNextActionsForStory(story, relatedBugs.length > 0, testCases.length > 0);
@@ -496,6 +614,105 @@ server.tool("searchStoriesByProductName", {
496
614
  }
497
615
  });
498
616
  // ==================== 测试用例相关接口 ====================
617
+ // Add getProductBugs tool
618
+ server.tool("getProductBugs", {
619
+ productId: z.number(),
620
+ status: z.enum(['active', 'resolved', 'closed', 'all']).optional(),
621
+ moduleId: z.number().optional()
622
+ }, async ({ productId, status, moduleId }) => {
623
+ await ensureInitialized();
624
+ try {
625
+ const bugs = await zentaoApi.getProductBugs(productId, status, moduleId);
626
+ return {
627
+ content: [{ type: "text", text: JSON.stringify(bugs, null, 2) }]
628
+ };
629
+ }
630
+ catch (error) {
631
+ if (error instanceof ZentaoError) {
632
+ throw error;
633
+ }
634
+ throw createError(ErrorCode.API_ERROR, `获取产品 ${productId} 的 Bug 列表失败: ${error instanceof Error ? error.message : String(error)}`, undefined, error);
635
+ }
636
+ });
637
+ // Add getModuleItems tool - 根据模块链接获取对应的需求、用例或Bug
638
+ server.tool("getModuleItems", {
639
+ url: z.string().describe("模块链接URL,例如:product-browse-245--byModule-1377.html")
640
+ }, async ({ url }) => {
641
+ await ensureInitialized();
642
+ try {
643
+ const parsed = parseModuleUrl(url);
644
+ if (!parsed) {
645
+ throw createError(ErrorCode.API_ERROR, `无法解析模块链接: ${url}。支持的格式:product-browse-{productId}--byModule-{moduleId}.html, bug-browse-{productId}--byModule-{moduleId}.html, testcase-browse-{productId}-byModule-{moduleId}.html`);
646
+ }
647
+ let result;
648
+ if (parsed.type === 'story') {
649
+ // 获取模块下的需求
650
+ const stories = await zentaoApi.getProductStories(parsed.productId, undefined, parsed.moduleId);
651
+ result = {
652
+ type: 'story',
653
+ productId: parsed.productId,
654
+ moduleId: parsed.moduleId,
655
+ items: stories,
656
+ count: stories.length
657
+ };
658
+ }
659
+ else if (parsed.type === 'testcase') {
660
+ // 获取模块下的用例
661
+ if (parsed.taskId) {
662
+ // 测试任务的用例模块,需要先获取任务详情获取产品ID
663
+ const task = await zentaoApi.getTaskDetail(parsed.taskId);
664
+ if (task.product) {
665
+ const productId = typeof task.product === 'string' ? parseInt(task.product) : task.product;
666
+ const testCases = await zentaoApi.getProductTestCases(productId, undefined, parsed.moduleId);
667
+ result = {
668
+ type: 'testcase',
669
+ productId: productId,
670
+ moduleId: parsed.moduleId,
671
+ taskId: parsed.taskId,
672
+ items: testCases,
673
+ count: testCases.length
674
+ };
675
+ }
676
+ else {
677
+ throw createError(ErrorCode.API_ERROR, `无法从任务 ${parsed.taskId} 获取产品ID`);
678
+ }
679
+ }
680
+ else if (parsed.productId > 0) {
681
+ const testCases = await zentaoApi.getProductTestCases(parsed.productId, undefined, parsed.moduleId);
682
+ result = {
683
+ type: 'testcase',
684
+ productId: parsed.productId,
685
+ moduleId: parsed.moduleId,
686
+ items: testCases,
687
+ count: testCases.length
688
+ };
689
+ }
690
+ else {
691
+ throw createError(ErrorCode.API_ERROR, `无法确定产品ID`);
692
+ }
693
+ }
694
+ else if (parsed.type === 'bug') {
695
+ // 获取模块下的Bug
696
+ const bugs = await zentaoApi.getProductBugs(parsed.productId, undefined, parsed.moduleId);
697
+ result = {
698
+ type: 'bug',
699
+ productId: parsed.productId,
700
+ moduleId: parsed.moduleId,
701
+ items: bugs,
702
+ count: bugs.length
703
+ };
704
+ }
705
+ return {
706
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
707
+ };
708
+ }
709
+ catch (error) {
710
+ if (error instanceof ZentaoError) {
711
+ throw error;
712
+ }
713
+ throw createError(ErrorCode.API_ERROR, `获取模块数据失败: ${error instanceof Error ? error.message : String(error)}`, undefined, error);
714
+ }
715
+ });
499
716
  // Add getProductTestCases tool
500
717
  server.tool("getProductTestCases", {
501
718
  productId: z.number(),
@@ -16,10 +16,12 @@ export interface DownloadedImage {
16
16
  * @param zentaoApi Zentao API 实例
17
17
  * @param imageUrls 图片 URL 列表
18
18
  * @param parallel 是否并行下载(默认 true)
19
+ * @param timeoutMs 单张图片超时时间(毫秒),默认 15 秒
19
20
  * @returns 下载结果数组
20
21
  */
21
- export declare function downloadImages(zentaoApi: ZentaoLegacyAPI, imageUrls: string[], parallel?: boolean): Promise<DownloadedImage[]>;
22
+ export declare function downloadImages(zentaoApi: ZentaoLegacyAPI, imageUrls: string[], parallel?: boolean, timeoutMs?: number): Promise<DownloadedImage[]>;
22
23
  /**
23
24
  * 构建 MCP image 内容数组
25
+ * 注意:不包含原始 URL,避免 Cursor 循环读取
24
26
  */
25
27
  export declare function buildImageContent(downloadedImages: DownloadedImage[], entityType: 'bug' | 'story', entityId: number): any[];
@@ -17,71 +17,91 @@ function detectMimeType(buffer) {
17
17
  }
18
18
  return 'image/png'; // 默认
19
19
  }
20
+ /**
21
+ * 带超时的图片下载
22
+ * @param zentaoApi Zentao API 实例
23
+ * @param url 图片 URL
24
+ * @param timeoutMs 超时时间(毫秒),默认 15 秒
25
+ * @returns 下载结果
26
+ */
27
+ async function downloadImageWithTimeout(zentaoApi, url, timeoutMs = 15000) {
28
+ const timeoutPromise = new Promise((resolve) => {
29
+ setTimeout(() => {
30
+ resolve({
31
+ url,
32
+ success: false,
33
+ error: `下载超时(${timeoutMs}ms)`
34
+ });
35
+ }, timeoutMs);
36
+ });
37
+ const downloadPromise = (async () => {
38
+ try {
39
+ const imageBuffer = await zentaoApi.downloadStoryImage(url);
40
+ const base64Image = imageBuffer.toString('base64');
41
+ const mimeType = detectMimeType(imageBuffer);
42
+ return {
43
+ url,
44
+ base64: base64Image,
45
+ mimeType,
46
+ size: imageBuffer.length,
47
+ success: true
48
+ };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ url,
53
+ success: false,
54
+ error: error instanceof Error ? error.message : String(error)
55
+ };
56
+ }
57
+ })();
58
+ // 使用 Promise.race 实现超时控制
59
+ return Promise.race([downloadPromise, timeoutPromise]);
60
+ }
20
61
  /**
21
62
  * 下载图片列表
22
63
  * @param zentaoApi Zentao API 实例
23
64
  * @param imageUrls 图片 URL 列表
24
65
  * @param parallel 是否并行下载(默认 true)
66
+ * @param timeoutMs 单张图片超时时间(毫秒),默认 15 秒
25
67
  * @returns 下载结果数组
26
68
  */
27
- export async function downloadImages(zentaoApi, imageUrls, parallel = true) {
69
+ export async function downloadImages(zentaoApi, imageUrls, parallel = true, timeoutMs = 15000) {
28
70
  if (imageUrls.length === 0) {
29
71
  return [];
30
72
  }
31
73
  if (parallel) {
32
- // 并行下载
33
- const downloadPromises = imageUrls.map(async (url) => {
34
- try {
35
- const imageBuffer = await zentaoApi.downloadStoryImage(url);
36
- const base64Image = imageBuffer.toString('base64');
37
- const mimeType = detectMimeType(imageBuffer);
38
- return {
39
- url,
40
- base64: base64Image,
41
- mimeType,
42
- size: imageBuffer.length,
43
- success: true
44
- };
74
+ // 并行下载,使用 Promise.allSettled 确保即使部分失败也能继续
75
+ const downloadPromises = imageUrls.map(url => downloadImageWithTimeout(zentaoApi, url, timeoutMs));
76
+ // 使用 allSettled 而不是 all,这样即使部分图片超时/失败,也能返回已成功的图片
77
+ const results = await Promise.allSettled(downloadPromises);
78
+ return results.map((result, index) => {
79
+ if (result.status === 'fulfilled') {
80
+ return result.value;
45
81
  }
46
- catch (error) {
82
+ else {
47
83
  return {
48
- url,
84
+ url: imageUrls[index],
49
85
  success: false,
50
- error: error instanceof Error ? error.message : String(error)
86
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
51
87
  };
52
88
  }
53
89
  });
54
- return Promise.all(downloadPromises);
55
90
  }
56
91
  else {
57
- // 串行下载(兼容旧逻辑)
92
+ // 串行下载(兼容旧逻辑),但添加超时控制
58
93
  const results = [];
59
94
  for (const url of imageUrls) {
60
- try {
61
- const imageBuffer = await zentaoApi.downloadStoryImage(url);
62
- const base64Image = imageBuffer.toString('base64');
63
- const mimeType = detectMimeType(imageBuffer);
64
- results.push({
65
- url,
66
- base64: base64Image,
67
- mimeType,
68
- size: imageBuffer.length,
69
- success: true
70
- });
71
- }
72
- catch (error) {
73
- results.push({
74
- url,
75
- success: false,
76
- error: error instanceof Error ? error.message : String(error)
77
- });
78
- }
95
+ const result = await downloadImageWithTimeout(zentaoApi, url, timeoutMs);
96
+ results.push(result);
97
+ // 即使失败也继续下载下一张
79
98
  }
80
99
  return results;
81
100
  }
82
101
  }
83
102
  /**
84
103
  * 构建 MCP image 内容数组
104
+ * 注意:不包含原始 URL,避免 Cursor 循环读取
85
105
  */
86
106
  export function buildImageContent(downloadedImages, entityType, entityId) {
87
107
  const content = [];
@@ -91,12 +111,8 @@ export function buildImageContent(downloadedImages, entityType, entityId) {
91
111
  type: "image",
92
112
  data: img.base64,
93
113
  mimeType: img.mimeType || 'image/png',
94
- annotations: {
95
- audience: ["user"],
96
- priority: 0.8,
97
- title: `${entityType === 'bug' ? 'Bug' : '需求'} ${entityId} 的图片 ${index + 1}`,
98
- description: `来源: ${img.url}`
99
- }
114
+ // 不包含 URL 信息,避免 Cursor 尝试读取原始 URL 导致循环
115
+ // 图片已通过 base64 内嵌,无需额外 URL
100
116
  });
101
117
  }
102
118
  });
@@ -2,7 +2,7 @@
2
2
  * 禅道旧版API (11.x版本)
3
3
  * 使用Session认证方式
4
4
  */
5
- import { Bug, Task, Story, StoryStatus, TestCase, TestCaseStatus, TestResult, TestTask, CreateTestCaseRequest, TestRunRequest, Product, TaskUpdate, BugResolution } from './types.js';
5
+ import { Bug, BugStatus, Task, Story, StoryStatus, TestCase, TestCaseStatus, TestResult, TestTask, CreateTestCaseRequest, TestRunRequest, Product, TaskUpdate, BugResolution } from './types.js';
6
6
  import { ZentaoConfig } from './config.js';
7
7
  export declare class ZentaoLegacyAPI {
8
8
  private config;
@@ -51,6 +51,13 @@ export declare class ZentaoLegacyAPI {
51
51
  * 获取我的Bug列表
52
52
  */
53
53
  getMyBugs(): Promise<Bug[]>;
54
+ /**
55
+ * 获取产品的Bug列表(支持分页和模块过滤)
56
+ * @param productId 产品ID
57
+ * @param status Bug状态(可选)
58
+ * @param moduleId 模块ID(可选),当提供时,只获取该模块下的Bug
59
+ */
60
+ getProductBugs(productId: number, status?: BugStatus, moduleId?: number): Promise<Bug[]>;
54
61
  /**
55
62
  * 获取Bug详情
56
63
  */
@@ -69,8 +76,11 @@ export declare class ZentaoLegacyAPI {
69
76
  resolveBug(bugId: number, resolution: BugResolution): Promise<void>;
70
77
  /**
71
78
  * 获取产品的需求列表(支持分页,自动获取所有需求)
79
+ * @param productId 产品ID
80
+ * @param status 需求状态(可选)
81
+ * @param moduleId 模块ID(可选),当提供时,只获取该模块下的需求
72
82
  */
73
- getProductStories(productId: number, status?: StoryStatus): Promise<Story[]>;
83
+ getProductStories(productId: number, status?: StoryStatus, moduleId?: number): Promise<Story[]>;
74
84
  /**
75
85
  * 获取需求详情
76
86
  */
@@ -227,6 +227,75 @@ export class ZentaoLegacyAPI {
227
227
  openedDate: bug.openedDate,
228
228
  }));
229
229
  }
230
+ /**
231
+ * 获取产品的Bug列表(支持分页和模块过滤)
232
+ * @param productId 产品ID
233
+ * @param status Bug状态(可选)
234
+ * @param moduleId 模块ID(可选),当提供时,只获取该模块下的Bug
235
+ */
236
+ async getProductBugs(productId, status, moduleId) {
237
+ try {
238
+ // 禅道11.x API路径:/bug-browse-{productId}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}.json
239
+ // 当 browseType = 'byModule' 时,param 是模块ID
240
+ // 当 browseType 是状态时,param 是 0
241
+ let browseType;
242
+ let param = 0;
243
+ if (moduleId) {
244
+ // 按模块浏览
245
+ browseType = 'byModule';
246
+ param = moduleId;
247
+ }
248
+ else if (status && status !== 'all') {
249
+ // 按状态浏览
250
+ browseType = status;
251
+ param = 0;
252
+ }
253
+ else {
254
+ // 浏览全部
255
+ browseType = 'all';
256
+ param = 0;
257
+ }
258
+ const allBugs = [];
259
+ let currentPage = 1;
260
+ const pageSize = 100;
261
+ let hasMore = true;
262
+ while (hasMore) {
263
+ const url = `/bug-browse-${productId}-0-${browseType}-${param}-id_desc-0-${pageSize}-${currentPage}.json`;
264
+ const data = await this.request(url);
265
+ const bugs = data.bugs || {};
266
+ const bugsArray = Object.values(bugs);
267
+ allBugs.push(...bugsArray);
268
+ // 检查分页信息
269
+ if (data.pager) {
270
+ const { recTotal, recPerPage, pageID } = data.pager;
271
+ const totalPages = Math.ceil(recTotal / recPerPage);
272
+ hasMore = currentPage < totalPages && bugsArray.length > 0;
273
+ }
274
+ else {
275
+ hasMore = false;
276
+ }
277
+ currentPage++;
278
+ // 安全限制:最多获取100页
279
+ if (currentPage > 100) {
280
+ break;
281
+ }
282
+ }
283
+ return allBugs.map((bug) => ({
284
+ id: parseInt(bug.id),
285
+ title: bug.title,
286
+ status: bug.status,
287
+ severity: parseInt(bug.severity),
288
+ steps: bug.steps || '',
289
+ openedDate: bug.openedDate,
290
+ product: bug.product ? parseInt(bug.product) : undefined,
291
+ module: bug.module ? parseInt(bug.module) : undefined,
292
+ }));
293
+ }
294
+ catch (error) {
295
+ console.error('获取产品Bug列表失败:', error);
296
+ throw error;
297
+ }
298
+ }
230
299
  /**
231
300
  * 获取Bug详情
232
301
  */
@@ -281,15 +350,18 @@ export class ZentaoLegacyAPI {
281
350
  }
282
351
  /**
283
352
  * 获取产品的需求列表(支持分页,自动获取所有需求)
353
+ * @param productId 产品ID
354
+ * @param status 需求状态(可选)
355
+ * @param moduleId 模块ID(可选),当提供时,只获取该模块下的需求
284
356
  */
285
- async getProductStories(productId, status) {
357
+ async getProductStories(productId, status, moduleId) {
286
358
  // 禅道11.x API分页支持:
287
359
  // URL格式:/product-browse-{productId}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}.json
288
360
  // 参数说明:
289
361
  // - productId: 产品ID
290
362
  // - branch: 分支(默认0)
291
- // - browseType: unclosed(未关闭) | all(全部) | active(激活) | draft(草稿) | closed(已关闭) | changed(已变更)
292
- // - param: 模块ID或查询ID(默认0
363
+ // - browseType: unclosed(未关闭) | all(全部) | active(激活) | draft(草稿) | closed(已关闭) | changed(已变更) | byModule(按模块)
364
+ // - param: 模块ID或查询ID(默认0),当browseType=byModule时,param是模块ID
293
365
  // - orderBy: 排序字段(默认id_desc)
294
366
  // - recTotal: 总记录数(可以为0,系统会自动计算)
295
367
  // - recPerPage: 每页记录数(默认20,可以设置更大值如100、500)
@@ -300,31 +372,40 @@ export class ZentaoLegacyAPI {
300
372
  let hasMore = true;
301
373
  // 映射status参数到browseType
302
374
  // 注意:禅道11.x的browseType只支持简单的值,不像RESTful API那样复杂
303
- let browseType = 'unclosed'; // 默认获取未关闭的需求
304
- if (status) {
305
- switch (status) {
306
- case 'all':
307
- browseType = 'unclosed'; // 11.x中all返回0条,所以用unclosed代替
308
- break;
309
- case 'active':
310
- browseType = 'unclosed'; // active也映射到unclosed
311
- break;
312
- case 'draft':
313
- browseType = 'unclosed'; // draft也映射到unclosed
314
- break;
315
- case 'closed':
316
- browseType = 'unclosed'; // closed也映射到unclosed
317
- break;
318
- case 'changed':
319
- browseType = 'unclosed'; // changed也映射到unclosed
320
- break;
321
- default:
322
- browseType = 'unclosed';
375
+ let browseType;
376
+ let param = 0;
377
+ if (moduleId) {
378
+ // 按模块浏览
379
+ browseType = 'byModule';
380
+ param = moduleId;
381
+ }
382
+ else {
383
+ browseType = 'unclosed'; // 默认获取未关闭的需求
384
+ if (status) {
385
+ switch (status) {
386
+ case 'all':
387
+ browseType = 'unclosed'; // 11.x中all返回0条,所以用unclosed代替
388
+ break;
389
+ case 'active':
390
+ browseType = 'unclosed'; // active也映射到unclosed
391
+ break;
392
+ case 'draft':
393
+ browseType = 'unclosed'; // draft也映射到unclosed
394
+ break;
395
+ case 'closed':
396
+ browseType = 'unclosed'; // closed也映射到unclosed
397
+ break;
398
+ case 'changed':
399
+ browseType = 'unclosed'; // changed也映射到unclosed
400
+ break;
401
+ default:
402
+ browseType = 'unclosed';
403
+ }
323
404
  }
324
405
  }
325
406
  while (hasMore) {
326
407
  // 构建URL:/product-browse-{productId}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}.json
327
- const url = `/product-browse-${productId}-0-${browseType}-0-id_desc-0-${pageSize}-${currentPage}.json`;
408
+ const url = `/product-browse-${productId}-0-${browseType}-${param}-id_desc-0-${pageSize}-${currentPage}.json`;
328
409
  const data = await this.request(url);
329
410
  const stories = data.stories || {};
330
411
  const storiesArray = Object.values(stories);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxenlin/mcp-zentao-11-3",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Zentao 11.3 legacy 版 MCP 服务器,只支持旧版 Session API,纯净无 REST v1",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -50,4 +50,3 @@
50
50
  "access": "public"
51
51
  }
52
52
  }
53
-