@aipper/zentao-mcp-server 0.1.9 → 0.1.11
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 -3
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +41 -18
- package/package.json +28 -4
- package/scripts/smoke.mjs +2 -1
- package/src/index.js +65 -40
- package/src/tools.js +82 -64
- package/src/zentao.js +650 -110
- package/scripts/release-npm.sh +0 -481
package/src/zentao.js
CHANGED
|
@@ -1,19 +1,69 @@
|
|
|
1
|
+
// Constants
|
|
2
|
+
const DEFAULT_RESOLUTION_PREFIX = "解决说明:";
|
|
3
|
+
const DEFAULT_RESOLUTION_FALLBACK = "已处理";
|
|
4
|
+
const MAX_ERROR_TEXT_LENGTH = 2000;
|
|
5
|
+
const MAX_BATCH_RESOLVE_ITEMS = 100;
|
|
6
|
+
const SAFE_BUG_LIST_PATHS = new Set(["/bugs", "/my/bug", "/my/bugs"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sleep for a specified duration
|
|
10
|
+
* @param {number} ms - Milliseconds to sleep
|
|
11
|
+
* @returns {Promise<void>}
|
|
12
|
+
*/
|
|
1
13
|
function sleep(ms) {
|
|
2
14
|
return new Promise((r) => setTimeout(r, ms));
|
|
3
15
|
}
|
|
4
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Check if a value looks like an absolute URL
|
|
19
|
+
* @param {string} value - Value to check
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
5
22
|
function isProbablyAbsoluteUrl(value) {
|
|
6
23
|
return /^https?:\/\//i.test(value);
|
|
7
24
|
}
|
|
8
25
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
26
|
+
function assertSafeRelativePath(value, fieldName) {
|
|
27
|
+
const raw = String(value || "").trim();
|
|
28
|
+
if (!raw) throw new Error(`${fieldName} is required`);
|
|
29
|
+
if (isProbablyAbsoluteUrl(raw)) {
|
|
30
|
+
throw new Error(`${fieldName} must be a relative path`);
|
|
31
|
+
}
|
|
32
|
+
if (raw.startsWith("//")) {
|
|
33
|
+
throw new Error(`${fieldName} must not be protocol-relative`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalized = raw.startsWith("/") ? raw : `/${raw}`;
|
|
37
|
+
if (normalized.includes("?") || normalized.includes("#")) {
|
|
38
|
+
throw new Error(`${fieldName} must not include query or hash`);
|
|
13
39
|
}
|
|
14
|
-
const
|
|
15
|
-
|
|
40
|
+
const lowered = normalized.toLowerCase();
|
|
41
|
+
if (lowered.includes("\\") || lowered.includes("%5c") || lowered.includes("%2f")) {
|
|
42
|
+
throw new Error(`${fieldName} must not contain backslashes or encoded separators`);
|
|
43
|
+
}
|
|
44
|
+
if (/(^|\/)\.{1,2}(?:\/|$)/.test(normalized) || lowered.includes("%2e")) {
|
|
45
|
+
throw new Error(`${fieldName} must not contain dot segments`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a full URL from base, prefix, path and query parameters
|
|
52
|
+
* @param {Object} params
|
|
53
|
+
* @param {string} params.baseUrl - Base URL
|
|
54
|
+
* @param {string} params.apiPrefix - API prefix
|
|
55
|
+
* @param {string} params.path - Relative path
|
|
56
|
+
* @param {Object} [params.query] - Query parameters
|
|
57
|
+
* @returns {URL}
|
|
58
|
+
*/
|
|
59
|
+
function buildUrl({ baseUrl, apiPrefix, path, query }) {
|
|
60
|
+
const normalizedPath = assertSafeRelativePath(path, "path");
|
|
61
|
+
const prefix = assertSafeRelativePath(apiPrefix, "apiPrefix").replace(/\/+$/, "");
|
|
16
62
|
const url = new URL(`${baseUrl}${prefix}${normalizedPath}`);
|
|
63
|
+
const allowedPrefix = `${prefix}/`;
|
|
64
|
+
if (url.pathname !== prefix && !url.pathname.startsWith(allowedPrefix)) {
|
|
65
|
+
throw new Error("path escapes apiPrefix");
|
|
66
|
+
}
|
|
17
67
|
|
|
18
68
|
if (query && typeof query === "object") {
|
|
19
69
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -24,12 +74,35 @@ function buildUrl({ baseUrl, apiPrefix, path, query }) {
|
|
|
24
74
|
return url;
|
|
25
75
|
}
|
|
26
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Create an abort signal with timeout
|
|
79
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
80
|
+
* @returns {{signal: AbortSignal, cleanup: Function}}
|
|
81
|
+
*/
|
|
27
82
|
function createAbortSignal(timeoutMs) {
|
|
28
83
|
const controller = new AbortController();
|
|
29
84
|
const timer = setTimeout(() => controller.abort(new Error("Request timeout")), timeoutMs);
|
|
30
85
|
return { signal: controller.signal, cleanup: () => clearTimeout(timer) };
|
|
31
86
|
}
|
|
32
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Create a ZenTao API client
|
|
90
|
+
* @param {Object} config - Client configuration
|
|
91
|
+
* @param {string} config.baseUrl - ZenTao base URL
|
|
92
|
+
* @param {string} config.apiPrefix - API prefix path
|
|
93
|
+
* @param {string} config.tokenPath - Token endpoint path
|
|
94
|
+
* @param {number} config.tokenTtlMs - Token TTL in milliseconds
|
|
95
|
+
* @param {number} config.timeoutMs - Request timeout in milliseconds
|
|
96
|
+
* @param {number} [config.defaultProductId] - Default product ID
|
|
97
|
+
* @param {number} [config.defaultProjectSetId] - Default project set ID
|
|
98
|
+
* @param {string} [config.myBugsPath] - My bugs path
|
|
99
|
+
* @param {string[]} [config.bugsFallbackPaths] - Bug fallback paths
|
|
100
|
+
* @param {string[]} [config.projectSetBugsPaths] - Project set bug paths
|
|
101
|
+
* @param {Object} config.auth - Authentication credentials
|
|
102
|
+
* @param {string} config.auth.account - Account name
|
|
103
|
+
* @param {string} config.auth.password - Account password
|
|
104
|
+
* @returns {Object} ZenTao client instance
|
|
105
|
+
*/
|
|
33
106
|
export function createZenTaoClient(config) {
|
|
34
107
|
const {
|
|
35
108
|
baseUrl,
|
|
@@ -37,6 +110,7 @@ export function createZenTaoClient(config) {
|
|
|
37
110
|
tokenPath,
|
|
38
111
|
tokenTtlMs,
|
|
39
112
|
timeoutMs,
|
|
113
|
+
allowInsecureHttp,
|
|
40
114
|
defaultProductId,
|
|
41
115
|
defaultProjectSetId,
|
|
42
116
|
myBugsPath,
|
|
@@ -45,23 +119,36 @@ export function createZenTaoClient(config) {
|
|
|
45
119
|
auth,
|
|
46
120
|
} = config;
|
|
47
121
|
|
|
122
|
+
const normalizedBaseUrl = String(baseUrl || "").replace(/\/+$/, "");
|
|
123
|
+
const parsedBaseUrl = new URL(normalizedBaseUrl);
|
|
124
|
+
if (parsedBaseUrl.protocol !== "https:" && !(allowInsecureHttp && parsedBaseUrl.protocol === "http:")) {
|
|
125
|
+
throw new Error("baseUrl must use HTTPS unless allowInsecureHttp=true");
|
|
126
|
+
}
|
|
127
|
+
const normalizedApiPrefix = assertSafeRelativePath(apiPrefix, "apiPrefix").replace(/\/+$/, "");
|
|
128
|
+
const normalizedTokenPath = assertSafeRelativePath(tokenPath, "tokenPath");
|
|
129
|
+
const baseOrigin = parsedBaseUrl.origin;
|
|
130
|
+
const baseProtocol = parsedBaseUrl.protocol;
|
|
131
|
+
const basePathname = parsedBaseUrl.pathname.replace(/\/+$/, "");
|
|
132
|
+
const deploymentBaseUrl = `${baseOrigin}${basePathname || ""}/`;
|
|
133
|
+
|
|
48
134
|
let cachedToken = "";
|
|
49
135
|
let cachedAt = 0;
|
|
50
136
|
|
|
51
137
|
async function fetchJson(url, { method, headers, body }) {
|
|
52
138
|
const { signal, cleanup } = createAbortSignal(timeoutMs);
|
|
53
139
|
try {
|
|
54
|
-
const resp = await fetch(url, { method, headers, body, signal });
|
|
140
|
+
const resp = await fetch(url, { method, headers, body, signal, redirect: "error" });
|
|
55
141
|
const text = await resp.text();
|
|
56
142
|
const contentType = resp.headers.get("content-type") || "";
|
|
57
143
|
const data = contentType.includes("application/json") ? safeJsonParse(text) : text;
|
|
58
144
|
if (!resp.ok) {
|
|
59
|
-
const err = new Error(`Request failed ${resp.status}
|
|
145
|
+
const err = new Error(`Request failed ${resp.status}`);
|
|
60
146
|
err.status = resp.status;
|
|
61
147
|
err.data = data;
|
|
148
|
+
err.responseText = truncate(String(text), MAX_ERROR_TEXT_LENGTH);
|
|
62
149
|
throw err;
|
|
63
150
|
}
|
|
64
|
-
return { status: resp.status,
|
|
151
|
+
return { status: resp.status, data };
|
|
65
152
|
} finally {
|
|
66
153
|
cleanup();
|
|
67
154
|
}
|
|
@@ -86,6 +173,12 @@ export function createZenTaoClient(config) {
|
|
|
86
173
|
return Date.now() - cachedAt > tokenTtlMs;
|
|
87
174
|
}
|
|
88
175
|
|
|
176
|
+
function resolveAgainstDeploymentPath(path) {
|
|
177
|
+
const normalizedPath = assertSafeRelativePath(path, "path");
|
|
178
|
+
const relativePath = normalizedPath.replace(/^\/+/, "");
|
|
179
|
+
return new URL(relativePath, deploymentBaseUrl);
|
|
180
|
+
}
|
|
181
|
+
|
|
89
182
|
async function getToken({ force } = {}) {
|
|
90
183
|
if (!force && !tokenExpired()) {
|
|
91
184
|
return { token: cachedToken, source: "cache" };
|
|
@@ -95,7 +188,7 @@ export function createZenTaoClient(config) {
|
|
|
95
188
|
}
|
|
96
189
|
|
|
97
190
|
// 兼容性:不同禅道版本 token 接口可能不同;先按常见 v1 约定尝试
|
|
98
|
-
const url =
|
|
191
|
+
const url = resolveAgainstDeploymentPath(normalizedTokenPath);
|
|
99
192
|
const payload = { account: auth.account, password: auth.password };
|
|
100
193
|
const resp = await fetchJson(url, {
|
|
101
194
|
method: "POST",
|
|
@@ -121,7 +214,7 @@ export function createZenTaoClient(config) {
|
|
|
121
214
|
|
|
122
215
|
async function call({ path, method = "GET", query, body } = {}) {
|
|
123
216
|
const tokenInfo = await getToken();
|
|
124
|
-
const url = buildUrl({ baseUrl, apiPrefix, path, query });
|
|
217
|
+
const url = buildUrl({ baseUrl: normalizedBaseUrl, apiPrefix: normalizedApiPrefix, path, query });
|
|
125
218
|
|
|
126
219
|
const headers = { Token: tokenInfo.token };
|
|
127
220
|
let payload;
|
|
@@ -174,6 +267,30 @@ export function createZenTaoClient(config) {
|
|
|
174
267
|
return [];
|
|
175
268
|
}
|
|
176
269
|
|
|
270
|
+
function parseProductsFromResponse(data) {
|
|
271
|
+
if (Array.isArray(data?.products)) return data.products;
|
|
272
|
+
if (Array.isArray(data?.data?.products)) return data.data.products;
|
|
273
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
274
|
+
if (Array.isArray(data)) return data;
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseProjectsFromResponse(data) {
|
|
279
|
+
if (Array.isArray(data?.projects)) return data.projects;
|
|
280
|
+
if (Array.isArray(data?.data?.projects)) return data.data.projects;
|
|
281
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
282
|
+
if (Array.isArray(data)) return data;
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseProgramsFromResponse(data) {
|
|
287
|
+
if (Array.isArray(data?.programs)) return data.programs;
|
|
288
|
+
if (Array.isArray(data?.data?.programs)) return data.data.programs;
|
|
289
|
+
if (Array.isArray(data?.data)) return data.data;
|
|
290
|
+
if (Array.isArray(data)) return data;
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
|
|
177
294
|
function parseBugDetailFromResponse(data) {
|
|
178
295
|
if (data?.bug && typeof data.bug === "object") return data.bug;
|
|
179
296
|
if (data?.data?.bug && typeof data.data.bug === "object") return data.data.bug;
|
|
@@ -208,7 +325,8 @@ export function createZenTaoClient(config) {
|
|
|
208
325
|
function buildMyBugsPath(pathTemplate) {
|
|
209
326
|
const template = String(pathTemplate || "").trim();
|
|
210
327
|
if (!template) return "";
|
|
211
|
-
|
|
328
|
+
const resolved = template.replaceAll("{account}", encodeURIComponent(auth.account || ""));
|
|
329
|
+
return assertSafeRelativePath(resolved, "myBugsPath");
|
|
212
330
|
}
|
|
213
331
|
|
|
214
332
|
function buildProjectSetPath(pathTemplate, projectSetId) {
|
|
@@ -216,7 +334,8 @@ export function createZenTaoClient(config) {
|
|
|
216
334
|
if (!template) return "";
|
|
217
335
|
const pid = normalizePositiveInt(projectSetId);
|
|
218
336
|
if (!pid) return "";
|
|
219
|
-
|
|
337
|
+
const resolved = template.replaceAll("{projectSetId}", String(pid));
|
|
338
|
+
return assertSafeRelativePath(resolved, "projectSetBugsPath");
|
|
220
339
|
}
|
|
221
340
|
|
|
222
341
|
function isMyBugsPath(path) {
|
|
@@ -239,6 +358,26 @@ export function createZenTaoClient(config) {
|
|
|
239
358
|
return merged.includes("need product id");
|
|
240
359
|
}
|
|
241
360
|
|
|
361
|
+
function normalizeBugsListPath(path) {
|
|
362
|
+
const normalizedPath = String(path || "/bugs").trim() || "/bugs";
|
|
363
|
+
if (!SAFE_BUG_LIST_PATHS.has(normalizedPath)) {
|
|
364
|
+
throw new Error("path must be one of /bugs, /my/bug, /my/bugs");
|
|
365
|
+
}
|
|
366
|
+
return normalizedPath;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildProgramProductsPath(projectSetId) {
|
|
370
|
+
const pid = normalizePositiveInt(projectSetId);
|
|
371
|
+
if (!pid) return "";
|
|
372
|
+
return `/programs/${pid}/products`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildProgramProjectsPath(projectSetId) {
|
|
376
|
+
const pid = normalizePositiveInt(projectSetId);
|
|
377
|
+
if (!pid) return "";
|
|
378
|
+
return `/programs/${pid}/projects`;
|
|
379
|
+
}
|
|
380
|
+
|
|
242
381
|
function buildBugsQueryForPath({ path, limit, page, assignedTo, status, productId }) {
|
|
243
382
|
if (isMyBugsPath(path) || isProjectSetPath(path)) {
|
|
244
383
|
// "我的bug"和"项目集bug"类端点在部分实例不接受 assignedTo/status/product 参数,使用最小分页参数后本地过滤。
|
|
@@ -253,21 +392,49 @@ export function createZenTaoClient(config) {
|
|
|
253
392
|
};
|
|
254
393
|
}
|
|
255
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Build resolution comment from solution, comment or resolution
|
|
397
|
+
* @param {Object} params
|
|
398
|
+
* @param {string} [params.solution] - Solution description (preferred)
|
|
399
|
+
* @param {string} [params.comment] - Comment text
|
|
400
|
+
* @param {string} [params.resolution] - Resolution type
|
|
401
|
+
* @returns {string} Formatted comment
|
|
402
|
+
*/
|
|
256
403
|
function buildResolutionComment({ solution, comment, resolution }) {
|
|
257
404
|
const normalizedSolution = String(solution || "").trim();
|
|
258
|
-
if (normalizedSolution) return
|
|
405
|
+
if (normalizedSolution) return `${DEFAULT_RESOLUTION_PREFIX}${normalizedSolution}`;
|
|
259
406
|
const normalizedComment = String(comment || "").trim();
|
|
260
407
|
if (normalizedComment) return normalizedComment;
|
|
261
|
-
return
|
|
408
|
+
return `${DEFAULT_RESOLUTION_FALLBACK},resolution=${String(resolution || "fixed")}`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function normalizeUserIdentity(value) {
|
|
412
|
+
if (!value) return "";
|
|
413
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
414
|
+
return String(value);
|
|
415
|
+
}
|
|
416
|
+
if (typeof value === "object") {
|
|
417
|
+
return (
|
|
418
|
+
value.account ||
|
|
419
|
+
value.username ||
|
|
420
|
+
value.userName ||
|
|
421
|
+
value.realname ||
|
|
422
|
+
value.realName ||
|
|
423
|
+
value.name ||
|
|
424
|
+
value.id ||
|
|
425
|
+
""
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return "";
|
|
262
429
|
}
|
|
263
430
|
|
|
264
431
|
function getBugAssignee(bug) {
|
|
265
|
-
return (
|
|
266
|
-
bug?.assignedTo
|
|
267
|
-
bug?.assignedto
|
|
268
|
-
bug?.assigned_to
|
|
269
|
-
bug?.assignedUser
|
|
270
|
-
bug?.owner
|
|
432
|
+
return normalizeUserIdentity(
|
|
433
|
+
bug?.assignedTo ??
|
|
434
|
+
bug?.assignedto ??
|
|
435
|
+
bug?.assigned_to ??
|
|
436
|
+
bug?.assignedUser ??
|
|
437
|
+
bug?.owner ??
|
|
271
438
|
""
|
|
272
439
|
);
|
|
273
440
|
}
|
|
@@ -305,40 +472,25 @@ export function createZenTaoClient(config) {
|
|
|
305
472
|
return true;
|
|
306
473
|
}
|
|
307
474
|
|
|
308
|
-
function buildBugDetailPath({ id
|
|
475
|
+
function buildBugDetailPath({ id }) {
|
|
309
476
|
const normalizedId = Number(id);
|
|
310
|
-
|
|
311
|
-
if (basePath.includes("{id}")) {
|
|
312
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
313
|
-
}
|
|
314
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
315
|
-
return `${trimmed}/${normalizedId}`;
|
|
477
|
+
return `/bugs/${normalizedId}`;
|
|
316
478
|
}
|
|
317
479
|
|
|
318
|
-
function buildBugResolvePath({ id
|
|
319
|
-
return buildBugTransitionPath({ id,
|
|
480
|
+
function buildBugResolvePath({ id }) {
|
|
481
|
+
return buildBugTransitionPath({ id, action: "resolve" });
|
|
320
482
|
}
|
|
321
483
|
|
|
322
|
-
function buildBugCommentPath({ id
|
|
484
|
+
function buildBugCommentPath({ id }) {
|
|
323
485
|
const normalizedId = Number(id);
|
|
324
|
-
|
|
325
|
-
if (basePath.includes("{id}")) {
|
|
326
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
327
|
-
}
|
|
328
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
329
|
-
return `${trimmed}/${normalizedId}/comment`;
|
|
486
|
+
return `/bugs/${normalizedId}/comment`;
|
|
330
487
|
}
|
|
331
488
|
|
|
332
|
-
function buildBugTransitionPath({ id,
|
|
489
|
+
function buildBugTransitionPath({ id, action }) {
|
|
333
490
|
const normalizedId = Number(id);
|
|
334
491
|
const safeAction = String(action || "").trim();
|
|
335
492
|
if (!safeAction) throw new Error("buildBugTransitionPath requires action");
|
|
336
|
-
|
|
337
|
-
if (basePath.includes("{id}")) {
|
|
338
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
339
|
-
}
|
|
340
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
341
|
-
return `${trimmed}/${normalizedId}/${safeAction}`;
|
|
493
|
+
return `/bugs/${normalizedId}/${safeAction}`;
|
|
342
494
|
}
|
|
343
495
|
|
|
344
496
|
function getBugId(bug) {
|
|
@@ -357,17 +509,153 @@ export function createZenTaoClient(config) {
|
|
|
357
509
|
if (!raw) return "";
|
|
358
510
|
if (raw.startsWith("data:")) return "";
|
|
359
511
|
const cleaned = raw.replaceAll("&", "&");
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
512
|
+
try {
|
|
513
|
+
const candidate = /^https?:\/\//i.test(cleaned)
|
|
514
|
+
? new URL(cleaned)
|
|
515
|
+
: cleaned.startsWith("//")
|
|
516
|
+
? new URL(`${baseProtocol}${cleaned}`)
|
|
517
|
+
: cleaned.startsWith("/")
|
|
518
|
+
? resolveAgainstDeploymentPath(cleaned)
|
|
519
|
+
: resolveAgainstDeploymentPath(`/${cleaned.replace(/^\.?\//, "")}`);
|
|
520
|
+
if (candidate.origin !== baseOrigin) return "";
|
|
521
|
+
return candidate.toString();
|
|
522
|
+
} catch {
|
|
523
|
+
return "";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function sanitizeTextContent(value) {
|
|
528
|
+
const text = String(value || "").trim();
|
|
529
|
+
if (!text) return "";
|
|
530
|
+
|
|
531
|
+
return text
|
|
532
|
+
.replace(/<img\b[^>]*>/gi, "")
|
|
533
|
+
.replace(/!\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/gi, "")
|
|
534
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (match) => normalizeResourceUrl(match) ? match : "")
|
|
535
|
+
.trim();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function hasMeaningfulValue(value) {
|
|
539
|
+
return value !== undefined && value !== null && value !== "";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function pickFirstValue(source, aliases) {
|
|
543
|
+
if (!source || typeof source !== "object") return undefined;
|
|
544
|
+
for (const alias of aliases) {
|
|
545
|
+
const value = source[alias];
|
|
546
|
+
if (hasMeaningfulValue(value)) return value;
|
|
547
|
+
}
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function assignCanonicalValue(target, fieldName, source, aliases, options = {}) {
|
|
552
|
+
const { sanitizeText = false, transform } = options;
|
|
553
|
+
const rawValue = pickFirstValue(source, aliases);
|
|
554
|
+
if (!hasMeaningfulValue(rawValue)) return;
|
|
555
|
+
|
|
556
|
+
let value = sanitizeText ? sanitizeTextContent(rawValue) : rawValue;
|
|
557
|
+
if (typeof transform === "function") {
|
|
558
|
+
value = transform(value);
|
|
364
559
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
560
|
+
if (hasMeaningfulValue(value)) {
|
|
561
|
+
target[fieldName] = value;
|
|
367
562
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function sanitizeFileRecord(record) {
|
|
566
|
+
if (!record || typeof record !== "object") return null;
|
|
567
|
+
|
|
568
|
+
const safe = {};
|
|
569
|
+
for (const key of [
|
|
570
|
+
"id",
|
|
571
|
+
"title",
|
|
572
|
+
"name",
|
|
573
|
+
"extension",
|
|
574
|
+
"ext",
|
|
575
|
+
"size",
|
|
576
|
+
"mime",
|
|
577
|
+
"contentType",
|
|
578
|
+
"addedBy",
|
|
579
|
+
"addedDate",
|
|
580
|
+
"createdBy",
|
|
581
|
+
"createdDate",
|
|
582
|
+
]) {
|
|
583
|
+
if (record[key] !== undefined && record[key] !== null && record[key] !== "") {
|
|
584
|
+
safe[key] = record[key];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const url = normalizeResourceUrl(
|
|
589
|
+
record.webPath ||
|
|
590
|
+
record.url ||
|
|
591
|
+
record.downloadUrl ||
|
|
592
|
+
record.downloadurl ||
|
|
593
|
+
record.path ||
|
|
594
|
+
record.pathname ||
|
|
595
|
+
record.viewUrl ||
|
|
596
|
+
record.href
|
|
597
|
+
);
|
|
598
|
+
if (url) safe.url = url;
|
|
599
|
+
|
|
600
|
+
return Object.keys(safe).length > 0 ? safe : null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function sanitizeFileCollection(files) {
|
|
604
|
+
if (!files) return [];
|
|
605
|
+
const list = Array.isArray(files) ? files : Object.values(files);
|
|
606
|
+
return list
|
|
607
|
+
.map((item) => sanitizeFileRecord(item))
|
|
608
|
+
.filter(Boolean);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function buildSafeBugDetail(bug) {
|
|
612
|
+
if (!bug || typeof bug !== "object") return null;
|
|
613
|
+
|
|
614
|
+
const safeBug = {};
|
|
615
|
+
const bugId = getBugId(bug);
|
|
616
|
+
if (bugId) safeBug.id = bugId;
|
|
617
|
+
|
|
618
|
+
const scalarFieldMappings = [
|
|
619
|
+
["title", ["title"]],
|
|
620
|
+
["status", ["status"]],
|
|
621
|
+
["severity", ["severity"]],
|
|
622
|
+
["pri", ["pri"]],
|
|
623
|
+
["type", ["type"]],
|
|
624
|
+
["openedBy", ["openedBy", "openedby", "opened_by"], { transform: normalizeUserIdentity }],
|
|
625
|
+
["openedDate", ["openedDate", "openeddate", "opened_date"]],
|
|
626
|
+
["assignedTo", ["assignedTo", "assignedto", "assigned_to", "assignedUser", "owner"], { transform: normalizeUserIdentity }],
|
|
627
|
+
["resolvedBy", ["resolvedBy", "resolvedby", "resolved_by"], { transform: normalizeUserIdentity }],
|
|
628
|
+
["resolvedDate", ["resolvedDate", "resolveddate", "resolved_date"]],
|
|
629
|
+
["closedBy", ["closedBy", "closedby", "closed_by"], { transform: normalizeUserIdentity }],
|
|
630
|
+
["closedDate", ["closedDate", "closeddate", "closed_date"]],
|
|
631
|
+
["product", ["product"]],
|
|
632
|
+
["project", ["project"]],
|
|
633
|
+
["projectSet", ["projectSet", "projectset", "project_set", "projectSetId", "projectsetid", "project_set_id"]],
|
|
634
|
+
["module", ["module"]],
|
|
635
|
+
["branch", ["branch"]],
|
|
636
|
+
["story", ["story"]],
|
|
637
|
+
["task", ["task"]],
|
|
638
|
+
["deadline", ["deadline"]],
|
|
639
|
+
["keywords", ["keywords"]],
|
|
640
|
+
];
|
|
641
|
+
for (const [fieldName, aliases, options] of scalarFieldMappings) {
|
|
642
|
+
assignCanonicalValue(safeBug, fieldName, bug, aliases, options);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
assignCanonicalValue(safeBug, "resolution", bug, ["resolution"], { sanitizeText: true });
|
|
646
|
+
assignCanonicalValue(safeBug, "steps", bug, ["steps", "stepsHtml", "reproStep", "reprostep"], { sanitizeText: true });
|
|
647
|
+
assignCanonicalValue(safeBug, "comment", bug, ["comment"], { sanitizeText: true });
|
|
648
|
+
|
|
649
|
+
const files = sanitizeFileCollection(bug.files);
|
|
650
|
+
if (files.length > 0) safeBug.files = files;
|
|
651
|
+
|
|
652
|
+
const attachments = sanitizeFileCollection(bug.attachments);
|
|
653
|
+
if (attachments.length > 0) safeBug.attachments = attachments;
|
|
654
|
+
|
|
655
|
+
const openedFiles = sanitizeFileCollection(bug.openedFiles);
|
|
656
|
+
if (openedFiles.length > 0) safeBug.openedFiles = openedFiles;
|
|
657
|
+
|
|
658
|
+
return safeBug;
|
|
371
659
|
}
|
|
372
660
|
|
|
373
661
|
function isImageLikeText(value) {
|
|
@@ -462,17 +750,18 @@ export function createZenTaoClient(config) {
|
|
|
462
750
|
productId,
|
|
463
751
|
projectSetId,
|
|
464
752
|
path = "/bugs",
|
|
465
|
-
assignedTo,
|
|
466
753
|
} = {}) {
|
|
467
754
|
const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
468
755
|
const safePage = Math.max(1, Number(page) || 1);
|
|
469
|
-
const
|
|
756
|
+
const dashboardScanLimit = Math.max(safeLimit, 100);
|
|
757
|
+
const assignee = normalizeString(auth.account);
|
|
470
758
|
const effectiveProductId = normalizePositiveInt(productId) || normalizePositiveInt(defaultProductId);
|
|
471
759
|
const effectiveProjectSetId = normalizePositiveInt(projectSetId) || normalizePositiveInt(defaultProjectSetId);
|
|
472
|
-
const
|
|
760
|
+
const requestedPath = normalizeBugsListPath(path);
|
|
761
|
+
const primaryPath = buildProductScopedBugsPath({ productId: effectiveProductId, path: requestedPath });
|
|
473
762
|
const preferProjectSetPath =
|
|
474
763
|
effectiveProjectSetId &&
|
|
475
|
-
|
|
764
|
+
requestedPath === "/bugs";
|
|
476
765
|
const configuredMyBugsPath = buildMyBugsPath(myBugsPath);
|
|
477
766
|
const fallbackPathCandidates = (bugsFallbackPaths && bugsFallbackPaths.length > 0)
|
|
478
767
|
? bugsFallbackPaths
|
|
@@ -515,9 +804,9 @@ export function createZenTaoClient(config) {
|
|
|
515
804
|
if (!candidatePaths.includes(fallback)) candidatePaths.push(fallback);
|
|
516
805
|
}
|
|
517
806
|
|
|
518
|
-
let bestResult = null;
|
|
519
807
|
let lastErr = null;
|
|
520
808
|
const triedPaths = [];
|
|
809
|
+
const successful = [];
|
|
521
810
|
for (const candidate of candidatePaths) {
|
|
522
811
|
try {
|
|
523
812
|
const query = buildBugsQueryForPath({ path: candidate, ...baseQuery });
|
|
@@ -532,27 +821,13 @@ export function createZenTaoClient(config) {
|
|
|
532
821
|
matched: filtered.length,
|
|
533
822
|
});
|
|
534
823
|
|
|
535
|
-
|
|
824
|
+
successful.push({
|
|
825
|
+
path: candidate,
|
|
826
|
+
status: resp?.status ?? null,
|
|
536
827
|
total: bugs.length,
|
|
537
828
|
matched: filtered.length,
|
|
538
|
-
|
|
539
|
-
|
|
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;
|
|
829
|
+
bugs,
|
|
830
|
+
});
|
|
556
831
|
} catch (err) {
|
|
557
832
|
lastErr = err;
|
|
558
833
|
triedPaths.push({
|
|
@@ -564,24 +839,296 @@ export function createZenTaoClient(config) {
|
|
|
564
839
|
if (candidate !== primaryPath) continue;
|
|
565
840
|
}
|
|
566
841
|
}
|
|
567
|
-
|
|
842
|
+
|
|
843
|
+
if (effectiveProjectSetId) {
|
|
844
|
+
const programProductsPath = buildProgramProductsPath(effectiveProjectSetId);
|
|
845
|
+
if (programProductsPath) {
|
|
846
|
+
try {
|
|
847
|
+
const productsResp = await call({
|
|
848
|
+
path: programProductsPath,
|
|
849
|
+
method: "GET",
|
|
850
|
+
query: { limit: safeLimit, page: safePage },
|
|
851
|
+
});
|
|
852
|
+
const products = parseProductsFromResponse(productsResp?.data);
|
|
853
|
+
const productIds = products
|
|
854
|
+
.map((product) => normalizePositiveInt(product?.id ?? product?.productId ?? product?.productID))
|
|
855
|
+
.filter(Boolean);
|
|
856
|
+
|
|
857
|
+
triedPaths.push({
|
|
858
|
+
path: programProductsPath,
|
|
859
|
+
status: productsResp?.status ?? null,
|
|
860
|
+
total: productIds.length,
|
|
861
|
+
matched: 0,
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
for (const scopedProductId of productIds) {
|
|
865
|
+
const productPath = `/products/${scopedProductId}/bugs`;
|
|
866
|
+
try {
|
|
867
|
+
const query = buildBugsQueryForPath({
|
|
868
|
+
path: productPath,
|
|
869
|
+
limit: safeLimit,
|
|
870
|
+
page: safePage,
|
|
871
|
+
assignedTo: assignee,
|
|
872
|
+
status,
|
|
873
|
+
productId: scopedProductId,
|
|
874
|
+
});
|
|
875
|
+
const resp = await call({ path: productPath, method: "GET", query });
|
|
876
|
+
const bugs = parseBugsFromResponse(resp?.data);
|
|
877
|
+
const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
878
|
+
|
|
879
|
+
triedPaths.push({
|
|
880
|
+
path: productPath,
|
|
881
|
+
status: resp?.status ?? null,
|
|
882
|
+
total: bugs.length,
|
|
883
|
+
matched: filtered.length,
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
successful.push({
|
|
887
|
+
path: productPath,
|
|
888
|
+
status: resp?.status ?? null,
|
|
889
|
+
total: bugs.length,
|
|
890
|
+
matched: filtered.length,
|
|
891
|
+
bugs,
|
|
892
|
+
});
|
|
893
|
+
} catch (err) {
|
|
894
|
+
lastErr = err;
|
|
895
|
+
triedPaths.push({
|
|
896
|
+
path: productPath,
|
|
897
|
+
status: err?.status ?? null,
|
|
898
|
+
error: String(err?.message || err),
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch (err) {
|
|
903
|
+
lastErr = err;
|
|
904
|
+
triedPaths.push({
|
|
905
|
+
path: programProductsPath,
|
|
906
|
+
status: err?.status ?? null,
|
|
907
|
+
error: String(err?.message || err),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const programProjectsPath = buildProgramProjectsPath(effectiveProjectSetId);
|
|
913
|
+
if (programProjectsPath) {
|
|
914
|
+
try {
|
|
915
|
+
const projectsResp = await call({
|
|
916
|
+
path: programProjectsPath,
|
|
917
|
+
method: "GET",
|
|
918
|
+
query: { limit: safeLimit, page: safePage },
|
|
919
|
+
});
|
|
920
|
+
const projects = parseProjectsFromResponse(projectsResp?.data);
|
|
921
|
+
const projectIds = projects
|
|
922
|
+
.map((project) => normalizePositiveInt(project?.id ?? project?.projectId ?? project?.projectID))
|
|
923
|
+
.filter(Boolean);
|
|
924
|
+
|
|
925
|
+
triedPaths.push({
|
|
926
|
+
path: programProjectsPath,
|
|
927
|
+
status: projectsResp?.status ?? null,
|
|
928
|
+
total: projectIds.length,
|
|
929
|
+
matched: 0,
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
for (const scopedProjectId of projectIds) {
|
|
933
|
+
const projectPath = `/projects/${scopedProjectId}/bugs`;
|
|
934
|
+
try {
|
|
935
|
+
const resp = await call({
|
|
936
|
+
path: projectPath,
|
|
937
|
+
method: "GET",
|
|
938
|
+
query: { limit: safeLimit, page: safePage, assignedTo: assignee },
|
|
939
|
+
});
|
|
940
|
+
const bugs = parseBugsFromResponse(resp?.data);
|
|
941
|
+
const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
942
|
+
|
|
943
|
+
triedPaths.push({
|
|
944
|
+
path: projectPath,
|
|
945
|
+
status: resp?.status ?? null,
|
|
946
|
+
total: bugs.length,
|
|
947
|
+
matched: filtered.length,
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
successful.push({
|
|
951
|
+
path: projectPath,
|
|
952
|
+
status: resp?.status ?? null,
|
|
953
|
+
total: bugs.length,
|
|
954
|
+
matched: filtered.length,
|
|
955
|
+
bugs,
|
|
956
|
+
});
|
|
957
|
+
} catch (err) {
|
|
958
|
+
lastErr = err;
|
|
959
|
+
triedPaths.push({
|
|
960
|
+
path: projectPath,
|
|
961
|
+
status: err?.status ?? null,
|
|
962
|
+
error: String(err?.message || err),
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
lastErr = err;
|
|
968
|
+
triedPaths.push({
|
|
969
|
+
path: programProjectsPath,
|
|
970
|
+
status: err?.status ?? null,
|
|
971
|
+
error: String(err?.message || err),
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (!effectiveProjectSetId && !effectiveProductId && successful.length === 0) {
|
|
978
|
+
try {
|
|
979
|
+
const programsResp = await call({
|
|
980
|
+
path: "/programs",
|
|
981
|
+
method: "GET",
|
|
982
|
+
query: { limit: 100, page: 1 },
|
|
983
|
+
});
|
|
984
|
+
const programs = parseProgramsFromResponse(programsResp?.data);
|
|
985
|
+
const programIds = programs
|
|
986
|
+
.map((program) => normalizePositiveInt(program?.id ?? program?.projectSetId ?? program?.programId))
|
|
987
|
+
.filter(Boolean);
|
|
988
|
+
const scannedProjectIds = new Set();
|
|
989
|
+
|
|
990
|
+
triedPaths.push({
|
|
991
|
+
path: "/programs",
|
|
992
|
+
status: programsResp?.status ?? null,
|
|
993
|
+
total: programIds.length,
|
|
994
|
+
matched: 0,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
for (const scopedProgramId of programIds) {
|
|
998
|
+
const programProjectsPath = `/programs/${scopedProgramId}/projects`;
|
|
999
|
+
try {
|
|
1000
|
+
const projectsResp = await call({
|
|
1001
|
+
path: programProjectsPath,
|
|
1002
|
+
method: "GET",
|
|
1003
|
+
query: { limit: 100, page: 1 },
|
|
1004
|
+
});
|
|
1005
|
+
const projects = parseProjectsFromResponse(projectsResp?.data);
|
|
1006
|
+
const projectIds = projects
|
|
1007
|
+
.map((project) => normalizePositiveInt(project?.id ?? project?.projectId ?? project?.projectID))
|
|
1008
|
+
.filter(Boolean);
|
|
1009
|
+
|
|
1010
|
+
triedPaths.push({
|
|
1011
|
+
path: programProjectsPath,
|
|
1012
|
+
status: projectsResp?.status ?? null,
|
|
1013
|
+
total: projectIds.length,
|
|
1014
|
+
matched: 0,
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
for (const scopedProjectId of projectIds) {
|
|
1018
|
+
if (scannedProjectIds.has(scopedProjectId)) continue;
|
|
1019
|
+
scannedProjectIds.add(scopedProjectId);
|
|
1020
|
+
const projectPath = `/projects/${scopedProjectId}/bugs`;
|
|
1021
|
+
try {
|
|
1022
|
+
const resp = await call({
|
|
1023
|
+
path: projectPath,
|
|
1024
|
+
method: "GET",
|
|
1025
|
+
query: {
|
|
1026
|
+
limit: dashboardScanLimit,
|
|
1027
|
+
page: safePage,
|
|
1028
|
+
assignedTo: assignee,
|
|
1029
|
+
status: status || undefined,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
const bugs = parseBugsFromResponse(resp?.data);
|
|
1033
|
+
const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
1034
|
+
|
|
1035
|
+
triedPaths.push({
|
|
1036
|
+
path: projectPath,
|
|
1037
|
+
status: resp?.status ?? null,
|
|
1038
|
+
total: bugs.length,
|
|
1039
|
+
matched: filtered.length,
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
successful.push({
|
|
1043
|
+
path: projectPath,
|
|
1044
|
+
status: resp?.status ?? null,
|
|
1045
|
+
total: bugs.length,
|
|
1046
|
+
matched: filtered.length,
|
|
1047
|
+
bugs,
|
|
1048
|
+
});
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
lastErr = err;
|
|
1051
|
+
triedPaths.push({
|
|
1052
|
+
path: projectPath,
|
|
1053
|
+
status: err?.status ?? null,
|
|
1054
|
+
error: String(err?.message || err),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
lastErr = err;
|
|
1060
|
+
triedPaths.push({
|
|
1061
|
+
path: programProjectsPath,
|
|
1062
|
+
status: err?.status ?? null,
|
|
1063
|
+
error: String(err?.message || err),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
lastErr = err;
|
|
1069
|
+
triedPaths.push({
|
|
1070
|
+
path: "/programs",
|
|
1071
|
+
status: err?.status ?? null,
|
|
1072
|
+
error: String(err?.message || err),
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (successful.length === 0) throw lastErr;
|
|
1078
|
+
|
|
1079
|
+
const mergedBugs = [];
|
|
1080
|
+
const seenBugIds = new Set();
|
|
1081
|
+
for (const result of successful) {
|
|
1082
|
+
for (const bug of result.bugs || []) {
|
|
1083
|
+
const bugId = getBugId(bug);
|
|
1084
|
+
if (bugId) {
|
|
1085
|
+
if (seenBugIds.has(bugId)) continue;
|
|
1086
|
+
seenBugIds.add(bugId);
|
|
1087
|
+
}
|
|
1088
|
+
mergedBugs.push(bug);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const mergedFiltered = mergedBugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
1093
|
+
const best = successful.reduce((acc, cur) => {
|
|
1094
|
+
if (!acc) return cur;
|
|
1095
|
+
if (cur.matched > acc.matched) return cur;
|
|
1096
|
+
if (cur.matched === acc.matched && cur.total > acc.total) return cur;
|
|
1097
|
+
return acc;
|
|
1098
|
+
}, null);
|
|
568
1099
|
|
|
569
1100
|
return {
|
|
570
|
-
|
|
1101
|
+
total: mergedFiltered.length,
|
|
1102
|
+
matched: mergedFiltered.length,
|
|
1103
|
+
page: safePage,
|
|
1104
|
+
limit: safeLimit,
|
|
1105
|
+
productId: effectiveProductId,
|
|
1106
|
+
projectSetId: effectiveProjectSetId,
|
|
1107
|
+
assignedTo: assignee,
|
|
1108
|
+
bugs: mergedFiltered,
|
|
571
1109
|
raw: {
|
|
572
|
-
|
|
1110
|
+
status: best?.status ?? null,
|
|
1111
|
+
path: best?.path ?? primaryPath,
|
|
573
1112
|
triedPaths,
|
|
1113
|
+
scannedTotal: mergedBugs.length,
|
|
1114
|
+
paths: successful.map((r) => ({
|
|
1115
|
+
path: r.path,
|
|
1116
|
+
status: r.status,
|
|
1117
|
+
total: r.total,
|
|
1118
|
+
matched: r.matched,
|
|
1119
|
+
})),
|
|
1120
|
+
merged: successful.length > 1,
|
|
574
1121
|
},
|
|
575
1122
|
};
|
|
576
1123
|
}
|
|
577
1124
|
|
|
578
|
-
async function getBugDetail({ id
|
|
1125
|
+
async function getBugDetail({ id } = {}) {
|
|
579
1126
|
const bugId = Number(id);
|
|
580
1127
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
581
1128
|
throw new Error("getBugDetail requires a valid bug id");
|
|
582
1129
|
}
|
|
583
1130
|
|
|
584
|
-
const detailPath = buildBugDetailPath({ id: bugId
|
|
1131
|
+
const detailPath = buildBugDetailPath({ id: bugId });
|
|
585
1132
|
const resp = await call({ path: detailPath, method: "GET" });
|
|
586
1133
|
const bug = parseBugDetailFromResponse(resp.data);
|
|
587
1134
|
if (!bug) {
|
|
@@ -589,14 +1136,14 @@ export function createZenTaoClient(config) {
|
|
|
589
1136
|
id: bugId,
|
|
590
1137
|
found: false,
|
|
591
1138
|
images: [],
|
|
592
|
-
raw: { status: resp.status
|
|
1139
|
+
raw: { status: resp.status },
|
|
593
1140
|
};
|
|
594
1141
|
}
|
|
595
1142
|
|
|
596
1143
|
return {
|
|
597
1144
|
id: bugId,
|
|
598
1145
|
found: true,
|
|
599
|
-
bug,
|
|
1146
|
+
bug: buildSafeBugDetail(bug),
|
|
600
1147
|
images: extractImageUrlsFromBug(bug),
|
|
601
1148
|
raw: { status: resp.status },
|
|
602
1149
|
};
|
|
@@ -607,14 +1154,13 @@ export function createZenTaoClient(config) {
|
|
|
607
1154
|
resolution = "fixed",
|
|
608
1155
|
solution = "",
|
|
609
1156
|
comment = "",
|
|
610
|
-
path = "/bugs/{id}/resolve",
|
|
611
1157
|
} = {}) {
|
|
612
1158
|
const bugId = Number(id);
|
|
613
1159
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
614
1160
|
throw new Error("resolveBug requires a valid bug id");
|
|
615
1161
|
}
|
|
616
1162
|
|
|
617
|
-
const resolvePath = buildBugResolvePath({ id: bugId
|
|
1163
|
+
const resolvePath = buildBugResolvePath({ id: bugId });
|
|
618
1164
|
const resolvedValue = String(resolution || "fixed");
|
|
619
1165
|
const resolvedComment = buildResolutionComment({ solution, comment, resolution: resolvedValue });
|
|
620
1166
|
const body = {
|
|
@@ -629,17 +1175,17 @@ export function createZenTaoClient(config) {
|
|
|
629
1175
|
resolution: resolvedValue,
|
|
630
1176
|
solution: String(solution || "").trim(),
|
|
631
1177
|
comment: resolvedComment,
|
|
632
|
-
raw: { status: resp.status
|
|
1178
|
+
raw: { status: resp.status },
|
|
633
1179
|
};
|
|
634
1180
|
}
|
|
635
1181
|
|
|
636
|
-
async function closeBug({ id, comment = ""
|
|
1182
|
+
async function closeBug({ id, comment = "" } = {}) {
|
|
637
1183
|
const bugId = Number(id);
|
|
638
1184
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
639
1185
|
throw new Error("closeBug requires a valid bug id");
|
|
640
1186
|
}
|
|
641
1187
|
|
|
642
|
-
const closePath = buildBugTransitionPath({ id: bugId,
|
|
1188
|
+
const closePath = buildBugTransitionPath({ id: bugId, action: "close" });
|
|
643
1189
|
const body = {};
|
|
644
1190
|
if (comment) body.comment = String(comment);
|
|
645
1191
|
|
|
@@ -647,17 +1193,17 @@ export function createZenTaoClient(config) {
|
|
|
647
1193
|
return {
|
|
648
1194
|
id: bugId,
|
|
649
1195
|
closed: true,
|
|
650
|
-
raw: { status: resp.status
|
|
1196
|
+
raw: { status: resp.status },
|
|
651
1197
|
};
|
|
652
1198
|
}
|
|
653
1199
|
|
|
654
|
-
async function activateBug({ id, comment = ""
|
|
1200
|
+
async function activateBug({ id, comment = "" } = {}) {
|
|
655
1201
|
const bugId = Number(id);
|
|
656
1202
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
657
1203
|
throw new Error("activateBug requires a valid bug id");
|
|
658
1204
|
}
|
|
659
1205
|
|
|
660
|
-
const activatePath = buildBugTransitionPath({ id: bugId,
|
|
1206
|
+
const activatePath = buildBugTransitionPath({ id: bugId, action: "activate" });
|
|
661
1207
|
const body = {};
|
|
662
1208
|
if (comment) body.comment = String(comment);
|
|
663
1209
|
|
|
@@ -665,7 +1211,7 @@ export function createZenTaoClient(config) {
|
|
|
665
1211
|
return {
|
|
666
1212
|
id: bugId,
|
|
667
1213
|
activated: true,
|
|
668
|
-
raw: { status: resp.status
|
|
1214
|
+
raw: { status: resp.status },
|
|
669
1215
|
};
|
|
670
1216
|
}
|
|
671
1217
|
|
|
@@ -673,8 +1219,6 @@ export function createZenTaoClient(config) {
|
|
|
673
1219
|
id,
|
|
674
1220
|
result = "pass",
|
|
675
1221
|
comment = "",
|
|
676
|
-
closePath = "/bugs/{id}/close",
|
|
677
|
-
activatePath = "/bugs/{id}/activate",
|
|
678
1222
|
} = {}) {
|
|
679
1223
|
const normalizedResult = normalizeString(result || "pass");
|
|
680
1224
|
if (normalizedResult !== "pass" && normalizedResult !== "fail") {
|
|
@@ -682,7 +1226,7 @@ export function createZenTaoClient(config) {
|
|
|
682
1226
|
}
|
|
683
1227
|
|
|
684
1228
|
if (normalizedResult === "pass") {
|
|
685
|
-
const closeResult = await closeBug({ id, comment
|
|
1229
|
+
const closeResult = await closeBug({ id, comment });
|
|
686
1230
|
return {
|
|
687
1231
|
id: Number(id),
|
|
688
1232
|
verified: true,
|
|
@@ -692,7 +1236,7 @@ export function createZenTaoClient(config) {
|
|
|
692
1236
|
};
|
|
693
1237
|
}
|
|
694
1238
|
|
|
695
|
-
const activateResult = await activateBug({ id, comment
|
|
1239
|
+
const activateResult = await activateBug({ id, comment });
|
|
696
1240
|
return {
|
|
697
1241
|
id: Number(id),
|
|
698
1242
|
verified: true,
|
|
@@ -702,7 +1246,7 @@ export function createZenTaoClient(config) {
|
|
|
702
1246
|
};
|
|
703
1247
|
}
|
|
704
1248
|
|
|
705
|
-
async function commentBug({ id, comment
|
|
1249
|
+
async function commentBug({ id, comment } = {}) {
|
|
706
1250
|
const bugId = Number(id);
|
|
707
1251
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
708
1252
|
throw new Error("commentBug requires a valid bug id");
|
|
@@ -712,7 +1256,7 @@ export function createZenTaoClient(config) {
|
|
|
712
1256
|
throw new Error("commentBug requires non-empty comment");
|
|
713
1257
|
}
|
|
714
1258
|
|
|
715
|
-
const primaryPath = buildBugCommentPath({ id: bugId
|
|
1259
|
+
const primaryPath = buildBugCommentPath({ id: bugId });
|
|
716
1260
|
const body = { comment: text };
|
|
717
1261
|
try {
|
|
718
1262
|
const resp = await call({ path: primaryPath, method: "POST", body });
|
|
@@ -720,7 +1264,7 @@ export function createZenTaoClient(config) {
|
|
|
720
1264
|
id: bugId,
|
|
721
1265
|
commented: true,
|
|
722
1266
|
comment: text,
|
|
723
|
-
raw: { status: resp.status, path: primaryPath
|
|
1267
|
+
raw: { status: resp.status, path: primaryPath },
|
|
724
1268
|
};
|
|
725
1269
|
} catch (err) {
|
|
726
1270
|
const fallbackPath = primaryPath.replace(/\/comment$/, "/comments");
|
|
@@ -730,7 +1274,7 @@ export function createZenTaoClient(config) {
|
|
|
730
1274
|
id: bugId,
|
|
731
1275
|
commented: true,
|
|
732
1276
|
comment: text,
|
|
733
|
-
raw: { status: resp.status, path: fallbackPath
|
|
1277
|
+
raw: { status: resp.status, path: fallbackPath },
|
|
734
1278
|
};
|
|
735
1279
|
}
|
|
736
1280
|
throw err;
|
|
@@ -744,16 +1288,14 @@ export function createZenTaoClient(config) {
|
|
|
744
1288
|
page = 1,
|
|
745
1289
|
productId,
|
|
746
1290
|
projectSetId,
|
|
747
|
-
maxItems =
|
|
748
|
-
assignedTo = "",
|
|
1291
|
+
maxItems = 20,
|
|
749
1292
|
resolution = "fixed",
|
|
750
1293
|
solution = "",
|
|
751
1294
|
comment = "",
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
stopOnError = false,
|
|
1295
|
+
path = "/bugs",
|
|
1296
|
+
stopOnError = true,
|
|
755
1297
|
} = {}) {
|
|
756
|
-
const safeMaxItems = Math.max(1, Math.min(Number(maxItems) ||
|
|
1298
|
+
const safeMaxItems = Math.max(1, Math.min(Number(maxItems) || 20, MAX_BATCH_RESOLVE_ITEMS));
|
|
757
1299
|
const listResult = await getMyBugs({
|
|
758
1300
|
status,
|
|
759
1301
|
keyword,
|
|
@@ -761,8 +1303,7 @@ export function createZenTaoClient(config) {
|
|
|
761
1303
|
page,
|
|
762
1304
|
productId,
|
|
763
1305
|
projectSetId,
|
|
764
|
-
path
|
|
765
|
-
assignedTo,
|
|
1306
|
+
path,
|
|
766
1307
|
});
|
|
767
1308
|
|
|
768
1309
|
const candidates = (listResult.bugs || []).slice(0, safeMaxItems);
|
|
@@ -782,7 +1323,6 @@ export function createZenTaoClient(config) {
|
|
|
782
1323
|
resolution,
|
|
783
1324
|
solution,
|
|
784
1325
|
comment,
|
|
785
|
-
path: resolvePath,
|
|
786
1326
|
});
|
|
787
1327
|
success.push({ id: bugId, status: result.raw.status });
|
|
788
1328
|
} catch (err) {
|