@aipper/zentao-mcp-server 0.1.6 → 0.1.7
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 -0
- package/README.md +5 -2
- package/package.json +1 -1
- package/src/index.js +108 -91
- package/src/tools.js +12 -1
- package/src/zentao.js +58 -5
package/.env.example
CHANGED
|
@@ -8,6 +8,9 @@ ZENTAO_PASSWORD=your_password
|
|
|
8
8
|
# 可选:API 前缀(默认 /api.php/v1)
|
|
9
9
|
# ZENTAO_API_PREFIX=/api.php/v1
|
|
10
10
|
|
|
11
|
+
# 可选:默认产品 ID(当实例请求 bug 列表需要 product id 时使用)
|
|
12
|
+
# ZENTAO_PRODUCT_ID=1
|
|
13
|
+
|
|
11
14
|
# 获取 Token 的路径(相对 ZENTAO_BASE_URL;通常为 /api.php/v1/tokens)
|
|
12
15
|
ZENTAO_TOKEN_PATH=/api.php/v1/tokens
|
|
13
16
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- `ZENTAO_BASE_URL`
|
|
17
17
|
- `ZENTAO_ACCOUNT`
|
|
18
18
|
- `ZENTAO_PASSWORD`
|
|
19
|
+
- (可选)`ZENTAO_PRODUCT_ID`:你的禅道实例若报 `Need product id` 时设置
|
|
19
20
|
|
|
20
21
|
> 注意:不同禅道版本/部署方式的 token 端点与返回结构可能不同;可通过 `ZENTAO_TOKEN_PATH`/`ZENTAO_API_PREFIX` 调整。
|
|
21
22
|
>
|
|
@@ -86,6 +87,7 @@ npm run smoke
|
|
|
86
87
|
## 常见错误(`-32000`)
|
|
87
88
|
`-32000` 通常是客户端侧“通用 MCP 调用失败”映射码,优先检查:
|
|
88
89
|
- `env` 是否完整传入(尤其是 `ZENTAO_BASE_URL`/`ZENTAO_ACCOUNT`/`ZENTAO_PASSWORD`)。
|
|
90
|
+
- 若报 `Need product id`,请设置 `ZENTAO_PRODUCT_ID`,或在 `get_my_bugs` 传 `productId`。
|
|
89
91
|
- `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致。
|
|
90
92
|
- MCP 客户端是否真的在执行 `npx -y @aipper/zentao-mcp-server`(而不是旧的本地命令)。
|
|
91
93
|
- 客户端日志中是否有启动报错(如找不到命令、401、超时)。
|
|
@@ -94,16 +96,17 @@ npm run smoke
|
|
|
94
96
|
- `get_token`:获取/刷新 token(默认不回显完整 token)
|
|
95
97
|
- `call`:调用任意相对 API 路径(自动带 Token 头)
|
|
96
98
|
- `list_my_projects`:示例:列出“我参与的项目”(字段匹配基于常见返回结构,可能需按你的实例微调)
|
|
97
|
-
- `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`,默认路径 `/bugs`)
|
|
99
|
+
- `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`,默认路径 `/bugs`)
|
|
98
100
|
- `get_bug_detail`:按 `id` 获取 bug 详情(默认路径模板 `/bugs/{id}`,返回详情与图片链接)
|
|
99
101
|
- `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`)
|
|
100
|
-
- `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`)
|
|
102
|
+
- `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`)
|
|
101
103
|
- `close_bug`:按 `id` 关闭 bug
|
|
102
104
|
- `verify_bug`:验证结果处理(`pass`=关闭,`fail`=激活)
|
|
103
105
|
|
|
104
106
|
示例参数:
|
|
105
107
|
- `resolve_bug`:`{"id":123,"resolution":"fixed","comment":"已修复并自测"}`
|
|
106
108
|
- `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"批量修复"}`
|
|
109
|
+
- `get_my_bugs`(按产品):`{"status":"active","productId":1,"limit":50}`
|
|
107
110
|
- `close_bug`:`{"id":123,"comment":"验证通过,关闭"}`
|
|
108
111
|
- `verify_bug`:`{"id":123,"result":"pass","comment":"验证通过"}`
|
|
109
112
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -48,6 +48,7 @@ function getConfigFromEnv() {
|
|
|
48
48
|
const tokenTtlMs = Number(process.env.ZENTAO_TOKEN_TTL_MS || "3000000");
|
|
49
49
|
const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
|
|
50
50
|
const exposeToken = String(process.env.ZENTAO_EXPOSE_TOKEN || "false").toLowerCase() === "true";
|
|
51
|
+
const defaultProductId = Number(process.env.ZENTAO_PRODUCT_ID || "0") || null;
|
|
51
52
|
|
|
52
53
|
const account = requireEnv("ZENTAO_ACCOUNT");
|
|
53
54
|
const password = requireEnv("ZENTAO_PASSWORD");
|
|
@@ -59,6 +60,7 @@ function getConfigFromEnv() {
|
|
|
59
60
|
tokenTtlMs,
|
|
60
61
|
timeoutMs,
|
|
61
62
|
exposeToken,
|
|
63
|
+
defaultProductId,
|
|
62
64
|
auth: { account, password },
|
|
63
65
|
};
|
|
64
66
|
}
|
|
@@ -80,99 +82,114 @@ async function main() {
|
|
|
80
82
|
const rawToolName = req.params?.name;
|
|
81
83
|
const toolName = normalizeToolName(rawToolName);
|
|
82
84
|
const args = req.params?.arguments || {};
|
|
83
|
-
assertToolArgs(toolName, args);
|
|
84
|
-
|
|
85
|
-
if (toolName === "get_token") {
|
|
86
|
-
const force = Boolean(args.force);
|
|
87
|
-
const result = await zentao.getToken({ force });
|
|
88
|
-
const output = config.exposeToken
|
|
89
|
-
? result
|
|
90
|
-
: {
|
|
91
|
-
...result,
|
|
92
|
-
token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
|
|
93
|
-
};
|
|
94
|
-
return toMcpTextResult(JSON.stringify(output, null, 2));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (toolName === "call") {
|
|
98
|
-
const { path, method, query, body } = args;
|
|
99
|
-
const resp = await zentao.call({ path, method, query, body });
|
|
100
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (toolName === "list_my_projects") {
|
|
104
|
-
const resp = await zentao.listMyProjects({ keyword: args.keyword || "" });
|
|
105
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (toolName === "get_my_bugs") {
|
|
109
|
-
const resp = await zentao.getMyBugs({
|
|
110
|
-
status: args.status || "",
|
|
111
|
-
keyword: args.keyword || "",
|
|
112
|
-
limit: args.limit,
|
|
113
|
-
page: args.page,
|
|
114
|
-
path: args.path || "/bugs",
|
|
115
|
-
assignedTo: args.assignedTo || "",
|
|
116
|
-
});
|
|
117
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (toolName === "get_bug_detail") {
|
|
121
|
-
const resp = await zentao.getBugDetail({
|
|
122
|
-
id: args.id,
|
|
123
|
-
path: args.path || "/bugs/{id}",
|
|
124
|
-
});
|
|
125
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (toolName === "resolve_bug") {
|
|
129
|
-
const resp = await zentao.resolveBug({
|
|
130
|
-
id: args.id,
|
|
131
|
-
resolution: args.resolution || "fixed",
|
|
132
|
-
comment: args.comment || "",
|
|
133
|
-
path: args.path || "/bugs/{id}/resolve",
|
|
134
|
-
});
|
|
135
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (toolName === "batch_resolve_my_bugs") {
|
|
139
|
-
const resp = await zentao.batchResolveMyBugs({
|
|
140
|
-
status: args.status || "active",
|
|
141
|
-
keyword: args.keyword || "",
|
|
142
|
-
limit: args.limit,
|
|
143
|
-
page: args.page,
|
|
144
|
-
maxItems: args.maxItems,
|
|
145
|
-
assignedTo: args.assignedTo || "",
|
|
146
|
-
resolution: args.resolution || "fixed",
|
|
147
|
-
comment: args.comment || "",
|
|
148
|
-
listPath: args.listPath || "/bugs",
|
|
149
|
-
resolvePath: args.resolvePath || "/bugs/{id}/resolve",
|
|
150
|
-
stopOnError: Boolean(args.stopOnError),
|
|
151
|
-
});
|
|
152
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (toolName === "close_bug") {
|
|
156
|
-
const resp = await zentao.closeBug({
|
|
157
|
-
id: args.id,
|
|
158
|
-
comment: args.comment || "",
|
|
159
|
-
path: args.path || "/bugs/{id}/close",
|
|
160
|
-
});
|
|
161
|
-
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
162
|
-
}
|
|
163
85
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
86
|
+
try {
|
|
87
|
+
assertToolArgs(toolName, args);
|
|
88
|
+
|
|
89
|
+
if (toolName === "get_token") {
|
|
90
|
+
const force = Boolean(args.force);
|
|
91
|
+
const result = await zentao.getToken({ force });
|
|
92
|
+
const output = config.exposeToken
|
|
93
|
+
? result
|
|
94
|
+
: {
|
|
95
|
+
...result,
|
|
96
|
+
token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
|
|
97
|
+
};
|
|
98
|
+
return toMcpTextResult(JSON.stringify(output, null, 2));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (toolName === "call") {
|
|
102
|
+
const { path, method, query, body } = args;
|
|
103
|
+
const resp = await zentao.call({ path, method, query, body });
|
|
104
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (toolName === "list_my_projects") {
|
|
108
|
+
const resp = await zentao.listMyProjects({ keyword: args.keyword || "" });
|
|
109
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (toolName === "get_my_bugs") {
|
|
113
|
+
const resp = await zentao.getMyBugs({
|
|
114
|
+
status: args.status || "",
|
|
115
|
+
keyword: args.keyword || "",
|
|
116
|
+
limit: args.limit,
|
|
117
|
+
page: args.page,
|
|
118
|
+
productId: args.productId,
|
|
119
|
+
path: args.path || "/bugs",
|
|
120
|
+
assignedTo: args.assignedTo || "",
|
|
121
|
+
});
|
|
122
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (toolName === "get_bug_detail") {
|
|
126
|
+
const resp = await zentao.getBugDetail({
|
|
127
|
+
id: args.id,
|
|
128
|
+
path: args.path || "/bugs/{id}",
|
|
129
|
+
});
|
|
130
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (toolName === "resolve_bug") {
|
|
134
|
+
const resp = await zentao.resolveBug({
|
|
135
|
+
id: args.id,
|
|
136
|
+
resolution: args.resolution || "fixed",
|
|
137
|
+
comment: args.comment || "",
|
|
138
|
+
path: args.path || "/bugs/{id}/resolve",
|
|
139
|
+
});
|
|
140
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (toolName === "batch_resolve_my_bugs") {
|
|
144
|
+
const resp = await zentao.batchResolveMyBugs({
|
|
145
|
+
status: args.status || "active",
|
|
146
|
+
keyword: args.keyword || "",
|
|
147
|
+
limit: args.limit,
|
|
148
|
+
page: args.page,
|
|
149
|
+
productId: args.productId,
|
|
150
|
+
maxItems: args.maxItems,
|
|
151
|
+
assignedTo: args.assignedTo || "",
|
|
152
|
+
resolution: args.resolution || "fixed",
|
|
153
|
+
comment: args.comment || "",
|
|
154
|
+
listPath: args.listPath || "/bugs",
|
|
155
|
+
resolvePath: args.resolvePath || "/bugs/{id}/resolve",
|
|
156
|
+
stopOnError: Boolean(args.stopOnError),
|
|
157
|
+
});
|
|
158
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (toolName === "close_bug") {
|
|
162
|
+
const resp = await zentao.closeBug({
|
|
163
|
+
id: args.id,
|
|
164
|
+
comment: args.comment || "",
|
|
165
|
+
path: args.path || "/bugs/{id}/close",
|
|
166
|
+
});
|
|
167
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === "verify_bug") {
|
|
171
|
+
const resp = await zentao.verifyBug({
|
|
172
|
+
id: args.id,
|
|
173
|
+
result: args.result || "pass",
|
|
174
|
+
comment: args.comment || "",
|
|
175
|
+
closePath: args.closePath || "/bugs/{id}/close",
|
|
176
|
+
activatePath: args.activatePath || "/bugs/{id}/activate",
|
|
177
|
+
});
|
|
178
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error(`Unknown tool: ${rawToolName}`);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const errorPayload = {
|
|
184
|
+
ok: false,
|
|
185
|
+
tool: rawToolName,
|
|
186
|
+
message: String(err?.message || err),
|
|
187
|
+
status: err?.status ?? null,
|
|
188
|
+
data: err?.data ?? null,
|
|
189
|
+
hint: "If you see 'Need product id', set env ZENTAO_PRODUCT_ID or pass productId in get_my_bugs.",
|
|
190
|
+
};
|
|
191
|
+
return toMcpTextResult(JSON.stringify(errorPayload, null, 2), { isError: true });
|
|
173
192
|
}
|
|
174
|
-
|
|
175
|
-
throw new Error(`Unknown tool: ${rawToolName}`);
|
|
176
193
|
});
|
|
177
194
|
|
|
178
195
|
const transport = new StdioServerTransport();
|
package/src/tools.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
export function toMcpTextResult(text) {
|
|
1
|
+
export function toMcpTextResult(text, options = {}) {
|
|
2
|
+
const { isError = false } = options;
|
|
2
3
|
return {
|
|
3
4
|
content: [{ type: "text", text }],
|
|
4
5
|
structuredContent: { output: text },
|
|
6
|
+
output: text,
|
|
7
|
+
...(isError ? { isError: true } : {}),
|
|
5
8
|
};
|
|
6
9
|
}
|
|
7
10
|
|
|
@@ -49,6 +52,7 @@ export const TOOLS = [
|
|
|
49
52
|
keyword: { type: "string", description: "Optional keyword in title/steps/severity/pri" },
|
|
50
53
|
limit: { type: "number", minimum: 1, maximum: 200, description: "Default 20, max 200" },
|
|
51
54
|
page: { type: "number", minimum: 1, description: "Default 1" },
|
|
55
|
+
productId: { type: "number", minimum: 1, description: "Optional product id (for instances requiring product scope)" },
|
|
52
56
|
path: { type: "string", description: "Optional bugs endpoint override, default /bugs" },
|
|
53
57
|
assignedTo: { type: "string", description: "Optional assignee override, default current account" },
|
|
54
58
|
},
|
|
@@ -93,6 +97,7 @@ export const TOOLS = [
|
|
|
93
97
|
keyword: { type: "string", description: "Optional keyword filter before resolve" },
|
|
94
98
|
limit: { type: "number", minimum: 1, maximum: 200, description: "List page size, default 50" },
|
|
95
99
|
page: { type: "number", minimum: 1, description: "Default 1" },
|
|
100
|
+
productId: { type: "number", minimum: 1, description: "Optional product id (for instances requiring product scope)" },
|
|
96
101
|
maxItems: { type: "number", minimum: 1, maximum: 500, description: "Max resolve count, default 50" },
|
|
97
102
|
assignedTo: { type: "string", description: "Optional assignee override" },
|
|
98
103
|
resolution: { type: "string", description: "Default fixed" },
|
|
@@ -152,6 +157,9 @@ export function assertToolArgs(name, args) {
|
|
|
152
157
|
if (args.path !== undefined && typeof args.path !== "string") {
|
|
153
158
|
throw new Error("get_my_bugs.path must be a string");
|
|
154
159
|
}
|
|
160
|
+
if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
|
|
161
|
+
throw new Error("get_my_bugs.productId must be a number >= 1");
|
|
162
|
+
}
|
|
155
163
|
}
|
|
156
164
|
if (name === "get_bug_detail") {
|
|
157
165
|
if (!Number.isFinite(args.id) || Number(args.id) < 1) {
|
|
@@ -179,6 +187,9 @@ export function assertToolArgs(name, args) {
|
|
|
179
187
|
if (args.maxItems !== undefined && (!Number.isFinite(args.maxItems) || args.maxItems < 1 || args.maxItems > 500)) {
|
|
180
188
|
throw new Error("batch_resolve_my_bugs.maxItems must be a number between 1 and 500");
|
|
181
189
|
}
|
|
190
|
+
if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
|
|
191
|
+
throw new Error("batch_resolve_my_bugs.productId must be a number >= 1");
|
|
192
|
+
}
|
|
182
193
|
if (args.listPath !== undefined && typeof args.listPath !== "string") {
|
|
183
194
|
throw new Error("batch_resolve_my_bugs.listPath must be a string");
|
|
184
195
|
}
|
package/src/zentao.js
CHANGED
|
@@ -31,7 +31,7 @@ function createAbortSignal(timeoutMs) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function createZenTaoClient(config) {
|
|
34
|
-
const { baseUrl, apiPrefix, tokenPath, tokenTtlMs, timeoutMs, auth } = config;
|
|
34
|
+
const { baseUrl, apiPrefix, tokenPath, tokenTtlMs, timeoutMs, defaultProductId, auth } = config;
|
|
35
35
|
|
|
36
36
|
let cachedToken = "";
|
|
37
37
|
let cachedAt = 0;
|
|
@@ -174,6 +174,30 @@ export function createZenTaoClient(config) {
|
|
|
174
174
|
return String(value || "").trim().toLowerCase();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
function normalizePositiveInt(value) {
|
|
178
|
+
const parsed = Number(value);
|
|
179
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildProductScopedBugsPath({ productId, path }) {
|
|
183
|
+
const pid = normalizePositiveInt(productId);
|
|
184
|
+
if (!pid) return path || "/bugs";
|
|
185
|
+
|
|
186
|
+
const basePath = path || "/bugs";
|
|
187
|
+
if (basePath.includes("{productId}")) {
|
|
188
|
+
return basePath.replaceAll("{productId}", String(pid));
|
|
189
|
+
}
|
|
190
|
+
if (basePath === "/bugs") {
|
|
191
|
+
return `/products/${pid}/bugs`;
|
|
192
|
+
}
|
|
193
|
+
return basePath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isNeedProductIdError(err) {
|
|
197
|
+
const merged = `${String(err?.message || "")} ${JSON.stringify(err?.data || "")}`.toLowerCase();
|
|
198
|
+
return merged.includes("need product id");
|
|
199
|
+
}
|
|
200
|
+
|
|
177
201
|
function getBugAssignee(bug) {
|
|
178
202
|
return (
|
|
179
203
|
bug?.assignedTo ||
|
|
@@ -275,19 +299,45 @@ export function createZenTaoClient(config) {
|
|
|
275
299
|
return Array.from(images);
|
|
276
300
|
}
|
|
277
301
|
|
|
278
|
-
async function getMyBugs({
|
|
302
|
+
async function getMyBugs({
|
|
303
|
+
status,
|
|
304
|
+
keyword,
|
|
305
|
+
limit = 20,
|
|
306
|
+
page = 1,
|
|
307
|
+
productId,
|
|
308
|
+
path = "/bugs",
|
|
309
|
+
assignedTo,
|
|
310
|
+
} = {}) {
|
|
279
311
|
const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
280
312
|
const safePage = Math.max(1, Number(page) || 1);
|
|
281
313
|
const assignee = normalizeString(assignedTo) || normalizeString(auth.account);
|
|
314
|
+
const effectiveProductId = normalizePositiveInt(productId) || normalizePositiveInt(defaultProductId);
|
|
315
|
+
const primaryPath = buildProductScopedBugsPath({ productId: effectiveProductId, path });
|
|
282
316
|
|
|
283
317
|
const query = {
|
|
284
318
|
limit: safeLimit,
|
|
285
319
|
page: safePage,
|
|
286
320
|
assignedTo: assignee || undefined,
|
|
287
321
|
status: status || undefined,
|
|
322
|
+
product: effectiveProductId || undefined,
|
|
288
323
|
};
|
|
289
|
-
|
|
290
|
-
|
|
324
|
+
let resp;
|
|
325
|
+
try {
|
|
326
|
+
resp = await call({ path: primaryPath, method: "GET", query });
|
|
327
|
+
} catch (err) {
|
|
328
|
+
const fallbackPath = effectiveProductId ? `/products/${effectiveProductId}/bugs` : null;
|
|
329
|
+
if (
|
|
330
|
+
fallbackPath &&
|
|
331
|
+
primaryPath !== fallbackPath &&
|
|
332
|
+
isNeedProductIdError(err)
|
|
333
|
+
) {
|
|
334
|
+
resp = await call({ path: fallbackPath, method: "GET", query: { limit: safeLimit, page: safePage } });
|
|
335
|
+
} else {
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const bugs = parseBugsFromResponse(resp?.data);
|
|
291
341
|
const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
292
342
|
|
|
293
343
|
return {
|
|
@@ -295,9 +345,10 @@ export function createZenTaoClient(config) {
|
|
|
295
345
|
matched: filtered.length,
|
|
296
346
|
page: safePage,
|
|
297
347
|
limit: safeLimit,
|
|
348
|
+
productId: effectiveProductId,
|
|
298
349
|
assignedTo: assignee,
|
|
299
350
|
bugs: filtered,
|
|
300
|
-
raw: { status: resp
|
|
351
|
+
raw: { status: resp?.status, path: primaryPath },
|
|
301
352
|
};
|
|
302
353
|
}
|
|
303
354
|
|
|
@@ -421,6 +472,7 @@ export function createZenTaoClient(config) {
|
|
|
421
472
|
keyword = "",
|
|
422
473
|
limit = 50,
|
|
423
474
|
page = 1,
|
|
475
|
+
productId,
|
|
424
476
|
maxItems = 50,
|
|
425
477
|
assignedTo = "",
|
|
426
478
|
resolution = "fixed",
|
|
@@ -435,6 +487,7 @@ export function createZenTaoClient(config) {
|
|
|
435
487
|
keyword,
|
|
436
488
|
limit,
|
|
437
489
|
page,
|
|
490
|
+
productId,
|
|
438
491
|
path: listPath,
|
|
439
492
|
assignedTo,
|
|
440
493
|
});
|