@szc-ft/mcp-szcd-client 0.24.0 → 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 +92 -48
- package/agents/qwen-extension/agents/szcd-component-expert.md +92 -48
- package/agents/src/szcd-component-expert.md +92 -48
- package/agents/src/tools.json +1 -1
- package/agents/szcd-component-expert.md +92 -48
- package/agents/szcd-component-expert.qoder.md +93 -49
- package/agents/szcd-component-expert.trae.md +92 -48
- package/opencode-extension/agents/szcd-component-expert.md +92 -48
- 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 +7 -4
- package/opencode-extension/skills/szcd-design-to-code/SKILL.md +95 -24
- package/package.json +1 -1
- package/qwen-extension/agents/szcd-component-expert.md +92 -48
- 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 +7 -4
- package/qwen-extension/skills/szcd-design-to-code/SKILL.md +95 -24
- 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 +7 -4
- package/standard-skill/szcd-design-to-code/SKILL.md +95 -24
|
@@ -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()
|
|
@@ -211,16 +211,19 @@ sketch-mcp-server 是社区版 MCP Server(npm: `sketch-mcp-server`),通过
|
|
|
211
211
|
### 设计稿分析工具
|
|
212
212
|
|
|
213
213
|
#### 5. analyze_design_image
|
|
214
|
-
**功能**:分析 UI 设计稿图片,提取 Token 配置、CSS 覆盖样式、视觉细节。支持 base64/URL/文件路径,pixel_perfect 模式最高还原度。
|
|
214
|
+
**功能**:分析 UI 设计稿图片,提取 Token 配置、CSS 覆盖样式、视觉细节。支持 upload_id/base64/URL/文件路径,pixel_perfect 模式最高还原度。
|
|
215
215
|
**参数**:
|
|
216
|
-
- `imageSource` (string, required): 图片数据(base64/URL/文件路径)
|
|
217
|
-
- `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
|
|
218
218
|
- `analysisType` (enum, optional, default: "full"): layout/components/tokens/full/pixel_perfect
|
|
219
219
|
- `pageContext` (string, optional): 页面业务上下文
|
|
220
220
|
- `targetLibrary` (enum, optional, default: "auto"): szcd/antd/pro-components/auto
|
|
221
221
|
- `outputFormat` (enum, optional, default: "markdown"): markdown/json/structured_text/restoration_code
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
**重要提示**:
|
|
224
|
+
- 远程 SSE/HTTP 连接时优先使用 upload_id:先调用 `get_upload_endpoint` 获取上传地址,用 curl 上传图片获取 upload_id,再传给本工具
|
|
225
|
+
- **上传重试限制**:curl 上传最多重试 3 次(含首次),超过后停止重试并向用户说明失败原因,不要无限重试浪费 token
|
|
226
|
+
- 本地 stdio 直连可用 file_path
|
|
224
227
|
|
|
225
228
|
### 样式注入指南工具
|
|
226
229
|
|