@szc-ft/mcp-szcd-client 0.24.0 → 0.26.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 (34) hide show
  1. package/agents/build.js +22 -12
  2. package/agents/opencode-extension/agents/szcd-component-expert.md +235 -49
  3. package/agents/qwen-extension/agents/szcd-component-expert.md +235 -49
  4. package/agents/src/szcd-component-expert.md +262 -49
  5. package/agents/src/tools.json +1 -1
  6. package/agents/szcd-component-expert.md +235 -49
  7. package/agents/szcd-component-expert.qoder.md +261 -50
  8. package/agents/szcd-component-expert.trae.md +260 -49
  9. package/opencode-extension/agents/szcd-component-expert.md +235 -49
  10. package/opencode-extension/skills/local-api-tool/SKILL.md +296 -60
  11. package/opencode-extension/skills/local-api-tool/scripts/extract-swagger.mjs +625 -0
  12. package/opencode-extension/skills/local-api-tool/scripts/extract_swagger.py +593 -0
  13. package/opencode-extension/skills/szcd-component-helper/SKILL.md +7 -4
  14. package/opencode-extension/skills/szcd-design-to-code/SKILL.md +267 -25
  15. package/package.json +1 -1
  16. package/qwen-extension/agents/szcd-component-expert.md +235 -49
  17. package/qwen-extension/qwen-extension.json +1 -1
  18. package/qwen-extension/skills/local-api-tool/SKILL.md +296 -60
  19. package/qwen-extension/skills/local-api-tool/scripts/extract-swagger.mjs +625 -0
  20. package/qwen-extension/skills/local-api-tool/scripts/extract_swagger.py +593 -0
  21. package/qwen-extension/skills/szcd-component-helper/SKILL.md +7 -4
  22. package/qwen-extension/skills/szcd-design-to-code/SKILL.md +267 -25
  23. package/scripts/lib/claude-code.js +1 -1
  24. package/scripts/lib/common.js +44 -0
  25. package/scripts/lib/opencode.js +1 -1
  26. package/scripts/lib/qoder.js +1 -1
  27. package/scripts/lib/trae-cli.js +1 -1
  28. package/scripts/lib/trae-ide.js +1 -1
  29. package/scripts/postinstall.js +1 -0
  30. package/standard-skill/local-api-tool/SKILL.md +296 -60
  31. package/standard-skill/local-api-tool/scripts/extract-swagger.mjs +625 -0
  32. package/standard-skill/local-api-tool/scripts/extract_swagger.py +593 -0
  33. package/standard-skill/szcd-component-helper/SKILL.md +7 -4
  34. package/standard-skill/szcd-design-to-code/SKILL.md +267 -25
@@ -0,0 +1,625 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * extract-swagger.mjs — 本地 Swagger 文档提取与解析(与服务端 parseApiDocs 一比一对齐)
4
+ *
5
+ * 同目录 extract_swagger.py 的 Node.js 等价版本。
6
+ * 选 Node 是因为 mcp 客户端用户 100% 已装 Node,跨平台零安装;
7
+ * .mjs 扩展名强制 ESM,避开宿主 package.json "type":"module" 冲突。
8
+ *
9
+ * 用法:
10
+ * node extract-swagger.mjs --url http://10.x.x.x/svc/v2/api-docs [--cookie 'JSESSIONID=xxx']
11
+ * node extract-swagger.mjs --url ... --no-cache # 强制重新下载
12
+ * node extract-swagger.mjs --url ... --cache-ttl 3600 # 自定义 TTL(秒),0 关闭
13
+ *
14
+ * node extract-swagger.mjs --file /tmp/swagger.json
15
+ * node extract-swagger.mjs --file /tmp/swagger.json --filter tag --filter user
16
+ * node extract-swagger.mjs --file /tmp/swagger.json --definition KnowledgeOperLogVO
17
+ * node extract-swagger.mjs --file /tmp/swagger.json --filter tag --out /tmp/parsed.json
18
+ *
19
+ * 容错:自动修复八进制字面量 `[011,015]` → `[11,15]`;JSON 解析失败给出 ±80 字符上下文 + 常见原因。
20
+ * 网络:优先用 Node ≥18 内置 fetch;不可用时降级 spawnSync('curl')。
21
+ * 退出码:0 成功;1 参数错误;2 网络/IO 错误;3 解析错误
22
+ */
23
+
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import os from "node:os";
27
+ import crypto from "node:crypto";
28
+ import { spawnSync } from "node:child_process";
29
+
30
+ const MAX_DEPTH = 5;
31
+ const METHOD_KEYS = ["post", "get", "put", "delete", "patch"];
32
+
33
+ // 缓存配置(与 Python 版完全一致:同 key 可共享缓存)
34
+ const CACHE_DIR = path.join(os.tmpdir(), "swagger-cache");
35
+ const DEFAULT_CACHE_TTL = 600;
36
+
37
+ // 八进制字面量:JSON 标准不允许数字以 0 开头(除 0 本身或小数),
38
+ // 但部分 Java/旧网关序列化会产出 [011,015,022]。
39
+ const OCTAL_LITERAL_RE = /(?<=[\s,\[:])0(\d+)(?=[\s,\]}])/g;
40
+
41
+ // ==================== 工具函数 ====================
42
+
43
+ function refName(prop) {
44
+ if (!prop || typeof prop !== "object") return null;
45
+ if (prop.originalRef) return prop.originalRef;
46
+ if (prop.$ref) return prop.$ref.split("/").pop();
47
+ return null;
48
+ }
49
+
50
+ function refNameWithItems(prop) {
51
+ if (!prop || typeof prop !== "object") return null;
52
+ const name = refName(prop);
53
+ if (name) return name;
54
+ if (prop.items && typeof prop.items === "object") return refName(prop.items);
55
+ return null;
56
+ }
57
+
58
+ function resolvePropertyType(prop) {
59
+ if (!prop || typeof prop !== "object") return "string";
60
+ if (prop.type === "array") {
61
+ const items = prop.items || {};
62
+ const itemType =
63
+ items.type ||
64
+ items.originalRef ||
65
+ (items.$ref ? items.$ref.split("/").pop() : null) ||
66
+ "object";
67
+ return `array<${itemType}>`;
68
+ }
69
+ if (prop.type) return prop.type;
70
+ if (prop.originalRef) return prop.originalRef;
71
+ if (prop.$ref) return prop.$ref.split("/").pop();
72
+ return "object";
73
+ }
74
+
75
+ // ==================== 递归展开(对齐 expandProperties,深度 5) ====================
76
+
77
+ function expandProperties(properties, definitions, depth = 0, maxDepth = MAX_DEPTH) {
78
+ if (!properties) return [];
79
+ const out = [];
80
+ for (const [name, rawProp] of Object.entries(properties)) {
81
+ const prop = (rawProp && typeof rawProp === "object") ? rawProp : {};
82
+ const entry = {
83
+ name,
84
+ type: resolvePropertyType(prop),
85
+ description: prop.description || "",
86
+ };
87
+ if (prop.format !== undefined) entry.format = prop.format;
88
+ if (prop.example !== undefined) entry.example = prop.example;
89
+ if (prop.enum !== undefined) entry.enum = prop.enum;
90
+
91
+ const rName = refNameWithItems(prop);
92
+ if (rName) entry.refName = rName;
93
+
94
+ if (prop.type === "array" && rName && depth < maxDepth) {
95
+ const itemDef = (definitions || {})[rName];
96
+ if (itemDef && typeof itemDef === "object" && itemDef.properties) {
97
+ entry.itemProperties = expandProperties(itemDef.properties, definitions, depth + 1, maxDepth);
98
+ }
99
+ }
100
+
101
+ if (prop.type === "object" && rName && depth < maxDepth) {
102
+ const nested = (definitions || {})[rName];
103
+ if (nested && typeof nested === "object" && nested.properties) {
104
+ entry.nestedProperties = expandProperties(nested.properties, definitions, depth + 1, maxDepth);
105
+ }
106
+ }
107
+
108
+ if (prop.type === "object" && !rName && prop.properties && depth < maxDepth) {
109
+ entry.nestedProperties = expandProperties(prop.properties, definitions, depth + 1, maxDepth);
110
+ }
111
+
112
+ out.push(entry);
113
+ }
114
+ return out;
115
+ }
116
+
117
+ // ==================== 主解析(对齐 parseApiDocs) ====================
118
+
119
+ function parseApiDocs(apiDocs, referencedDefs = null) {
120
+ const paths = apiDocs.paths || {};
121
+ const tags = apiDocs.tags || [];
122
+ const definitions = apiDocs.definitions || {};
123
+ const info = apiDocs.info || null;
124
+ const apiBasePath = apiDocs.basePath || "";
125
+
126
+ const parsedTags = tags.map((t) => ({ name: t.name, description: t.description || "" }));
127
+ const parsedApis = [];
128
+ const autoReferencedDefs = new Set();
129
+
130
+ for (const [url, pathValue] of Object.entries(paths)) {
131
+ if (!pathValue || typeof pathValue !== "object") continue;
132
+ for (const method of METHOD_KEYS) {
133
+ const apiDef = pathValue[method];
134
+ if (!apiDef || typeof apiDef !== "object") continue;
135
+ const tag = (apiDef.tags && apiDef.tags[0]) || "default";
136
+ const parameters = apiDef.parameters || [];
137
+
138
+ const params = parameters.map((p) => {
139
+ const schema = p.schema || {};
140
+ const paramInfo = {
141
+ name: p.name,
142
+ in: p.in,
143
+ required: p.required || false,
144
+ type: p.type || schema.type || "string",
145
+ description: p.description || "",
146
+ };
147
+ const rName = schema.originalRef || (schema.$ref ? schema.$ref.split("/").pop() : null);
148
+ if (rName) {
149
+ paramInfo.dtoRef = rName;
150
+ autoReferencedDefs.add(rName);
151
+ const dto = definitions[rName];
152
+ if (dto && typeof dto === "object" && dto.properties) {
153
+ const dtoProps = expandProperties(dto.properties, definitions, 0, MAX_DEPTH);
154
+ const requiredFields = dto.required || [];
155
+ for (const prop of dtoProps) {
156
+ if (requiredFields.includes(prop.name)) prop.required = true;
157
+ }
158
+ paramInfo.dtoProperties = dtoProps;
159
+ }
160
+ }
161
+ return paramInfo;
162
+ });
163
+
164
+ // 响应(递归展开嵌套 VO)
165
+ let responseInfo = null;
166
+ const responses = apiDef.responses || {};
167
+ const r200 = responses["200"] || {};
168
+ const rSchema = (r200 && typeof r200 === "object") ? r200.schema : null;
169
+ if (rSchema && typeof rSchema === "object") {
170
+ const rName = rSchema.originalRef || (rSchema.$ref ? rSchema.$ref.split("/").pop() : null);
171
+ if (rName) {
172
+ autoReferencedDefs.add(rName);
173
+ const vo = definitions[rName];
174
+ if (vo && typeof vo === "object" && vo.properties) {
175
+ responseInfo = {
176
+ voName: rName,
177
+ properties: expandProperties(vo.properties, definitions, 0, MAX_DEPTH),
178
+ };
179
+ } else {
180
+ responseInfo = { voName: rName };
181
+ }
182
+ }
183
+ }
184
+
185
+ parsedApis.push({
186
+ url,
187
+ method: method.toUpperCase(),
188
+ tag,
189
+ summary: apiDef.summary || "",
190
+ description: apiDef.description || "",
191
+ deprecated: apiDef.deprecated || false,
192
+ parameters: params,
193
+ response: responseInfo,
194
+ operationId: apiDef.operationId || null,
195
+ });
196
+ }
197
+ }
198
+
199
+ // 收集被引用 definitions 的传递闭包
200
+ const collectedRefs = new Set(autoReferencedDefs);
201
+ const queue = [...autoReferencedDefs];
202
+ while (queue.length > 0) {
203
+ const name = queue.shift();
204
+ const def = definitions[name];
205
+ if (!def || typeof def !== "object" || !def.properties) continue;
206
+ for (const prop of Object.values(def.properties)) {
207
+ const rName = refNameWithItems(prop);
208
+ if (rName && definitions[rName] && !collectedRefs.has(rName)) {
209
+ collectedRefs.add(rName);
210
+ queue.push(rName);
211
+ }
212
+ }
213
+ }
214
+
215
+ // definitions 摘要层
216
+ const definitionsSummary = Object.entries(definitions).map(([name, def]) => ({
217
+ name,
218
+ type: (def && typeof def === "object" && def.type) ? def.type : "object",
219
+ description: (def && typeof def === "object" && (def.description || def.title)) || "",
220
+ }));
221
+
222
+ // definitionsDetail 详情层
223
+ const effectiveRefs = referencedDefs !== null ? referencedDefs : collectedRefs;
224
+ const definitionsDetail = [];
225
+ for (const name of effectiveRefs) {
226
+ const def = definitions[name];
227
+ if (def && typeof def === "object") {
228
+ definitionsDetail.push({
229
+ name,
230
+ type: def.type || "object",
231
+ description: def.description || def.title || "",
232
+ properties: expandProperties(def.properties || {}, definitions, 0, MAX_DEPTH),
233
+ });
234
+ }
235
+ }
236
+
237
+ return {
238
+ info: info ? { title: info.title, version: info.version, description: info.description } : null,
239
+ basePath: apiBasePath,
240
+ tags: parsedTags,
241
+ apis: parsedApis,
242
+ definitions: definitionsSummary,
243
+ definitionsDetail,
244
+ };
245
+ }
246
+
247
+ // ==================== 过滤(对齐 filterApis + trim_definitions_detail) ====================
248
+
249
+ function filterApis(apis, keywords) {
250
+ if (!keywords || keywords.length === 0 || !apis || apis.length === 0) return apis;
251
+ const lower = keywords.map((k) => k.toLowerCase());
252
+ return apis.filter((api) => {
253
+ const dtoRefs = (api.parameters || [])
254
+ .map((p) => p.dtoRef || "")
255
+ .filter(Boolean)
256
+ .join(" ");
257
+ const responseVo = (api.response && api.response.voName) || "";
258
+ const searchable = [
259
+ api.url || "",
260
+ api.summary || "",
261
+ api.operationId || "",
262
+ api.tag || "",
263
+ api.description || "",
264
+ dtoRefs,
265
+ responseVo,
266
+ ].join(" ").toLowerCase();
267
+ return lower.some((kw) => searchable.includes(kw));
268
+ });
269
+ }
270
+
271
+ function trimDefinitionsDetail(result, keywords) {
272
+ const apis = result.apis || [];
273
+ const matchedRefs = new Set();
274
+ for (const api of apis) {
275
+ for (const p of api.parameters || []) {
276
+ if (p.dtoRef) matchedRefs.add(p.dtoRef);
277
+ }
278
+ if (api.response && api.response.voName) matchedRefs.add(api.response.voName);
279
+ }
280
+
281
+ const detailIndex = new Map();
282
+ for (const d of result.definitionsDetail || []) detailIndex.set(d.name, d);
283
+
284
+ const detailNames = new Set(matchedRefs);
285
+ const queue = [...matchedRefs];
286
+ while (queue.length > 0) {
287
+ const name = queue.shift();
288
+ const detail = detailIndex.get(name);
289
+ if (!detail || !detail.properties) continue;
290
+ for (const prop of detail.properties) {
291
+ const ref = prop.refName;
292
+ if (ref && !detailNames.has(ref)) {
293
+ detailNames.add(ref);
294
+ queue.push(ref);
295
+ }
296
+ }
297
+ }
298
+
299
+ result.definitionsDetail = (result.definitionsDetail || []).filter((d) => detailNames.has(d.name));
300
+ result._filter = { keywords, matched: apis.length };
301
+ return result;
302
+ }
303
+
304
+ // ==================== get_definition(单 VO 兜底) ====================
305
+
306
+ function getDefinition(apiDocs, definitionName) {
307
+ const definitions = apiDocs.definitions || {};
308
+ if (definitions[definitionName]) {
309
+ const result = parseApiDocs(apiDocs, new Set([definitionName]));
310
+ const target = result.definitionsDetail.find((d) => d.name === definitionName) || null;
311
+ return { found: true, definition: target };
312
+ }
313
+ const lower = definitionName.toLowerCase();
314
+ const suggestions = Object.keys(definitions).filter((n) => n.toLowerCase().includes(lower)).slice(0, 10);
315
+ return {
316
+ found: false,
317
+ definitionName,
318
+ suggestions,
319
+ message: `未找到 definition '${definitionName}'。` + (
320
+ suggestions.length > 0 ? `相近建议:${suggestions.join(", ")}` : "无相近匹配。"
321
+ ),
322
+ };
323
+ }
324
+
325
+ // ==================== JSON 解析 + 容错 ====================
326
+
327
+ function fixOctalLiterals(text) {
328
+ return text.replace(OCTAL_LITERAL_RE, (_, digits) => digits);
329
+ }
330
+
331
+ function diagnoseJsonError(text, err) {
332
+ // 从错误消息中提取 position
333
+ let pos = -1;
334
+ const m = /position\s+(\d+)/i.exec(err.message);
335
+ if (m) pos = parseInt(m[1], 10);
336
+ if (pos < 0) {
337
+ // 兜底:用 line/column(部分实现)
338
+ const lm = /line\s+(\d+)\s+column\s+(\d+)/i.exec(err.message);
339
+ if (lm) {
340
+ const line = parseInt(lm[1], 10), col = parseInt(lm[2], 10);
341
+ const lines = text.split("\n");
342
+ pos = 0;
343
+ for (let i = 0; i < line - 1 && i < lines.length; i++) pos += lines[i].length + 1;
344
+ pos += col - 1;
345
+ }
346
+ }
347
+ if (pos < 0) pos = 0;
348
+
349
+ const start = Math.max(0, pos - 80);
350
+ const end = Math.min(text.length, pos + 80);
351
+ const snippet = text.slice(start, end).replace(/\n/g, "\\n");
352
+ const marker = " ".repeat(pos - start) + "^";
353
+
354
+ // 反推 line/col 给提示
355
+ const before = text.slice(0, pos);
356
+ const beforeLines = before.split("\n");
357
+ const lineno = beforeLines.length;
358
+ const colno = beforeLines[beforeLines.length - 1].length + 1;
359
+
360
+ const hints = [];
361
+ const head = text.slice(0, 500).trimStart();
362
+ if (head.startsWith("<")) {
363
+ hints.push("响应以 '<' 开头,可能是 HTML 错误页/登录跳转页,而非 JSON。检查 cookie/auth 是否过期");
364
+ }
365
+ if (OCTAL_LITERAL_RE.test(text)) {
366
+ OCTAL_LITERAL_RE.lastIndex = 0; // 重置 stateful regex
367
+ hints.push("检测到八进制字面量(如 [011,015]),脚本应已自动修复——若仍报错请提交 issue");
368
+ }
369
+ OCTAL_LITERAL_RE.lastIndex = 0;
370
+ const trimmedEnd = text.trimEnd();
371
+ if (text.length < 1024 && !trimmedEnd.endsWith("}") && !trimmedEnd.endsWith("]")) {
372
+ hints.push("响应可能被截断(curl 管道/终端缓冲),建议改用 --file 模式直接读本地文件");
373
+ }
374
+
375
+ let msg = `JSON 解析失败 at line ${lineno} col ${colno}: ${err.message}\n`;
376
+ msg += ` 上下文: ...${snippet}...\n`;
377
+ msg += ` ${" ".repeat(11)}${marker}\n`;
378
+ if (hints.length > 0) {
379
+ msg += " 可能原因:\n" + hints.map((h) => ` - ${h}`).join("\n");
380
+ }
381
+ return msg;
382
+ }
383
+
384
+ function loadJsonText(text) {
385
+ try {
386
+ return JSON.parse(text);
387
+ } catch (e) {
388
+ // Fallback: 八进制修复
389
+ OCTAL_LITERAL_RE.lastIndex = 0;
390
+ if (OCTAL_LITERAL_RE.test(text)) {
391
+ OCTAL_LITERAL_RE.lastIndex = 0;
392
+ const fixed = fixOctalLiterals(text);
393
+ if (fixed !== text) {
394
+ try {
395
+ const parsed = JSON.parse(fixed);
396
+ process.stderr.write("[INFO] 自动修复八进制字面量后解析成功\n");
397
+ return parsed;
398
+ } catch (e2) {
399
+ throw new Error(diagnoseJsonError(fixed, e2));
400
+ }
401
+ }
402
+ }
403
+ throw new Error(diagnoseJsonError(text, e));
404
+ }
405
+ }
406
+
407
+ // ==================== 下载(fetch 优先,降级 curl) ====================
408
+
409
+ async function fetchViaFetch(url, { cookie, auth, timeout } = {}) {
410
+ const controller = new AbortController();
411
+ const tid = setTimeout(() => controller.abort(), timeout * 1000);
412
+ try {
413
+ const headers = { Accept: "application/json" };
414
+ if (cookie) headers["Cookie"] = cookie;
415
+ if (auth) headers["Authorization"] = `Basic ${auth}`;
416
+ const res = await fetch(url, { headers, signal: controller.signal });
417
+ if (!res.ok) {
418
+ const text = await res.text().catch(() => "");
419
+ throw new Error(`HTTP ${res.status} ${res.statusText}${text ? ": " + text.slice(0, 200) : ""}`);
420
+ }
421
+ return await res.text();
422
+ } finally {
423
+ clearTimeout(tid);
424
+ }
425
+ }
426
+
427
+ function fetchViaCurl(url, { cookie, auth, timeout } = {}) {
428
+ const args = ["-sS", "--connect-timeout", "10", "--max-time", String(timeout), "-H", "Accept: application/json"];
429
+ if (cookie) args.push("-H", `Cookie: ${cookie}`);
430
+ if (auth) args.push("-H", `Authorization: Basic ${auth}`);
431
+ args.push(url);
432
+ const r = spawnSync("curl", args, { encoding: "utf-8" });
433
+ if (r.error) {
434
+ if (r.error.code === "ENOENT") throw new Error("找不到 curl 命令;且 Node 内置 fetch 也不可用(需 Node ≥ 18)");
435
+ throw new Error(`curl spawn 失败: ${r.error.message}`);
436
+ }
437
+ if (r.status !== 0) {
438
+ throw new Error(`curl 失败 (exit ${r.status}): ${(r.stderr || "").trim() || "无错误输出"}`);
439
+ }
440
+ if (!r.stdout || !r.stdout.trim()) throw new Error("curl 返回空响应");
441
+ return r.stdout;
442
+ }
443
+
444
+ async function fetchRaw(url, opts) {
445
+ if (typeof fetch === "function") {
446
+ try {
447
+ return await fetchViaFetch(url, opts);
448
+ } catch (e) {
449
+ // 网络错误/超时直接抛;HTTP 错误也直接抛(不应回退到 curl 掩盖问题)
450
+ throw new Error(`fetch 失败: ${e.message}`);
451
+ }
452
+ }
453
+ return fetchViaCurl(url, opts);
454
+ }
455
+
456
+ function cacheKey(url, cookie, auth) {
457
+ const h = crypto.createHash("sha256");
458
+ h.update(url, "utf-8");
459
+ if (cookie) h.update(Buffer.from("\x00" + cookie, "utf-8"));
460
+ if (auth) h.update(Buffer.from("\x00" + auth, "utf-8"));
461
+ return h.digest("hex").slice(0, 16);
462
+ }
463
+
464
+ async function fetchWithCache(url, { cookie, auth, timeout, ttl, noCache } = {}) {
465
+ if (noCache || ttl <= 0) {
466
+ return fetchRaw(url, { cookie, auth, timeout });
467
+ }
468
+ const key = cacheKey(url, cookie, auth);
469
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
470
+ const cacheFile = path.join(CACHE_DIR, `${key}.json`);
471
+ const metaFile = path.join(CACHE_DIR, `${key}.meta.json`);
472
+
473
+ if (fs.existsSync(cacheFile) && fs.existsSync(metaFile)) {
474
+ try {
475
+ const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
476
+ const age = Date.now() / 1000 - (meta.cached_at || 0);
477
+ if (age < ttl) {
478
+ process.stderr.write(`[CACHE] 命中 ${key} (age=${Math.floor(age)}s, ttl=${ttl}s)\n`);
479
+ return fs.readFileSync(cacheFile, "utf-8");
480
+ }
481
+ } catch { /* 缓存损坏,重新拉 */ }
482
+ }
483
+
484
+ const raw = await fetchRaw(url, { cookie, auth, timeout });
485
+ try {
486
+ fs.writeFileSync(cacheFile, raw, "utf-8");
487
+ fs.writeFileSync(metaFile, JSON.stringify({
488
+ url, cached_at: Date.now() / 1000, ttl, size: raw.length,
489
+ }), "utf-8");
490
+ process.stderr.write(`[CACHE] 已写入 ${key} (${raw.length} bytes)\n`);
491
+ } catch (e) {
492
+ process.stderr.write(`[WARN] 缓存写入失败: ${e.message}\n`);
493
+ }
494
+ return raw;
495
+ }
496
+
497
+ // ==================== CLI 参数 ====================
498
+
499
+ function parseArgs(argv) {
500
+ const opts = {
501
+ url: null, file: null, cookie: null, auth: null,
502
+ timeout: 30, cacheTtl: DEFAULT_CACHE_TTL, noCache: false,
503
+ filter: [], definition: null, out: null, indent: 2,
504
+ help: false,
505
+ };
506
+ for (let i = 0; i < argv.length; i++) {
507
+ const a = argv[i];
508
+ const next = () => argv[++i];
509
+ switch (a) {
510
+ case "--url": opts.url = next(); break;
511
+ case "--file": opts.file = next(); break;
512
+ case "--cookie": opts.cookie = next(); break;
513
+ case "--auth": opts.auth = next(); break;
514
+ case "--timeout": opts.timeout = parseInt(next(), 10); break;
515
+ case "--cache-ttl": opts.cacheTtl = parseInt(next(), 10); break;
516
+ case "--no-cache": opts.noCache = true; break;
517
+ case "--filter": opts.filter.push(next()); break;
518
+ case "--definition": opts.definition = next(); break;
519
+ case "--out": opts.out = next(); break;
520
+ case "--indent": opts.indent = parseInt(next(), 10); break;
521
+ case "-h": case "--help": opts.help = true; break;
522
+ default:
523
+ process.stderr.write(`[ERROR] 未知参数: ${a}\n`);
524
+ process.exit(1);
525
+ }
526
+ }
527
+ return opts;
528
+ }
529
+
530
+ function printHelp() {
531
+ process.stdout.write(`extract-swagger.mjs — Swagger 文档提取(Node ≥18)
532
+
533
+ 必选(互斥):
534
+ --url <api-docs URL> 从 URL 下载(内置 TTL 缓存)
535
+ --file <path> 读本地 JSON 文件
536
+
537
+ 可选:
538
+ --cookie '<JSESSIONID=xxx>' 鉴权 Cookie
539
+ --auth <base64(user:pass)> Basic 鉴权
540
+ --timeout <秒> 网络超时,默认 30
541
+ --cache-ttl <秒> 缓存 TTL,默认 ${DEFAULT_CACHE_TTL}(10 分钟),0 关闭
542
+ --no-cache 强制重新下载
543
+ --filter <关键词> 过滤 api(可多次,匹配 url/summary/operationId/tag/description/dtoRef/voName)
544
+ --definition <名称> 仅查询单个 VO/DTO
545
+ --out <path> 输出到文件(默认 stdout)
546
+ --indent <数字> JSON 缩进,默认 2,0 = 紧凑
547
+
548
+ 退出码:0 成功;1 参数错误;2 网络/IO;3 解析
549
+ `);
550
+ }
551
+
552
+ // ==================== 主入口 ====================
553
+
554
+ async function main() {
555
+ const opts = parseArgs(process.argv.slice(2));
556
+ if (opts.help) { printHelp(); process.exit(0); }
557
+ if (!opts.url && !opts.file) {
558
+ process.stderr.write("[ERROR] 必须指定 --url 或 --file\n");
559
+ process.exit(1);
560
+ }
561
+ if (opts.url && opts.file) {
562
+ process.stderr.write("[ERROR] --url 与 --file 互斥\n");
563
+ process.exit(1);
564
+ }
565
+
566
+ // 1. 读 raw
567
+ let rawText;
568
+ try {
569
+ if (opts.url) {
570
+ rawText = await fetchWithCache(opts.url, {
571
+ cookie: opts.cookie, auth: opts.auth, timeout: opts.timeout,
572
+ ttl: opts.cacheTtl, noCache: opts.noCache,
573
+ });
574
+ } else {
575
+ rawText = fs.readFileSync(opts.file, "utf-8");
576
+ }
577
+ } catch (e) {
578
+ process.stderr.write(`[ERROR] 读取 Swagger 失败: ${e.message}\n`);
579
+ process.exit(2);
580
+ }
581
+
582
+ // 2. 解析 JSON
583
+ let apiDocs;
584
+ try {
585
+ apiDocs = loadJsonText(rawText);
586
+ } catch (e) {
587
+ process.stderr.write(`[ERROR] ${e.message}\n`);
588
+ process.exit(3);
589
+ }
590
+
591
+ // 3. 分支
592
+ let result;
593
+ try {
594
+ if (opts.definition) {
595
+ result = getDefinition(apiDocs, opts.definition);
596
+ } else {
597
+ result = parseApiDocs(apiDocs);
598
+ if (opts.filter.length > 0) {
599
+ const total = result.apis.length;
600
+ result.apis = filterApis(result.apis, opts.filter);
601
+ result = trimDefinitionsDetail(result, opts.filter);
602
+ result._filter.total = total;
603
+ }
604
+ }
605
+ } catch (e) {
606
+ process.stderr.write(`[ERROR] 解析失败: ${e.message}\n${e.stack || ""}\n`);
607
+ process.exit(3);
608
+ }
609
+
610
+ // 4. 输出
611
+ const indent = opts.indent > 0 ? opts.indent : 0;
612
+ const outText = indent > 0 ? JSON.stringify(result, null, indent) : JSON.stringify(result);
613
+ if (opts.out) {
614
+ fs.writeFileSync(opts.out, outText, "utf-8");
615
+ process.stderr.write(`[OK] 已输出到 ${opts.out} (${outText.length} bytes)\n`);
616
+ } else {
617
+ process.stdout.write(outText);
618
+ process.stdout.write("\n");
619
+ }
620
+ }
621
+
622
+ main().catch((e) => {
623
+ process.stderr.write(`[FATAL] ${e.message}\n${e.stack || ""}\n`);
624
+ process.exit(2);
625
+ });