@maxenlin/mcp-zentao-11-3 1.0.0-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,815 @@
1
+ /**
2
+ * 禅道旧版API (11.x版本)
3
+ * 使用Session认证方式
4
+ */
5
+ import axios from 'axios';
6
+ export class ZentaoLegacyAPI {
7
+ constructor(config) {
8
+ this.sessionId = null;
9
+ this.config = config;
10
+ // 禅道11.x使用的是传统的URL格式,不是RESTful API
11
+ this.client = axios.create({
12
+ baseURL: this.config.url,
13
+ timeout: 10000,
14
+ headers: {
15
+ 'Content-Type': 'application/x-www-form-urlencoded',
16
+ },
17
+ });
18
+ }
19
+ /**
20
+ * 获取SessionID
21
+ */
22
+ async getSessionId() {
23
+ if (this.sessionId)
24
+ return this.sessionId;
25
+ try {
26
+ const response = await this.client.get('/api-getSessionID.json');
27
+ if (response.data.status === 'success') {
28
+ const data = JSON.parse(response.data.data);
29
+ this.sessionId = data.sessionID;
30
+ return this.sessionId;
31
+ }
32
+ throw new Error(`获取SessionID失败: ${JSON.stringify(response.data)}`);
33
+ }
34
+ catch (error) {
35
+ if (axios.isAxiosError(error)) {
36
+ const errorMessage = error.response
37
+ ? `状态码: ${error.response.status}, 响应: ${JSON.stringify(error.response.data)}`
38
+ : error.message;
39
+ throw new Error(`获取SessionID失败: ${errorMessage}`);
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ /**
45
+ * 登录
46
+ */
47
+ async login() {
48
+ const sid = await this.getSessionId();
49
+ try {
50
+ const params = new URLSearchParams();
51
+ params.append('account', this.config.username);
52
+ params.append('password', this.config.password);
53
+ params.append('keepLogin[]', 'on');
54
+ params.append('referer', `${this.config.url}/my/`);
55
+ const response = await this.client.post(`/user-login.json?zentaosid=${sid}`, params);
56
+ if (response.data.status === 'success') {
57
+ return;
58
+ }
59
+ throw new Error(`登录失败: ${JSON.stringify(response.data)}`);
60
+ }
61
+ catch (error) {
62
+ if (axios.isAxiosError(error)) {
63
+ const errorMessage = error.response
64
+ ? `状态码: ${error.response.status}, 响应: ${JSON.stringify(error.response.data)}`
65
+ : error.message;
66
+ throw new Error(`登录失败: ${errorMessage}`);
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ /**
72
+ * 确保已登录
73
+ */
74
+ async ensureLoggedIn() {
75
+ if (!this.sessionId) {
76
+ await this.login();
77
+ }
78
+ return this.sessionId;
79
+ }
80
+ /**
81
+ * 发起请求
82
+ */
83
+ async request(url, params) {
84
+ const sid = await this.ensureLoggedIn();
85
+ try {
86
+ const fullUrl = `${url}?zentaosid=${sid}`;
87
+ const response = await this.client.get(fullUrl, { params });
88
+ if (response.data.status === 'success') {
89
+ return JSON.parse(response.data.data);
90
+ }
91
+ throw new Error(`请求失败: ${JSON.stringify(response.data)}`);
92
+ }
93
+ catch (error) {
94
+ if (axios.isAxiosError(error)) {
95
+ console.error('请求失败:', {
96
+ status: error.response?.status,
97
+ data: error.response?.data,
98
+ message: error.message
99
+ });
100
+ throw new Error(`请求失败: ${error.message}`);
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+ /**
106
+ * POST请求
107
+ */
108
+ async postRequest(url, data) {
109
+ const sid = await this.ensureLoggedIn();
110
+ try {
111
+ const fullUrl = `${url}?zentaosid=${sid}`;
112
+ const params = new URLSearchParams();
113
+ if (data) {
114
+ Object.keys(data).forEach(key => {
115
+ params.append(key, data[key]);
116
+ });
117
+ }
118
+ const response = await this.client.post(fullUrl, params);
119
+ if (response.data.status === 'success') {
120
+ return response.data.data ? JSON.parse(response.data.data) : response.data;
121
+ }
122
+ throw new Error(`请求失败: ${JSON.stringify(response.data)}`);
123
+ }
124
+ catch (error) {
125
+ if (axios.isAxiosError(error)) {
126
+ console.error('请求失败:', {
127
+ status: error.response?.status,
128
+ data: error.response?.data,
129
+ message: error.message
130
+ });
131
+ throw new Error(`请求失败: ${error.message}`);
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+ /**
137
+ * 获取产品列表
138
+ */
139
+ async getProducts() {
140
+ const data = await this.request('/product-index-no.json');
141
+ const products = data.products || {};
142
+ return Object.keys(products).map(id => ({
143
+ id: parseInt(id),
144
+ name: products[id],
145
+ code: '',
146
+ status: 'normal',
147
+ desc: ''
148
+ }));
149
+ }
150
+ /**
151
+ * 获取产品的模块树
152
+ * @param productId 产品ID
153
+ * @returns 模块树对象,key是模块ID,value是模块名称
154
+ */
155
+ async getProductModules(productId) {
156
+ try {
157
+ // 尝试多个可能的API路径
158
+ const paths = [
159
+ `/product-browse-${productId}.json`, // 产品浏览页面
160
+ `/story-create-${productId}.json`, // 创建需求页面(包含模块树)
161
+ ];
162
+ for (const apiPath of paths) {
163
+ try {
164
+ const data = await this.request(apiPath);
165
+ const modules = data.modules || data.moduleTree || {};
166
+ if (Object.keys(modules).length > 0) {
167
+ // modules 格式: { "1296": "家族养成游戏道具兑换——肖仲政", ... }
168
+ return modules;
169
+ }
170
+ }
171
+ catch (err) {
172
+ // 继续尝试下一个路径
173
+ continue;
174
+ }
175
+ }
176
+ return {};
177
+ }
178
+ catch (error) {
179
+ console.error(`获取产品${productId}的模块树失败:`, error);
180
+ return {};
181
+ }
182
+ }
183
+ /**
184
+ * 获取我的任务列表
185
+ */
186
+ async getMyTasks() {
187
+ const data = await this.request('/my-task.json');
188
+ const tasks = data.tasks || {};
189
+ return Object.values(tasks).map((task) => ({
190
+ id: parseInt(task.id),
191
+ name: task.name,
192
+ status: task.status,
193
+ pri: parseInt(task.pri),
194
+ deadline: task.deadline,
195
+ desc: task.desc || '',
196
+ }));
197
+ }
198
+ /**
199
+ * 获取任务详情
200
+ */
201
+ async getTaskDetail(taskId) {
202
+ const data = await this.request(`/task-view-${taskId}.json`);
203
+ const task = data.task;
204
+ return {
205
+ id: parseInt(task.id),
206
+ name: task.name,
207
+ status: task.status,
208
+ pri: parseInt(task.pri),
209
+ deadline: task.deadline,
210
+ desc: task.desc || '',
211
+ story: task.story || undefined,
212
+ product: task.product || undefined,
213
+ };
214
+ }
215
+ /**
216
+ * 获取我的Bug列表
217
+ */
218
+ async getMyBugs() {
219
+ const data = await this.request('/my-bug.json');
220
+ const bugs = data.bugs || {};
221
+ return Object.values(bugs).map((bug) => ({
222
+ id: parseInt(bug.id),
223
+ title: bug.title,
224
+ status: bug.status,
225
+ severity: parseInt(bug.severity),
226
+ steps: bug.steps || '',
227
+ openedDate: bug.openedDate,
228
+ }));
229
+ }
230
+ /**
231
+ * 获取Bug详情
232
+ */
233
+ async getBugDetail(bugId) {
234
+ const data = await this.request(`/bug-view-${bugId}.json`);
235
+ const bug = data.bug;
236
+ const product = data.product;
237
+ return {
238
+ id: parseInt(bug.id),
239
+ title: bug.title,
240
+ status: bug.status,
241
+ severity: parseInt(bug.severity),
242
+ steps: bug.steps || '',
243
+ openedDate: bug.openedDate,
244
+ story: bug.story || undefined,
245
+ product: bug.product || undefined,
246
+ productName: product?.name || undefined,
247
+ };
248
+ }
249
+ /**
250
+ * 更新任务
251
+ */
252
+ async updateTask(taskId, update) {
253
+ await this.postRequest(`/task-edit-${taskId}.json`, {
254
+ consumed: update.consumed,
255
+ left: update.left,
256
+ status: update.status,
257
+ comment: update.comment || '',
258
+ });
259
+ // 返回更新后的任务详情
260
+ return await this.getTaskDetail(taskId);
261
+ }
262
+ /**
263
+ * 完成任务
264
+ */
265
+ async finishTask(taskId, update) {
266
+ await this.postRequest(`/task-finish-${taskId}.json`, {
267
+ consumed: update.consumed || 0,
268
+ finishedDate: update.finishedDate || new Date().toISOString().split('T')[0],
269
+ comment: update.comment || '',
270
+ });
271
+ }
272
+ /**
273
+ * 解决Bug
274
+ */
275
+ async resolveBug(bugId, resolution) {
276
+ await this.postRequest(`/bug-resolve-${bugId}.json`, {
277
+ resolution: resolution.resolution,
278
+ resolvedBuild: resolution.resolvedBuild || '',
279
+ comment: resolution.comment || '',
280
+ });
281
+ }
282
+ /**
283
+ * 获取产品的需求列表(支持分页,自动获取所有需求)
284
+ */
285
+ async getProductStories(productId, status) {
286
+ // 禅道11.x API分页支持:
287
+ // URL格式:/product-browse-{productId}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}.json
288
+ // 参数说明:
289
+ // - productId: 产品ID
290
+ // - branch: 分支(默认0)
291
+ // - browseType: unclosed(未关闭) | all(全部) | active(激活) | draft(草稿) | closed(已关闭) | changed(已变更)
292
+ // - param: 模块ID或查询ID(默认0)
293
+ // - orderBy: 排序字段(默认id_desc)
294
+ // - recTotal: 总记录数(可以为0,系统会自动计算)
295
+ // - recPerPage: 每页记录数(默认20,可以设置更大值如100、500)
296
+ // - pageID: 页码(从1开始)
297
+ const allStories = [];
298
+ let currentPage = 1;
299
+ const pageSize = 100; // 每页获取100条
300
+ let hasMore = true;
301
+ // 映射status参数到browseType
302
+ // 注意:禅道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';
323
+ }
324
+ }
325
+ while (hasMore) {
326
+ // 构建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`;
328
+ const data = await this.request(url);
329
+ const stories = data.stories || {};
330
+ const storiesArray = Object.values(stories);
331
+ // 添加到结果数组
332
+ allStories.push(...storiesArray);
333
+ // 检查分页信息
334
+ if (data.pager) {
335
+ const { recTotal, recPerPage, pageID } = data.pager;
336
+ const totalPages = Math.ceil(recTotal / recPerPage);
337
+ // 判断是否还有更多数据
338
+ hasMore = currentPage < totalPages && storiesArray.length > 0;
339
+ }
340
+ else {
341
+ // 没有分页信息,说明没有更多数据
342
+ hasMore = false;
343
+ }
344
+ currentPage++;
345
+ // 安全限制:最多获取100页,避免无限循环
346
+ if (currentPage > 100) {
347
+ break;
348
+ }
349
+ }
350
+ // 映射为标准格式
351
+ const mappedStories = allStories.map((story) => ({
352
+ id: parseInt(story.id),
353
+ title: story.title,
354
+ status: story.status,
355
+ pri: parseInt(story.pri),
356
+ stage: story.stage,
357
+ estimate: story.estimate ? parseFloat(story.estimate) : undefined,
358
+ openedBy: story.openedBy,
359
+ openedDate: story.openedDate,
360
+ assignedTo: story.assignedTo,
361
+ spec: story.spec || '',
362
+ }));
363
+ return mappedStories;
364
+ }
365
+ /**
366
+ * 获取需求详情
367
+ */
368
+ async getStoryDetail(storyId) {
369
+ const data = await this.request(`/story-view-${storyId}.json`);
370
+ const story = data.story;
371
+ const product = data.product;
372
+ // 获取模块名称
373
+ let moduleName;
374
+ // 如果有模块ID,从模块树API获取模块名称
375
+ if (story.module && story.module !== '0' && story.product) {
376
+ try {
377
+ const modules = await this.getProductModules(parseInt(story.product));
378
+ moduleName = modules[story.module];
379
+ }
380
+ catch (error) {
381
+ console.error('获取模块名称失败:', error);
382
+ }
383
+ }
384
+ return {
385
+ id: parseInt(story.id),
386
+ title: story.title,
387
+ status: story.status,
388
+ pri: parseInt(story.pri),
389
+ stage: story.stage,
390
+ estimate: story.estimate ? parseFloat(story.estimate) : undefined,
391
+ openedBy: story.openedBy,
392
+ openedDate: story.openedDate,
393
+ assignedTo: story.assignedTo,
394
+ spec: story.spec || '',
395
+ module: story.module,
396
+ moduleName: moduleName,
397
+ product: story.product,
398
+ productName: product?.name,
399
+ };
400
+ }
401
+ /**
402
+ * 下载需求中的图片文件
403
+ */
404
+ async downloadStoryImage(imageUrl) {
405
+ const sid = await this.ensureLoggedIn();
406
+ // 构建完整的图片URL
407
+ let fullImageUrl;
408
+ if (imageUrl.startsWith('/zentao/')) {
409
+ // 移除重复的 /zentao 前缀
410
+ const cleanUrl = imageUrl.replace('/zentao/', '/');
411
+ fullImageUrl = `${this.config.url}${cleanUrl}`;
412
+ }
413
+ else if (imageUrl.startsWith('/')) {
414
+ fullImageUrl = `${this.config.url}${imageUrl}`;
415
+ }
416
+ else {
417
+ fullImageUrl = imageUrl;
418
+ }
419
+ const response = await this.client.get(fullImageUrl, {
420
+ params: { zentaosid: sid },
421
+ responseType: 'arraybuffer',
422
+ timeout: 30000
423
+ });
424
+ return Buffer.from(response.data);
425
+ }
426
+ /**
427
+ * 提取需求描述中的所有图片URL
428
+ */
429
+ extractImageUrls(spec) {
430
+ const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/g;
431
+ const images = [];
432
+ let match;
433
+ while ((match = imgRegex.exec(spec)) !== null) {
434
+ images.push(match[1]);
435
+ }
436
+ return images;
437
+ }
438
+ /**
439
+ * 提取需求描述中的文件ID
440
+ */
441
+ extractFileIds(spec) {
442
+ const fileRegex = /file-read-(\d+)/g;
443
+ const fileIds = [];
444
+ let match;
445
+ while ((match = fileRegex.exec(spec)) !== null) {
446
+ fileIds.push(match[1]);
447
+ }
448
+ return fileIds;
449
+ }
450
+ /**
451
+ * 搜索需求(通过关键字)
452
+ * 由于禅道11.3的搜索API权限限制,我们通过获取所有产品的需求然后本地过滤
453
+ */
454
+ async searchStories(keyword, options) {
455
+ const { productId, status, limit = 50 } = options || {};
456
+ try {
457
+ let allStories = [];
458
+ if (productId) {
459
+ // 搜索指定产品的需求
460
+ allStories = await this.getProductStories(productId, status);
461
+ }
462
+ else {
463
+ // 搜索所有产品的需求
464
+ const products = await this.getProducts();
465
+ // 限制搜索范围,避免请求过多
466
+ const searchProducts = products.slice(0, 20); // 只搜索前20个产品
467
+ for (const product of searchProducts) {
468
+ try {
469
+ const stories = await this.getProductStories(product.id, status);
470
+ allStories.push(...stories);
471
+ }
472
+ catch (error) {
473
+ continue;
474
+ }
475
+ }
476
+ }
477
+ // 本地过滤:按关键字搜索
478
+ const keyword_lower = keyword.toLowerCase();
479
+ const matchedStories = allStories.filter(story => {
480
+ // 在标题、描述中搜索关键字
481
+ const titleMatch = story.title.toLowerCase().includes(keyword_lower);
482
+ const specMatch = story.spec && story.spec.toLowerCase().includes(keyword_lower);
483
+ return titleMatch || specMatch;
484
+ });
485
+ // 按相关性排序(标题匹配优先)
486
+ matchedStories.sort((a, b) => {
487
+ const aTitle = a.title.toLowerCase().includes(keyword_lower);
488
+ const bTitle = b.title.toLowerCase().includes(keyword_lower);
489
+ if (aTitle && !bTitle)
490
+ return -1;
491
+ if (!aTitle && bTitle)
492
+ return 1;
493
+ // 都匹配标题或都不匹配标题,按ID倒序(新的在前)
494
+ return b.id - a.id;
495
+ });
496
+ // 限制返回数量
497
+ return matchedStories.slice(0, limit);
498
+ }
499
+ catch (error) {
500
+ console.error('搜索需求失败:', error);
501
+ throw new Error(`搜索需求失败: ${error instanceof Error ? error.message : String(error)}`);
502
+ }
503
+ }
504
+ /**
505
+ * 按产品名称搜索需求
506
+ */
507
+ async searchStoriesByProductName(productName, keyword, options) {
508
+ try {
509
+ // 获取所有产品
510
+ const products = await this.getProducts();
511
+ // 按产品名称过滤
512
+ const matchedProducts = products.filter(product => product.name.toLowerCase().includes(productName.toLowerCase()));
513
+ const results = [];
514
+ for (const product of matchedProducts) {
515
+ try {
516
+ const stories = await this.searchStories(keyword, {
517
+ productId: product.id,
518
+ status: options?.status,
519
+ limit: options?.limit
520
+ });
521
+ if (stories.length > 0) {
522
+ results.push({ product, stories });
523
+ }
524
+ }
525
+ catch (error) {
526
+ continue;
527
+ }
528
+ }
529
+ return results;
530
+ }
531
+ catch (error) {
532
+ console.error('按产品名称搜索需求失败:', error);
533
+ throw new Error(`按产品名称搜索需求失败: ${error instanceof Error ? error.message : String(error)}`);
534
+ }
535
+ }
536
+ /**
537
+ * 获取产品的测试用例列表
538
+ * @param productId 产品ID
539
+ * @param status 用例状态
540
+ * @param moduleId 模块ID(可选)
541
+ */
542
+ async getProductTestCases(productId, status, moduleId) {
543
+ try {
544
+ // 禅道11.x API路径:/testcase-browse-{productId}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}.json
545
+ // 当 browseType = 'byModule' 时,param 是模块ID
546
+ // 当 browseType 是状态时,param 是 0
547
+ let browseType;
548
+ let param;
549
+ if (moduleId) {
550
+ // 按模块浏览
551
+ browseType = 'byModule';
552
+ param = moduleId;
553
+ }
554
+ else if (status && status !== 'all') {
555
+ // 按状态浏览
556
+ browseType = status;
557
+ param = 0;
558
+ }
559
+ else {
560
+ // 浏览全部
561
+ browseType = 'all';
562
+ param = 0;
563
+ }
564
+ const url = `/testcase-browse-${productId}-0-${browseType}-${param}-id_desc-0-100-1.json`;
565
+ const data = await this.request(url);
566
+ const cases = data.cases || {};
567
+ const casesArray = Object.values(cases);
568
+ const mappedCases = casesArray.map((testCase) => ({
569
+ id: parseInt(testCase.id),
570
+ product: parseInt(testCase.product),
571
+ module: testCase.module ? parseInt(testCase.module) : undefined,
572
+ story: testCase.story ? parseInt(testCase.story) : undefined,
573
+ title: testCase.title,
574
+ type: testCase.type,
575
+ pri: parseInt(testCase.pri),
576
+ status: testCase.status,
577
+ precondition: testCase.precondition || '',
578
+ steps: testCase.steps || '',
579
+ openedBy: testCase.openedBy,
580
+ openedDate: testCase.openedDate,
581
+ lastEditedBy: testCase.lastEditedBy,
582
+ lastEditedDate: testCase.lastEditedDate,
583
+ }));
584
+ return mappedCases;
585
+ }
586
+ catch (error) {
587
+ console.error('获取测试用例列表失败:', error);
588
+ throw error;
589
+ }
590
+ }
591
+ /**
592
+ * 获取测试用例详情
593
+ */
594
+ async getTestCaseDetail(caseId) {
595
+ try {
596
+ const data = await this.request(`/testcase-view-${caseId}.json`);
597
+ const testCase = data.case;
598
+ return {
599
+ id: parseInt(testCase.id),
600
+ product: parseInt(testCase.product),
601
+ productName: data.product?.name,
602
+ module: testCase.module ? parseInt(testCase.module) : undefined,
603
+ moduleName: testCase.moduleName,
604
+ story: testCase.story ? parseInt(testCase.story) : undefined,
605
+ title: testCase.title,
606
+ type: testCase.type,
607
+ pri: parseInt(testCase.pri),
608
+ status: testCase.status,
609
+ precondition: testCase.precondition || '',
610
+ steps: testCase.steps || '',
611
+ openedBy: testCase.openedBy,
612
+ openedDate: testCase.openedDate,
613
+ lastEditedBy: testCase.lastEditedBy,
614
+ lastEditedDate: testCase.lastEditedDate,
615
+ };
616
+ }
617
+ catch (error) {
618
+ console.error('获取测试用例详情失败:', error);
619
+ throw error;
620
+ }
621
+ }
622
+ /**
623
+ * 创建测试用例
624
+ */
625
+ async createTestCase(testCase) {
626
+ try {
627
+ const data = await this.postRequest(`/testcase-create-${testCase.product}.json`, {
628
+ title: testCase.title,
629
+ type: testCase.type || 'feature',
630
+ pri: testCase.pri || 3,
631
+ module: testCase.module || 0,
632
+ story: testCase.story || 0,
633
+ precondition: testCase.precondition || '',
634
+ steps: testCase.steps || '',
635
+ status: testCase.status || 'normal',
636
+ });
637
+ // 从响应中提取测试用例ID
638
+ return data.id || 0;
639
+ }
640
+ catch (error) {
641
+ console.error('创建测试用例失败:', error);
642
+ throw error;
643
+ }
644
+ }
645
+ /**
646
+ * 获取需求的测试用例
647
+ */
648
+ async getStoryTestCases(storyId) {
649
+ try {
650
+ const data = await this.request(`/story-view-${storyId}.json`);
651
+ const cases = data.cases || {};
652
+ const casesArray = Object.values(cases);
653
+ const mappedCases = casesArray.map((testCase) => ({
654
+ id: parseInt(testCase.id),
655
+ title: testCase.title,
656
+ type: testCase.type,
657
+ pri: parseInt(testCase.pri),
658
+ status: testCase.status,
659
+ }));
660
+ return mappedCases;
661
+ }
662
+ catch (error) {
663
+ console.error('获取需求测试用例失败:', error);
664
+ return [];
665
+ }
666
+ }
667
+ /**
668
+ * 获取测试单列表
669
+ */
670
+ async getTestTasks(productId) {
671
+ try {
672
+ // 禅道11.x API路径:/testtask-browse-{productId}.json
673
+ const url = productId ? `/testtask-browse-${productId}.json` : '/my-testtask.json';
674
+ const data = await this.request(url);
675
+ const tasks = data.tasks || {};
676
+ const tasksArray = Object.values(tasks);
677
+ const mappedTasks = tasksArray.map((task) => ({
678
+ id: parseInt(task.id),
679
+ name: task.name,
680
+ product: parseInt(task.product),
681
+ productName: task.productName,
682
+ project: task.project ? parseInt(task.project) : undefined,
683
+ execution: task.execution ? parseInt(task.execution) : undefined,
684
+ build: task.build,
685
+ owner: task.owner,
686
+ status: task.status,
687
+ begin: task.begin,
688
+ end: task.end,
689
+ desc: task.desc || '',
690
+ }));
691
+ return mappedTasks;
692
+ }
693
+ catch (error) {
694
+ console.error('获取测试单列表失败:', error);
695
+ throw error;
696
+ }
697
+ }
698
+ /**
699
+ * 获取测试单详情
700
+ */
701
+ async getTestTaskDetail(taskId) {
702
+ try {
703
+ const data = await this.request(`/testtask-view-${taskId}.json`);
704
+ const task = data.task;
705
+ return {
706
+ id: parseInt(task.id),
707
+ name: task.name,
708
+ product: parseInt(task.product),
709
+ productName: data.product?.name,
710
+ project: task.project ? parseInt(task.project) : undefined,
711
+ execution: task.execution ? parseInt(task.execution) : undefined,
712
+ build: task.build,
713
+ owner: task.owner,
714
+ status: task.status,
715
+ begin: task.begin,
716
+ end: task.end,
717
+ desc: task.desc || '',
718
+ };
719
+ }
720
+ catch (error) {
721
+ console.error('获取测试单详情失败:', error);
722
+ throw error;
723
+ }
724
+ }
725
+ /**
726
+ * 获取测试单的测试结果
727
+ */
728
+ async getTestTaskResults(taskId) {
729
+ try {
730
+ const data = await this.request(`/testtask-cases-${taskId}.json`);
731
+ const runs = data.runs || {};
732
+ const runsArray = Object.values(runs);
733
+ const mappedResults = runsArray.map((run) => ({
734
+ id: parseInt(run.id),
735
+ run: parseInt(run.task),
736
+ case: parseInt(run.case),
737
+ caseTitle: run.title,
738
+ version: parseInt(run.version),
739
+ status: run.caseStatus,
740
+ lastRunner: run.lastRunner,
741
+ lastRunDate: run.lastRunDate,
742
+ lastRunResult: run.lastRunResult,
743
+ }));
744
+ return mappedResults;
745
+ }
746
+ catch (error) {
747
+ console.error('获取测试结果失败:', error);
748
+ return [];
749
+ }
750
+ }
751
+ /**
752
+ * 执行测试用例
753
+ */
754
+ async runTestCase(taskId, testRun) {
755
+ try {
756
+ await this.postRequest(`/testtask-runCase-${taskId}-${testRun.caseId}.json`, {
757
+ version: testRun.version || 1,
758
+ caseResult: testRun.result,
759
+ steps: testRun.steps || '',
760
+ comment: testRun.comment || '',
761
+ });
762
+ }
763
+ catch (error) {
764
+ console.error('执行测试用例失败:', error);
765
+ throw error;
766
+ }
767
+ }
768
+ /**
769
+ * 获取需求关联的 Bug 列表
770
+ */
771
+ async getStoryRelatedBugs(storyId) {
772
+ try {
773
+ // 获取所有 Bug,然后过滤出关联到该需求的 Bug
774
+ const allBugs = await this.getMyBugs();
775
+ const relatedBugs = [];
776
+ // 并行获取所有 Bug 的详情以检查关联关系
777
+ const bugDetailsPromises = allBugs.map(bug => this.getBugDetail(bug.id).catch(() => null));
778
+ const bugDetails = await Promise.all(bugDetailsPromises);
779
+ for (const bugDetail of bugDetails) {
780
+ if (bugDetail && bugDetail.story) {
781
+ const bugStoryId = typeof bugDetail.story === 'string'
782
+ ? parseInt(bugDetail.story)
783
+ : bugDetail.story;
784
+ if (bugStoryId === storyId) {
785
+ relatedBugs.push(bugDetail);
786
+ }
787
+ }
788
+ }
789
+ return relatedBugs;
790
+ }
791
+ catch (error) {
792
+ console.error(`获取需求 ${storyId} 关联的 Bug 失败:`, error);
793
+ throw error;
794
+ }
795
+ }
796
+ /**
797
+ * 获取 Bug 关联的需求
798
+ */
799
+ async getBugRelatedStory(bugId) {
800
+ try {
801
+ const bug = await this.getBugDetail(bugId);
802
+ if (!bug.story) {
803
+ return null;
804
+ }
805
+ const storyId = typeof bug.story === 'string'
806
+ ? parseInt(bug.story)
807
+ : bug.story;
808
+ return await this.getStoryDetail(storyId);
809
+ }
810
+ catch (error) {
811
+ console.error(`获取 Bug ${bugId} 关联的需求失败:`, error);
812
+ return null;
813
+ }
814
+ }
815
+ }