@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 +6 -0
- package/README.md +10 -6
- package/package.json +1 -1
- package/src/index.js +10 -0
- package/src/tools.js +8 -0
- package/src/zentao.js +198 -25
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
|
|
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
|
|
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
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("&", "&");
|
|
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])
|
|
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"'<>]
|
|
417
|
+
const urlRegex = /https?:\/\/[^\s"'<>]+/gi;
|
|
326
418
|
for (const match of text.matchAll(urlRegex)) {
|
|
327
|
-
if (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
|
-
|
|
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
|
|
360
|
-
status
|
|
361
|
-
|
|
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
|
|
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
|
-
|
|
382
|
-
resp = await call({ path: candidate, method: "GET", query });
|
|
383
|
-
|
|
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 (!
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
});
|