@bangdao-ai/zentao-mcp 1.0.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 +189 -9
- package/package.json +3 -4
- package/requirements.txt +1 -0
- package/src/__pycache__/mcp_server.cpython-313.pyc +0 -0
- package/src/__pycache__/zentao_nexus.cpython-313.pyc +0 -0
- package/src/mcp_server.py +144 -1
- package/src/zentao_nexus.py +387 -109
- package/bug_config.example.json +0 -7
package/README.md
CHANGED
|
@@ -2,9 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
这是一个基于 Python 的 MCP(Model Context Protocol)工具,用于连接禅道Bug管理系统。支持通过 npm/npx 直接使用。
|
|
4
4
|
|
|
5
|
+
## 使用场景与AI沟通话术
|
|
6
|
+
|
|
7
|
+
### 场景一:创建Bug
|
|
8
|
+
|
|
9
|
+
**话术示例**:
|
|
10
|
+
- "帮我创建一个Bug,标题是'登录页面验证码不显示',重现步骤是:1. 打开登录页面 2. 点击验证码区域 3. 验证码未显示"
|
|
11
|
+
- "创建一个Bug,标题'支付接口超时',步骤:调用支付接口后等待30秒无响应,截图路径是 /path/to/screenshot.png"
|
|
12
|
+
- "在禅道创建一个Bug,标题'数据导出功能异常',重现步骤:进入数据管理页面,点击导出按钮,提示错误"
|
|
13
|
+
|
|
14
|
+
### 场景二:获取需求详情
|
|
15
|
+
|
|
16
|
+
**话术示例**:
|
|
17
|
+
- "帮我获取需求ID为12345的需求详情"
|
|
18
|
+
- "查询一下需求12345的详细信息,包括标题、描述、验收标准等"
|
|
19
|
+
- "获取需求12345的完整信息"
|
|
20
|
+
|
|
21
|
+
### 场景三:创建任务
|
|
22
|
+
|
|
23
|
+
**话术示例**:
|
|
24
|
+
- "帮我创建一个任务,任务名称是'优化登录接口性能',分配给张三"
|
|
25
|
+
- "创建一个开发任务,名称'修复支付bug',优先级高,分配给李四"
|
|
26
|
+
- "在禅道创建一个任务,标题'重构用户模块',分配给王五,预计3天完成"
|
|
27
|
+
|
|
28
|
+
### 场景四:指派任务
|
|
29
|
+
|
|
30
|
+
**话术示例**:
|
|
31
|
+
- "把任务ID为67890的任务指派给张三"
|
|
32
|
+
- "将任务67890重新指派给李四,备注'需要紧急处理'"
|
|
33
|
+
- "指派任务67890给王五,备注说明'这个任务需要前端配合'"
|
|
34
|
+
|
|
35
|
+
### 场景五:查询BUG列表
|
|
36
|
+
|
|
37
|
+
**话术示例**:
|
|
38
|
+
- "帮我查询产品365下所有已确认的BUG"
|
|
39
|
+
- "查询产品365中分配给张三的未解决BUG"
|
|
40
|
+
- "获取产品365在2024-01-01到2024-01-31期间创建的BUG列表"
|
|
41
|
+
- "查询产品365中状态为'激活'的BUG,创建人是李四"
|
|
42
|
+
- "帮我拉取产品365下所有已确认但未解决的BUG"
|
|
43
|
+
|
|
44
|
+
### 场景六:批量解决BUG
|
|
45
|
+
|
|
46
|
+
**话术示例**:
|
|
47
|
+
- "批量解决产品365下的BUG,BUG ID是[76016, 75241],解决方案是fixed,解决人是68249,解决版本是2700"
|
|
48
|
+
- "将产品365的BUG [76016, 75241] 标记为'设计如此',解决人是张三"
|
|
49
|
+
- "批量解决BUG,产品ID 365,BUG列表[76016, 75241],解决方案duplicate(重复上报),解决人李四"
|
|
50
|
+
|
|
51
|
+
### 场景七:创建测试用例
|
|
52
|
+
|
|
53
|
+
**话术示例**:
|
|
54
|
+
- "帮我创建一个接口测试用例,名称'用户登录接口测试',接口地址是 /api/login,请求方法是POST"
|
|
55
|
+
- "创建一个功能测试用例,名称'用户注册流程测试',步骤包括:1. 打开注册页面 2. 填写信息 3. 提交注册"
|
|
56
|
+
- "在禅道创建一个测试用例,类型是接口测试,名称'订单查询接口'"
|
|
57
|
+
|
|
58
|
+
### 场景八:批量创建测试用例
|
|
59
|
+
|
|
60
|
+
**话术示例**:
|
|
61
|
+
- "从CSV文件 /path/to/cases.csv 批量创建测试用例"
|
|
62
|
+
- "帮我将CSV文件中的测试用例批量提交到禅道,文件路径是 /path/to/test_cases.csv"
|
|
63
|
+
- "从CSV文件批量创建接口测试用例,文件路径 /path/to/api_cases.csv"
|
|
64
|
+
|
|
65
|
+
### 场景九:上传截图到BUG
|
|
66
|
+
|
|
67
|
+
**话术示例**:
|
|
68
|
+
- "把截图 /path/to/screenshot.png 上传到BUG 12345"
|
|
69
|
+
- "上传图片到BUG 12345,图片路径是 /path/to/image.jpg,文件名改为'错误截图'"
|
|
70
|
+
- "帮我把截图上传到BUG 12345的附件中"
|
|
71
|
+
|
|
72
|
+
### 场景十:查看配置
|
|
73
|
+
|
|
74
|
+
**话术示例**:
|
|
75
|
+
- "查看一下当前的禅道配置信息"
|
|
76
|
+
- "显示当前的配置"
|
|
77
|
+
- "获取配置信息"
|
|
78
|
+
|
|
5
79
|
## 功能特性
|
|
6
80
|
|
|
7
81
|
- ✅ 创建Bug(支持截图上传)
|
|
82
|
+
- ✅ 获取需求详情
|
|
83
|
+
- ✅ 创建任务
|
|
84
|
+
- ✅ 指派任务
|
|
85
|
+
- ✅ 查询BUG列表(支持多条件筛选)
|
|
86
|
+
- ✅ 批量解决BUG
|
|
8
87
|
- ✅ 创建测试用例
|
|
9
88
|
- ✅ 批量从CSV文件创建测试用例
|
|
10
89
|
- ✅ 上传图片到Bug附件
|
|
@@ -13,12 +92,37 @@
|
|
|
13
92
|
|
|
14
93
|
## 安装
|
|
15
94
|
|
|
16
|
-
|
|
95
|
+
### 前置要求
|
|
96
|
+
|
|
97
|
+
- **Node.js**: >= 14.0.0
|
|
98
|
+
- **Python**: >= 3.8
|
|
99
|
+
|
|
100
|
+
### 安装方式
|
|
101
|
+
|
|
102
|
+
#### 方式一:通过 npx 使用(推荐,无需安装)
|
|
103
|
+
|
|
104
|
+
直接在 Cursor 配置中使用,无需手动安装:
|
|
17
105
|
|
|
18
106
|
```bash
|
|
19
107
|
npx -y @bangdao-ai/zentao-mcp@latest
|
|
20
108
|
```
|
|
21
109
|
|
|
110
|
+
#### 方式二:全局安装
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm install -g @bangdao-ai/zentao-mcp@latest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### 方式三:本地安装(开发模式)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git clone https://github.com/bangdao-ai/zentao-mcp.git
|
|
120
|
+
cd zentao-mcp
|
|
121
|
+
pip3 install -r requirements.txt
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**详细安装指南请参考**:[docs/INSTALL.md](docs/INSTALL.md)
|
|
125
|
+
|
|
22
126
|
## 配置
|
|
23
127
|
|
|
24
128
|
### 在 Cursor 中配置
|
|
@@ -37,6 +141,7 @@ npx -y @bangdao-ai/zentao-mcp@latest
|
|
|
37
141
|
"env": {
|
|
38
142
|
"ZENTAO_PRODUCT_ID": "365",
|
|
39
143
|
"ZENTAO_OPENED_BY": "69610",
|
|
144
|
+
"KEY": "643f0f490d1ea0d47520dd270989c99a",
|
|
40
145
|
"ZENTAO_KEYWORDS": "AI",
|
|
41
146
|
"ZENTAO_TITLE_PREFIX": ""
|
|
42
147
|
}
|
|
@@ -47,13 +152,14 @@ npx -y @bangdao-ai/zentao-mcp@latest
|
|
|
47
152
|
|
|
48
153
|
**配置说明**:
|
|
49
154
|
- `ZENTAO_PRODUCT_ID`(必需):产品ID
|
|
50
|
-
- `ZENTAO_OPENED_BY`(必需):创建人和指派人ID
|
|
155
|
+
- `ZENTAO_OPENED_BY`(必需):创建人和指派人ID(同一个人),作为API认证的code参数
|
|
156
|
+
- `KEY`(必需):API认证密钥,作为API认证的key参数
|
|
51
157
|
- `ZENTAO_KEYWORDS`(可选):关键词,默认为空
|
|
52
158
|
- `ZENTAO_TITLE_PREFIX`(可选):标题前缀,默认为空
|
|
53
159
|
|
|
54
160
|
### 兜底配置(可选)
|
|
55
161
|
|
|
56
|
-
如果未在环境变量中配置,可以使用配置文件 `bug_config.json
|
|
162
|
+
如果未在环境变量中配置,可以使用配置文件 `bug_config.json`。配置文件路径可通过环境变量 `ZENTAO_CONFIG_PATH` 指定。
|
|
57
163
|
|
|
58
164
|
## MCP 工具
|
|
59
165
|
|
|
@@ -131,21 +237,95 @@ npx -y @bangdao-ai/zentao-mcp@latest
|
|
|
131
237
|
|
|
132
238
|
获取当前配置信息。
|
|
133
239
|
|
|
240
|
+
### 7. get_story_detail
|
|
241
|
+
|
|
242
|
+
获取需求详情。
|
|
243
|
+
|
|
244
|
+
**参数**:
|
|
245
|
+
- `story_id` (必需): 需求ID
|
|
246
|
+
|
|
247
|
+
**返回**:包含需求标题、描述、验收标准、创建人、指派人等详细信息
|
|
248
|
+
|
|
249
|
+
### 8. create_task
|
|
250
|
+
|
|
251
|
+
创建任务。
|
|
252
|
+
|
|
253
|
+
**参数**:
|
|
254
|
+
- `task_data` (必需): 任务数据对象,包含任务相关字段(form-data格式)
|
|
255
|
+
|
|
256
|
+
**注意**:任务数据的具体字段请参考禅道API文档,常见字段包括:
|
|
257
|
+
- `name`: 任务名称
|
|
258
|
+
- `assignedTo`: 指派人
|
|
259
|
+
- `pri`: 优先级
|
|
260
|
+
- `estimate`: 预计工时
|
|
261
|
+
- `desc`: 任务描述
|
|
262
|
+
- 等其他字段
|
|
263
|
+
|
|
264
|
+
### 9. assign_task
|
|
265
|
+
|
|
266
|
+
指派任务。
|
|
267
|
+
|
|
268
|
+
**参数**:
|
|
269
|
+
- `task_id` (必需): 任务ID
|
|
270
|
+
- `assigned_to` (必需): 新的指派人(姓名或工号)
|
|
271
|
+
- `comment` (可选): 指派备注
|
|
272
|
+
|
|
273
|
+
### 10. get_bug_list
|
|
274
|
+
|
|
275
|
+
拉取已确认/未解决的BUG列表。
|
|
276
|
+
|
|
277
|
+
**参数**:
|
|
278
|
+
- `product_id` (必需): 产品ID
|
|
279
|
+
- `assigned_to` (可选): 指派人(姓名或工号)
|
|
280
|
+
- `opened_by` (可选): 创建人(姓名或工号)
|
|
281
|
+
- `resolved_by` (可选): 解决人(姓名或工号)
|
|
282
|
+
- `confirmed` (可选): 是否确认(2=已确认,1=未确认,0或不传=所有)
|
|
283
|
+
- `status` (可选): 状态(激活/已解决/已关闭)
|
|
284
|
+
- `start_date` (可选): 创建开始日期(格式:YYYY-MM-DD)
|
|
285
|
+
- `end_date` (可选): 创建截止日期(格式:YYYY-MM-DD)
|
|
286
|
+
|
|
287
|
+
### 11. resolve_bug
|
|
288
|
+
|
|
289
|
+
批量解决BUG。
|
|
290
|
+
|
|
291
|
+
**参数**:
|
|
292
|
+
- `product_id` (必需): 产品ID
|
|
293
|
+
- `bug_ids` (必需): BUG ID列表(数组)
|
|
294
|
+
- `resolution` (必需): 解决方案,可选值:
|
|
295
|
+
- `fixed`: 修复解决
|
|
296
|
+
- `bydesign`: 设计如此
|
|
297
|
+
- `reqchange`: 修改需求
|
|
298
|
+
- `duplicate`: 重复上报
|
|
299
|
+
- `external`: 外部原因
|
|
300
|
+
- `notrepro`: 无法重现
|
|
301
|
+
- `postponed`: 延期处理
|
|
302
|
+
- `willnotfix`: 不予解决
|
|
303
|
+
- `tostory`: 转为需求
|
|
304
|
+
- `history`: 历史遗留
|
|
305
|
+
- `configchange`: 调整配置
|
|
306
|
+
- `resolved_by` (必需): 解决人(姓名或工号)
|
|
307
|
+
- `resolved_build` (可选): 解决版本ID(当解决方案为`fixed`时必传)
|
|
308
|
+
|
|
134
309
|
## 项目结构
|
|
135
310
|
|
|
136
311
|
```
|
|
137
312
|
禅道mcp/
|
|
138
|
-
├──
|
|
139
|
-
├── package.json # npm 包配置
|
|
140
|
-
├── src/
|
|
313
|
+
├── src/ # 源码目录
|
|
141
314
|
│ ├── mcp_server.py # MCP服务器主文件
|
|
142
315
|
│ └── zentao_nexus.py # 禅道工具类
|
|
316
|
+
├── scripts/ # 脚本目录
|
|
317
|
+
│ └── publish/ # 发布脚本
|
|
318
|
+
├── docs/ # 文档目录
|
|
319
|
+
│ ├── PUBLISH.md # 发布指南
|
|
320
|
+
│ └── README-PUBLISH.md # 发布脚本使用指南
|
|
321
|
+
├── index.js # npm 入口文件
|
|
322
|
+
├── package.json # npm 包配置
|
|
143
323
|
├── requirements.txt # Python 依赖
|
|
144
|
-
|
|
145
|
-
├── PUBLISH.md # 发布指南
|
|
146
|
-
└── bug_config.example.json # 兜底配置示例
|
|
324
|
+
└── README.md # 项目说明
|
|
147
325
|
```
|
|
148
326
|
|
|
327
|
+
详细的项目结构说明请参考 [docs/PROJECT-STRUCTURE.md](docs/PROJECT-STRUCTURE.md)
|
|
328
|
+
|
|
149
329
|
## 发布
|
|
150
330
|
|
|
151
331
|
参考 [PUBLISH.md](PUBLISH.md) 了解如何发布到 npm。
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bangdao-ai/zentao-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "禅道Bug管理系统MCP工具",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"zentao-mcp": "
|
|
7
|
+
"zentao-mcp": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node index.js"
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"index.js",
|
|
22
22
|
"src/",
|
|
23
23
|
"requirements.txt",
|
|
24
|
-
"bug_config.example.json",
|
|
25
24
|
"README.md",
|
|
26
25
|
"package.json"
|
|
27
26
|
],
|
|
@@ -30,6 +29,6 @@
|
|
|
30
29
|
},
|
|
31
30
|
"repository": {
|
|
32
31
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/bangdao-ai/zentao-mcp.git"
|
|
32
|
+
"url": "git+https://github.com/bangdao-ai/zentao-mcp.git"
|
|
34
33
|
}
|
|
35
34
|
}
|
package/requirements.txt
CHANGED
|
Binary file
|
|
Binary file
|
package/src/mcp_server.py
CHANGED
|
@@ -127,6 +127,98 @@ class ZenTaoMCPServer:
|
|
|
127
127
|
name="get_config",
|
|
128
128
|
description="获取当前配置信息",
|
|
129
129
|
inputSchema={"type": "object", "properties": {}}
|
|
130
|
+
),
|
|
131
|
+
Tool(
|
|
132
|
+
name="get_story_detail",
|
|
133
|
+
description="获取需求详情",
|
|
134
|
+
inputSchema={
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"story_id": {"type": "string", "description": "需求ID"}
|
|
138
|
+
},
|
|
139
|
+
"required": ["story_id"]
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
Tool(
|
|
143
|
+
name="create_task",
|
|
144
|
+
description="创建任务",
|
|
145
|
+
inputSchema={
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"task_data": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"description": "任务数据(form-data格式的键值对)",
|
|
151
|
+
"additionalProperties": True
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"required": ["task_data"]
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
Tool(
|
|
158
|
+
name="assign_task",
|
|
159
|
+
description="指派任务",
|
|
160
|
+
inputSchema={
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"task_id": {"type": "string", "description": "任务ID"},
|
|
164
|
+
"assigned_to": {"type": "string", "description": "新的指派人(姓名或工号)"},
|
|
165
|
+
"comment": {"type": "string", "description": "指派备注(可选)"}
|
|
166
|
+
},
|
|
167
|
+
"required": ["task_id", "assigned_to"]
|
|
168
|
+
}
|
|
169
|
+
),
|
|
170
|
+
Tool(
|
|
171
|
+
name="get_bug_list",
|
|
172
|
+
description="拉取已确认/未解决的BUG",
|
|
173
|
+
inputSchema={
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {
|
|
176
|
+
"product_id": {"type": "string", "description": "产品ID"},
|
|
177
|
+
"assigned_to": {"type": "string", "description": "指派人(可选,姓名或工号)"},
|
|
178
|
+
"opened_by": {"type": "string", "description": "创建人(可选,姓名或工号)"},
|
|
179
|
+
"resolved_by": {"type": "string", "description": "解决人(可选,姓名或工号)"},
|
|
180
|
+
"confirmed": {"type": "string", "description": "是否确认(可选,2已确认 1未确认 0或不传是所有)"},
|
|
181
|
+
"status": {"type": "string", "description": "状态(可选,激活/已解决/已关闭)"},
|
|
182
|
+
"start_date": {"type": "string", "description": "创建开始日期(可选)"},
|
|
183
|
+
"end_date": {"type": "string", "description": "创建截止日期(可选)"}
|
|
184
|
+
},
|
|
185
|
+
"required": ["product_id"]
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
Tool(
|
|
189
|
+
name="resolve_bug",
|
|
190
|
+
description="批量解决BUG",
|
|
191
|
+
inputSchema={
|
|
192
|
+
"type": "object",
|
|
193
|
+
"properties": {
|
|
194
|
+
"product_id": {"type": "string", "description": "产品ID"},
|
|
195
|
+
"bug_ids": {
|
|
196
|
+
"type": "array",
|
|
197
|
+
"items": {"type": "string"},
|
|
198
|
+
"description": "BUG ID列表"
|
|
199
|
+
},
|
|
200
|
+
"resolution": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "解决方案",
|
|
203
|
+
"enum": ["fixed", "bydesign", "reqchange", "duplicate", "external", "notrepro", "postponed", "willnotfix", "tostory", "history", "configchange"]
|
|
204
|
+
},
|
|
205
|
+
"resolved_by": {"type": "string", "description": "解决人(姓名或工号)"},
|
|
206
|
+
"resolved_build": {"type": "string", "description": "解决版本ID(可选,如果未提供则默认使用trunk)"}
|
|
207
|
+
},
|
|
208
|
+
"required": ["product_id", "bug_ids", "resolution", "resolved_by"]
|
|
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
|
+
}
|
|
130
222
|
)
|
|
131
223
|
]
|
|
132
224
|
|
|
@@ -150,7 +242,8 @@ class ZenTaoMCPServer:
|
|
|
150
242
|
"product_id": product_id,
|
|
151
243
|
"opened_by": opened_by,
|
|
152
244
|
"keywords": os.getenv("ZENTAO_KEYWORDS", ""),
|
|
153
|
-
"title_prefix": os.getenv("ZENTAO_TITLE_PREFIX", "")
|
|
245
|
+
"title_prefix": os.getenv("ZENTAO_TITLE_PREFIX", ""),
|
|
246
|
+
"role_type": os.getenv("ZENTAO_ROLE_TYPE", "")
|
|
154
247
|
}
|
|
155
248
|
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
156
249
|
json.dump(temp_config, temp_file, ensure_ascii=False)
|
|
@@ -312,6 +405,56 @@ class ZenTaoMCPServer:
|
|
|
312
405
|
config = self.nexus.get_config()
|
|
313
406
|
return [TextContent(type="text", text=json.dumps(config, ensure_ascii=False, indent=2))]
|
|
314
407
|
|
|
408
|
+
elif name == "get_story_detail":
|
|
409
|
+
story_id = arguments["story_id"]
|
|
410
|
+
result = self.nexus.get_story_detail(story_id)
|
|
411
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
412
|
+
|
|
413
|
+
elif name == "create_task":
|
|
414
|
+
task_data = arguments["task_data"]
|
|
415
|
+
result = self.nexus.create_task_by_api(task_data)
|
|
416
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
417
|
+
|
|
418
|
+
elif name == "assign_task":
|
|
419
|
+
task_id = arguments["task_id"]
|
|
420
|
+
assigned_to = arguments["assigned_to"]
|
|
421
|
+
comment = arguments.get("comment")
|
|
422
|
+
result = self.nexus.assign_task(task_id, assigned_to, comment)
|
|
423
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
424
|
+
|
|
425
|
+
elif name == "get_bug_list":
|
|
426
|
+
product_id = arguments["product_id"]
|
|
427
|
+
assigned_to = arguments.get("assigned_to")
|
|
428
|
+
opened_by = arguments.get("opened_by")
|
|
429
|
+
resolved_by = arguments.get("resolved_by")
|
|
430
|
+
confirmed = arguments.get("confirmed")
|
|
431
|
+
status = arguments.get("status")
|
|
432
|
+
start_date = arguments.get("start_date")
|
|
433
|
+
end_date = arguments.get("end_date")
|
|
434
|
+
|
|
435
|
+
result = self.nexus.get_bug_list(
|
|
436
|
+
product_id, assigned_to, opened_by, resolved_by,
|
|
437
|
+
confirmed, status, start_date, end_date
|
|
438
|
+
)
|
|
439
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
440
|
+
|
|
441
|
+
elif name == "resolve_bug":
|
|
442
|
+
product_id = arguments["product_id"]
|
|
443
|
+
bug_ids = arguments["bug_ids"]
|
|
444
|
+
resolution = arguments["resolution"]
|
|
445
|
+
resolved_by = arguments["resolved_by"]
|
|
446
|
+
resolved_build = arguments.get("resolved_build")
|
|
447
|
+
|
|
448
|
+
result = self.nexus.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
449
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
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
|
+
|
|
315
458
|
else:
|
|
316
459
|
raise ValueError(f"未知的工具: {name}")
|
|
317
460
|
|
package/src/zentao_nexus.py
CHANGED
|
@@ -103,115 +103,350 @@ class ZenTaoAPIClient:
|
|
|
103
103
|
|
|
104
104
|
# 写死的配置
|
|
105
105
|
BASE_URL = "https://zentao.bangdao-tech.com"
|
|
106
|
-
|
|
107
|
-
SECRET_KEY = "aa287b7b5a4ef5d051f82fb1825ca1ac"
|
|
108
|
-
CASE_SECRET_KEY = "db1d522a190f1135c6e7c324bd337fda"
|
|
109
|
-
CASE_ACCOUNT = "AUTOTEST"
|
|
106
|
+
TEST_BASE_URL = "https://test-zendao.bangdao-tech.com"
|
|
110
107
|
|
|
111
|
-
def __init__(self, silent=True):
|
|
108
|
+
def __init__(self, silent=True, use_test_env=False):
|
|
112
109
|
"""
|
|
113
110
|
初始化禅道API客户端
|
|
114
111
|
|
|
115
112
|
Args:
|
|
116
113
|
silent (bool): 是否静默模式,不输出调试信息
|
|
114
|
+
use_test_env (bool): 是否使用测试环境
|
|
117
115
|
"""
|
|
118
|
-
self.base_url = self.BASE_URL
|
|
119
|
-
self.account = self.ACCOUNT
|
|
120
|
-
self.secret_key = self.SECRET_KEY
|
|
121
|
-
self.case_secret_key = self.CASE_SECRET_KEY
|
|
116
|
+
self.base_url = self.TEST_BASE_URL if use_test_env else self.BASE_URL
|
|
122
117
|
self.silent = silent
|
|
118
|
+
self.use_test_env = use_test_env
|
|
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)")
|
|
123
128
|
|
|
124
|
-
def
|
|
125
|
-
"""
|
|
126
|
-
token_string = f"
|
|
127
|
-
md5_hash = hashlib.md5()
|
|
128
|
-
md5_hash.update(token_string.encode('utf-8'))
|
|
129
|
-
return md5_hash.hexdigest()
|
|
130
|
-
|
|
131
|
-
def generate_case_bdtoken(self, timestamp):
|
|
132
|
-
"""生成用例创建用的BDTOKEN"""
|
|
133
|
-
token_string = f"accountfmtamp{timestamp}{self.case_secret_key}"
|
|
129
|
+
def generate_api_token(self, timestamp):
|
|
130
|
+
"""生成统一API的token(md5(code + key + time),32位小写)"""
|
|
131
|
+
token_string = f"{self.code}{self.key}{timestamp}"
|
|
134
132
|
md5_hash = hashlib.md5()
|
|
135
133
|
md5_hash.update(token_string.encode('utf-8'))
|
|
136
|
-
return md5_hash.hexdigest()
|
|
134
|
+
return md5_hash.hexdigest().lower()
|
|
137
135
|
|
|
138
|
-
def
|
|
139
|
-
"""
|
|
136
|
+
def _call_new_api(self, function_name, params=None, form_data=None, files=None, m_param='allneedlist'):
|
|
137
|
+
"""
|
|
138
|
+
调用新的API接口(使用code、time、token认证)
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
function_name: API函数名(f参数)
|
|
142
|
+
params: URL参数(字典)
|
|
143
|
+
form_data: POST form-data数据(字典或列表,列表格式用于数组参数)
|
|
144
|
+
files: 文件上传(字典,用于文件上传接口)
|
|
145
|
+
m_param: m参数值(默认allneedlist,某些接口可能需要其他值如bug)
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict: API响应结果
|
|
149
|
+
"""
|
|
140
150
|
current_timestamp = int(time.time())
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
151
|
+
token = self.generate_api_token(current_timestamp)
|
|
152
|
+
|
|
153
|
+
api_url = f"{self.base_url}/zentaopms/www/api.php"
|
|
154
|
+
url_params = {
|
|
155
|
+
'code': self.code,
|
|
156
|
+
'time': current_timestamp,
|
|
157
|
+
'token': token,
|
|
158
|
+
'm': m_param,
|
|
159
|
+
'f': function_name
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
# 合并URL参数
|
|
163
|
+
if params:
|
|
164
|
+
url_params.update(params)
|
|
165
|
+
|
|
151
166
|
headers = {
|
|
152
|
-
'BDTOKEN': bdtoken,
|
|
153
167
|
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
154
168
|
}
|
|
155
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
|
+
|
|
156
232
|
try:
|
|
157
|
-
if
|
|
158
|
-
|
|
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:
|
|
237
|
+
# 如果form_data是列表,直接使用(用于数组参数)
|
|
238
|
+
# 如果是字典,需要检查是否有数组类型的值
|
|
239
|
+
if isinstance(form_data, dict):
|
|
240
|
+
# 检查是否有数组类型的值,转换为元组列表格式
|
|
241
|
+
processed_data = []
|
|
242
|
+
for key, value in form_data.items():
|
|
243
|
+
if isinstance(value, (list, tuple)):
|
|
244
|
+
# 数组参数,使用元组列表格式
|
|
245
|
+
for item in value:
|
|
246
|
+
processed_data.append((key, str(item)))
|
|
247
|
+
else:
|
|
248
|
+
processed_data.append((key, str(value)))
|
|
249
|
+
form_data = processed_data
|
|
250
|
+
|
|
251
|
+
response = requests.post(api_url, params=url_params, headers=headers, data=form_data, timeout=30)
|
|
159
252
|
else:
|
|
160
|
-
headers
|
|
161
|
-
|
|
253
|
+
response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
|
|
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}")
|
|
162
262
|
|
|
163
263
|
try:
|
|
164
|
-
|
|
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
|
|
165
269
|
except json.JSONDecodeError:
|
|
270
|
+
print(f"\n响应Body (非JSON):")
|
|
271
|
+
print(response.text[:1000]) # 只显示前1000个字符
|
|
272
|
+
print("=" * 80 + "\n")
|
|
166
273
|
return {
|
|
167
|
-
"status":
|
|
168
|
-
"message": "
|
|
274
|
+
"status": 0,
|
|
275
|
+
"message": "fail",
|
|
276
|
+
"info": "响应不是有效的JSON格式",
|
|
277
|
+
"data": {},
|
|
169
278
|
"raw_response": response.text
|
|
170
279
|
}
|
|
171
280
|
except requests.exceptions.RequestException as e:
|
|
281
|
+
print(f"\n【请求异常】")
|
|
282
|
+
print(f"错误信息: {str(e)}")
|
|
283
|
+
print("=" * 80 + "\n")
|
|
172
284
|
return {
|
|
173
|
-
"status":
|
|
174
|
-
"message":
|
|
285
|
+
"status": 0,
|
|
286
|
+
"message": "fail",
|
|
287
|
+
"info": f"请求失败: {str(e)}",
|
|
288
|
+
"data": {}
|
|
175
289
|
}
|
|
176
290
|
|
|
177
|
-
def
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
bdtoken = self.generate_case_bdtoken(current_timestamp)
|
|
181
|
-
|
|
182
|
-
url = f"{self.base_url}/zentaopms/www/index.php"
|
|
183
|
-
params = {
|
|
184
|
-
'm': 'allneedlist',
|
|
185
|
-
'f': 'createcasebyapi',
|
|
186
|
-
'account': self.CASE_ACCOUNT,
|
|
187
|
-
'tamp': current_timestamp
|
|
188
|
-
}
|
|
291
|
+
def get_story_detail(self, story_id):
|
|
292
|
+
"""
|
|
293
|
+
获取需求详情
|
|
189
294
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
295
|
+
Args:
|
|
296
|
+
story_id: 需求ID
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict: 需求详情
|
|
300
|
+
"""
|
|
301
|
+
params = {'storyID': story_id}
|
|
302
|
+
return self._call_new_api('getStoryDetail', params=params)
|
|
303
|
+
|
|
304
|
+
def create_task_by_api(self, task_data):
|
|
305
|
+
"""
|
|
306
|
+
创建任务
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
task_data: 任务数据(字典,form-data格式)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
dict: 创建结果,包含taskID
|
|
313
|
+
"""
|
|
314
|
+
return self._call_new_api('createtaskbyapi', form_data=task_data)
|
|
315
|
+
|
|
316
|
+
def assign_task(self, task_id, assigned_to, comment=None):
|
|
317
|
+
"""
|
|
318
|
+
指派任务
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
task_id: 任务ID(必传)
|
|
322
|
+
assigned_to: 新的指派人(姓名或工号)
|
|
323
|
+
comment: 指派备注(可选)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
dict: 指派结果
|
|
327
|
+
"""
|
|
328
|
+
params = {'taskID': task_id}
|
|
329
|
+
form_data = {'assignedTo': assigned_to}
|
|
330
|
+
if comment:
|
|
331
|
+
form_data['comment'] = comment
|
|
332
|
+
return self._call_new_api('assignTask', params=params, form_data=form_data)
|
|
333
|
+
|
|
334
|
+
def get_bug_list(self, product_id, assigned_to=None, opened_by=None, resolved_by=None,
|
|
335
|
+
confirmed=None, status=None, start_date=None, end_date=None):
|
|
336
|
+
"""
|
|
337
|
+
拉取已确认/未解决的BUG
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
product_id: 产品ID(必传)
|
|
341
|
+
assigned_to: 指派人(可选,姓名或工号)
|
|
342
|
+
opened_by: 创建人(可选,姓名或工号)
|
|
343
|
+
resolved_by: 解决人(可选,姓名或工号)
|
|
344
|
+
confirmed: 是否确认(可选,2已确认 1未确认 0或不传是所有)
|
|
345
|
+
status: 状态(可选,激活/已解决/已关闭)
|
|
346
|
+
start_date: 创建开始日期(可选)
|
|
347
|
+
end_date: 创建截止日期(可选)
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
dict: BUG列表
|
|
351
|
+
"""
|
|
352
|
+
params = {'productID': product_id}
|
|
353
|
+
form_data = {}
|
|
354
|
+
|
|
355
|
+
if assigned_to:
|
|
356
|
+
form_data['assignedTo'] = assigned_to
|
|
357
|
+
if opened_by:
|
|
358
|
+
form_data['openedBy'] = opened_by
|
|
359
|
+
if resolved_by:
|
|
360
|
+
form_data['resolvedBy'] = resolved_by
|
|
361
|
+
if confirmed is not None:
|
|
362
|
+
form_data['confirmed'] = str(confirmed)
|
|
363
|
+
if status:
|
|
364
|
+
form_data['status'] = status
|
|
365
|
+
if start_date:
|
|
366
|
+
form_data['startDate'] = start_date
|
|
367
|
+
if end_date:
|
|
368
|
+
form_data['endDate'] = end_date
|
|
369
|
+
|
|
370
|
+
return self._call_new_api('getBugList', params=params, form_data=form_data if form_data else None)
|
|
371
|
+
|
|
372
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
373
|
+
"""
|
|
374
|
+
批量解决BUG
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
product_id: 产品ID(必传)
|
|
378
|
+
bug_ids: BUG ID列表(必传),可以是单个ID或ID列表
|
|
379
|
+
resolution: 解决方案(可选,默认fixed),可选值:
|
|
380
|
+
fixed: 修复解决
|
|
381
|
+
bydesign: 设计如此
|
|
382
|
+
reqchange: 修改需求
|
|
383
|
+
duplicate: 重复上报
|
|
384
|
+
external: 外部原因
|
|
385
|
+
notrepro: 无法重现
|
|
386
|
+
postponed: 延期处理
|
|
387
|
+
willnotfix: 不予解决
|
|
388
|
+
tostory: 转为需求
|
|
389
|
+
history: 历史遗留
|
|
390
|
+
configchange: 调整配置
|
|
391
|
+
resolved_by: 解决人(必传,姓名或工号)
|
|
392
|
+
resolved_build: 解决版本ID(可选,如果未提供则默认使用"trunk")
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
dict: 解决结果
|
|
396
|
+
"""
|
|
397
|
+
# 如果没有提供 resolved_build,使用默认值 "trunk"
|
|
398
|
+
if not resolved_build:
|
|
399
|
+
resolved_build = "trunk"
|
|
400
|
+
|
|
401
|
+
params = {'productID': product_id}
|
|
402
|
+
form_data = {
|
|
403
|
+
'resolution': resolution,
|
|
404
|
+
'resolvedBy': resolved_by
|
|
193
405
|
}
|
|
194
406
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
407
|
+
# bugID[] 数组格式 - 转换为列表格式,_call_new_api会自动处理
|
|
408
|
+
if isinstance(bug_ids, (list, tuple)):
|
|
409
|
+
form_data['bugID[]'] = [str(bug_id) for bug_id in bug_ids]
|
|
410
|
+
else:
|
|
411
|
+
form_data['bugID[]'] = [str(bug_ids)]
|
|
412
|
+
|
|
413
|
+
if resolved_build:
|
|
414
|
+
form_data['resolvedBuild'] = str(resolved_build)
|
|
415
|
+
|
|
416
|
+
return self._call_new_api('resolveBug', params=params, form_data=form_data)
|
|
417
|
+
|
|
418
|
+
def close_bug(self, bug_id, comment="AI关闭"):
|
|
419
|
+
"""
|
|
420
|
+
关闭BUG
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
bug_id: BUG ID(必传)
|
|
424
|
+
comment: 关闭备注(可选,默认"AI关闭")
|
|
425
|
+
|
|
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":
|
|
211
435
|
return {
|
|
212
|
-
"status":
|
|
213
|
-
"message":
|
|
436
|
+
"status": 1,
|
|
437
|
+
"message": "success",
|
|
438
|
+
"info": "success"
|
|
214
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)
|
|
446
|
+
|
|
447
|
+
def create_case_by_api(self, case_data, use_form_data=True):
|
|
448
|
+
"""通过API创建测试用例"""
|
|
449
|
+
return self._call_new_api('createcasebyapi', form_data=case_data)
|
|
215
450
|
|
|
216
451
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
217
452
|
"""上传图片到BUG附件"""
|
|
@@ -221,25 +456,6 @@ class ZenTaoAPIClient:
|
|
|
221
456
|
"message": f"图片文件不存在: {image_path}"
|
|
222
457
|
}
|
|
223
458
|
|
|
224
|
-
current_timestamp = int(datetime.now().timestamp())
|
|
225
|
-
url = f"{self.base_url}/zentaopms/www/index.php"
|
|
226
|
-
params = {
|
|
227
|
-
'm': 'allneedlist',
|
|
228
|
-
'f': 'uploadbyapi',
|
|
229
|
-
'account': self.CASE_ACCOUNT,
|
|
230
|
-
'tamp': current_timestamp
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
token_string = f"accountfmtamp{current_timestamp}{self.case_secret_key}"
|
|
234
|
-
md5_hash = hashlib.md5()
|
|
235
|
-
md5_hash.update(token_string.encode('utf-8'))
|
|
236
|
-
bdtoken = md5_hash.hexdigest()
|
|
237
|
-
|
|
238
|
-
headers = {
|
|
239
|
-
'BDTOKEN': bdtoken,
|
|
240
|
-
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
241
|
-
}
|
|
242
|
-
|
|
243
459
|
filename = custom_filename or os.path.basename(image_path)
|
|
244
460
|
|
|
245
461
|
try:
|
|
@@ -247,22 +463,8 @@ class ZenTaoAPIClient:
|
|
|
247
463
|
files = {
|
|
248
464
|
'files[]': (filename, f, 'application/octet-stream')
|
|
249
465
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try:
|
|
254
|
-
return response.json()
|
|
255
|
-
except json.JSONDecodeError:
|
|
256
|
-
return {
|
|
257
|
-
"status": -1,
|
|
258
|
-
"message": "响应不是有效的JSON格式",
|
|
259
|
-
"raw_response": response.text
|
|
260
|
-
}
|
|
261
|
-
except requests.exceptions.RequestException as e:
|
|
262
|
-
return {
|
|
263
|
-
"status": -1,
|
|
264
|
-
"message": f"请求失败: {str(e)}"
|
|
265
|
-
}
|
|
466
|
+
form_data = {'id': bug_id}
|
|
467
|
+
return self._call_new_api('uploadbyapi', form_data=form_data, files=files)
|
|
266
468
|
except Exception as e:
|
|
267
469
|
return {
|
|
268
470
|
"status": -1,
|
|
@@ -467,6 +669,15 @@ class ZenTaoNexus:
|
|
|
467
669
|
LOWER_BUG = "0"
|
|
468
670
|
REGRESSION = "0"
|
|
469
671
|
|
|
672
|
+
# 职位类型到任务类型的映射
|
|
673
|
+
ROLE_TO_TASK_TYPE = {
|
|
674
|
+
"测试": "4", # 测试
|
|
675
|
+
"前端": "8", # 前端开发
|
|
676
|
+
"后端": "3", # 后端开发
|
|
677
|
+
"其它": "20", # 其他
|
|
678
|
+
"其他": "20" # 其他(兼容两种写法)
|
|
679
|
+
}
|
|
680
|
+
|
|
470
681
|
def __init__(self, config_path: str = None):
|
|
471
682
|
"""
|
|
472
683
|
初始化ZenTao Nexus工具
|
|
@@ -505,6 +716,10 @@ class ZenTaoNexus:
|
|
|
505
716
|
env_title_prefix = os.getenv("ZENTAO_TITLE_PREFIX") or os.getenv("TITLE_PREFIX")
|
|
506
717
|
self.title_prefix = env_title_prefix or self.config_loader.get("title_prefix", "")
|
|
507
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
|
+
|
|
508
723
|
# 创建API客户端
|
|
509
724
|
self.client = ZenTaoAPIClient(silent=True)
|
|
510
725
|
|
|
@@ -621,7 +836,8 @@ class ZenTaoNexus:
|
|
|
621
836
|
"product_id": self.product_id,
|
|
622
837
|
"opened_by": self.opened_by,
|
|
623
838
|
"keywords": self.keywords,
|
|
624
|
-
"title_prefix": self.title_prefix
|
|
839
|
+
"title_prefix": self.title_prefix,
|
|
840
|
+
"role_type": self.role_type
|
|
625
841
|
}
|
|
626
842
|
|
|
627
843
|
def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
|
|
@@ -635,6 +851,68 @@ class ZenTaoNexus:
|
|
|
635
851
|
def submit_csv_cases_to_zentao(self, csv_file_path, case_type=None):
|
|
636
852
|
"""将CSV文件中的所有测试用例提交到禅道"""
|
|
637
853
|
return self.client.submit_csv_cases_to_zentao(csv_file_path, case_type, self.product_id, self.opened_by, self.keywords)
|
|
854
|
+
|
|
855
|
+
def get_story_detail(self, story_id):
|
|
856
|
+
"""获取需求详情"""
|
|
857
|
+
return self.client.get_story_detail(story_id)
|
|
858
|
+
|
|
859
|
+
def create_task_by_api(self, task_data):
|
|
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
|
+
|
|
897
|
+
return self.client.create_task_by_api(task_data)
|
|
898
|
+
|
|
899
|
+
def assign_task(self, task_id, assigned_to, comment=None):
|
|
900
|
+
"""指派任务"""
|
|
901
|
+
return self.client.assign_task(task_id, assigned_to, comment)
|
|
902
|
+
|
|
903
|
+
def get_bug_list(self, product_id, assigned_to=None, opened_by=None, resolved_by=None,
|
|
904
|
+
confirmed=None, status=None, start_date=None, end_date=None):
|
|
905
|
+
"""拉取已确认/未解决的BUG"""
|
|
906
|
+
return self.client.get_bug_list(product_id, assigned_to, opened_by, resolved_by,
|
|
907
|
+
confirmed, status, start_date, end_date)
|
|
908
|
+
|
|
909
|
+
def resolve_bug(self, product_id, bug_ids, resolution="fixed", resolved_by=None, resolved_build=None):
|
|
910
|
+
"""批量解决BUG"""
|
|
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)
|
|
638
916
|
|
|
639
917
|
|
|
640
918
|
# 便捷函数
|