pull-apifox 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # pull-apifox
2
+
3
+ 从 [Apifox](https://apifox.com) 拉取接口,按需筛选,自动生成带完整 JSDoc 的 JS 请求函数。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g pull-apifox
9
+ ```
10
+
11
+ ## 快速开始
12
+
13
+ ### 1. 在项目根目录创建 `apifox.config.json`
14
+
15
+ ```json
16
+ {
17
+ "projectId": "1577893",
18
+ "accessToken": "aps-xxxxxxxxxx",
19
+ "output": "./src/apifox",
20
+ "showReturns": true,
21
+ "importPath": "@/utils/request",
22
+
23
+ "pull": [
24
+ { "tags": ["移库单管理"], "name": "womeiTransfer", "prefix": "/chainova/womei" }
25
+ ]
26
+ }
27
+ ```
28
+
29
+ ### 2. 运行
30
+
31
+ ```bash
32
+ pull-apifox
33
+ # 或指定配置文件
34
+ pull-apifox ./path/to/apifox.config.json
35
+ ```
36
+
37
+ ## 配置项
38
+
39
+ | 字段 | 类型 | 默认值 | 说明 |
40
+ |------|------|--------|------|
41
+ | `projectId` | string | **必填** | Apifox 项目 ID |
42
+ | `accessToken` | string | — | Apifox 令牌,填了走云端 API |
43
+ | `localPort` | number | `4523` | 本地客户端端口 |
44
+ | `output` | string | `./src/apifox` | 输出目录 |
45
+ | `showReturns` | boolean | `false` | 是否生成返回值 JSDoc |
46
+ | `importPath` | string | `@/utils/request` | request 导入路径 |
47
+ | `pull` | array | **必填** | 拉取任务列表 |
48
+
49
+ ### pull 配置
50
+
51
+ 每项生成一个 JS 文件,三个筛选维度**取交集**:
52
+
53
+ ```json
54
+ {
55
+ "tags": ["移库单管理"], // Apifox 文件夹名,支持层级模糊匹配
56
+ "folder": "/movement-orders", // URL 路径前缀匹配
57
+ "apis": ["login", "order"], // 接口名关键词
58
+ "name": "womeiTransfer", // 生成文件名
59
+ "prefix": "/chainova/womei" // 请求路径前缀
60
+ }
61
+ ```
62
+
63
+ > `tags` 对应 Apifox 文件夹层级路径(如 `沃美/移库单管理`),配 `"移库单管理"` 即可匹配。
64
+
65
+ ## 获取 Token
66
+
67
+ Apifox 网页 → 头像 → 账号设置 → 访问令牌 → 新建令牌。
68
+
69
+ ## 生成结果
70
+
71
+ ```
72
+ src/apifox/
73
+ ├── index.js # 入口文件,聚合导出
74
+ └── api/
75
+ └── womeiTransfer.js # 接口函数,带 JSDoc
76
+ ```
77
+
78
+ ### 生成代码示例
79
+
80
+ ```js
81
+ import request from '@/utils/request';
82
+
83
+ /**
84
+ * 新建移库单
85
+ *
86
+ * POST /movement-orders/create
87
+ * @param {object} data - 请求体
88
+ * @param {string} data.ownerCorpCode - 货主企业编码
89
+ * @param {string} data.warehouseCode - 仓库编码
90
+ * @param {string} [data.remark] - 备注
91
+ * @param {object[]} data.items - 移库单项列表
92
+ * @param {string} data.items[].skuCode - SKU编码
93
+ * @param {number} data.items[].quantity - 数量
94
+ * @returns {Promise<object>} - 响应数据
95
+ * @param {number} returns.code - 状态码
96
+ * @param {string} returns.msg - 消息
97
+ */
98
+ export function createMovementOrder(data) {
99
+ return request({
100
+ url: '/chainova/womei/movement-orders/create',
101
+ method: 'POST',
102
+ data,
103
+ });
104
+ }
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../lib');
4
+ main().catch(err => {
5
+ console.error('\n❌', err.message);
6
+ process.exit(1);
7
+ });
package/lib/index.js ADDED
@@ -0,0 +1,478 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+ const http = require('http');
7
+
8
+ // ======================== HTTP 请求 ========================
9
+
10
+ function request(url, options = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ const isHttps = url.startsWith('https://');
13
+ const lib = isHttps ? https : http;
14
+ const urlObj = new URL(url);
15
+
16
+ const req = lib.request({
17
+ hostname: urlObj.hostname,
18
+ port: urlObj.port || (isHttps ? 443 : 80),
19
+ path: urlObj.pathname + urlObj.search,
20
+ method: options.method || 'GET',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ 'User-Agent': 'pull-apifox/1.0',
24
+ ...options.headers,
25
+ },
26
+ rejectUnauthorized: false,
27
+ }, (res) => {
28
+ let body = '';
29
+ res.on('data', (c) => { body += c; });
30
+ res.on('end', () => {
31
+ if (res.statusCode >= 200 && res.statusCode < 300) {
32
+ try { resolve(JSON.parse(body)); } catch { resolve(body); }
33
+ } else {
34
+ let msg = `HTTP ${res.statusCode}`;
35
+ try {
36
+ const errBody = JSON.parse(body);
37
+ msg = errBody.message || errBody.msg || JSON.stringify(errBody);
38
+ } catch {}
39
+ reject(new Error(msg));
40
+ }
41
+ });
42
+ });
43
+ req.on('error', reject);
44
+ req.setTimeout(30000, () => { req.destroy(); reject(new Error('请求超时')); });
45
+ if (options.body) req.write(JSON.stringify(options.body));
46
+ req.end();
47
+ });
48
+ }
49
+
50
+ // ======================== 拉取 OpenAPI 规范 ========================
51
+
52
+ async function fetchSpec(projectId, scope, config) {
53
+ const token = config.accessToken;
54
+
55
+ if (token) {
56
+ const url = `https://api.apifox.com/api/v1/projects/${projectId}/export-openapi`;
57
+ console.log(` 📡 云端 API: ${projectId}`);
58
+ const body = scope
59
+ ? { exportFormat: 'OpenAPI3.0', scope }
60
+ : { exportFormat: 'OpenAPI3.0' };
61
+
62
+ return request(url, {
63
+ method: 'POST',
64
+ body,
65
+ headers: {
66
+ 'Authorization': `Bearer ${token}`,
67
+ 'X-Apifox-Api-Version': '2024-03-28',
68
+ },
69
+ });
70
+ } else {
71
+ const port = config.localPort || 4523;
72
+ const url = `http://127.0.0.1:${port}/export/openapi/${projectId}?version=3.0`;
73
+ console.log(` 📡 本地客户端: ${url}`);
74
+
75
+ try {
76
+ return await request(url, {
77
+ method: 'POST',
78
+ body: scope ? { exportFormat: 'OpenAPI3.0', scope } : { exportFormat: 'OpenAPI3.0' },
79
+ });
80
+ } catch {
81
+ return request(url);
82
+ }
83
+ }
84
+ }
85
+
86
+ // ======================== 筛选引擎 ========================
87
+
88
+ function matchApiName(operation, route, keyword) {
89
+ const kw = keyword.toLowerCase();
90
+
91
+ if (operation.operationId && operation.operationId.toLowerCase().includes(kw)) return true;
92
+ if (operation.summary && operation.summary.toLowerCase().includes(kw)) return true;
93
+
94
+ const lastSegment = route.split('/').pop().toLowerCase();
95
+ if (lastSegment.includes(kw)) return true;
96
+ if (route.toLowerCase().includes(kw)) return true;
97
+
98
+ return false;
99
+ }
100
+
101
+ function matchTags(operation, tags) {
102
+ if (!tags || tags.length === 0) return true;
103
+ const opTags = (operation.tags || []).map(t => t.toLowerCase());
104
+ return tags.some(t => {
105
+ const kw = t.toLowerCase();
106
+ return opTags.some(ot => ot === kw || ot.endsWith('/' + kw) || ot.includes('/' + kw + '/'));
107
+ });
108
+ }
109
+
110
+ function matchFolder(route, folder) {
111
+ if (!folder) return true;
112
+ const normalizedRoute = route.startsWith('/') ? route : `/${route}`;
113
+ const normalizedFolder = folder.startsWith('/') ? folder : `/${folder}`;
114
+ return normalizedRoute === normalizedFolder || normalizedRoute.startsWith(normalizedFolder + '/');
115
+ }
116
+
117
+ function matchAll(route, methodObj, operation, filters) {
118
+ if (filters.folder && !matchFolder(route, filters.folder)) return false;
119
+ if (filters.tags && filters.tags.length > 0 && !matchTags(operation, filters.tags)) return false;
120
+ if (filters.apis && filters.apis.length > 0) {
121
+ if (!filters.apis.some(kw => matchApiName(operation, route, kw))) return false;
122
+ }
123
+ return true;
124
+ }
125
+
126
+ function filterSpec(spec, filters) {
127
+ const result = JSON.parse(JSON.stringify(spec));
128
+ const allCount = Object.keys(result.paths || {}).length;
129
+
130
+ for (const [route, methods] of Object.entries(result.paths || {})) {
131
+ if (!methods || typeof methods !== 'object') continue;
132
+ let keepRoute = false;
133
+
134
+ for (const [method, operation] of Object.entries(methods)) {
135
+ if (!operation || typeof operation !== 'object') continue;
136
+ if (matchAll(route, method, operation, filters)) {
137
+ keepRoute = true;
138
+ } else {
139
+ delete methods[method];
140
+ }
141
+ }
142
+
143
+ if (!keepRoute) {
144
+ delete result.paths[route];
145
+ } else {
146
+ const remaining = Object.values(methods).filter(v => v && typeof v === 'object');
147
+ if (remaining.length === 0) delete result.paths[route];
148
+ }
149
+ }
150
+
151
+ const keptCount = Object.keys(result.paths).length;
152
+ console.log(` 🔍 ${allCount} → ${keptCount} 个接口`);
153
+ return result;
154
+ }
155
+
156
+ // ======================== JS 代码生成 ========================
157
+
158
+ function isAscii(str) {
159
+ if (!str) return false;
160
+ return /^[\x00-\x7F]+$/.test(str);
161
+ }
162
+
163
+ function deriveModuleName(apis) {
164
+ for (const api of apis) {
165
+ const seg = api.route.split('/').filter(Boolean)[0];
166
+ if (seg && /^[a-zA-Z]/.test(seg)) return seg;
167
+ }
168
+ return 'api';
169
+ }
170
+
171
+ function toCamel(str) {
172
+ return str
173
+ .replace(/[^a-zA-Z0-9_]/g, '_')
174
+ .replace(/_+(.)/g, (_, c) => c.toUpperCase())
175
+ .replace(/^[A-Z]/, c => c.toLowerCase());
176
+ }
177
+
178
+ function toFuncName(operation, method, route) {
179
+ if (operation.operationId) return toCamel(operation.operationId);
180
+ if (isAscii(operation.summary)) return toCamel(operation.summary);
181
+ return toCamel(`${method}_${route.replace(/[\/{}]/g, '_')}`);
182
+ }
183
+
184
+ function resolveRef(schema, fullSpec) {
185
+ if (!schema) return schema;
186
+ if (schema.$ref) {
187
+ const p = schema.$ref.replace('#/', '').split('/');
188
+ let obj = fullSpec;
189
+ for (const seg of p) obj = obj?.[seg];
190
+ return obj || schema;
191
+ }
192
+ return schema;
193
+ }
194
+
195
+ function toJSDocType(schema, fullSpec) {
196
+ const resolved = resolveRef(schema, fullSpec);
197
+ if (!resolved || !resolved.type) return 'any';
198
+ switch (resolved.type) {
199
+ case 'string': return 'string';
200
+ case 'integer':
201
+ case 'number': return 'number';
202
+ case 'boolean': return 'boolean';
203
+ case 'array': {
204
+ const itemType = toJSDocType(resolved.items, fullSpec);
205
+ return itemType === 'any' ? 'any[]' : `${itemType}[]`;
206
+ }
207
+ case 'object': return 'object';
208
+ default: return 'any';
209
+ }
210
+ }
211
+
212
+ function buildJSDoc(operation, method, route, funcName, fullSpec, showReturns) {
213
+ const lines = [];
214
+ const allParams = operation.parameters || [];
215
+ const queryParamDefs = allParams.filter(p => p.in === 'query');
216
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(method) && !!operation.requestBody;
217
+ const rawBodySchema = hasBody
218
+ ? (operation.requestBody.content?.['application/json']?.schema
219
+ || Object.values(operation.requestBody.content || {})[0]?.schema)
220
+ : null;
221
+ const bodySchema = resolveRef(rawBodySchema, fullSpec);
222
+
223
+ const rawRespSchema = showReturns
224
+ ? (operation.responses?.['200']?.content?.['application/json']?.schema
225
+ || operation.responses?.['201']?.content?.['application/json']?.schema
226
+ || Object.values(operation.responses || {}).find(r => r.content)?.['content']?.['application/json']?.schema)
227
+ : null;
228
+ const respSchema = resolveRef(rawRespSchema, fullSpec);
229
+
230
+ lines.push('/**');
231
+
232
+ const title = operation.summary || operation.description || funcName;
233
+ lines.push(` * ${title}`);
234
+ if (operation.summary && operation.description && operation.description !== operation.summary) {
235
+ lines.push(' *');
236
+ lines.push(` * ${operation.description}`);
237
+ }
238
+ lines.push(' *');
239
+ lines.push(` * ${method.toUpperCase()} ${route}`);
240
+
241
+ const pathParamDefs = allParams.filter(p => p.in === 'path');
242
+ for (const p of pathParamDefs) {
243
+ const type = toJSDocType(p.schema, fullSpec);
244
+ const desc = p.description || '';
245
+ lines.push(` * @param {${type}} ${p.name}${desc ? ` - ${desc}` : ''}`);
246
+ }
247
+
248
+ if (queryParamDefs.length > 0) {
249
+ const hasRequired = queryParamDefs.some(p => p.required === true);
250
+ lines.push(` * @param {object} ${hasRequired ? '' : '['}params${hasRequired ? '' : ']'} - 查询参数`);
251
+ for (const p of queryParamDefs) {
252
+ const type = toJSDocType(p.schema, fullSpec);
253
+ const desc = p.description || '';
254
+ const req = p.required === true;
255
+ lines.push(` * @param {${type}} ${req ? '' : '['}params.${p.name}${req ? '' : ']'}${desc ? ` - ${desc}` : ''}`);
256
+ }
257
+ }
258
+
259
+ if (hasBody) {
260
+ const bodyRequired = operation.requestBody.required !== false;
261
+ const bodyDesc = operation.requestBody.description || '请求体';
262
+ const type = bodySchema ? toJSDocType(bodySchema, fullSpec) : 'object';
263
+ lines.push(` * @param {${type}} ${bodyRequired ? '' : '['}data${bodyRequired ? '' : ']'} - ${bodyDesc}`);
264
+ expandProperties(bodySchema, 'data', bodySchema?.required || [], fullSpec, lines, 0);
265
+ }
266
+
267
+ if (showReturns && respSchema) {
268
+ const respType = toJSDocType(respSchema, fullSpec);
269
+ lines.push(` * @returns {Promise<${respType}>} - 响应数据`);
270
+ expandProperties(respSchema, 'returns', respSchema?.required || [], fullSpec, lines, 0);
271
+ } else {
272
+ lines.push(' * @returns {Promise}');
273
+ }
274
+ lines.push(' */');
275
+
276
+ return lines.join('\n');
277
+ }
278
+
279
+ function expandProperties(schema, prefix, requiredList, fullSpec, lines, depth) {
280
+ if (depth > 3) return;
281
+ const resolved = resolveRef(schema, fullSpec);
282
+ if (resolved?.type !== 'object' || !resolved.properties) return;
283
+
284
+ const reqs = resolved.required || requiredList || [];
285
+ for (const [prop, propSchema] of Object.entries(resolved.properties)) {
286
+ const resolvedProp = resolveRef(propSchema, fullSpec);
287
+ const propType = toJSDocType(resolvedProp, fullSpec);
288
+ const propDesc = resolvedProp.description || '';
289
+ const req = reqs.includes(prop);
290
+ const fullPath = prefix ? `${prefix}.${prop}` : prop;
291
+ lines.push(` * @param {${propType}} ${req ? '' : '['}${fullPath}${req ? '' : ']'}${propDesc ? ` - ${propDesc}` : ''}`);
292
+
293
+ if (resolvedProp?.type === 'object' && resolvedProp.properties) {
294
+ expandProperties(resolvedProp, fullPath, resolvedProp.required || [], fullSpec, lines, depth + 1);
295
+ } else if (resolvedProp?.type === 'array' && resolvedProp.items) {
296
+ const resolvedItems = resolveRef(resolvedProp.items, fullSpec);
297
+ if (resolvedItems?.type === 'object' && resolvedItems.properties) {
298
+ expandProperties(resolvedItems, fullPath + '[]', resolvedItems.required || [], fullSpec, lines, depth + 1);
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ function generateJS(spec, outputDir, groupName, prefix, showReturns, importPath) {
305
+ const apiDir = path.join(outputDir, 'api');
306
+ fs.mkdirSync(apiDir, { recursive: true });
307
+
308
+ const paths = spec.paths || {};
309
+ const modules = {};
310
+ const pathPrefix = prefix || '';
311
+ const reqImport = importPath || '@/utils/request';
312
+
313
+ for (const [route, methods] of Object.entries(paths)) {
314
+ for (const [method, operation] of Object.entries(methods)) {
315
+ if (!operation || typeof operation !== 'object') continue;
316
+ let tags = operation.tags?.length ? operation.tags : null;
317
+ if (!tags) {
318
+ const seg = route.split('/').filter(Boolean)[0] || 'default';
319
+ tags = [seg];
320
+ }
321
+ for (const tag of tags) {
322
+ if (!modules[tag]) modules[tag] = [];
323
+ modules[tag].push({ route, method: method.toUpperCase(), operation });
324
+ }
325
+ }
326
+ }
327
+
328
+ const fileNames = [];
329
+
330
+ for (const [tag, apis] of Object.entries(modules)) {
331
+ let fileName;
332
+ if (groupName) {
333
+ fileName = groupName;
334
+ } else if (isAscii(tag) && tag !== 'default') {
335
+ fileName = tag.replace(/[^a-zA-Z0-9]/g, '_');
336
+ } else {
337
+ fileName = deriveModuleName(apis);
338
+ }
339
+ fileNames.push(fileName);
340
+
341
+ const lines = [
342
+ '/**',
343
+ ` * ${tag} 模块`,
344
+ ' * 由 pull-apifox 自动生成,请勿手动修改',
345
+ ' */',
346
+ '',
347
+ `import request from '${reqImport}';`,
348
+ '',
349
+ ];
350
+
351
+ for (const { route, method, operation } of apis) {
352
+ const funcName = toFuncName(operation, method, route);
353
+
354
+ const pathParams = (operation.parameters || []).filter(p => p.in === 'path').map(p => p.name);
355
+ const queryParams = (operation.parameters || []).filter(p => p.in === 'query').map(p => p.name);
356
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(method) && operation.requestBody;
357
+
358
+ const paramParts = [];
359
+ if (pathParams.length) paramParts.push(pathParams.join(', '));
360
+ if (queryParams.length) paramParts.push('params');
361
+ if (hasBody) paramParts.push('data');
362
+
363
+ lines.push(buildJSDoc(operation, method, route, funcName, spec, showReturns));
364
+ lines.push(`export function ${funcName}(${paramParts.join(', ')}) {`);
365
+
366
+ const fullPath = pathPrefix + route;
367
+ const urlExpr = pathParams.length
368
+ ? '`' + fullPath.replace(/\{(\w+)\}/g, '${$1}') + '`'
369
+ : `'${fullPath}'`;
370
+
371
+ lines.push(' return request({');
372
+ lines.push(` url: ${urlExpr},`);
373
+ lines.push(` method: '${method}',`);
374
+ if (queryParams.length) lines.push(' params,');
375
+ if (hasBody) {
376
+ const ct = Object.keys(operation.requestBody?.content || { 'application/json': {} })[0];
377
+ lines.push(' data,');
378
+ if (ct !== 'application/json') lines.push(` headers: { 'Content-Type': '${ct}' },`);
379
+ }
380
+ lines.push(' });');
381
+ lines.push('}\n');
382
+ }
383
+
384
+ fs.writeFileSync(path.join(apiDir, `${fileName}.js`), lines.join('\n'), 'utf-8');
385
+ console.log(` ✅ ${fileName}.js (${apis.length} 个接口)`);
386
+ }
387
+
388
+ if (fileNames.length > 0) {
389
+ const indexLines = fileNames
390
+ .map(name => `export * from './api/${name}';`)
391
+ .join('\n');
392
+ fs.writeFileSync(path.join(outputDir, 'index.js'), indexLines + '\n', 'utf-8');
393
+ } else {
394
+ console.log(' ⚠ 没有匹配到接口,请检查筛选条件(folder/tags/apis)是否正确');
395
+ }
396
+ }
397
+
398
+ // ======================== 主流程 ========================
399
+
400
+ async function main() {
401
+ const configPath = process.argv[2] || path.join(process.cwd(), 'apifox.config.json');
402
+
403
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
404
+ console.log('pull-apifox — 从 Apifox 拉取接口并生成 JS 代码');
405
+ console.log('');
406
+ console.log('用法:');
407
+ console.log(' pull-apifox 使用当前目录下的 apifox.config.json');
408
+ console.log(' pull-apifox ./my-config.json 指定配置文件');
409
+ console.log('');
410
+ console.log('配置文档: https://github.com/xxx/pull-apifox');
411
+ process.exit(0);
412
+ }
413
+
414
+ if (!fs.existsSync(configPath)) {
415
+ console.error(`❌ 配置文件不存在: ${configPath}`);
416
+ console.error(' 请在项目根目录创建 apifox.config.json,或指定路径: pull-apifox ./my-config.json');
417
+ process.exit(1);
418
+ }
419
+
420
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
421
+
422
+ if (!config.projectId) {
423
+ console.error('❌ 配置文件缺少 projectId');
424
+ process.exit(1);
425
+ }
426
+
427
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
428
+ console.log(' pull-apifox — Apifox 接口拉取 + JS 代码生成');
429
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
430
+ console.log(` 项目 ID: ${config.projectId}`);
431
+ console.log(` 拉取组数: ${config.pull.length}`);
432
+ console.log('');
433
+
434
+ console.log('📥 拉取完整 OpenAPI 规范...');
435
+ const fullSpec = await fetchSpec(config.projectId, null, config);
436
+ const totalPaths = Object.keys(fullSpec.paths || {}).length;
437
+ console.log(` 📊 项目共有 ${totalPaths} 个接口\n`);
438
+
439
+ const allTags = new Set();
440
+ for (const methods of Object.values(fullSpec.paths || {})) {
441
+ for (const op of Object.values(methods)) {
442
+ (op.tags || []).forEach(t => allTags.add(t));
443
+ }
444
+ }
445
+
446
+ const baseOutput = path.resolve(config.output || './src/apifox');
447
+
448
+ for (let i = 0; i < config.pull.length; i++) {
449
+ const group = config.pull[i];
450
+ const filterLabel = group.folder || group.tags?.join(', ') || '全部接口';
451
+ console.log(`📦 [${i + 1}/${config.pull.length}] ${filterLabel}`);
452
+
453
+ const filtered = filterSpec(fullSpec, group);
454
+
455
+ if (Object.keys(filtered.paths || {}).length === 0) {
456
+ const kw = group.folder || group.apis?.[0] || group.tags?.[0] || '';
457
+ if (kw) {
458
+ const matchedPaths = Object.keys(fullSpec.paths || {}).filter(p => p.toLowerCase().includes(kw.toLowerCase()));
459
+ if (matchedPaths.length > 0) {
460
+ console.log(` 💡 路径中包含「${kw}」的接口: ${matchedPaths.slice(0, 10).join(', ')}${matchedPaths.length > 10 ? '...' : ''}`);
461
+ }
462
+ const matchedTags = [...allTags].filter(t => t.toLowerCase().includes(kw.toLowerCase()));
463
+ if (matchedTags.length > 0) {
464
+ console.log(` 💡 标签中包含「${kw}」的: ${matchedTags.join(', ')}`);
465
+ }
466
+ }
467
+ }
468
+
469
+ generateJS(filtered, baseOutput, group.name, group.prefix, config.showReturns, config.importPath);
470
+ console.log('');
471
+ }
472
+
473
+ console.log('✅ 全部完成!');
474
+ }
475
+
476
+ // ======================== 导出 ========================
477
+
478
+ module.exports = { main, fetchSpec, filterSpec, generateJS };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pull-apifox",
3
+ "version": "1.0.0",
4
+ "description": "从 Apifox 拉取接口并生成带 JSDoc 的 JS 请求代码",
5
+ "keywords": ["apifox", "openapi", "codegen", "api", "jsdoc"],
6
+ "license": "MIT",
7
+ "author": "fanwb1212",
8
+ "main": "lib/index.js",
9
+ "bin": {
10
+ "pull-apifox": "bin/cli.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": ""
19
+ },
20
+ "engines": {
21
+ "node": ">=14"
22
+ }
23
+ }