@maxenlin/mcp-zentao-11-3 1.0.2 → 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/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.2",
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",