@bangdao-ai/zentao-mcp 1.0.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 ADDED
@@ -0,0 +1,155 @@
1
+ # 禅道 MCP 工具
2
+
3
+ 这是一个基于 Python 的 MCP(Model Context Protocol)工具,用于连接禅道Bug管理系统。支持通过 npm/npx 直接使用。
4
+
5
+ ## 功能特性
6
+
7
+ - ✅ 创建Bug(支持截图上传)
8
+ - ✅ 创建测试用例
9
+ - ✅ 批量从CSV文件创建测试用例
10
+ - ✅ 上传图片到Bug附件
11
+ - ✅ 支持配置文件(支持JSON注释)
12
+ - ✅ 自动格式化步骤为HTML
13
+
14
+ ## 安装
15
+
16
+ 通过 npm/npx 使用(推荐):
17
+
18
+ ```bash
19
+ npx -y @bangdao-ai/zentao-mcp@latest
20
+ ```
21
+
22
+ ## 配置
23
+
24
+ ### 在 Cursor 中配置
25
+
26
+ 在 Cursor 的 MCP 配置文件中添加:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "zentao": {
32
+ "command": "npx",
33
+ "args": [
34
+ "-y",
35
+ "@bangdao-ai/zentao-mcp@latest"
36
+ ],
37
+ "env": {
38
+ "ZENTAO_PRODUCT_ID": "365",
39
+ "ZENTAO_OPENED_BY": "69610",
40
+ "ZENTAO_KEYWORDS": "AI",
41
+ "ZENTAO_TITLE_PREFIX": ""
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ **配置说明**:
49
+ - `ZENTAO_PRODUCT_ID`(必需):产品ID
50
+ - `ZENTAO_OPENED_BY`(必需):创建人和指派人ID(同一个人)
51
+ - `ZENTAO_KEYWORDS`(可选):关键词,默认为空
52
+ - `ZENTAO_TITLE_PREFIX`(可选):标题前缀,默认为空
53
+
54
+ ### 兜底配置(可选)
55
+
56
+ 如果未在环境变量中配置,可以使用配置文件 `bug_config.json`(参考 `bug_config.example.json`)。配置文件路径可通过环境变量 `ZENTAO_CONFIG_PATH` 指定。
57
+
58
+ ## MCP 工具
59
+
60
+ ### 1. create_bug
61
+
62
+ 创建Bug到禅道系统。
63
+
64
+ **参数**:
65
+ - `title` (必需): Bug标题
66
+ - `steps` (必需): 重现步骤(支持换行,会自动转换为HTML格式)
67
+ - `product_id` (可选): 产品ID
68
+ - `opened_by` (可选): 创建人和指派人
69
+ - `screenshot_path` (可选): 截图文件路径
70
+ - `severity` (可选): 严重程度(默认3)
71
+ - `pri` (可选): 优先级(默认3)
72
+ - `type` (可选): Bug类型(默认codeissue)
73
+ - `environment` (可选): 环境(默认测试环境)
74
+ - `story` (可选): 相关需求id
75
+ - `task` (可选): 相关任务id
76
+ - `mailto` (可选): 抄送人账号
77
+ - `keywords` (可选): 关键词
78
+
79
+ ### 2. upload_image_to_bug
80
+
81
+ 上传图片到指定Bug的附件。
82
+
83
+ **参数**:
84
+ - `bug_id` (必需): Bug ID
85
+ - `image_path` (必需): 图片文件路径
86
+ - `custom_filename` (可选): 自定义文件名
87
+
88
+ ### 3. create_case
89
+
90
+ 创建测试用例到禅道系统。
91
+
92
+ **参数**:
93
+ - `product` (必需): 产品ID
94
+ - `type` (必需): 用例类型(interface 或 feature)
95
+ - `title` (必需): 用例名称
96
+ - `opened_by` (必需): 创建人
97
+ - `module` (可选): 模块ID(默认0)
98
+ - `stage` (可选): 阶段(默认intergrate)
99
+ - `precondition` (可选): 前置条件
100
+ - `keywords` (可选): 关键词
101
+ - `steps` (可选): 测试步骤数组
102
+ - `expects` (可选): 预期结果数组
103
+
104
+ ### 4. create_cases_from_csv
105
+
106
+ 从CSV文件批量创建测试用例。
107
+
108
+ **参数**:
109
+ - `csv_file_path` (必需): CSV文件路径
110
+ - `case_type` (可选): 用例类型(interface、feature、接口测试、功能测试,会自动识别)
111
+ - `product_id` (可选): 产品ID
112
+ - `opened_by` (可选): 创建人
113
+ - `keywords` (可选): 关键词
114
+
115
+ **CSV格式**:
116
+ - 接口测试用例需要包含:`用例名称`、`接口地址`、`请求方法`、`请求体`、`断言`、`预期结果`
117
+ - 功能测试用例需要包含:`用例名称`、`预期结果`
118
+
119
+ ### 5. submit_csv_cases_to_zentao
120
+
121
+ 将CSV文件中的所有测试用例提交到禅道(会自动备份原文件)。
122
+
123
+ **参数**:
124
+ - `csv_file_path` (必需): CSV文件路径
125
+ - `case_type` (可选): 用例类型(interface、feature、接口测试、功能测试,会自动识别)
126
+ - `product_id` (可选): 产品ID
127
+ - `opened_by` (可选): 创建人
128
+ - `keywords` (可选): 关键词
129
+
130
+ ### 6. get_config
131
+
132
+ 获取当前配置信息。
133
+
134
+ ## 项目结构
135
+
136
+ ```
137
+ 禅道mcp/
138
+ ├── index.js # npm 入口文件
139
+ ├── package.json # npm 包配置
140
+ ├── src/
141
+ │ ├── mcp_server.py # MCP服务器主文件
142
+ │ └── zentao_nexus.py # 禅道工具类
143
+ ├── requirements.txt # Python 依赖
144
+ ├── README.md # 项目说明
145
+ ├── PUBLISH.md # 发布指南
146
+ └── bug_config.example.json # 兜底配置示例
147
+ ```
148
+
149
+ ## 发布
150
+
151
+ 参考 [PUBLISH.md](PUBLISH.md) 了解如何发布到 npm。
152
+
153
+ ## 许可证
154
+
155
+ MIT
@@ -0,0 +1,7 @@
1
+ {
2
+ "product_id": "365",
3
+ "opened_by": "69610",
4
+ "keywords": "AI",
5
+ "title_prefix": ""
6
+ }
7
+
package/index.js ADDED
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 禅道 MCP 服务器入口
4
+ * 通过 npm/npx 调用 Python MCP 服务器
5
+ */
6
+
7
+ const { spawn } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ // 获取 Python 可执行文件路径
12
+ function findPython() {
13
+ const pythonCommands = ['python3', 'python'];
14
+
15
+ for (const cmd of pythonCommands) {
16
+ try {
17
+ const result = require('child_process').execSync(`which ${cmd}`, { encoding: 'utf-8' }).trim();
18
+ if (result) {
19
+ return result;
20
+ }
21
+ } catch (e) {
22
+ // 继续尝试下一个
23
+ }
24
+ }
25
+
26
+ throw new Error('未找到 Python 3,请确保已安装 Python 3.8+');
27
+ }
28
+
29
+ // 获取脚本路径
30
+ function getScriptPath() {
31
+ // 获取当前包的目录
32
+ const packageDir = __dirname;
33
+ const scriptPath = path.join(packageDir, 'src', 'mcp_server.py');
34
+
35
+ if (!fs.existsSync(scriptPath)) {
36
+ // 尝试相对路径(开发环境)
37
+ const devPath = path.join(packageDir, '..', 'src', 'mcp_server.py');
38
+ if (fs.existsSync(devPath)) {
39
+ return devPath;
40
+ }
41
+ throw new Error(`找不到 MCP 服务器脚本: ${scriptPath}`);
42
+ }
43
+
44
+ return scriptPath;
45
+ }
46
+
47
+ // 检查 Python 依赖
48
+ function checkPythonDependencies() {
49
+ const requirementsPath = path.join(__dirname, 'requirements.txt');
50
+
51
+ if (fs.existsSync(requirementsPath)) {
52
+ // 可以在这里添加依赖检查逻辑
53
+ // 例如:尝试导入模块,如果失败则提示用户安装
54
+ }
55
+ }
56
+
57
+ // 主函数
58
+ function main() {
59
+ try {
60
+ const python = findPython();
61
+ const scriptPath = getScriptPath();
62
+
63
+ // 检查 Python 依赖(可选)
64
+ checkPythonDependencies();
65
+
66
+ // 启动 Python MCP 服务器
67
+ // 注意:MCP 服务器通过 stdio 通信,不会输出到控制台
68
+ // 这是正常行为,服务器会等待 MCP 协议消息
69
+ const child = spawn(python, [scriptPath], {
70
+ stdio: 'inherit',
71
+ env: process.env
72
+ });
73
+
74
+ // 处理退出
75
+ child.on('exit', (code) => {
76
+ if (code !== 0 && code !== null) {
77
+ // 非正常退出,可能有问题
78
+ process.exit(code);
79
+ } else {
80
+ process.exit(0);
81
+ }
82
+ });
83
+
84
+ // 处理错误
85
+ child.on('error', (err) => {
86
+ console.error('启动 MCP 服务器失败:', err.message);
87
+ console.error('请确保:');
88
+ console.error('1. Python 3.8+ 已安装');
89
+ console.error('2. 已安装依赖: pip install -r requirements.txt');
90
+ console.error('3. 脚本路径正确:', scriptPath);
91
+ process.exit(1);
92
+ });
93
+
94
+ // 处理信号
95
+ process.on('SIGINT', () => {
96
+ if (child && !child.killed) {
97
+ child.kill('SIGINT');
98
+ }
99
+ process.exit(0);
100
+ });
101
+
102
+ process.on('SIGTERM', () => {
103
+ if (child && !child.killed) {
104
+ child.kill('SIGTERM');
105
+ }
106
+ process.exit(0);
107
+ });
108
+
109
+ } catch (error) {
110
+ console.error('错误:', error.message);
111
+ console.error('\n故障排查:');
112
+ console.error('1. 检查 Python 是否安装: python3 --version');
113
+ console.error('2. 检查脚本是否存在: ls -la src/mcp_server.py');
114
+ console.error('3. 检查依赖是否安装: pip3 list | grep mcp');
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ // 如果直接运行此文件
120
+ if (require.main === module) {
121
+ main();
122
+ }
123
+
124
+ module.exports = { main };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@bangdao-ai/zentao-mcp",
3
+ "version": "1.0.0",
4
+ "description": "禅道Bug管理系统MCP工具",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "zentao-mcp": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "zentao",
15
+ "bug-management",
16
+ "禅道"
17
+ ],
18
+ "author": "bangdao-ai",
19
+ "license": "MIT",
20
+ "files": [
21
+ "index.js",
22
+ "src/",
23
+ "requirements.txt",
24
+ "bug_config.example.json",
25
+ "README.md",
26
+ "package.json"
27
+ ],
28
+ "engines": {
29
+ "node": ">=14.0.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/bangdao-ai/zentao-mcp.git"
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ mcp>=0.9.0
2
+ requests>=2.31.0
3
+ python-dotenv>=1.0.0
4
+ fastapi>=0.104.0
5
+ uvicorn[standard]>=0.24.0
6
+
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 禅道 MCP 服务器
5
+ 基于 Python 的 MCP(Model Context Protocol)服务器实现
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import os
11
+ import sys
12
+ from typing import Any, Dict, Optional
13
+
14
+ from dotenv import load_dotenv
15
+ from mcp.server import Server
16
+ from mcp.server.stdio import stdio_server
17
+ from mcp.types import Tool, TextContent
18
+
19
+ from zentao_nexus import ZenTaoNexus
20
+
21
+ # 加载 .env 文件
22
+ load_dotenv()
23
+
24
+
25
+ class ZenTaoMCPServer:
26
+ """禅道 MCP 服务器"""
27
+
28
+ def __init__(self):
29
+ self.server = Server("zentao-mcp-server")
30
+ self.nexus: Optional[ZenTaoNexus] = None
31
+ self.setup_handlers()
32
+
33
+ def setup_handlers(self):
34
+ """设置请求处理器"""
35
+
36
+ @self.server.list_tools()
37
+ async def list_tools() -> list[Tool]:
38
+ """列出可用工具"""
39
+ return [
40
+ Tool(
41
+ name="create_bug",
42
+ description="创建Bug到禅道系统。支持上传截图。",
43
+ inputSchema={
44
+ "type": "object",
45
+ "properties": {
46
+ "title": {"type": "string", "description": "Bug标题"},
47
+ "steps": {"type": "string", "description": "重现步骤(支持换行,会自动转换为HTML格式)"},
48
+ "product_id": {"type": "string", "description": "产品ID(可选,默认使用配置文件中的值)"},
49
+ "opened_by": {"type": "string", "description": "创建人和指派人(可选,默认使用配置文件中的值)"},
50
+ "screenshot_path": {"type": "string", "description": "截图文件路径(可选)"},
51
+ "severity": {"type": "string", "description": "严重程度(可选,默认3)"},
52
+ "pri": {"type": "string", "description": "优先级(可选,默认3)"},
53
+ "type": {"type": "string", "description": "Bug类型(可选,默认codeissue)"},
54
+ "environment": {"type": "string", "description": "环境(可选,默认测试环境)"},
55
+ "story": {"type": "string", "description": "相关需求id(可选)"},
56
+ "task": {"type": "string", "description": "相关任务id(可选)"},
57
+ "mailto": {"type": "string", "description": "抄送人账号(可选)"},
58
+ "keywords": {"type": "string", "description": "关键词(可选)"}
59
+ },
60
+ "required": ["title", "steps"]
61
+ }
62
+ ),
63
+ Tool(
64
+ name="upload_image_to_bug",
65
+ description="上传图片到指定Bug的附件",
66
+ inputSchema={
67
+ "type": "object",
68
+ "properties": {
69
+ "bug_id": {"type": "string", "description": "Bug ID"},
70
+ "image_path": {"type": "string", "description": "图片文件路径"},
71
+ "custom_filename": {"type": "string", "description": "自定义文件名(可选)"}
72
+ },
73
+ "required": ["bug_id", "image_path"]
74
+ }
75
+ ),
76
+ Tool(
77
+ name="create_case",
78
+ description="创建测试用例到禅道系统",
79
+ inputSchema={
80
+ "type": "object",
81
+ "properties": {
82
+ "product": {"type": "string", "description": "产品ID"},
83
+ "type": {"type": "string", "description": "用例类型(interface或feature)", "enum": ["interface", "feature"]},
84
+ "title": {"type": "string", "description": "用例名称"},
85
+ "module": {"type": "string", "description": "模块ID(默认0)"},
86
+ "stage": {"type": "string", "description": "阶段(默认intergrate)"},
87
+ "precondition": {"type": "string", "description": "前置条件"},
88
+ "keywords": {"type": "string", "description": "关键词"},
89
+ "opened_by": {"type": "string", "description": "创建人"},
90
+ "steps": {"type": "array", "description": "测试步骤数组", "items": {"type": "string"}},
91
+ "expects": {"type": "array", "description": "预期结果数组", "items": {"type": "string"}}
92
+ },
93
+ "required": ["product", "type", "title", "opened_by"]
94
+ }
95
+ ),
96
+ Tool(
97
+ name="create_cases_from_csv",
98
+ description="从CSV文件批量创建测试用例",
99
+ inputSchema={
100
+ "type": "object",
101
+ "properties": {
102
+ "csv_file_path": {"type": "string", "description": "CSV文件路径"},
103
+ "case_type": {"type": "string", "description": "用例类型(interface、feature、接口测试、功能测试,可选,会自动识别)"},
104
+ "product_id": {"type": "string", "description": "产品ID(可选,默认使用配置文件中的值)"},
105
+ "opened_by": {"type": "string", "description": "创建人(可选,默认使用配置文件中的值)"},
106
+ "keywords": {"type": "string", "description": "关键词(可选,默认使用配置文件中的值)"}
107
+ },
108
+ "required": ["csv_file_path"]
109
+ }
110
+ ),
111
+ Tool(
112
+ name="submit_csv_cases_to_zentao",
113
+ description="将CSV文件中的所有测试用例提交到禅道(会自动备份原文件)",
114
+ inputSchema={
115
+ "type": "object",
116
+ "properties": {
117
+ "csv_file_path": {"type": "string", "description": "CSV文件路径"},
118
+ "case_type": {"type": "string", "description": "用例类型(interface、feature、接口测试、功能测试,可选,会自动识别)"},
119
+ "product_id": {"type": "string", "description": "产品ID(可选,默认使用配置文件中的值)"},
120
+ "opened_by": {"type": "string", "description": "创建人(可选,默认使用配置文件中的值)"},
121
+ "keywords": {"type": "string", "description": "关键词(可选,默认使用配置文件中的值)"}
122
+ },
123
+ "required": ["csv_file_path"]
124
+ }
125
+ ),
126
+ Tool(
127
+ name="get_config",
128
+ description="获取当前配置信息",
129
+ inputSchema={"type": "object", "properties": {}}
130
+ )
131
+ ]
132
+
133
+ @self.server.call_tool()
134
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> list[TextContent]:
135
+ """处理工具调用"""
136
+ try:
137
+ # 延迟初始化 nexus(只在第一次使用时初始化)
138
+ if self.nexus is None:
139
+ # 优先使用环境变量配置(Cursor MCP 配置)
140
+ config_path = os.getenv("ZENTAO_CONFIG_PATH")
141
+
142
+ # 如果通过环境变量直接配置了产品ID和创建人,则创建临时配置文件
143
+ product_id = os.getenv("ZENTAO_PRODUCT_ID")
144
+ opened_by = os.getenv("ZENTAO_OPENED_BY")
145
+
146
+ if product_id and opened_by and not config_path:
147
+ # 使用环境变量配置,创建临时配置
148
+ import tempfile
149
+ temp_config = {
150
+ "product_id": product_id,
151
+ "opened_by": opened_by,
152
+ "keywords": os.getenv("ZENTAO_KEYWORDS", ""),
153
+ "title_prefix": os.getenv("ZENTAO_TITLE_PREFIX", "")
154
+ }
155
+ temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
156
+ json.dump(temp_config, temp_file, ensure_ascii=False)
157
+ temp_file.close()
158
+ config_path = temp_file.name
159
+
160
+ self.nexus = ZenTaoNexus(config_path)
161
+
162
+ if name == "create_bug":
163
+ title = arguments["title"]
164
+ steps = arguments["steps"]
165
+ product_id = arguments.get("product_id")
166
+ opened_by = arguments.get("opened_by")
167
+ screenshot_path = arguments.get("screenshot_path")
168
+
169
+ # 提取其他可选参数
170
+ kwargs = {}
171
+ for key in ["severity", "pri", "type", "environment", "story", "task", "mailto", "keywords"]:
172
+ if key in arguments:
173
+ kwargs[key] = arguments[key]
174
+
175
+ result = self.nexus.create_bug(
176
+ title, steps, product_id, opened_by, screenshot_path, **kwargs
177
+ )
178
+
179
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
180
+
181
+ elif name == "upload_image_to_bug":
182
+ bug_id = arguments["bug_id"]
183
+ image_path = arguments["image_path"]
184
+ custom_filename = arguments.get("custom_filename")
185
+
186
+ if not os.path.exists(image_path):
187
+ return [TextContent(
188
+ type="text",
189
+ text=json.dumps({
190
+ "status": -1,
191
+ "message": f"图片文件不存在: {image_path}"
192
+ }, ensure_ascii=False, indent=2)
193
+ )]
194
+
195
+ result = self.nexus.upload_image_to_bug(bug_id, image_path, custom_filename)
196
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
197
+
198
+ elif name == "create_case":
199
+ product = arguments["product"]
200
+ case_type = arguments["type"]
201
+ title = arguments["title"]
202
+ opened_by = arguments["opened_by"]
203
+ module = arguments.get("module", "0")
204
+ stage = arguments.get("stage", "intergrate")
205
+ precondition = arguments.get("precondition", "")
206
+ keywords = arguments.get("keywords", "")
207
+ steps = arguments.get("steps", [])
208
+ expects = arguments.get("expects", [])
209
+
210
+ case_data = {
211
+ "product": product,
212
+ "type": case_type,
213
+ "title": title,
214
+ "module": module,
215
+ "stage": stage,
216
+ "precondition": precondition,
217
+ "keywords": keywords,
218
+ "openedBy": opened_by
219
+ }
220
+
221
+ # 添加步骤和预期结果
222
+ for i, step in enumerate(steps, 1):
223
+ case_data[f"steps[{i}]"] = step
224
+
225
+ for i, expect in enumerate(expects, 1):
226
+ case_data[f"expects[{i}]"] = expect
227
+
228
+ result = self.nexus.client.create_case_by_api(case_data)
229
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
230
+
231
+ elif name == "create_cases_from_csv":
232
+ csv_file_path = arguments["csv_file_path"]
233
+ case_type = arguments.get("case_type")
234
+ product_id = arguments.get("product_id")
235
+ opened_by = arguments.get("opened_by")
236
+ keywords = arguments.get("keywords")
237
+
238
+ if not os.path.exists(csv_file_path):
239
+ return [TextContent(
240
+ type="text",
241
+ text=json.dumps({
242
+ "status": -1,
243
+ "message": f"CSV文件不存在: {csv_file_path}"
244
+ }, ensure_ascii=False, indent=2)
245
+ )]
246
+
247
+ # 如果提供了参数,需要临时修改配置
248
+ original_product_id = self.nexus.product_id
249
+ original_opened_by = self.nexus.opened_by
250
+ original_keywords = self.nexus.keywords
251
+
252
+ if product_id:
253
+ self.nexus.product_id = product_id
254
+ if opened_by:
255
+ self.nexus.opened_by = opened_by
256
+ if keywords:
257
+ self.nexus.keywords = keywords
258
+
259
+ try:
260
+ results = self.nexus.create_case_from_csv(csv_file_path, case_type)
261
+ finally:
262
+ # 恢复原始配置
263
+ self.nexus.product_id = original_product_id
264
+ self.nexus.opened_by = original_opened_by
265
+ self.nexus.keywords = original_keywords
266
+
267
+ return [TextContent(type="text", text=json.dumps(results, ensure_ascii=False, indent=2))]
268
+
269
+ elif name == "submit_csv_cases_to_zentao":
270
+ csv_file_path = arguments["csv_file_path"]
271
+ case_type = arguments.get("case_type")
272
+ product_id = arguments.get("product_id")
273
+ opened_by = arguments.get("opened_by")
274
+ keywords = arguments.get("keywords")
275
+
276
+ if not os.path.exists(csv_file_path):
277
+ return [TextContent(
278
+ type="text",
279
+ text=json.dumps({
280
+ "status": "error",
281
+ "message": f"CSV文件不存在: {csv_file_path}",
282
+ "total_cases": 0,
283
+ "success_count": 0,
284
+ "failed_count": 0,
285
+ "success_rate": 0.0
286
+ }, ensure_ascii=False, indent=2)
287
+ )]
288
+
289
+ # 如果提供了参数,需要临时修改配置
290
+ original_product_id = self.nexus.product_id
291
+ original_opened_by = self.nexus.opened_by
292
+ original_keywords = self.nexus.keywords
293
+
294
+ if product_id:
295
+ self.nexus.product_id = product_id
296
+ if opened_by:
297
+ self.nexus.opened_by = opened_by
298
+ if keywords:
299
+ self.nexus.keywords = keywords
300
+
301
+ try:
302
+ result = self.nexus.submit_csv_cases_to_zentao(csv_file_path, case_type)
303
+ finally:
304
+ # 恢复原始配置
305
+ self.nexus.product_id = original_product_id
306
+ self.nexus.opened_by = original_opened_by
307
+ self.nexus.keywords = original_keywords
308
+
309
+ return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
310
+
311
+ elif name == "get_config":
312
+ config = self.nexus.get_config()
313
+ return [TextContent(type="text", text=json.dumps(config, ensure_ascii=False, indent=2))]
314
+
315
+ else:
316
+ raise ValueError(f"未知的工具: {name}")
317
+
318
+ except Exception as e:
319
+ error_result = {
320
+ "status": -1,
321
+ "message": str(e),
322
+ "error": str(e)
323
+ }
324
+ import traceback
325
+ error_result["traceback"] = traceback.format_exc()
326
+ return [TextContent(type="text", text=json.dumps(error_result, ensure_ascii=False, indent=2))]
327
+
328
+ async def run(self):
329
+ """运行服务器"""
330
+ async with stdio_server() as (read_stream, write_stream):
331
+ await self.server.run(
332
+ read_stream,
333
+ write_stream,
334
+ self.server.create_initialization_options()
335
+ )
336
+
337
+
338
+ async def main():
339
+ """主函数"""
340
+ server = ZenTaoMCPServer()
341
+ await server.run()
342
+
343
+
344
+ if __name__ == "__main__":
345
+ asyncio.run(main())
@@ -0,0 +1,673 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ZenTao Nexus - 禅道Bug管理系统连接工具类
5
+ 支持创建Bug、上传截图、批量创建测试用例等功能
6
+ 配置通过JSON文件管理,只需配置 product_id、opened_by 和 keywords
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import hashlib
12
+ import time
13
+ import requests
14
+ import csv
15
+ import shutil
16
+ import re
17
+ from datetime import datetime, timedelta
18
+ from pathlib import Path
19
+ from typing import Dict, Any, Optional
20
+
21
+
22
+ # ==================== 配置加载 ====================
23
+
24
+ class ConfigLoader:
25
+ """配置加载器类"""
26
+
27
+ def __init__(self, config_path: Optional[str] = None):
28
+ """
29
+ 初始化配置加载器
30
+
31
+ Args:
32
+ config_path: 配置文件路径,如果为None则使用默认路径
33
+ """
34
+ self.config_path = config_path
35
+ self.config: Dict[str, Any] = {}
36
+
37
+ # 如果提供了配置文件路径,加载配置文件
38
+ if config_path and os.path.exists(config_path):
39
+ if isinstance(config_path, Path):
40
+ config_path = str(config_path.resolve())
41
+ elif not os.path.isabs(config_path):
42
+ config_path = str(Path(config_path).resolve())
43
+ self.config_path = config_path
44
+ self.load_config()
45
+ elif config_path is None:
46
+ # 尝试使用默认路径
47
+ current_dir = os.path.dirname(os.path.abspath(__file__))
48
+ project_root = Path(current_dir).parent.parent.parent.parent
49
+ default_config_path = project_root / "testing" / "bug_config.json"
50
+
51
+ if default_config_path.exists():
52
+ self.config_path = str(default_config_path.resolve())
53
+ self.load_config()
54
+ else:
55
+ # 如果没有配置文件,使用空配置(将依赖环境变量)
56
+ self.config = {}
57
+
58
+ def load_config(self) -> None:
59
+ """加载配置文件(支持JSON注释)"""
60
+ if not self.config_path or not os.path.exists(self.config_path):
61
+ # 如果配置文件不存在,使用空配置(将依赖环境变量)
62
+ self.config = {}
63
+ return
64
+
65
+ try:
66
+ with open(self.config_path, 'r', encoding='utf-8') as f:
67
+ content = f.read()
68
+
69
+ # 移除JSON注释(支持 // 和 /* */ 两种格式)
70
+ # 移除单行注释 //
71
+ content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
72
+ # 移除多行注释 /* */
73
+ content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
74
+
75
+ self.config = json.loads(content)
76
+ except json.JSONDecodeError as e:
77
+ raise ValueError(f"配置文件JSON格式错误: {str(e)}")
78
+ except Exception as e:
79
+ raise RuntimeError(f"加载配置文件失败: {str(e)}")
80
+
81
+ def get(self, key: str, default: Any = None) -> Any:
82
+ """获取配置值"""
83
+ return self.config.get(key, default)
84
+
85
+ def validate(self) -> bool:
86
+ """验证配置文件的完整性(如果配置文件存在)"""
87
+ # 如果配置文件不存在或为空,则依赖环境变量,不强制验证
88
+ if not self.config_path or not os.path.exists(self.config_path):
89
+ return True
90
+
91
+ # 如果配置文件存在,则验证必需字段
92
+ required_keys = ["product_id", "opened_by"]
93
+ for key in required_keys:
94
+ if key not in self.config:
95
+ return False
96
+ return True
97
+
98
+
99
+ # ==================== API客户端 ====================
100
+
101
+ class ZenTaoAPIClient:
102
+ """禅道API客户端"""
103
+
104
+ # 写死的配置
105
+ BASE_URL = "https://zentao.bangdao-tech.com"
106
+ ACCOUNT = "KUBETEST"
107
+ SECRET_KEY = "aa287b7b5a4ef5d051f82fb1825ca1ac"
108
+ CASE_SECRET_KEY = "db1d522a190f1135c6e7c324bd337fda"
109
+ CASE_ACCOUNT = "AUTOTEST"
110
+
111
+ def __init__(self, silent=True):
112
+ """
113
+ 初始化禅道API客户端
114
+
115
+ Args:
116
+ silent (bool): 是否静默模式,不输出调试信息
117
+ """
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
122
+ self.silent = silent
123
+
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}"
134
+ md5_hash = hashlib.md5()
135
+ md5_hash.update(token_string.encode('utf-8'))
136
+ return md5_hash.hexdigest()
137
+
138
+ def create_bug_by_api(self, bug_data, use_form_data=True):
139
+ """通过API创建bug"""
140
+ 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
149
+ }
150
+
151
+ headers = {
152
+ 'BDTOKEN': bdtoken,
153
+ 'User-Agent': 'Python-ZenTao-Client/1.0'
154
+ }
155
+
156
+ try:
157
+ if use_form_data:
158
+ response = requests.post(url, params=params, headers=headers, data=bug_data, timeout=30)
159
+ else:
160
+ headers['Content-Type'] = 'application/json'
161
+ response = requests.post(url, params=params, headers=headers, json=bug_data, timeout=30)
162
+
163
+ try:
164
+ return response.json()
165
+ except json.JSONDecodeError:
166
+ return {
167
+ "status": -1,
168
+ "message": "响应不是有效的JSON格式",
169
+ "raw_response": response.text
170
+ }
171
+ except requests.exceptions.RequestException as e:
172
+ return {
173
+ "status": -1,
174
+ "message": f"请求失败: {str(e)}"
175
+ }
176
+
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
+ }
189
+
190
+ headers = {
191
+ 'BDTOKEN': bdtoken,
192
+ 'User-Agent': 'Python-ZenTao-Client/1.0'
193
+ }
194
+
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:
211
+ return {
212
+ "status": -1,
213
+ "message": f"请求失败: {str(e)}"
214
+ }
215
+
216
+ def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
217
+ """上传图片到BUG附件"""
218
+ if not os.path.exists(image_path):
219
+ return {
220
+ "status": -1,
221
+ "message": f"图片文件不存在: {image_path}"
222
+ }
223
+
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
+ filename = custom_filename or os.path.basename(image_path)
244
+
245
+ try:
246
+ with open(image_path, 'rb') as f:
247
+ files = {
248
+ 'files[]': (filename, f, 'application/octet-stream')
249
+ }
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
+ }
266
+ except Exception as e:
267
+ return {
268
+ "status": -1,
269
+ "message": f"文件处理失败: {str(e)}"
270
+ }
271
+
272
+ def create_case_from_csv(self, csv_file_path, case_type=None, product_id="365", opened_by="69610", keywords="AI"):
273
+ """从CSV文件批量创建测试用例"""
274
+ if not os.path.exists(csv_file_path):
275
+ return [{"status": -1, "message": f"CSV文件不存在: {csv_file_path}"}]
276
+
277
+ current_time = datetime.now()
278
+ time_str = current_time.strftime("%H:%M:%S")
279
+ results = []
280
+ updated_rows = []
281
+
282
+ try:
283
+ with open(csv_file_path, 'r', encoding='utf-8') as f:
284
+ reader = csv.DictReader(f)
285
+ fieldnames = reader.fieldnames
286
+
287
+ if case_type is None:
288
+ if '接口地址' in fieldnames and '请求方法' in fieldnames:
289
+ case_type = "interface"
290
+ else:
291
+ case_type = "feature"
292
+ else:
293
+ if case_type == "接口测试":
294
+ case_type = "interface"
295
+ elif case_type == "功能测试":
296
+ case_type = "feature"
297
+
298
+ for row in reader:
299
+ if not row.get('用例名称'):
300
+ row['用例ID'] = ''
301
+ updated_rows.append(row)
302
+ continue
303
+
304
+ if case_type == "interface":
305
+ interface_url = row.get('接口地址', '')
306
+ request_method = row.get('请求方法', 'POST')
307
+ request_body = row.get('请求体', '')
308
+ assertion = row.get('断言', '')
309
+ expected_result = row.get('预期结果', '')
310
+
311
+ steps = [
312
+ "准备测试数据",
313
+ f"发送请求体:{request_body}",
314
+ f"调用接口 {interface_url}",
315
+ "验证响应结果"
316
+ ]
317
+
318
+ expects = [
319
+ "测试数据准备完成",
320
+ f"请求体格式正确:{request_body}",
321
+ f"接口调用成功,方法:{request_method}",
322
+ f"断言验证:{assertion}\\n预期结果:{expected_result}"
323
+ ]
324
+
325
+ case_data = {
326
+ "product": product_id,
327
+ "type": "interface",
328
+ "title": row['用例名称'],
329
+ "module": "0",
330
+ "stage": "intergrate",
331
+ "precondition": f"前置条件:\\n1. 系统已启动\\n2. 接口服务正常运行\\n3. 接口地址:{interface_url}\\n4. 请求方法:{request_method}",
332
+ "keywords": f"{keywords} {time_str}",
333
+ "openedBy": opened_by
334
+ }
335
+
336
+ for i, step in enumerate(steps, 1):
337
+ case_data[f"steps[{i}]"] = step
338
+
339
+ for i, expect in enumerate(expects, 1):
340
+ case_data[f"expects[{i}]"] = expect
341
+ else:
342
+ steps = [
343
+ "打开系统页面",
344
+ "执行相关操作",
345
+ "验证功能结果"
346
+ ]
347
+
348
+ expects = [
349
+ "系统页面正常打开",
350
+ "操作执行成功",
351
+ row.get('预期结果', '功能符合预期')
352
+ ]
353
+
354
+ case_data = {
355
+ "product": product_id,
356
+ "type": "feature",
357
+ "title": row['用例名称'],
358
+ "module": "0",
359
+ "stage": "intergrate",
360
+ "precondition": f"前置条件:\\n1. 系统已启动\\n2. 用户已登录",
361
+ "keywords": f"{keywords} {time_str}",
362
+ "openedBy": opened_by
363
+ }
364
+
365
+ for i, step in enumerate(steps, 1):
366
+ case_data[f"steps[{i}]"] = step
367
+
368
+ for i, expect in enumerate(expects, 1):
369
+ case_data[f"expects[{i}]"] = expect
370
+
371
+ result = self.create_case_by_api(case_data)
372
+ result['case_name'] = row['用例名称']
373
+ results.append(result)
374
+
375
+ if result.get('status') == 'success':
376
+ row['用例ID'] = result.get('info', '')
377
+ else:
378
+ row['用例ID'] = f"创建失败: {result.get('message', '未知错误')}"
379
+
380
+ updated_rows.append(row)
381
+
382
+ if '用例ID' not in fieldnames:
383
+ fieldnames.append('用例ID')
384
+
385
+ with open(csv_file_path, 'w', encoding='utf-8', newline='') as f:
386
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
387
+ writer.writeheader()
388
+ writer.writerows(updated_rows)
389
+
390
+ except Exception as e:
391
+ results.append({"status": -1, "message": f"处理CSV文件失败: {str(e)}"})
392
+
393
+ return results
394
+
395
+ def submit_csv_cases_to_zentao(self, csv_file_path, case_type=None, product_id="365", opened_by="69610", keywords="AI"):
396
+ """将CSV文件中的所有测试用例提交到禅道"""
397
+ if not os.path.exists(csv_file_path):
398
+ return {
399
+ "status": "error",
400
+ "message": f"CSV文件不存在: {csv_file_path}",
401
+ "total_cases": 0,
402
+ "success_count": 0,
403
+ "failed_count": 0,
404
+ "success_rate": 0.0
405
+ }
406
+
407
+ backup_file = csv_file_path + ".backup_submit"
408
+ shutil.copy2(csv_file_path, backup_file)
409
+
410
+ total_cases = 0
411
+ with open(csv_file_path, 'r', encoding='utf-8') as f:
412
+ reader = csv.DictReader(f)
413
+ for row in reader:
414
+ if row.get('用例名称'):
415
+ total_cases += 1
416
+
417
+ results = self.create_case_from_csv(csv_file_path, case_type, product_id, opened_by, keywords)
418
+
419
+ success_count = 0
420
+ failed_count = 0
421
+
422
+ for result in results:
423
+ if result.get('status') == 'success':
424
+ success_count += 1
425
+ else:
426
+ failed_count += 1
427
+
428
+ success_rate = (success_count / len(results) * 100) if results else 0.0
429
+
430
+ return {
431
+ "status": "success",
432
+ "message": "CSV文件测试用例提交完成",
433
+ "total_cases": len(results),
434
+ "success_count": success_count,
435
+ "failed_count": failed_count,
436
+ "success_rate": success_rate,
437
+ "backup_file": backup_file
438
+ }
439
+
440
+
441
+ # ==================== 工具类 ====================
442
+
443
+ class ZenTaoNexus:
444
+ """
445
+ ZenTao Nexus 工具类
446
+
447
+ 用于连接禅道Bug管理系统,支持创建Bug、上传截图等功能。
448
+ 配置通过 config.json 文件管理,只需配置 product_id、opened_by 和 keywords。
449
+
450
+ 示例:
451
+ >>> nexus = ZenTaoNexus()
452
+ >>> result = nexus.create_bug(
453
+ ... title="测试Bug",
454
+ ... steps="重现步骤\\n1. 第一步\\n2. 第二步"
455
+ ... )
456
+ >>> print(result)
457
+ """
458
+
459
+ # 写死的默认值
460
+ OPENED_BUILD = "trunk"
461
+ ENVIRONMENT = "测试环境"
462
+ BUG_TYPE = "codeissue"
463
+ BUG_SOURCE = "codemiss"
464
+ SEVERITY = "3"
465
+ PRIORITY = "3"
466
+ DEADLINE_DAYS = 2
467
+ LOWER_BUG = "0"
468
+ REGRESSION = "0"
469
+
470
+ def __init__(self, config_path: str = None):
471
+ """
472
+ 初始化ZenTao Nexus工具
473
+
474
+ Args:
475
+ config_path: 配置文件路径,如果为None则使用默认路径(当前目录下的config.json)
476
+
477
+ Raises:
478
+ ValueError: 配置文件验证失败时抛出
479
+ FileNotFoundError: 配置文件不存在时抛出
480
+ """
481
+ # 加载配置
482
+ self.config_loader = ConfigLoader(config_path)
483
+
484
+ # 验证配置(如果配置文件存在)
485
+ if not self.config_loader.validate():
486
+ # 检查是否可以通过环境变量获取必需配置
487
+ product_id = os.getenv("ZENTAO_PRODUCT_ID") or os.getenv("PRODUCT_ID")
488
+ opened_by = os.getenv("ZENTAO_OPENED_BY") or os.getenv("OPENED_BY")
489
+
490
+ if not product_id or not opened_by:
491
+ raise ValueError("配置文件验证失败,且未找到必需的环境变量 ZENTAO_PRODUCT_ID 和 ZENTAO_OPENED_BY。请检查配置文件格式或设置环境变量。")
492
+
493
+ # 从配置加载(优先使用环境变量,支持 ZENTAO_ 前缀)
494
+ self.product_id = os.getenv("ZENTAO_PRODUCT_ID") or os.getenv("PRODUCT_ID") or self.config_loader.get("product_id", "365")
495
+ self.opened_by = os.getenv("ZENTAO_OPENED_BY") or os.getenv("OPENED_BY") or self.config_loader.get("opened_by", "69610")
496
+ # 关键词处理:如果配置中有 keywords,则使用 "ai," 拼接;如果没有,则使用 "ai"
497
+ env_keywords = os.getenv("ZENTAO_KEYWORDS") or os.getenv("KEYWORDS")
498
+ config_keywords = env_keywords or self.config_loader.get("keywords")
499
+ if config_keywords:
500
+ self.keywords = f"ai,{config_keywords}"
501
+ else:
502
+ self.keywords = "ai"
503
+
504
+ # 标题前缀
505
+ env_title_prefix = os.getenv("ZENTAO_TITLE_PREFIX") or os.getenv("TITLE_PREFIX")
506
+ self.title_prefix = env_title_prefix or self.config_loader.get("title_prefix", "")
507
+
508
+ # 创建API客户端
509
+ self.client = ZenTaoAPIClient(silent=True)
510
+
511
+ def _format_steps_to_html(self, steps):
512
+ """将steps中的换行转换为HTML格式"""
513
+ if not steps:
514
+ return ""
515
+
516
+ lines = steps.strip().split('\n')
517
+ html_steps = []
518
+
519
+ for line in lines:
520
+ line = line.strip()
521
+ if not line:
522
+ continue
523
+
524
+ if line and line[0].isdigit() and ('.' in line[:3] or '、' in line[:3]):
525
+ html_steps.append(f"<p>{line}</p>")
526
+ elif line.startswith('- '):
527
+ html_steps.append(f"<p>• {line[2:]}</p>")
528
+ elif ':' in line or ':' in line:
529
+ html_steps.append(f"<p><strong>{line}</strong></p>")
530
+ else:
531
+ html_steps.append(f"<p>{line}</p>")
532
+
533
+ return ''.join(html_steps)
534
+
535
+ def create_bug(self, title, steps, product_id=None, opened_by=None, screenshot_path=None, **kwargs):
536
+ """
537
+ 创建Bug(支持截图上传)
538
+
539
+ Args:
540
+ title (str): Bug标题
541
+ steps (str): 重现步骤(支持换行,会自动转换为HTML格式)
542
+ product_id (str): 产品ID,如果为None则使用配置值
543
+ opened_by (str): 创建人和指派人,如果为None则使用配置值
544
+ screenshot_path (str): 截图文件路径,如果提供则会上传到禅道
545
+ **kwargs: 其他可选参数
546
+ - severity: 严重程度(默认3)
547
+ - pri: 优先级(默认3)
548
+ - type: Bug类型(默认功能缺陷)
549
+ - environment: 环境(默认测试环境)
550
+ - story: 相关需求id
551
+ - task: 相关任务id
552
+ - mailto: 抄送人账号
553
+ - keywords: 关键词
554
+
555
+ Returns:
556
+ dict: 创建结果
557
+ """
558
+ product_id = product_id or self.product_id
559
+ opened_by = opened_by or self.opened_by
560
+ keywords = kwargs.get("keywords", self.keywords)
561
+
562
+ # 处理标题前缀:如果 title_prefix 不为空字符串,则在标题前面加上【title_prefix】
563
+ original_title = title
564
+ if self.title_prefix and self.title_prefix.strip():
565
+ title = f"【{self.title_prefix}】{title}"
566
+
567
+ html_steps = self._format_steps_to_html(steps)
568
+
569
+ bug_data = {
570
+ "product": product_id,
571
+ "openedBuild": kwargs.get("opened_build", self.OPENED_BUILD),
572
+ "environment": kwargs.get("environment", self.ENVIRONMENT),
573
+ "title": title,
574
+ "assignedTo": opened_by,
575
+ "deadline": (datetime.now() + timedelta(days=self.DEADLINE_DAYS)).strftime("%Y-%m-%d"),
576
+ "type": kwargs.get("type", self.BUG_TYPE),
577
+ "source": kwargs.get("source", self.BUG_SOURCE),
578
+ "lowerBug": kwargs.get("lowerBug", self.LOWER_BUG),
579
+ "regression": kwargs.get("regression", self.REGRESSION),
580
+ "severity": kwargs.get("severity", self.SEVERITY),
581
+ "pri": kwargs.get("pri", self.PRIORITY),
582
+ "steps": html_steps,
583
+ "story": kwargs.get("story", "0"),
584
+ "task": kwargs.get("task", "0"),
585
+ "mailto": kwargs.get("mailto", ""),
586
+ "keywords": keywords,
587
+ "openedBy": opened_by
588
+ }
589
+
590
+ import contextlib
591
+ with contextlib.redirect_stderr(open(os.devnull, 'w')):
592
+ result = self.client.create_bug_by_api(bug_data)
593
+
594
+ if result.get("status") == 1 and result.get("bugID"):
595
+ bug_id = result.get("bugID")
596
+ bug_url = f"{self.client.base_url}/zentaopms/www/index.php?m=bug&f=view&bugID={bug_id}"
597
+ result["bug_url"] = bug_url
598
+ result["actual_title"] = title # 返回实际提交的标题(包含前缀)
599
+
600
+ if screenshot_path and os.path.exists(screenshot_path):
601
+ try:
602
+ upload_result = self.client.upload_image_to_bug(bug_id, screenshot_path)
603
+ if upload_result.get("status") == 1:
604
+ result["screenshot_uploaded"] = True
605
+ result["screenshot_message"] = "截图上传成功"
606
+ else:
607
+ result["screenshot_uploaded"] = False
608
+ result["screenshot_message"] = f"截图上传失败: {upload_result.get('message')}"
609
+ except Exception as e:
610
+ result["screenshot_uploaded"] = False
611
+ result["screenshot_message"] = f"截图上传异常: {str(e)}"
612
+ else:
613
+ result["screenshot_uploaded"] = False
614
+ result["screenshot_message"] = "未提供截图文件"
615
+
616
+ return result
617
+
618
+ def get_config(self):
619
+ """获取当前配置信息"""
620
+ return {
621
+ "product_id": self.product_id,
622
+ "opened_by": self.opened_by,
623
+ "keywords": self.keywords,
624
+ "title_prefix": self.title_prefix
625
+ }
626
+
627
+ def upload_image_to_bug(self, bug_id, image_path, custom_filename=None):
628
+ """上传图片到指定Bug"""
629
+ return self.client.upload_image_to_bug(bug_id, image_path, custom_filename)
630
+
631
+ def create_case_from_csv(self, csv_file_path, case_type=None):
632
+ """从CSV文件批量创建测试用例"""
633
+ return self.client.create_case_from_csv(csv_file_path, case_type, self.product_id, self.opened_by, self.keywords)
634
+
635
+ def submit_csv_cases_to_zentao(self, csv_file_path, case_type=None):
636
+ """将CSV文件中的所有测试用例提交到禅道"""
637
+ return self.client.submit_csv_cases_to_zentao(csv_file_path, case_type, self.product_id, self.opened_by, self.keywords)
638
+
639
+
640
+ # 便捷函数
641
+ def create_bug(title, steps, product_id=None, opened_by=None, screenshot_path=None, config_path=None, **kwargs):
642
+ """便捷函数:快速创建Bug"""
643
+ nexus = ZenTaoNexus(config_path)
644
+ return nexus.create_bug(title, steps, product_id, opened_by, screenshot_path, **kwargs)
645
+
646
+
647
+ if __name__ == "__main__":
648
+ print("=== ZenTao Nexus 工具类示例 ===\n")
649
+
650
+ try:
651
+ nexus = ZenTaoNexus()
652
+
653
+ print("当前配置:")
654
+ config = nexus.get_config()
655
+ for key, value in config.items():
656
+ print(f" {key}: {value}")
657
+
658
+ print("\n✅ ZenTao Nexus 初始化成功!")
659
+ print("\n使用示例:")
660
+ print(" from zentao_nexus import ZenTaoNexus")
661
+ print(" nexus = ZenTaoNexus()")
662
+ print(" result = nexus.create_bug(")
663
+ print(" title='测试Bug',")
664
+ print(" steps='重现步骤\\n1. 第一步'")
665
+ print(" )")
666
+
667
+ except Exception as e:
668
+ print(f"❌ 初始化失败: {str(e)}")
669
+ print("\n请检查:")
670
+ print(" 1. config.json 文件是否存在")
671
+ print(" 2. 配置文件格式是否正确")
672
+ print(" 3. 配置文件中是否包含 product_id、opened_by 和 keywords")
673
+