@aipper/zentao-mcp-server 0.1.10 → 0.1.12
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 +3 -3
- package/README.md +29 -17
- package/package.json +11 -2
- package/scripts/smoke.mjs +2 -1
- package/src/index.js +36 -52
- package/src/tools.js +82 -64
- package/src/zentao.js +549 -86
- package/IMPROVEMENTS.md +0 -179
- package/scripts/release-npm.sh +0 -481
package/src/zentao.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
const DEFAULT_RESOLUTION_PREFIX = "解决说明:";
|
|
3
3
|
const DEFAULT_RESOLUTION_FALLBACK = "已处理";
|
|
4
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"]);
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Sleep for a specified duration
|
|
@@ -21,6 +23,30 @@ function isProbablyAbsoluteUrl(value) {
|
|
|
21
23
|
return /^https?:\/\//i.test(value);
|
|
22
24
|
}
|
|
23
25
|
|
|
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`);
|
|
39
|
+
}
|
|
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
|
+
|
|
24
50
|
/**
|
|
25
51
|
* Build a full URL from base, prefix, path and query parameters
|
|
26
52
|
* @param {Object} params
|
|
@@ -31,13 +57,13 @@ function isProbablyAbsoluteUrl(value) {
|
|
|
31
57
|
* @returns {URL}
|
|
32
58
|
*/
|
|
33
59
|
function buildUrl({ baseUrl, apiPrefix, path, query }) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
throw new Error("path must be relative (absolute URL is not allowed)");
|
|
37
|
-
}
|
|
38
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
39
|
-
const prefix = apiPrefix.startsWith("/") ? apiPrefix : `/${apiPrefix}`;
|
|
60
|
+
const normalizedPath = assertSafeRelativePath(path, "path");
|
|
61
|
+
const prefix = assertSafeRelativePath(apiPrefix, "apiPrefix").replace(/\/+$/, "");
|
|
40
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
|
+
}
|
|
41
67
|
|
|
42
68
|
if (query && typeof query === "object") {
|
|
43
69
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -84,6 +110,7 @@ export function createZenTaoClient(config) {
|
|
|
84
110
|
tokenPath,
|
|
85
111
|
tokenTtlMs,
|
|
86
112
|
timeoutMs,
|
|
113
|
+
allowInsecureHttp,
|
|
87
114
|
defaultProductId,
|
|
88
115
|
defaultProjectSetId,
|
|
89
116
|
myBugsPath,
|
|
@@ -92,23 +119,36 @@ export function createZenTaoClient(config) {
|
|
|
92
119
|
auth,
|
|
93
120
|
} = config;
|
|
94
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
|
+
|
|
95
134
|
let cachedToken = "";
|
|
96
135
|
let cachedAt = 0;
|
|
97
136
|
|
|
98
137
|
async function fetchJson(url, { method, headers, body }) {
|
|
99
138
|
const { signal, cleanup } = createAbortSignal(timeoutMs);
|
|
100
139
|
try {
|
|
101
|
-
const resp = await fetch(url, { method, headers, body, signal });
|
|
140
|
+
const resp = await fetch(url, { method, headers, body, signal, redirect: "error" });
|
|
102
141
|
const text = await resp.text();
|
|
103
142
|
const contentType = resp.headers.get("content-type") || "";
|
|
104
143
|
const data = contentType.includes("application/json") ? safeJsonParse(text) : text;
|
|
105
144
|
if (!resp.ok) {
|
|
106
|
-
const err = new Error(`Request failed ${resp.status}
|
|
145
|
+
const err = new Error(`Request failed ${resp.status}`);
|
|
107
146
|
err.status = resp.status;
|
|
108
147
|
err.data = data;
|
|
148
|
+
err.responseText = truncate(String(text), MAX_ERROR_TEXT_LENGTH);
|
|
109
149
|
throw err;
|
|
110
150
|
}
|
|
111
|
-
return { status: resp.status,
|
|
151
|
+
return { status: resp.status, data };
|
|
112
152
|
} finally {
|
|
113
153
|
cleanup();
|
|
114
154
|
}
|
|
@@ -133,6 +173,12 @@ export function createZenTaoClient(config) {
|
|
|
133
173
|
return Date.now() - cachedAt > tokenTtlMs;
|
|
134
174
|
}
|
|
135
175
|
|
|
176
|
+
function resolveAgainstDeploymentPath(path) {
|
|
177
|
+
const normalizedPath = assertSafeRelativePath(path, "path");
|
|
178
|
+
const relativePath = normalizedPath.replace(/^\/+/, "");
|
|
179
|
+
return new URL(relativePath, deploymentBaseUrl);
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
async function getToken({ force } = {}) {
|
|
137
183
|
if (!force && !tokenExpired()) {
|
|
138
184
|
return { token: cachedToken, source: "cache" };
|
|
@@ -142,7 +188,7 @@ export function createZenTaoClient(config) {
|
|
|
142
188
|
}
|
|
143
189
|
|
|
144
190
|
// 兼容性:不同禅道版本 token 接口可能不同;先按常见 v1 约定尝试
|
|
145
|
-
const url =
|
|
191
|
+
const url = resolveAgainstDeploymentPath(normalizedTokenPath);
|
|
146
192
|
const payload = { account: auth.account, password: auth.password };
|
|
147
193
|
const resp = await fetchJson(url, {
|
|
148
194
|
method: "POST",
|
|
@@ -168,7 +214,7 @@ export function createZenTaoClient(config) {
|
|
|
168
214
|
|
|
169
215
|
async function call({ path, method = "GET", query, body } = {}) {
|
|
170
216
|
const tokenInfo = await getToken();
|
|
171
|
-
const url = buildUrl({ baseUrl, apiPrefix, path, query });
|
|
217
|
+
const url = buildUrl({ baseUrl: normalizedBaseUrl, apiPrefix: normalizedApiPrefix, path, query });
|
|
172
218
|
|
|
173
219
|
const headers = { Token: tokenInfo.token };
|
|
174
220
|
let payload;
|
|
@@ -221,6 +267,30 @@ export function createZenTaoClient(config) {
|
|
|
221
267
|
return [];
|
|
222
268
|
}
|
|
223
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
|
+
|
|
224
294
|
function parseBugDetailFromResponse(data) {
|
|
225
295
|
if (data?.bug && typeof data.bug === "object") return data.bug;
|
|
226
296
|
if (data?.data?.bug && typeof data.data.bug === "object") return data.data.bug;
|
|
@@ -255,7 +325,8 @@ export function createZenTaoClient(config) {
|
|
|
255
325
|
function buildMyBugsPath(pathTemplate) {
|
|
256
326
|
const template = String(pathTemplate || "").trim();
|
|
257
327
|
if (!template) return "";
|
|
258
|
-
|
|
328
|
+
const resolved = template.replaceAll("{account}", encodeURIComponent(auth.account || ""));
|
|
329
|
+
return assertSafeRelativePath(resolved, "myBugsPath");
|
|
259
330
|
}
|
|
260
331
|
|
|
261
332
|
function buildProjectSetPath(pathTemplate, projectSetId) {
|
|
@@ -263,7 +334,8 @@ export function createZenTaoClient(config) {
|
|
|
263
334
|
if (!template) return "";
|
|
264
335
|
const pid = normalizePositiveInt(projectSetId);
|
|
265
336
|
if (!pid) return "";
|
|
266
|
-
|
|
337
|
+
const resolved = template.replaceAll("{projectSetId}", String(pid));
|
|
338
|
+
return assertSafeRelativePath(resolved, "projectSetBugsPath");
|
|
267
339
|
}
|
|
268
340
|
|
|
269
341
|
function isMyBugsPath(path) {
|
|
@@ -286,6 +358,26 @@ export function createZenTaoClient(config) {
|
|
|
286
358
|
return merged.includes("need product id");
|
|
287
359
|
}
|
|
288
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
|
+
|
|
289
381
|
function buildBugsQueryForPath({ path, limit, page, assignedTo, status, productId }) {
|
|
290
382
|
if (isMyBugsPath(path) || isProjectSetPath(path)) {
|
|
291
383
|
// "我的bug"和"项目集bug"类端点在部分实例不接受 assignedTo/status/product 参数,使用最小分页参数后本地过滤。
|
|
@@ -316,13 +408,33 @@ export function createZenTaoClient(config) {
|
|
|
316
408
|
return `${DEFAULT_RESOLUTION_FALLBACK},resolution=${String(resolution || "fixed")}`;
|
|
317
409
|
}
|
|
318
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 "";
|
|
429
|
+
}
|
|
430
|
+
|
|
319
431
|
function getBugAssignee(bug) {
|
|
320
|
-
return (
|
|
321
|
-
bug?.assignedTo
|
|
322
|
-
bug?.assignedto
|
|
323
|
-
bug?.assigned_to
|
|
324
|
-
bug?.assignedUser
|
|
325
|
-
bug?.owner
|
|
432
|
+
return normalizeUserIdentity(
|
|
433
|
+
bug?.assignedTo ??
|
|
434
|
+
bug?.assignedto ??
|
|
435
|
+
bug?.assigned_to ??
|
|
436
|
+
bug?.assignedUser ??
|
|
437
|
+
bug?.owner ??
|
|
326
438
|
""
|
|
327
439
|
);
|
|
328
440
|
}
|
|
@@ -360,40 +472,25 @@ export function createZenTaoClient(config) {
|
|
|
360
472
|
return true;
|
|
361
473
|
}
|
|
362
474
|
|
|
363
|
-
function buildBugDetailPath({ id
|
|
475
|
+
function buildBugDetailPath({ id }) {
|
|
364
476
|
const normalizedId = Number(id);
|
|
365
|
-
|
|
366
|
-
if (basePath.includes("{id}")) {
|
|
367
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
368
|
-
}
|
|
369
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
370
|
-
return `${trimmed}/${normalizedId}`;
|
|
477
|
+
return `/bugs/${normalizedId}`;
|
|
371
478
|
}
|
|
372
479
|
|
|
373
|
-
function buildBugResolvePath({ id
|
|
374
|
-
return buildBugTransitionPath({ id,
|
|
480
|
+
function buildBugResolvePath({ id }) {
|
|
481
|
+
return buildBugTransitionPath({ id, action: "resolve" });
|
|
375
482
|
}
|
|
376
483
|
|
|
377
|
-
function buildBugCommentPath({ id
|
|
484
|
+
function buildBugCommentPath({ id }) {
|
|
378
485
|
const normalizedId = Number(id);
|
|
379
|
-
|
|
380
|
-
if (basePath.includes("{id}")) {
|
|
381
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
382
|
-
}
|
|
383
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
384
|
-
return `${trimmed}/${normalizedId}/comment`;
|
|
486
|
+
return `/bugs/${normalizedId}/comment`;
|
|
385
487
|
}
|
|
386
488
|
|
|
387
|
-
function buildBugTransitionPath({ id,
|
|
489
|
+
function buildBugTransitionPath({ id, action }) {
|
|
388
490
|
const normalizedId = Number(id);
|
|
389
491
|
const safeAction = String(action || "").trim();
|
|
390
492
|
if (!safeAction) throw new Error("buildBugTransitionPath requires action");
|
|
391
|
-
|
|
392
|
-
if (basePath.includes("{id}")) {
|
|
393
|
-
return basePath.replaceAll("{id}", String(normalizedId));
|
|
394
|
-
}
|
|
395
|
-
const trimmed = basePath.replace(/\/+$/, "");
|
|
396
|
-
return `${trimmed}/${normalizedId}/${safeAction}`;
|
|
493
|
+
return `/bugs/${normalizedId}/${safeAction}`;
|
|
397
494
|
}
|
|
398
495
|
|
|
399
496
|
function getBugId(bug) {
|
|
@@ -412,17 +509,153 @@ export function createZenTaoClient(config) {
|
|
|
412
509
|
if (!raw) return "";
|
|
413
510
|
if (raw.startsWith("data:")) return "";
|
|
414
511
|
const cleaned = raw.replaceAll("&", "&");
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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);
|
|
559
|
+
}
|
|
560
|
+
if (hasMeaningfulValue(value)) {
|
|
561
|
+
target[fieldName] = value;
|
|
419
562
|
}
|
|
420
|
-
|
|
421
|
-
|
|
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);
|
|
422
643
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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;
|
|
426
659
|
}
|
|
427
660
|
|
|
428
661
|
function isImageLikeText(value) {
|
|
@@ -517,17 +750,18 @@ export function createZenTaoClient(config) {
|
|
|
517
750
|
productId,
|
|
518
751
|
projectSetId,
|
|
519
752
|
path = "/bugs",
|
|
520
|
-
assignedTo,
|
|
521
753
|
} = {}) {
|
|
522
754
|
const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
523
755
|
const safePage = Math.max(1, Number(page) || 1);
|
|
524
|
-
const
|
|
756
|
+
const dashboardScanLimit = Math.max(safeLimit, 100);
|
|
757
|
+
const assignee = normalizeString(auth.account);
|
|
525
758
|
const effectiveProductId = normalizePositiveInt(productId) || normalizePositiveInt(defaultProductId);
|
|
526
759
|
const effectiveProjectSetId = normalizePositiveInt(projectSetId) || normalizePositiveInt(defaultProjectSetId);
|
|
527
|
-
const
|
|
760
|
+
const requestedPath = normalizeBugsListPath(path);
|
|
761
|
+
const primaryPath = buildProductScopedBugsPath({ productId: effectiveProductId, path: requestedPath });
|
|
528
762
|
const preferProjectSetPath =
|
|
529
763
|
effectiveProjectSetId &&
|
|
530
|
-
|
|
764
|
+
requestedPath === "/bugs";
|
|
531
765
|
const configuredMyBugsPath = buildMyBugsPath(myBugsPath);
|
|
532
766
|
const fallbackPathCandidates = (bugsFallbackPaths && bugsFallbackPaths.length > 0)
|
|
533
767
|
? bugsFallbackPaths
|
|
@@ -605,6 +839,241 @@ export function createZenTaoClient(config) {
|
|
|
605
839
|
if (candidate !== primaryPath) continue;
|
|
606
840
|
}
|
|
607
841
|
}
|
|
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
|
+
|
|
608
1077
|
if (successful.length === 0) throw lastErr;
|
|
609
1078
|
|
|
610
1079
|
const mergedBugs = [];
|
|
@@ -629,7 +1098,7 @@ export function createZenTaoClient(config) {
|
|
|
629
1098
|
}, null);
|
|
630
1099
|
|
|
631
1100
|
return {
|
|
632
|
-
total:
|
|
1101
|
+
total: mergedFiltered.length,
|
|
633
1102
|
matched: mergedFiltered.length,
|
|
634
1103
|
page: safePage,
|
|
635
1104
|
limit: safeLimit,
|
|
@@ -641,6 +1110,7 @@ export function createZenTaoClient(config) {
|
|
|
641
1110
|
status: best?.status ?? null,
|
|
642
1111
|
path: best?.path ?? primaryPath,
|
|
643
1112
|
triedPaths,
|
|
1113
|
+
scannedTotal: mergedBugs.length,
|
|
644
1114
|
paths: successful.map((r) => ({
|
|
645
1115
|
path: r.path,
|
|
646
1116
|
status: r.status,
|
|
@@ -652,13 +1122,13 @@ export function createZenTaoClient(config) {
|
|
|
652
1122
|
};
|
|
653
1123
|
}
|
|
654
1124
|
|
|
655
|
-
async function getBugDetail({ id
|
|
1125
|
+
async function getBugDetail({ id } = {}) {
|
|
656
1126
|
const bugId = Number(id);
|
|
657
1127
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
658
1128
|
throw new Error("getBugDetail requires a valid bug id");
|
|
659
1129
|
}
|
|
660
1130
|
|
|
661
|
-
const detailPath = buildBugDetailPath({ id: bugId
|
|
1131
|
+
const detailPath = buildBugDetailPath({ id: bugId });
|
|
662
1132
|
const resp = await call({ path: detailPath, method: "GET" });
|
|
663
1133
|
const bug = parseBugDetailFromResponse(resp.data);
|
|
664
1134
|
if (!bug) {
|
|
@@ -666,14 +1136,14 @@ export function createZenTaoClient(config) {
|
|
|
666
1136
|
id: bugId,
|
|
667
1137
|
found: false,
|
|
668
1138
|
images: [],
|
|
669
|
-
raw: { status: resp.status
|
|
1139
|
+
raw: { status: resp.status },
|
|
670
1140
|
};
|
|
671
1141
|
}
|
|
672
1142
|
|
|
673
1143
|
return {
|
|
674
1144
|
id: bugId,
|
|
675
1145
|
found: true,
|
|
676
|
-
bug,
|
|
1146
|
+
bug: buildSafeBugDetail(bug),
|
|
677
1147
|
images: extractImageUrlsFromBug(bug),
|
|
678
1148
|
raw: { status: resp.status },
|
|
679
1149
|
};
|
|
@@ -684,14 +1154,13 @@ export function createZenTaoClient(config) {
|
|
|
684
1154
|
resolution = "fixed",
|
|
685
1155
|
solution = "",
|
|
686
1156
|
comment = "",
|
|
687
|
-
path = "/bugs/{id}/resolve",
|
|
688
1157
|
} = {}) {
|
|
689
1158
|
const bugId = Number(id);
|
|
690
1159
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
691
1160
|
throw new Error("resolveBug requires a valid bug id");
|
|
692
1161
|
}
|
|
693
1162
|
|
|
694
|
-
const resolvePath = buildBugResolvePath({ id: bugId
|
|
1163
|
+
const resolvePath = buildBugResolvePath({ id: bugId });
|
|
695
1164
|
const resolvedValue = String(resolution || "fixed");
|
|
696
1165
|
const resolvedComment = buildResolutionComment({ solution, comment, resolution: resolvedValue });
|
|
697
1166
|
const body = {
|
|
@@ -706,17 +1175,17 @@ export function createZenTaoClient(config) {
|
|
|
706
1175
|
resolution: resolvedValue,
|
|
707
1176
|
solution: String(solution || "").trim(),
|
|
708
1177
|
comment: resolvedComment,
|
|
709
|
-
raw: { status: resp.status
|
|
1178
|
+
raw: { status: resp.status },
|
|
710
1179
|
};
|
|
711
1180
|
}
|
|
712
1181
|
|
|
713
|
-
async function closeBug({ id, comment = ""
|
|
1182
|
+
async function closeBug({ id, comment = "" } = {}) {
|
|
714
1183
|
const bugId = Number(id);
|
|
715
1184
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
716
1185
|
throw new Error("closeBug requires a valid bug id");
|
|
717
1186
|
}
|
|
718
1187
|
|
|
719
|
-
const closePath = buildBugTransitionPath({ id: bugId,
|
|
1188
|
+
const closePath = buildBugTransitionPath({ id: bugId, action: "close" });
|
|
720
1189
|
const body = {};
|
|
721
1190
|
if (comment) body.comment = String(comment);
|
|
722
1191
|
|
|
@@ -724,17 +1193,17 @@ export function createZenTaoClient(config) {
|
|
|
724
1193
|
return {
|
|
725
1194
|
id: bugId,
|
|
726
1195
|
closed: true,
|
|
727
|
-
raw: { status: resp.status
|
|
1196
|
+
raw: { status: resp.status },
|
|
728
1197
|
};
|
|
729
1198
|
}
|
|
730
1199
|
|
|
731
|
-
async function activateBug({ id, comment = ""
|
|
1200
|
+
async function activateBug({ id, comment = "" } = {}) {
|
|
732
1201
|
const bugId = Number(id);
|
|
733
1202
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
734
1203
|
throw new Error("activateBug requires a valid bug id");
|
|
735
1204
|
}
|
|
736
1205
|
|
|
737
|
-
const activatePath = buildBugTransitionPath({ id: bugId,
|
|
1206
|
+
const activatePath = buildBugTransitionPath({ id: bugId, action: "activate" });
|
|
738
1207
|
const body = {};
|
|
739
1208
|
if (comment) body.comment = String(comment);
|
|
740
1209
|
|
|
@@ -742,7 +1211,7 @@ export function createZenTaoClient(config) {
|
|
|
742
1211
|
return {
|
|
743
1212
|
id: bugId,
|
|
744
1213
|
activated: true,
|
|
745
|
-
raw: { status: resp.status
|
|
1214
|
+
raw: { status: resp.status },
|
|
746
1215
|
};
|
|
747
1216
|
}
|
|
748
1217
|
|
|
@@ -750,8 +1219,6 @@ export function createZenTaoClient(config) {
|
|
|
750
1219
|
id,
|
|
751
1220
|
result = "pass",
|
|
752
1221
|
comment = "",
|
|
753
|
-
closePath = "/bugs/{id}/close",
|
|
754
|
-
activatePath = "/bugs/{id}/activate",
|
|
755
1222
|
} = {}) {
|
|
756
1223
|
const normalizedResult = normalizeString(result || "pass");
|
|
757
1224
|
if (normalizedResult !== "pass" && normalizedResult !== "fail") {
|
|
@@ -759,7 +1226,7 @@ export function createZenTaoClient(config) {
|
|
|
759
1226
|
}
|
|
760
1227
|
|
|
761
1228
|
if (normalizedResult === "pass") {
|
|
762
|
-
const closeResult = await closeBug({ id, comment
|
|
1229
|
+
const closeResult = await closeBug({ id, comment });
|
|
763
1230
|
return {
|
|
764
1231
|
id: Number(id),
|
|
765
1232
|
verified: true,
|
|
@@ -769,7 +1236,7 @@ export function createZenTaoClient(config) {
|
|
|
769
1236
|
};
|
|
770
1237
|
}
|
|
771
1238
|
|
|
772
|
-
const activateResult = await activateBug({ id, comment
|
|
1239
|
+
const activateResult = await activateBug({ id, comment });
|
|
773
1240
|
return {
|
|
774
1241
|
id: Number(id),
|
|
775
1242
|
verified: true,
|
|
@@ -779,7 +1246,7 @@ export function createZenTaoClient(config) {
|
|
|
779
1246
|
};
|
|
780
1247
|
}
|
|
781
1248
|
|
|
782
|
-
async function commentBug({ id, comment
|
|
1249
|
+
async function commentBug({ id, comment } = {}) {
|
|
783
1250
|
const bugId = Number(id);
|
|
784
1251
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
785
1252
|
throw new Error("commentBug requires a valid bug id");
|
|
@@ -789,7 +1256,7 @@ export function createZenTaoClient(config) {
|
|
|
789
1256
|
throw new Error("commentBug requires non-empty comment");
|
|
790
1257
|
}
|
|
791
1258
|
|
|
792
|
-
const primaryPath = buildBugCommentPath({ id: bugId
|
|
1259
|
+
const primaryPath = buildBugCommentPath({ id: bugId });
|
|
793
1260
|
const body = { comment: text };
|
|
794
1261
|
try {
|
|
795
1262
|
const resp = await call({ path: primaryPath, method: "POST", body });
|
|
@@ -797,7 +1264,7 @@ export function createZenTaoClient(config) {
|
|
|
797
1264
|
id: bugId,
|
|
798
1265
|
commented: true,
|
|
799
1266
|
comment: text,
|
|
800
|
-
raw: { status: resp.status, path: primaryPath
|
|
1267
|
+
raw: { status: resp.status, path: primaryPath },
|
|
801
1268
|
};
|
|
802
1269
|
} catch (err) {
|
|
803
1270
|
const fallbackPath = primaryPath.replace(/\/comment$/, "/comments");
|
|
@@ -807,7 +1274,7 @@ export function createZenTaoClient(config) {
|
|
|
807
1274
|
id: bugId,
|
|
808
1275
|
commented: true,
|
|
809
1276
|
comment: text,
|
|
810
|
-
raw: { status: resp.status, path: fallbackPath
|
|
1277
|
+
raw: { status: resp.status, path: fallbackPath },
|
|
811
1278
|
};
|
|
812
1279
|
}
|
|
813
1280
|
throw err;
|
|
@@ -821,16 +1288,14 @@ export function createZenTaoClient(config) {
|
|
|
821
1288
|
page = 1,
|
|
822
1289
|
productId,
|
|
823
1290
|
projectSetId,
|
|
824
|
-
maxItems =
|
|
825
|
-
assignedTo = "",
|
|
1291
|
+
maxItems = 20,
|
|
826
1292
|
resolution = "fixed",
|
|
827
1293
|
solution = "",
|
|
828
1294
|
comment = "",
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
stopOnError = false,
|
|
1295
|
+
path = "/bugs",
|
|
1296
|
+
stopOnError = true,
|
|
832
1297
|
} = {}) {
|
|
833
|
-
const safeMaxItems = Math.max(1, Math.min(Number(maxItems) ||
|
|
1298
|
+
const safeMaxItems = Math.max(1, Math.min(Number(maxItems) || 20, MAX_BATCH_RESOLVE_ITEMS));
|
|
834
1299
|
const listResult = await getMyBugs({
|
|
835
1300
|
status,
|
|
836
1301
|
keyword,
|
|
@@ -838,8 +1303,7 @@ export function createZenTaoClient(config) {
|
|
|
838
1303
|
page,
|
|
839
1304
|
productId,
|
|
840
1305
|
projectSetId,
|
|
841
|
-
path
|
|
842
|
-
assignedTo,
|
|
1306
|
+
path,
|
|
843
1307
|
});
|
|
844
1308
|
|
|
845
1309
|
const candidates = (listResult.bugs || []).slice(0, safeMaxItems);
|
|
@@ -859,7 +1323,6 @@ export function createZenTaoClient(config) {
|
|
|
859
1323
|
resolution,
|
|
860
1324
|
solution,
|
|
861
1325
|
comment,
|
|
862
|
-
path: resolvePath,
|
|
863
1326
|
});
|
|
864
1327
|
success.push({ id: bugId, status: result.raw.status });
|
|
865
1328
|
} catch (err) {
|