hookehuyr

feat(claude): 添加API生成器技能并更新Claude配置

添加完整的API生成器技能,包括从OpenAPI文档生成前端API代码的工具链
更新Claude配置以支持pnpm包管理器和MCP服务器
重构权限设置,优化开发工具链集成
1 -# API Diff Skill
2 -
3 -对比两个版本的 OpenAPI 文档或生成的 API 文件,检测接口变更。
4 -
5 -## 使用场景
6 -
7 -1. **更新 API 后自动检查**:运行 `api:generate` 后自动对比新旧接口
8 -2. **手动对比**:对比两个不同的 OpenAPI 文档
9 -3. **CI/CD 集成**:在部署前检查破坏性 API 变更
10 -
11 -## 如何调用
12 -
13 -### 在生成 API 后自动对比
14 -```bash
15 -pnpm run api:generate
16 -```
17 -生成器会自动调用对比逻辑,检查是否有接口变更。
18 -
19 -### 手动对比两个文档
20 -```bash
21 -# 对比两个 OpenAPI markdown 文档
22 -node scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md
23 -
24 -# 对比整个模块目录
25 -node scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/
26 -
27 -# 对比生成的 API 文件
28 -node scripts/apiDiff.js src/api/user.js src/api/user-new.js
29 -```
30 -
31 -## 对比维度
32 -
33 -1. **接口增删**:新增或删除的接口
34 -2. **参数变更**
35 - - 新增必填参数(破坏性变更)
36 - - 删除参数(破坏性变更)
37 - - 参数类型变更(破坏性变更)
38 - - 新增可选参数(非破坏性)
39 -3. **返回值变更**
40 - - 返回结构变更
41 - - 字段类型变更
42 -4. **HTTP 方法变更**:GET ↔ POST(破坏性变更)
43 -
44 -## 输出格式
45 -
46 -对比结果会以以下格式输出:
47 -
48 -```
49 -=== API 变更检测报告 ===
50 -
51 -📦 模块: user
52 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53 -
54 -✅ 新增接口 (1):
55 - + getUserProfile
56 -
57 -⚠️ 修改接口 (2):
58 - ↪ editUserInfo
59 - ✗ [破坏性] 删除必填参数: sms_code
60 - ✓ [非破坏性] 新增可选参数: avatar
61 -
62 - ↪ getUserInfo
63 - ✓ [非破坏性] 新增可选参数: include_profile
64 -
65 -❌ 删除接口 (1):
66 - - deleteUserAccount
67 -
68 -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69 -总计: 1 新增, 2 修改, 1 删除
70 -⚠️ 检测到 1 个破坏性变更,请仔细检查业务逻辑!
71 -```
72 -
73 -## 退出码
74 -
75 -- `0`: 无变更或仅有非破坏性变更
76 -- `1`: 检测到破坏性变更(可用于 CI/CD 失败)
77 -
78 -## 配置选项
79 -
80 -可以通过环境变量配置:
81 -
82 -```bash
83 -# 严格模式:任何变更都返回失败码
84 -API_DIFF_STRICT=true node scripts/apiDiff.js ...
85 -
86 -# 输出 JSON 格式(用于程序解析)
87 -API_DIFF_FORMAT=json node scripts/apiDiff.js ...
88 -```
89 -
90 -## 注意事项
91 -
92 -1. 对比逻辑基于 OpenAPI 规范,确保文档格式正确
93 -2. 破坏性变更需要在业务代码中做兼容处理
94 -3. 新增接口通常不需要修改现有代码
95 -4. 删除接口前请确认没有地方在使用
1 +/**
2 + * API 对比工具
3 + *
4 + * 功能:
5 + * 1. 对比两个 OpenAPI 文档的差异
6 + * 2. 检测破坏性变更
7 + * 3. 生成详细的变更报告
8 + *
9 + * 使用方式:
10 + * node .claude/custom_skills/api-generator/scripts/apiDiff.js <oldPath> <newPath>
11 + */
12 +
13 +const fs = require('fs');
14 +const path = require('path');
15 +const yaml = require('js-yaml');
16 +
17 +/**
18 + * 从 Markdown 文件中提取 YAML
19 + */
20 +function extractYAMLFromMarkdown(content) {
21 + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/;
22 + const match = content.match(yamlRegex);
23 + return match ? match[1] : null;
24 +}
25 +
26 +/**
27 + * 解析 OpenAPI 文档(支持 .md 和目录)
28 + */
29 +function parseOpenAPIPath(filePath) {
30 + const stat = fs.statSync(filePath);
31 +
32 + if (stat.isFile()) {
33 + // 单个文件
34 + if (filePath.endsWith('.md')) {
35 + const content = fs.readFileSync(filePath, 'utf8');
36 + const yamlContent = extractYAMLFromMarkdown(content);
37 + if (!yamlContent) {
38 + throw new Error(`文件 ${filePath} 中未找到 YAML 代码块`);
39 + }
40 + return [yaml.load(yamlContent)];
41 + } else if (filePath.endsWith('.js')) {
42 + // TODO: 支持对比生成的 JS 文件(需要解析 AST)
43 + throw new Error('暂不支持对比生成的 JS 文件,请对比 OpenAPI 文档');
44 + } else {
45 + throw new Error(`不支持的文件类型: ${filePath}`);
46 + }
47 + } else if (stat.isDirectory()) {
48 + // 目录,读取所有 .md 文件
49 + const files = fs.readdirSync(filePath).filter(f => f.endsWith('.md'));
50 + const docs = [];
51 + files.forEach(file => {
52 + const fullPath = path.join(filePath, file);
53 + const content = fs.readFileSync(fullPath, 'utf8');
54 + const yamlContent = extractYAMLFromMarkdown(content);
55 + if (yamlContent) {
56 + const doc = yaml.load(yamlContent);
57 + // 保存文件名用于标识
58 + doc._fileName = path.basename(file, '.md');
59 + docs.push(doc);
60 + }
61 + });
62 + return docs;
63 + }
64 +}
65 +
66 +/**
67 + * 从 OpenAPI 文档提取 API 信息
68 + */
69 +function extractAPIInfo(openapiDoc) {
70 + const path = Object.keys(openapiDoc.paths)[0];
71 + const method = Object.keys(openapiDoc.paths[path])[0];
72 + const apiInfo = openapiDoc.paths[path][method];
73 +
74 + // 提取参数
75 + const queryParams = (apiInfo.parameters || [])
76 + .filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f')
77 + .map(p => ({
78 + name: p.name,
79 + type: p.schema?.type || 'any',
80 + required: p.required || false,
81 + description: p.description || '',
82 + }));
83 +
84 + // 提取 body 参数
85 + const bodyParams = [];
86 + if (apiInfo.requestBody && apiInfo.requestBody.content) {
87 + const content = apiInfo.requestBody.content['application/x-www-form-urlencoded'] ||
88 + apiInfo.requestBody.content['application/json'];
89 + if (content && content.schema && content.schema.properties) {
90 + Object.entries(content.schema.properties).forEach(([key, value]) => {
91 + if (key !== 'a' && key !== 'f') {
92 + bodyParams.push({
93 + name: key,
94 + type: value.type || 'any',
95 + required: content.schema.required?.includes(key) || false,
96 + description: value.description || '',
97 + });
98 + }
99 + });
100 + }
101 + }
102 +
103 + // 提取响应结构
104 + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;
105 +
106 + return {
107 + name: openapiDoc._fileName || 'unknown',
108 + path,
109 + method: method.toUpperCase(),
110 + queryParams: new Set(queryParams.map(p => p.name)),
111 + bodyParams: new Set(bodyParams.map(p => p.name)),
112 + requiredQueryParams: new Set(queryParams.filter(p => p.required).map(p => p.name)),
113 + requiredBodyParams: new Set(bodyParams.filter(p => p.required).map(p => p.name)),
114 + allQueryParams: queryParams,
115 + allBodyParams: bodyParams,
116 + responseSchema,
117 + summary: apiInfo.summary || '',
118 + };
119 +}
120 +
121 +/**
122 + * 对比两个 API 信息
123 + */
124 +function compareAPI(oldAPI, newAPI) {
125 + const changes = {
126 + breaking: [],
127 + nonBreaking: [],
128 + };
129 +
130 + // 检查 HTTP 方法变更
131 + if (oldAPI.method !== newAPI.method) {
132 + changes.breaking.push(`HTTP 方法变更: ${oldAPI.method} → ${newAPI.method}`);
133 + }
134 +
135 + // 检查 GET 参数变更
136 + oldAPI.allQueryParams.forEach(oldParam => {
137 + const newParam = newAPI.allQueryParams.find(p => p.name === oldParam.name);
138 +
139 + if (!newParam) {
140 + // 参数被删除
141 + if (oldAPI.requiredQueryParams.has(oldParam.name)) {
142 + changes.breaking.push(`删除必填 query 参数: ${oldParam.name}`);
143 + } else {
144 + changes.nonBreaking.push(`删除可选 query 参数: ${oldParam.name}`);
145 + }
146 + } else {
147 + // 参数类型变更
148 + if (oldParam.type !== newParam.type) {
149 + changes.breaking.push(`query 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})`);
150 + }
151 + // 可选 → 必填
152 + if (!oldParam.required && newParam.required) {
153 + changes.breaking.push(`query 参数变为必填: ${newParam.name}`);
154 + }
155 + // 必填 → 可选
156 + if (oldParam.required && !newParam.required) {
157 + changes.nonBreaking.push(`query 参数变为可选: ${newParam.name}`);
158 + }
159 + }
160 + });
161 +
162 + // 检查新增 GET 参数
163 + newAPI.allQueryParams.forEach(newParam => {
164 + const oldParam = oldAPI.allQueryParams.find(p => p.name === newParam.name);
165 + if (!oldParam) {
166 + if (newParam.required) {
167 + changes.breaking.push(`新增必填 query 参数: ${newParam.name}`);
168 + } else {
169 + changes.nonBreaking.push(`新增可选 query 参数: ${newParam.name}`);
170 + }
171 + }
172 + });
173 +
174 + // 检查 POST body 参数变更
175 + oldAPI.allBodyParams.forEach(oldParam => {
176 + const newParam = newAPI.allBodyParams.find(p => p.name === oldParam.name);
177 +
178 + if (!newParam) {
179 + // 参数被删除
180 + if (oldAPI.requiredBodyParams.has(oldParam.name)) {
181 + changes.breaking.push(`删除必填 body 参数: ${oldParam.name}`);
182 + } else {
183 + changes.nonBreaking.push(`删除可选 body 参数: ${oldParam.name}`);
184 + }
185 + } else {
186 + // 参数类型变更
187 + if (oldParam.type !== newParam.type) {
188 + changes.breaking.push(`body 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})`);
189 + }
190 + // 可选 → 必填
191 + if (!oldParam.required && newParam.required) {
192 + changes.breaking.push(`body 参数变为必填: ${newParam.name}`);
193 + }
194 + // 必填 → 可选
195 + if (oldParam.required && !newParam.required) {
196 + changes.nonBreaking.push(`body 参数变为可选: ${newParam.name}`);
197 + }
198 + }
199 + });
200 +
201 + // 检查新增 body 参数
202 + newAPI.allBodyParams.forEach(newParam => {
203 + const oldParam = oldAPI.allBodyParams.find(p => p.name === newParam.name);
204 + if (!oldParam) {
205 + if (newParam.required) {
206 + changes.breaking.push(`新增必填 body 参数: ${newParam.name}`);
207 + } else {
208 + changes.nonBreaking.push(`新增可选 body 参数: ${newParam.name}`);
209 + }
210 + }
211 + });
212 +
213 + return changes;
214 +}
215 +
216 +/**
217 + * 生成变更报告
218 + */
219 +function generateReport(oldDocs, newDocs, format = 'text') {
220 + const oldAPIs = oldDocs.map(extractAPIInfo);
221 + const newAPIs = newDocs.map(extractAPIInfo);
222 +
223 + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]));
224 + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]));
225 +
226 + const addedAPIs = [];
227 + const removedAPIs = [];
228 + const modifiedAPIs = [];
229 +
230 + // 检测新增接口
231 + newAPIs.forEach(api => {
232 + if (!oldAPIsMap.has(api.name)) {
233 + addedAPIs.push(api);
234 + }
235 + });
236 +
237 + // 检测删除接口
238 + oldAPIs.forEach(api => {
239 + if (!newAPIsMap.has(api.name)) {
240 + removedAPIs.push(api);
241 + }
242 + });
243 +
244 + // 检测修改接口
245 + newAPIs.forEach(api => {
246 + const oldAPI = oldAPIsMap.get(api.name);
247 + if (oldAPI) {
248 + const changes = compareAPI(oldAPI, api);
249 + if (changes.breaking.length > 0 || changes.nonBreaking.length > 0) {
250 + modifiedAPIs.push({
251 + name: api.name,
252 + summary: api.summary,
253 + changes,
254 + });
255 + }
256 + }
257 + });
258 +
259 + // 统计
260 + const totalBreaking = modifiedAPIs.reduce(
261 + (sum, api) => sum + api.changes.breaking.length,
262 + 0
263 + );
264 +
265 + // 生成文本报告
266 + if (format === 'text') {
267 + const lines = [];
268 + lines.push('=== API 变更检测报告 ===\n');
269 + lines.push(`📦 对比范围: ${oldAPIs.length} 个旧接口 → ${newAPIs.length} 个新接口`);
270 + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
271 +
272 + if (addedAPIs.length > 0) {
273 + lines.push(`✅ 新增接口 (${addedAPIs.length}):`);
274 + addedAPIs.forEach(api => {
275 + lines.push(` + ${api.name} - ${api.summary}`);
276 + });
277 + lines.push('');
278 + }
279 +
280 + if (modifiedAPIs.length > 0) {
281 + lines.push(`⚠️ 修改接口 (${modifiedAPIs.length}):`);
282 + modifiedAPIs.forEach(api => {
283 + lines.push(` ↪ ${api.name} - ${api.summary}`);
284 + api.changes.breaking.forEach(change => {
285 + lines.push(` ✗ [破坏性] ${change}`);
286 + });
287 + api.changes.nonBreaking.forEach(change => {
288 + lines.push(` ✓ [非破坏性] ${change}`);
289 + });
290 + });
291 + lines.push('');
292 + }
293 +
294 + if (removedAPIs.length > 0) {
295 + lines.push(`❌ 删除接口 (${removedAPIs.length}):`);
296 + removedAPIs.forEach(api => {
297 + lines.push(` - ${api.name} - ${api.summary}`);
298 + });
299 + lines.push('');
300 + }
301 +
302 + lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
303 + lines.push(`总计: ${addedAPIs.length} 新增, ${modifiedAPIs.length} 修改, ${removedAPIs.length} 删除`);
304 +
305 + if (totalBreaking > 0) {
306 + lines.push(`⚠️ 检测到 ${totalBreaking} 个破坏性变更,请仔细检查业务逻辑!`);
307 + } else if (addedAPIs.length > 0 || modifiedAPIs.length > 0 || removedAPIs.length > 0) {
308 + lines.push('✅ 未检测到破坏性变更');
309 + } else {
310 + lines.push('✅ 无接口变更');
311 + }
312 +
313 + return lines.join('\n');
314 + }
315 +
316 + // 生成 JSON 报告
317 + if (format === 'json') {
318 + return JSON.stringify({
319 + summary: {
320 + added: addedAPIs.length,
321 + modified: modifiedAPIs.length,
322 + removed: removedAPIs.length,
323 + breakingChanges: totalBreaking,
324 + },
325 + added: addedAPIs.map(api => ({
326 + name: api.name,
327 + summary: api.summary,
328 + method: api.method,
329 + path: api.path,
330 + })),
331 + modified: modifiedAPIs.map(api => ({
332 + name: api.name,
333 + summary: api.summary,
334 + breakingChanges: api.changes.breaking,
335 + nonBreakingChanges: api.changes.nonBreaking,
336 + })),
337 + removed: removedAPIs.map(api => ({
338 + name: api.name,
339 + summary: api.summary,
340 + method: api.method,
341 + path: api.path,
342 + })),
343 + }, null, 2);
344 + }
345 +}
346 +
347 +/**
348 + * 主函数
349 + */
350 +function main() {
351 + const args = process.argv.slice(2);
352 +
353 + if (args.length < 2) {
354 + console.error('用法: node .claude/custom_skills/api-generator/scripts/apiDiff.js <oldPath> <newPath>');
355 + console.error('示例:');
356 + console.error(' node .claude/custom_skills/api-generator/scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/');
357 + console.error(' node .claude/custom_skills/api-generator/scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md');
358 + process.exit(1);
359 + }
360 +
361 + const [oldPath, newPath] = args;
362 +
363 + if (!fs.existsSync(oldPath)) {
364 + console.error(`❌ 旧路径不存在: ${oldPath}`);
365 + process.exit(1);
366 + }
367 +
368 + if (!fs.existsSync(newPath)) {
369 + console.error(`❌ 新路径不存在: ${newPath}`);
370 + process.exit(1);
371 + }
372 +
373 + try {
374 + const oldDocs = parseOpenAPIPath(oldPath);
375 + const newDocs = parseOpenAPIPath(newPath);
376 +
377 + const format = process.env.API_DIFF_FORMAT || 'text';
378 + const report = generateReport(oldDocs, newDocs, format);
379 +
380 + console.log(report);
381 +
382 + // 如果有破坏性变更,返回退出码 1
383 + const oldAPIs = oldDocs.map(extractAPIInfo);
384 + const newAPIs = newDocs.map(extractAPIInfo);
385 + const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]));
386 + const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]));
387 +
388 + let totalBreaking = 0;
389 + newAPIs.forEach(api => {
390 + const oldAPI = oldAPIsMap.get(api.name);
391 + if (oldAPI) {
392 + const changes = compareAPI(oldAPI, api);
393 + totalBreaking += changes.breaking.length;
394 + }
395 + });
396 +
397 + // 严格模式:任何变更都返回 1
398 + const strictMode = process.env.API_DIFF_STRICT === 'true';
399 + const hasChanges = oldAPIs.length !== newAPIs.length ||
400 + newAPIs.some(api => !oldAPIsMap.has(api.name)) ||
401 + oldAPIs.some(api => !newAPIsMap.has(api.name));
402 +
403 + if (totalBreaking > 0 || (strictMode && hasChanges)) {
404 + process.exit(1);
405 + } else {
406 + process.exit(0);
407 + }
408 + } catch (error) {
409 + console.error(`❌ 对比失败: ${error.message}`);
410 + process.exit(1);
411 + }
412 +}
413 +
414 +// 如果直接运行此脚本
415 +if (require.main === module) {
416 + main();
417 +}
418 +
419 +module.exports = {
420 + compareAPI,
421 + generateReport,
422 + parseOpenAPIPath,
423 + extractAPIInfo,
424 +};
1 +/**
2 + * 从 OpenAPI 文档自动生成 API 接口文件
3 + *
4 + * 功能:
5 + * 1. 扫描 docs/api-specs 目录
6 + * 2. 解析每个 .md 文件中的 OpenAPI YAML 规范
7 + * 3. 提取 API 信息并生成对应的 JavaScript API 文件
8 + * 4. 保存到 src/api/ 目录
9 + *
10 + * 目录结构:
11 + * docs/api-specs/
12 + * ├── module1/
13 + * │ ├── api1.md
14 + * │ └── api2.md
15 + * └── module2/
16 + * └── api3.md
17 + *
18 + * 生成到:
19 + * src/api/
20 + * ├── module1.js
21 + * └── module2.js
22 + */
23 +
24 +const fs = require('fs');
25 +const path = require('path');
26 +const yaml = require('js-yaml');
27 +const { generateReport, parseOpenAPIPath } = require('./apiDiff.cjs');
28 +
29 +/**
30 + * 提取 Markdown 文件中的 YAML 代码块
31 + * @param {string} content - Markdown 文件内容
32 + * @returns {string|null} - YAML 字符串或 null
33 + */
34 +function extractYAMLFromMarkdown(content) {
35 + const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/;
36 + const match = content.match(yamlRegex);
37 + return match ? match[1] : null;
38 +}
39 +
40 +/**
41 + * 将字符串转换为驼峰命名
42 + * @param {string} str - 输入字符串
43 + * @returns {string} - 驼峰命名字符串
44 + */
45 +function toCamelCase(str) {
46 + return str
47 + .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
48 + .replace(/^(.)/, (c) => c.toLowerCase());
49 +}
50 +
51 +/**
52 + * 将字符串转换为帕斯卡命名(首字母大写)
53 + * @param {string} str - 输入字符串
54 + * @returns {string} - 帕斯卡命名字符串
55 + */
56 +function toPascalCase(str) {
57 + const camelCase = toCamelCase(str);
58 + return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
59 +}
60 +
61 +/**
62 + * 解析对象属性,生成字段描述
63 + * @param {object} properties - 属性对象
64 + * @param {number} indent - 缩进级别
65 + * @returns {string} - 字段描述字符串
66 + */
67 +function parseProperties(properties, indent = 0) {
68 + if (!properties) return '';
69 +
70 + const lines = [];
71 + const prefix = ' '.repeat(indent);
72 +
73 + Object.entries(properties).forEach(([key, value]) => {
74 + const type = value.type || 'any';
75 + const desc = value.description || value.title || '';
76 + const required = value.required ? '' : ' (可选)';
77 +
78 + // 基本类型
79 + if (type !== 'object' && type !== 'array') {
80 + lines.push(`${prefix}${key}: ${type}${required} - ${desc}`);
81 + }
82 + // 对象类型
83 + else if (type === 'object' && value.properties) {
84 + lines.push(`${prefix}${key}: {`);
85 + lines.push(`${prefix} // ${desc}`);
86 + lines.push(parseProperties(value.properties, indent + 2));
87 + lines.push(prefix + '}');
88 + }
89 + // 数组类型
90 + else if (type === 'array' && value.items) {
91 + const itemType = value.items.type || 'any';
92 + if (itemType === 'object' && value.items.properties) {
93 + lines.push(`${prefix}${key}: Array<{`);
94 + lines.push(`${prefix} // ${desc}`);
95 + lines.push(parseProperties(value.items.properties, indent + 2));
96 + lines.push(prefix + '}>');
97 + } else {
98 + lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`);
99 + }
100 + }
101 + });
102 +
103 + return lines.join('\n');
104 +}
105 +
106 +/**
107 + * 从 requestBody 中提取参数
108 + * @param {object} requestBody - requestBody 对象
109 + * @returns {Array} - 参数数组
110 + */
111 +function extractRequestParams(requestBody) {
112 + if (!requestBody || !requestBody.content) {
113 + return [];
114 + }
115 +
116 + // 获取内容类型(可能是 application/x-www-form-urlencoded 或 application/json)
117 + const content = requestBody.content['application/x-www-form-urlencoded'] ||
118 + requestBody.content['application/json'];
119 +
120 + if (!content || !content.schema || !content.schema.properties) {
121 + return [];
122 + }
123 +
124 + const params = [];
125 + Object.entries(content.schema.properties).forEach(([key, value]) => {
126 + params.push({
127 + name: key,
128 + type: value.type || 'any',
129 + description: value.description || '',
130 + example: value.example || '',
131 + required: content.schema.required?.includes(key) || false,
132 + });
133 + });
134 +
135 + return params;
136 +}
137 +
138 +/**
139 + * 生成 JSDoc 参数注释
140 + * @param {Array} parameters - parameters 数组(GET 请求)
141 + * @param {Array} bodyParams - requestBody 参数数组(POST 请求)
142 + * @param {string} method - HTTP 方法
143 + * @returns {string} - JSDoc 参数注释
144 + */
145 +function generateParamJSDoc(parameters, bodyParams, method) {
146 + const lines = [' * @param {Object} params 请求参数'];
147 +
148 + // POST 请求使用 body 参数
149 + if (method === 'POST' && bodyParams && bodyParams.length > 0) {
150 + // 过滤掉 a 和 f 参数
151 + const filteredParams = bodyParams.filter(p => p.name !== 'a' && p.name !== 'f');
152 +
153 + filteredParams.forEach((param) => {
154 + const type = param.type || 'any';
155 + const desc = param.description || '';
156 + const required = param.required ? '' : ' (可选)';
157 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
158 + });
159 + }
160 + // GET 请求使用 query 参数
161 + else if (method === 'GET' && parameters && parameters.length > 0) {
162 + // 只保留 query 参数,过滤 header 参数
163 + const queryParams = parameters.filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f');
164 +
165 + queryParams.forEach((param) => {
166 + const type = param.schema?.type || 'any';
167 + const desc = param.description || '';
168 + const required = param.required ? '' : ' (可选)';
169 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
170 + });
171 + }
172 +
173 + return lines.join('\n');
174 +}
175 +
176 +/**
177 + * 生成 JSDoc 返回值注释
178 + * @param {object} responseSchema - 响应 schema
179 + * @returns {string} - JSDoc 返回值注释
180 + */
181 +function generateReturnJSDoc(responseSchema) {
182 + if (!responseSchema || !responseSchema.properties) {
183 + return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回';
184 + }
185 +
186 + const { code, msg, data } = responseSchema.properties;
187 +
188 + let returnDesc = ' * @returns {Promise<{\n';
189 + returnDesc += ' * code: number; // 状态码\n';
190 + returnDesc += ' * msg: string; // 消息\n';
191 +
192 + if (data && data.properties) {
193 + returnDesc += ' * data: {\n';
194 +
195 + Object.entries(data.properties).forEach(([key, value]) => {
196 + const type = value.type || 'any';
197 + const desc = value.description || value.title || '';
198 +
199 + if (type === 'object' && value.properties) {
200 + returnDesc += ` * ${key}: {\n`;
201 + Object.entries(value.properties).forEach(([subKey, subValue]) => {
202 + const subType = subValue.type || 'any';
203 + const subDesc = subValue.description || subValue.title || '';
204 + returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
205 + });
206 + returnDesc += ` * };\n`;
207 + } else if (type === 'array' && value.items && value.items.properties) {
208 + returnDesc += ` * ${key}: Array<{\n`;
209 + Object.entries(value.items.properties).forEach(([subKey, subValue]) => {
210 + const subType = subValue.type || 'any';
211 + const subDesc = subValue.description || subValue.title || '';
212 + returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
213 + });
214 + returnDesc += ` * }>;\n`;
215 + } else {
216 + returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
217 + }
218 + });
219 +
220 + returnDesc += ' * };\n';
221 + } else {
222 + returnDesc += ' * data: any;\n';
223 + }
224 +
225 + returnDesc += ' * }>}';
226 +
227 + return returnDesc;
228 +}
229 +
230 +/**
231 + * 解析 OpenAPI 文档并提取 API 信息
232 + * @param {object} openapiDoc - 解析后的 OpenAPI 对象
233 + * @param {string} fileName - 文件名(用作 API 名称)
234 + * @returns {object} - 提取的 API 信息
235 + */
236 +function parseOpenAPIDocument(openapiDoc, fileName) {
237 + try {
238 + const path = Object.keys(openapiDoc.paths)[0];
239 + const method = Object.keys(openapiDoc.paths[path])[0];
240 + const apiInfo = openapiDoc.paths[path][method];
241 +
242 + // 提取 query 参数
243 + const parameters = apiInfo.parameters || [];
244 + const queryParams = {};
245 + let actionValue = '';
246 +
247 + // 提取 body 参数(用于 POST 请求)
248 + const requestBody = apiInfo.requestBody;
249 + const bodyParams = extractRequestParams(requestBody);
250 +
251 + // 对于 POST 请求,从 requestBody 中提取 action
252 + if (requestBody && bodyParams.length > 0) {
253 + const actionParam = bodyParams.find(p => p.name === 'a');
254 + if (actionParam) {
255 + actionValue = actionParam.example || '';
256 + }
257 + }
258 +
259 + // 对于 GET 请求,从 query 参数中提取 action
260 + if (!actionValue && parameters.length > 0) {
261 + parameters.forEach((param) => {
262 + if (param.in === 'query') {
263 + queryParams[param.name] = param.example || param.schema?.default || '';
264 +
265 + // 提取 action 参数(通常是 'a' 参数)
266 + if (param.name === 'a') {
267 + actionValue = param.example || '';
268 + }
269 + }
270 + });
271 + }
272 +
273 + // 提取响应结构
274 + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;
275 +
276 + return {
277 + summary: apiInfo.summary || fileName,
278 + description: apiInfo.description || '',
279 + method: method.toUpperCase(),
280 + action: actionValue,
281 + queryParams,
282 + parameters, // 保存完整的参数信息用于生成 JSDoc(GET 请求)
283 + bodyParams, // 保存 requestBody 参数用于生成 JSDoc(POST 请求)
284 + responseSchema, // 保存响应结构用于生成 JSDoc
285 + fileName,
286 + };
287 + } catch (error) {
288 + console.error(`解析 OpenAPI 文档失败: ${error.message}`);
289 + return null;
290 + }
291 +}
292 +
293 +/**
294 + * 生成 API 文件内容
295 + * @param {string} moduleName - 模块名称
296 + * @param {Array} apis - API 信息数组
297 + * @returns {string} - 生成的文件内容
298 + */
299 +function generateApiFileContent(moduleName, apis) {
300 + const imports = `import { fn, fetch } from '@/api/fn';\n\n`;
301 + const apiConstants = [];
302 + const apiFunctions = [];
303 +
304 + apis.forEach((api) => {
305 + // 生成常量名(帕斯卡命名)
306 + const constantName = toPascalCase(api.fileName);
307 + // 生成函数名(驼峰命名 + API 后缀)
308 + const functionName = toCamelCase(api.fileName) + 'API';
309 +
310 + // 添加常量定义
311 + apiConstants.push(
312 + ` ${constantName}: '/srv/?a=${api.action}',`
313 + );
314 +
315 + // 生成详细的 JSDoc 注释
316 + const paramJSDoc = generateParamJSDoc(api.parameters, api.bodyParams, api.method);
317 + const returnJSDoc = generateReturnJSDoc(api.responseSchema);
318 +
319 + // 添加函数定义
320 + const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post';
321 + const comment = `/**
322 + * @description: ${api.summary}
323 +${paramJSDoc}
324 +${returnJSDoc}
325 + */`;
326 +
327 + apiFunctions.push(`${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));`);
328 + });
329 +
330 + return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n`;
331 +}
332 +
333 +/**
334 + * 扫描目录并处理所有 OpenAPI 文档
335 + * @param {string} openAPIDir - OpenAPI 文档目录
336 + * @param {string} outputDir - 输出目录
337 + */
338 +function scanAndGenerate(openAPIDir, outputDir) {
339 + if (!fs.existsSync(openAPIDir)) {
340 + console.error(`OpenAPI 目录不存在: ${openAPIDir}`);
341 + return;
342 + }
343 +
344 + // 确保输出目录存在
345 + if (!fs.existsSync(outputDir)) {
346 + fs.mkdirSync(outputDir, { recursive: true });
347 + }
348 +
349 + // 扫描第一级目录(模块)
350 + const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
351 + .filter(dirent => dirent.isDirectory())
352 + .map(dirent => dirent.name);
353 +
354 + console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`);
355 +
356 + modules.forEach((moduleName) => {
357 + const moduleDir = path.join(openAPIDir, moduleName);
358 + const apiFiles = fs.readdirSync(moduleDir)
359 + .filter(file => file.endsWith('.md'));
360 +
361 + if (apiFiles.length === 0) {
362 + console.log(`模块 ${moduleName} 中没有找到 .md 文件`);
363 + return;
364 + }
365 +
366 + console.log(`\n处理模块: ${moduleName}`);
367 + console.log(`找到 ${apiFiles.length} 个 API 文档`);
368 +
369 + const apis = [];
370 +
371 + apiFiles.forEach((fileName) => {
372 + const filePath = path.join(moduleDir, fileName);
373 + const content = fs.readFileSync(filePath, 'utf8');
374 + const yamlContent = extractYAMLFromMarkdown(content);
375 +
376 + if (!yamlContent) {
377 + console.warn(` ⚠️ ${fileName}: 未找到 YAML 代码块`);
378 + return;
379 + }
380 +
381 + try {
382 + const openapiDoc = yaml.load(yamlContent);
383 + const apiName = path.basename(fileName, '.md');
384 + const apiInfo = parseOpenAPIDocument(openapiDoc, apiName);
385 +
386 + if (apiInfo) {
387 + apis.push(apiInfo);
388 + console.log(` ✓ ${apiName}: ${apiInfo.summary}`);
389 + }
390 + } catch (error) {
391 + console.error(` ✗ ${fileName}: 解析失败 - ${error.message}`);
392 + }
393 + });
394 +
395 + // 生成并保存 API 文件
396 + if (apis.length > 0) {
397 + const fileContent = generateApiFileContent(moduleName, apis);
398 + const outputPath = path.join(outputDir, `${moduleName}.js`);
399 + fs.writeFileSync(outputPath, fileContent, 'utf8');
400 + console.log(` 📝 生成文件: ${outputPath}`);
401 + }
402 + });
403 +
404 + console.log('\n✅ API 文档生成完成!');
405 +
406 + // 对比新旧 API
407 + console.log('\n🔍 开始检测 API 变更...\n');
408 + compareAPIChanges(openAPIDir);
409 +}
410 +
411 +/**
412 + * 备份 OpenAPI 文档目录
413 + * @param {string} sourceDir - 源目录
414 + * @returns {string} - 备份目录路径
415 + */
416 +function backupOpenAPIDir(sourceDir) {
417 + const backupBaseDir = path.resolve(__dirname, '../.tmp');
418 + const backupDir = path.join(backupBaseDir, 'openAPI-backup');
419 +
420 + // 创建备份目录
421 + if (!fs.existsSync(backupBaseDir)) {
422 + fs.mkdirSync(backupBaseDir, { recursive: true });
423 + }
424 +
425 + // 删除旧备份
426 + if (fs.existsSync(backupDir)) {
427 + fs.rmSync(backupDir, { recursive: true, force: true });
428 + }
429 +
430 + // 复制目录
431 + copyDirectory(sourceDir, backupDir);
432 +
433 + return backupDir;
434 +}
435 +
436 +/**
437 + * 递归复制目录
438 + * @param {string} src - 源路径
439 + * @param {string} dest - 目标路径
440 + */
441 +function copyDirectory(src, dest) {
442 + if (!fs.existsSync(dest)) {
443 + fs.mkdirSync(dest, { recursive: true });
444 + }
445 +
446 + const entries = fs.readdirSync(src, { withFileTypes: true });
447 +
448 + for (const entry of entries) {
449 + const srcPath = path.join(src, entry.name);
450 + const destPath = path.join(dest, entry.name);
451 +
452 + if (entry.isDirectory()) {
453 + copyDirectory(srcPath, destPath);
454 + } else {
455 + fs.copyFileSync(srcPath, destPath);
456 + }
457 + }
458 +}
459 +
460 +/**
461 + * 对比新旧 API 变更
462 + * @param {string} openAPIDir - OpenAPI 文档目录
463 + */
464 +function compareAPIChanges(openAPIDir) {
465 + const backupDir = path.resolve(__dirname, '../.tmp/openAPI-backup');
466 + const tempDir = path.resolve(__dirname, '../.tmp/openAPI-temp');
467 +
468 + // 检查是否存在临时备份(上一次的版本)
469 + if (!fs.existsSync(tempDir)) {
470 + console.log('ℹ️ 首次运行,已建立基线。下次运行将检测 API 变更。');
471 + // 将当前备份移动到临时目录,作为下次对比的基线
472 + if (fs.existsSync(backupDir)) {
473 + fs.renameSync(backupDir, tempDir);
474 + }
475 + return;
476 + }
477 +
478 + // 扫描模块
479 + const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
480 + .filter(dirent => dirent.isDirectory())
481 + .map(dirent => dirent.name);
482 +
483 + let hasChanges = false;
484 + const moduleReports = [];
485 +
486 + modules.forEach((moduleName) => {
487 + const moduleDir = path.join(openAPIDir, moduleName);
488 + const tempModuleDir = path.join(tempDir, moduleName);
489 +
490 + // 如果临时备份中不存在该模块,说明是新增模块
491 + if (!fs.existsSync(tempModuleDir)) {
492 + console.log(`📦 新增模块: ${moduleName}`);
493 + hasChanges = true;
494 + return;
495 + }
496 +
497 + // 读取当前和临时备份的文档
498 + const currentFiles = fs.readdirSync(moduleDir).filter(f => f.endsWith('.md'));
499 + const tempFiles = fs.readdirSync(tempModuleDir).filter(f => f.endsWith('.md'));
500 +
501 + // 检查是否有文件变更
502 + const hasNewFiles = currentFiles.some(f => !tempFiles.includes(f));
503 + const hasRemovedFiles = tempFiles.some(f => !currentFiles.includes(f));
504 + const hasModifiedFiles = currentFiles.some(f => {
505 + if (!tempFiles.includes(f)) return false;
506 + const currentContent = fs.readFileSync(path.join(moduleDir, f), 'utf8');
507 + const tempContent = fs.readFileSync(path.join(tempModuleDir, f), 'utf8');
508 + return currentContent !== tempContent;
509 + });
510 +
511 + if (hasNewFiles || hasRemovedFiles || hasModifiedFiles) {
512 + hasChanges = true;
513 + moduleReports.push({ moduleName, moduleDir, tempModuleDir });
514 + }
515 + });
516 +
517 + // 检查删除的模块
518 + const tempModules = fs.existsSync(tempDir)
519 + ? fs.readdirSync(tempDir, { withFileTypes: true })
520 + .filter(dirent => dirent.isDirectory())
521 + .map(dirent => dirent.name)
522 + : [];
523 +
524 + const deletedModules = tempModules.filter(m => !modules.includes(m));
525 + if (deletedModules.length > 0) {
526 + hasChanges = true;
527 + console.log(`\n❌ 删除模块: ${deletedModules.join(', ')}`);
528 + }
529 +
530 + if (!hasChanges) {
531 + console.log('✅ 未检测到 API 变更');
532 + // 更新基线
533 + if (fs.existsSync(backupDir)) {
534 + fs.rmSync(tempDir, { recursive: true, force: true });
535 + fs.renameSync(backupDir, tempDir);
536 + }
537 + return;
538 + }
539 +
540 + // 逐个模块对比
541 + console.log('');
542 + moduleReports.forEach(({ moduleName, moduleDir, tempModuleDir }) => {
543 + try {
544 + const oldDocs = parseOpenAPIPath(tempModuleDir);
545 + const newDocs = parseOpenAPIPath(moduleDir);
546 + const report = generateReport(oldDocs, newDocs, 'text');
547 +
548 + console.log(report);
549 + console.log('');
550 + } catch (error) {
551 + console.error(`⚠️ 模块 ${moduleName} 对比失败: ${error.message}`);
552 + }
553 + });
554 +
555 + // 更新基线:将当前备份作为下次对比的基准
556 + console.log('📝 更新 API 基线...');
557 + if (fs.existsSync(tempDir)) {
558 + fs.rmSync(tempDir, { recursive: true, force: true });
559 + }
560 + if (fs.existsSync(backupDir)) {
561 + fs.renameSync(backupDir, tempDir);
562 + }
563 +}
564 +
565 +// 执行生成
566 +const openAPIDir = path.resolve(__dirname, '../../../../docs/api-specs');
567 +const outputDir = path.resolve(__dirname, '../../../../src/api');
568 +
569 +console.log('=== OpenAPI 转 API 文档生成器 ===\n');
570 +console.log(`输入目录: ${openAPIDir}`);
571 +console.log(`输出目录: ${outputDir}\n`);
572 +
573 +// 备份当前的 OpenAPI 文档(用于下次对比)
574 +if (fs.existsSync(openAPIDir)) {
575 + console.log('💾 备份当前 OpenAPI 文档...');
576 + backupOpenAPIDir(openAPIDir);
577 + console.log('');
578 +}
579 +
580 +scanAndGenerate(openAPIDir, outputDir);
1 +#!/bin/bash
2 +
3 +# API Generator Skill 安装脚本
4 +# 用于将 API Generator 功能安装到当前项目
5 +
6 +set -e
7 +
8 +echo "🚀 API Generator Skill 安装程序"
9 +echo "================================"
10 +echo ""
11 +
12 +# 颜色定义
13 +GREEN='\033[0;32m'
14 +YELLOW='\033[1;33m'
15 +RED='\033[0;31m'
16 +NC='\033[0m' # No Color
17 +
18 +# 检查是否在项目根目录
19 +if [ ! -f "package.json" ]; then
20 + echo -e "${RED}❌ 错误: 请在项目根目录运行此脚本${NC}"
21 + exit 1
22 +fi
23 +
24 +# 检查 package.json 中是否已有 api:generate 命令
25 +if grep -q '"api:generate"' package.json; then
26 + echo -e "${YELLOW}⚠️ 检测到已存在 api:generate 命令${NC}"
27 + read -p "是否覆盖? (y/N) " -n 1 -r
28 + echo
29 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then
30 + echo "安装已取消"
31 + exit 0
32 + fi
33 +fi
34 +
35 +# 创建目录结构
36 +echo -e "${GREEN}📁 创建目录结构...${NC}"
37 +mkdir -p .claude/custom_skills/api-generator/{scripts,templates,setup}
38 +mkdir -p docs/openAPI
39 +mkdir -p .tmp
40 +
41 +# 检查必要的文件
42 +echo -e "${GREEN}📋 检查文件...${NC}"
43 +
44 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45 +
46 +# 检查脚本文件
47 +if [ ! -f "$SCRIPT_DIR/../scripts/generateApiFromOpenAPI.js" ]; then
48 + echo -e "${RED}❌ 缺少 generateApiFromOpenAPI.js${NC}"
49 + exit 1
50 +fi
51 +
52 +if [ ! -f "$SCRIPT_DIR/../scripts/apiDiff.js" ]; then
53 + echo -e "${RED}❌ 缺少 apiDiff.js${NC}"
54 + exit 1
55 +fi
56 +
57 +# 检查模板文件
58 +if [ ! -f "$SCRIPT_DIR/../templates/openAPI-template.md" ]; then
59 + echo -e "${RED}❌ 缺少 openAPI-template.md${NC}"
60 + exit 1
61 +fi
62 +
63 +# 安装依赖
64 +echo -e "${GREEN}📦 检查依赖...${NC}"
65 +
66 +if ! command -v pnpm &> /dev/null; then
67 + echo -e "${YELLOW}⚠️ pnpm 未安装,尝试使用 npm...${NC}"
68 + PKG_MANAGER="npm"
69 +else
70 + PKG_MANAGER="pnpm"
71 +fi
72 +
73 +# 检查 js-yaml 是否已安装
74 +if ! $PKG_MANAGER list js-yaml &> /dev/null; then
75 + echo -e "${GREEN}📦 安装 js-yaml...${NC}"
76 + $PKG_MANAGER add -D js-yaml
77 +else
78 + echo -e "${GREEN}✅ js-yaml 已安装${NC}"
79 +fi
80 +
81 +# 添加 npm scripts
82 +echo -e "${GREEN}🔧 配置 npm scripts...${NC}"
83 +
84 +# 使用 jq 或临时文件添加 scripts
85 +if command -v jq &> /dev/null; then
86 + jq '.scripts."api:generate" = "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js" |
87 + .scripts."api:diff" = "node .claude/custom_skills/api-generator/scripts/apiDiff.js"' package.json > package.json.tmp
88 + mv package.json.tmp package.json
89 +else
90 + # 使用 sed 添加(更兼容)
91 + if ! grep -q '"api:generate"' package.json; then
92 + # 找到 "scripts" 行并在后面插入
93 + sed -i '' '/"scripts":/a\
94 +\ "api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",\
95 +\ "api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js",
96 +' package.json 2>/dev/null || sed -i '/"scripts":/a\
97 +\ "api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",\
98 +\ "api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js",
99 +' package.json
100 + fi
101 +fi
102 +
103 +# 创建示例文档
104 +echo -e "${GREEN}📝 创建示例文档...${NC}"
105 +mkdir -p docs/openAPI/example
106 +cat > docs/openAPI/example/getExample.md << 'EOF'
107 +# 获取示例数据
108 +
109 +## OpenAPI Specification
110 +
111 +```yaml
112 +openapi: 3.0.1
113 +info:
114 + title: ''
115 + version: 1.0.0
116 +paths:
117 + /srv/:
118 + get:
119 + summary: 获取示例数据
120 + description: 这是一个示例接口,展示如何编写 OpenAPI 文档
121 + tags:
122 + - 示例模块
123 + parameters:
124 + - name: a
125 + in: query
126 + description: action 参数
127 + required: false
128 + example: example_data
129 + schema:
130 + type: string
131 + - name: id
132 + in: query
133 + description: 数据ID
134 + required: true
135 + example: 123
136 + schema:
137 + type: integer
138 + responses:
139 + '200':
140 + description: 成功返回
141 + content:
142 + application/json:
143 + schema:
144 + type: object
145 + properties:
146 + code:
147 + type: integer
148 + description: 0=失败,1=成功
149 + msg:
150 + type: string
151 + description: 错误信息
152 + data:
153 + type: object
154 + properties:
155 + id:
156 + type: integer
157 + description: 数据ID
158 + name:
159 + type: string
160 + description: 名称
161 + created_at:
162 + type: string
163 + description: 创建时间
164 +```
165 +EOF
166 +
167 +# 创建 README
168 +cat > docs/openAPI/README.md << 'EOF'
169 +# OpenAPI 文档目录
170 +
171 +本目录用于存放 OpenAPI 规范的接口文档,这些文档将自动转换为前端 API 调用代码。
172 +
173 +## 目录结构
174 +
175 +```
176 +docs/openAPI/
177 +├── example/ # 示例模块
178 +│ └── getExample.md # 示例接口
179 +├── user/ # 用户模块(你的模块)
180 +├── course/ # 课程模块(你的模块)
181 +└── order/ # 订单模块(你的模块)
182 +```
183 +
184 +## 如何添加新接口
185 +
186 +1. **创建模块目录**(如果不存在)
187 + ```bash
188 + mkdir -p docs/openAPI/yourModule
189 + ```
190 +
191 +2. **创建接口文档**
192 + ```bash
193 + # 使用模板创建
194 + cp .claude/custom_skills/api-generator/templates/openAPI-template.md \
195 + docs/openAPI/yourModule/yourApiName.md
196 + ```
197 +
198 +3. **编辑文档**
199 + - 按照模板填写接口信息
200 + - 遵循 OpenAPI 3.0.1 规范
201 + - 添加详细的参数说明和返回值结构
202 +
203 +4. **生成代码**
204 + ```bash
205 + pnpm api:generate
206 + ```
207 +
208 +5. **使用生成的 API**
209 + ```javascript
210 + import { yourApiNameAPI } from '@/api/yourModule'
211 + ```
212 +
213 +## 命令速查
214 +
215 +```bash
216 +# 生成 API 代码
217 +pnpm api:generate
218 +
219 +# 对比 API 变更
220 +pnpm api:diff docs/openAPI/user/ docs/openAPI/user-new/
221 +
222 +# 查看帮助
223 +cat .claude/custom_skills/api-generator/skill.md
224 +```
225 +
226 +## 注意事项
227 +
228 +- 第一级目录名 = 模块名(会生成 `模块名.js`)
229 +- 第二级文件名 = 接口名(会生成 `接口名API` 函数)
230 +- 所有 `.md` 文件必须包含 YAML 代码块
231 +- 遵循 OpenAPI 3.0.1 规范编写 YAML
232 +
233 +## 参考文档
234 +
235 +详细使用说明请参考:[API Generator Skill 文档](../../.claude/custom_skills/api-generator/skill.md)
236 +EOF
237 +
238 +# 完成
239 +echo ""
240 +echo -e "${GREEN}✅ 安装完成!${NC}"
241 +echo ""
242 +echo "📚 下一步:"
243 +echo " 1. 查看示例文档: cat docs/openAPI/example/getExample.md"
244 +echo " 2. 创建你的第一个接口: cp docs/openAPI/example/getExample.md docs/openAPI/yourModule/yourApi.md"
245 +echo " 3. 生成 API 代码: pnpm api:generate"
246 +echo ""
247 +echo "📖 完整文档: cat .claude/custom_skills/api-generator/skill.md"
248 +echo ""
1 +# API Generator Skill
2 +
3 +自动从 OpenAPI 文档生成前端 API 调用代码的完整解决方案。
4 +
5 +## 功能特性
6 +
7 +### 核心功能
8 +
9 +1. **自动生成 API 代码** - 从 OpenAPI YAML 文档生成标准的 API 调用函数
10 +2. **变更检测** - 自动对比 API 变更,识别破坏性变更
11 +3. **增量更新** - 智能备份和基线管理,只检测实际变更
12 +4. **JSDoc 注释** - 自动生成完整的类型注释和参数说明
13 +
14 +### 生成内容
15 +
16 +- **API 常量** - 帕斯卡命名的端点常量(如 `GET_USER_INFO`
17 +- **API 函数** - 驼峰命名的调用函数(如 `getUserInfoAPI`
18 +- **完整 JSDoc** - 参数类型、返回值结构、嵌套对象说明
19 +
20 +## 快速开始
21 +
22 +### 第一次使用
23 +
24 +1. **创建 OpenAPI 文档**
25 +
26 +```bash
27 +# 在 docs/api-specs/ 创建模块目录
28 +mkdir -p docs/api-specs/user
29 +
30 +# 创建接口文档(使用下方模板)
31 +cp .claude/custom_skills/api-generator/templates/api-specs-template.md docs/api-specs/user/getUserInfo.md
32 +```
33 +
34 +2. **编辑 OpenAPI 文档**
35 +
36 +按照 OpenAPI 3.0.1 规范编辑 YAML 代码块:
37 +
38 +```markdown
39 +# 获取用户信息
40 +
41 +## OpenAPI Specification
42 +
43 +```yaml
44 +openapi: 3.0.1
45 +info:
46 + title: ''
47 + version: 1.0.0
48 +paths:
49 + /srv/:
50 + get:
51 + summary: 获取用户信息
52 + parameters:
53 + - name: a
54 + in: query
55 + example: user_info
56 + responses:
57 + '200':
58 + content:
59 + application/json:
60 + schema:
61 + type: object
62 + properties:
63 + code:
64 + type: integer
65 + data:
66 + type: object
67 +```
68 +
69 +3. **生成 API 代码**
70 +
71 +```bash
72 +# 安装依赖(首次)
73 +pnpm add -D js-yaml
74 +
75 +# 生成 API 代码
76 +pnpm api:generate
77 +```
78 +
79 +4. **使用生成的 API**
80 +
81 +```javascript
82 +import { getUserInfoAPI } from '@/api/user'
83 +
84 +const { code, data } = await getUserInfoAPI({ id: 123 })
85 +if (code === 1) {
86 + console.log(data)
87 +}
88 +```
89 +
90 +## 依赖安装
91 +
92 +### 必需依赖
93 +
94 +```bash
95 +pnpm add -D js-yaml
96 +```
97 +
98 +### package.json 配置
99 +
100 +```json
101 +{
102 + "scripts": {
103 + "api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",
104 + "api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js"
105 + }
106 +}
107 +```
108 +
109 +## 目录结构
110 +
111 +```
112 +docs/api-specs/
113 +├── user/ # 用户模块
114 +│ ├── getUserInfo.md # 获取用户信息
115 +│ └── editUserInfo.md # 编辑用户信息
116 +├── course/ # 课程模块
117 +│ ├── getList.md # 获取课程列表
118 +│ └── getDetail.md # 获取课程详情
119 +└── order/ # 订单模块
120 + └── getList.md # 获取订单列表
121 +```
122 +
123 +**重要规则**:
124 +- 第一级目录 = 模块名(生成 `模块名.js` 文件)
125 +- 第二级文件 = 接口名(生成 `接口名API` 函数)
126 +
127 +## OpenAPI 文档规范
128 +
129 +### 基本结构
130 +
131 +每个 `.md` 文件必须包含一个 YAML 代码块:
132 +
133 +```markdown
134 +# 接口标题
135 +
136 +## OpenAPI Specification
137 +
138 +```yaml
139 +openapi: 3.0.1
140 +info:
141 + title: ''
142 + version: 1.0.0
143 +paths:
144 + /srv/:
145 + get: # 或 post
146 + summary: 接口简介
147 + parameters: # GET 请求参数
148 + - name: a
149 + in: query
150 + example: action_name
151 + responses:
152 + '200':
153 + content:
154 + application/json:
155 + schema:
156 + type: object
157 + properties:
158 + code:
159 + type: integer
160 + data:
161 + type: any
162 +```
163 +
164 +### GET 请求示例
165 +
166 +```yaml
167 +paths:
168 + /srv/:
169 + get:
170 + summary: 获取课程列表
171 + parameters:
172 + - name: a
173 + in: query
174 + example: course_list
175 + - name: page
176 + in: query
177 + description: 页码
178 + required: true
179 + schema:
180 + type: integer
181 + - name: limit
182 + in: query
183 + description: 每页数量
184 + schema:
185 + type: integer
186 +```
187 +
188 +### POST 请求示例
189 +
190 +```yaml
191 +paths:
192 + /srv/:
193 + post:
194 + summary: 创建订单
195 + requestBody:
196 + content:
197 + application/x-www-form-urlencoded:
198 + schema:
199 + type: object
200 + required:
201 + - course_id
202 + properties:
203 + course_id:
204 + type: integer
205 + description: 课程ID
206 + quantity:
207 + type: integer
208 + description: 数量
209 +```
210 +
211 +### 响应结构示例
212 +
213 +```yaml
214 +responses:
215 + '200':
216 + content:
217 + application/json:
218 + schema:
219 + type: object
220 + properties:
221 + code:
222 + type: integer
223 + description: 0=失败,1=成功
224 + msg:
225 + type: string
226 + description: 错误信息
227 + data:
228 + type: object
229 + properties:
230 + user:
231 + type: object
232 + properties:
233 + id:
234 + type: integer
235 + name:
236 + type: string
237 + items:
238 + type: array
239 + items:
240 + type: object
241 + properties:
242 + id:
243 + type: integer
244 + title:
245 + type: string
246 +```
247 +
248 +## 生成的代码示例
249 +
250 +### 输入:`docs/api-specs/user/getUserInfo.md`
251 +
252 +```yaml
253 +paths:
254 + /srv/:
255 + get:
256 + summary: 获取用户信息
257 + parameters:
258 + - name: a
259 + example: user_info
260 + - name: id
261 + description: 用户ID
262 + required: true
263 +```
264 +
265 +### 输出:`src/api/user.js`
266 +
267 +```javascript
268 +import { fn, fetch } from '@/api/fn';
269 +
270 +const Api = {
271 + GetUserInfo: '/srv/?a=user_info',
272 +}
273 +
274 +/**
275 + * @description: 获取用户信息
276 + * @param {Object} params 请求参数
277 + * @param {integer} params.id 用户ID
278 + * @returns {Promise<{
279 + * code: number; // 状态码
280 + * msg: string; // 消息
281 + * data: any;
282 + * }>}
283 + */
284 +export const getUserInfoAPI = (params) => fn(fetch.get(Api.GetUserInfo, params));
285 +```
286 +
287 +## 变更检测
288 +
289 +### 自动变更检测
290 +
291 +每次运行 `pnpm api:generate` 时会自动:
292 +
293 +1. 备份当前文档到 `/.tmp/openAPI-backup`
294 +2. 与上次版本对比(保存在 `/.tmp/openAPI-temp`
295 +3. 生成变更报告
296 +
297 +### 变更报告示例
298 +
299 +```
300 +🔍 开始检测 API 变更...
301 +
302 +=== API 变更检测报告 ===
303 +
304 +📦 对比范围: 5 个旧接口 → 6 个新接口
305 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
306 +
307 +✅ 新增接口 (1):
308 + + getUserProfile
309 +
310 +⚠️ 修改接口 (2):
311 + ↪ editUserInfo
312 + ✗ [破坏性] 删除必填参数: sms_code
313 + ✓ [非破坏性] 新增可选参数: avatar
314 +
315 + ↪ getUserInfo
316 + ✓ [非破坏性] 新增可选参数: include_profile
317 +
318 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
319 +总计: 1 新增, 2 修改, 0 删除
320 +⚠️ 检测到 1 个破坏性变更,请仔细检查业务逻辑!
321 +```
322 +
323 +### 手动对比
324 +
325 +```bash
326 +# 对比两个目录
327 +pnpm api:diff docs/api-specs/user/ docs/api-specs/user-new/
328 +
329 +# 对比两个文件
330 +pnpm api:diff docs/api-specs/user/getInfo.md docs/api-specs/user/getInfo-v2.md
331 +
332 +# 输出 JSON 格式
333 +API_DIFF_FORMAT=json pnpm api:diff ...
334 +```
335 +
336 +## 高级配置
337 +
338 +### 环境变量
339 +
340 +```bash
341 +# 严格模式:任何变更都返回失败码
342 +API_DIFF_STRICT=true pnpm api:generate
343 +
344 +# JSON 格式输出
345 +API_DIFF_FORMAT=json pnpm api:diff ...
346 +```
347 +
348 +### 自定义输出路径
349 +
350 +编辑 `scripts/generateApiFromOpenAPI.js` 底部:
351 +
352 +```javascript
353 +// 修改输入/输出路径
354 +const openAPIDir = path.resolve(__dirname, '../../docs/api-specs');
355 +const outputDir = path.resolve(__dirname, '../../src/api');
356 +```
357 +
358 +## 跨项目使用
359 +
360 +### 方法 1:复制整个 skill
361 +
362 +```bash
363 +# 复制到其他项目
364 +cp -r .claude/custom_skills/api-generator /other/project/.claude/custom_skills/
365 +
366 +# 复制 npm 脚本
367 +# 在其他项目的 package.json 中添加 scripts
368 +
369 +# 安装依赖
370 +cd /other/project
371 +pnpm add -D js-yaml
372 +```
373 +
374 +### 方法 2:使用安装脚本
375 +
376 +```bash
377 +# 在目标项目中运行
378 +bash .claude/custom_skills/api-generator/setup/install.sh
379 +```
380 +
381 +## 工作流程
382 +
383 +### 开发新接口
384 +
385 +1. **创建文档** - 在 `docs/api-specs/模块名/接口名.md`
386 +2. **编写规范** - 按照 OpenAPI 3.0.1 规范编写 YAML
387 +3. **生成代码** - 运行 `pnpm api:generate`
388 +4. **检查变更** - 查看变更报告,确认没有破坏性变更
389 +5. **使用代码** - 在组件中导入并使用生成的 API 函数
390 +
391 +### 修改接口
392 +
393 +1. **更新文档** - 修改对应的 `.md` 文件
394 +2. **生成代码** - 运行 `pnpm api:generate`
395 +3. **检查影响** - 查看变更报告,评估影响范围
396 +4. **更新业务** - 根据变更类型更新业务代码
397 + - 破坏性变更:必须修改业务代码
398 + - 非破坏性变更:可选修改
399 +
400 +### 删除接口
401 +
402 +1. **删除文档** - 删除对应的 `.md` 文件
403 +2. **生成代码** - 运行 `pnpm api:generate`
404 +3. **检查引用** - 全局搜索被删除的 API 函数,移除引用
405 +
406 +## 常见问题
407 +
408 +### Q: 生成代码后报错找不到模块?
409 +
410 +**A**: 检查以下几点:
411 +1. 确认 `src/api/fn.js` 存在且导出 `fn``fetch`
412 +2. 确认路径别名 `@/` 正确配置
413 +3. 确认生成的文件在 `src/api/` 目录下
414 +
415 +### Q: 如何处理需要认证的接口?
416 +
417 +**A**: 生成的代码会自动使用 `src/utils/axios.js` 中的拦截器,自动添加认证头。无需特殊处理。
418 +
419 +### Q: 如何处理不同的请求方法?
420 +
421 +**A**: 脚本会自动识别:
422 +- `get``fetch.get()`
423 +- `post``fetch.post()`
424 +- `application/x-www-form-urlencoded``fetch.stringifyPost()`
425 +
426 +### Q: 生成的代码格式不统一?
427 +
428 +**A**: 运行 `pnpm lint` 自动格式化生成的代码。
429 +
430 +### Q: 如何复用已有的 API 定义?
431 +
432 +**A**: 可以在 YAML 中使用 `$ref` 引用,但当前版本暂不支持,建议直接展开定义。
433 +
434 +## 最佳实践
435 +
436 +### 1. 文档命名规范
437 +
438 +- **模块目录**: 小写,多个单词用下划线(`user_profile`
439 +- **接口文件**: 小写,动词开头(`getUserInfo.md`
440 +
441 +### 2. 接口分组
442 +
443 +- 按业务模块分组(`user`, `course`, `order`
444 +- 避免单个模块过大(建议 < 20 个接口)
445 +- 相关接口放在同一模块
446 +
447 +### 3. 版本管理
448 +
449 +-`docs/api-specs/` 纳入版本控制
450 +- 生成的 `src/api/*.js` 也纳入版本控制
451 +- 保留变更历史,方便回滚
452 +
453 +### 4. 团队协作
454 +
455 +- 前后端共同维护 OpenAPI 文档
456 +- 接口变更前先更新文档
457 +- 使用变更报告评估影响
458 +- 破坏性变更必须通知团队
459 +
460 +### 5. 文档注释
461 +
462 +- **summary**: 简短描述(1行)
463 +- **description**: 详细说明(可选)
464 +- **参数**: 每个参数都应有 description
465 +- **响应**: 嵌套对象也应有说明
466 +
467 +## 故障排除
468 +
469 +### 问题:YAML 解析失败
470 +
471 +**错误信息**: `解析 OpenAPI 文档失败: YAMLException`
472 +
473 +**解决方案**:
474 +1. 检查 YAML 缩进(必须使用空格,不能用 Tab)
475 +2. 检查 YAML 语法(使用在线验证器)
476 +3. 确保所有字符串正确引号包裹
477 +
478 +### 问题:生成的函数名不符合预期
479 +
480 +**原因**: 文件名包含特殊字符
481 +
482 +**解决方案**:
483 +- 使用小写字母和数字
484 +- 多个单词用下划线分隔
485 +- 避免使用中划线(会转换为驼峰)
486 +
487 +### 问题:变更检测不准确
488 +
489 +**原因**: 首次运行或备份文件损坏
490 +
491 +**解决方案**:
492 +```bash
493 +# 清理备份
494 +rm -rf .tmp/openAPI-*
495 +
496 +# 重新建立基线
497 +pnpm api:generate
498 +```
499 +
500 +## 相关资源
501 +
502 +- [OpenAPI 3.0 规范](https://swagger.io/specification/)
503 +- [YAML 语法指南](https://yaml.org/spec/1.2/spec.html)
504 +- [API 设计最佳实践](https://github.com/microsoft/api-guidelines)
505 +
506 +## 更新日志
507 +
508 +### v1.0.0 (2026-01-29)
509 +
510 +- ✨ 初始版本
511 +- ✅ 支持 GET/POST 请求
512 +- ✅ 自动生成 JSDoc 注释
513 +- ✅ API 变更检测
514 +- ✅ 增量更新机制
515 +- ✅ 跨项目支持
1 +# 接口名称
2 +
3 +## 接口描述
4 +
5 +详细描述这个接口的功能、使用场景和注意事项。
6 +
7 +## OpenAPI Specification
8 +
9 +```yaml
10 +openapi: 3.0.1
11 +info:
12 + title: ''
13 + version: 1.0.0
14 +paths:
15 + /srv/:
16 + get: # 或 post
17 + summary: 接口简介(一行描述)
18 + description: |
19 + 接口详细说明...
20 + - 使用场景 1
21 + - 使用场景 2
22 + tags:
23 + - 模块名称
24 + parameters: # GET 请求使用 parameters
25 + - name: a
26 + in: query
27 + description: action 参数
28 + required: false
29 + example: your_action_name
30 + schema:
31 + type: string
32 + - name: f
33 + in: query
34 + description: 业务模块
35 + required: false
36 + example: behalo
37 + schema:
38 + type: string
39 + - name: id
40 + in: query
41 + description: 参数描述
42 + required: true # true=必填,false=可选
43 + example: 123
44 + schema:
45 + type: integer
46 + requestBody: # POST 请求使用 requestBody
47 + content:
48 + application/x-www-form-urlencoded: # 或 application/json
49 + schema:
50 + type: object
51 + required:
52 + - course_id
53 + - quantity
54 + properties:
55 + course_id:
56 + type: integer
57 + description: 课程ID
58 + example: 1
59 + quantity:
60 + type: integer
61 + description: 数量
62 + example: 1
63 + responses:
64 + '200':
65 + description: 成功返回
66 + content:
67 + application/json:
68 + schema:
69 + type: object
70 + properties:
71 + code:
72 + type: integer
73 + description: 0=失败,1=成功
74 + msg:
75 + type: string
76 + description: 错误信息
77 + data:
78 + type: object
79 + description: 返回数据
80 + properties:
81 + id:
82 + type: integer
83 + description: 数据ID
84 + name:
85 + type: string
86 + description: 名称
87 + items:
88 + type: array
89 + description: 列表数据
90 + items:
91 + type: object
92 + properties:
93 + item_id:
94 + type: integer
95 + description: 项目ID
96 + item_name:
97 + type: string
98 + description: 项目名称
99 +```
100 +
101 +## 使用示例
102 +
103 +### GET 请求示例
104 +
105 +```javascript
106 +import { getYourAPINameAPI } from '@/api/yourModule'
107 +
108 +const { code, data } = await getYourAPINameAPI({
109 + id: 123,
110 + page: 1,
111 + limit: 10
112 +})
113 +
114 +if (code === 1) {
115 + console.log('成功:', data)
116 +}
117 +```
118 +
119 +### POST 请求示例
120 +
121 +```javascript
122 +import { createYourResourceAPI } from '@/api/yourModule'
123 +
124 +const { code, data } = await createYourResourceAPI({
125 + course_id: 1,
126 + quantity: 2
127 +})
128 +
129 +if (code === 1) {
130 + console.log('创建成功:', data)
131 +}
132 +```
133 +
134 +## 注意事项
135 +
136 +- **权限要求**: 说明是否需要登录、特殊权限等
137 +- **限流规则**: 说明接口调用频率限制
138 +- **错误码**: 列出常见的错误码及含义
139 +- **兼容性**: 说明版本兼容性要求
140 +
141 +## 相关接口
142 +
143 +- [相关接口1](./relatedApi1.md)
144 +- [相关接口2](./relatedApi2.md)
145 +
146 +## 更新记录
147 +
148 +- **2026-01-29**: 初始版本,创建接口
1 +{
2 + "packageManager": "pnpm"
3 +}
1 +{
2 + "mcpServers": {
3 + "mlaj_API_文档": {
4 + "command": "npx",
5 + "args": [
6 + "-y",
7 + "apifox-mcp-server@latest",
8 + "--project-id=6084040"
9 + ],
10 + "env": {
11 + "APIFOX_ACCESS_TOKEN": "APS-jkT1Q61MCKgzgvfCL2euIR2TcgKsnSyc"
12 + }
13 + }
14 + }
15 +}
1 { 1 {
2 "permissions": { 2 "permissions": {
3 "allow": [ 3 "allow": [
4 - "Bash(pandoc:*)",
5 - "Bash(npx skills --help:*)",
6 - "Bash(pnpm add:*)",
7 - "Bash(pnpm api:generate:*)",
8 - "Bash(node:*)",
9 - "Bash(ls:*)",
10 "Bash(tree:*)", 4 "Bash(tree:*)",
11 - "Bash(git checkout:*)", 5 + "Bash(xargs awk:*)",
6 + "Bash(find:*)",
7 + "Bash(grep:*)",
8 + "Bash(git diff:*)",
9 + "mcp__web-search-prime__webSearchPrime",
10 + "mcp__web-reader__webReader",
11 + "Bash(ls:*)",
12 + "Bash(for:*)",
13 + "Bash(do if [ -f \"$dirskill.md\" ])",
14 + "Bash(fi:*)",
15 + "Bash(done:*)",
16 + "Bash(if:*)",
17 + "Bash(then echo \"找到项目本地技能:\")",
18 + "Bash(else echo \"当前项目没有本地技能目录\")",
19 + "Bash(command:*)",
20 + "Bash(then mv \"$dirskill.md\" \"$dirSKILL.md\")",
21 + "Bash(do if [ -f \"$dirSKILL.md\" ])",
22 + "Bash(then rmdir ~/.config/claude-code/skills)",
23 + "Bash(else echo \"目录不为空或不存在,跳过删除\")",
24 + "Bash(chmod:*)",
25 + "Bash(scripts/upload-to-qiniu.sh:*)",
26 + "Bash(curl:*)",
12 "Bash(git add:*)", 27 "Bash(git add:*)",
13 "Bash(git commit:*)", 28 "Bash(git commit:*)",
29 + "Bash(git push)",
30 + "Bash(git restore:*)",
31 + "Bash(pnpm add:*)",
32 + "Bash(pnpm lint:check:*)",
33 + "Bash(pnpm format:check:*)",
34 + "Bash(pnpm list:*)",
35 + "Bash(npx husky init:*)",
36 + "Bash(npx lint-staged:*)",
37 + "Bash(__NEW_LINE_9f2146480c6bc5cf__ echo \"✅ Husky + lint-staged 配置完成!\" echo \"\" echo \"📁 配置文件:\" echo \" - .husky/pre-commit\" echo \" - package.json \\(lint-staged 配置\\)\" echo \"\" echo \"🔧 已配置的 Git Hooks:\" ls -la .husky/)",
38 + "Bash(__NEW_LINE_d9f17a4e47d5be4b__ echo \"╔════════════════════════════════════════════════════════════╗\" echo \"║ ✅ Husky + lint-staged 配置完成! ║\" echo \"╚════════════════════════════════════════════════════════════╝\" echo \"\" echo \"📁 配置文件:\" echo \" • .husky/pre-commit - Git pre-commit hook\" echo \" • package.json - lint-staged 配置\" echo \" • docs/HUSKY_LINT_STAGED.md - 使用文档\" echo \"\" echo \"📦 已安装的包:\" pnpm list husky lint-staged --depth 0)",
39 + "Bash(npx playwright install:*)",
14 "Bash(git push:*)", 40 "Bash(git push:*)",
15 - "Bash(find:*)", 41 + "Bash(pnpm test:e2e:*)",
16 - "mcp__web-search-prime__webSearchPrime", 42 + "mcp__zai-mcp-server__ui_to_artifact",
17 - "mcp__web-reader__webReader" 43 + "mcp__zai-mcp-server__extract_text_from_screenshot",
44 + "mcp__zai-mcp-server__analyze_image",
45 + "Bash(git mv:*)",
46 + "Bash(yarn api:generate:*)",
47 + "Bash(./test-mcp.sh:*)",
48 + "Bash(npx:*)",
49 + "Bash(APIFOX_ACCESS_TOKEN=\"APS-jkT1Q61MCKgzgvfCL2euIR2TcgKsnSyc\" npx -y apifox-mcp-server@latest:*)",
50 + "Bash(git checkout:*)"
18 ] 51 ]
19 } 52 }
20 } 53 }
......