@bangdao-ai/zentao-mcp 1.1.0 → 1.1.1
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/README.md +3 -1
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/__pycache__/zentao_nexus.cpython-313.pyc +0 -0
- package/src/mcp_server.py +22 -2
- package/src/zentao_nexus.py +191 -149
package/README.md
CHANGED
|
@@ -141,6 +141,7 @@ pip3 install -r requirements.txt
|
|
|
141
141
|
"env": {
|
|
142
142
|
"ZENTAO_PRODUCT_ID": "365",
|
|
143
143
|
"ZENTAO_OPENED_BY": "69610",
|
|
144
|
+
"KEY": "643f0f490d1ea0d47520dd270989c99a",
|
|
144
145
|
"ZENTAO_KEYWORDS": "AI",
|
|
145
146
|
"ZENTAO_TITLE_PREFIX": ""
|
|
146
147
|
}
|
|
@@ -151,7 +152,8 @@ pip3 install -r requirements.txt
|
|
|
151
152
|
|
|
152
153
|
**配置说明**:
|
|
153
154
|
- `ZENTAO_PRODUCT_ID`(必需):产品ID
|
|
154
|
-
- `ZENTAO_OPENED_BY`(必需):创建人和指派人ID
|
|
155
|
+
- `ZENTAO_OPENED_BY`(必需):创建人和指派人ID(同一个人),作为API认证的code参数
|
|
156
|
+
- `KEY`(必需):API认证密钥,作为API认证的key参数
|
|
155
157
|
- `ZENTAO_KEYWORDS`(可选):关键词,默认为空
|
|
156
158
|
- `ZENTAO_TITLE_PREFIX`(可选):标题前缀,默认为空
|
|
157
159
|
|
package/package.json
CHANGED
package/requirements.txt
CHANGED
|
Binary file
|
package/src/mcp_server.py
CHANGED
|
@@ -203,10 +203,22 @@ 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(可选,如果未提供则默认使用trunk)"}
|
|
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
|
+
}
|
|
210
222
|
)
|
|
211
223
|
]
|
|
212
224
|
|
|
@@ -230,7 +242,8 @@ class ZenTaoMCPServer:
|
|
|
230
242
|
"product_id": product_id,
|
|
231
243
|
"opened_by": opened_by,
|
|
232
244
|
"keywords": os.getenv("ZENTAO_KEYWORDS", ""),
|
|
233
|
-
"title_prefix": os.getenv("ZENTAO_TITLE_PREFIX", "")
|
|
245
|
+
"title_prefix": os.getenv("ZENTAO_TITLE_PREFIX", ""),
|
|
246
|
+
"role_type": os.getenv("ZENTAO_ROLE_TYPE", "")
|
|
234
247
|
}
|
|
235
248
|
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
236
249
|
json.dump(temp_config, temp_file, ensure_ascii=False)
|
|
@@ -435,6 +448,13 @@ class ZenTaoMCPServer:
|
|
|
435
448
|
result = self.nexus.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
436
449
|
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
437
450
|
|
|
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
|
+
|
|
438
458
|
else:
|
|
439
459
|
raise ValueError(f"未知的工具: {name}")
|
|
440
460
|
|
package/src/zentao_nexus.py
CHANGED
|
@@ -104,13 +104,6 @@ class ZenTaoAPIClient:
|
|
|
104
104
|
# 写死的配置
|
|
105
105
|
BASE_URL = "https://zentao.bangdao-tech.com"
|
|
106
106
|
TEST_BASE_URL = "https://test-zendao.bangdao-tech.com"
|
|
107
|
-
ACCOUNT = "KUBETEST"
|
|
108
|
-
SECRET_KEY = "aa287b7b5a4ef5d051f82fb1825ca1ac"
|
|
109
|
-
CASE_SECRET_KEY = "db1d522a190f1135c6e7c324bd337fda"
|
|
110
|
-
CASE_ACCOUNT = "AUTOTEST"
|
|
111
|
-
# 新API的配置
|
|
112
|
-
API_KEY = "1acca81c8a4d131cf7c86e772fa64351"
|
|
113
|
-
API_CODE = "test"
|
|
114
107
|
|
|
115
108
|
def __init__(self, silent=True, use_test_env=False):
|
|
116
109
|
"""
|
|
@@ -121,34 +114,26 @@ class ZenTaoAPIClient:
|
|
|
121
114
|
use_test_env (bool): 是否使用测试环境
|
|
122
115
|
"""
|
|
123
116
|
self.base_url = self.TEST_BASE_URL if use_test_env else self.BASE_URL
|
|
124
|
-
self.account = self.ACCOUNT
|
|
125
|
-
self.secret_key = self.SECRET_KEY
|
|
126
|
-
self.case_secret_key = self.CASE_SECRET_KEY
|
|
127
117
|
self.silent = silent
|
|
128
118
|
self.use_test_env = use_test_env
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"""生成用例创建用的BDTOKEN"""
|
|
139
|
-
token_string = f"accountfmtamp{timestamp}{self.case_secret_key}"
|
|
140
|
-
md5_hash = hashlib.md5()
|
|
141
|
-
md5_hash.update(token_string.encode('utf-8'))
|
|
142
|
-
return md5_hash.hexdigest()
|
|
119
|
+
|
|
120
|
+
# 从环境变量读取 code 和 key
|
|
121
|
+
self.code = os.getenv("ZENTAO_OPENED_BY") or os.getenv("OPENED_BY")
|
|
122
|
+
self.key = os.getenv("ZENTAO_KEY") or os.getenv("KEY")
|
|
123
|
+
|
|
124
|
+
if not self.code:
|
|
125
|
+
raise ValueError("未设置环境变量 ZENTAO_OPENED_BY 或 OPENED_BY(用于 code)")
|
|
126
|
+
if not self.key:
|
|
127
|
+
raise ValueError("未设置环境变量 ZENTAO_KEY 或 KEY(用于 key)")
|
|
143
128
|
|
|
144
129
|
def generate_api_token(self, timestamp):
|
|
145
|
-
"""
|
|
146
|
-
token_string = f"{self.
|
|
130
|
+
"""生成统一API的token(md5(code + key + time),32位小写)"""
|
|
131
|
+
token_string = f"{self.code}{self.key}{timestamp}"
|
|
147
132
|
md5_hash = hashlib.md5()
|
|
148
133
|
md5_hash.update(token_string.encode('utf-8'))
|
|
149
134
|
return md5_hash.hexdigest().lower()
|
|
150
135
|
|
|
151
|
-
def _call_new_api(self, function_name, params=None, form_data=None):
|
|
136
|
+
def _call_new_api(self, function_name, params=None, form_data=None, files=None, m_param='allneedlist'):
|
|
152
137
|
"""
|
|
153
138
|
调用新的API接口(使用code、time、token认证)
|
|
154
139
|
|
|
@@ -156,6 +141,8 @@ class ZenTaoAPIClient:
|
|
|
156
141
|
function_name: API函数名(f参数)
|
|
157
142
|
params: URL参数(字典)
|
|
158
143
|
form_data: POST form-data数据(字典或列表,列表格式用于数组参数)
|
|
144
|
+
files: 文件上传(字典,用于文件上传接口)
|
|
145
|
+
m_param: m参数值(默认allneedlist,某些接口可能需要其他值如bug)
|
|
159
146
|
|
|
160
147
|
Returns:
|
|
161
148
|
dict: API响应结果
|
|
@@ -165,10 +152,10 @@ class ZenTaoAPIClient:
|
|
|
165
152
|
|
|
166
153
|
api_url = f"{self.base_url}/zentaopms/www/api.php"
|
|
167
154
|
url_params = {
|
|
168
|
-
'code': self.
|
|
155
|
+
'code': self.code,
|
|
169
156
|
'time': current_timestamp,
|
|
170
157
|
'token': token,
|
|
171
|
-
'm':
|
|
158
|
+
'm': m_param,
|
|
172
159
|
'f': function_name
|
|
173
160
|
}
|
|
174
161
|
|
|
@@ -180,8 +167,73 @@ class ZenTaoAPIClient:
|
|
|
180
167
|
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
181
168
|
}
|
|
182
169
|
|
|
170
|
+
# 打印请求信息
|
|
171
|
+
print("\n" + "=" * 80)
|
|
172
|
+
print("【禅道API请求信息】")
|
|
173
|
+
print("=" * 80)
|
|
174
|
+
print(f"API URL: {api_url}")
|
|
175
|
+
print(f"\n【Headers】")
|
|
176
|
+
for key, value in headers.items():
|
|
177
|
+
print(f" {key}: {value}")
|
|
178
|
+
|
|
179
|
+
print(f"\n【Token生成信息】")
|
|
180
|
+
print(f" code: {self.code}")
|
|
181
|
+
print(f" key: {self.key[:10]}...{self.key[-10:] if len(self.key) > 20 else self.key}") # 只显示部分key,保护隐私
|
|
182
|
+
print(f" time: {current_timestamp}")
|
|
183
|
+
print(f" token生成公式: md5(code + key + time)")
|
|
184
|
+
print(f" token生成字符串: {self.code}{self.key}{current_timestamp}")
|
|
185
|
+
print(f" token: {token}")
|
|
186
|
+
|
|
187
|
+
print(f"\n【URL参数】")
|
|
188
|
+
for key, value in url_params.items():
|
|
189
|
+
print(f" {key}: {value}")
|
|
190
|
+
|
|
191
|
+
# 处理form_data用于显示
|
|
192
|
+
display_form_data = form_data
|
|
193
|
+
if isinstance(form_data, dict):
|
|
194
|
+
# 检查是否有数组类型的值
|
|
195
|
+
processed_data = []
|
|
196
|
+
for key, value in form_data.items():
|
|
197
|
+
if isinstance(value, (list, tuple)):
|
|
198
|
+
for item in value:
|
|
199
|
+
processed_data.append((key, str(item)))
|
|
200
|
+
else:
|
|
201
|
+
processed_data.append((key, str(value)))
|
|
202
|
+
display_form_data = processed_data
|
|
203
|
+
|
|
204
|
+
if display_form_data:
|
|
205
|
+
print(f"\n【Body (form-data)】")
|
|
206
|
+
if isinstance(display_form_data, list):
|
|
207
|
+
for item in display_form_data:
|
|
208
|
+
if isinstance(item, tuple):
|
|
209
|
+
key, value = item
|
|
210
|
+
# 如果是文件,只显示文件名
|
|
211
|
+
if hasattr(value, 'read'):
|
|
212
|
+
print(f" {key}: <文件对象>")
|
|
213
|
+
else:
|
|
214
|
+
print(f" {key}: {value}")
|
|
215
|
+
else:
|
|
216
|
+
print(f" {item}")
|
|
217
|
+
elif isinstance(display_form_data, dict):
|
|
218
|
+
for key, value in display_form_data.items():
|
|
219
|
+
print(f" {key}: {value}")
|
|
220
|
+
|
|
221
|
+
if files:
|
|
222
|
+
print(f"\n【Files】")
|
|
223
|
+
for key, file_info in files.items():
|
|
224
|
+
if isinstance(file_info, tuple):
|
|
225
|
+
filename = file_info[0] if len(file_info) > 0 else "unknown"
|
|
226
|
+
print(f" {key}: {filename}")
|
|
227
|
+
else:
|
|
228
|
+
print(f" {key}: {file_info}")
|
|
229
|
+
|
|
230
|
+
print("=" * 80)
|
|
231
|
+
|
|
183
232
|
try:
|
|
184
|
-
if
|
|
233
|
+
if files:
|
|
234
|
+
# 文件上传
|
|
235
|
+
response = requests.post(api_url, params=url_params, headers=headers, data=form_data, files=files, timeout=30)
|
|
236
|
+
elif form_data:
|
|
185
237
|
# 如果form_data是列表,直接使用(用于数组参数)
|
|
186
238
|
# 如果是字典,需要检查是否有数组类型的值
|
|
187
239
|
if isinstance(form_data, dict):
|
|
@@ -200,9 +252,24 @@ class ZenTaoAPIClient:
|
|
|
200
252
|
else:
|
|
201
253
|
response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
|
|
202
254
|
|
|
255
|
+
# 打印响应信息
|
|
256
|
+
print("\n【禅道API响应信息】")
|
|
257
|
+
print("=" * 80)
|
|
258
|
+
print(f"状态码: {response.status_code}")
|
|
259
|
+
print(f"响应Headers:")
|
|
260
|
+
for key, value in response.headers.items():
|
|
261
|
+
print(f" {key}: {value}")
|
|
262
|
+
|
|
203
263
|
try:
|
|
204
|
-
|
|
264
|
+
response_json = response.json()
|
|
265
|
+
print(f"\n响应Body (JSON):")
|
|
266
|
+
print(json.dumps(response_json, ensure_ascii=False, indent=2))
|
|
267
|
+
print("=" * 80 + "\n")
|
|
268
|
+
return response_json
|
|
205
269
|
except json.JSONDecodeError:
|
|
270
|
+
print(f"\n响应Body (非JSON):")
|
|
271
|
+
print(response.text[:1000]) # 只显示前1000个字符
|
|
272
|
+
print("=" * 80 + "\n")
|
|
206
273
|
return {
|
|
207
274
|
"status": 0,
|
|
208
275
|
"message": "fail",
|
|
@@ -211,6 +278,9 @@ class ZenTaoAPIClient:
|
|
|
211
278
|
"raw_response": response.text
|
|
212
279
|
}
|
|
213
280
|
except requests.exceptions.RequestException as e:
|
|
281
|
+
print(f"\n【请求异常】")
|
|
282
|
+
print(f"错误信息: {str(e)}")
|
|
283
|
+
print("=" * 80 + "\n")
|
|
214
284
|
return {
|
|
215
285
|
"status": 0,
|
|
216
286
|
"message": "fail",
|
|
@@ -299,14 +369,14 @@ class ZenTaoAPIClient:
|
|
|
299
369
|
|
|
300
370
|
return self._call_new_api('getBugList', params=params, form_data=form_data if form_data else None)
|
|
301
371
|
|
|
302
|
-
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
372
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
303
373
|
"""
|
|
304
374
|
批量解决BUG
|
|
305
375
|
|
|
306
376
|
Args:
|
|
307
377
|
product_id: 产品ID(必传)
|
|
308
378
|
bug_ids: BUG ID列表(必传),可以是单个ID或ID列表
|
|
309
|
-
resolution:
|
|
379
|
+
resolution: 解决方案(可选,默认fixed),可选值:
|
|
310
380
|
fixed: 修复解决
|
|
311
381
|
bydesign: 设计如此
|
|
312
382
|
reqchange: 修改需求
|
|
@@ -319,18 +389,14 @@ class ZenTaoAPIClient:
|
|
|
319
389
|
history: 历史遗留
|
|
320
390
|
configchange: 调整配置
|
|
321
391
|
resolved_by: 解决人(必传,姓名或工号)
|
|
322
|
-
resolved_build: 解决版本ID
|
|
392
|
+
resolved_build: 解决版本ID(可选,如果未提供则默认使用"trunk")
|
|
323
393
|
|
|
324
394
|
Returns:
|
|
325
395
|
dict: 解决结果
|
|
326
396
|
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"message": "fail",
|
|
331
|
-
"info": "解决方案为fixed时,解决版本(resolvedBuild)必传",
|
|
332
|
-
"data": {}
|
|
333
|
-
}
|
|
397
|
+
# 如果没有提供 resolved_build,使用默认值 "trunk"
|
|
398
|
+
if not resolved_build:
|
|
399
|
+
resolved_build = "trunk"
|
|
334
400
|
|
|
335
401
|
params = {'productID': product_id}
|
|
336
402
|
form_data = {
|
|
@@ -349,83 +415,38 @@ class ZenTaoAPIClient:
|
|
|
349
415
|
|
|
350
416
|
return self._call_new_api('resolveBug', params=params, form_data=form_data)
|
|
351
417
|
|
|
352
|
-
def
|
|
353
|
-
"""
|
|
354
|
-
|
|
355
|
-
bdtoken = self.generate_bdtoken(current_timestamp)
|
|
356
|
-
|
|
357
|
-
url = f"{self.base_url}/zentaopms/www/index.php"
|
|
358
|
-
params = {
|
|
359
|
-
'm': 'allneedlist',
|
|
360
|
-
'f': 'createbugbyapi',
|
|
361
|
-
'account': self.account,
|
|
362
|
-
'tamp': current_timestamp
|
|
363
|
-
}
|
|
418
|
+
def close_bug(self, bug_id, comment="AI关闭"):
|
|
419
|
+
"""
|
|
420
|
+
关闭BUG
|
|
364
421
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
422
|
+
Args:
|
|
423
|
+
bug_id: BUG ID(必传)
|
|
424
|
+
comment: 关闭备注(可选,默认"AI关闭")
|
|
369
425
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
except json.JSONDecodeError:
|
|
380
|
-
return {
|
|
381
|
-
"status": -1,
|
|
382
|
-
"message": "响应不是有效的JSON格式",
|
|
383
|
-
"raw_response": response.text
|
|
384
|
-
}
|
|
385
|
-
except requests.exceptions.RequestException as e:
|
|
426
|
+
Returns:
|
|
427
|
+
dict: 关闭结果
|
|
428
|
+
"""
|
|
429
|
+
params = {'bugID': bug_id}
|
|
430
|
+
form_data = {'comment': comment}
|
|
431
|
+
result = self._call_new_api('close', params=params, form_data=form_data, m_param='bug')
|
|
432
|
+
|
|
433
|
+
# 如果返回的是"Array"字符串,表示操作成功(禅道某些接口的特殊返回格式)
|
|
434
|
+
if isinstance(result, dict) and result.get("raw_response") == "Array":
|
|
386
435
|
return {
|
|
387
|
-
"status":
|
|
388
|
-
"message":
|
|
436
|
+
"status": 1,
|
|
437
|
+
"message": "success",
|
|
438
|
+
"info": "success"
|
|
389
439
|
}
|
|
440
|
+
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
def create_bug_by_api(self, bug_data, use_form_data=True):
|
|
444
|
+
"""通过API创建bug"""
|
|
445
|
+
return self._call_new_api('createbugbyapi', form_data=bug_data)
|
|
390
446
|
|
|
391
447
|
def create_case_by_api(self, case_data, use_form_data=True):
|
|
392
448
|
"""通过API创建测试用例"""
|
|
393
|
-
|
|
394
|
-
bdtoken = self.generate_case_bdtoken(current_timestamp)
|
|
395
|
-
|
|
396
|
-
url = f"{self.base_url}/zentaopms/www/index.php"
|
|
397
|
-
params = {
|
|
398
|
-
'm': 'allneedlist',
|
|
399
|
-
'f': 'createcasebyapi',
|
|
400
|
-
'account': self.CASE_ACCOUNT,
|
|
401
|
-
'tamp': current_timestamp
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
headers = {
|
|
405
|
-
'BDTOKEN': bdtoken,
|
|
406
|
-
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
try:
|
|
410
|
-
if use_form_data:
|
|
411
|
-
response = requests.post(url, params=params, headers=headers, data=case_data, timeout=30)
|
|
412
|
-
else:
|
|
413
|
-
headers['Content-Type'] = 'application/json'
|
|
414
|
-
response = requests.post(url, params=params, headers=headers, json=case_data, timeout=30)
|
|
415
|
-
|
|
416
|
-
try:
|
|
417
|
-
return response.json()
|
|
418
|
-
except json.JSONDecodeError:
|
|
419
|
-
return {
|
|
420
|
-
"status": -1,
|
|
421
|
-
"message": "响应不是有效的JSON格式",
|
|
422
|
-
"raw_response": response.text
|
|
423
|
-
}
|
|
424
|
-
except requests.exceptions.RequestException as e:
|
|
425
|
-
return {
|
|
426
|
-
"status": -1,
|
|
427
|
-
"message": f"请求失败: {str(e)}"
|
|
428
|
-
}
|
|
449
|
+
return self._call_new_api('createcasebyapi', form_data=case_data)
|
|
429
450
|
|
|
430
451
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
431
452
|
"""上传图片到BUG附件"""
|
|
@@ -435,25 +456,6 @@ class ZenTaoAPIClient:
|
|
|
435
456
|
"message": f"图片文件不存在: {image_path}"
|
|
436
457
|
}
|
|
437
458
|
|
|
438
|
-
current_timestamp = int(datetime.now().timestamp())
|
|
439
|
-
url = f"{self.base_url}/zentaopms/www/index.php"
|
|
440
|
-
params = {
|
|
441
|
-
'm': 'allneedlist',
|
|
442
|
-
'f': 'uploadbyapi',
|
|
443
|
-
'account': self.CASE_ACCOUNT,
|
|
444
|
-
'tamp': current_timestamp
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
token_string = f"accountfmtamp{current_timestamp}{self.case_secret_key}"
|
|
448
|
-
md5_hash = hashlib.md5()
|
|
449
|
-
md5_hash.update(token_string.encode('utf-8'))
|
|
450
|
-
bdtoken = md5_hash.hexdigest()
|
|
451
|
-
|
|
452
|
-
headers = {
|
|
453
|
-
'BDTOKEN': bdtoken,
|
|
454
|
-
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
455
|
-
}
|
|
456
|
-
|
|
457
459
|
filename = custom_filename or os.path.basename(image_path)
|
|
458
460
|
|
|
459
461
|
try:
|
|
@@ -461,22 +463,8 @@ class ZenTaoAPIClient:
|
|
|
461
463
|
files = {
|
|
462
464
|
'files[]': (filename, f, 'application/octet-stream')
|
|
463
465
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
try:
|
|
468
|
-
return response.json()
|
|
469
|
-
except json.JSONDecodeError:
|
|
470
|
-
return {
|
|
471
|
-
"status": -1,
|
|
472
|
-
"message": "响应不是有效的JSON格式",
|
|
473
|
-
"raw_response": response.text
|
|
474
|
-
}
|
|
475
|
-
except requests.exceptions.RequestException as e:
|
|
476
|
-
return {
|
|
477
|
-
"status": -1,
|
|
478
|
-
"message": f"请求失败: {str(e)}"
|
|
479
|
-
}
|
|
466
|
+
form_data = {'id': bug_id}
|
|
467
|
+
return self._call_new_api('uploadbyapi', form_data=form_data, files=files)
|
|
480
468
|
except Exception as e:
|
|
481
469
|
return {
|
|
482
470
|
"status": -1,
|
|
@@ -681,6 +669,15 @@ class ZenTaoNexus:
|
|
|
681
669
|
LOWER_BUG = "0"
|
|
682
670
|
REGRESSION = "0"
|
|
683
671
|
|
|
672
|
+
# 职位类型到任务类型的映射
|
|
673
|
+
ROLE_TO_TASK_TYPE = {
|
|
674
|
+
"测试": "4", # 测试
|
|
675
|
+
"前端": "8", # 前端开发
|
|
676
|
+
"后端": "3", # 后端开发
|
|
677
|
+
"其它": "20", # 其他
|
|
678
|
+
"其他": "20" # 其他(兼容两种写法)
|
|
679
|
+
}
|
|
680
|
+
|
|
684
681
|
def __init__(self, config_path: str = None):
|
|
685
682
|
"""
|
|
686
683
|
初始化ZenTao Nexus工具
|
|
@@ -719,6 +716,10 @@ class ZenTaoNexus:
|
|
|
719
716
|
env_title_prefix = os.getenv("ZENTAO_TITLE_PREFIX") or os.getenv("TITLE_PREFIX")
|
|
720
717
|
self.title_prefix = env_title_prefix or self.config_loader.get("title_prefix", "")
|
|
721
718
|
|
|
719
|
+
# 职位类型(用于创建任务时自动设置任务类型)
|
|
720
|
+
env_role_type = os.getenv("ZENTAO_ROLE_TYPE") or os.getenv("ROLE_TYPE")
|
|
721
|
+
self.role_type = env_role_type or self.config_loader.get("role_type", "")
|
|
722
|
+
|
|
722
723
|
# 创建API客户端
|
|
723
724
|
self.client = ZenTaoAPIClient(silent=True)
|
|
724
725
|
|
|
@@ -835,7 +836,8 @@ class ZenTaoNexus:
|
|
|
835
836
|
"product_id": self.product_id,
|
|
836
837
|
"opened_by": self.opened_by,
|
|
837
838
|
"keywords": self.keywords,
|
|
838
|
-
"title_prefix": self.title_prefix
|
|
839
|
+
"title_prefix": self.title_prefix,
|
|
840
|
+
"role_type": self.role_type
|
|
839
841
|
}
|
|
840
842
|
|
|
841
843
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
@@ -855,7 +857,43 @@ class ZenTaoNexus:
|
|
|
855
857
|
return self.client.get_story_detail(story_id)
|
|
856
858
|
|
|
857
859
|
def create_task_by_api(self, task_data):
|
|
858
|
-
"""
|
|
860
|
+
"""
|
|
861
|
+
创建任务
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
task_data: 任务数据(字典,form-data格式)
|
|
865
|
+
如果 task_data 中没有 'mold' 字段,且配置了职位类型(role_type),
|
|
866
|
+
则根据职位类型自动设置任务类型
|
|
867
|
+
自动设置字段:
|
|
868
|
+
- estStarted: 当前日期(如果未指定)
|
|
869
|
+
- deadline: 当前日期+3天(如果未指定)
|
|
870
|
+
- pri: 2(如果未指定)
|
|
871
|
+
- openedBy: 配置的创建人(如果未指定)
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
dict: 创建结果,包含taskID
|
|
875
|
+
"""
|
|
876
|
+
# 如果 task_data 中没有指定 mold,且配置了职位类型,则自动设置
|
|
877
|
+
if 'mold' not in task_data or (task_data.get('mold') == '' or task_data.get('mold') is None):
|
|
878
|
+
if self.role_type and self.role_type in self.ROLE_TO_TASK_TYPE:
|
|
879
|
+
task_data['mold'] = self.ROLE_TO_TASK_TYPE[self.role_type]
|
|
880
|
+
|
|
881
|
+
# 自动设置预计开始时间(当前日期)
|
|
882
|
+
if 'estStarted' not in task_data or (task_data.get('estStarted') == '' or task_data.get('estStarted') is None):
|
|
883
|
+
task_data['estStarted'] = datetime.now().strftime("%Y-%m-%d")
|
|
884
|
+
|
|
885
|
+
# 自动设置预计结束时间(当前日期+3天)
|
|
886
|
+
if 'deadline' not in task_data or (task_data.get('deadline') == '' or task_data.get('deadline') is None):
|
|
887
|
+
task_data['deadline'] = (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d")
|
|
888
|
+
|
|
889
|
+
# 自动设置优先级为2
|
|
890
|
+
if 'pri' not in task_data or (task_data.get('pri') == '' or task_data.get('pri') is None):
|
|
891
|
+
task_data['pri'] = '2'
|
|
892
|
+
|
|
893
|
+
# 自动设置创建人
|
|
894
|
+
if 'openedBy' not in task_data or (task_data.get('openedBy') == '' or task_data.get('openedBy') is None):
|
|
895
|
+
task_data['openedBy'] = self.opened_by
|
|
896
|
+
|
|
859
897
|
return self.client.create_task_by_api(task_data)
|
|
860
898
|
|
|
861
899
|
def assign_task(self, task_id, assigned_to, comment=None):
|
|
@@ -868,9 +906,13 @@ class ZenTaoNexus:
|
|
|
868
906
|
return self.client.get_bug_list(product_id, assigned_to, opened_by, resolved_by,
|
|
869
907
|
confirmed, status, start_date, end_date)
|
|
870
908
|
|
|
871
|
-
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
909
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
872
910
|
"""批量解决BUG"""
|
|
873
911
|
return self.client.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
912
|
+
|
|
913
|
+
def close_bug(self, bug_id, comment="AI关闭"):
|
|
914
|
+
"""关闭BUG"""
|
|
915
|
+
return self.client.close_bug(bug_id, comment)
|
|
874
916
|
|
|
875
917
|
|
|
876
918
|
# 便捷函数
|