@hupan56/wlkj 2.7.4 → 2.7.5
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/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hupan56/wlkj",
|
|
3
|
-
"version": "2.7.
|
|
4
|
-
"description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
|
|
5
|
-
"bin": {
|
|
6
|
-
"wlkj": "bin/cli.js"
|
|
7
|
-
},
|
|
8
|
-
"files": [
|
|
9
|
-
"bin/",
|
|
10
|
-
"templates/",
|
|
11
|
-
"MAC-VERIFY.md"
|
|
12
|
-
],
|
|
13
|
-
"keywords": [
|
|
14
|
-
"workflow",
|
|
15
|
-
"ai",
|
|
16
|
-
"prd",
|
|
17
|
-
"pipeline",
|
|
18
|
-
"qoder",
|
|
19
|
-
"product",
|
|
20
|
-
"team"
|
|
21
|
-
],
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"publishConfig": {
|
|
24
|
-
"access": "public"
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=16"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@hupan56/wlkj",
|
|
3
|
+
"version": "2.7.5",
|
|
4
|
+
"description": "AI Product R&D Workflow - PRD/Prototype/Search/Task/Report",
|
|
5
|
+
"bin": {
|
|
6
|
+
"wlkj": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"templates/",
|
|
11
|
+
"MAC-VERIFY.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"workflow",
|
|
15
|
+
"ai",
|
|
16
|
+
"prd",
|
|
17
|
+
"pipeline",
|
|
18
|
+
"qoder",
|
|
19
|
+
"product",
|
|
20
|
+
"team"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=16"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -33,8 +33,6 @@ pipeline:
|
|
|
33
33
|
req_id_start: 1
|
|
34
34
|
|
|
35
35
|
# ── MySQL 只读 MCP (QAS 测试环境, 团队共享) ──
|
|
36
|
-
# mcp_launcher.py 启动 mysql MCP 时从这里读连接信息。
|
|
37
|
-
# 是测试库只读账号, 安全可进 git 团队共享。
|
|
38
36
|
mysql:
|
|
39
37
|
host: 10.54.6.9
|
|
40
38
|
port: 3306
|
|
@@ -42,6 +40,13 @@ mysql:
|
|
|
42
40
|
password: Fywl_ics_test56476
|
|
43
41
|
env_name: "QAS 测试环境"
|
|
44
42
|
|
|
43
|
+
# ── 禅道 MCP (自建内网, 团队共享) ──
|
|
44
|
+
# /wl-task 对接禅道: 创建需求/查任务/跟踪状态
|
|
45
|
+
zentao:
|
|
46
|
+
url: http://10.89.2.91/zentao
|
|
47
|
+
user: hupan
|
|
48
|
+
password: Aa123456
|
|
49
|
+
|
|
45
50
|
# ── Platforms (知识图谱/搜索的平台映射, 换项目改这里即可) ──
|
|
46
51
|
# search_index.py --platform <key或alias> 会过滤到对应 project 目录
|
|
47
52
|
platforms:
|
|
@@ -278,6 +278,40 @@ def launch_mysql(repo):
|
|
|
278
278
|
os.execv(py, [py, str(server)])
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
ZENTAO_URL_DEFAULT = 'http://10.89.2.91/zentao'
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def launch_zentao(repo):
|
|
285
|
+
"""启动禅道 MCP。连接信息从 config.yaml 读(团队共享)。"""
|
|
286
|
+
server = repo / '.qoder' / 'scripts' / 'zentao_mcp_server.py'
|
|
287
|
+
if not server.is_file():
|
|
288
|
+
_log(f'zentao server 不存在: {server}')
|
|
289
|
+
_launch_disabled_mcp('qoder-zentao', 'zentao_mcp_server.py 缺失')
|
|
290
|
+
return
|
|
291
|
+
# 从 config.yaml 读禅道配置
|
|
292
|
+
cfg_path = repo / '.qoder' / 'config.yaml'
|
|
293
|
+
if cfg_path.is_file():
|
|
294
|
+
try:
|
|
295
|
+
import yaml
|
|
296
|
+
d = yaml.safe_load(cfg_path.read_text(encoding='utf-8')) or {}
|
|
297
|
+
zt = d.get('zentao', {}) or {}
|
|
298
|
+
if zt:
|
|
299
|
+
os.environ['ZENTAO_URL'] = str(zt.get('url', ZENTAO_URL_DEFAULT))
|
|
300
|
+
os.environ['ZENTAO_USER'] = str(zt.get('user', ''))
|
|
301
|
+
os.environ['ZENTAO_PASSWORD'] = str(zt.get('password', ''))
|
|
302
|
+
_log(f'zentao env 从 config.yaml 注入')
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
_log(f'启动 zentao')
|
|
306
|
+
py = _find_python_with_deps('requests')
|
|
307
|
+
if not py:
|
|
308
|
+
_log('⚠️ 找不到装了 requests 的 Python!')
|
|
309
|
+
_launch_disabled_mcp('qoder-zentao', 'requests 未装。pip install requests')
|
|
310
|
+
return
|
|
311
|
+
_log(f' python={py}')
|
|
312
|
+
os.execv(py, [py, str(server)])
|
|
313
|
+
|
|
314
|
+
|
|
281
315
|
def launch_lanhu(repo):
|
|
282
316
|
"""启动蓝湖 MCP。用 venv 的 3.10+ python (fastmcp 要求), 读角色 cookie。
|
|
283
317
|
没装 lanhu venv 或没配 cookie 时, 输出友好提示并启动一个空壳 MCP
|
|
@@ -328,6 +362,7 @@ LAUNCHERS = {
|
|
|
328
362
|
'kg': launch_kg,
|
|
329
363
|
'knowledge-graph': launch_kg,
|
|
330
364
|
'mysql': launch_mysql,
|
|
365
|
+
'zentao': launch_zentao,
|
|
331
366
|
'lanhu': launch_lanhu,
|
|
332
367
|
}
|
|
333
368
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""ZenTao (禅道) MCP Server - 对接禅道项目管理, 替代本地 task。
|
|
4
|
+
|
|
5
|
+
禅道自建内网开源版 REST API。
|
|
6
|
+
/wl-task 的底层从本地 task.json 切换到禅道。
|
|
7
|
+
|
|
8
|
+
工具:
|
|
9
|
+
- list_products: 列产品(找到需求的归属)
|
|
10
|
+
- list_stories: 列需求(PRD→禅道需求)
|
|
11
|
+
- create_story: 创建需求(PRD发布→自动建禅道需求)
|
|
12
|
+
- list_execution_tasks: 列执行的任务(谁在做什么)
|
|
13
|
+
- get_story_detail: 查需求详情
|
|
14
|
+
|
|
15
|
+
认证: 用户名密码 → token (config.yaml zentao 段配, 团队共享)
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import json
|
|
20
|
+
import socket
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
24
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
# 配置 (从环境变量或 config.yaml)
|
|
29
|
+
ZENTAO_URL = os.environ.get('ZENTAO_URL', 'http://10.89.2.91/zentao')
|
|
30
|
+
ZENTAO_USER = os.environ.get('ZENTAO_USER', 'hupan')
|
|
31
|
+
ZENTAO_PASSWORD = os.environ.get('ZENTAO_PASSWORD', 'Aa123456')
|
|
32
|
+
PING_HOST = ZENTAO_URL.replace('http://', '').replace('https://', '').split('/')[0]
|
|
33
|
+
PING_PORT = 80 if '://' not in ZENTAO_URL or ':80' in ZENTAO_URL else (
|
|
34
|
+
int(ZENTAO_URL.split(':')[2].split('/')[0]) if ZENTAO_URL.count(':') >= 2 else 80
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_real_stdout = sys.stdout
|
|
38
|
+
sys.stdout = sys.stderr
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import requests
|
|
42
|
+
except ImportError:
|
|
43
|
+
sys.stdout = _real_stdout
|
|
44
|
+
print('[zentao-mcp] requests 未装, 跑 pip install requests')
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
_TOKEN = None
|
|
48
|
+
_SESSION = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_intranet():
|
|
52
|
+
"""内网连通性校验。"""
|
|
53
|
+
try:
|
|
54
|
+
host = PING_HOST
|
|
55
|
+
port = PING_PORT
|
|
56
|
+
with socket.create_connection((host, port), timeout=3):
|
|
57
|
+
return True, ''
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return False, '无法连接禅道 (%s): %s。请确认已连接内网。' % (ZENTAO_URL, str(e)[:60])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_token():
|
|
63
|
+
"""登录拿 token (缓存)。"""
|
|
64
|
+
global _TOKEN
|
|
65
|
+
if _TOKEN:
|
|
66
|
+
return _TOKEN
|
|
67
|
+
r = requests.post(
|
|
68
|
+
ZENTAO_URL.rstrip('/') + '/api.php/v1/tokens',
|
|
69
|
+
json={'account': ZENTAO_USER, 'password': ZENTAO_PASSWORD},
|
|
70
|
+
timeout=10
|
|
71
|
+
)
|
|
72
|
+
if r.status_code in (200, 201):
|
|
73
|
+
_TOKEN = r.json().get('token')
|
|
74
|
+
return _TOKEN
|
|
75
|
+
raise RuntimeError('禅道登录失败: %s %s' % (r.status_code, r.text[:100]))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _api(method, path, data=None):
|
|
79
|
+
"""调禅道 API。"""
|
|
80
|
+
token = _get_token()
|
|
81
|
+
url = ZENTAO_URL.rstrip('/') + '/api.php/v1' + path
|
|
82
|
+
r = requests.request(method, url, headers={'Token': token},
|
|
83
|
+
json=data if method in ('POST', 'PUT') else None,
|
|
84
|
+
params=data if method == 'GET' else None,
|
|
85
|
+
timeout=15)
|
|
86
|
+
if r.status_code == 401:
|
|
87
|
+
global _TOKEN
|
|
88
|
+
_TOKEN = None
|
|
89
|
+
token = _get_token()
|
|
90
|
+
r = requests.request(method, url, headers={'Token': token},
|
|
91
|
+
json=data if method in ('POST', 'PUT') else None,
|
|
92
|
+
params=data if method == 'GET' else None,
|
|
93
|
+
timeout=15)
|
|
94
|
+
return r
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
TOOLS = [
|
|
98
|
+
{
|
|
99
|
+
"name": "list_products",
|
|
100
|
+
"description": "列禅道产品(找到需求归属)。例: list_products()",
|
|
101
|
+
"inputSchema": {"type": "object", "properties": {}},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "list_stories",
|
|
105
|
+
"description": "查某产品的需求列表。例: list_stories(product_id=1, limit=10)",
|
|
106
|
+
"inputSchema": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {
|
|
109
|
+
"product_id": {"type": "integer", "description": "产品ID"},
|
|
110
|
+
"limit": {"type": "integer", "description": "返回数量(默认20)"},
|
|
111
|
+
},
|
|
112
|
+
"required": ["product_id"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "create_story",
|
|
117
|
+
"description": "在禅道创建需求(PRD发布→禅道需求)。例: create_story(product_id=1, title='品质案件催办', spec='需求详情', pri=3)",
|
|
118
|
+
"inputSchema": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"product_id": {"type": "integer", "description": "产品ID"},
|
|
122
|
+
"title": {"type": "string", "description": "需求标题"},
|
|
123
|
+
"spec": {"type": "string", "description": "需求描述"},
|
|
124
|
+
"pri": {"type": "integer", "description": "优先级 1-4(1最高)"},
|
|
125
|
+
"category": {"type": "string", "description": "类别(feature/interface/optimization)"},
|
|
126
|
+
},
|
|
127
|
+
"required": ["product_id", "title", "spec"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "list_execution_tasks",
|
|
132
|
+
"description": "查某执行的的任务(谁在做什么)。例: list_execution_tasks(execution_id=75)",
|
|
133
|
+
"inputSchema": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"properties": {
|
|
136
|
+
"execution_id": {"type": "integer", "description": "执行ID"},
|
|
137
|
+
"limit": {"type": "integer", "description": "返回数量"},
|
|
138
|
+
},
|
|
139
|
+
"required": ["execution_id"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "get_story_detail",
|
|
144
|
+
"description": "查需求详情(完整描述+状态)。例: get_story_detail(story_id=761)",
|
|
145
|
+
"inputSchema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"story_id": {"type": "integer", "description": "需求ID"},
|
|
149
|
+
},
|
|
150
|
+
"required": ["story_id"],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _execute_tool(name, args):
|
|
157
|
+
args = args or {}
|
|
158
|
+
ok, msg = check_intranet()
|
|
159
|
+
if not ok:
|
|
160
|
+
return msg
|
|
161
|
+
|
|
162
|
+
if name == 'list_products':
|
|
163
|
+
r = _api('GET', '/products')
|
|
164
|
+
if r.status_code != 200:
|
|
165
|
+
return '查询失败: %s' % r.text[:100]
|
|
166
|
+
products = r.json().get('products', [])
|
|
167
|
+
lines = ['禅道产品 (%d 个):' % len(products)]
|
|
168
|
+
for p in products:
|
|
169
|
+
lines.append(' [%s] %s' % (p.get('id'), p.get('name', '')[:20]))
|
|
170
|
+
return '\n'.join(lines)
|
|
171
|
+
|
|
172
|
+
if name == 'list_stories':
|
|
173
|
+
pid = args.get('product_id', 1)
|
|
174
|
+
limit = args.get('limit', 20)
|
|
175
|
+
r = _api('GET', '/products/%s/stories' % pid, {'limit': limit})
|
|
176
|
+
if r.status_code not in (200, 201):
|
|
177
|
+
return '查询失败: %s' % r.text[:100]
|
|
178
|
+
stories = r.json().get('stories', [])
|
|
179
|
+
lines = ['产品 %s 的需求 (%d 个):' % (pid, len(stories))]
|
|
180
|
+
for s in stories[:limit]:
|
|
181
|
+
lines.append(' [#%s] %s (pri=%s status=%s)' % (
|
|
182
|
+
s.get('id'), s.get('title', '')[:30], s.get('pri'), s.get('status')))
|
|
183
|
+
return '\n'.join(lines)
|
|
184
|
+
|
|
185
|
+
if name == 'create_story':
|
|
186
|
+
pid = args.get('product_id', 1)
|
|
187
|
+
r = _api('POST', '/products/%s/stories' % pid, {
|
|
188
|
+
'title': args.get('title', ''),
|
|
189
|
+
'spec': args.get('spec', ''),
|
|
190
|
+
'category': args.get('category', 'feature'),
|
|
191
|
+
'pri': args.get('pri', 3),
|
|
192
|
+
})
|
|
193
|
+
if r.status_code in (200, 201):
|
|
194
|
+
d = r.json()
|
|
195
|
+
sid = d.get('id', '?')
|
|
196
|
+
return '✅ 需求已创建! ID=%s\n禅道地址: %s/story-view-%s.html' % (
|
|
197
|
+
sid, ZENTAO_URL.rstrip('/'), sid)
|
|
198
|
+
return '创建失败: %s %s' % (r.status_code, r.text[:150])
|
|
199
|
+
|
|
200
|
+
if name == 'list_execution_tasks':
|
|
201
|
+
eid = args.get('execution_id', 1)
|
|
202
|
+
r = _api('GET', '/executions/%s/tasks' % eid)
|
|
203
|
+
if r.status_code != 200:
|
|
204
|
+
return '查询失败: %s' % r.text[:100]
|
|
205
|
+
tasks = r.json().get('tasks', [])
|
|
206
|
+
lines = ['执行 %s 的任务 (%d 个):' % (eid, len(tasks))]
|
|
207
|
+
for t in tasks[:20]:
|
|
208
|
+
if isinstance(t, dict):
|
|
209
|
+
lines.append(' [#%s] %s (assignedTo=%s status=%s)' % (
|
|
210
|
+
t.get('id'), str(t.get('name', ''))[:25],
|
|
211
|
+
t.get('assignedTo', ''), t.get('status', '')))
|
|
212
|
+
return '\n'.join(lines)
|
|
213
|
+
|
|
214
|
+
if name == 'get_story_detail':
|
|
215
|
+
sid = args.get('story_id', 0)
|
|
216
|
+
r = _api('GET', '/stories/%s' % sid)
|
|
217
|
+
if r.status_code != 200:
|
|
218
|
+
return '查询失败: %s' % r.text[:100]
|
|
219
|
+
s = r.json()
|
|
220
|
+
return '需求 #%s: %s\n状态: %s\n优先级: %s\n描述: %s' % (
|
|
221
|
+
sid, s.get('title', ''), s.get('status', ''),
|
|
222
|
+
s.get('pri', ''), str(s.get('spec', ''))[:200])
|
|
223
|
+
|
|
224
|
+
return '未知工具: %s' % name
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── MCP Stdio Protocol ──
|
|
228
|
+
def _send(msg):
|
|
229
|
+
_real_stdout.write(json.dumps(msg, ensure_ascii=False) + '\n')
|
|
230
|
+
_real_stdout.flush()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
async def _handle(req):
|
|
234
|
+
method = req.get('method', '')
|
|
235
|
+
req_id = req.get('id')
|
|
236
|
+
params = req.get('params', {})
|
|
237
|
+
|
|
238
|
+
if method == 'initialize':
|
|
239
|
+
ok, msg = check_intranet()
|
|
240
|
+
info = {"name": "qoder-zentao", "version": "1.0.0"}
|
|
241
|
+
if not ok:
|
|
242
|
+
info['warning'] = msg
|
|
243
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {
|
|
244
|
+
"protocolVersion": "2024-11-05",
|
|
245
|
+
"capabilities": {"tools": {}}, "serverInfo": info}})
|
|
246
|
+
elif method == 'notifications/initialized':
|
|
247
|
+
pass
|
|
248
|
+
elif method == 'tools/list':
|
|
249
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
|
|
250
|
+
elif method == 'tools/call':
|
|
251
|
+
import asyncio
|
|
252
|
+
try:
|
|
253
|
+
result = await asyncio.get_event_loop().run_in_executor(
|
|
254
|
+
None, _execute_tool, params.get('name', ''), params.get('arguments', {}))
|
|
255
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {
|
|
256
|
+
"content": [{"type": "text", "text": result}], "isError": False}})
|
|
257
|
+
except Exception as e:
|
|
258
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {
|
|
259
|
+
"content": [{"type": "text", "text": "[error] %s" % str(e)[:200]}],
|
|
260
|
+
"isError": True}})
|
|
261
|
+
elif method == 'ping':
|
|
262
|
+
_send({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
263
|
+
elif req_id is not None:
|
|
264
|
+
_send({"jsonrpc": "2.0", "id": req_id,
|
|
265
|
+
"error": {"code": -32601, "message": "Method not found"}})
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main():
|
|
269
|
+
import asyncio
|
|
270
|
+
while True:
|
|
271
|
+
try:
|
|
272
|
+
line = sys.stdin.buffer.readline()
|
|
273
|
+
except (EOFError, OSError):
|
|
274
|
+
break
|
|
275
|
+
if not line:
|
|
276
|
+
break
|
|
277
|
+
line = line.strip()
|
|
278
|
+
if not line:
|
|
279
|
+
continue
|
|
280
|
+
try:
|
|
281
|
+
req = json.loads(line.decode('utf-8'))
|
|
282
|
+
loop = asyncio.new_event_loop()
|
|
283
|
+
loop.run_until_complete(_handle(req))
|
|
284
|
+
loop.close()
|
|
285
|
+
except json.JSONDecodeError:
|
|
286
|
+
pass
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print('[zentao-mcp] error: %s' % str(e)[:80])
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == '__main__':
|
|
292
|
+
try:
|
|
293
|
+
main()
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
pass
|