@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 +23 -54
- package/dist/index.js +231 -14
- package/dist/utils/imageDownloader.d.ts +3 -1
- package/dist/utils/imageDownloader.js +60 -44
- package/dist/zentaoLegacyApi.d.ts +12 -2
- package/dist/zentaoLegacyApi.js +105 -24
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -15,27 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
## 📦 安装
|
|
17
17
|
|
|
18
|
-
### 方法 1
|
|
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
|
-
### 方法
|
|
47
|
+
### 方法 2:使用 npx
|
|
65
48
|
|
|
66
|
-
在支持 MCP 的 IDE
|
|
49
|
+
在支持 MCP 的 IDE/工具配置文件中添加(以 Cursor IDE 为例):
|
|
67
50
|
|
|
68
51
|
```json
|
|
69
52
|
{
|
|
70
53
|
"mcpServers": {
|
|
71
54
|
"zentao-11-3": {
|
|
72
|
-
"command": "
|
|
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
|
-
|
|
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(
|
|
333
|
+
{ type: "text", text: JSON.stringify(jsonResult, null, 2) }
|
|
250
334
|
];
|
|
251
335
|
// 添加图片内容(使用MCP协议的image类型)
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
528
|
+
{ type: "text", text: JSON.stringify(jsonResult, null, 2) }
|
|
412
529
|
];
|
|
413
530
|
// 添加图片内容(使用MCP协议的image类型)
|
|
414
|
-
|
|
415
|
-
|
|
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(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
82
|
+
else {
|
|
47
83
|
return {
|
|
48
|
-
url,
|
|
84
|
+
url: imageUrls[index],
|
|
49
85
|
success: false,
|
|
50
|
-
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
*/
|
package/dist/zentaoLegacyApi.js
CHANGED
|
@@ -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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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}-
|
|
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.
|
|
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
|
-
|