@bangdao-ai/zentao-mcp 1.0.0 → 1.1.0
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
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 中配置
|
|
@@ -53,7 +157,7 @@ npx -y @bangdao-ai/zentao-mcp@latest
|
|
|
53
157
|
|
|
54
158
|
### 兜底配置(可选)
|
|
55
159
|
|
|
56
|
-
如果未在环境变量中配置,可以使用配置文件 `bug_config.json
|
|
160
|
+
如果未在环境变量中配置,可以使用配置文件 `bug_config.json`。配置文件路径可通过环境变量 `ZENTAO_CONFIG_PATH` 指定。
|
|
57
161
|
|
|
58
162
|
## MCP 工具
|
|
59
163
|
|
|
@@ -131,21 +235,95 @@ npx -y @bangdao-ai/zentao-mcp@latest
|
|
|
131
235
|
|
|
132
236
|
获取当前配置信息。
|
|
133
237
|
|
|
238
|
+
### 7. get_story_detail
|
|
239
|
+
|
|
240
|
+
获取需求详情。
|
|
241
|
+
|
|
242
|
+
**参数**:
|
|
243
|
+
- `story_id` (必需): 需求ID
|
|
244
|
+
|
|
245
|
+
**返回**:包含需求标题、描述、验收标准、创建人、指派人等详细信息
|
|
246
|
+
|
|
247
|
+
### 8. create_task
|
|
248
|
+
|
|
249
|
+
创建任务。
|
|
250
|
+
|
|
251
|
+
**参数**:
|
|
252
|
+
- `task_data` (必需): 任务数据对象,包含任务相关字段(form-data格式)
|
|
253
|
+
|
|
254
|
+
**注意**:任务数据的具体字段请参考禅道API文档,常见字段包括:
|
|
255
|
+
- `name`: 任务名称
|
|
256
|
+
- `assignedTo`: 指派人
|
|
257
|
+
- `pri`: 优先级
|
|
258
|
+
- `estimate`: 预计工时
|
|
259
|
+
- `desc`: 任务描述
|
|
260
|
+
- 等其他字段
|
|
261
|
+
|
|
262
|
+
### 9. assign_task
|
|
263
|
+
|
|
264
|
+
指派任务。
|
|
265
|
+
|
|
266
|
+
**参数**:
|
|
267
|
+
- `task_id` (必需): 任务ID
|
|
268
|
+
- `assigned_to` (必需): 新的指派人(姓名或工号)
|
|
269
|
+
- `comment` (可选): 指派备注
|
|
270
|
+
|
|
271
|
+
### 10. get_bug_list
|
|
272
|
+
|
|
273
|
+
拉取已确认/未解决的BUG列表。
|
|
274
|
+
|
|
275
|
+
**参数**:
|
|
276
|
+
- `product_id` (必需): 产品ID
|
|
277
|
+
- `assigned_to` (可选): 指派人(姓名或工号)
|
|
278
|
+
- `opened_by` (可选): 创建人(姓名或工号)
|
|
279
|
+
- `resolved_by` (可选): 解决人(姓名或工号)
|
|
280
|
+
- `confirmed` (可选): 是否确认(2=已确认,1=未确认,0或不传=所有)
|
|
281
|
+
- `status` (可选): 状态(激活/已解决/已关闭)
|
|
282
|
+
- `start_date` (可选): 创建开始日期(格式:YYYY-MM-DD)
|
|
283
|
+
- `end_date` (可选): 创建截止日期(格式:YYYY-MM-DD)
|
|
284
|
+
|
|
285
|
+
### 11. resolve_bug
|
|
286
|
+
|
|
287
|
+
批量解决BUG。
|
|
288
|
+
|
|
289
|
+
**参数**:
|
|
290
|
+
- `product_id` (必需): 产品ID
|
|
291
|
+
- `bug_ids` (必需): BUG ID列表(数组)
|
|
292
|
+
- `resolution` (必需): 解决方案,可选值:
|
|
293
|
+
- `fixed`: 修复解决
|
|
294
|
+
- `bydesign`: 设计如此
|
|
295
|
+
- `reqchange`: 修改需求
|
|
296
|
+
- `duplicate`: 重复上报
|
|
297
|
+
- `external`: 外部原因
|
|
298
|
+
- `notrepro`: 无法重现
|
|
299
|
+
- `postponed`: 延期处理
|
|
300
|
+
- `willnotfix`: 不予解决
|
|
301
|
+
- `tostory`: 转为需求
|
|
302
|
+
- `history`: 历史遗留
|
|
303
|
+
- `configchange`: 调整配置
|
|
304
|
+
- `resolved_by` (必需): 解决人(姓名或工号)
|
|
305
|
+
- `resolved_build` (可选): 解决版本ID(当解决方案为`fixed`时必传)
|
|
306
|
+
|
|
134
307
|
## 项目结构
|
|
135
308
|
|
|
136
309
|
```
|
|
137
310
|
禅道mcp/
|
|
138
|
-
├──
|
|
139
|
-
├── package.json # npm 包配置
|
|
140
|
-
├── src/
|
|
311
|
+
├── src/ # 源码目录
|
|
141
312
|
│ ├── mcp_server.py # MCP服务器主文件
|
|
142
313
|
│ └── zentao_nexus.py # 禅道工具类
|
|
314
|
+
├── scripts/ # 脚本目录
|
|
315
|
+
│ └── publish/ # 发布脚本
|
|
316
|
+
├── docs/ # 文档目录
|
|
317
|
+
│ ├── PUBLISH.md # 发布指南
|
|
318
|
+
│ └── README-PUBLISH.md # 发布脚本使用指南
|
|
319
|
+
├── index.js # npm 入口文件
|
|
320
|
+
├── package.json # npm 包配置
|
|
143
321
|
├── requirements.txt # Python 依赖
|
|
144
|
-
|
|
145
|
-
├── PUBLISH.md # 发布指南
|
|
146
|
-
└── bug_config.example.json # 兜底配置示例
|
|
322
|
+
└── README.md # 项目说明
|
|
147
323
|
```
|
|
148
324
|
|
|
325
|
+
详细的项目结构说明请参考 [docs/PROJECT-STRUCTURE.md](docs/PROJECT-STRUCTURE.md)
|
|
326
|
+
|
|
149
327
|
## 发布
|
|
150
328
|
|
|
151
329
|
参考 [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.0",
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
package/src/mcp_server.py
CHANGED
|
@@ -127,6 +127,86 @@ 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(如果解决方案是fixed时,解决版本必传)"}
|
|
207
|
+
},
|
|
208
|
+
"required": ["product_id", "bug_ids", "resolution", "resolved_by"]
|
|
209
|
+
}
|
|
130
210
|
)
|
|
131
211
|
]
|
|
132
212
|
|
|
@@ -312,6 +392,49 @@ class ZenTaoMCPServer:
|
|
|
312
392
|
config = self.nexus.get_config()
|
|
313
393
|
return [TextContent(type="text", text=json.dumps(config, ensure_ascii=False, indent=2))]
|
|
314
394
|
|
|
395
|
+
elif name == "get_story_detail":
|
|
396
|
+
story_id = arguments["story_id"]
|
|
397
|
+
result = self.nexus.get_story_detail(story_id)
|
|
398
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
399
|
+
|
|
400
|
+
elif name == "create_task":
|
|
401
|
+
task_data = arguments["task_data"]
|
|
402
|
+
result = self.nexus.create_task_by_api(task_data)
|
|
403
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
404
|
+
|
|
405
|
+
elif name == "assign_task":
|
|
406
|
+
task_id = arguments["task_id"]
|
|
407
|
+
assigned_to = arguments["assigned_to"]
|
|
408
|
+
comment = arguments.get("comment")
|
|
409
|
+
result = self.nexus.assign_task(task_id, assigned_to, comment)
|
|
410
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
411
|
+
|
|
412
|
+
elif name == "get_bug_list":
|
|
413
|
+
product_id = arguments["product_id"]
|
|
414
|
+
assigned_to = arguments.get("assigned_to")
|
|
415
|
+
opened_by = arguments.get("opened_by")
|
|
416
|
+
resolved_by = arguments.get("resolved_by")
|
|
417
|
+
confirmed = arguments.get("confirmed")
|
|
418
|
+
status = arguments.get("status")
|
|
419
|
+
start_date = arguments.get("start_date")
|
|
420
|
+
end_date = arguments.get("end_date")
|
|
421
|
+
|
|
422
|
+
result = self.nexus.get_bug_list(
|
|
423
|
+
product_id, assigned_to, opened_by, resolved_by,
|
|
424
|
+
confirmed, status, start_date, end_date
|
|
425
|
+
)
|
|
426
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
427
|
+
|
|
428
|
+
elif name == "resolve_bug":
|
|
429
|
+
product_id = arguments["product_id"]
|
|
430
|
+
bug_ids = arguments["bug_ids"]
|
|
431
|
+
resolution = arguments["resolution"]
|
|
432
|
+
resolved_by = arguments["resolved_by"]
|
|
433
|
+
resolved_build = arguments.get("resolved_build")
|
|
434
|
+
|
|
435
|
+
result = self.nexus.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
436
|
+
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
|
|
437
|
+
|
|
315
438
|
else:
|
|
316
439
|
raise ValueError(f"未知的工具: {name}")
|
|
317
440
|
|
package/src/zentao_nexus.py
CHANGED
|
@@ -103,23 +103,29 @@ class ZenTaoAPIClient:
|
|
|
103
103
|
|
|
104
104
|
# 写死的配置
|
|
105
105
|
BASE_URL = "https://zentao.bangdao-tech.com"
|
|
106
|
+
TEST_BASE_URL = "https://test-zendao.bangdao-tech.com"
|
|
106
107
|
ACCOUNT = "KUBETEST"
|
|
107
108
|
SECRET_KEY = "aa287b7b5a4ef5d051f82fb1825ca1ac"
|
|
108
109
|
CASE_SECRET_KEY = "db1d522a190f1135c6e7c324bd337fda"
|
|
109
110
|
CASE_ACCOUNT = "AUTOTEST"
|
|
111
|
+
# 新API的配置
|
|
112
|
+
API_KEY = "1acca81c8a4d131cf7c86e772fa64351"
|
|
113
|
+
API_CODE = "test"
|
|
110
114
|
|
|
111
|
-
def __init__(self, silent=True):
|
|
115
|
+
def __init__(self, silent=True, use_test_env=False):
|
|
112
116
|
"""
|
|
113
117
|
初始化禅道API客户端
|
|
114
118
|
|
|
115
119
|
Args:
|
|
116
120
|
silent (bool): 是否静默模式,不输出调试信息
|
|
121
|
+
use_test_env (bool): 是否使用测试环境
|
|
117
122
|
"""
|
|
118
|
-
self.base_url = self.BASE_URL
|
|
123
|
+
self.base_url = self.TEST_BASE_URL if use_test_env else self.BASE_URL
|
|
119
124
|
self.account = self.ACCOUNT
|
|
120
125
|
self.secret_key = self.SECRET_KEY
|
|
121
126
|
self.case_secret_key = self.CASE_SECRET_KEY
|
|
122
127
|
self.silent = silent
|
|
128
|
+
self.use_test_env = use_test_env
|
|
123
129
|
|
|
124
130
|
def generate_bdtoken(self, timestamp):
|
|
125
131
|
"""生成BDTOKEN(用于BUG创建)"""
|
|
@@ -135,6 +141,214 @@ class ZenTaoAPIClient:
|
|
|
135
141
|
md5_hash.update(token_string.encode('utf-8'))
|
|
136
142
|
return md5_hash.hexdigest()
|
|
137
143
|
|
|
144
|
+
def generate_api_token(self, timestamp):
|
|
145
|
+
"""生成新API的token(md5(code + key + time),32位小写)"""
|
|
146
|
+
token_string = f"{self.API_CODE}{self.API_KEY}{timestamp}"
|
|
147
|
+
md5_hash = hashlib.md5()
|
|
148
|
+
md5_hash.update(token_string.encode('utf-8'))
|
|
149
|
+
return md5_hash.hexdigest().lower()
|
|
150
|
+
|
|
151
|
+
def _call_new_api(self, function_name, params=None, form_data=None):
|
|
152
|
+
"""
|
|
153
|
+
调用新的API接口(使用code、time、token认证)
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
function_name: API函数名(f参数)
|
|
157
|
+
params: URL参数(字典)
|
|
158
|
+
form_data: POST form-data数据(字典或列表,列表格式用于数组参数)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
dict: API响应结果
|
|
162
|
+
"""
|
|
163
|
+
current_timestamp = int(time.time())
|
|
164
|
+
token = self.generate_api_token(current_timestamp)
|
|
165
|
+
|
|
166
|
+
api_url = f"{self.base_url}/zentaopms/www/api.php"
|
|
167
|
+
url_params = {
|
|
168
|
+
'code': self.API_CODE,
|
|
169
|
+
'time': current_timestamp,
|
|
170
|
+
'token': token,
|
|
171
|
+
'm': 'allneedlist',
|
|
172
|
+
'f': function_name
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# 合并URL参数
|
|
176
|
+
if params:
|
|
177
|
+
url_params.update(params)
|
|
178
|
+
|
|
179
|
+
headers = {
|
|
180
|
+
'User-Agent': 'Python-ZenTao-Client/1.0'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if form_data:
|
|
185
|
+
# 如果form_data是列表,直接使用(用于数组参数)
|
|
186
|
+
# 如果是字典,需要检查是否有数组类型的值
|
|
187
|
+
if isinstance(form_data, dict):
|
|
188
|
+
# 检查是否有数组类型的值,转换为元组列表格式
|
|
189
|
+
processed_data = []
|
|
190
|
+
for key, value in form_data.items():
|
|
191
|
+
if isinstance(value, (list, tuple)):
|
|
192
|
+
# 数组参数,使用元组列表格式
|
|
193
|
+
for item in value:
|
|
194
|
+
processed_data.append((key, str(item)))
|
|
195
|
+
else:
|
|
196
|
+
processed_data.append((key, str(value)))
|
|
197
|
+
form_data = processed_data
|
|
198
|
+
|
|
199
|
+
response = requests.post(api_url, params=url_params, headers=headers, data=form_data, timeout=30)
|
|
200
|
+
else:
|
|
201
|
+
response = requests.post(api_url, params=url_params, headers=headers, timeout=30)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
return response.json()
|
|
205
|
+
except json.JSONDecodeError:
|
|
206
|
+
return {
|
|
207
|
+
"status": 0,
|
|
208
|
+
"message": "fail",
|
|
209
|
+
"info": "响应不是有效的JSON格式",
|
|
210
|
+
"data": {},
|
|
211
|
+
"raw_response": response.text
|
|
212
|
+
}
|
|
213
|
+
except requests.exceptions.RequestException as e:
|
|
214
|
+
return {
|
|
215
|
+
"status": 0,
|
|
216
|
+
"message": "fail",
|
|
217
|
+
"info": f"请求失败: {str(e)}",
|
|
218
|
+
"data": {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def get_story_detail(self, story_id):
|
|
222
|
+
"""
|
|
223
|
+
获取需求详情
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
story_id: 需求ID
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
dict: 需求详情
|
|
230
|
+
"""
|
|
231
|
+
params = {'storyID': story_id}
|
|
232
|
+
return self._call_new_api('getStoryDetail', params=params)
|
|
233
|
+
|
|
234
|
+
def create_task_by_api(self, task_data):
|
|
235
|
+
"""
|
|
236
|
+
创建任务
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
task_data: 任务数据(字典,form-data格式)
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
dict: 创建结果,包含taskID
|
|
243
|
+
"""
|
|
244
|
+
return self._call_new_api('createtaskbyapi', form_data=task_data)
|
|
245
|
+
|
|
246
|
+
def assign_task(self, task_id, assigned_to, comment=None):
|
|
247
|
+
"""
|
|
248
|
+
指派任务
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
task_id: 任务ID(必传)
|
|
252
|
+
assigned_to: 新的指派人(姓名或工号)
|
|
253
|
+
comment: 指派备注(可选)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
dict: 指派结果
|
|
257
|
+
"""
|
|
258
|
+
params = {'taskID': task_id}
|
|
259
|
+
form_data = {'assignedTo': assigned_to}
|
|
260
|
+
if comment:
|
|
261
|
+
form_data['comment'] = comment
|
|
262
|
+
return self._call_new_api('assignTask', params=params, form_data=form_data)
|
|
263
|
+
|
|
264
|
+
def get_bug_list(self, product_id, assigned_to=None, opened_by=None, resolved_by=None,
|
|
265
|
+
confirmed=None, status=None, start_date=None, end_date=None):
|
|
266
|
+
"""
|
|
267
|
+
拉取已确认/未解决的BUG
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
product_id: 产品ID(必传)
|
|
271
|
+
assigned_to: 指派人(可选,姓名或工号)
|
|
272
|
+
opened_by: 创建人(可选,姓名或工号)
|
|
273
|
+
resolved_by: 解决人(可选,姓名或工号)
|
|
274
|
+
confirmed: 是否确认(可选,2已确认 1未确认 0或不传是所有)
|
|
275
|
+
status: 状态(可选,激活/已解决/已关闭)
|
|
276
|
+
start_date: 创建开始日期(可选)
|
|
277
|
+
end_date: 创建截止日期(可选)
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
dict: BUG列表
|
|
281
|
+
"""
|
|
282
|
+
params = {'productID': product_id}
|
|
283
|
+
form_data = {}
|
|
284
|
+
|
|
285
|
+
if assigned_to:
|
|
286
|
+
form_data['assignedTo'] = assigned_to
|
|
287
|
+
if opened_by:
|
|
288
|
+
form_data['openedBy'] = opened_by
|
|
289
|
+
if resolved_by:
|
|
290
|
+
form_data['resolvedBy'] = resolved_by
|
|
291
|
+
if confirmed is not None:
|
|
292
|
+
form_data['confirmed'] = str(confirmed)
|
|
293
|
+
if status:
|
|
294
|
+
form_data['status'] = status
|
|
295
|
+
if start_date:
|
|
296
|
+
form_data['startDate'] = start_date
|
|
297
|
+
if end_date:
|
|
298
|
+
form_data['endDate'] = end_date
|
|
299
|
+
|
|
300
|
+
return self._call_new_api('getBugList', params=params, form_data=form_data if form_data else None)
|
|
301
|
+
|
|
302
|
+
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
303
|
+
"""
|
|
304
|
+
批量解决BUG
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
product_id: 产品ID(必传)
|
|
308
|
+
bug_ids: BUG ID列表(必传),可以是单个ID或ID列表
|
|
309
|
+
resolution: 解决方案(必传),可选值:
|
|
310
|
+
fixed: 修复解决
|
|
311
|
+
bydesign: 设计如此
|
|
312
|
+
reqchange: 修改需求
|
|
313
|
+
duplicate: 重复上报
|
|
314
|
+
external: 外部原因
|
|
315
|
+
notrepro: 无法重现
|
|
316
|
+
postponed: 延期处理
|
|
317
|
+
willnotfix: 不予解决
|
|
318
|
+
tostory: 转为需求
|
|
319
|
+
history: 历史遗留
|
|
320
|
+
configchange: 调整配置
|
|
321
|
+
resolved_by: 解决人(必传,姓名或工号)
|
|
322
|
+
resolved_build: 解决版本ID(如果解决方案是fixed时,解决版本必传)
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
dict: 解决结果
|
|
326
|
+
"""
|
|
327
|
+
if resolution == 'fixed' and not resolved_build:
|
|
328
|
+
return {
|
|
329
|
+
"status": 0,
|
|
330
|
+
"message": "fail",
|
|
331
|
+
"info": "解决方案为fixed时,解决版本(resolvedBuild)必传",
|
|
332
|
+
"data": {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
params = {'productID': product_id}
|
|
336
|
+
form_data = {
|
|
337
|
+
'resolution': resolution,
|
|
338
|
+
'resolvedBy': resolved_by
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# bugID[] 数组格式 - 转换为列表格式,_call_new_api会自动处理
|
|
342
|
+
if isinstance(bug_ids, (list, tuple)):
|
|
343
|
+
form_data['bugID[]'] = [str(bug_id) for bug_id in bug_ids]
|
|
344
|
+
else:
|
|
345
|
+
form_data['bugID[]'] = [str(bug_ids)]
|
|
346
|
+
|
|
347
|
+
if resolved_build:
|
|
348
|
+
form_data['resolvedBuild'] = str(resolved_build)
|
|
349
|
+
|
|
350
|
+
return self._call_new_api('resolveBug', params=params, form_data=form_data)
|
|
351
|
+
|
|
138
352
|
def create_bug_by_api(self, bug_data, use_form_data=True):
|
|
139
353
|
"""通过API创建bug"""
|
|
140
354
|
current_timestamp = int(time.time())
|
|
@@ -635,6 +849,28 @@ class ZenTaoNexus:
|
|
|
635
849
|
def submit_csv_cases_to_zentao(self, csv_file_path, case_type=None):
|
|
636
850
|
"""将CSV文件中的所有测试用例提交到禅道"""
|
|
637
851
|
return self.client.submit_csv_cases_to_zentao(csv_file_path, case_type, self.product_id, self.opened_by, self.keywords)
|
|
852
|
+
|
|
853
|
+
def get_story_detail(self, story_id):
|
|
854
|
+
"""获取需求详情"""
|
|
855
|
+
return self.client.get_story_detail(story_id)
|
|
856
|
+
|
|
857
|
+
def create_task_by_api(self, task_data):
|
|
858
|
+
"""创建任务"""
|
|
859
|
+
return self.client.create_task_by_api(task_data)
|
|
860
|
+
|
|
861
|
+
def assign_task(self, task_id, assigned_to, comment=None):
|
|
862
|
+
"""指派任务"""
|
|
863
|
+
return self.client.assign_task(task_id, assigned_to, comment)
|
|
864
|
+
|
|
865
|
+
def get_bug_list(self, product_id, assigned_to=None, opened_by=None, resolved_by=None,
|
|
866
|
+
confirmed=None, status=None, start_date=None, end_date=None):
|
|
867
|
+
"""拉取已确认/未解决的BUG"""
|
|
868
|
+
return self.client.get_bug_list(product_id, assigned_to, opened_by, resolved_by,
|
|
869
|
+
confirmed, status, start_date, end_date)
|
|
870
|
+
|
|
871
|
+
def resolve_bug(self, product_id, bug_ids, resolution, resolved_by, resolved_build=None):
|
|
872
|
+
"""批量解决BUG"""
|
|
873
|
+
return self.client.resolve_bug(product_id, bug_ids, resolution, resolved_by, resolved_build)
|
|
638
874
|
|
|
639
875
|
|
|
640
876
|
# 便捷函数
|