@szc-ft/mcp-szcd-client 0.23.1 → 0.25.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/agents/build.js +22 -12
- package/agents/opencode-extension/agents/szcd-component-expert.md +94 -50
- package/agents/qwen-extension/agents/szcd-component-expert.md +94 -50
- package/agents/src/szcd-component-expert.md +94 -50
- package/agents/src/tools.json +1 -1
- package/agents/szcd-component-expert.md +94 -50
- package/agents/szcd-component-expert.qoder.md +95 -51
- package/agents/szcd-component-expert.trae.md +94 -50
- package/opencode-extension/agents/szcd-component-expert.md +94 -50
- package/opencode-extension/skills/local-api-tool/SKILL.md +164 -60
- package/opencode-extension/skills/local-api-tool/scripts/extract_swagger.py +477 -0
- package/opencode-extension/skills/szcd-component-helper/SKILL.md +22 -17
- package/opencode-extension/skills/szcd-design-to-code/SKILL.md +141 -31
- package/package.json +1 -1
- package/qwen-extension/agents/szcd-component-expert.md +94 -50
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-api-tool/SKILL.md +164 -60
- package/qwen-extension/skills/local-api-tool/scripts/extract_swagger.py +477 -0
- package/qwen-extension/skills/szcd-component-helper/SKILL.md +22 -17
- package/qwen-extension/skills/szcd-design-to-code/SKILL.md +141 -31
- package/scripts/lib/claude-code.js +1 -1
- package/scripts/lib/common.js +44 -0
- package/scripts/lib/opencode.js +1 -1
- package/scripts/lib/qoder.js +1 -1
- package/scripts/lib/trae-cli.js +1 -1
- package/scripts/lib/trae-ide.js +1 -1
- package/scripts/postinstall.js +1 -0
- package/standard-skill/local-api-tool/SKILL.md +164 -60
- package/standard-skill/local-api-tool/scripts/extract_swagger.py +477 -0
- package/standard-skill/szcd-component-helper/SKILL.md +22 -17
- package/standard-skill/szcd-design-to-code/SKILL.md +141 -31
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
extract_swagger.py — 本地 Swagger 文档提取与解析(与服务端 parseApiDocs 一比一对齐)
|
|
4
|
+
|
|
5
|
+
设计目标:
|
|
6
|
+
- 一步完成"下载 → 解析 → 按 tag/path 过滤 → 关联 DTO definitions → 输出"
|
|
7
|
+
- 避免 AI 在客户端用 curl|head 截断 JSON / 写临时 python 脚本踩花括号坑
|
|
8
|
+
- 输出结构与服务端 api_tool(action="parse_swagger_json") 完全一致,可平替
|
|
9
|
+
|
|
10
|
+
参考实现:szcd-mcp-server/src/lib/swagger.js 的 parseApiDocs + server.js 的 filterApis
|
|
11
|
+
递归深度 5,两层 definitions(summary + detail),引用树传递闭包裁剪。
|
|
12
|
+
|
|
13
|
+
用法:
|
|
14
|
+
# 方式 A:脚本内部 curl 下载
|
|
15
|
+
extract_swagger.py --url http://10.x.x.x/svc/v2/api-docs [--cookie 'JSESSIONID=xxx']
|
|
16
|
+
|
|
17
|
+
# 方式 B:读取本地已下载的 JSON 文件
|
|
18
|
+
extract_swagger.py --file /tmp/swagger.json
|
|
19
|
+
|
|
20
|
+
# 过滤(对 url/summary/operationId/tag/description/dtoRef/responseVoName 模糊匹配,大小写不敏感)
|
|
21
|
+
extract_swagger.py --file /tmp/swagger.json --filter tag
|
|
22
|
+
extract_swagger.py --file /tmp/swagger.json --filter tag --filter user
|
|
23
|
+
|
|
24
|
+
# 仅查询单个 definition(兜底单 VO 查询,等价于服务端 get_definition)
|
|
25
|
+
extract_swagger.py --file /tmp/swagger.json --definition KnowledgeOperLogVO
|
|
26
|
+
|
|
27
|
+
# 输出到文件而非 stdout
|
|
28
|
+
extract_swagger.py --file /tmp/swagger.json --filter tag --out /tmp/parsed.json
|
|
29
|
+
|
|
30
|
+
退出码:0 成功;1 参数错误;2 网络/IO 错误;3 解析错误
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import sys
|
|
36
|
+
import subprocess
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
MAX_DEPTH = 5
|
|
40
|
+
METHOD_KEYS = ["post", "get", "put", "delete", "patch"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ==================== 工具函数 ====================
|
|
44
|
+
|
|
45
|
+
def _ref_name(prop):
|
|
46
|
+
"""从 property 定义中提取 $ref/originalRef 名称(不含数组 items 层)。"""
|
|
47
|
+
if not isinstance(prop, dict):
|
|
48
|
+
return None
|
|
49
|
+
if prop.get("originalRef"):
|
|
50
|
+
return prop["originalRef"]
|
|
51
|
+
ref = prop.get("$ref")
|
|
52
|
+
if ref:
|
|
53
|
+
return ref.rsplit("/", 1)[-1]
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ref_name_with_items(prop):
|
|
58
|
+
"""提取 $ref 名称,包含数组 items 层(对齐 JS 端 expandProperties 第 250-251 行)。"""
|
|
59
|
+
if not isinstance(prop, dict):
|
|
60
|
+
return None
|
|
61
|
+
name = _ref_name(prop)
|
|
62
|
+
if name:
|
|
63
|
+
return name
|
|
64
|
+
items = prop.get("items")
|
|
65
|
+
if isinstance(items, dict):
|
|
66
|
+
return _ref_name(items)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_property_type(prop):
|
|
71
|
+
"""对齐 JS 端 resolvePropertyType:数组返回 array<XXX>,否则返回 type 或 $ref 名。"""
|
|
72
|
+
if not isinstance(prop, dict):
|
|
73
|
+
return "string"
|
|
74
|
+
if prop.get("type") == "array":
|
|
75
|
+
items = prop.get("items") or {}
|
|
76
|
+
item_type = (
|
|
77
|
+
items.get("type")
|
|
78
|
+
or items.get("originalRef")
|
|
79
|
+
or (items.get("$ref", "").rsplit("/", 1)[-1] if items.get("$ref") else None)
|
|
80
|
+
or "object"
|
|
81
|
+
)
|
|
82
|
+
return f"array<{item_type}>"
|
|
83
|
+
if prop.get("type"):
|
|
84
|
+
return prop["type"]
|
|
85
|
+
if prop.get("originalRef"):
|
|
86
|
+
return prop["originalRef"]
|
|
87
|
+
if prop.get("$ref"):
|
|
88
|
+
return prop["$ref"].rsplit("/", 1)[-1]
|
|
89
|
+
return "object"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ==================== 递归展开(对齐 JS 端 expandProperties,深度 5) ====================
|
|
93
|
+
|
|
94
|
+
def expand_properties(properties, definitions, depth=0, max_depth=MAX_DEPTH):
|
|
95
|
+
if not properties:
|
|
96
|
+
return []
|
|
97
|
+
result = []
|
|
98
|
+
for name, prop in properties.items():
|
|
99
|
+
if not isinstance(prop, dict):
|
|
100
|
+
prop = {}
|
|
101
|
+
entry = {
|
|
102
|
+
"name": name,
|
|
103
|
+
"type": _resolve_property_type(prop),
|
|
104
|
+
"description": prop.get("description", ""),
|
|
105
|
+
}
|
|
106
|
+
if "format" in prop:
|
|
107
|
+
entry["format"] = prop["format"]
|
|
108
|
+
if "example" in prop:
|
|
109
|
+
entry["example"] = prop["example"]
|
|
110
|
+
if "enum" in prop:
|
|
111
|
+
entry["enum"] = prop["enum"]
|
|
112
|
+
|
|
113
|
+
ref_name = _ref_name_with_items(prop)
|
|
114
|
+
if ref_name:
|
|
115
|
+
entry["refName"] = ref_name
|
|
116
|
+
|
|
117
|
+
# 数组类型:展开 items 的嵌套属性
|
|
118
|
+
if prop.get("type") == "array" and ref_name and depth < max_depth:
|
|
119
|
+
item_def = (definitions or {}).get(ref_name)
|
|
120
|
+
if isinstance(item_def, dict) and item_def.get("properties"):
|
|
121
|
+
entry["itemProperties"] = expand_properties(
|
|
122
|
+
item_def["properties"], definitions, depth + 1, max_depth
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# 对象类型(非数组):展开嵌套属性
|
|
126
|
+
if prop.get("type") == "object" and ref_name and depth < max_depth:
|
|
127
|
+
nested_def = (definitions or {}).get(ref_name)
|
|
128
|
+
if isinstance(nested_def, dict) and nested_def.get("properties"):
|
|
129
|
+
entry["nestedProperties"] = expand_properties(
|
|
130
|
+
nested_def["properties"], definitions, depth + 1, max_depth
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 无 $ref 的 object 且有自身 properties(内联定义)
|
|
134
|
+
if (
|
|
135
|
+
prop.get("type") == "object"
|
|
136
|
+
and not ref_name
|
|
137
|
+
and prop.get("properties")
|
|
138
|
+
and depth < max_depth
|
|
139
|
+
):
|
|
140
|
+
entry["nestedProperties"] = expand_properties(
|
|
141
|
+
prop["properties"], definitions, depth + 1, max_depth
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
result.append(entry)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ==================== 主解析(对齐 JS 端 parseApiDocs) ====================
|
|
149
|
+
|
|
150
|
+
def parse_api_docs(api_docs, referenced_defs=None):
|
|
151
|
+
paths = api_docs.get("paths") or {}
|
|
152
|
+
tags = api_docs.get("tags") or []
|
|
153
|
+
definitions = api_docs.get("definitions") or {}
|
|
154
|
+
info = api_docs.get("info") or {}
|
|
155
|
+
api_base_path = api_docs.get("basePath") or ""
|
|
156
|
+
|
|
157
|
+
parsed_tags = [
|
|
158
|
+
{"name": t.get("name"), "description": t.get("description", "")} for t in tags
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
parsed_apis = []
|
|
162
|
+
auto_referenced_defs = set()
|
|
163
|
+
|
|
164
|
+
for url, path_value in paths.items():
|
|
165
|
+
if not isinstance(path_value, dict):
|
|
166
|
+
continue
|
|
167
|
+
for method in METHOD_KEYS:
|
|
168
|
+
api_def = path_value.get(method)
|
|
169
|
+
if not isinstance(api_def, dict):
|
|
170
|
+
continue
|
|
171
|
+
tag = (api_def.get("tags") or ["default"])[0]
|
|
172
|
+
parameters = api_def.get("parameters") or []
|
|
173
|
+
|
|
174
|
+
params = []
|
|
175
|
+
for p in parameters:
|
|
176
|
+
schema = p.get("schema") or {}
|
|
177
|
+
param_info = {
|
|
178
|
+
"name": p.get("name"),
|
|
179
|
+
"in": p.get("in"),
|
|
180
|
+
"required": p.get("required", False),
|
|
181
|
+
"type": p.get("type") or schema.get("type") or "string",
|
|
182
|
+
"description": p.get("description", ""),
|
|
183
|
+
}
|
|
184
|
+
# body 参数引用的 DTO
|
|
185
|
+
ref_name = schema.get("originalRef") or (
|
|
186
|
+
schema.get("$ref", "").rsplit("/", 1)[-1] if schema.get("$ref") else None
|
|
187
|
+
)
|
|
188
|
+
if ref_name:
|
|
189
|
+
param_info["dtoRef"] = ref_name
|
|
190
|
+
auto_referenced_defs.add(ref_name)
|
|
191
|
+
dto = definitions.get(ref_name)
|
|
192
|
+
if isinstance(dto, dict) and dto.get("properties"):
|
|
193
|
+
dto_props = expand_properties(dto["properties"], definitions, 0, MAX_DEPTH)
|
|
194
|
+
required_fields = dto.get("required") or []
|
|
195
|
+
for prop in dto_props:
|
|
196
|
+
if prop["name"] in required_fields:
|
|
197
|
+
prop["required"] = True
|
|
198
|
+
param_info["dtoProperties"] = dto_props
|
|
199
|
+
params.append(param_info)
|
|
200
|
+
|
|
201
|
+
# 响应(递归展开嵌套 VO)
|
|
202
|
+
response_info = None
|
|
203
|
+
responses = api_def.get("responses") or {}
|
|
204
|
+
r200 = responses.get("200") or {}
|
|
205
|
+
r_schema = r200.get("schema") if isinstance(r200, dict) else None
|
|
206
|
+
if isinstance(r_schema, dict):
|
|
207
|
+
ref_name = r_schema.get("originalRef") or (
|
|
208
|
+
r_schema.get("$ref", "").rsplit("/", 1)[-1] if r_schema.get("$ref") else None
|
|
209
|
+
)
|
|
210
|
+
if ref_name:
|
|
211
|
+
auto_referenced_defs.add(ref_name)
|
|
212
|
+
vo = definitions.get(ref_name)
|
|
213
|
+
if isinstance(vo, dict) and vo.get("properties"):
|
|
214
|
+
response_info = {
|
|
215
|
+
"voName": ref_name,
|
|
216
|
+
"properties": expand_properties(
|
|
217
|
+
vo["properties"], definitions, 0, MAX_DEPTH
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
else:
|
|
221
|
+
response_info = {"voName": ref_name}
|
|
222
|
+
|
|
223
|
+
parsed_apis.append({
|
|
224
|
+
"url": url,
|
|
225
|
+
"method": method.upper(),
|
|
226
|
+
"tag": tag,
|
|
227
|
+
"summary": api_def.get("summary", ""),
|
|
228
|
+
"description": api_def.get("description", ""),
|
|
229
|
+
"deprecated": api_def.get("deprecated", False),
|
|
230
|
+
"parameters": params,
|
|
231
|
+
"response": response_info,
|
|
232
|
+
"operationId": api_def.get("operationId"),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
# 收集被引用 definitions 的传递闭包
|
|
236
|
+
collected_refs = set(auto_referenced_defs)
|
|
237
|
+
queue = list(auto_referenced_defs)
|
|
238
|
+
while queue:
|
|
239
|
+
name = queue.pop(0)
|
|
240
|
+
d = definitions.get(name)
|
|
241
|
+
if not isinstance(d, dict) or not d.get("properties"):
|
|
242
|
+
continue
|
|
243
|
+
for prop in d["properties"].values():
|
|
244
|
+
ref_name = _ref_name_with_items(prop)
|
|
245
|
+
if ref_name and ref_name in definitions and ref_name not in collected_refs:
|
|
246
|
+
collected_refs.add(ref_name)
|
|
247
|
+
queue.append(ref_name)
|
|
248
|
+
|
|
249
|
+
# definitions 摘要层:所有 VO/DTO 的 name + type + description
|
|
250
|
+
definitions_summary = [
|
|
251
|
+
{
|
|
252
|
+
"name": name,
|
|
253
|
+
"type": d.get("type", "object") if isinstance(d, dict) else "object",
|
|
254
|
+
"description": (d.get("description") or d.get("title") or "") if isinstance(d, dict) else "",
|
|
255
|
+
}
|
|
256
|
+
for name, d in definitions.items()
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
# definitionsDetail 详情层:仅展开被引用的(含传递闭包),递归深度 5
|
|
260
|
+
effective_refs = referenced_defs if referenced_defs is not None else collected_refs
|
|
261
|
+
definitions_detail = []
|
|
262
|
+
for name in effective_refs:
|
|
263
|
+
d = definitions.get(name)
|
|
264
|
+
if isinstance(d, dict):
|
|
265
|
+
definitions_detail.append({
|
|
266
|
+
"name": name,
|
|
267
|
+
"type": d.get("type", "object"),
|
|
268
|
+
"description": d.get("description") or d.get("title") or "",
|
|
269
|
+
"properties": expand_properties(
|
|
270
|
+
d.get("properties") or {}, definitions, 0, MAX_DEPTH
|
|
271
|
+
),
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"info": {
|
|
276
|
+
"title": info.get("title"),
|
|
277
|
+
"version": info.get("version"),
|
|
278
|
+
"description": info.get("description"),
|
|
279
|
+
} if info else None,
|
|
280
|
+
"basePath": api_base_path,
|
|
281
|
+
"tags": parsed_tags,
|
|
282
|
+
"apis": parsed_apis,
|
|
283
|
+
"definitions": definitions_summary,
|
|
284
|
+
"definitionsDetail": definitions_detail,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ==================== 过滤(对齐 JS 端 filterApis + apiFilter 裁剪 definitionsDetail) ====================
|
|
289
|
+
|
|
290
|
+
def filter_apis(apis, keywords):
|
|
291
|
+
"""对齐 JS 端 filterApis:对 url/summary/operationId/tag/description/dtoRef/voName 模糊匹配。"""
|
|
292
|
+
if not keywords or not apis:
|
|
293
|
+
return apis
|
|
294
|
+
lower_keywords = [k.lower() for k in keywords]
|
|
295
|
+
out = []
|
|
296
|
+
for api in apis:
|
|
297
|
+
dto_refs = " ".join(p.get("dtoRef", "") for p in (api.get("parameters") or []) if p.get("dtoRef"))
|
|
298
|
+
response_vo = (api.get("response") or {}).get("voName", "") if api.get("response") else ""
|
|
299
|
+
searchable = " ".join([
|
|
300
|
+
api.get("url", ""),
|
|
301
|
+
api.get("summary", ""),
|
|
302
|
+
api.get("operationId") or "",
|
|
303
|
+
api.get("tag", ""),
|
|
304
|
+
api.get("description", ""),
|
|
305
|
+
dto_refs,
|
|
306
|
+
response_vo,
|
|
307
|
+
]).lower()
|
|
308
|
+
if any(kw in searchable for kw in lower_keywords):
|
|
309
|
+
out.append(api)
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def trim_definitions_detail(result, keywords):
|
|
314
|
+
"""apiFilter 生效时,definitionsDetail 仅保留匹配 API 引用的(含传递闭包)。
|
|
315
|
+
对齐 JS 端 server.js 第 1581-1604 行的裁剪逻辑。"""
|
|
316
|
+
apis = result.get("apis") or []
|
|
317
|
+
matched_refs = set()
|
|
318
|
+
for api in apis:
|
|
319
|
+
for p in api.get("parameters") or []:
|
|
320
|
+
if p.get("dtoRef"):
|
|
321
|
+
matched_refs.add(p["dtoRef"])
|
|
322
|
+
if api.get("response") and api["response"].get("voName"):
|
|
323
|
+
matched_refs.add(api["response"]["voName"])
|
|
324
|
+
|
|
325
|
+
detail_index = {d["name"]: d for d in result.get("definitionsDetail") or []}
|
|
326
|
+
detail_names = set(matched_refs)
|
|
327
|
+
queue = list(matched_refs)
|
|
328
|
+
while queue:
|
|
329
|
+
name = queue.pop(0)
|
|
330
|
+
detail = detail_index.get(name)
|
|
331
|
+
if not detail or not detail.get("properties"):
|
|
332
|
+
continue
|
|
333
|
+
for prop in detail["properties"]:
|
|
334
|
+
ref = prop.get("refName")
|
|
335
|
+
if ref and ref not in detail_names:
|
|
336
|
+
detail_names.add(ref)
|
|
337
|
+
queue.append(ref)
|
|
338
|
+
|
|
339
|
+
result["definitionsDetail"] = [d for d in result.get("definitionsDetail") or [] if d["name"] in detail_names]
|
|
340
|
+
result["_filter"] = {
|
|
341
|
+
"keywords": keywords,
|
|
342
|
+
"matched": len(apis),
|
|
343
|
+
}
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ==================== get_definition(单 VO 查询兜底) ====================
|
|
348
|
+
|
|
349
|
+
def get_definition(api_docs, definition_name):
|
|
350
|
+
"""对齐 JS 端 get_definition action:按名查询单个 VO,找不到给模糊匹配建议。"""
|
|
351
|
+
definitions = api_docs.get("definitions") or {}
|
|
352
|
+
if definition_name in definitions:
|
|
353
|
+
result = parse_api_docs(api_docs, referenced_defs={definition_name})
|
|
354
|
+
target = next((d for d in result["definitionsDetail"] if d["name"] == definition_name), None)
|
|
355
|
+
return {"found": True, "definition": target}
|
|
356
|
+
|
|
357
|
+
# 模糊匹配
|
|
358
|
+
lower = definition_name.lower()
|
|
359
|
+
suggestions = [n for n in definitions.keys() if lower in n.lower()][:10]
|
|
360
|
+
return {
|
|
361
|
+
"found": False,
|
|
362
|
+
"definitionName": definition_name,
|
|
363
|
+
"suggestions": suggestions,
|
|
364
|
+
"message": f"未找到 definition '{definition_name}'。" + (
|
|
365
|
+
f"相近建议:{', '.join(suggestions)}" if suggestions else "无相近匹配。"
|
|
366
|
+
),
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ==================== 输入:curl 下载 / 读文件 ====================
|
|
371
|
+
|
|
372
|
+
def fetch_via_curl(url, cookie=None, auth=None, timeout=30):
|
|
373
|
+
"""脚本内部走 curl 下载 Swagger JSON。失败抛 RuntimeError。"""
|
|
374
|
+
cmd = ["curl", "-sS", "--connect-timeout", "10", "--max-time", str(timeout),
|
|
375
|
+
"-H", "Accept: application/json"]
|
|
376
|
+
if cookie:
|
|
377
|
+
cmd += ["-H", f"Cookie: {cookie}"]
|
|
378
|
+
if auth:
|
|
379
|
+
cmd += ["-H", f"Authorization: Basic {auth}"]
|
|
380
|
+
cmd.append(url)
|
|
381
|
+
try:
|
|
382
|
+
r = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
383
|
+
except FileNotFoundError:
|
|
384
|
+
raise RuntimeError("找不到 curl 命令,请确认本地已安装 curl") from None
|
|
385
|
+
if r.returncode != 0:
|
|
386
|
+
raise RuntimeError(f"curl 失败 (exit {r.returncode}): {r.stderr.strip() or '无错误输出'}")
|
|
387
|
+
if not r.stdout.strip():
|
|
388
|
+
raise RuntimeError("curl 返回空响应")
|
|
389
|
+
return r.stdout
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def load_json_text(text):
|
|
393
|
+
try:
|
|
394
|
+
return json.loads(text)
|
|
395
|
+
except json.JSONDecodeError as e:
|
|
396
|
+
# 给出第一行 200 字符 + 错误位置,方便排查
|
|
397
|
+
preview = text[:200].replace("\n", "\\n")
|
|
398
|
+
raise RuntimeError(f"JSON 解析失败: {e.msg} (line {e.lineno} col {e.colno})。响应预览: {preview}") from None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ==================== CLI ====================
|
|
402
|
+
|
|
403
|
+
def main():
|
|
404
|
+
parser = argparse.ArgumentParser(
|
|
405
|
+
description="本地 Swagger 文档提取与解析(与服务端 parseApiDocs 一比一对齐)",
|
|
406
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
407
|
+
epilog=__doc__,
|
|
408
|
+
)
|
|
409
|
+
src = parser.add_mutually_exclusive_group(required=True)
|
|
410
|
+
src.add_argument("--url", help="Swagger api-docs URL(脚本内部走 curl 下载)")
|
|
411
|
+
src.add_argument("--file", help="本地 Swagger JSON 文件路径")
|
|
412
|
+
|
|
413
|
+
parser.add_argument("--cookie", help="--url 模式可选,鉴权后的 Cookie 值(如 'JSESSIONID=xxx')")
|
|
414
|
+
parser.add_argument("--auth", help="--url 模式可选,Basic 鉴权 base64(user:pass)")
|
|
415
|
+
parser.add_argument("--timeout", type=int, default=30, help="--url 模式 curl 超时秒数,默认 30")
|
|
416
|
+
|
|
417
|
+
parser.add_argument(
|
|
418
|
+
"--filter", action="append", default=[],
|
|
419
|
+
help="按关键词过滤 api(可多次指定,对 url/summary/operationId/tag/description/dtoRef/voName 模糊匹配)",
|
|
420
|
+
)
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"--definition",
|
|
423
|
+
help="仅查询单个 VO/DTO 的完整字段定义(等价于服务端 get_definition action)",
|
|
424
|
+
)
|
|
425
|
+
parser.add_argument("--out", help="输出到文件(默认 stdout)")
|
|
426
|
+
parser.add_argument(
|
|
427
|
+
"--indent", type=int, default=2,
|
|
428
|
+
help="JSON 输出缩进,默认 2;设为 0 输出紧凑 JSON",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
args = parser.parse_args()
|
|
432
|
+
|
|
433
|
+
# 1. 读取原始 Swagger JSON
|
|
434
|
+
try:
|
|
435
|
+
if args.url:
|
|
436
|
+
raw_text = fetch_via_curl(args.url, cookie=args.cookie, auth=args.auth, timeout=args.timeout)
|
|
437
|
+
else:
|
|
438
|
+
raw_text = Path(args.file).read_text(encoding="utf-8")
|
|
439
|
+
except Exception as e:
|
|
440
|
+
print(f"[ERROR] 读取 Swagger 失败: {e}", file=sys.stderr)
|
|
441
|
+
sys.exit(2)
|
|
442
|
+
|
|
443
|
+
# 2. 解析 JSON
|
|
444
|
+
try:
|
|
445
|
+
api_docs = load_json_text(raw_text)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
print(f"[ERROR] {e}", file=sys.stderr)
|
|
448
|
+
sys.exit(3)
|
|
449
|
+
|
|
450
|
+
# 3. 路径分支:--definition 单查 / 默认完整解析
|
|
451
|
+
try:
|
|
452
|
+
if args.definition:
|
|
453
|
+
result = get_definition(api_docs, args.definition)
|
|
454
|
+
else:
|
|
455
|
+
result = parse_api_docs(api_docs)
|
|
456
|
+
if args.filter:
|
|
457
|
+
total = len(result["apis"])
|
|
458
|
+
result["apis"] = filter_apis(result["apis"], args.filter)
|
|
459
|
+
result = trim_definitions_detail(result, args.filter)
|
|
460
|
+
result["_filter"]["total"] = total
|
|
461
|
+
except Exception as e:
|
|
462
|
+
print(f"[ERROR] 解析失败: {e}", file=sys.stderr)
|
|
463
|
+
sys.exit(3)
|
|
464
|
+
|
|
465
|
+
# 4. 输出
|
|
466
|
+
indent = args.indent if args.indent > 0 else None
|
|
467
|
+
out_text = json.dumps(result, ensure_ascii=False, indent=indent)
|
|
468
|
+
if args.out:
|
|
469
|
+
Path(args.out).write_text(out_text, encoding="utf-8")
|
|
470
|
+
print(f"[OK] 已输出到 {args.out} ({len(out_text)} bytes)", file=sys.stderr)
|
|
471
|
+
else:
|
|
472
|
+
sys.stdout.write(out_text)
|
|
473
|
+
sys.stdout.write("\n")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
if __name__ == "__main__":
|
|
477
|
+
main()
|
|
@@ -81,14 +81,14 @@ AI 助手在处理页面生成需求时,必须按以下流程使用工具:
|
|
|
81
81
|
### 步骤3.5:Sketch 文件解析(如有 .sketch 设计稿)
|
|
82
82
|
- **当用户提供 .sketch 文件时**,使用 `sketch-mcp-server`(独立 MCP Server,stdio 模式)解析,直接提取结构化数据,结合组件库架构推理组件方案,无需经过视觉模型和 `map_design_data`
|
|
83
83
|
- 工作流(4步直传):
|
|
84
|
-
1. **解析 Sketch 结构**:`loadSketchByPath` → `listPages` → `
|
|
84
|
+
1. **解析 Sketch 结构**:`loadSketchByPath` → `listPages` → `queryNodes(pageId, type="artboard")` → `getNodeInfo`/`getPageStructure(maxDepth=2)` 提取结构
|
|
85
85
|
2. **获取组件库架构**(复用步骤1结果):`get_architecture_overview` 的 `templatePatterns` 提供模板组合模式,`llmMappingHints` 提供常见错误修正
|
|
86
86
|
3. **LLM 推理组件范围**:从 Sketch 结构化数据推断画板名称→`pageName`、区域分布→`layoutType`、图层类型→组件映射,对照 `templatePatterns` 选择最匹配的模板,输出组件候选列表
|
|
87
87
|
4. **批量获取组件详情**:调用 `get_component_full_profile(name="Query,TableOrList,LeftTree,TemplateMode", depth="deep")` 一次性获取所有需要的组件 API
|
|
88
88
|
|
|
89
89
|
**Sketch → szcd 工作流(结构化直传,无需视觉模型)**:
|
|
90
90
|
```
|
|
91
|
-
.sketch → loadSketchByPath → listPages →
|
|
91
|
+
.sketch → loadSketchByPath → listPages → queryNodes(pageId, type=artboard)
|
|
92
92
|
→ getNodeInfo/getPageStructure → [步骤1架构数据] → LLM推理组件
|
|
93
93
|
→ get_component_full_profile(批量) → 编码
|
|
94
94
|
```
|
|
@@ -107,7 +107,7 @@ AI 助手在处理页面生成需求时,必须按以下流程使用工具:
|
|
|
107
107
|
|
|
108
108
|
**提示**:
|
|
109
109
|
- 大型 .sketch 文件用 `getPageStructure(maxDepth=1-2)` 即可获取布局概况,避免深层递归超时
|
|
110
|
-
- `
|
|
110
|
+
- `queryNodes(pageId, type="artboard")` 优先于 `getPageStructure`,更轻量
|
|
111
111
|
- 仅当结构化数据不足以判断视觉细节(颜色、间距、字体)时,才回退到 `renderNodeAsBase64` + `analyze_design_image`
|
|
112
112
|
- 如果 sketch-mcp-server 工具不可用,提示用户安装:`npm install -g sketch-mcp-server`
|
|
113
113
|
|
|
@@ -124,7 +124,7 @@ AI 助手在处理页面生成需求时,必须按以下流程使用工具:
|
|
|
124
124
|
- 若用户表示不准确或不采纳,收集原因
|
|
125
125
|
- 调用 `submit_feedback` 将反馈提交到服务器
|
|
126
126
|
|
|
127
|
-
## 可用工具(13 +
|
|
127
|
+
## 可用工具(13 + 15 Sketch)
|
|
128
128
|
|
|
129
129
|
### Sketch 文件解析工具(sketch-mcp-server,独立 MCP Server)
|
|
130
130
|
|
|
@@ -152,26 +152,28 @@ sketch-mcp-server 是社区版 MCP Server(npm: `sketch-mcp-server`),通过
|
|
|
152
152
|
- `nodeId` (string, required): 节点 ID(artboard 级别效果最佳)
|
|
153
153
|
- `format` (enum, optional, default: "svg"): 输出格式,当前仅支持 svg
|
|
154
154
|
|
|
155
|
-
#### S5.
|
|
155
|
+
#### S5. getPageStructure (mode="summary")
|
|
156
156
|
**功能**:获取节点统计摘要,Token 消耗比完整信息减少 80-90%。
|
|
157
157
|
**参数**:
|
|
158
|
-
- `pageId` (string,
|
|
158
|
+
- `pageId` (string, required): 页面 ID
|
|
159
|
+
- `mode` (string, required): "summary"
|
|
159
160
|
- `groupBy` (enum, optional): type/style/position/size
|
|
160
|
-
- `
|
|
161
|
+
- `maxSamples` (number, optional, default: 5)
|
|
161
162
|
|
|
162
|
-
#### S6.
|
|
163
|
+
#### S6. queryNodes (name 参数)
|
|
163
164
|
**功能**:按名称搜索节点,支持模糊匹配。
|
|
164
165
|
**参数**:
|
|
165
166
|
- `name` (string, required): 搜索关键词
|
|
166
|
-
- `limit` (number, optional, default:
|
|
167
|
+
- `limit` (number, optional, default: 50): 最大返回数量
|
|
167
168
|
|
|
168
|
-
#### S7-
|
|
169
|
-
- `
|
|
169
|
+
#### S7-S15. 其他 Sketch 工具
|
|
170
|
+
- `queryNodes` — 统一节点查询(支持 pageId/type/name/nameContains/limit/offset)
|
|
170
171
|
- `getNodeInfo` / `getMultipleNodeInfo` — 获取节点详情
|
|
171
|
-
- `getNodePosition` — 获取节点位置
|
|
172
172
|
- `getDocumentStructure` — 获取文档结构(支持字段过滤和摘要模式)
|
|
173
|
-
- `
|
|
173
|
+
- `getSymbols` — Symbol 组件查询(type="master" 或 "instance")
|
|
174
174
|
- `getSymbolMasterBySymbolID` / `getSymbolInstanceStyles` — Symbol 详情
|
|
175
|
+
- `getShapePathData` — 提取 shapePath SVG 路径数据
|
|
176
|
+
- `extractBitmaps` / `matchIconFromName` — 资源提取与图标匹配
|
|
175
177
|
|
|
176
178
|
### 复合组件工具
|
|
177
179
|
|
|
@@ -209,16 +211,19 @@ sketch-mcp-server 是社区版 MCP Server(npm: `sketch-mcp-server`),通过
|
|
|
209
211
|
### 设计稿分析工具
|
|
210
212
|
|
|
211
213
|
#### 5. analyze_design_image
|
|
212
|
-
**功能**:分析 UI 设计稿图片,提取 Token 配置、CSS 覆盖样式、视觉细节。支持 base64/URL/文件路径,pixel_perfect 模式最高还原度。
|
|
214
|
+
**功能**:分析 UI 设计稿图片,提取 Token 配置、CSS 覆盖样式、视觉细节。支持 upload_id/base64/URL/文件路径,pixel_perfect 模式最高还原度。
|
|
213
215
|
**参数**:
|
|
214
|
-
- `imageSource` (string, required): 图片数据(base64/URL/文件路径)
|
|
215
|
-
- `sourceType` (enum, optional, default: "
|
|
216
|
+
- `imageSource` (string, required): 图片数据(upload_id/base64/URL/文件路径)
|
|
217
|
+
- `sourceType` (enum, optional, default: "upload_id"): upload_id/base64/url/file_path
|
|
216
218
|
- `analysisType` (enum, optional, default: "full"): layout/components/tokens/full/pixel_perfect
|
|
217
219
|
- `pageContext` (string, optional): 页面业务上下文
|
|
218
220
|
- `targetLibrary` (enum, optional, default: "auto"): szcd/antd/pro-components/auto
|
|
219
221
|
- `outputFormat` (enum, optional, default: "markdown"): markdown/json/structured_text/restoration_code
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
**重要提示**:
|
|
224
|
+
- 远程 SSE/HTTP 连接时优先使用 upload_id:先调用 `get_upload_endpoint` 获取上传地址,用 curl 上传图片获取 upload_id,再传给本工具
|
|
225
|
+
- **上传重试限制**:curl 上传最多重试 3 次(含首次),超过后停止重试并向用户说明失败原因,不要无限重试浪费 token
|
|
226
|
+
- 本地 stdio 直连可用 file_path
|
|
222
227
|
|
|
223
228
|
### 样式注入指南工具
|
|
224
229
|
|