@bangdao-ai/zentao-mcp 1.1.2 → 1.1.4
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/index.js +102 -23
- package/package.json +3 -2
- package/src/mcp_server.py +1 -20
- package/src/zentao_nexus.py +81 -123
- package/src/__pycache__/mcp_server.cpython-313.pyc +0 -0
- package/src/__pycache__/zentao_nexus.cpython-313.pyc +0 -0
package/index.js
CHANGED
|
@@ -4,22 +4,70 @@
|
|
|
4
4
|
* 通过 npm/npx 调用 Python MCP 服务器
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const { spawn } = require('child_process');
|
|
7
|
+
const { spawn, execSync } = require('child_process');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// 检测操作系统平台
|
|
13
|
+
const isWindows = process.platform === 'win32';
|
|
14
|
+
|
|
15
|
+
// 获取 Python 可执行文件路径(跨平台)
|
|
12
16
|
function findPython() {
|
|
13
17
|
const pythonCommands = ['python3', 'python'];
|
|
14
18
|
|
|
19
|
+
// 优先检查环境变量
|
|
20
|
+
const pythonEnv = process.env.PYTHON || process.env.PYTHON3;
|
|
21
|
+
if (pythonEnv && fs.existsSync(pythonEnv)) {
|
|
22
|
+
return pythonEnv;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 尝试直接执行命令来查找 Python
|
|
15
26
|
for (const cmd of pythonCommands) {
|
|
16
27
|
try {
|
|
17
|
-
|
|
28
|
+
let result;
|
|
29
|
+
if (isWindows) {
|
|
30
|
+
// Windows 使用 where 命令
|
|
31
|
+
try {
|
|
32
|
+
result = execSync(`where ${cmd}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
33
|
+
// where 可能返回多行,取第一行
|
|
34
|
+
if (result) {
|
|
35
|
+
const lines = result.split('\n').filter(line => line.trim());
|
|
36
|
+
if (lines.length > 0) {
|
|
37
|
+
result = lines[0].trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// where 命令失败,尝试直接执行
|
|
42
|
+
try {
|
|
43
|
+
execSync(`${cmd} --version`, { encoding: 'utf-8', stdio: ['ignore', 'ignore', 'ignore'] });
|
|
44
|
+
// 如果能执行,返回命令名(spawn 会自动查找)
|
|
45
|
+
return cmd;
|
|
46
|
+
} catch (e2) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Unix/Linux/Mac 使用 which 命令
|
|
52
|
+
result = execSync(`which ${cmd}`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
if (result) {
|
|
19
56
|
return result;
|
|
20
57
|
}
|
|
21
58
|
} catch (e) {
|
|
22
|
-
//
|
|
59
|
+
// 继续尝试下一个命令
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 如果都失败了,尝试直接使用命令名(让系统 PATH 处理)
|
|
65
|
+
for (const cmd of pythonCommands) {
|
|
66
|
+
try {
|
|
67
|
+
execSync(`${cmd} --version`, { encoding: 'utf-8', stdio: ['ignore', 'ignore', 'ignore'] });
|
|
68
|
+
return cmd;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
continue;
|
|
23
71
|
}
|
|
24
72
|
}
|
|
25
73
|
|
|
@@ -66,10 +114,17 @@ function main() {
|
|
|
66
114
|
// 启动 Python MCP 服务器
|
|
67
115
|
// 注意:MCP 服务器通过 stdio 通信,不会输出到控制台
|
|
68
116
|
// 这是正常行为,服务器会等待 MCP 协议消息
|
|
69
|
-
const
|
|
117
|
+
const spawnOptions = {
|
|
70
118
|
stdio: 'inherit',
|
|
71
119
|
env: process.env
|
|
72
|
-
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Windows 兼容性:如果 Python 路径包含空格或特殊字符,可能需要 shell
|
|
123
|
+
if (isWindows && (python.includes(' ') || python.includes('(') || python.includes(')'))) {
|
|
124
|
+
spawnOptions.shell = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const child = spawn(python, [scriptPath], spawnOptions);
|
|
73
128
|
|
|
74
129
|
// 处理退出
|
|
75
130
|
child.on('exit', (code) => {
|
|
@@ -91,27 +146,51 @@ function main() {
|
|
|
91
146
|
process.exit(1);
|
|
92
147
|
});
|
|
93
148
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
child.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
child.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
// 处理信号(Windows 兼容性)
|
|
150
|
+
if (!isWindows) {
|
|
151
|
+
process.on('SIGINT', () => {
|
|
152
|
+
if (child && !child.killed) {
|
|
153
|
+
child.kill('SIGINT');
|
|
154
|
+
}
|
|
155
|
+
process.exit(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
process.on('SIGTERM', () => {
|
|
159
|
+
if (child && !child.killed) {
|
|
160
|
+
child.kill('SIGTERM');
|
|
161
|
+
}
|
|
162
|
+
process.exit(0);
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// Windows 使用不同的信号处理
|
|
166
|
+
process.on('SIGINT', () => {
|
|
167
|
+
if (child && !child.killed) {
|
|
168
|
+
child.kill();
|
|
169
|
+
}
|
|
170
|
+
process.exit(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
process.on('SIGTERM', () => {
|
|
174
|
+
if (child && !child.killed) {
|
|
175
|
+
child.kill();
|
|
176
|
+
}
|
|
177
|
+
process.exit(0);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
108
180
|
|
|
109
181
|
} catch (error) {
|
|
110
182
|
console.error('错误:', error.message);
|
|
111
183
|
console.error('\n故障排查:');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
184
|
+
if (isWindows) {
|
|
185
|
+
console.error('1. 检查 Python 是否安装: python --version 或 python3 --version');
|
|
186
|
+
console.error('2. 检查脚本是否存在: dir src\\mcp_server.py');
|
|
187
|
+
console.error('3. 检查依赖是否安装: pip list | findstr mcp');
|
|
188
|
+
console.error('4. 确保 Python 已添加到系统 PATH 环境变量');
|
|
189
|
+
} else {
|
|
190
|
+
console.error('1. 检查 Python 是否安装: python3 --version');
|
|
191
|
+
console.error('2. 检查脚本是否存在: ls -la src/mcp_server.py');
|
|
192
|
+
console.error('3. 检查依赖是否安装: pip3 list | grep mcp');
|
|
193
|
+
}
|
|
115
194
|
process.exit(1);
|
|
116
195
|
}
|
|
117
196
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bangdao-ai/zentao-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "禅道Bug管理系统MCP工具",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"files": [
|
|
21
21
|
"index.js",
|
|
22
|
-
"src/",
|
|
22
|
+
"src/mcp_server.py",
|
|
23
|
+
"src/zentao_nexus.py",
|
|
23
24
|
"requirements.txt",
|
|
24
25
|
"README.md",
|
|
25
26
|
"package.json"
|
package/src/mcp_server.py
CHANGED
|
@@ -203,22 +203,10 @@ class ZenTaoMCPServer:
|
|
|
203
203
|
"enum": ["fixed", "bydesign", "reqchange", "duplicate", "external", "notrepro", "postponed", "willnotfix", "tostory", "history", "configchange"]
|
|
204
204
|
},
|
|
205
205
|
"resolved_by": {"type": "string", "description": "解决人(姓名或工号)"},
|
|
206
|
-
"resolved_build": {"type": "string", "description": "解决版本ID
|
|
206
|
+
"resolved_build": {"type": "string", "description": "解决版本ID(如果解决方案是fixed时,解决版本必传)"}
|
|
207
207
|
},
|
|
208
208
|
"required": ["product_id", "bug_ids", "resolution", "resolved_by"]
|
|
209
209
|
}
|
|
210
|
-
),
|
|
211
|
-
Tool(
|
|
212
|
-
name="close_bug",
|
|
213
|
-
description="关闭BUG",
|
|
214
|
-
inputSchema={
|
|
215
|
-
"type": "object",
|
|
216
|
-
"properties": {
|
|
217
|
-
"bug_id": {"type": "string", "description": "BUG ID"},
|
|
218
|
-
"comment": {"type": "string", "description": "关闭备注(可选,默认AI关闭)"}
|
|
219
|
-
},
|
|
220
|
-
"required": ["bug_id"]
|
|
221
|
-
}
|
|
222
210
|
)
|
|
223
211
|
]
|
|
224
212
|
|
|
@@ -448,13 +436,6 @@ class ZenTaoMCPServer:
|
|
|
448
436
|
result = self.nexus.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
449
437
|
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
450
438
|
|
|
451
|
-
elif name == "close_bug":
|
|
452
|
-
bug_id = arguments["bug_id"]
|
|
453
|
-
comment = arguments.get("comment", "AI关闭")
|
|
454
|
-
|
|
455
|
-
result = self.nexus.close_bug(bug_id, comment)
|
|
456
|
-
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
457
|
-
|
|
458
439
|
else:
|
|
459
440
|
raise ValueError(f"未知的工具: {name}")
|
|
460
441
|
|
package/src/zentao_nexus.py
CHANGED
|
@@ -133,7 +133,7 @@ class ZenTaoAPIClient:
|
|
|
133
133
|
md5_hash.update(token_string.encode('utf-8'))
|
|
134
134
|
return md5_hash.hexdigest().lower()
|
|
135
135
|
|
|
136
|
-
def _call_new_api(self, function_name, params=None, form_data=None, files=None
|
|
136
|
+
def _call_new_api(self, function_name, params=None, form_data=None, files=None):
|
|
137
137
|
"""
|
|
138
138
|
调用新的API接口(使用code、time、token认证)
|
|
139
139
|
|
|
@@ -142,7 +142,6 @@ class ZenTaoAPIClient:
|
|
|
142
142
|
params: URL参数(字典)
|
|
143
143
|
form_data: POST form-data数据(字典或列表,列表格式用于数组参数)
|
|
144
144
|
files: 文件上传(字典,用于文件上传接口)
|
|
145
|
-
m_param: m参数值(默认allneedlist,某些接口可能需要其他值如bug)
|
|
146
145
|
|
|
147
146
|
Returns:
|
|
148
147
|
dict: API响应结果
|
|
@@ -155,7 +154,7 @@ class ZenTaoAPIClient:
|
|
|
155
154
|
'code': self.code,
|
|
156
155
|
'time': current_timestamp,
|
|
157
156
|
'token': token,
|
|
158
|
-
'm':
|
|
157
|
+
'm': 'allneedlist',
|
|
159
158
|
'f': function_name
|
|
160
159
|
}
|
|
161
160
|
|
|
@@ -167,68 +166,67 @@ class ZenTaoAPIClient:
|
|
|
167
166
|
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
print(f" {key}: <文件对象>")
|
|
214
|
-
else:
|
|
215
|
-
print(f" {key}: {value}")
|
|
169
|
+
# 打印请求信息
|
|
170
|
+
print("\n" + "=" * 80)
|
|
171
|
+
print("【禅道API请求信息】")
|
|
172
|
+
print("=" * 80)
|
|
173
|
+
print(f"API URL: {api_url}")
|
|
174
|
+
print(f"\n【Headers】")
|
|
175
|
+
for key, value in headers.items():
|
|
176
|
+
print(f" {key}: {value}")
|
|
177
|
+
|
|
178
|
+
print(f"\n【Token生成信息】")
|
|
179
|
+
print(f" code: {self.code}")
|
|
180
|
+
print(f" key: {self.key[:10]}...{self.key[-10:] if len(self.key) > 20 else self.key}") # 只显示部分key,保护隐私
|
|
181
|
+
print(f" time: {current_timestamp}")
|
|
182
|
+
print(f" token生成公式: md5(code + key + time)")
|
|
183
|
+
print(f" token生成字符串: {self.code}{self.key}{current_timestamp}")
|
|
184
|
+
print(f" token: {token}")
|
|
185
|
+
|
|
186
|
+
print(f"\n【URL参数】")
|
|
187
|
+
for key, value in url_params.items():
|
|
188
|
+
print(f" {key}: {value}")
|
|
189
|
+
|
|
190
|
+
# 处理form_data用于显示
|
|
191
|
+
display_form_data = form_data
|
|
192
|
+
if isinstance(form_data, dict):
|
|
193
|
+
# 检查是否有数组类型的值
|
|
194
|
+
processed_data = []
|
|
195
|
+
for key, value in form_data.items():
|
|
196
|
+
if isinstance(value, (list, tuple)):
|
|
197
|
+
for item in value:
|
|
198
|
+
processed_data.append((key, str(item)))
|
|
199
|
+
else:
|
|
200
|
+
processed_data.append((key, str(value)))
|
|
201
|
+
display_form_data = processed_data
|
|
202
|
+
|
|
203
|
+
if display_form_data:
|
|
204
|
+
print(f"\n【Body (form-data)】")
|
|
205
|
+
if isinstance(display_form_data, list):
|
|
206
|
+
for item in display_form_data:
|
|
207
|
+
if isinstance(item, tuple):
|
|
208
|
+
key, value = item
|
|
209
|
+
# 如果是文件,只显示文件名
|
|
210
|
+
if hasattr(value, 'read'):
|
|
211
|
+
print(f" {key}: <文件对象>")
|
|
216
212
|
else:
|
|
217
|
-
print(f" {
|
|
218
|
-
elif isinstance(display_form_data, dict):
|
|
219
|
-
for key, value in display_form_data.items():
|
|
220
|
-
print(f" {key}: {value}")
|
|
221
|
-
|
|
222
|
-
if files:
|
|
223
|
-
print(f"\n【Files】")
|
|
224
|
-
for key, file_info in files.items():
|
|
225
|
-
if isinstance(file_info, tuple):
|
|
226
|
-
filename = file_info[0] if len(file_info) > 0 else "unknown"
|
|
227
|
-
print(f" {key}: {filename}")
|
|
213
|
+
print(f" {key}: {value}")
|
|
228
214
|
else:
|
|
229
|
-
print(f" {
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
print(f" {item}")
|
|
216
|
+
elif isinstance(display_form_data, dict):
|
|
217
|
+
for key, value in display_form_data.items():
|
|
218
|
+
print(f" {key}: {value}")
|
|
219
|
+
|
|
220
|
+
if files:
|
|
221
|
+
print(f"\n【Files】")
|
|
222
|
+
for key, file_info in files.items():
|
|
223
|
+
if isinstance(file_info, tuple):
|
|
224
|
+
filename = file_info[0] if len(file_info) > 0 else "unknown"
|
|
225
|
+
print(f" {key}: {filename}")
|
|
226
|
+
else:
|
|
227
|
+
print(f" {key}: {file_info}")
|
|
228
|
+
|
|
229
|
+
print("=" * 80)
|
|
232
230
|
|
|
233
231
|
try:
|
|
234
232
|
if files:
|
|
@@ -253,33 +251,24 @@ class ZenTaoAPIClient:
|
|
|
253
251
|
else:
|
|
254
252
|
response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
|
|
255
253
|
|
|
256
|
-
#
|
|
254
|
+
# 打印响应信息
|
|
255
|
+
print("\n【禅道API响应信息】")
|
|
256
|
+
print("=" * 80)
|
|
257
|
+
print(f"状态码: {response.status_code}")
|
|
258
|
+
print(f"响应Headers:")
|
|
259
|
+
for key, value in response.headers.items():
|
|
260
|
+
print(f" {key}: {value}")
|
|
261
|
+
|
|
257
262
|
try:
|
|
258
263
|
response_json = response.json()
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
print("=" * 80)
|
|
263
|
-
print(f"状态码: {response.status_code}")
|
|
264
|
-
print(f"响应Headers:")
|
|
265
|
-
for key, value in response.headers.items():
|
|
266
|
-
print(f" {key}: {value}")
|
|
267
|
-
print(f"\n响应Body (JSON):")
|
|
268
|
-
print(json.dumps(response_json, ensure_ascii=False, indent=2))
|
|
269
|
-
print("=" * 80 + "\n")
|
|
264
|
+
print(f"\n响应Body (JSON):")
|
|
265
|
+
print(json.dumps(response_json, ensure_ascii=False, indent=2))
|
|
266
|
+
print("=" * 80 + "\n")
|
|
270
267
|
return response_json
|
|
271
268
|
except json.JSONDecodeError:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
print("=" * 80)
|
|
276
|
-
print(f"状态码: {response.status_code}")
|
|
277
|
-
print(f"响应Headers:")
|
|
278
|
-
for key, value in response.headers.items():
|
|
279
|
-
print(f" {key}: {value}")
|
|
280
|
-
print(f"\n响应Body (非JSON):")
|
|
281
|
-
print(response.text[:1000]) # 只显示前1000个字符
|
|
282
|
-
print("=" * 80 + "\n")
|
|
269
|
+
print(f"\n响应Body (非JSON):")
|
|
270
|
+
print(response.text[:1000]) # 只显示前1000个字符
|
|
271
|
+
print("=" * 80 + "\n")
|
|
283
272
|
return {
|
|
284
273
|
"status": 0,
|
|
285
274
|
"message": "fail",
|
|
@@ -288,11 +277,9 @@ class ZenTaoAPIClient:
|
|
|
288
277
|
"raw_response": response.text
|
|
289
278
|
}
|
|
290
279
|
except requests.exceptions.RequestException as e:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
print(f"错误信息: {str(e)}")
|
|
295
|
-
print("=" * 80 + "\n")
|
|
280
|
+
print(f"\n【请求异常】")
|
|
281
|
+
print(f"错误信息: {str(e)}")
|
|
282
|
+
print("=" * 80 + "\n")
|
|
296
283
|
return {
|
|
297
284
|
"status": 0,
|
|
298
285
|
"message": "fail",
|
|
@@ -401,13 +388,13 @@ class ZenTaoAPIClient:
|
|
|
401
388
|
history: 历史遗留
|
|
402
389
|
configchange: 调整配置
|
|
403
390
|
resolved_by: 解决人(必传,姓名或工号)
|
|
404
|
-
resolved_build: 解决版本ID
|
|
391
|
+
resolved_build: 解决版本ID(如果解决方案是fixed时,默认使用"trunk")
|
|
405
392
|
|
|
406
393
|
Returns:
|
|
407
394
|
dict: 解决结果
|
|
408
395
|
"""
|
|
409
|
-
#
|
|
410
|
-
if not resolved_build:
|
|
396
|
+
# 如果 resolution 是 "fixed" 且没有提供 resolved_build,使用默认值 "trunk"
|
|
397
|
+
if resolution == 'fixed' and not resolved_build:
|
|
411
398
|
resolved_build = "trunk"
|
|
412
399
|
|
|
413
400
|
params = {'productID': product_id}
|
|
@@ -427,31 +414,6 @@ class ZenTaoAPIClient:
|
|
|
427
414
|
|
|
428
415
|
return self._call_new_api('resolveBug', params=params, form_data=form_data)
|
|
429
416
|
|
|
430
|
-
def close_bug(self, bug_id, comment="AI关闭"):
|
|
431
|
-
"""
|
|
432
|
-
关闭BUG
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
bug_id: BUG ID(必传)
|
|
436
|
-
comment: 关闭备注(可选,默认"AI关闭")
|
|
437
|
-
|
|
438
|
-
Returns:
|
|
439
|
-
dict: 关闭结果
|
|
440
|
-
"""
|
|
441
|
-
params = {'bugID': bug_id}
|
|
442
|
-
form_data = {'comment': comment}
|
|
443
|
-
result = self._call_new_api('close', params=params, form_data=form_data, m_param='bug')
|
|
444
|
-
|
|
445
|
-
# 如果返回的是"Array"字符串,表示操作成功(禅道某些接口的特殊返回格式)
|
|
446
|
-
if isinstance(result, dict) and result.get("raw_response") == "Array":
|
|
447
|
-
return {
|
|
448
|
-
"status": 1,
|
|
449
|
-
"message": "success",
|
|
450
|
-
"info": "success"
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return result
|
|
454
|
-
|
|
455
417
|
def create_bug_by_api(self, bug_data, use_form_data=True):
|
|
456
418
|
"""通过API创建bug"""
|
|
457
419
|
return self._call_new_api('createbugbyapi', form_data=bug_data)
|
|
@@ -921,10 +883,6 @@ class ZenTaoNexus:
|
|
|
921
883
|
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
922
884
|
"""批量解决BUG"""
|
|
923
885
|
return self.client.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
924
|
-
|
|
925
|
-
def close_bug(self, bug_id, comment="AI关闭"):
|
|
926
|
-
"""关闭BUG"""
|
|
927
|
-
return self.client.close_bug(bug_id, comment)
|
|
928
886
|
|
|
929
887
|
|
|
930
888
|
# 便捷函数
|
|
Binary file
|
|
Binary file
|