feat(claude): 添加API生成器技能并更新Claude配置
添加完整的API生成器技能,包括从OpenAPI文档生成前端API代码的工具链 更新Claude配置以支持pnpm包管理器和MCP服务器 重构权限设置,优化开发工具链集成
Showing
9 changed files
with
1976 additions
and
105 deletions
| 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 "" |
.claude/custom_skills/api-generator/skill.md
0 → 100644
| 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**: 初始版本,创建接口 |
.claude/package-manager.json
0 → 100644
.claude/settings.json
0 → 100644
| 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 | } | ... | ... |
-
Please register or login to post a comment