@aipper/zentao-mcp-server 0.1.6 → 0.1.8
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 +9 -0
- package/README.md +14 -2
- package/package.json +1 -1
- package/src/index.js +127 -91
- package/src/tools.js +45 -1
- package/src/zentao.js +164 -9
package/.env.example
CHANGED
|
@@ -8,6 +8,15 @@ 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
|
+
|
|
14
|
+
# 可选:我的 bug 列表路径(例如 /my/bug 或 /my/bugs)
|
|
15
|
+
# ZENTAO_MY_BUGS_PATH=/my/bug
|
|
16
|
+
|
|
17
|
+
# 可选:bug 列表回退路径(逗号分隔)
|
|
18
|
+
# ZENTAO_BUGS_FALLBACK_PATHS=/my/bug,/my/bugs
|
|
19
|
+
|
|
11
20
|
# 获取 Token 的路径(相对 ZENTAO_BASE_URL;通常为 /api.php/v1/tokens)
|
|
12
21
|
ZENTAO_TOKEN_PATH=/api.php/v1/tokens
|
|
13
22
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
- `ZENTAO_BASE_URL`
|
|
17
17
|
- `ZENTAO_ACCOUNT`
|
|
18
18
|
- `ZENTAO_PASSWORD`
|
|
19
|
+
- (可选)`ZENTAO_PRODUCT_ID`:你的禅道实例若报 `Need product id` 时设置
|
|
20
|
+
- (可选)`ZENTAO_MY_BUGS_PATH`:我的 bug 专用接口路径(如 `/my/bug`)
|
|
21
|
+
- (可选)`ZENTAO_BUGS_FALLBACK_PATHS`:bug 列表回退路径(逗号分隔)
|
|
19
22
|
|
|
20
23
|
> 注意:不同禅道版本/部署方式的 token 端点与返回结构可能不同;可通过 `ZENTAO_TOKEN_PATH`/`ZENTAO_API_PREFIX` 调整。
|
|
21
24
|
>
|
|
@@ -86,6 +89,8 @@ npm run smoke
|
|
|
86
89
|
## 常见错误(`-32000`)
|
|
87
90
|
`-32000` 通常是客户端侧“通用 MCP 调用失败”映射码,优先检查:
|
|
88
91
|
- `env` 是否完整传入(尤其是 `ZENTAO_BASE_URL`/`ZENTAO_ACCOUNT`/`ZENTAO_PASSWORD`)。
|
|
92
|
+
- 若报 `Need product id`,请设置 `ZENTAO_PRODUCT_ID`,或在 `get_my_bugs` 传 `productId`。
|
|
93
|
+
- 若你的 bug 在“项目集/我的视角”而非产品,建议设置 `ZENTAO_MY_BUGS_PATH=/my/bug`(或 `/my/bugs`)。
|
|
89
94
|
- `ZENTAO_API_PREFIX`/`ZENTAO_TOKEN_PATH` 是否和你的禅道实例一致。
|
|
90
95
|
- MCP 客户端是否真的在执行 `npx -y @aipper/zentao-mcp-server`(而不是旧的本地命令)。
|
|
91
96
|
- 客户端日志中是否有启动报错(如找不到命令、401、超时)。
|
|
@@ -94,18 +99,25 @@ npm run smoke
|
|
|
94
99
|
- `get_token`:获取/刷新 token(默认不回显完整 token)
|
|
95
100
|
- `call`:调用任意相对 API 路径(自动带 Token 头)
|
|
96
101
|
- `list_my_projects`:示例:列出“我参与的项目”(字段匹配基于常见返回结构,可能需按你的实例微调)
|
|
97
|
-
- `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`,默认路径 `/bugs`)
|
|
102
|
+
- `get_my_bugs`:获取“指派给我”的 bug(支持 `status`/`keyword`/`limit`/`page`/`productId`,默认路径 `/bugs`)
|
|
98
103
|
- `get_bug_detail`:按 `id` 获取 bug 详情(默认路径模板 `/bugs/{id}`,返回详情与图片链接)
|
|
99
104
|
- `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`)
|
|
100
|
-
- `
|
|
105
|
+
- `resolve_bug`:按 `id` 处理单个 bug 状态(默认 `resolution=fixed`,支持 `solution` 解决说明)
|
|
106
|
+
- `batch_resolve_my_bugs`:批量处理“我的 bug”(默认筛选 `status=active`,支持 `productId`)
|
|
101
107
|
- `close_bug`:按 `id` 关闭 bug
|
|
102
108
|
- `verify_bug`:验证结果处理(`pass`=关闭,`fail`=激活)
|
|
109
|
+
- `comment_bug`:按 `id` 添加备注(默认路径 `/bugs/{id}/comment`)
|
|
103
110
|
|
|
104
111
|
示例参数:
|
|
105
112
|
- `resolve_bug`:`{"id":123,"resolution":"fixed","comment":"已修复并自测"}`
|
|
113
|
+
- `resolve_bug`(建议):`{"id":123,"resolution":"fixed","solution":"修复空指针并补充参数校验"}`
|
|
106
114
|
- `batch_resolve_my_bugs`:`{"status":"active","maxItems":20,"comment":"批量修复"}`
|
|
115
|
+
- `batch_resolve_my_bugs`(建议):`{"status":"active","maxItems":20,"solution":"统一修复分页参数为空导致的报错"}`
|
|
116
|
+
- `get_my_bugs`(按产品):`{"status":"active","productId":1,"limit":50}`
|
|
117
|
+
- `get_my_bugs`(项目集/我的):`{"status":"active","path":"/my/bug","limit":50}`
|
|
107
118
|
- `close_bug`:`{"id":123,"comment":"验证通过,关闭"}`
|
|
108
119
|
- `verify_bug`:`{"id":123,"result":"pass","comment":"验证通过"}`
|
|
120
|
+
- `comment_bug`:`{"id":123,"comment":"已复现,正在定位根因"}`
|
|
109
121
|
|
|
110
122
|
## 安全建议
|
|
111
123
|
- 使用最小权限账号(仅需要的项目权限),避免使用管理员账号。
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const KNOWN_TOOL_NAMES = new Set([
|
|
|
18
18
|
"batch_resolve_my_bugs",
|
|
19
19
|
"close_bug",
|
|
20
20
|
"verify_bug",
|
|
21
|
+
"comment_bug",
|
|
21
22
|
]);
|
|
22
23
|
|
|
23
24
|
function normalizeToolName(rawName) {
|
|
@@ -48,6 +49,12 @@ function getConfigFromEnv() {
|
|
|
48
49
|
const tokenTtlMs = Number(process.env.ZENTAO_TOKEN_TTL_MS || "3000000");
|
|
49
50
|
const timeoutMs = Number(process.env.ZENTAO_HTTP_TIMEOUT_MS || "30000");
|
|
50
51
|
const exposeToken = String(process.env.ZENTAO_EXPOSE_TOKEN || "false").toLowerCase() === "true";
|
|
52
|
+
const defaultProductId = Number(process.env.ZENTAO_PRODUCT_ID || "0") || null;
|
|
53
|
+
const myBugsPath = String(process.env.ZENTAO_MY_BUGS_PATH || "").trim();
|
|
54
|
+
const bugsFallbackPaths = String(process.env.ZENTAO_BUGS_FALLBACK_PATHS || "")
|
|
55
|
+
.split(",")
|
|
56
|
+
.map((value) => value.trim())
|
|
57
|
+
.filter(Boolean);
|
|
51
58
|
|
|
52
59
|
const account = requireEnv("ZENTAO_ACCOUNT");
|
|
53
60
|
const password = requireEnv("ZENTAO_PASSWORD");
|
|
@@ -59,6 +66,9 @@ function getConfigFromEnv() {
|
|
|
59
66
|
tokenTtlMs,
|
|
60
67
|
timeoutMs,
|
|
61
68
|
exposeToken,
|
|
69
|
+
defaultProductId,
|
|
70
|
+
myBugsPath,
|
|
71
|
+
bugsFallbackPaths,
|
|
62
72
|
auth: { account, password },
|
|
63
73
|
};
|
|
64
74
|
}
|
|
@@ -80,99 +90,125 @@ async function main() {
|
|
|
80
90
|
const rawToolName = req.params?.name;
|
|
81
91
|
const toolName = normalizeToolName(rawToolName);
|
|
82
92
|
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
93
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
94
|
+
try {
|
|
95
|
+
assertToolArgs(toolName, args);
|
|
96
|
+
|
|
97
|
+
if (toolName === "get_token") {
|
|
98
|
+
const force = Boolean(args.force);
|
|
99
|
+
const result = await zentao.getToken({ force });
|
|
100
|
+
const output = config.exposeToken
|
|
101
|
+
? result
|
|
102
|
+
: {
|
|
103
|
+
...result,
|
|
104
|
+
token: result.token ? `${result.token.slice(0, 6)}…${result.token.slice(-4)}` : "",
|
|
105
|
+
};
|
|
106
|
+
return toMcpTextResult(JSON.stringify(output, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (toolName === "call") {
|
|
110
|
+
const { path, method, query, body } = args;
|
|
111
|
+
const resp = await zentao.call({ path, method, query, body });
|
|
112
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (toolName === "list_my_projects") {
|
|
116
|
+
const resp = await zentao.listMyProjects({ keyword: args.keyword || "" });
|
|
117
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (toolName === "get_my_bugs") {
|
|
121
|
+
const resp = await zentao.getMyBugs({
|
|
122
|
+
status: args.status || "",
|
|
123
|
+
keyword: args.keyword || "",
|
|
124
|
+
limit: args.limit,
|
|
125
|
+
page: args.page,
|
|
126
|
+
productId: args.productId,
|
|
127
|
+
path: args.path || "/bugs",
|
|
128
|
+
assignedTo: args.assignedTo || "",
|
|
129
|
+
});
|
|
130
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (toolName === "get_bug_detail") {
|
|
134
|
+
const resp = await zentao.getBugDetail({
|
|
135
|
+
id: args.id,
|
|
136
|
+
path: args.path || "/bugs/{id}",
|
|
137
|
+
});
|
|
138
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (toolName === "resolve_bug") {
|
|
142
|
+
const resp = await zentao.resolveBug({
|
|
143
|
+
id: args.id,
|
|
144
|
+
resolution: args.resolution || "fixed",
|
|
145
|
+
solution: args.solution || "",
|
|
146
|
+
comment: args.comment || "",
|
|
147
|
+
path: args.path || "/bugs/{id}/resolve",
|
|
148
|
+
});
|
|
149
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (toolName === "batch_resolve_my_bugs") {
|
|
153
|
+
const resp = await zentao.batchResolveMyBugs({
|
|
154
|
+
status: args.status || "active",
|
|
155
|
+
keyword: args.keyword || "",
|
|
156
|
+
limit: args.limit,
|
|
157
|
+
page: args.page,
|
|
158
|
+
productId: args.productId,
|
|
159
|
+
maxItems: args.maxItems,
|
|
160
|
+
assignedTo: args.assignedTo || "",
|
|
161
|
+
resolution: args.resolution || "fixed",
|
|
162
|
+
solution: args.solution || "",
|
|
163
|
+
comment: args.comment || "",
|
|
164
|
+
listPath: args.listPath || "/bugs",
|
|
165
|
+
resolvePath: args.resolvePath || "/bugs/{id}/resolve",
|
|
166
|
+
stopOnError: Boolean(args.stopOnError),
|
|
167
|
+
});
|
|
168
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (toolName === "close_bug") {
|
|
172
|
+
const resp = await zentao.closeBug({
|
|
173
|
+
id: args.id,
|
|
174
|
+
comment: args.comment || "",
|
|
175
|
+
path: args.path || "/bugs/{id}/close",
|
|
176
|
+
});
|
|
177
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (toolName === "verify_bug") {
|
|
181
|
+
const resp = await zentao.verifyBug({
|
|
182
|
+
id: args.id,
|
|
183
|
+
result: args.result || "pass",
|
|
184
|
+
comment: args.comment || "",
|
|
185
|
+
closePath: args.closePath || "/bugs/{id}/close",
|
|
186
|
+
activatePath: args.activatePath || "/bugs/{id}/activate",
|
|
187
|
+
});
|
|
188
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (toolName === "comment_bug") {
|
|
192
|
+
const resp = await zentao.commentBug({
|
|
193
|
+
id: args.id,
|
|
194
|
+
comment: args.comment || "",
|
|
195
|
+
path: args.path || "/bugs/{id}/comment",
|
|
196
|
+
});
|
|
197
|
+
return toMcpTextResult(JSON.stringify(resp, null, 2));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new Error(`Unknown tool: ${rawToolName}`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const errorPayload = {
|
|
203
|
+
ok: false,
|
|
204
|
+
tool: rawToolName,
|
|
205
|
+
message: String(err?.message || err),
|
|
206
|
+
status: err?.status ?? null,
|
|
207
|
+
data: err?.data ?? null,
|
|
208
|
+
hint: "If you see 'Need product id', set env ZENTAO_PRODUCT_ID or pass productId in get_my_bugs.",
|
|
209
|
+
};
|
|
210
|
+
return toMcpTextResult(JSON.stringify(errorPayload, null, 2), { isError: true });
|
|
173
211
|
}
|
|
174
|
-
|
|
175
|
-
throw new Error(`Unknown tool: ${rawToolName}`);
|
|
176
212
|
});
|
|
177
213
|
|
|
178
214
|
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
|
},
|
|
@@ -76,6 +80,7 @@ export const TOOLS = [
|
|
|
76
80
|
properties: {
|
|
77
81
|
id: { type: "number", minimum: 1, description: "Bug ID" },
|
|
78
82
|
resolution: { type: "string", description: "Default fixed" },
|
|
83
|
+
solution: { type: "string", description: "Resolution description (preferred)" },
|
|
79
84
|
comment: { type: "string", description: "Optional resolve comment" },
|
|
80
85
|
path: { type: "string", description: "Optional resolve endpoint template, default /bugs/{id}/resolve" },
|
|
81
86
|
},
|
|
@@ -93,9 +98,11 @@ export const TOOLS = [
|
|
|
93
98
|
keyword: { type: "string", description: "Optional keyword filter before resolve" },
|
|
94
99
|
limit: { type: "number", minimum: 1, maximum: 200, description: "List page size, default 50" },
|
|
95
100
|
page: { type: "number", minimum: 1, description: "Default 1" },
|
|
101
|
+
productId: { type: "number", minimum: 1, description: "Optional product id (for instances requiring product scope)" },
|
|
96
102
|
maxItems: { type: "number", minimum: 1, maximum: 500, description: "Max resolve count, default 50" },
|
|
97
103
|
assignedTo: { type: "string", description: "Optional assignee override" },
|
|
98
104
|
resolution: { type: "string", description: "Default fixed" },
|
|
105
|
+
solution: { type: "string", description: "Resolution description (preferred)" },
|
|
99
106
|
comment: { type: "string", description: "Optional resolve comment" },
|
|
100
107
|
listPath: { type: "string", description: "Optional list endpoint, default /bugs" },
|
|
101
108
|
resolvePath: { type: "string", description: "Optional resolve path template, default /bugs/{id}/resolve" },
|
|
@@ -134,6 +141,20 @@ export const TOOLS = [
|
|
|
134
141
|
additionalProperties: false,
|
|
135
142
|
},
|
|
136
143
|
},
|
|
144
|
+
{
|
|
145
|
+
name: "comment_bug",
|
|
146
|
+
description: "Add comment to one bug by ID.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
id: { type: "number", minimum: 1, description: "Bug ID" },
|
|
151
|
+
comment: { type: "string", description: "Comment content" },
|
|
152
|
+
path: { type: "string", description: "Optional comment endpoint template, default /bugs/{id}/comment" },
|
|
153
|
+
},
|
|
154
|
+
required: ["id", "comment"],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
137
158
|
];
|
|
138
159
|
|
|
139
160
|
export function assertToolArgs(name, args) {
|
|
@@ -152,6 +173,9 @@ export function assertToolArgs(name, args) {
|
|
|
152
173
|
if (args.path !== undefined && typeof args.path !== "string") {
|
|
153
174
|
throw new Error("get_my_bugs.path must be a string");
|
|
154
175
|
}
|
|
176
|
+
if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
|
|
177
|
+
throw new Error("get_my_bugs.productId must be a number >= 1");
|
|
178
|
+
}
|
|
155
179
|
}
|
|
156
180
|
if (name === "get_bug_detail") {
|
|
157
181
|
if (!Number.isFinite(args.id) || Number(args.id) < 1) {
|
|
@@ -168,6 +192,9 @@ export function assertToolArgs(name, args) {
|
|
|
168
192
|
if (args.path !== undefined && typeof args.path !== "string") {
|
|
169
193
|
throw new Error("resolve_bug.path must be a string");
|
|
170
194
|
}
|
|
195
|
+
if (args.solution !== undefined && typeof args.solution !== "string") {
|
|
196
|
+
throw new Error("resolve_bug.solution must be a string");
|
|
197
|
+
}
|
|
171
198
|
}
|
|
172
199
|
if (name === "batch_resolve_my_bugs") {
|
|
173
200
|
if (args.limit !== undefined && (!Number.isFinite(args.limit) || args.limit < 1 || args.limit > 200)) {
|
|
@@ -179,12 +206,18 @@ export function assertToolArgs(name, args) {
|
|
|
179
206
|
if (args.maxItems !== undefined && (!Number.isFinite(args.maxItems) || args.maxItems < 1 || args.maxItems > 500)) {
|
|
180
207
|
throw new Error("batch_resolve_my_bugs.maxItems must be a number between 1 and 500");
|
|
181
208
|
}
|
|
209
|
+
if (args.productId !== undefined && (!Number.isFinite(args.productId) || args.productId < 1)) {
|
|
210
|
+
throw new Error("batch_resolve_my_bugs.productId must be a number >= 1");
|
|
211
|
+
}
|
|
182
212
|
if (args.listPath !== undefined && typeof args.listPath !== "string") {
|
|
183
213
|
throw new Error("batch_resolve_my_bugs.listPath must be a string");
|
|
184
214
|
}
|
|
185
215
|
if (args.resolvePath !== undefined && typeof args.resolvePath !== "string") {
|
|
186
216
|
throw new Error("batch_resolve_my_bugs.resolvePath must be a string");
|
|
187
217
|
}
|
|
218
|
+
if (args.solution !== undefined && typeof args.solution !== "string") {
|
|
219
|
+
throw new Error("batch_resolve_my_bugs.solution must be a string");
|
|
220
|
+
}
|
|
188
221
|
}
|
|
189
222
|
if (name === "close_bug") {
|
|
190
223
|
if (!Number.isFinite(args.id) || Number(args.id) < 1) {
|
|
@@ -211,4 +244,15 @@ export function assertToolArgs(name, args) {
|
|
|
211
244
|
throw new Error("verify_bug.activatePath must be a string");
|
|
212
245
|
}
|
|
213
246
|
}
|
|
247
|
+
if (name === "comment_bug") {
|
|
248
|
+
if (!Number.isFinite(args.id) || Number(args.id) < 1) {
|
|
249
|
+
throw new Error("comment_bug.id must be a number >= 1");
|
|
250
|
+
}
|
|
251
|
+
if (typeof args.comment !== "string" || !args.comment.trim()) {
|
|
252
|
+
throw new Error("comment_bug.comment must be a non-empty string");
|
|
253
|
+
}
|
|
254
|
+
if (args.path !== undefined && typeof args.path !== "string") {
|
|
255
|
+
throw new Error("comment_bug.path must be a string");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
214
258
|
}
|
package/src/zentao.js
CHANGED
|
@@ -31,7 +31,17 @@ function createAbortSignal(timeoutMs) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function createZenTaoClient(config) {
|
|
34
|
-
const {
|
|
34
|
+
const {
|
|
35
|
+
baseUrl,
|
|
36
|
+
apiPrefix,
|
|
37
|
+
tokenPath,
|
|
38
|
+
tokenTtlMs,
|
|
39
|
+
timeoutMs,
|
|
40
|
+
defaultProductId,
|
|
41
|
+
myBugsPath,
|
|
42
|
+
bugsFallbackPaths,
|
|
43
|
+
auth,
|
|
44
|
+
} = config;
|
|
35
45
|
|
|
36
46
|
let cachedToken = "";
|
|
37
47
|
let cachedAt = 0;
|
|
@@ -174,6 +184,44 @@ export function createZenTaoClient(config) {
|
|
|
174
184
|
return String(value || "").trim().toLowerCase();
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
function normalizePositiveInt(value) {
|
|
188
|
+
const parsed = Number(value);
|
|
189
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildProductScopedBugsPath({ productId, path }) {
|
|
193
|
+
const pid = normalizePositiveInt(productId);
|
|
194
|
+
if (!pid) return path || "/bugs";
|
|
195
|
+
|
|
196
|
+
const basePath = path || "/bugs";
|
|
197
|
+
if (basePath.includes("{productId}")) {
|
|
198
|
+
return basePath.replaceAll("{productId}", String(pid));
|
|
199
|
+
}
|
|
200
|
+
if (basePath === "/bugs") {
|
|
201
|
+
return `/products/${pid}/bugs`;
|
|
202
|
+
}
|
|
203
|
+
return basePath;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildMyBugsPath(pathTemplate) {
|
|
207
|
+
const template = String(pathTemplate || "").trim();
|
|
208
|
+
if (!template) return "";
|
|
209
|
+
return template.replaceAll("{account}", encodeURIComponent(auth.account || ""));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isNeedProductIdError(err) {
|
|
213
|
+
const merged = `${String(err?.message || "")} ${JSON.stringify(err?.data || "")}`.toLowerCase();
|
|
214
|
+
return merged.includes("need product id");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildResolutionComment({ solution, comment, resolution }) {
|
|
218
|
+
const normalizedSolution = String(solution || "").trim();
|
|
219
|
+
if (normalizedSolution) return `解决说明:${normalizedSolution}`;
|
|
220
|
+
const normalizedComment = String(comment || "").trim();
|
|
221
|
+
if (normalizedComment) return normalizedComment;
|
|
222
|
+
return `已处理,resolution=${String(resolution || "fixed")}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
177
225
|
function getBugAssignee(bug) {
|
|
178
226
|
return (
|
|
179
227
|
bug?.assignedTo ||
|
|
@@ -232,6 +280,16 @@ export function createZenTaoClient(config) {
|
|
|
232
280
|
return buildBugTransitionPath({ id, path, action: "resolve" });
|
|
233
281
|
}
|
|
234
282
|
|
|
283
|
+
function buildBugCommentPath({ id, path }) {
|
|
284
|
+
const normalizedId = Number(id);
|
|
285
|
+
const basePath = path || "/bugs/{id}/comment";
|
|
286
|
+
if (basePath.includes("{id}")) {
|
|
287
|
+
return basePath.replaceAll("{id}", String(normalizedId));
|
|
288
|
+
}
|
|
289
|
+
const trimmed = basePath.replace(/\/+$/, "");
|
|
290
|
+
return `${trimmed}/${normalizedId}/comment`;
|
|
291
|
+
}
|
|
292
|
+
|
|
235
293
|
function buildBugTransitionPath({ id, path, action }) {
|
|
236
294
|
const normalizedId = Number(id);
|
|
237
295
|
const safeAction = String(action || "").trim();
|
|
@@ -275,19 +333,63 @@ export function createZenTaoClient(config) {
|
|
|
275
333
|
return Array.from(images);
|
|
276
334
|
}
|
|
277
335
|
|
|
278
|
-
async function getMyBugs({
|
|
336
|
+
async function getMyBugs({
|
|
337
|
+
status,
|
|
338
|
+
keyword,
|
|
339
|
+
limit = 20,
|
|
340
|
+
page = 1,
|
|
341
|
+
productId,
|
|
342
|
+
path = "/bugs",
|
|
343
|
+
assignedTo,
|
|
344
|
+
} = {}) {
|
|
279
345
|
const safeLimit = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
280
346
|
const safePage = Math.max(1, Number(page) || 1);
|
|
281
347
|
const assignee = normalizeString(assignedTo) || normalizeString(auth.account);
|
|
348
|
+
const effectiveProductId = normalizePositiveInt(productId) || normalizePositiveInt(defaultProductId);
|
|
349
|
+
const primaryPath = buildProductScopedBugsPath({ productId: effectiveProductId, path });
|
|
350
|
+
const configuredMyBugsPath = buildMyBugsPath(myBugsPath);
|
|
351
|
+
const fallbackPathCandidates = (bugsFallbackPaths && bugsFallbackPaths.length > 0)
|
|
352
|
+
? bugsFallbackPaths
|
|
353
|
+
: ["/my/bug", "/my/bugs"];
|
|
354
|
+
const fallbackPaths = fallbackPathCandidates.map((item) => buildMyBugsPath(item)).filter(Boolean);
|
|
282
355
|
|
|
283
356
|
const query = {
|
|
284
357
|
limit: safeLimit,
|
|
285
358
|
page: safePage,
|
|
286
359
|
assignedTo: assignee || undefined,
|
|
287
360
|
status: status || undefined,
|
|
361
|
+
product: effectiveProductId || undefined,
|
|
288
362
|
};
|
|
289
|
-
const
|
|
290
|
-
|
|
363
|
+
const candidatePaths = [];
|
|
364
|
+
candidatePaths.push(primaryPath);
|
|
365
|
+
if (effectiveProductId) {
|
|
366
|
+
const productPath = `/products/${effectiveProductId}/bugs`;
|
|
367
|
+
if (!candidatePaths.includes(productPath)) candidatePaths.push(productPath);
|
|
368
|
+
}
|
|
369
|
+
if (configuredMyBugsPath && !candidatePaths.includes(configuredMyBugsPath)) {
|
|
370
|
+
candidatePaths.push(configuredMyBugsPath);
|
|
371
|
+
}
|
|
372
|
+
for (const fallback of fallbackPaths) {
|
|
373
|
+
if (!candidatePaths.includes(fallback)) candidatePaths.push(fallback);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let resp = null;
|
|
377
|
+
let usedPath = primaryPath;
|
|
378
|
+
let lastErr = null;
|
|
379
|
+
for (const candidate of candidatePaths) {
|
|
380
|
+
try {
|
|
381
|
+
usedPath = candidate;
|
|
382
|
+
resp = await call({ path: candidate, method: "GET", query });
|
|
383
|
+
break;
|
|
384
|
+
} catch (err) {
|
|
385
|
+
lastErr = err;
|
|
386
|
+
if (!isNeedProductIdError(err)) continue;
|
|
387
|
+
if (candidate !== primaryPath) continue;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!resp) throw lastErr;
|
|
391
|
+
|
|
392
|
+
const bugs = parseBugsFromResponse(resp?.data);
|
|
291
393
|
const filtered = bugs.filter((bug) => matchesBugFilters(bug, { status, keyword, assignee }));
|
|
292
394
|
|
|
293
395
|
return {
|
|
@@ -295,9 +397,10 @@ export function createZenTaoClient(config) {
|
|
|
295
397
|
matched: filtered.length,
|
|
296
398
|
page: safePage,
|
|
297
399
|
limit: safeLimit,
|
|
400
|
+
productId: effectiveProductId,
|
|
298
401
|
assignedTo: assignee,
|
|
299
402
|
bugs: filtered,
|
|
300
|
-
raw: { status: resp
|
|
403
|
+
raw: { status: resp?.status, path: usedPath },
|
|
301
404
|
};
|
|
302
405
|
}
|
|
303
406
|
|
|
@@ -328,21 +431,33 @@ export function createZenTaoClient(config) {
|
|
|
328
431
|
};
|
|
329
432
|
}
|
|
330
433
|
|
|
331
|
-
async function resolveBug({
|
|
434
|
+
async function resolveBug({
|
|
435
|
+
id,
|
|
436
|
+
resolution = "fixed",
|
|
437
|
+
solution = "",
|
|
438
|
+
comment = "",
|
|
439
|
+
path = "/bugs/{id}/resolve",
|
|
440
|
+
} = {}) {
|
|
332
441
|
const bugId = Number(id);
|
|
333
442
|
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
334
443
|
throw new Error("resolveBug requires a valid bug id");
|
|
335
444
|
}
|
|
336
445
|
|
|
337
446
|
const resolvePath = buildBugResolvePath({ id: bugId, path });
|
|
338
|
-
const
|
|
339
|
-
|
|
447
|
+
const resolvedValue = String(resolution || "fixed");
|
|
448
|
+
const resolvedComment = buildResolutionComment({ solution, comment, resolution: resolvedValue });
|
|
449
|
+
const body = {
|
|
450
|
+
resolution: resolvedValue,
|
|
451
|
+
comment: resolvedComment,
|
|
452
|
+
};
|
|
340
453
|
|
|
341
454
|
const resp = await call({ path: resolvePath, method: "POST", body });
|
|
342
455
|
return {
|
|
343
456
|
id: bugId,
|
|
344
457
|
resolved: true,
|
|
345
|
-
resolution:
|
|
458
|
+
resolution: resolvedValue,
|
|
459
|
+
solution: String(solution || "").trim(),
|
|
460
|
+
comment: resolvedComment,
|
|
346
461
|
raw: { status: resp.status, data: resp.data },
|
|
347
462
|
};
|
|
348
463
|
}
|
|
@@ -416,14 +531,51 @@ export function createZenTaoClient(config) {
|
|
|
416
531
|
};
|
|
417
532
|
}
|
|
418
533
|
|
|
534
|
+
async function commentBug({ id, comment, path = "/bugs/{id}/comment" } = {}) {
|
|
535
|
+
const bugId = Number(id);
|
|
536
|
+
if (!Number.isFinite(bugId) || bugId < 1) {
|
|
537
|
+
throw new Error("commentBug requires a valid bug id");
|
|
538
|
+
}
|
|
539
|
+
const text = String(comment || "").trim();
|
|
540
|
+
if (!text) {
|
|
541
|
+
throw new Error("commentBug requires non-empty comment");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const primaryPath = buildBugCommentPath({ id: bugId, path });
|
|
545
|
+
const body = { comment: text };
|
|
546
|
+
try {
|
|
547
|
+
const resp = await call({ path: primaryPath, method: "POST", body });
|
|
548
|
+
return {
|
|
549
|
+
id: bugId,
|
|
550
|
+
commented: true,
|
|
551
|
+
comment: text,
|
|
552
|
+
raw: { status: resp.status, path: primaryPath, data: resp.data },
|
|
553
|
+
};
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const fallbackPath = primaryPath.replace(/\/comment$/, "/comments");
|
|
556
|
+
if (fallbackPath !== primaryPath && Number(err?.status) === 404) {
|
|
557
|
+
const resp = await call({ path: fallbackPath, method: "POST", body });
|
|
558
|
+
return {
|
|
559
|
+
id: bugId,
|
|
560
|
+
commented: true,
|
|
561
|
+
comment: text,
|
|
562
|
+
raw: { status: resp.status, path: fallbackPath, data: resp.data },
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
419
569
|
async function batchResolveMyBugs({
|
|
420
570
|
status = "active",
|
|
421
571
|
keyword = "",
|
|
422
572
|
limit = 50,
|
|
423
573
|
page = 1,
|
|
574
|
+
productId,
|
|
424
575
|
maxItems = 50,
|
|
425
576
|
assignedTo = "",
|
|
426
577
|
resolution = "fixed",
|
|
578
|
+
solution = "",
|
|
427
579
|
comment = "",
|
|
428
580
|
listPath = "/bugs",
|
|
429
581
|
resolvePath = "/bugs/{id}/resolve",
|
|
@@ -435,6 +587,7 @@ export function createZenTaoClient(config) {
|
|
|
435
587
|
keyword,
|
|
436
588
|
limit,
|
|
437
589
|
page,
|
|
590
|
+
productId,
|
|
438
591
|
path: listPath,
|
|
439
592
|
assignedTo,
|
|
440
593
|
});
|
|
@@ -454,6 +607,7 @@ export function createZenTaoClient(config) {
|
|
|
454
607
|
const result = await resolveBug({
|
|
455
608
|
id: bugId,
|
|
456
609
|
resolution,
|
|
610
|
+
solution,
|
|
457
611
|
comment,
|
|
458
612
|
path: resolvePath,
|
|
459
613
|
});
|
|
@@ -499,6 +653,7 @@ export function createZenTaoClient(config) {
|
|
|
499
653
|
resolveBug,
|
|
500
654
|
closeBug,
|
|
501
655
|
verifyBug,
|
|
656
|
+
commentBug,
|
|
502
657
|
batchResolveMyBugs,
|
|
503
658
|
};
|
|
504
659
|
}
|