@hupan56/wlkj 2.7.3 → 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/bin/cli.js +162 -32
- package/package.json +29 -29
- package/templates/qoder/commands/wl-design.md +113 -20
- package/templates/qoder/commands/wl-prd.md +89 -17
- package/templates/qoder/config.yaml +7 -2
- package/templates/qoder/scripts/git_sync.py +5 -1
- package/templates/qoder/scripts/install_qoderwork.py +1 -0
- package/templates/qoder/scripts/mcp_launcher.py +35 -0
- package/templates/qoder/scripts/zentao_mcp_server.py +295 -0
- package/templates/qoder/commands/wl-design-draw.md +0 -78
- package/templates/qoder/commands/wl-design-scan.md +0 -108
- package/templates/qoder/commands/wl-design-spec.md +0 -154
- package/templates/qoder/commands/wl-prd-full.md +0 -226
- package/templates/qoder/commands/wl-prd-quick.md +0 -134
- package/templates/qoder/commands/wl-prd-review.md +0 -104
|
@@ -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
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-design-draw
|
|
3
|
-
description: "画界面/原型。说你要什么,AI 画出来。新页面、改页面、参考竞品都行。"
|
|
4
|
-
argument-hint: "<你要画什么>"
|
|
5
|
-
auto-approve: true
|
|
6
|
-
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit, mcp__qoder-knowledge-graph, mcp__lanhu]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /wl-design-draw - 画界面
|
|
10
|
-
|
|
11
|
-
User input: $ARGUMENTS
|
|
12
|
-
|
|
13
|
-
> 用户说"画什么",AI 画出来。就这么简单。
|
|
14
|
-
|
|
15
|
-
## 🔧 环境自检(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
16
|
-
|
|
17
|
-
**先确定仓库根 R**(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
18
|
-
```bash
|
|
19
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
20
|
-
```
|
|
21
|
-
> 后续脚本统一用 `python "$R/.qoder/scripts/xxx.py"`。
|
|
22
|
-
|
|
23
|
-
## Step 1: 平台(问了就停)
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
这个界面是针对哪个平台?
|
|
27
|
-
1. Web 管理端 (fywl-ui)
|
|
28
|
-
2. APP 移动端 (Carmg-H5)
|
|
29
|
-
3. 两端都要
|
|
30
|
-
请选择 (1/2/3):
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Step 2: 如果输入是大板块,先缩小到具体页面
|
|
34
|
-
|
|
35
|
-
"资产"有 38 个页面,不能直接画。调 `mcp__qoder-knowledge-graph__feature_overview(feature='资产')`,
|
|
36
|
-
列出页面问用户选哪个(**问了就停**)。
|
|
37
|
-
|
|
38
|
-
具体功能(如"营业外合同筛选")跳过这步。
|
|
39
|
-
|
|
40
|
-
## Step 3: 画
|
|
41
|
-
|
|
42
|
-
**先拿设计约束 + 真实数据(不许跳过,不许手写 HTML):**
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
mcp__qoder-knowledge-graph__get_design_system(platform='web') → 颜色/布局/按钮/组件
|
|
46
|
-
mcp__qoder-knowledge-graph__fill_prototype(keyword='营业外合同', platform='web') → 80% 草稿
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
FALLBACK(无 MCP 时):
|
|
50
|
-
```bash
|
|
51
|
-
python "$R/.qoder/scripts/gen_design_doc.py"
|
|
52
|
-
python "$R/.qoder/scripts/fill_prototype.py" 营业外合同 --platform web
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**拿到草稿后微调:**
|
|
56
|
-
- 按钮用真实文案(数据清单里的,不是编的"新增/编辑")
|
|
57
|
-
- 补交互细节(弹窗/Tab/折叠)
|
|
58
|
-
- 画完整状态(有数据 + 空状态 + 加载中)
|
|
59
|
-
- **颜色/宽度/间距不许改**(来自真实系统,AI 猜的一定不对)
|
|
60
|
-
|
|
61
|
-
## 铁律
|
|
62
|
-
|
|
63
|
-
1. **必须先调 get_design_system + fill_prototype**,不许跳过直接手写
|
|
64
|
-
2. **若设计师录入了蓝湖 spec,fill_prototype 会自动注入**——先确认 `data/style/{关键词}-design-spec.json` 是否存在。若有,fill_prototype 会把蓝湖的真实 `design_tokens`(如 `rgba(255,115,10,1)`)原样注入原型 `:root`,**这一步全自动,不用手动调蓝湖**。验证:看输出原型 `:root` 里有没有 `/* design-import spec tokens (优先级最高) */`
|
|
65
|
-
3. **颜色只用真源**(蓝湖 spec 注入值 > get_design_system 的 HSL,不是 #1890ff)
|
|
66
|
-
4. **按钮用真实文案**(entity-registry 的,不是编的)
|
|
67
|
-
5. **布局参数不猜**(蓝湖 spec 的 width 或 layout_fingerprint 的 160px)
|
|
68
|
-
6. **图标禁 emoji**(Web 用 Ant Design SVG,APP 用 Vant 字体图标)
|
|
69
|
-
7. **画完整状态**(不只画"有数据",还要画空状态/加载中/错误)
|
|
70
|
-
|
|
71
|
-
> 蓝湖与画图的关系:**蓝湖在录入环节(/wl-design-spec)发力**,读出的 CSS 值存进 spec.json;
|
|
72
|
-
> 画图时 fill_prototype 自动消费 spec.json,不需要 draw 再调蓝湖。蓝湖没起/没录入?不影响 draw,
|
|
73
|
-
> fill_prototype 降级用 layout_fingerprint + 代码风格,照样出原型(只是不如蓝湖精确)。
|
|
74
|
-
|
|
75
|
-
## 存储与 Figma
|
|
76
|
-
|
|
77
|
-
存到 `workspace/members/{developer}/drafts/prototype-{feature}.html`。
|
|
78
|
-
设计师可在 Figma 用 html.to.design 插件导入精修。
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-design-scan
|
|
3
|
-
description: "扫描/审视系统现状。看功能模块有哪些页面/按钮/流程/风格/测试覆盖。画原型前必做。"
|
|
4
|
-
argument-hint: "<功能模块名或关键词>"
|
|
5
|
-
auto-approve: true
|
|
6
|
-
allowed-tools: [Read, Glob, Grep, Bash, mcp__qoder-knowledge-graph]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /wl-design-scan - 审视系统现状
|
|
10
|
-
|
|
11
|
-
User input: $ARGUMENTS
|
|
12
|
-
|
|
13
|
-
> 画之前先看。设计师/PM 需要理解"系统现在长什么样"才能动手。
|
|
14
|
-
> 这是业界 Discover 阶段的工具——understand before you design。
|
|
15
|
-
|
|
16
|
-
## 🔧 环境自检(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
17
|
-
|
|
18
|
-
**先确定仓库根 R**(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
19
|
-
```bash
|
|
20
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
21
|
-
```
|
|
22
|
-
> 后续脚本统一用 `python "$R/.qoder/scripts/xxx.py"`。
|
|
23
|
-
|
|
24
|
-
## 第一步:判断用户想看什么
|
|
25
|
-
|
|
26
|
-
| 用户说什么 | 调什么 | 返回什么 |
|
|
27
|
-
|-----------|--------|---------|
|
|
28
|
-
| "XX 有哪些页面""XX 功能模块" | `feature_overview(feature='XX')` | 页面列表 + 端点数 + 按钮数 + 测试数 |
|
|
29
|
-
| "XX 的操作流程""XX 业务流程" | `get_workflow(module='XX')` | 操作链(查询→新增→审批→...) |
|
|
30
|
-
| "XX 用什么风格""系统主色" | `get_design_system(platform='web')` | token + 布局指纹 + 组件表 + 真实按钮 |
|
|
31
|
-
| "XX 有没有测试""XX 测试覆盖" | `coverage_matrix()` | 17 个功能的测试覆盖红黄绿 |
|
|
32
|
-
| "改 XX 影响什么" | `get_impact(endpoint='XX')` | 影响页面数 + 操作数 + 可回归测试 |
|
|
33
|
-
| "XX 按钮 调什么接口" | `context_360(symbol='handleXX')` | 按钮关联的 API + Controller + 调用链 |
|
|
34
|
-
| 模糊 | 先问 | "你想看功能模块、操作流程、设计风格、测试覆盖、还是影响分析?" |
|
|
35
|
-
|
|
36
|
-
## 第二步:执行查询
|
|
37
|
-
|
|
38
|
-
**PREFERRED: MCP 工具**(QoderWork 有 MCP 时直接调)
|
|
39
|
-
```
|
|
40
|
-
mcp__qoder-knowledge-graph__feature_overview(feature='资产管理')
|
|
41
|
-
mcp__qoder-knowledge-graph__get_workflow(module='资产')
|
|
42
|
-
mcp__qoder-knowledge-graph__get_design_system(platform='web')
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**FALLBACK: Python 脚本**
|
|
46
|
-
```bash
|
|
47
|
-
python "$R/.qoder/scripts/search_index.py" 资产 --platform web
|
|
48
|
-
python "$R/.qoder/scripts/search_index.py" --style table --platform web
|
|
49
|
-
python "$R/.qoder/scripts/search_index.py" --field assetName
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## 第三步:结构化输出
|
|
53
|
-
|
|
54
|
-
不是简单返回数据,而是**帮用户理解**:
|
|
55
|
-
|
|
56
|
-
### 功能模块视图(用户说"XX 有哪些页面"时)
|
|
57
|
-
```
|
|
58
|
-
📊 资产管理 (assets) 模块全景
|
|
59
|
-
页面: 38 个
|
|
60
|
-
API: 12 个端点
|
|
61
|
-
按钮: 108 个操作
|
|
62
|
-
测试: 3 个测试用例 (覆盖率偏低 ⚠️)
|
|
63
|
-
|
|
64
|
-
主要页面类型:
|
|
65
|
-
- table-page: 20 个 (异常申请/记录/分析...)
|
|
66
|
-
- form-page: 12 个 (新增/编辑/审批...)
|
|
67
|
-
- dashboard: 3 个 (资产看板)
|
|
68
|
-
- detail-page: 3 个
|
|
69
|
-
|
|
70
|
-
标杆页面 (最适合做参考的):
|
|
71
|
-
1. assets/abnormal/apply/index.vue (异常申请, 19个字段)
|
|
72
|
-
2. assets/abnormalManage/abnormalAnalysis/index.vue (分析看板)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### 操作流程视图(用户说"XX 流程"时)
|
|
76
|
-
```
|
|
77
|
-
🔄 资产管理操作链 (7 步)
|
|
78
|
-
查询 → 新增 → 提交 → 审批 → 归档 → 导出 → 删除
|
|
79
|
-
|
|
80
|
-
⚠️ 步骤较多 (7步), 考虑优化:
|
|
81
|
-
- 第3步"提交"和第4步"审批"可能合并
|
|
82
|
-
- 第6步"导出"可放到工具栏, 不算独立步骤
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### 设计风格视图(用户说"XX 风格"时)
|
|
86
|
-
```
|
|
87
|
-
🎨 fywl-ui 设计指纹
|
|
88
|
-
主色: hsl(214.58, 86.34%, 59.8%) (Vben 蓝)
|
|
89
|
-
侧边栏: 160px, 浅色, mixed-nav 布局
|
|
90
|
-
顶栏: 深色, 含 logo + 通知 + 头像
|
|
91
|
-
圆角: 0.25rem
|
|
92
|
-
字体: -apple-system, BlinkMacSystemFont, ...
|
|
93
|
-
|
|
94
|
-
真实按钮文案示例: 添加反馈资产, 导出Excel, 批量导入, ...
|
|
95
|
-
常用组件: Modal(273次), Input(135次), FormItem(95次), ...
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## 使用示例
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
/wl-design-scan 资产管理 → 功能模块全景
|
|
102
|
-
/wl-design-scan 资产的操作流程 → 操作链分析
|
|
103
|
-
/wl-design-scan 系统设计风格 → 设计 token + 布局指纹
|
|
104
|
-
/wl-design-scan 哪些功能没测试 → 覆盖矩阵
|
|
105
|
-
/wl-design-scan 改 /asset 影响 → 影响分析
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
> scan 不产出文件,只返回结构化分析。帮设计师/PM"看清楚"再动手。
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wl-design-spec
|
|
3
|
-
description: "设计交付。录入设计稿→spec.json + 评审原型合规性。设计师和系统对接的唯一入口。"
|
|
4
|
-
argument-hint: "<录入|评审> <描述>"
|
|
5
|
-
auto-approve: true
|
|
6
|
-
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit, mcp__qoder-knowledge-graph, mcp__lanhu]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /wl-design-spec - 设计交付
|
|
10
|
-
|
|
11
|
-
User input: $ARGUMENTS
|
|
12
|
-
|
|
13
|
-
> 设计的终点:把设计意图"规格化"让前端能实现,并评审原型是否合规。
|
|
14
|
-
> 包含两个动作:**录入**(设计师→系统)和**评审**(系统→设计师)。
|
|
15
|
-
|
|
16
|
-
## 🔧 环境自检(QoderWork 桌面端 vs Qoder IDE/CLI)
|
|
17
|
-
|
|
18
|
-
**先确定仓库根 R**(QoderWork 桌面端工作目录不是仓库根,相对路径会失效):
|
|
19
|
-
```bash
|
|
20
|
-
R=$(python ~/.qoderwork/repo_root.py 2>/dev/null) || R=.
|
|
21
|
-
```
|
|
22
|
-
> 后续脚本统一用 `python "$R/.qoder/scripts/xxx.py"`。
|
|
23
|
-
|
|
24
|
-
## 第一步:判断用户要做哪个动作
|
|
25
|
-
|
|
26
|
-
| 用户说什么 | 动作 | 跳到 |
|
|
27
|
-
|-----------|------|------|
|
|
28
|
-
| "录入""这是 Figma 稿""把这个设计录进去""设计稿录入" | **录入** | 录入段 |
|
|
29
|
-
| "评审""检查""看看这个原型""合规吗" | **评审** | 评审段 |
|
|
30
|
-
| 模糊 | 先问 | "你是要录入设计稿,还是评审现有原型?" |
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 录入(设计师把 Figma 稿变成 spec.json)
|
|
35
|
-
|
|
36
|
-
**铁律:平台必须先问。** 问完就停。
|
|
37
|
-
|
|
38
|
-
### 流程
|
|
39
|
-
|
|
40
|
-
完整执行见 `.qoder/skills/design-import/SKILL.md`:
|
|
41
|
-
|
|
42
|
-
1. **录入前先看现状**(让 spec 不是凭空设计,是基于系统增量改进):
|
|
43
|
-
```
|
|
44
|
-
mcp__qoder-knowledge-graph__feature_overview(feature='资产') → 这个模块现有页面/按钮
|
|
45
|
-
mcp__qoder-knowledge-graph__get_workflow(module='资产') → 现有操作流程
|
|
46
|
-
mcp__qoder-knowledge-graph__get_design_system(platform='web') → 现有 token/布局/组件
|
|
47
|
-
```
|
|
48
|
-
设计师据此知道"系统现在有什么、我的设计改了什么"。
|
|
49
|
-
|
|
50
|
-
2. **收集设计信息**(按精度从高到低):
|
|
51
|
-
- **蓝湖链接(最推荐)**:设计师发 `https://lanhuapp.com/web/#/item/...`,走 4 步直读
|
|
52
|
-
(详见 design-import/SKILL.md 方式 A):
|
|
53
|
-
① `mcp__lanhu__get_designs(url)` 探测+列图(失败就降级截图口述,不报错)
|
|
54
|
-
② 让设计师选哪张图(**问了就停**)
|
|
55
|
-
③ `mcp__lanhu__get_ai_analyze_design_result(url, design_names)` 读 CSS 标注+切图
|
|
56
|
-
④ AI 语义映射进 spec.json(出现最多的色→主色,width→侧边栏...)
|
|
57
|
-
- **铁律:蓝湖 CSS 值原样填**。`rgba(255,115,10,1)` 不写成 `#FF730A`,`200px` 不四舍五入
|
|
58
|
-
- STDIO 自动开关(开 QoderWork 自动起/关自动停,无需手动 start);cookie 按角色在 `workspace/members/{当前用户}/.secrets/lanhu.env`
|
|
59
|
-
- **截图 + 口述**(蓝湖不可用时降级):设计师发一张 Figma 截图,口述关键决策
|
|
60
|
-
- **导出标注**(CSS / JSON / PDF)
|
|
61
|
-
- **参照现有页面改**("类似 XX 页面,但侧边栏改成手风琴")
|
|
62
|
-
|
|
63
|
-
3. **生成 spec.json**,结构包含:
|
|
64
|
-
```json
|
|
65
|
-
{
|
|
66
|
-
"platform": "web",
|
|
67
|
-
"requirement": "异常资产申请页改版",
|
|
68
|
-
"design_tokens": { "--primary-color": "rgba(255,115,10,1)", "--sidebar-width": "160px" },
|
|
69
|
-
"layout": { "description": "...", "sidebar": "...", "content": "..." },
|
|
70
|
-
"components": [{ "name": "...", "spec": "..." }],
|
|
71
|
-
"behaviors": { "sidebar_collapse": "click", "form_mode": "wizard" },
|
|
72
|
-
"source": "蓝湖直读 (设计师: XXX)",
|
|
73
|
-
"lanhu_source": { "url": "...", "image_names": ["..."] }
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
> ⚠️ `requirement` 必须跟未来 `/wl-design-draw` 关键词一致(fill_prototype 靠它匹配)。
|
|
77
|
-
> 蓝湖来源填 `"source": "蓝湖直读 (...)"` + `lanhu_source`;截图口述只填 `source`。
|
|
78
|
-
|
|
79
|
-
4. **存储到** `data/style/{需求名}-{平台}-design-spec.json`(平台=web/app,避免同功能多端冲突;同需求同平台重录=覆盖更新)
|
|
80
|
-
> `requirement` 用"功能名+同义词"(如 `待办 我的待办 设置待办`),让 PM 搜各种词都能命中。
|
|
81
|
-
|
|
82
|
-
5. **优先级声明**:spec.json > 代码风格 > PDF 规范
|
|
83
|
-
一旦录入,同需求的 `/wl-design-draw` 必须锚定它。
|
|
84
|
-
|
|
85
|
-
### 铁律
|
|
86
|
-
|
|
87
|
-
- **绝不编造 token**:设计师没说的值,留空或标"待确认"
|
|
88
|
-
- **图标必须来自真源**:即使设计师用 emoji 示意,录入时替换成系统真源
|
|
89
|
-
(Web: `data/index/ref-icon.json` Ant Design SVG;APP: Vant 字体图标)
|
|
90
|
-
- **录入后同需求原型必须锚定它**
|
|
91
|
-
|
|
92
|
-
### 完成提示
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
✅ 设计规范已录入: data/style/{需求名}-design-spec.json
|
|
96
|
-
下次 /wl-design-draw 同关键词时,会自动优先用这份 spec。
|
|
97
|
-
设计师可随时 /wl-design-spec 录入 更新它。
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Figma 协作流程
|
|
101
|
-
|
|
102
|
-
设计师在 Figma 精修完设计稿后,导出/截图,用 `/wl-design-spec 录入` 录入。
|
|
103
|
-
完整 Figma 协作指南见 `.qoder/skills/design-import/figma-workflow.md`。
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 评审(检查原型是否合规)
|
|
108
|
-
|
|
109
|
-
完整执行见 `.qoder/skills/design-review/SKILL.md`。
|
|
110
|
-
|
|
111
|
-
读最新的 `workspace/members/{dev}/drafts/prototype-*.html`,按 checklist 评审:
|
|
112
|
-
|
|
113
|
-
### 评审 checklist
|
|
114
|
-
|
|
115
|
-
**视觉合规:**
|
|
116
|
-
- [ ] 颜色来自真源(get_design_system 的 HSL token,不是 #1890ff)
|
|
117
|
-
- [ ] 图标来自真源(ref-icon.json 的 SVG,无 emoji)
|
|
118
|
-
- [ ] 侧边栏宽度 = layout_fingerprint 值(fywl-ui 是 160px)
|
|
119
|
-
- [ ] 字体/圆角/间距跟系统一致
|
|
120
|
-
|
|
121
|
-
**内容合规:**
|
|
122
|
-
- [ ] 按钮文案用了系统真实文案(entity-registry,非编的"新增/编辑")
|
|
123
|
-
- [ ] 表格列/表单字段来自真实代码(fill_prototype 数据清单)
|
|
124
|
-
- [ ] 若设计师 spec 存在:原型匹配 spec 的布局/配色/组件
|
|
125
|
-
|
|
126
|
-
**交互合规:**
|
|
127
|
-
- [ ] 交互流程闭环(对照 get_workflow 的操作链,原型覆盖了全部步骤)
|
|
128
|
-
- [ ] 包含完整状态(有数据/空状态/加载中/错误)
|
|
129
|
-
- [ ] 布局跟同功能模块标杆页面一致(对照 feature_overview)
|
|
130
|
-
|
|
131
|
-
**测试影响:**
|
|
132
|
-
- [ ] 若改动页面有测试覆盖(coverage_matrix),提醒"改动后需回归测试"
|
|
133
|
-
|
|
134
|
-
### 评审时可调 MCP 工具
|
|
135
|
-
|
|
136
|
-
- `mcp__qoder-knowledge-graph__feature_overview(feature='XX')` → 功能画像,对比原型是否遗漏
|
|
137
|
-
- `mcp__qoder-knowledge-graph__get_workflow(module='XX')` → 操作链,检查原型是否覆盖完整流程
|
|
138
|
-
- `mcp__coverage_matrix()` → 测试覆盖,判断改动是否需回归
|
|
139
|
-
- `mcp__qoder-knowledge-graph__get_design_system(platform='web')` → 真实 token/按钮做对照
|
|
140
|
-
|
|
141
|
-
### 输出
|
|
142
|
-
|
|
143
|
-
报告:PASS 或 🔴/🟡 问题清单(不产文件,只出报告)。
|
|
144
|
-
若不通过,附上修复建议和参考文件路径。
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## 使用示例
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
/wl-design-spec 录入 这个 Figma 稿 → 录入设计稿
|
|
152
|
-
/wl-design-spec 评审 最新原型 → 评审原型
|
|
153
|
-
/wl-design-spec 这个按钮文案对不对 → 评审(按钮文案检查)
|
|
154
|
-
```
|