@hupan56/wlkj 2.7.11 → 3.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.
Files changed (152) hide show
  1. package/bin/cli.js +375 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -1,424 +1,384 @@
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, module=104)",
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最高, 默认3)"},
125
- "module": {"type": "integer", "description": "模块ID(可选)"},
126
- "category": {"type": "string", "description": "类别(feature/interface/optimization, 默认feature)"},
127
- },
128
- "required": ["product_id", "title", "spec"],
129
- },
130
- },
131
- {
132
- "name": "list_plans",
133
- "description": "查某产品的计划/迭代列表(让用户选归属)。例: list_plans(product_id=1)",
134
- "inputSchema": {
135
- "type": "object",
136
- "properties": {
137
- "product_id": {"type": "integer", "description": "产品ID"},
138
- },
139
- "required": ["product_id"],
140
- },
141
- },
142
- {
143
- "name": "list_executions",
144
- "description": "查活跃的执行/迭代(按周划分的后端/前端/移动端)。例: list_executions(limit=5)",
145
- "inputSchema": {
146
- "type": "object",
147
- "properties": {
148
- "limit": {"type": "integer", "description": "返回数量(默认10)"},
149
- },
150
- },
151
- },
152
- {
153
- "name": "link_story_to_execution",
154
- "description": "把需求关联到执行/迭代(让开发能看到)。权限不足返回提示不报错。例: link_story_to_execution(story_id=761, execution_id=73)",
155
- "inputSchema": {
156
- "type": "object",
157
- "properties": {
158
- "story_id": {"type": "integer", "description": "需求ID"},
159
- "execution_id": {"type": "integer", "description": "执行ID"},
160
- },
161
- "required": ["story_id", "execution_id"],
162
- },
163
- },
164
- {
165
- "name": "assign_story",
166
- "description": "指派需求给某人(设负责人)。权限不足返回提示不报错。例: assign_story(story_id=761, account='hupan')",
167
- "inputSchema": {
168
- "type": "object",
169
- "properties": {
170
- "story_id": {"type": "integer", "description": "需求ID"},
171
- "account": {"type": "string", "description": "禅道账号"},
172
- },
173
- "required": ["story_id", "account"],
174
- },
175
- },
176
- {
177
- "name": "change_story_status",
178
- "description": "变更需求状态(draft→reviewing→active→closed)。权限不足返回提示不报错。例: change_story_status(story_id=761, status='active')",
179
- "inputSchema": {
180
- "type": "object",
181
- "properties": {
182
- "story_id": {"type": "integer", "description": "需求ID"},
183
- "status": {"type": "string", "description": "目标状态(draft/reviewing/active/closed)"},
184
- },
185
- "required": ["story_id", "status"],
186
- },
187
- },
188
- {
189
- "name": "list_execution_tasks",
190
- "description": "查某执行的任务列表(谁在做什么)。例: list_execution_tasks(execution_id=73)",
191
- "inputSchema": {
192
- "type": "object",
193
- "properties": {
194
- "execution_id": {"type": "integer", "description": "执行ID"},
195
- "limit": {"type": "integer", "description": "返回数量"},
196
- },
197
- "required": ["execution_id"],
198
- },
199
- },
200
- {
201
- "name": "get_story_detail",
202
- "description": "查需求详情(完整描述+状态+关联)。例: get_story_detail(story_id=761)",
203
- "inputSchema": {
204
- "type": "object",
205
- "properties": {
206
- "story_id": {"type": "integer", "description": "需求ID"},
207
- },
208
- "required": ["story_id"],
209
- },
210
- },
211
- ]
212
-
213
-
214
- def _execute_tool(name, args):
215
- args = args or {}
216
- ok, msg = check_intranet()
217
- if not ok:
218
- return msg
219
-
220
- if name == 'list_products':
221
- r = _api('GET', '/products')
222
- if r.status_code != 200:
223
- return '查询失败: %s' % r.text[:100]
224
- products = r.json().get('products', [])
225
- lines = ['禅道产品 (%d 个):' % len(products)]
226
- for p in products:
227
- lines.append(' [%s] %s' % (p.get('id'), p.get('name', '')[:20]))
228
- return '\n'.join(lines)
229
-
230
- if name == 'list_stories':
231
- pid = args.get('product_id', 1)
232
- limit = args.get('limit', 20)
233
- r = _api('GET', '/products/%s/stories' % pid, {'limit': limit})
234
- if r.status_code not in (200, 201):
235
- return '查询失败: %s' % r.text[:100]
236
- stories = r.json().get('stories', [])
237
- lines = ['产品 %s 的需求 (%d 个):' % (pid, len(stories))]
238
- for s in stories[:limit]:
239
- lines.append(' [#%s] %s (pri=%s status=%s)' % (
240
- s.get('id'), s.get('title', '')[:30], s.get('pri'), s.get('status')))
241
- return '\n'.join(lines)
242
-
243
- if name == 'create_story':
244
- pid = args.get('product_id', 1)
245
- payload = {
246
- 'title': args.get('title', ''),
247
- 'spec': args.get('spec', ''),
248
- 'category': args.get('category', 'feature'),
249
- 'pri': args.get('pri', 3),
250
- }
251
- if args.get('module'):
252
- payload['module'] = args['module']
253
- r = _api('POST', '/products/%s/stories' % pid, payload)
254
- if r.status_code in (200, 201):
255
- d = r.json()
256
- sid = d.get('id', '?')
257
- return '✅ 需求已创建! ID=%s\n禅道地址: %s/story-view-%s.html' % (
258
- sid, ZENTAO_URL.rstrip('/'), sid)
259
- # 权限不足或字段错误, 友好返回不崩
260
- try:
261
- err = r.json().get('error', r.text[:100])
262
- except Exception:
263
- err = r.text[:100]
264
- return '⚠️ 需求创建失败: %s\n(可能权限不足或字段缺失, 不影响其它操作)' % err
265
-
266
- if name == 'list_plans':
267
- pid = args.get('product_id', 1)
268
- r = _api('GET', '/products/%s/plans' % pid)
269
- if r.status_code != 200:
270
- return '查询计划失败: %s' % r.text[:100]
271
- plans = r.json().get('plans', [])
272
- if not plans:
273
- return '产品 %s 暂无计划。可在禅道里创建计划/迭代。' % pid
274
- lines = ['产品 %s 的计划 (%d 个):' % (pid, len(plans))]
275
- for p in plans:
276
- lines.append(' [%s] %s (%s~%s)' % (
277
- p.get('id'), str(p.get('title', ''))[:20], p.get('begin', ''), p.get('end', '')))
278
- return '\n'.join(lines)
279
-
280
- if name == 'list_executions':
281
- limit = args.get('limit', 10)
282
- r = _api('GET', '/executions', {'limit': limit})
283
- if r.status_code != 200:
284
- return '查询执行失败: %s' % r.text[:100]
285
- execs = r.json().get('executions', [])
286
- lines = ['活跃执行/迭代 (%d 个):' % len(execs)]
287
- for ex in execs[:limit]:
288
- lines.append(' [%s] %s (%s~%s project=%s)' % (
289
- ex.get('id'), str(ex.get('name', ''))[:15],
290
- ex.get('begin', ''), ex.get('end', ''), ex.get('project', '')))
291
- return '\n'.join(lines)
292
-
293
- if name == 'link_story_to_execution':
294
- sid = args.get('story_id', 0)
295
- eid = args.get('execution_id', 0)
296
- r = _api('POST', '/executions/%s/stories' % eid, {'stories': [sid]})
297
- if r.status_code in (200, 201):
298
- return '✅ 需求 #%s 已关联到执行 #%s' % (sid, eid)
299
- try:
300
- err = r.json().get('error', r.text[:100])
301
- except Exception:
302
- err = r.text[:100]
303
- return '⚠️ 关联失败: %s\n(可能权限不足, 需在禅道手动关联)' % err
304
-
305
- if name == 'assign_story':
306
- sid = args.get('story_id', 0)
307
- account = args.get('account', '')
308
- r = _api('POST', '/stories/%s/assign' % sid, {'assignedTo': account})
309
- if r.status_code in (200, 201):
310
- return '✅ 需求 #%s 已指派给 %s' % (sid, account)
311
- try:
312
- err = r.json().get('error', r.text[:100])
313
- except Exception:
314
- err = r.text[:100]
315
- return '⚠️ 指派失败: %s\n(可能权限不足, 需在禅道手动指派)' % err
316
-
317
- if name == 'change_story_status':
318
- sid = args.get('story_id', 0)
319
- status = args.get('status', '')
320
- r = _api('POST', '/stories/%s/change' % sid, {'status': status})
321
- if r.status_code in (200, 201):
322
- return '✅ 需求 #%s 状态变更为 %s' % (sid, status)
323
- try:
324
- err = r.json().get('error', r.text[:100])
325
- except Exception:
326
- err = r.text[:100]
327
- return '⚠️ 状态变更失败: %s\n(可能权限不足或状态转换不合法)' % err
328
-
329
- if name == 'list_execution_tasks':
330
- eid = args.get('execution_id', 1)
331
- r = _api('GET', '/executions/%s/tasks' % eid)
332
- if r.status_code != 200:
333
- return '查询失败: %s' % r.text[:100]
334
- tasks = r.json().get('tasks', [])
335
- lines = ['执行 %s 的任务 (%d 个):' % (eid, len(tasks))]
336
- for t in tasks[:20]:
337
- if isinstance(t, dict):
338
- lines.append(' [#%s] %s (assignedTo=%s status=%s)' % (
339
- t.get('id'), str(t.get('name', ''))[:25],
340
- t.get('assignedTo', ''), t.get('status', '')))
341
- return '\n'.join(lines)
342
-
343
- if name == 'get_story_detail':
344
- sid = args.get('story_id', 0)
345
- r = _api('GET', '/stories/%s' % sid)
346
- if r.status_code != 200:
347
- return '查询失败: %s' % r.text[:100]
348
- s = r.json()
349
- return '需求 #%s: %s\n状态: %s\n优先级: %s\n描述: %s' % (
350
- sid, s.get('title', ''), s.get('status', ''),
351
- s.get('pri', ''), str(s.get('spec', ''))[:200])
352
-
353
- return '未知工具: %s' % name
354
-
355
-
356
- # ── MCP Stdio Protocol ──
357
- def _send(msg):
358
- _real_stdout.write(json.dumps(msg, ensure_ascii=False) + '\n')
359
- _real_stdout.flush()
360
-
361
-
362
- async def _handle(req):
363
- method = req.get('method', '')
364
- req_id = req.get('id')
365
- params = req.get('params', {})
366
-
367
- if method == 'initialize':
368
- ok, msg = check_intranet()
369
- info = {"name": "qoder-zentao", "version": "1.0.0"}
370
- if not ok:
371
- info['warning'] = msg
372
- _send({"jsonrpc": "2.0", "id": req_id, "result": {
373
- "protocolVersion": "2024-11-05",
374
- "capabilities": {"tools": {}}, "serverInfo": info}})
375
- elif method == 'notifications/initialized':
376
- pass
377
- elif method == 'tools/list':
378
- _send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": TOOLS}})
379
- elif method == 'tools/call':
380
- import asyncio
381
- try:
382
- result = await asyncio.get_event_loop().run_in_executor(
383
- None, _execute_tool, params.get('name', ''), params.get('arguments', {}))
384
- _send({"jsonrpc": "2.0", "id": req_id, "result": {
385
- "content": [{"type": "text", "text": result}], "isError": False}})
386
- except Exception as e:
387
- _send({"jsonrpc": "2.0", "id": req_id, "result": {
388
- "content": [{"type": "text", "text": "[error] %s" % str(e)[:200]}],
389
- "isError": True}})
390
- elif method == 'ping':
391
- _send({"jsonrpc": "2.0", "id": req_id, "result": {}})
392
- elif req_id is not None:
393
- _send({"jsonrpc": "2.0", "id": req_id,
394
- "error": {"code": -32601, "message": "Method not found"}})
395
-
396
-
397
- def main():
398
- import asyncio
399
- while True:
400
- try:
401
- line = sys.stdin.buffer.readline()
402
- except (EOFError, OSError):
403
- break
404
- if not line:
405
- break
406
- line = line.strip()
407
- if not line:
408
- continue
409
- try:
410
- req = json.loads(line.decode('utf-8'))
411
- loop = asyncio.new_event_loop()
412
- loop.run_until_complete(_handle(req))
413
- loop.close()
414
- except json.JSONDecodeError:
415
- pass
416
- except Exception as e:
417
- print('[zentao-mcp] error: %s' % str(e)[:80])
418
-
419
-
420
- if __name__ == '__main__':
421
- try:
422
- main()
423
- except KeyboardInterrupt:
424
- pass
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
6
+ if _cp not in _s.path: _s.path.insert(0, _cp)
7
+ from bootstrap import setup; setup()
8
+
9
+ """ZenTao (禅道) MCP Server - 对接禅道项目管理, 替代本地 task。
10
+
11
+ 禅道自建内网开源版 REST API。
12
+ /wl-task 的底层从本地 task.json 切换到禅道。
13
+
14
+ 工具:
15
+ - list_products: 列产品(找到需求的归属)
16
+ - list_stories: 列需求(PRD→禅道需求)
17
+ - create_story: 创建需求(PRD发布→自动建禅道需求)
18
+ - list_execution_tasks: 列执行的任务(谁在做什么)
19
+ - get_story_detail: 查需求详情
20
+
21
+ 认证: 用户名密码 → token (config.yaml zentao 段配, 团队共享)
22
+ """
23
+ import os
24
+ import sys
25
+ import json
26
+ import socket
27
+
28
+ try:
29
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
30
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
31
+ except Exception:
32
+ pass
33
+
34
+ # 配置 (从环境变量或 config.yaml)
35
+ ZENTAO_URL = os.environ.get('ZENTAO_URL', 'http://10.89.2.91/zentao')
36
+ ZENTAO_USER = os.environ.get('ZENTAO_USER', 'hupan')
37
+ ZENTAO_PASSWORD = os.environ.get('ZENTAO_PASSWORD', 'Aa123456')
38
+ PING_HOST = ZENTAO_URL.replace('http://', '').replace('https://', '').split('/')[0]
39
+ PING_PORT = 80 if '://' not in ZENTAO_URL or ':80' in ZENTAO_URL else (
40
+ int(ZENTAO_URL.split(':')[2].split('/')[0]) if ZENTAO_URL.count(':') >= 2 else 80
41
+ )
42
+
43
+ # stdout 重定向 + 协议层由 BaseMCPServer 接管 (v3.0 重构)
44
+ # 注意: requests run() 内才需要 (延迟 import, 避免没装时启动即崩)
45
+
46
+ _TOKEN = None
47
+ _SESSION = None
48
+
49
+
50
+ def check_intranet():
51
+ """内网连通性校验。"""
52
+ try:
53
+ host = PING_HOST
54
+ port = PING_PORT
55
+ with socket.create_connection((host, port), timeout=3):
56
+ return True, ''
57
+ except Exception as e:
58
+ return False, '无法连接禅道 (%s): %s。请确认已连接内网。' % (ZENTAO_URL, str(e)[:60])
59
+
60
+
61
+ def _get_token():
62
+ """登录拿 token (缓存)。"""
63
+ global _TOKEN
64
+ if _TOKEN:
65
+ return _TOKEN
66
+ r = requests.post(
67
+ ZENTAO_URL.rstrip('/') + '/api.php/v1/tokens',
68
+ json={'account': ZENTAO_USER, 'password': ZENTAO_PASSWORD},
69
+ timeout=10
70
+ )
71
+ if r.status_code in (200, 201):
72
+ _TOKEN = r.json().get('token')
73
+ return _TOKEN
74
+ raise RuntimeError('禅道登录失败: %s %s' % (r.status_code, r.text[:100]))
75
+
76
+
77
+ def _api(method, path, data=None):
78
+ """调禅道 API。"""
79
+ token = _get_token()
80
+ url = ZENTAO_URL.rstrip('/') + '/api.php/v1' + path
81
+ r = requests.request(method, url, headers={'Token': token},
82
+ json=data if method in ('POST', 'PUT') else None,
83
+ params=data if method == 'GET' else None,
84
+ timeout=15)
85
+ if r.status_code == 401:
86
+ global _TOKEN
87
+ _TOKEN = None
88
+ token = _get_token()
89
+ r = requests.request(method, url, headers={'Token': token},
90
+ json=data if method in ('POST', 'PUT') else None,
91
+ params=data if method == 'GET' else None,
92
+ timeout=15)
93
+ return r
94
+
95
+
96
+ TOOLS = [
97
+ {
98
+ "name": "list_products",
99
+ "description": "列禅道产品(找到需求归属)。例: list_products()",
100
+ "inputSchema": {"type": "object", "properties": {}},
101
+ },
102
+ {
103
+ "name": "list_stories",
104
+ "description": "查某产品的需求列表。例: list_stories(product_id=1, limit=10)",
105
+ "inputSchema": {
106
+ "type": "object",
107
+ "properties": {
108
+ "product_id": {"type": "integer", "description": "产品ID"},
109
+ "limit": {"type": "integer", "description": "返回数量(默认20)"},
110
+ },
111
+ "required": ["product_id"],
112
+ },
113
+ },
114
+ {
115
+ "name": "create_story",
116
+ "description": "在禅道创建需求(PRD发布→禅道需求)。权限不足会返回友好提示不报错。例: create_story(product_id=1, title='品质案件催办', spec='需求详情', pri=3, module=104)",
117
+ "inputSchema": {
118
+ "type": "object",
119
+ "properties": {
120
+ "product_id": {"type": "integer", "description": "产品ID"},
121
+ "title": {"type": "string", "description": "需求标题"},
122
+ "spec": {"type": "string", "description": "需求描述"},
123
+ "pri": {"type": "integer", "description": "优先级 1-4(1最高, 默认3)"},
124
+ "module": {"type": "integer", "description": "模块ID(可选)"},
125
+ "category": {"type": "string", "description": "类别(feature/interface/optimization, 默认feature)"},
126
+ },
127
+ "required": ["product_id", "title", "spec"],
128
+ },
129
+ },
130
+ {
131
+ "name": "list_plans",
132
+ "description": "查某产品的计划/迭代列表(让用户选归属)。例: list_plans(product_id=1)",
133
+ "inputSchema": {
134
+ "type": "object",
135
+ "properties": {
136
+ "product_id": {"type": "integer", "description": "产品ID"},
137
+ },
138
+ "required": ["product_id"],
139
+ },
140
+ },
141
+ {
142
+ "name": "list_executions",
143
+ "description": "查活跃的执行/迭代(按周划分的后端/前端/移动端)。例: list_executions(limit=5)",
144
+ "inputSchema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "limit": {"type": "integer", "description": "返回数量(默认10)"},
148
+ },
149
+ },
150
+ },
151
+ {
152
+ "name": "link_story_to_execution",
153
+ "description": "把需求关联到执行/迭代(让开发能看到)。权限不足返回提示不报错。例: link_story_to_execution(story_id=761, execution_id=73)",
154
+ "inputSchema": {
155
+ "type": "object",
156
+ "properties": {
157
+ "story_id": {"type": "integer", "description": "需求ID"},
158
+ "execution_id": {"type": "integer", "description": "执行ID"},
159
+ },
160
+ "required": ["story_id", "execution_id"],
161
+ },
162
+ },
163
+ {
164
+ "name": "assign_story",
165
+ "description": "指派需求给某人(设负责人)。权限不足返回提示不报错。例: assign_story(story_id=761, account='hupan')",
166
+ "inputSchema": {
167
+ "type": "object",
168
+ "properties": {
169
+ "story_id": {"type": "integer", "description": "需求ID"},
170
+ "account": {"type": "string", "description": "禅道账号"},
171
+ },
172
+ "required": ["story_id", "account"],
173
+ },
174
+ },
175
+ {
176
+ "name": "change_story_status",
177
+ "description": "变更需求状态(draft→reviewing→active→closed)。权限不足返回提示不报错。例: change_story_status(story_id=761, status='active')",
178
+ "inputSchema": {
179
+ "type": "object",
180
+ "properties": {
181
+ "story_id": {"type": "integer", "description": "需求ID"},
182
+ "status": {"type": "string", "description": "目标状态(draft/reviewing/active/closed)"},
183
+ },
184
+ "required": ["story_id", "status"],
185
+ },
186
+ },
187
+ {
188
+ "name": "list_execution_tasks",
189
+ "description": "查某执行的任务列表(谁在做什么)。例: list_execution_tasks(execution_id=73)",
190
+ "inputSchema": {
191
+ "type": "object",
192
+ "properties": {
193
+ "execution_id": {"type": "integer", "description": "执行ID"},
194
+ "limit": {"type": "integer", "description": "返回数量"},
195
+ },
196
+ "required": ["execution_id"],
197
+ },
198
+ },
199
+ {
200
+ "name": "get_story_detail",
201
+ "description": "查需求详情(完整描述+状态+关联)。例: get_story_detail(story_id=761)",
202
+ "inputSchema": {
203
+ "type": "object",
204
+ "properties": {
205
+ "story_id": {"type": "integer", "description": "需求ID"},
206
+ },
207
+ "required": ["story_id"],
208
+ },
209
+ },
210
+ ]
211
+
212
+
213
+ def _execute_tool(name, args):
214
+ args = args or {}
215
+ ok, msg = check_intranet()
216
+ if not ok:
217
+ return msg
218
+
219
+ if name == 'list_products':
220
+ r = _api('GET', '/products')
221
+ if r.status_code != 200:
222
+ return '查询失败: %s' % r.text[:100]
223
+ products = r.json().get('products', [])
224
+ lines = ['禅道产品 (%d 个):' % len(products)]
225
+ for p in products:
226
+ lines.append(' [%s] %s' % (p.get('id'), p.get('name', '')[:20]))
227
+ return '\n'.join(lines)
228
+
229
+ if name == 'list_stories':
230
+ pid = args.get('product_id', 1)
231
+ limit = args.get('limit', 20)
232
+ r = _api('GET', '/products/%s/stories' % pid, {'limit': limit})
233
+ if r.status_code not in (200, 201):
234
+ return '查询失败: %s' % r.text[:100]
235
+ stories = r.json().get('stories', [])
236
+ lines = ['产品 %s 的需求 (%d 个):' % (pid, len(stories))]
237
+ for s in stories[:limit]:
238
+ lines.append(' [#%s] %s (pri=%s status=%s)' % (
239
+ s.get('id'), s.get('title', '')[:30], s.get('pri'), s.get('status')))
240
+ return '\n'.join(lines)
241
+
242
+ if name == 'create_story':
243
+ pid = args.get('product_id', 1)
244
+ payload = {
245
+ 'title': args.get('title', ''),
246
+ 'spec': args.get('spec', ''),
247
+ 'category': args.get('category', 'feature'),
248
+ 'pri': args.get('pri', 3),
249
+ }
250
+ if args.get('module'):
251
+ payload['module'] = args['module']
252
+ r = _api('POST', '/products/%s/stories' % pid, payload)
253
+ if r.status_code in (200, 201):
254
+ d = r.json()
255
+ sid = d.get('id', '?')
256
+ return '✅ 需求已创建! ID=%s\n禅道地址: %s/story-view-%s.html' % (
257
+ sid, ZENTAO_URL.rstrip('/'), sid)
258
+ # 权限不足或字段错误, 友好返回不崩
259
+ try:
260
+ err = r.json().get('error', r.text[:100])
261
+ except Exception:
262
+ err = r.text[:100]
263
+ return '⚠️ 需求创建失败: %s\n(可能权限不足或字段缺失, 不影响其它操作)' % err
264
+
265
+ if name == 'list_plans':
266
+ pid = args.get('product_id', 1)
267
+ r = _api('GET', '/products/%s/plans' % pid)
268
+ if r.status_code != 200:
269
+ return '查询计划失败: %s' % r.text[:100]
270
+ plans = r.json().get('plans', [])
271
+ if not plans:
272
+ return '产品 %s 暂无计划。可在禅道里创建计划/迭代。' % pid
273
+ lines = ['产品 %s 的计划 (%d 个):' % (pid, len(plans))]
274
+ for p in plans:
275
+ lines.append(' [%s] %s (%s~%s)' % (
276
+ p.get('id'), str(p.get('title', ''))[:20], p.get('begin', ''), p.get('end', '')))
277
+ return '\n'.join(lines)
278
+
279
+ if name == 'list_executions':
280
+ limit = args.get('limit', 10)
281
+ r = _api('GET', '/executions', {'limit': limit})
282
+ if r.status_code != 200:
283
+ return '查询执行失败: %s' % r.text[:100]
284
+ execs = r.json().get('executions', [])
285
+ lines = ['活跃执行/迭代 (%d 个):' % len(execs)]
286
+ for ex in execs[:limit]:
287
+ lines.append(' [%s] %s (%s~%s project=%s)' % (
288
+ ex.get('id'), str(ex.get('name', ''))[:15],
289
+ ex.get('begin', ''), ex.get('end', ''), ex.get('project', '')))
290
+ return '\n'.join(lines)
291
+
292
+ if name == 'link_story_to_execution':
293
+ sid = args.get('story_id', 0)
294
+ eid = args.get('execution_id', 0)
295
+ r = _api('POST', '/executions/%s/stories' % eid, {'stories': [sid]})
296
+ if r.status_code in (200, 201):
297
+ return '✅ 需求 #%s 已关联到执行 #%s' % (sid, eid)
298
+ try:
299
+ err = r.json().get('error', r.text[:100])
300
+ except Exception:
301
+ err = r.text[:100]
302
+ return '⚠️ 关联失败: %s\n(可能权限不足, 需在禅道手动关联)' % err
303
+
304
+ if name == 'assign_story':
305
+ sid = args.get('story_id', 0)
306
+ account = args.get('account', '')
307
+ r = _api('POST', '/stories/%s/assign' % sid, {'assignedTo': account})
308
+ if r.status_code in (200, 201):
309
+ return '✅ 需求 #%s 已指派给 %s' % (sid, account)
310
+ try:
311
+ err = r.json().get('error', r.text[:100])
312
+ except Exception:
313
+ err = r.text[:100]
314
+ return '⚠️ 指派失败: %s\n(可能权限不足, 需在禅道手动指派)' % err
315
+
316
+ if name == 'change_story_status':
317
+ sid = args.get('story_id', 0)
318
+ status = args.get('status', '')
319
+ r = _api('POST', '/stories/%s/change' % sid, {'status': status})
320
+ if r.status_code in (200, 201):
321
+ return '✅ 需求 #%s 状态变更为 %s' % (sid, status)
322
+ try:
323
+ err = r.json().get('error', r.text[:100])
324
+ except Exception:
325
+ err = r.text[:100]
326
+ return '⚠️ 状态变更失败: %s\n(可能权限不足或状态转换不合法)' % err
327
+
328
+ if name == 'list_execution_tasks':
329
+ eid = args.get('execution_id', 1)
330
+ r = _api('GET', '/executions/%s/tasks' % eid)
331
+ if r.status_code != 200:
332
+ return '查询失败: %s' % r.text[:100]
333
+ tasks = r.json().get('tasks', [])
334
+ lines = ['执行 %s 的任务 (%d 个):' % (eid, len(tasks))]
335
+ for t in tasks[:20]:
336
+ if isinstance(t, dict):
337
+ lines.append(' [#%s] %s (assignedTo=%s status=%s)' % (
338
+ t.get('id'), str(t.get('name', ''))[:25],
339
+ t.get('assignedTo', ''), t.get('status', '')))
340
+ return '\n'.join(lines)
341
+
342
+ if name == 'get_story_detail':
343
+ sid = args.get('story_id', 0)
344
+ r = _api('GET', '/stories/%s' % sid)
345
+ if r.status_code != 200:
346
+ return '查询失败: %s' % r.text[:100]
347
+ s = r.json()
348
+ return '需求 #%s: %s\n状态: %s\n优先级: %s\n描述: %s' % (
349
+ sid, s.get('title', ''), s.get('status', ''),
350
+ s.get('pri', ''), str(s.get('spec', ''))[:200])
351
+
352
+ return '未知工具: %s' % name
353
+
354
+
355
+ # ── MCP Server (继承 BaseMCPServer, 协议层由基类接管) ──────────────────
356
+ # v3.0 重构: 删除 _send/_handle/main 三段逐字复制代码 (~69行),
357
+ # 改为继承 BaseMCPServer (Template Method), 只填差异点。
358
+
359
+ def _bootstrap_base():
360
+ """延迟 import BaseMCPServer (scripts/ 在 sys.path)。"""
361
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
362
+ from common.mcp_base import BaseMCPServer
363
+ return BaseMCPServer
364
+
365
+
366
+ class ZentaoMCPServer(_bootstrap_base()):
367
+ """禅道 MCP Server。继承基类协议层, 只填工具逻辑。"""
368
+
369
+ SERVER_NAME = "qoder-zentao"
370
+ SERVER_VERSION = "1.0.0"
371
+ TOOLS = TOOLS # 引用上方模块级 TOOLS
372
+
373
+ def handle_tool(self, name, args):
374
+ return _execute_tool(name, args)
375
+
376
+ def on_initialize_hook(self, info):
377
+ # 启动时内网校验, 不通塞 warning (与旧版行为一致)
378
+ ok, msg = check_intranet()
379
+ if not ok:
380
+ info['warning'] = msg
381
+
382
+
383
+ if __name__ == '__main__':
384
+ ZentaoMCPServer().run()