@bangdao-ai/zentao-mcp 1.1.0 → 1.1.2
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 +203 -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,74 @@ class ZenTaoAPIClient:
|
|
|
180
167
|
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
181
168
|
}
|
|
182
169
|
|
|
170
|
+
# 打印请求信息(仅在非静默模式下)
|
|
171
|
+
if not self.silent:
|
|
172
|
+
print("\n" + "=" * 80)
|
|
173
|
+
print("【禅道API请求信息】")
|
|
174
|
+
print("=" * 80)
|
|
175
|
+
print(f"API URL: {api_url}")
|
|
176
|
+
print(f"\n【Headers】")
|
|
177
|
+
for key, value in headers.items():
|
|
178
|
+
print(f" {key}: {value}")
|
|
179
|
+
|
|
180
|
+
print(f"\n【Token生成信息】")
|
|
181
|
+
print(f" code: {self.code}")
|
|
182
|
+
print(f" key: {self.key[:10]}...{self.key[-10:] if len(self.key) > 20 else self.key}") # 只显示部分key,保护隐私
|
|
183
|
+
print(f" time: {current_timestamp}")
|
|
184
|
+
print(f" token生成公式: md5(code + key + time)")
|
|
185
|
+
print(f" token生成字符串: {self.code}{self.key}{current_timestamp}")
|
|
186
|
+
print(f" token: {token}")
|
|
187
|
+
|
|
188
|
+
print(f"\n【URL参数】")
|
|
189
|
+
for key, value in url_params.items():
|
|
190
|
+
print(f" {key}: {value}")
|
|
191
|
+
|
|
192
|
+
# 处理form_data用于显示
|
|
193
|
+
display_form_data = form_data
|
|
194
|
+
if isinstance(form_data, dict):
|
|
195
|
+
# 检查是否有数组类型的值
|
|
196
|
+
processed_data = []
|
|
197
|
+
for key, value in form_data.items():
|
|
198
|
+
if isinstance(value, (list, tuple)):
|
|
199
|
+
for item in value:
|
|
200
|
+
processed_data.append((key, str(item)))
|
|
201
|
+
else:
|
|
202
|
+
processed_data.append((key, str(value)))
|
|
203
|
+
display_form_data = processed_data
|
|
204
|
+
|
|
205
|
+
if display_form_data:
|
|
206
|
+
print(f"\n【Body (form-data)】")
|
|
207
|
+
if isinstance(display_form_data, list):
|
|
208
|
+
for item in display_form_data:
|
|
209
|
+
if isinstance(item, tuple):
|
|
210
|
+
key, value = item
|
|
211
|
+
# 如果是文件,只显示文件名
|
|
212
|
+
if hasattr(value, 'read'):
|
|
213
|
+
print(f" {key}: <文件对象>")
|
|
214
|
+
else:
|
|
215
|
+
print(f" {key}: {value}")
|
|
216
|
+
else:
|
|
217
|
+
print(f" {item}")
|
|
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}")
|
|
228
|
+
else:
|
|
229
|
+
print(f" {key}: {file_info}")
|
|
230
|
+
|
|
231
|
+
print("=" * 80)
|
|
232
|
+
|
|
183
233
|
try:
|
|
184
|
-
if
|
|
234
|
+
if files:
|
|
235
|
+
# 文件上传
|
|
236
|
+
response = requests.post(api_url, params=url_params, headers=headers, data=form_data, files=files, timeout=30)
|
|
237
|
+
elif form_data:
|
|
185
238
|
# 如果form_data是列表,直接使用(用于数组参数)
|
|
186
239
|
# 如果是字典,需要检查是否有数组类型的值
|
|
187
240
|
if isinstance(form_data, dict):
|
|
@@ -200,9 +253,33 @@ class ZenTaoAPIClient:
|
|
|
200
253
|
else:
|
|
201
254
|
response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
|
|
202
255
|
|
|
256
|
+
# 处理响应
|
|
203
257
|
try:
|
|
204
|
-
|
|
258
|
+
response_json = response.json()
|
|
259
|
+
# 打印响应信息(仅在非静默模式下)
|
|
260
|
+
if not self.silent:
|
|
261
|
+
print("\n【禅道API响应信息】")
|
|
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")
|
|
270
|
+
return response_json
|
|
205
271
|
except json.JSONDecodeError:
|
|
272
|
+
# 打印响应信息(仅在非静默模式下)
|
|
273
|
+
if not self.silent:
|
|
274
|
+
print("\n【禅道API响应信息】")
|
|
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")
|
|
206
283
|
return {
|
|
207
284
|
"status": 0,
|
|
208
285
|
"message": "fail",
|
|
@@ -211,6 +288,11 @@ class ZenTaoAPIClient:
|
|
|
211
288
|
"raw_response": response.text
|
|
212
289
|
}
|
|
213
290
|
except requests.exceptions.RequestException as e:
|
|
291
|
+
# 打印异常信息(仅在非静默模式下)
|
|
292
|
+
if not self.silent:
|
|
293
|
+
print(f"\n【请求异常】")
|
|
294
|
+
print(f"错误信息: {str(e)}")
|
|
295
|
+
print("=" * 80 + "\n")
|
|
214
296
|
return {
|
|
215
297
|
"status": 0,
|
|
216
298
|
"message": "fail",
|
|
@@ -299,14 +381,14 @@ class ZenTaoAPIClient:
|
|
|
299
381
|
|
|
300
382
|
return self._call_new_api('getBugList', params=params, form_data=form_data if form_data else None)
|
|
301
383
|
|
|
302
|
-
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
384
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
303
385
|
"""
|
|
304
386
|
批量解决BUG
|
|
305
387
|
|
|
306
388
|
Args:
|
|
307
389
|
product_id: 产品ID(必传)
|
|
308
390
|
bug_ids: BUG ID列表(必传),可以是单个ID或ID列表
|
|
309
|
-
resolution:
|
|
391
|
+
resolution: 解决方案(可选,默认fixed),可选值:
|
|
310
392
|
fixed: 修复解决
|
|
311
393
|
bydesign: 设计如此
|
|
312
394
|
reqchange: 修改需求
|
|
@@ -319,18 +401,14 @@ class ZenTaoAPIClient:
|
|
|
319
401
|
history: 历史遗留
|
|
320
402
|
configchange: 调整配置
|
|
321
403
|
resolved_by: 解决人(必传,姓名或工号)
|
|
322
|
-
resolved_build: 解决版本ID
|
|
404
|
+
resolved_build: 解决版本ID(可选,如果未提供则默认使用"trunk")
|
|
323
405
|
|
|
324
406
|
Returns:
|
|
325
407
|
dict: 解决结果
|
|
326
408
|
"""
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"message": "fail",
|
|
331
|
-
"info": "解决方案为fixed时,解决版本(resolvedBuild)必传",
|
|
332
|
-
"data": {}
|
|
333
|
-
}
|
|
409
|
+
# 如果没有提供 resolved_build,使用默认值 "trunk"
|
|
410
|
+
if not resolved_build:
|
|
411
|
+
resolved_build = "trunk"
|
|
334
412
|
|
|
335
413
|
params = {'productID': product_id}
|
|
336
414
|
form_data = {
|
|
@@ -349,83 +427,38 @@ class ZenTaoAPIClient:
|
|
|
349
427
|
|
|
350
428
|
return self._call_new_api('resolveBug', params=params, form_data=form_data)
|
|
351
429
|
|
|
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
|
-
}
|
|
430
|
+
def close_bug(self, bug_id, comment="AI关闭"):
|
|
431
|
+
"""
|
|
432
|
+
关闭BUG
|
|
364
433
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
434
|
+
Args:
|
|
435
|
+
bug_id: BUG ID(必传)
|
|
436
|
+
comment: 关闭备注(可选,默认"AI关闭")
|
|
369
437
|
|
|
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:
|
|
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":
|
|
386
447
|
return {
|
|
387
|
-
"status":
|
|
388
|
-
"message":
|
|
448
|
+
"status": 1,
|
|
449
|
+
"message": "success",
|
|
450
|
+
"info": "success"
|
|
389
451
|
}
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
def create_bug_by_api(self, bug_data, use_form_data=True):
|
|
456
|
+
"""通过API创建bug"""
|
|
457
|
+
return self._call_new_api('createbugbyapi', form_data=bug_data)
|
|
390
458
|
|
|
391
459
|
def create_case_by_api(self, case_data, use_form_data=True):
|
|
392
460
|
"""通过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
|
-
}
|
|
461
|
+
return self._call_new_api('createcasebyapi', form_data=case_data)
|
|
429
462
|
|
|
430
463
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
431
464
|
"""上传图片到BUG附件"""
|
|
@@ -435,25 +468,6 @@ class ZenTaoAPIClient:
|
|
|
435
468
|
"message": f"图片文件不存在: {image_path}"
|
|
436
469
|
}
|
|
437
470
|
|
|
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
471
|
filename = custom_filename or os.path.basename(image_path)
|
|
458
472
|
|
|
459
473
|
try:
|
|
@@ -461,22 +475,8 @@ class ZenTaoAPIClient:
|
|
|
461
475
|
files = {
|
|
462
476
|
'files[]': (filename, f, 'application/octet-stream')
|
|
463
477
|
}
|
|
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
|
-
}
|
|
478
|
+
form_data = {'id': bug_id}
|
|
479
|
+
return self._call_new_api('uploadbyapi', form_data=form_data, files=files)
|
|
480
480
|
except Exception as e:
|
|
481
481
|
return {
|
|
482
482
|
"status": -1,
|
|
@@ -681,6 +681,15 @@ class ZenTaoNexus:
|
|
|
681
681
|
LOWER_BUG = "0"
|
|
682
682
|
REGRESSION = "0"
|
|
683
683
|
|
|
684
|
+
# 职位类型到任务类型的映射
|
|
685
|
+
ROLE_TO_TASK_TYPE = {
|
|
686
|
+
"测试": "4", # 测试
|
|
687
|
+
"前端": "8", # 前端开发
|
|
688
|
+
"后端": "3", # 后端开发
|
|
689
|
+
"其它": "20", # 其他
|
|
690
|
+
"其他": "20" # 其他(兼容两种写法)
|
|
691
|
+
}
|
|
692
|
+
|
|
684
693
|
def __init__(self, config_path: str = None):
|
|
685
694
|
"""
|
|
686
695
|
初始化ZenTao Nexus工具
|
|
@@ -719,6 +728,10 @@ class ZenTaoNexus:
|
|
|
719
728
|
env_title_prefix = os.getenv("ZENTAO_TITLE_PREFIX") or os.getenv("TITLE_PREFIX")
|
|
720
729
|
self.title_prefix = env_title_prefix or self.config_loader.get("title_prefix", "")
|
|
721
730
|
|
|
731
|
+
# 职位类型(用于创建任务时自动设置任务类型)
|
|
732
|
+
env_role_type = os.getenv("ZENTAO_ROLE_TYPE") or os.getenv("ROLE_TYPE")
|
|
733
|
+
self.role_type = env_role_type or self.config_loader.get("role_type", "")
|
|
734
|
+
|
|
722
735
|
# 创建API客户端
|
|
723
736
|
self.client = ZenTaoAPIClient(silent=True)
|
|
724
737
|
|
|
@@ -835,7 +848,8 @@ class ZenTaoNexus:
|
|
|
835
848
|
"product_id": self.product_id,
|
|
836
849
|
"opened_by": self.opened_by,
|
|
837
850
|
"keywords": self.keywords,
|
|
838
|
-
"title_prefix": self.title_prefix
|
|
851
|
+
"title_prefix": self.title_prefix,
|
|
852
|
+
"role_type": self.role_type
|
|
839
853
|
}
|
|
840
854
|
|
|
841
855
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
@@ -855,7 +869,43 @@ class ZenTaoNexus:
|
|
|
855
869
|
return self.client.get_story_detail(story_id)
|
|
856
870
|
|
|
857
871
|
def create_task_by_api(self, task_data):
|
|
858
|
-
"""
|
|
872
|
+
"""
|
|
873
|
+
创建任务
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
task_data: 任务数据(字典,form-data格式)
|
|
877
|
+
如果 task_data 中没有 'mold' 字段,且配置了职位类型(role_type),
|
|
878
|
+
则根据职位类型自动设置任务类型
|
|
879
|
+
自动设置字段:
|
|
880
|
+
- estStarted: 当前日期(如果未指定)
|
|
881
|
+
- deadline: 当前日期+3天(如果未指定)
|
|
882
|
+
- pri: 2(如果未指定)
|
|
883
|
+
- openedBy: 配置的创建人(如果未指定)
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
dict: 创建结果,包含taskID
|
|
887
|
+
"""
|
|
888
|
+
# 如果 task_data 中没有指定 mold,且配置了职位类型,则自动设置
|
|
889
|
+
if 'mold' not in task_data or (task_data.get('mold') == '' or task_data.get('mold') is None):
|
|
890
|
+
if self.role_type and self.role_type in self.ROLE_TO_TASK_TYPE:
|
|
891
|
+
task_data['mold'] = self.ROLE_TO_TASK_TYPE[self.role_type]
|
|
892
|
+
|
|
893
|
+
# 自动设置预计开始时间(当前日期)
|
|
894
|
+
if 'estStarted' not in task_data or (task_data.get('estStarted') == '' or task_data.get('estStarted') is None):
|
|
895
|
+
task_data['estStarted'] = datetime.now().strftime("%Y-%m-%d")
|
|
896
|
+
|
|
897
|
+
# 自动设置预计结束时间(当前日期+3天)
|
|
898
|
+
if 'deadline' not in task_data or (task_data.get('deadline') == '' or task_data.get('deadline') is None):
|
|
899
|
+
task_data['deadline'] = (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d")
|
|
900
|
+
|
|
901
|
+
# 自动设置优先级为2
|
|
902
|
+
if 'pri' not in task_data or (task_data.get('pri') == '' or task_data.get('pri') is None):
|
|
903
|
+
task_data['pri'] = '2'
|
|
904
|
+
|
|
905
|
+
# 自动设置创建人
|
|
906
|
+
if 'openedBy' not in task_data or (task_data.get('openedBy') == '' or task_data.get('openedBy') is None):
|
|
907
|
+
task_data['openedBy'] = self.opened_by
|
|
908
|
+
|
|
859
909
|
return self.client.create_task_by_api(task_data)
|
|
860
910
|
|
|
861
911
|
def assign_task(self, task_id, assigned_to, comment=None):
|
|
@@ -868,9 +918,13 @@ class ZenTaoNexus:
|
|
|
868
918
|
return self.client.get_bug_list(product_id, assigned_to, opened_by, resolved_by,
|
|
869
919
|
confirmed, status, start_date, end_date)
|
|
870
920
|
|
|
871
|
-
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
921
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
872
922
|
"""批量解决BUG"""
|
|
873
923
|
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)
|
|
874
928
|
|
|
875
929
|
|
|
876
930
|
# 便捷函数
|