hookehuyr

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

添加完整的API生成器技能,包括从OpenAPI文档生成前端API代码的工具链
更新Claude配置以支持pnpm包管理器和MCP服务器
重构权限设置,优化开发工具链集成
# API Diff Skill
对比两个版本的 OpenAPI 文档或生成的 API 文件,检测接口变更。
## 使用场景
1. **更新 API 后自动检查**:运行 `api:generate` 后自动对比新旧接口
2. **手动对比**:对比两个不同的 OpenAPI 文档
3. **CI/CD 集成**:在部署前检查破坏性 API 变更
## 如何调用
### 在生成 API 后自动对比
```bash
pnpm run api:generate
```
生成器会自动调用对比逻辑,检查是否有接口变更。
### 手动对比两个文档
```bash
# 对比两个 OpenAPI markdown 文档
node scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md
# 对比整个模块目录
node scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/
# 对比生成的 API 文件
node scripts/apiDiff.js src/api/user.js src/api/user-new.js
```
## 对比维度
1. **接口增删**:新增或删除的接口
2. **参数变更**
- 新增必填参数(破坏性变更)
- 删除参数(破坏性变更)
- 参数类型变更(破坏性变更)
- 新增可选参数(非破坏性)
3. **返回值变更**
- 返回结构变更
- 字段类型变更
4. **HTTP 方法变更**:GET ↔ POST(破坏性变更)
## 输出格式
对比结果会以以下格式输出:
```
=== API 变更检测报告 ===
📦 模块: user
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 新增接口 (1):
+ getUserProfile
⚠️ 修改接口 (2):
↪ editUserInfo
✗ [破坏性] 删除必填参数: sms_code
✓ [非破坏性] 新增可选参数: avatar
↪ getUserInfo
✓ [非破坏性] 新增可选参数: include_profile
❌ 删除接口 (1):
- deleteUserAccount
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计: 1 新增, 2 修改, 1 删除
⚠️ 检测到 1 个破坏性变更,请仔细检查业务逻辑!
```
## 退出码
- `0`: 无变更或仅有非破坏性变更
- `1`: 检测到破坏性变更(可用于 CI/CD 失败)
## 配置选项
可以通过环境变量配置:
```bash
# 严格模式:任何变更都返回失败码
API_DIFF_STRICT=true node scripts/apiDiff.js ...
# 输出 JSON 格式(用于程序解析)
API_DIFF_FORMAT=json node scripts/apiDiff.js ...
```
## 注意事项
1. 对比逻辑基于 OpenAPI 规范,确保文档格式正确
2. 破坏性变更需要在业务代码中做兼容处理
3. 新增接口通常不需要修改现有代码
4. 删除接口前请确认没有地方在使用
/**
* API 对比工具
*
* 功能:
* 1. 对比两个 OpenAPI 文档的差异
* 2. 检测破坏性变更
* 3. 生成详细的变更报告
*
* 使用方式:
* node .claude/custom_skills/api-generator/scripts/apiDiff.js <oldPath> <newPath>
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
/**
* 从 Markdown 文件中提取 YAML
*/
function extractYAMLFromMarkdown(content) {
const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/;
const match = content.match(yamlRegex);
return match ? match[1] : null;
}
/**
* 解析 OpenAPI 文档(支持 .md 和目录)
*/
function parseOpenAPIPath(filePath) {
const stat = fs.statSync(filePath);
if (stat.isFile()) {
// 单个文件
if (filePath.endsWith('.md')) {
const content = fs.readFileSync(filePath, 'utf8');
const yamlContent = extractYAMLFromMarkdown(content);
if (!yamlContent) {
throw new Error(`文件 ${filePath} 中未找到 YAML 代码块`);
}
return [yaml.load(yamlContent)];
} else if (filePath.endsWith('.js')) {
// TODO: 支持对比生成的 JS 文件(需要解析 AST)
throw new Error('暂不支持对比生成的 JS 文件,请对比 OpenAPI 文档');
} else {
throw new Error(`不支持的文件类型: ${filePath}`);
}
} else if (stat.isDirectory()) {
// 目录,读取所有 .md 文件
const files = fs.readdirSync(filePath).filter(f => f.endsWith('.md'));
const docs = [];
files.forEach(file => {
const fullPath = path.join(filePath, file);
const content = fs.readFileSync(fullPath, 'utf8');
const yamlContent = extractYAMLFromMarkdown(content);
if (yamlContent) {
const doc = yaml.load(yamlContent);
// 保存文件名用于标识
doc._fileName = path.basename(file, '.md');
docs.push(doc);
}
});
return docs;
}
}
/**
* 从 OpenAPI 文档提取 API 信息
*/
function extractAPIInfo(openapiDoc) {
const path = Object.keys(openapiDoc.paths)[0];
const method = Object.keys(openapiDoc.paths[path])[0];
const apiInfo = openapiDoc.paths[path][method];
// 提取参数
const queryParams = (apiInfo.parameters || [])
.filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f')
.map(p => ({
name: p.name,
type: p.schema?.type || 'any',
required: p.required || false,
description: p.description || '',
}));
// 提取 body 参数
const bodyParams = [];
if (apiInfo.requestBody && apiInfo.requestBody.content) {
const content = apiInfo.requestBody.content['application/x-www-form-urlencoded'] ||
apiInfo.requestBody.content['application/json'];
if (content && content.schema && content.schema.properties) {
Object.entries(content.schema.properties).forEach(([key, value]) => {
if (key !== 'a' && key !== 'f') {
bodyParams.push({
name: key,
type: value.type || 'any',
required: content.schema.required?.includes(key) || false,
description: value.description || '',
});
}
});
}
}
// 提取响应结构
const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;
return {
name: openapiDoc._fileName || 'unknown',
path,
method: method.toUpperCase(),
queryParams: new Set(queryParams.map(p => p.name)),
bodyParams: new Set(bodyParams.map(p => p.name)),
requiredQueryParams: new Set(queryParams.filter(p => p.required).map(p => p.name)),
requiredBodyParams: new Set(bodyParams.filter(p => p.required).map(p => p.name)),
allQueryParams: queryParams,
allBodyParams: bodyParams,
responseSchema,
summary: apiInfo.summary || '',
};
}
/**
* 对比两个 API 信息
*/
function compareAPI(oldAPI, newAPI) {
const changes = {
breaking: [],
nonBreaking: [],
};
// 检查 HTTP 方法变更
if (oldAPI.method !== newAPI.method) {
changes.breaking.push(`HTTP 方法变更: ${oldAPI.method} → ${newAPI.method}`);
}
// 检查 GET 参数变更
oldAPI.allQueryParams.forEach(oldParam => {
const newParam = newAPI.allQueryParams.find(p => p.name === oldParam.name);
if (!newParam) {
// 参数被删除
if (oldAPI.requiredQueryParams.has(oldParam.name)) {
changes.breaking.push(`删除必填 query 参数: ${oldParam.name}`);
} else {
changes.nonBreaking.push(`删除可选 query 参数: ${oldParam.name}`);
}
} else {
// 参数类型变更
if (oldParam.type !== newParam.type) {
changes.breaking.push(`query 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})`);
}
// 可选 → 必填
if (!oldParam.required && newParam.required) {
changes.breaking.push(`query 参数变为必填: ${newParam.name}`);
}
// 必填 → 可选
if (oldParam.required && !newParam.required) {
changes.nonBreaking.push(`query 参数变为可选: ${newParam.name}`);
}
}
});
// 检查新增 GET 参数
newAPI.allQueryParams.forEach(newParam => {
const oldParam = oldAPI.allQueryParams.find(p => p.name === newParam.name);
if (!oldParam) {
if (newParam.required) {
changes.breaking.push(`新增必填 query 参数: ${newParam.name}`);
} else {
changes.nonBreaking.push(`新增可选 query 参数: ${newParam.name}`);
}
}
});
// 检查 POST body 参数变更
oldAPI.allBodyParams.forEach(oldParam => {
const newParam = newAPI.allBodyParams.find(p => p.name === oldParam.name);
if (!newParam) {
// 参数被删除
if (oldAPI.requiredBodyParams.has(oldParam.name)) {
changes.breaking.push(`删除必填 body 参数: ${oldParam.name}`);
} else {
changes.nonBreaking.push(`删除可选 body 参数: ${oldParam.name}`);
}
} else {
// 参数类型变更
if (oldParam.type !== newParam.type) {
changes.breaking.push(`body 参数类型变更: ${oldParam.name} (${oldParam.type} → ${newParam.type})`);
}
// 可选 → 必填
if (!oldParam.required && newParam.required) {
changes.breaking.push(`body 参数变为必填: ${newParam.name}`);
}
// 必填 → 可选
if (oldParam.required && !newParam.required) {
changes.nonBreaking.push(`body 参数变为可选: ${newParam.name}`);
}
}
});
// 检查新增 body 参数
newAPI.allBodyParams.forEach(newParam => {
const oldParam = oldAPI.allBodyParams.find(p => p.name === newParam.name);
if (!oldParam) {
if (newParam.required) {
changes.breaking.push(`新增必填 body 参数: ${newParam.name}`);
} else {
changes.nonBreaking.push(`新增可选 body 参数: ${newParam.name}`);
}
}
});
return changes;
}
/**
* 生成变更报告
*/
function generateReport(oldDocs, newDocs, format = 'text') {
const oldAPIs = oldDocs.map(extractAPIInfo);
const newAPIs = newDocs.map(extractAPIInfo);
const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]));
const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]));
const addedAPIs = [];
const removedAPIs = [];
const modifiedAPIs = [];
// 检测新增接口
newAPIs.forEach(api => {
if (!oldAPIsMap.has(api.name)) {
addedAPIs.push(api);
}
});
// 检测删除接口
oldAPIs.forEach(api => {
if (!newAPIsMap.has(api.name)) {
removedAPIs.push(api);
}
});
// 检测修改接口
newAPIs.forEach(api => {
const oldAPI = oldAPIsMap.get(api.name);
if (oldAPI) {
const changes = compareAPI(oldAPI, api);
if (changes.breaking.length > 0 || changes.nonBreaking.length > 0) {
modifiedAPIs.push({
name: api.name,
summary: api.summary,
changes,
});
}
}
});
// 统计
const totalBreaking = modifiedAPIs.reduce(
(sum, api) => sum + api.changes.breaking.length,
0
);
// 生成文本报告
if (format === 'text') {
const lines = [];
lines.push('=== API 变更检测报告 ===\n');
lines.push(`📦 对比范围: ${oldAPIs.length} 个旧接口 → ${newAPIs.length} 个新接口`);
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
if (addedAPIs.length > 0) {
lines.push(`✅ 新增接口 (${addedAPIs.length}):`);
addedAPIs.forEach(api => {
lines.push(` + ${api.name} - ${api.summary}`);
});
lines.push('');
}
if (modifiedAPIs.length > 0) {
lines.push(`⚠️ 修改接口 (${modifiedAPIs.length}):`);
modifiedAPIs.forEach(api => {
lines.push(` ↪ ${api.name} - ${api.summary}`);
api.changes.breaking.forEach(change => {
lines.push(` ✗ [破坏性] ${change}`);
});
api.changes.nonBreaking.forEach(change => {
lines.push(` ✓ [非破坏性] ${change}`);
});
});
lines.push('');
}
if (removedAPIs.length > 0) {
lines.push(`❌ 删除接口 (${removedAPIs.length}):`);
removedAPIs.forEach(api => {
lines.push(` - ${api.name} - ${api.summary}`);
});
lines.push('');
}
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
lines.push(`总计: ${addedAPIs.length} 新增, ${modifiedAPIs.length} 修改, ${removedAPIs.length} 删除`);
if (totalBreaking > 0) {
lines.push(`⚠️ 检测到 ${totalBreaking} 个破坏性变更,请仔细检查业务逻辑!`);
} else if (addedAPIs.length > 0 || modifiedAPIs.length > 0 || removedAPIs.length > 0) {
lines.push('✅ 未检测到破坏性变更');
} else {
lines.push('✅ 无接口变更');
}
return lines.join('\n');
}
// 生成 JSON 报告
if (format === 'json') {
return JSON.stringify({
summary: {
added: addedAPIs.length,
modified: modifiedAPIs.length,
removed: removedAPIs.length,
breakingChanges: totalBreaking,
},
added: addedAPIs.map(api => ({
name: api.name,
summary: api.summary,
method: api.method,
path: api.path,
})),
modified: modifiedAPIs.map(api => ({
name: api.name,
summary: api.summary,
breakingChanges: api.changes.breaking,
nonBreakingChanges: api.changes.nonBreaking,
})),
removed: removedAPIs.map(api => ({
name: api.name,
summary: api.summary,
method: api.method,
path: api.path,
})),
}, null, 2);
}
}
/**
* 主函数
*/
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('用法: node .claude/custom_skills/api-generator/scripts/apiDiff.js <oldPath> <newPath>');
console.error('示例:');
console.error(' node .claude/custom_skills/api-generator/scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/');
console.error(' node .claude/custom_skills/api-generator/scripts/apiDiff.js docs/api-specs/user/api1.md docs/api-specs/user/api1-new.md');
process.exit(1);
}
const [oldPath, newPath] = args;
if (!fs.existsSync(oldPath)) {
console.error(`❌ 旧路径不存在: ${oldPath}`);
process.exit(1);
}
if (!fs.existsSync(newPath)) {
console.error(`❌ 新路径不存在: ${newPath}`);
process.exit(1);
}
try {
const oldDocs = parseOpenAPIPath(oldPath);
const newDocs = parseOpenAPIPath(newPath);
const format = process.env.API_DIFF_FORMAT || 'text';
const report = generateReport(oldDocs, newDocs, format);
console.log(report);
// 如果有破坏性变更,返回退出码 1
const oldAPIs = oldDocs.map(extractAPIInfo);
const newAPIs = newDocs.map(extractAPIInfo);
const oldAPIsMap = new Map(oldAPIs.map(api => [api.name, api]));
const newAPIsMap = new Map(newAPIs.map(api => [api.name, api]));
let totalBreaking = 0;
newAPIs.forEach(api => {
const oldAPI = oldAPIsMap.get(api.name);
if (oldAPI) {
const changes = compareAPI(oldAPI, api);
totalBreaking += changes.breaking.length;
}
});
// 严格模式:任何变更都返回 1
const strictMode = process.env.API_DIFF_STRICT === 'true';
const hasChanges = oldAPIs.length !== newAPIs.length ||
newAPIs.some(api => !oldAPIsMap.has(api.name)) ||
oldAPIs.some(api => !newAPIsMap.has(api.name));
if (totalBreaking > 0 || (strictMode && hasChanges)) {
process.exit(1);
} else {
process.exit(0);
}
} catch (error) {
console.error(`❌ 对比失败: ${error.message}`);
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module) {
main();
}
module.exports = {
compareAPI,
generateReport,
parseOpenAPIPath,
extractAPIInfo,
};
/**
* 从 OpenAPI 文档自动生成 API 接口文件
*
* 功能:
* 1. 扫描 docs/api-specs 目录
* 2. 解析每个 .md 文件中的 OpenAPI YAML 规范
* 3. 提取 API 信息并生成对应的 JavaScript API 文件
* 4. 保存到 src/api/ 目录
*
* 目录结构:
* docs/api-specs/
* ├── module1/
* │ ├── api1.md
* │ └── api2.md
* └── module2/
* └── api3.md
*
* 生成到:
* src/api/
* ├── module1.js
* └── module2.js
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const { generateReport, parseOpenAPIPath } = require('./apiDiff.cjs');
/**
* 提取 Markdown 文件中的 YAML 代码块
* @param {string} content - Markdown 文件内容
* @returns {string|null} - YAML 字符串或 null
*/
function extractYAMLFromMarkdown(content) {
const yamlRegex = /```yaml\s*\n([\s\S]*?)\n```/;
const match = content.match(yamlRegex);
return match ? match[1] : null;
}
/**
* 将字符串转换为驼峰命名
* @param {string} str - 输入字符串
* @returns {string} - 驼峰命名字符串
*/
function toCamelCase(str) {
return str
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
.replace(/^(.)/, (c) => c.toLowerCase());
}
/**
* 将字符串转换为帕斯卡命名(首字母大写)
* @param {string} str - 输入字符串
* @returns {string} - 帕斯卡命名字符串
*/
function toPascalCase(str) {
const camelCase = toCamelCase(str);
return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
}
/**
* 解析对象属性,生成字段描述
* @param {object} properties - 属性对象
* @param {number} indent - 缩进级别
* @returns {string} - 字段描述字符串
*/
function parseProperties(properties, indent = 0) {
if (!properties) return '';
const lines = [];
const prefix = ' '.repeat(indent);
Object.entries(properties).forEach(([key, value]) => {
const type = value.type || 'any';
const desc = value.description || value.title || '';
const required = value.required ? '' : ' (可选)';
// 基本类型
if (type !== 'object' && type !== 'array') {
lines.push(`${prefix}${key}: ${type}${required} - ${desc}`);
}
// 对象类型
else if (type === 'object' && value.properties) {
lines.push(`${prefix}${key}: {`);
lines.push(`${prefix} // ${desc}`);
lines.push(parseProperties(value.properties, indent + 2));
lines.push(prefix + '}');
}
// 数组类型
else if (type === 'array' && value.items) {
const itemType = value.items.type || 'any';
if (itemType === 'object' && value.items.properties) {
lines.push(`${prefix}${key}: Array<{`);
lines.push(`${prefix} // ${desc}`);
lines.push(parseProperties(value.items.properties, indent + 2));
lines.push(prefix + '}>');
} else {
lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`);
}
}
});
return lines.join('\n');
}
/**
* 从 requestBody 中提取参数
* @param {object} requestBody - requestBody 对象
* @returns {Array} - 参数数组
*/
function extractRequestParams(requestBody) {
if (!requestBody || !requestBody.content) {
return [];
}
// 获取内容类型(可能是 application/x-www-form-urlencoded 或 application/json)
const content = requestBody.content['application/x-www-form-urlencoded'] ||
requestBody.content['application/json'];
if (!content || !content.schema || !content.schema.properties) {
return [];
}
const params = [];
Object.entries(content.schema.properties).forEach(([key, value]) => {
params.push({
name: key,
type: value.type || 'any',
description: value.description || '',
example: value.example || '',
required: content.schema.required?.includes(key) || false,
});
});
return params;
}
/**
* 生成 JSDoc 参数注释
* @param {Array} parameters - parameters 数组(GET 请求)
* @param {Array} bodyParams - requestBody 参数数组(POST 请求)
* @param {string} method - HTTP 方法
* @returns {string} - JSDoc 参数注释
*/
function generateParamJSDoc(parameters, bodyParams, method) {
const lines = [' * @param {Object} params 请求参数'];
// POST 请求使用 body 参数
if (method === 'POST' && bodyParams && bodyParams.length > 0) {
// 过滤掉 a 和 f 参数
const filteredParams = bodyParams.filter(p => p.name !== 'a' && p.name !== 'f');
filteredParams.forEach((param) => {
const type = param.type || 'any';
const desc = param.description || '';
const required = param.required ? '' : ' (可选)';
lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
});
}
// GET 请求使用 query 参数
else if (method === 'GET' && parameters && parameters.length > 0) {
// 只保留 query 参数,过滤 header 参数
const queryParams = parameters.filter(p => p.in === 'query' && p.name !== 'a' && p.name !== 'f');
queryParams.forEach((param) => {
const type = param.schema?.type || 'any';
const desc = param.description || '';
const required = param.required ? '' : ' (可选)';
lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
});
}
return lines.join('\n');
}
/**
* 生成 JSDoc 返回值注释
* @param {object} responseSchema - 响应 schema
* @returns {string} - JSDoc 返回值注释
*/
function generateReturnJSDoc(responseSchema) {
if (!responseSchema || !responseSchema.properties) {
return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回';
}
const { code, msg, data } = responseSchema.properties;
let returnDesc = ' * @returns {Promise<{\n';
returnDesc += ' * code: number; // 状态码\n';
returnDesc += ' * msg: string; // 消息\n';
if (data && data.properties) {
returnDesc += ' * data: {\n';
Object.entries(data.properties).forEach(([key, value]) => {
const type = value.type || 'any';
const desc = value.description || value.title || '';
if (type === 'object' && value.properties) {
returnDesc += ` * ${key}: {\n`;
Object.entries(value.properties).forEach(([subKey, subValue]) => {
const subType = subValue.type || 'any';
const subDesc = subValue.description || subValue.title || '';
returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
});
returnDesc += ` * };\n`;
} else if (type === 'array' && value.items && value.items.properties) {
returnDesc += ` * ${key}: Array<{\n`;
Object.entries(value.items.properties).forEach(([subKey, subValue]) => {
const subType = subValue.type || 'any';
const subDesc = subValue.description || subValue.title || '';
returnDesc += ` * ${subKey}: ${subType}; // ${subDesc}\n`;
});
returnDesc += ` * }>;\n`;
} else {
returnDesc += ` * ${key}: ${type}; // ${desc}\n`;
}
});
returnDesc += ' * };\n';
} else {
returnDesc += ' * data: any;\n';
}
returnDesc += ' * }>}';
return returnDesc;
}
/**
* 解析 OpenAPI 文档并提取 API 信息
* @param {object} openapiDoc - 解析后的 OpenAPI 对象
* @param {string} fileName - 文件名(用作 API 名称)
* @returns {object} - 提取的 API 信息
*/
function parseOpenAPIDocument(openapiDoc, fileName) {
try {
const path = Object.keys(openapiDoc.paths)[0];
const method = Object.keys(openapiDoc.paths[path])[0];
const apiInfo = openapiDoc.paths[path][method];
// 提取 query 参数
const parameters = apiInfo.parameters || [];
const queryParams = {};
let actionValue = '';
// 提取 body 参数(用于 POST 请求)
const requestBody = apiInfo.requestBody;
const bodyParams = extractRequestParams(requestBody);
// 对于 POST 请求,从 requestBody 中提取 action
if (requestBody && bodyParams.length > 0) {
const actionParam = bodyParams.find(p => p.name === 'a');
if (actionParam) {
actionValue = actionParam.example || '';
}
}
// 对于 GET 请求,从 query 参数中提取 action
if (!actionValue && parameters.length > 0) {
parameters.forEach((param) => {
if (param.in === 'query') {
queryParams[param.name] = param.example || param.schema?.default || '';
// 提取 action 参数(通常是 'a' 参数)
if (param.name === 'a') {
actionValue = param.example || '';
}
}
});
}
// 提取响应结构
const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;
return {
summary: apiInfo.summary || fileName,
description: apiInfo.description || '',
method: method.toUpperCase(),
action: actionValue,
queryParams,
parameters, // 保存完整的参数信息用于生成 JSDoc(GET 请求)
bodyParams, // 保存 requestBody 参数用于生成 JSDoc(POST 请求)
responseSchema, // 保存响应结构用于生成 JSDoc
fileName,
};
} catch (error) {
console.error(`解析 OpenAPI 文档失败: ${error.message}`);
return null;
}
}
/**
* 生成 API 文件内容
* @param {string} moduleName - 模块名称
* @param {Array} apis - API 信息数组
* @returns {string} - 生成的文件内容
*/
function generateApiFileContent(moduleName, apis) {
const imports = `import { fn, fetch } from '@/api/fn';\n\n`;
const apiConstants = [];
const apiFunctions = [];
apis.forEach((api) => {
// 生成常量名(帕斯卡命名)
const constantName = toPascalCase(api.fileName);
// 生成函数名(驼峰命名 + API 后缀)
const functionName = toCamelCase(api.fileName) + 'API';
// 添加常量定义
apiConstants.push(
` ${constantName}: '/srv/?a=${api.action}',`
);
// 生成详细的 JSDoc 注释
const paramJSDoc = generateParamJSDoc(api.parameters, api.bodyParams, api.method);
const returnJSDoc = generateReturnJSDoc(api.responseSchema);
// 添加函数定义
const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post';
const comment = `/**
* @description: ${api.summary}
${paramJSDoc}
${returnJSDoc}
*/`;
apiFunctions.push(`${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));`);
});
return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n`;
}
/**
* 扫描目录并处理所有 OpenAPI 文档
* @param {string} openAPIDir - OpenAPI 文档目录
* @param {string} outputDir - 输出目录
*/
function scanAndGenerate(openAPIDir, outputDir) {
if (!fs.existsSync(openAPIDir)) {
console.error(`OpenAPI 目录不存在: ${openAPIDir}`);
return;
}
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 扫描第一级目录(模块)
const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`);
modules.forEach((moduleName) => {
const moduleDir = path.join(openAPIDir, moduleName);
const apiFiles = fs.readdirSync(moduleDir)
.filter(file => file.endsWith('.md'));
if (apiFiles.length === 0) {
console.log(`模块 ${moduleName} 中没有找到 .md 文件`);
return;
}
console.log(`\n处理模块: ${moduleName}`);
console.log(`找到 ${apiFiles.length} 个 API 文档`);
const apis = [];
apiFiles.forEach((fileName) => {
const filePath = path.join(moduleDir, fileName);
const content = fs.readFileSync(filePath, 'utf8');
const yamlContent = extractYAMLFromMarkdown(content);
if (!yamlContent) {
console.warn(` ⚠️ ${fileName}: 未找到 YAML 代码块`);
return;
}
try {
const openapiDoc = yaml.load(yamlContent);
const apiName = path.basename(fileName, '.md');
const apiInfo = parseOpenAPIDocument(openapiDoc, apiName);
if (apiInfo) {
apis.push(apiInfo);
console.log(` ✓ ${apiName}: ${apiInfo.summary}`);
}
} catch (error) {
console.error(` ✗ ${fileName}: 解析失败 - ${error.message}`);
}
});
// 生成并保存 API 文件
if (apis.length > 0) {
const fileContent = generateApiFileContent(moduleName, apis);
const outputPath = path.join(outputDir, `${moduleName}.js`);
fs.writeFileSync(outputPath, fileContent, 'utf8');
console.log(` 📝 生成文件: ${outputPath}`);
}
});
console.log('\n✅ API 文档生成完成!');
// 对比新旧 API
console.log('\n🔍 开始检测 API 变更...\n');
compareAPIChanges(openAPIDir);
}
/**
* 备份 OpenAPI 文档目录
* @param {string} sourceDir - 源目录
* @returns {string} - 备份目录路径
*/
function backupOpenAPIDir(sourceDir) {
const backupBaseDir = path.resolve(__dirname, '../.tmp');
const backupDir = path.join(backupBaseDir, 'openAPI-backup');
// 创建备份目录
if (!fs.existsSync(backupBaseDir)) {
fs.mkdirSync(backupBaseDir, { recursive: true });
}
// 删除旧备份
if (fs.existsSync(backupDir)) {
fs.rmSync(backupDir, { recursive: true, force: true });
}
// 复制目录
copyDirectory(sourceDir, backupDir);
return backupDir;
}
/**
* 递归复制目录
* @param {string} src - 源路径
* @param {string} dest - 目标路径
*/
function copyDirectory(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
/**
* 对比新旧 API 变更
* @param {string} openAPIDir - OpenAPI 文档目录
*/
function compareAPIChanges(openAPIDir) {
const backupDir = path.resolve(__dirname, '../.tmp/openAPI-backup');
const tempDir = path.resolve(__dirname, '../.tmp/openAPI-temp');
// 检查是否存在临时备份(上一次的版本)
if (!fs.existsSync(tempDir)) {
console.log('ℹ️ 首次运行,已建立基线。下次运行将检测 API 变更。');
// 将当前备份移动到临时目录,作为下次对比的基线
if (fs.existsSync(backupDir)) {
fs.renameSync(backupDir, tempDir);
}
return;
}
// 扫描模块
const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
let hasChanges = false;
const moduleReports = [];
modules.forEach((moduleName) => {
const moduleDir = path.join(openAPIDir, moduleName);
const tempModuleDir = path.join(tempDir, moduleName);
// 如果临时备份中不存在该模块,说明是新增模块
if (!fs.existsSync(tempModuleDir)) {
console.log(`📦 新增模块: ${moduleName}`);
hasChanges = true;
return;
}
// 读取当前和临时备份的文档
const currentFiles = fs.readdirSync(moduleDir).filter(f => f.endsWith('.md'));
const tempFiles = fs.readdirSync(tempModuleDir).filter(f => f.endsWith('.md'));
// 检查是否有文件变更
const hasNewFiles = currentFiles.some(f => !tempFiles.includes(f));
const hasRemovedFiles = tempFiles.some(f => !currentFiles.includes(f));
const hasModifiedFiles = currentFiles.some(f => {
if (!tempFiles.includes(f)) return false;
const currentContent = fs.readFileSync(path.join(moduleDir, f), 'utf8');
const tempContent = fs.readFileSync(path.join(tempModuleDir, f), 'utf8');
return currentContent !== tempContent;
});
if (hasNewFiles || hasRemovedFiles || hasModifiedFiles) {
hasChanges = true;
moduleReports.push({ moduleName, moduleDir, tempModuleDir });
}
});
// 检查删除的模块
const tempModules = fs.existsSync(tempDir)
? fs.readdirSync(tempDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name)
: [];
const deletedModules = tempModules.filter(m => !modules.includes(m));
if (deletedModules.length > 0) {
hasChanges = true;
console.log(`\n❌ 删除模块: ${deletedModules.join(', ')}`);
}
if (!hasChanges) {
console.log('✅ 未检测到 API 变更');
// 更新基线
if (fs.existsSync(backupDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
fs.renameSync(backupDir, tempDir);
}
return;
}
// 逐个模块对比
console.log('');
moduleReports.forEach(({ moduleName, moduleDir, tempModuleDir }) => {
try {
const oldDocs = parseOpenAPIPath(tempModuleDir);
const newDocs = parseOpenAPIPath(moduleDir);
const report = generateReport(oldDocs, newDocs, 'text');
console.log(report);
console.log('');
} catch (error) {
console.error(`⚠️ 模块 ${moduleName} 对比失败: ${error.message}`);
}
});
// 更新基线:将当前备份作为下次对比的基准
console.log('📝 更新 API 基线...');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
if (fs.existsSync(backupDir)) {
fs.renameSync(backupDir, tempDir);
}
}
// 执行生成
const openAPIDir = path.resolve(__dirname, '../../../../docs/api-specs');
const outputDir = path.resolve(__dirname, '../../../../src/api');
console.log('=== OpenAPI 转 API 文档生成器 ===\n');
console.log(`输入目录: ${openAPIDir}`);
console.log(`输出目录: ${outputDir}\n`);
// 备份当前的 OpenAPI 文档(用于下次对比)
if (fs.existsSync(openAPIDir)) {
console.log('💾 备份当前 OpenAPI 文档...');
backupOpenAPIDir(openAPIDir);
console.log('');
}
scanAndGenerate(openAPIDir, outputDir);
#!/bin/bash
# API Generator Skill 安装脚本
# 用于将 API Generator 功能安装到当前项目
set -e
echo "🚀 API Generator Skill 安装程序"
echo "================================"
echo ""
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# 检查是否在项目根目录
if [ ! -f "package.json" ]; then
echo -e "${RED}❌ 错误: 请在项目根目录运行此脚本${NC}"
exit 1
fi
# 检查 package.json 中是否已有 api:generate 命令
if grep -q '"api:generate"' package.json; then
echo -e "${YELLOW}⚠️ 检测到已存在 api:generate 命令${NC}"
read -p "是否覆盖? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "安装已取消"
exit 0
fi
fi
# 创建目录结构
echo -e "${GREEN}📁 创建目录结构...${NC}"
mkdir -p .claude/custom_skills/api-generator/{scripts,templates,setup}
mkdir -p docs/openAPI
mkdir -p .tmp
# 检查必要的文件
echo -e "${GREEN}📋 检查文件...${NC}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 检查脚本文件
if [ ! -f "$SCRIPT_DIR/../scripts/generateApiFromOpenAPI.js" ]; then
echo -e "${RED}❌ 缺少 generateApiFromOpenAPI.js${NC}"
exit 1
fi
if [ ! -f "$SCRIPT_DIR/../scripts/apiDiff.js" ]; then
echo -e "${RED}❌ 缺少 apiDiff.js${NC}"
exit 1
fi
# 检查模板文件
if [ ! -f "$SCRIPT_DIR/../templates/openAPI-template.md" ]; then
echo -e "${RED}❌ 缺少 openAPI-template.md${NC}"
exit 1
fi
# 安装依赖
echo -e "${GREEN}📦 检查依赖...${NC}"
if ! command -v pnpm &> /dev/null; then
echo -e "${YELLOW}⚠️ pnpm 未安装,尝试使用 npm...${NC}"
PKG_MANAGER="npm"
else
PKG_MANAGER="pnpm"
fi
# 检查 js-yaml 是否已安装
if ! $PKG_MANAGER list js-yaml &> /dev/null; then
echo -e "${GREEN}📦 安装 js-yaml...${NC}"
$PKG_MANAGER add -D js-yaml
else
echo -e "${GREEN}✅ js-yaml 已安装${NC}"
fi
# 添加 npm scripts
echo -e "${GREEN}🔧 配置 npm scripts...${NC}"
# 使用 jq 或临时文件添加 scripts
if command -v jq &> /dev/null; then
jq '.scripts."api:generate" = "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js" |
.scripts."api:diff" = "node .claude/custom_skills/api-generator/scripts/apiDiff.js"' package.json > package.json.tmp
mv package.json.tmp package.json
else
# 使用 sed 添加(更兼容)
if ! grep -q '"api:generate"' package.json; then
# 找到 "scripts" 行并在后面插入
sed -i '' '/"scripts":/a\
\ "api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",\
\ "api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js",
' package.json 2>/dev/null || sed -i '/"scripts":/a\
\ "api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",\
\ "api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js",
' package.json
fi
fi
# 创建示例文档
echo -e "${GREEN}📝 创建示例文档...${NC}"
mkdir -p docs/openAPI/example
cat > docs/openAPI/example/getExample.md << 'EOF'
# 获取示例数据
## OpenAPI Specification
```yaml
openapi: 3.0.1
info:
title: ''
version: 1.0.0
paths:
/srv/:
get:
summary: 获取示例数据
description: 这是一个示例接口,展示如何编写 OpenAPI 文档
tags:
- 示例模块
parameters:
- name: a
in: query
description: action 参数
required: false
example: example_data
schema:
type: string
- name: id
in: query
description: 数据ID
required: true
example: 123
schema:
type: integer
responses:
'200':
description: 成功返回
content:
application/json:
schema:
type: object
properties:
code:
type: integer
description: 0=失败,1=成功
msg:
type: string
description: 错误信息
data:
type: object
properties:
id:
type: integer
description: 数据ID
name:
type: string
description: 名称
created_at:
type: string
description: 创建时间
```
EOF
# 创建 README
cat > docs/openAPI/README.md << 'EOF'
# OpenAPI 文档目录
本目录用于存放 OpenAPI 规范的接口文档,这些文档将自动转换为前端 API 调用代码。
## 目录结构
```
docs/openAPI/
├── example/ # 示例模块
│ └── getExample.md # 示例接口
├── user/ # 用户模块(你的模块)
├── course/ # 课程模块(你的模块)
└── order/ # 订单模块(你的模块)
```
## 如何添加新接口
1. **创建模块目录**(如果不存在)
```bash
mkdir -p docs/openAPI/yourModule
```
2. **创建接口文档**
```bash
# 使用模板创建
cp .claude/custom_skills/api-generator/templates/openAPI-template.md \
docs/openAPI/yourModule/yourApiName.md
```
3. **编辑文档**
- 按照模板填写接口信息
- 遵循 OpenAPI 3.0.1 规范
- 添加详细的参数说明和返回值结构
4. **生成代码**
```bash
pnpm api:generate
```
5. **使用生成的 API**
```javascript
import { yourApiNameAPI } from '@/api/yourModule'
```
## 命令速查
```bash
# 生成 API 代码
pnpm api:generate
# 对比 API 变更
pnpm api:diff docs/openAPI/user/ docs/openAPI/user-new/
# 查看帮助
cat .claude/custom_skills/api-generator/skill.md
```
## 注意事项
- 第一级目录名 = 模块名(会生成 `模块名.js`)
- 第二级文件名 = 接口名(会生成 `接口名API` 函数)
- 所有 `.md` 文件必须包含 YAML 代码块
- 遵循 OpenAPI 3.0.1 规范编写 YAML
## 参考文档
详细使用说明请参考:[API Generator Skill 文档](../../.claude/custom_skills/api-generator/skill.md)
EOF
# 完成
echo ""
echo -e "${GREEN}✅ 安装完成!${NC}"
echo ""
echo "📚 下一步:"
echo " 1. 查看示例文档: cat docs/openAPI/example/getExample.md"
echo " 2. 创建你的第一个接口: cp docs/openAPI/example/getExample.md docs/openAPI/yourModule/yourApi.md"
echo " 3. 生成 API 代码: pnpm api:generate"
echo ""
echo "📖 完整文档: cat .claude/custom_skills/api-generator/skill.md"
echo ""
# API Generator Skill
自动从 OpenAPI 文档生成前端 API 调用代码的完整解决方案。
## 功能特性
### 核心功能
1. **自动生成 API 代码** - 从 OpenAPI YAML 文档生成标准的 API 调用函数
2. **变更检测** - 自动对比 API 变更,识别破坏性变更
3. **增量更新** - 智能备份和基线管理,只检测实际变更
4. **JSDoc 注释** - 自动生成完整的类型注释和参数说明
### 生成内容
- **API 常量** - 帕斯卡命名的端点常量(如 `GET_USER_INFO`
- **API 函数** - 驼峰命名的调用函数(如 `getUserInfoAPI`
- **完整 JSDoc** - 参数类型、返回值结构、嵌套对象说明
## 快速开始
### 第一次使用
1. **创建 OpenAPI 文档**
```bash
# 在 docs/api-specs/ 创建模块目录
mkdir -p docs/api-specs/user
# 创建接口文档(使用下方模板)
cp .claude/custom_skills/api-generator/templates/api-specs-template.md docs/api-specs/user/getUserInfo.md
```
2. **编辑 OpenAPI 文档**
按照 OpenAPI 3.0.1 规范编辑 YAML 代码块:
```markdown
# 获取用户信息
## OpenAPI Specification
```yaml
openapi: 3.0.1
info:
title: ''
version: 1.0.0
paths:
/srv/:
get:
summary: 获取用户信息
parameters:
- name: a
in: query
example: user_info
responses:
'200':
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: object
```
3. **生成 API 代码**
```bash
# 安装依赖(首次)
pnpm add -D js-yaml
# 生成 API 代码
pnpm api:generate
```
4. **使用生成的 API**
```javascript
import { getUserInfoAPI } from '@/api/user'
const { code, data } = await getUserInfoAPI({ id: 123 })
if (code === 1) {
console.log(data)
}
```
## 依赖安装
### 必需依赖
```bash
pnpm add -D js-yaml
```
### package.json 配置
```json
{
"scripts": {
"api:generate": "node .claude/custom_skills/api-generator/scripts/generateApiFromOpenAPI.js",
"api:diff": "node .claude/custom_skills/api-generator/scripts/apiDiff.js"
}
}
```
## 目录结构
```
docs/api-specs/
├── user/ # 用户模块
│ ├── getUserInfo.md # 获取用户信息
│ └── editUserInfo.md # 编辑用户信息
├── course/ # 课程模块
│ ├── getList.md # 获取课程列表
│ └── getDetail.md # 获取课程详情
└── order/ # 订单模块
└── getList.md # 获取订单列表
```
**重要规则**:
- 第一级目录 = 模块名(生成 `模块名.js` 文件)
- 第二级文件 = 接口名(生成 `接口名API` 函数)
## OpenAPI 文档规范
### 基本结构
每个 `.md` 文件必须包含一个 YAML 代码块:
```markdown
# 接口标题
## OpenAPI Specification
```yaml
openapi: 3.0.1
info:
title: ''
version: 1.0.0
paths:
/srv/:
get: # 或 post
summary: 接口简介
parameters: # GET 请求参数
- name: a
in: query
example: action_name
responses:
'200':
content:
application/json:
schema:
type: object
properties:
code:
type: integer
data:
type: any
```
### GET 请求示例
```yaml
paths:
/srv/:
get:
summary: 获取课程列表
parameters:
- name: a
in: query
example: course_list
- name: page
in: query
description: 页码
required: true
schema:
type: integer
- name: limit
in: query
description: 每页数量
schema:
type: integer
```
### POST 请求示例
```yaml
paths:
/srv/:
post:
summary: 创建订单
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
required:
- course_id
properties:
course_id:
type: integer
description: 课程ID
quantity:
type: integer
description: 数量
```
### 响应结构示例
```yaml
responses:
'200':
content:
application/json:
schema:
type: object
properties:
code:
type: integer
description: 0=失败,1=成功
msg:
type: string
description: 错误信息
data:
type: object
properties:
user:
type: object
properties:
id:
type: integer
name:
type: string
items:
type: array
items:
type: object
properties:
id:
type: integer
title:
type: string
```
## 生成的代码示例
### 输入:`docs/api-specs/user/getUserInfo.md`
```yaml
paths:
/srv/:
get:
summary: 获取用户信息
parameters:
- name: a
example: user_info
- name: id
description: 用户ID
required: true
```
### 输出:`src/api/user.js`
```javascript
import { fn, fetch } from '@/api/fn';
const Api = {
GetUserInfo: '/srv/?a=user_info',
}
/**
* @description: 获取用户信息
* @param {Object} params 请求参数
* @param {integer} params.id 用户ID
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: any;
* }>}
*/
export const getUserInfoAPI = (params) => fn(fetch.get(Api.GetUserInfo, params));
```
## 变更检测
### 自动变更检测
每次运行 `pnpm api:generate` 时会自动:
1. 备份当前文档到 `/.tmp/openAPI-backup`
2. 与上次版本对比(保存在 `/.tmp/openAPI-temp`
3. 生成变更报告
### 变更报告示例
```
🔍 开始检测 API 变更...
=== API 变更检测报告 ===
📦 对比范围: 5 个旧接口 → 6 个新接口
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 新增接口 (1):
+ getUserProfile
⚠️ 修改接口 (2):
↪ editUserInfo
✗ [破坏性] 删除必填参数: sms_code
✓ [非破坏性] 新增可选参数: avatar
↪ getUserInfo
✓ [非破坏性] 新增可选参数: include_profile
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计: 1 新增, 2 修改, 0 删除
⚠️ 检测到 1 个破坏性变更,请仔细检查业务逻辑!
```
### 手动对比
```bash
# 对比两个目录
pnpm api:diff docs/api-specs/user/ docs/api-specs/user-new/
# 对比两个文件
pnpm api:diff docs/api-specs/user/getInfo.md docs/api-specs/user/getInfo-v2.md
# 输出 JSON 格式
API_DIFF_FORMAT=json pnpm api:diff ...
```
## 高级配置
### 环境变量
```bash
# 严格模式:任何变更都返回失败码
API_DIFF_STRICT=true pnpm api:generate
# JSON 格式输出
API_DIFF_FORMAT=json pnpm api:diff ...
```
### 自定义输出路径
编辑 `scripts/generateApiFromOpenAPI.js` 底部:
```javascript
// 修改输入/输出路径
const openAPIDir = path.resolve(__dirname, '../../docs/api-specs');
const outputDir = path.resolve(__dirname, '../../src/api');
```
## 跨项目使用
### 方法 1:复制整个 skill
```bash
# 复制到其他项目
cp -r .claude/custom_skills/api-generator /other/project/.claude/custom_skills/
# 复制 npm 脚本
# 在其他项目的 package.json 中添加 scripts
# 安装依赖
cd /other/project
pnpm add -D js-yaml
```
### 方法 2:使用安装脚本
```bash
# 在目标项目中运行
bash .claude/custom_skills/api-generator/setup/install.sh
```
## 工作流程
### 开发新接口
1. **创建文档** - 在 `docs/api-specs/模块名/接口名.md`
2. **编写规范** - 按照 OpenAPI 3.0.1 规范编写 YAML
3. **生成代码** - 运行 `pnpm api:generate`
4. **检查变更** - 查看变更报告,确认没有破坏性变更
5. **使用代码** - 在组件中导入并使用生成的 API 函数
### 修改接口
1. **更新文档** - 修改对应的 `.md` 文件
2. **生成代码** - 运行 `pnpm api:generate`
3. **检查影响** - 查看变更报告,评估影响范围
4. **更新业务** - 根据变更类型更新业务代码
- 破坏性变更:必须修改业务代码
- 非破坏性变更:可选修改
### 删除接口
1. **删除文档** - 删除对应的 `.md` 文件
2. **生成代码** - 运行 `pnpm api:generate`
3. **检查引用** - 全局搜索被删除的 API 函数,移除引用
## 常见问题
### Q: 生成代码后报错找不到模块?
**A**: 检查以下几点:
1. 确认 `src/api/fn.js` 存在且导出 `fn``fetch`
2. 确认路径别名 `@/` 正确配置
3. 确认生成的文件在 `src/api/` 目录下
### Q: 如何处理需要认证的接口?
**A**: 生成的代码会自动使用 `src/utils/axios.js` 中的拦截器,自动添加认证头。无需特殊处理。
### Q: 如何处理不同的请求方法?
**A**: 脚本会自动识别:
- `get``fetch.get()`
- `post``fetch.post()`
- `application/x-www-form-urlencoded``fetch.stringifyPost()`
### Q: 生成的代码格式不统一?
**A**: 运行 `pnpm lint` 自动格式化生成的代码。
### Q: 如何复用已有的 API 定义?
**A**: 可以在 YAML 中使用 `$ref` 引用,但当前版本暂不支持,建议直接展开定义。
## 最佳实践
### 1. 文档命名规范
- **模块目录**: 小写,多个单词用下划线(`user_profile`
- **接口文件**: 小写,动词开头(`getUserInfo.md`
### 2. 接口分组
- 按业务模块分组(`user`, `course`, `order`
- 避免单个模块过大(建议 < 20 个接口)
- 相关接口放在同一模块
### 3. 版本管理
-`docs/api-specs/` 纳入版本控制
- 生成的 `src/api/*.js` 也纳入版本控制
- 保留变更历史,方便回滚
### 4. 团队协作
- 前后端共同维护 OpenAPI 文档
- 接口变更前先更新文档
- 使用变更报告评估影响
- 破坏性变更必须通知团队
### 5. 文档注释
- **summary**: 简短描述(1行)
- **description**: 详细说明(可选)
- **参数**: 每个参数都应有 description
- **响应**: 嵌套对象也应有说明
## 故障排除
### 问题:YAML 解析失败
**错误信息**: `解析 OpenAPI 文档失败: YAMLException`
**解决方案**:
1. 检查 YAML 缩进(必须使用空格,不能用 Tab)
2. 检查 YAML 语法(使用在线验证器)
3. 确保所有字符串正确引号包裹
### 问题:生成的函数名不符合预期
**原因**: 文件名包含特殊字符
**解决方案**:
- 使用小写字母和数字
- 多个单词用下划线分隔
- 避免使用中划线(会转换为驼峰)
### 问题:变更检测不准确
**原因**: 首次运行或备份文件损坏
**解决方案**:
```bash
# 清理备份
rm -rf .tmp/openAPI-*
# 重新建立基线
pnpm api:generate
```
## 相关资源
- [OpenAPI 3.0 规范](https://swagger.io/specification/)
- [YAML 语法指南](https://yaml.org/spec/1.2/spec.html)
- [API 设计最佳实践](https://github.com/microsoft/api-guidelines)
## 更新日志
### v1.0.0 (2026-01-29)
- ✨ 初始版本
- ✅ 支持 GET/POST 请求
- ✅ 自动生成 JSDoc 注释
- ✅ API 变更检测
- ✅ 增量更新机制
- ✅ 跨项目支持
# 接口名称
## 接口描述
详细描述这个接口的功能、使用场景和注意事项。
## OpenAPI Specification
```yaml
openapi: 3.0.1
info:
title: ''
version: 1.0.0
paths:
/srv/:
get: # 或 post
summary: 接口简介(一行描述)
description: |
接口详细说明...
- 使用场景 1
- 使用场景 2
tags:
- 模块名称
parameters: # GET 请求使用 parameters
- name: a
in: query
description: action 参数
required: false
example: your_action_name
schema:
type: string
- name: f
in: query
description: 业务模块
required: false
example: behalo
schema:
type: string
- name: id
in: query
description: 参数描述
required: true # true=必填,false=可选
example: 123
schema:
type: integer
requestBody: # POST 请求使用 requestBody
content:
application/x-www-form-urlencoded: # 或 application/json
schema:
type: object
required:
- course_id
- quantity
properties:
course_id:
type: integer
description: 课程ID
example: 1
quantity:
type: integer
description: 数量
example: 1
responses:
'200':
description: 成功返回
content:
application/json:
schema:
type: object
properties:
code:
type: integer
description: 0=失败,1=成功
msg:
type: string
description: 错误信息
data:
type: object
description: 返回数据
properties:
id:
type: integer
description: 数据ID
name:
type: string
description: 名称
items:
type: array
description: 列表数据
items:
type: object
properties:
item_id:
type: integer
description: 项目ID
item_name:
type: string
description: 项目名称
```
## 使用示例
### GET 请求示例
```javascript
import { getYourAPINameAPI } from '@/api/yourModule'
const { code, data } = await getYourAPINameAPI({
id: 123,
page: 1,
limit: 10
})
if (code === 1) {
console.log('成功:', data)
}
```
### POST 请求示例
```javascript
import { createYourResourceAPI } from '@/api/yourModule'
const { code, data } = await createYourResourceAPI({
course_id: 1,
quantity: 2
})
if (code === 1) {
console.log('创建成功:', data)
}
```
## 注意事项
- **权限要求**: 说明是否需要登录、特殊权限等
- **限流规则**: 说明接口调用频率限制
- **错误码**: 列出常见的错误码及含义
- **兼容性**: 说明版本兼容性要求
## 相关接口
- [相关接口1](./relatedApi1.md)
- [相关接口2](./relatedApi2.md)
## 更新记录
- **2026-01-29**: 初始版本,创建接口
{
"packageManager": "pnpm"
}
{
"mcpServers": {
"mlaj_API_文档": {
"command": "npx",
"args": [
"-y",
"apifox-mcp-server@latest",
"--project-id=6084040"
],
"env": {
"APIFOX_ACCESS_TOKEN": "APS-jkT1Q61MCKgzgvfCL2euIR2TcgKsnSyc"
}
}
}
}
{
"permissions": {
"allow": [
"Bash(pandoc:*)",
"Bash(npx skills --help:*)",
"Bash(pnpm add:*)",
"Bash(pnpm api:generate:*)",
"Bash(node:*)",
"Bash(ls:*)",
"Bash(tree:*)",
"Bash(git checkout:*)",
"Bash(xargs awk:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(git diff:*)",
"mcp__web-search-prime__webSearchPrime",
"mcp__web-reader__webReader",
"Bash(ls:*)",
"Bash(for:*)",
"Bash(do if [ -f \"$dirskill.md\" ])",
"Bash(fi:*)",
"Bash(done:*)",
"Bash(if:*)",
"Bash(then echo \"找到项目本地技能:\")",
"Bash(else echo \"当前项目没有本地技能目录\")",
"Bash(command:*)",
"Bash(then mv \"$dirskill.md\" \"$dirSKILL.md\")",
"Bash(do if [ -f \"$dirSKILL.md\" ])",
"Bash(then rmdir ~/.config/claude-code/skills)",
"Bash(else echo \"目录不为空或不存在,跳过删除\")",
"Bash(chmod:*)",
"Bash(scripts/upload-to-qiniu.sh:*)",
"Bash(curl:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push)",
"Bash(git restore:*)",
"Bash(pnpm add:*)",
"Bash(pnpm lint:check:*)",
"Bash(pnpm format:check:*)",
"Bash(pnpm list:*)",
"Bash(npx husky init:*)",
"Bash(npx lint-staged:*)",
"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/)",
"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)",
"Bash(npx playwright install:*)",
"Bash(git push:*)",
"Bash(find:*)",
"mcp__web-search-prime__webSearchPrime",
"mcp__web-reader__webReader"
"Bash(pnpm test:e2e:*)",
"mcp__zai-mcp-server__ui_to_artifact",
"mcp__zai-mcp-server__extract_text_from_screenshot",
"mcp__zai-mcp-server__analyze_image",
"Bash(git mv:*)",
"Bash(yarn api:generate:*)",
"Bash(./test-mcp.sh:*)",
"Bash(npx:*)",
"Bash(APIFOX_ACCESS_TOKEN=\"APS-jkT1Q61MCKgzgvfCL2euIR2TcgKsnSyc\" npx -y apifox-mcp-server@latest:*)",
"Bash(git checkout:*)"
]
}
}
......