@aipper/zentao-mcp-server 0.1.8 → 0.1.9

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/.env.example CHANGED
@@ -11,12 +11,18 @@ ZENTAO_PASSWORD=your_password
11
11
  # 可选:默认产品 ID(当实例请求 bug 列表需要 product id 时使用)
12
12
  # ZENTAO_PRODUCT_ID=1
13
13
 
14
+ # 可选:默认项目集 ID(当 bug 在项目集视角时使用)
15
+ # ZENTAO_PROJECT_SET_ID=1001
16
+
14
17
  # 可选:我的 bug 列表路径(例如 /my/bug 或 /my/bugs)
15
18
  # ZENTAO_MY_BUGS_PATH=/my/bug
16
19
 
17
20
  # 可选:bug 列表回退路径(逗号分隔)
18
21
  # ZENTAO_BUGS_FALLBACK_PATHS=/my/bug,/my/bugs
19
22
 
23
+ # 可选:项目集 bug 路径模板(逗号分隔,支持 {projectSetId} 占位)
24
+ # ZENTAO_PROJECT_SET_BUGS_PATHS=/projectsets/{projectSetId}/bugs,/programs/{projectSetId}/bugs
25
+
20
26
  # 获取 Token 的路径(相对 ZENTAO_BASE_URL;通常为 /api.php/v1/tokens)
21
27
  ZENTAO_TOKEN_PATH=/api.php/v1/tokens
22
28
 
package/README.md CHANGED
@@ -17,8 +17,10 @@
17
17
  - `ZENTAO_ACCOUNT`
18
18
  - `ZENTAO_PASSWORD`
19
19
  - (可选)`ZENTAO_PRODUCT_ID`:你的禅道实例若报 `Need product id` 时设置
20
+ - (可选)`ZENTAO_PROJECT_SET_ID`:你的 bug 若在项目集视角,设置项目集 ID
20
21
  - (可选)`ZENTAO_MY_BUGS_PATH`:我的 bug 专用接口路径(如 `/my/bug`)
21
22
  - (可选)`ZENTAO_BUGS_FALLBACK_PATHS`:bug 列表回退路径(逗号分隔)
23
+ - (可选)`ZENTAO_PROJECT_SET_BUGS_PATHS`:项目集 bug 路径模板(支持 `{projectSetId}`)
22
24
 
23
25
  > 注意:不同禅道版本/部署方式的 token 端点与返回结构可能不同;可通过 `ZENTAO_TOKEN_PATH`/`ZENTAO_API_PREFIX` 调整。
24
26
  >
@@ -90,7 +92,9 @@ npm run smoke
90
92
  `-32000` 通常是客户端侧“通用 MCP 调用失败”映射码,优先检查:
91
93
  - `env` 是否完整传入(尤其是 `ZENTAO_BASE_URL`/`ZENTAO_ACCOUNT`/`ZENTAO_PASSWORD`)。
92
94
  - 若报 `Need product id`,请设置 `ZENTAO_PRODUCT_ID`,或在 `get_my_bugs` 传 `productId`。
93
- - 若你的 bug 在“项目集/我的视角”而非产品,建议设置 `ZENTAO_MY_BUGS_PATH=/my/bug`(或 `/my/bugs`)。
95
+ - 若你的 bug 在“项目集/我的视角”而非产品,建议设置 `ZENTAO_PROJECT_SET_ID`,并配置 `ZENTAO_MY_BUGS_PATH=/my/bug`。
96
+ - `get_my_bugs` 会按候选路径回退(包含项目集路径);即使首个路径返回空列表也会继续尝试。
97
+ - 排查时看工具返回里的 `raw.triedPaths`,可确认每条路径的返回码与命中数量。
94
98
  - `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致。
95
99
  - MCP 客户端是否真的在执行 `npx -y @aipper/zentao-mcp-server`(而不是旧的本地命令)。
96
100
  - 客户端日志中是否有启动报错(如找不到命令、401、超时)。
@@ -99,11 +103,10 @@ npm run smoke
99
103
  - `get_token`:获取/刷新 token(默认不回显完整 token)
100
104
  - `call`:调用任意相对 API 路径(自动带 Token 头)
101
105
  - `list_my_projects`:示例:列出“我参与的项目”(字段匹配基于常见返回结构,可能需按你的实例微调)
102
- - `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`,默认路径 `/bugs`)
103
- - `get_bug_detail`:按 `id` 获取 bug 详情(默认路径模板 `/bugs/{id}`,返回详情与图片链接)
104
- - `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`)
106
+ - `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`/`projectSetId`,默认路径 `/bugs`)
107
+ - `get_bug_detail`:按 `id` 获取 bug 详情(默认路径模板 `/bugs/{id}`,返回详情与图片链接;会提取富文本 `<img>`、Markdown 图片、附件图片并归一化为可访问 URL)
105
108
  - `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`,支持 `solution` 解决说明)
106
- - `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`)
109
+ - `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`/`projectSetId`)
107
110
  - `close_bug`:按 `id` 关闭 bug
108
111
  - `verify_bug`:验证结果处理(`pass`=关闭,`fail`=激活)
109
112
  - `comment_bug`:按 `id` 添加备注(默认路径 `/bugs/{id}/comment`)
@@ -114,7 +117,8 @@ npm run smoke
114
117
  - `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"批量修复"}`
115
118
  - `batch_resolve_my_bugs`(建议):`{"status":"active","maxItems":20,"solution":"统一修复分页参数为空导致的报错"}`
116
119
  - `get_my_bugs`(按产品):`{"status":"active","productId":1,"limit":50}`
117
- - `get_my_bugs`(项目集/我的):`{"status":"active","path":"/my/bug","limit":50}`
120
+ - `get_my_bugs`(项目集):`{"status":"active","projectSetId":1001,"limit":50}`
121
+ - `get_my_bugs`(我的):`{"status":"active","path":"/my/bug","limit":50}`
118
122
  - `close_bug`:`{"id":123,"comment":"验证通过,关闭"}`
119
123
  - `verify_bug`:`{"id":123,"result":"pass","comment":"验证通过"}`
120
124
  - `comment_bug`:`{"id":123,"comment":"已复现,正在定位根因"}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipper/zentao-mcp-server",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A minimal MCP server for ZenTao (token + generic REST call + a few helper tools).",
package/src/index.js CHANGED
@@ -50,11 +50,16 @@ function getConfigFromEnv() {
50
50
  const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
51
51
  const exposeToken = String(process.env.ZENTAO_EXPOSE_TOKEN || "false").toLowerCase() === "true";
52
52
  const defaultProductId = Number(process.env.ZENTAO_PRODUCT_ID || "0") || null;
53
+ const defaultProjectSetId = Number(process.env.ZENTAO_PROJECT_SET_ID || "0") || null;
53
54
  const myBugsPath = String(process.env.ZENTAO_MY_BUGS_PATH || "").trim();
54
55
  const bugsFallbackPaths = String(process.env.ZENTAO_BUGS_FALLBACK_PATHS || "")
55
56
  .split(",")
56
57
  .map((value) => value.trim())
57
58
  .filter(Boolean);
59
+ const projectSetBugsPaths = String(process.env.ZENTAO_PROJECT_SET_BUGS_PATHS || "")
60
+ .split(",")
61
+ .map((value) => value.trim())
62
+ .filter(Boolean);
58
63
 
59
64
  const account = requireEnv("ZENTAO_ACCOUNT");
60
65
  const password = requireEnv("ZENTAO_PASSWORD");
@@ -67,8 +72,10 @@ function getConfigFromEnv() {
67
72
  timeoutMs,
68
73
  exposeToken,
69
74
  defaultProductId,
75
+ defaultProjectSetId,
70
76
  myBugsPath,
71
77
  bugsFallbackPaths,
78
+ projectSetBugsPaths,
72
79
  auth: { account, password },
73
80
  };
74
81
  }
@@ -124,6 +131,7 @@ async function main() {
124
131
  limit: args.limit,
125
132
  page: args.page,
126
133
  productId: args.productId,
134
+ projectSetId: args.projectSetId,
127
135
  path: args.path || "/bugs",
128
136
  assignedTo: args.assignedTo || "",
129
137
  });
@@ -156,6 +164,7 @@ async function main() {
156
164
  limit: args.limit,
157
165
  page: args.page,
158
166
  productId: args.productId,
167
+ projectSetId: args.projectSetId,
159
168
  maxItems: args.maxItems,
160
169
  assignedTo: args.assignedTo || "",
161
170
  resolution: args.resolution || "fixed",
@@ -206,6 +215,7 @@ async function main() {
206
215
  status: err?.status ?? null,
207
216
  data: err?.data ?? null,
208
217
  hint: "If you see 'Need product id', set env ZENTAO_PRODUCT_ID or pass productId in get_my_bugs.",
218
+ hint2: "For project-set instances, set ZENTAO_PROJECT_SET_ID or ZENTAO_MY_BUGS_PATH.",
209
219
  };
210
220
  return toMcpTextResult(JSON.stringify(errorPayload, null, 2), { isError: true });
211
221
  }
package/src/tools.js CHANGED
@@ -53,6 +53,7 @@ export const TOOLS = [
53
53
  limit: { type: "number", minimum: 1, maximum: 200, description: "Default 20, max 200" },
54
54
  page: { type: "number", minimum: 1, description: "Default 1" },
55
55
  productId: { type: "number", minimum: 1, description: "Optional product id (for instances requiring product scope)" },
56
+ projectSetId: { type: "number", minimum: 1, description: "Optional project-set id (for project-set scope)" },
56
57
  path: { type: "string", description: "Optional bugs endpoint override, default /bugs" },
57
58
  assignedTo: { type: "string", description: "Optional assignee override, default current account" },
58
59
  },
@@ -99,6 +100,7 @@ export const TOOLS = [
99
100
  limit: { type: "number", minimum: 1, maximum: 200, description: "List page size, default 50" },
100
101
  page: { type: "number", minimum: 1, description: "Default 1" },
101
102
  productId: { type: "number", minimum: 1, description: "Optional product id (for instances requiring product scope)" },
103
+ projectSetId: { type: "number", minimum: 1, description: "Optional project-set id (for project-set scope)" },
102
104
  maxItems: { type: "number", minimum: 1, maximum: 500, description: "Max resolve count, default 50" },
103
105
  assignedTo: { type: "string", description: "Optional assignee override" },
104
106
  resolution: { type: "string", description: "Default fixed" },
@@ -176,6 +178,9 @@ export function assertToolArgs(name, args) {
176
178
  if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
177
179
  throw new Error("get_my_bugs.productId must be a number >= 1");
178
180
  }
181
+ if (args.projectSetId !== undefined && (!Number.isFinite(args.projectSetId) || args.projectSetId < 1)) {
182
+ throw new Error("get_my_bugs.projectSetId must be a number >= 1");
183
+ }
179
184
  }
180
185
  if (name === "get_bug_detail") {
181
186
  if (!Number.isFinite(args.id) || Number(args.id) < 1) {
@@ -209,6 +214,9 @@ export function assertToolArgs(name, args) {
209
214
  if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
210
215
  throw new Error("batch_resolve_my_bugs.productId must be a number >= 1");
211
216
  }
217
+ if (args.projectSetId !== undefined && (!Number.isFinite(args.projectSetId) || args.projectSetId < 1)) {
218
+ throw new Error("batch_resolve_my_bugs.projectSetId must be a number >= 1");
219
+ }
212
220
  if (args.listPath !== undefined && typeof args.listPath !== "string") {
213
221
  throw new Error("batch_resolve_my_bugs.listPath must be a string");
214
222
  }
package/src/zentao.js CHANGED
@@ -38,8 +38,10 @@ export function createZenTaoClient(config) {
38
38
  tokenTtlMs,
39
39
  timeoutMs,
40
40
  defaultProductId,
41
+ defaultProjectSetId,
41
42
  myBugsPath,
42
43
  bugsFallbackPaths,
44
+ projectSetBugsPaths,
43
45
  auth,
44
46
  } = config;
45
47
 
@@ -209,11 +211,48 @@ export function createZenTaoClient(config) {
209
211
  return template.replaceAll("{account}", encodeURIComponent(auth.account || ""));
210
212
  }
211
213
 
214
+ function buildProjectSetPath(pathTemplate, projectSetId) {
215
+ const template = String(pathTemplate || "").trim();
216
+ if (!template) return "";
217
+ const pid = normalizePositiveInt(projectSetId);
218
+ if (!pid) return "";
219
+ return template.replaceAll("{projectSetId}", String(pid));
220
+ }
221
+
222
+ function isMyBugsPath(path) {
223
+ const normalized = String(path || "").toLowerCase();
224
+ return normalized.includes("/my/bug");
225
+ }
226
+
227
+ function isProjectSetPath(path) {
228
+ const normalized = String(path || "").toLowerCase();
229
+ return (
230
+ normalized.includes("/projectset") ||
231
+ normalized.includes("/project-set") ||
232
+ normalized.includes("/projectsets") ||
233
+ normalized.includes("/programs/")
234
+ );
235
+ }
236
+
212
237
  function isNeedProductIdError(err) {
213
238
  const merged = `${String(err?.message || "")} ${JSON.stringify(err?.data || "")}`.toLowerCase();
214
239
  return merged.includes("need product id");
215
240
  }
216
241
 
242
+ function buildBugsQueryForPath({ path, limit, page, assignedTo, status, productId }) {
243
+ if (isMyBugsPath(path) || isProjectSetPath(path)) {
244
+ // "我的bug"和"项目集bug"类端点在部分实例不接受 assignedTo/status/product 参数,使用最小分页参数后本地过滤。
245
+ return { limit, page };
246
+ }
247
+ return {
248
+ limit,
249
+ page,
250
+ assignedTo: assignedTo || undefined,
251
+ status: status || undefined,
252
+ product: productId || undefined,
253
+ };
254
+ }
255
+
217
256
  function buildResolutionComment({ solution, comment, resolution }) {
218
257
  const normalizedSolution = String(solution || "").trim();
219
258
  if (normalizedSolution) return `解决说明:${normalizedSolution}`;
@@ -313,23 +352,105 @@ export function createZenTaoClient(config) {
313
352
  return Number.isFinite(id) && id > 0 ? id : null;
314
353
  }
315
354
 
355
+ function normalizeResourceUrl(value) {
356
+ const raw = String(value || "").trim();
357
+ if (!raw) return "";
358
+ if (raw.startsWith("data:")) return "";
359
+ const cleaned = raw.replaceAll("&amp;", "&");
360
+ if (/^https?:\/\//i.test(cleaned)) return cleaned;
361
+ if (cleaned.startsWith("//")) {
362
+ const protocol = new URL(baseUrl).protocol || "https:";
363
+ return `${protocol}${cleaned}`;
364
+ }
365
+ if (cleaned.startsWith("/")) {
366
+ return new URL(cleaned, `${baseUrl}/`).toString();
367
+ }
368
+ const normalized = cleaned.replace(/^\.?\//, "");
369
+ if (!normalized) return "";
370
+ return new URL(`/${normalized}`, `${baseUrl}/`).toString();
371
+ }
372
+
373
+ function isImageLikeText(value) {
374
+ const text = String(value || "").toLowerCase();
375
+ if (!text) return false;
376
+ if (text.includes("image/")) return true;
377
+ return /\.(png|jpe?g|gif|webp|bmp|svg|ico|avif)(?:$|[?#])/i.test(text);
378
+ }
379
+
380
+ function fileRecordLooksImage(record) {
381
+ if (!record || typeof record !== "object") return false;
382
+ if (record.isImage === true || record.isImage === 1 || record.image === true) return true;
383
+ const probes = [
384
+ record.mime,
385
+ record.contentType,
386
+ record.extension,
387
+ record.ext,
388
+ record.title,
389
+ record.name,
390
+ record.pathname,
391
+ record.path,
392
+ record.url,
393
+ record.webPath,
394
+ record.downloadUrl,
395
+ record.downloadurl,
396
+ ];
397
+ return probes.some((value) => isImageLikeText(value));
398
+ }
399
+
316
400
  function extractImageUrlsFromBug(bug) {
317
401
  const images = new Set();
402
+ const addImageUrl = (value) => {
403
+ const url = normalizeResourceUrl(value);
404
+ if (url) images.add(url);
405
+ };
318
406
  const addMatches = (value) => {
319
407
  if (!value) return;
320
408
  const text = String(value);
321
409
  const srcRegex = /<img[^>]+src=["']([^"']+)["']/gi;
322
410
  for (const match of text.matchAll(srcRegex)) {
323
- if (match[1]) images.add(match[1]);
411
+ if (match[1]) addImageUrl(match[1]);
412
+ }
413
+ const markdownImageRegex = /!\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/gi;
414
+ for (const match of text.matchAll(markdownImageRegex)) {
415
+ if (match[1]) addImageUrl(match[1]);
324
416
  }
325
- const urlRegex = /https?:\/\/[^\s"'<>]+\.(png|jpe?g|gif|webp|bmp|svg)/gi;
417
+ const urlRegex = /https?:\/\/[^\s"'<>]+/gi;
326
418
  for (const match of text.matchAll(urlRegex)) {
327
- if (match[0]) images.add(match[0]);
419
+ if (isImageLikeText(match[0])) addImageUrl(match[0]);
328
420
  }
329
421
  };
422
+
423
+ const addImagesFromFiles = (files) => {
424
+ if (!files) return;
425
+ const list = Array.isArray(files) ? files : Object.values(files);
426
+ for (const item of list) {
427
+ if (!item || typeof item !== "object") continue;
428
+ if (!fileRecordLooksImage(item)) continue;
429
+ const candidates = [
430
+ item.webPath,
431
+ item.url,
432
+ item.downloadUrl,
433
+ item.downloadurl,
434
+ item.path,
435
+ item.pathname,
436
+ item.viewUrl,
437
+ item.href,
438
+ ];
439
+ for (const candidate of candidates) {
440
+ if (candidate) addImageUrl(candidate);
441
+ }
442
+ }
443
+ };
444
+
330
445
  addMatches(bug?.steps);
331
446
  addMatches(bug?.stepsHtml);
447
+ addMatches(bug?.reproStep);
448
+ addMatches(bug?.comment);
449
+ addMatches(bug?.resolution);
332
450
  addMatches(bug?.openedBuild);
451
+ addImagesFromFiles(bug?.files);
452
+ addImagesFromFiles(bug?.attachments);
453
+ addImagesFromFiles(bug?.openedFiles);
333
454
  return Array.from(images);
334
455
  }
335
456
 
@@ -339,6 +460,7 @@ export function createZenTaoClient(config) {
339
460
  limit = 20,
340
461
  page = 1,
341
462
  productId,
463
+ projectSetId,
342
464
  path = "/bugs",
343
465
  assignedTo,
344
466
  } = {}) {
@@ -346,26 +468,46 @@ export function createZenTaoClient(config) {
346
468
  const safePage = Math.max(1, Number(page) || 1);
347
469
  const assignee = normalizeString(assignedTo) || normalizeString(auth.account);
348
470
  const effectiveProductId = normalizePositiveInt(productId) || normalizePositiveInt(defaultProductId);
471
+ const effectiveProjectSetId = normalizePositiveInt(projectSetId) || normalizePositiveInt(defaultProjectSetId);
349
472
  const primaryPath = buildProductScopedBugsPath({ productId: effectiveProductId, path });
473
+ const preferProjectSetPath =
474
+ effectiveProjectSetId &&
475
+ (!path || String(path).trim() === "" || String(path).trim() === "/bugs");
350
476
  const configuredMyBugsPath = buildMyBugsPath(myBugsPath);
351
477
  const fallbackPathCandidates = (bugsFallbackPaths && bugsFallbackPaths.length > 0)
352
478
  ? bugsFallbackPaths
353
479
  : ["/my/bug", "/my/bugs"];
354
480
  const fallbackPaths = fallbackPathCandidates.map((item) => buildMyBugsPath(item)).filter(Boolean);
355
-
356
- const query = {
481
+ const projectSetCandidates = (projectSetBugsPaths && projectSetBugsPaths.length > 0)
482
+ ? projectSetBugsPaths
483
+ : ["/projectsets/{projectSetId}/bugs", "/projectset/{projectSetId}/bugs", "/programs/{projectSetId}/bugs"];
484
+ const projectSetPaths = projectSetCandidates
485
+ .map((item) => buildProjectSetPath(item, effectiveProjectSetId))
486
+ .filter(Boolean);
487
+
488
+ const baseQuery = {
357
489
  limit: safeLimit,
358
490
  page: safePage,
359
- assignedTo: assignee || undefined,
360
- status: status || undefined,
361
- product: effectiveProductId || undefined,
491
+ assignedTo: assignee,
492
+ status,
493
+ productId: effectiveProductId,
362
494
  };
363
495
  const candidatePaths = [];
496
+ if (preferProjectSetPath) {
497
+ for (const projectSetPath of projectSetPaths) {
498
+ if (!candidatePaths.includes(projectSetPath)) candidatePaths.push(projectSetPath);
499
+ }
500
+ }
364
501
  candidatePaths.push(primaryPath);
365
502
  if (effectiveProductId) {
366
503
  const productPath = `/products/${effectiveProductId}/bugs`;
367
504
  if (!candidatePaths.includes(productPath)) candidatePaths.push(productPath);
368
505
  }
506
+ if (!preferProjectSetPath) {
507
+ for (const projectSetPath of projectSetPaths) {
508
+ if (!candidatePaths.includes(projectSetPath)) candidatePaths.push(projectSetPath);
509
+ }
510
+ }
369
511
  if (configuredMyBugsPath && !candidatePaths.includes(configuredMyBugsPath)) {
370
512
  candidatePaths.push(configuredMyBugsPath);
371
513
  }
@@ -373,34 +515,63 @@ export function createZenTaoClient(config) {
373
515
  if (!candidatePaths.includes(fallback)) candidatePaths.push(fallback);
374
516
  }
375
517
 
376
- let resp = null;
377
- let usedPath = primaryPath;
518
+ let bestResult = null;
378
519
  let lastErr = null;
520
+ const triedPaths = [];
379
521
  for (const candidate of candidatePaths) {
380
522
  try {
381
- usedPath = candidate;
382
- resp = await call({ path: candidate, method: "GET", query });
383
- break;
523
+ const query = buildBugsQueryForPath({ path: candidate, ...baseQuery });
524
+ const resp = await call({ path: candidate, method: "GET", query });
525
+ const bugs = parseBugsFromResponse(resp?.data);
526
+ const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
527
+
528
+ triedPaths.push({
529
+ path: candidate,
530
+ status: resp?.status ?? null,
531
+ total: bugs.length,
532
+ matched: filtered.length,
533
+ });
534
+
535
+ const currentResult = {
536
+ total: bugs.length,
537
+ matched: filtered.length,
538
+ page: safePage,
539
+ limit: safeLimit,
540
+ productId: effectiveProductId,
541
+ projectSetId: effectiveProjectSetId,
542
+ assignedTo: assignee,
543
+ bugs: filtered,
544
+ raw: { status: resp?.status, path: candidate },
545
+ };
546
+
547
+ if (
548
+ !bestResult ||
549
+ currentResult.matched > bestResult.matched ||
550
+ (currentResult.matched === bestResult.matched && currentResult.total > bestResult.total)
551
+ ) {
552
+ bestResult = currentResult;
553
+ }
554
+
555
+ if (currentResult.matched > 0) break;
384
556
  } catch (err) {
385
557
  lastErr = err;
558
+ triedPaths.push({
559
+ path: candidate,
560
+ status: err?.status ?? null,
561
+ error: String(err?.message || err),
562
+ });
386
563
  if (!isNeedProductIdError(err)) continue;
387
564
  if (candidate !== primaryPath) continue;
388
565
  }
389
566
  }
390
- if (!resp) throw lastErr;
391
-
392
- const bugs = parseBugsFromResponse(resp?.data);
393
- const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
567
+ if (!bestResult) throw lastErr;
394
568
 
395
569
  return {
396
- total: bugs.length,
397
- matched: filtered.length,
398
- page: safePage,
399
- limit: safeLimit,
400
- productId: effectiveProductId,
401
- assignedTo: assignee,
402
- bugs: filtered,
403
- raw: { status: resp?.status, path: usedPath },
570
+ ...bestResult,
571
+ raw: {
572
+ ...bestResult.raw,
573
+ triedPaths,
574
+ },
404
575
  };
405
576
  }
406
577
 
@@ -572,6 +743,7 @@ export function createZenTaoClient(config) {
572
743
  limit = 50,
573
744
  page = 1,
574
745
  productId,
746
+ projectSetId,
575
747
  maxItems = 50,
576
748
  assignedTo = "",
577
749
  resolution = "fixed",
@@ -588,6 +760,7 @@ export function createZenTaoClient(config) {
588
760
  limit,
589
761
  page,
590
762
  productId,
763
+ projectSetId,
591
764
  path: listPath,
592
765
  assignedTo,
593
766
  });