@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 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
- 通过 npm/npx 使用(推荐):
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`(参考 `bug_config.example.json`)。配置文件路径可通过环境变量 `ZENTAO_CONFIG_PATH` 指定。
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
- ├── index.js # npm 入口文件
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
- ├── README.md # 项目说明
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.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "禅道Bug管理系统MCP工具",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "zentao-mcp": "./index.js"
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
@@ -3,4 +3,5 @@ requests>=2.31.0
3
3
  python-dotenv>=1.0.0
4
4
  fastapi>=0.104.0
5
5
  uvicorn[standard]>=0.24.0
6
+ pytest>=7.0.0
6
7
 
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
 
@@ -103,115 +103,350 @@ class ZenTaoAPIClient:
103
103
 
104
104
  # 写死的配置
105
105
  BASE_URL = "https://zentao.bangdao-tech.com"
106
- ACCOUNT = "KUBETEST"
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 generate_bdtoken(self, timestamp):
125
- """生成BDTOKEN(用于BUG创建)"""
126
- token_string = f"accountfmtamp{timestamp}{self.secret_key}"
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 create_bug_by_api(self, bug_data, use_form_data=True):
139
- """通过API创建bug"""
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
- bdtoken = self.generate_bdtoken(current_timestamp)
142
-
143
- url = f"{self.base_url}/zentaopms/www/index.php"
144
- params = {
145
- 'm': 'allneedlist',
146
- 'f': 'createbugbyapi',
147
- 'account': self.account,
148
- 'tamp': current_timestamp
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 use_form_data:
158
- response = requests.post(url, params=params, headers=headers, data=bug_data, timeout=30)
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['Content-Type'] = 'application/json'
161
- response = requests.post(url, params=params, headers=headers, json=bug_data, timeout=30)
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
- return response.json()
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": -1,
168
- "message": "响应不是有效的JSON格式",
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": -1,
174
- "message": f"请求失败: {str(e)}"
285
+ "status": 0,
286
+ "message": "fail",
287
+ "info": f"请求失败: {str(e)}",
288
+ "data": {}
175
289
  }
176
290
 
177
- def create_case_by_api(self, case_data, use_form_data=True):
178
- """通过API创建测试用例"""
179
- current_timestamp = int(time.time())
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
- headers = {
191
- 'BDTOKEN': bdtoken,
192
- 'User-Agent': 'Python-ZenTao-Client/1.0'
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
- try:
196
- if use_form_data:
197
- response = requests.post(url, params=params, headers=headers, data=case_data, timeout=30)
198
- else:
199
- headers['Content-Type'] = 'application/json'
200
- response = requests.post(url, params=params, headers=headers, json=case_data, timeout=30)
201
-
202
- try:
203
- return response.json()
204
- except json.JSONDecodeError:
205
- return {
206
- "status": -1,
207
- "message": "响应不是有效的JSON格式",
208
- "raw_response": response.text
209
- }
210
- except requests.exceptions.RequestException as e:
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": -1,
213
- "message": f"请求失败: {str(e)}"
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
- data = {'id': bug_id}
251
- response = requests.post(url, params=params, headers=headers, files=files, data=data, timeout=30)
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
  # 便捷函数
@@ -1,7 +0,0 @@
1
- {
2
- "product_id": "365",
3
- "opened_by": "69610",
4
- "keywords": "AI",
5
- "title_prefix": ""
6
- }
7
-