feat(api-generator): 新增 API 变更自动检测功能
新增独立的 API 对比工具,在生成 API 文档时自动检测接口变更并识别破坏性变更。 主要特性: - 自动对比新旧 OpenAPI 文档,检测接口增删改 - 智能识别破坏性变更(新增必填参数、删除参数、类型变更等) - 分类展示破坏性/非破坏性变更,便于业务代码审查 - 支持独立调用和集成调用两种模式 - 提供文本和 JSON 两种输出格式 - 双基线机制自动管理版本对比 技术实现: - 新增 apiDiff.js 核心对比脚本,支持 GET/POST 参数对比 - 集成到 generateApiFromOpenAPI.js,自动触发检测 - 新增 api-diff skill,可独立使用或 CI/CD 集成 - 详细的 API_DIFF_GUIDE.md 使用文档 使用方式: - 自动检测:node scripts/generateApiFromOpenAPI.js - 手动对比:node scripts/apiDiff.js <oldPath> <newPath> Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
6 changed files
with
546 additions
and
1 deletions
.claude/custom_skills/api-diff/skill.md
0 → 100644
| 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/openAPI/user/api1.md docs/openAPI/user/api1-new.md | ||
| 23 | + | ||
| 24 | +# 对比整个模块目录 | ||
| 25 | +node scripts/apiDiff.js docs/openAPI/user/ docs/openAPI/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. 删除接口前请确认没有地方在使用 |
docs/API_DIFF_GUIDE.md
0 → 100644
| 1 | +# API 变更检测工具使用指南 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +API 变更检测工具是 OpenAPI 转 API 文档生成器的增强功能,可以自动检测接口变更并识别破坏性变更,帮助开发者在更新接口时及时发现潜在问题。 | ||
| 6 | + | ||
| 7 | +## 工作原理 | ||
| 8 | + | ||
| 9 | +1. **基线建立**:首次运行时自动建立 API 基线 | ||
| 10 | +2. **变更检测**:后续运行时对比当前版本与基线版本 | ||
| 11 | +3. **分类标记**:自动区分破坏性变更和非破坏性变更 | ||
| 12 | +4. **自动更新**:检测完成后自动更新基线 | ||
| 13 | + | ||
| 14 | +## 使用方式 | ||
| 15 | + | ||
| 16 | +### 自动检测(推荐) | ||
| 17 | + | ||
| 18 | +每次运行 API 生成器时自动检测变更: | ||
| 19 | + | ||
| 20 | +```bash | ||
| 21 | +node scripts/generateApiFromOpenAPI.js | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +输出示例: | ||
| 25 | + | ||
| 26 | +``` | ||
| 27 | +=== OpenAPI 转 API 文档生成器 === | ||
| 28 | + | ||
| 29 | +输入目录: /Users/huyirui/program/itomix/git/manulife-weapp/docs/openAPI | ||
| 30 | +输出目录: /Users/huyirui/program/itomix/git/manulife-weapp/src/api | ||
| 31 | + | ||
| 32 | +💾 备份当前 OpenAPI 文档... | ||
| 33 | + | ||
| 34 | +找到 2 个模块: order, user | ||
| 35 | + | ||
| 36 | +处理模块: order | ||
| 37 | +找到 2 个 API 文档 | ||
| 38 | + ✓ getDetail: 获取订单详情 | ||
| 39 | + ✓ getList: 获取订单列表 | ||
| 40 | + 📝 生成文件: /Users/huyirui/program/itomix/git/manulife-weapp/src/api/order.js | ||
| 41 | + | ||
| 42 | +处理模块: user | ||
| 43 | +找到 2 个 API 文档 | ||
| 44 | + ✓ editUserInfo: 修改我的信息 | ||
| 45 | + ✓ getUserInfo: 查询我的信息 | ||
| 46 | + 📝 生成文件: /Users/huyirui/program/itomix/git/manulife-weapp/src/api/user.js | ||
| 47 | + | ||
| 48 | +✅ API 文档生成完成! | ||
| 49 | + | ||
| 50 | +🔍 开始检测 API 变更... | ||
| 51 | + | ||
| 52 | + | ||
| 53 | +=== API 变更检测报告 === | ||
| 54 | + | ||
| 55 | +📦 对比范围: 2 个旧接口 → 2 个新接口 | ||
| 56 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 57 | + | ||
| 58 | +⚠️ 修改接口 (1): | ||
| 59 | + ↪ getUserInfo - 查询我的信息 | ||
| 60 | + ✓ [非破坏性] 新增可选 query 参数: include_profile | ||
| 61 | + | ||
| 62 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 63 | +总计: 0 新增, 1 修改, 0 删除 | ||
| 64 | +✅ 未检测到破坏性变更 | ||
| 65 | + | ||
| 66 | +📝 更新 API 基线... | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +### 手动检测 | ||
| 70 | + | ||
| 71 | +可以独立运行对比脚本: | ||
| 72 | + | ||
| 73 | +```bash | ||
| 74 | +# 对比两个 OpenAPI 文档 | ||
| 75 | +node scripts/apiDiff.js docs/openAPI/user/getUserInfo.md docs/openAPI/user/getUserInfo-new.md | ||
| 76 | + | ||
| 77 | +# 对比两个模块目录 | ||
| 78 | +node scripts/apiDiff.js docs/openAPI/user/ docs/openAPI/user-new/ | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +## 变更类型说明 | ||
| 82 | + | ||
| 83 | +### 非破坏性变更(✓) | ||
| 84 | + | ||
| 85 | +这些变更不会影响现有代码: | ||
| 86 | + | ||
| 87 | +- **新增可选参数**:客户端可以不传,不影响现有调用 | ||
| 88 | +- **删除可选参数**:服务端不再接收,但客户端不传也没问题 | ||
| 89 | +- **必填参数变可选**:限制放宽,兼容性提升 | ||
| 90 | +- **返回值新增字段**:客户端可以选择性使用 | ||
| 91 | + | ||
| 92 | +### 破坏性变更(✗) | ||
| 93 | + | ||
| 94 | +这些变更需要检查业务逻辑: | ||
| 95 | + | ||
| 96 | +- **新增必填参数**:现有调用会缺少参数 | ||
| 97 | +- **删除必填参数**:现有调用可能传了被删除的参数 | ||
| 98 | +- **可选参数变必填**:现有调用可能不传该参数 | ||
| 99 | +- **参数类型变更**:可能导致类型错误 | ||
| 100 | +- **HTTP 方法变更**:GET ↔ POST 会导致调用失败 | ||
| 101 | +- **删除接口**:所有调用该接口的地方需要修改 | ||
| 102 | + | ||
| 103 | +## 输出格式 | ||
| 104 | + | ||
| 105 | +### 文本格式(默认) | ||
| 106 | + | ||
| 107 | +``` | ||
| 108 | +=== API 变更检测报告 === | ||
| 109 | + | ||
| 110 | +📦 对比范围: 2 个旧接口 → 2 个新接口 | ||
| 111 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 112 | + | ||
| 113 | +✅ 新增接口 (1): | ||
| 114 | + + getUserProfile - 获取用户详细资料 | ||
| 115 | + | ||
| 116 | +⚠️ 修改接口 (2): | ||
| 117 | + ↪ editUserInfo - 修改我的信息 | ||
| 118 | + ✗ [破坏性] 删除必填 body 参数: sms_code | ||
| 119 | + ✓ [非破坏性] 新增可选 body 参数: avatar | ||
| 120 | + | ||
| 121 | + ↪ getUserInfo - 查询我的信息 | ||
| 122 | + ✓ [非破坏性] 新增可选 query 参数: include_profile | ||
| 123 | + | ||
| 124 | +❌ 删除接口 (1): | ||
| 125 | + - deleteUserAccount - 删除用户账号 | ||
| 126 | + | ||
| 127 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 128 | +总计: 1 新增, 2 修改, 1 删除 | ||
| 129 | +⚠️ 检测到 1 个破坏性变更,请仔细检查业务逻辑! | ||
| 130 | +``` | ||
| 131 | + | ||
| 132 | +### JSON 格式 | ||
| 133 | + | ||
| 134 | +设置环境变量输出 JSON 格式(便于程序解析): | ||
| 135 | + | ||
| 136 | +```bash | ||
| 137 | +API_DIFF_FORMAT=json node scripts/apiDiff.js <oldPath> <newPath> | ||
| 138 | +``` | ||
| 139 | + | ||
| 140 | +输出示例: | ||
| 141 | + | ||
| 142 | +```json | ||
| 143 | +{ | ||
| 144 | + "summary": { | ||
| 145 | + "added": 1, | ||
| 146 | + "modified": 2, | ||
| 147 | + "removed": 1, | ||
| 148 | + "breakingChanges": 1 | ||
| 149 | + }, | ||
| 150 | + "added": [ | ||
| 151 | + { | ||
| 152 | + "name": "getUserProfile", | ||
| 153 | + "summary": "获取用户详细资料", | ||
| 154 | + "method": "GET", | ||
| 155 | + "path": "/srv/" | ||
| 156 | + } | ||
| 157 | + ], | ||
| 158 | + "modified": [ | ||
| 159 | + { | ||
| 160 | + "name": "editUserInfo", | ||
| 161 | + "summary": "修改我的信息", | ||
| 162 | + "breakingChanges": [ | ||
| 163 | + "删除必填 body 参数: sms_code" | ||
| 164 | + ], | ||
| 165 | + "nonBreakingChanges": [ | ||
| 166 | + "新增可选 body 参数: avatar" | ||
| 167 | + ] | ||
| 168 | + } | ||
| 169 | + ], | ||
| 170 | + "removed": [ | ||
| 171 | + { | ||
| 172 | + "name": "deleteUserAccount", | ||
| 173 | + "summary": "删除用户账号", | ||
| 174 | + "method": "POST", | ||
| 175 | + "path": "/srv/" | ||
| 176 | + } | ||
| 177 | + ] | ||
| 178 | +} | ||
| 179 | +``` | ||
| 180 | + | ||
| 181 | +## CI/CD 集成 | ||
| 182 | + | ||
| 183 | +### 严格模式 | ||
| 184 | + | ||
| 185 | +任何变更都返回失败码: | ||
| 186 | + | ||
| 187 | +```bash | ||
| 188 | +API_DIFF_STRICT=true node scripts/apiDiff.js <oldPath> <newPath> | ||
| 189 | +``` | ||
| 190 | + | ||
| 191 | +### 检查退出码 | ||
| 192 | + | ||
| 193 | +```bash | ||
| 194 | +node scripts/apiDiff.js <oldPath> <newPath> | ||
| 195 | +EXIT_CODE=$? | ||
| 196 | + | ||
| 197 | +if [ $EXIT_CODE -eq 1 ]; then | ||
| 198 | + echo "检测到破坏性变更或启用严格模式" | ||
| 199 | + exit 1 | ||
| 200 | +fi | ||
| 201 | +``` | ||
| 202 | + | ||
| 203 | +退出码: | ||
| 204 | +- `0`: 无破坏性变更 | ||
| 205 | +- `1`: 检测到破坏性变更或严格模式下有任何变更 | ||
| 206 | + | ||
| 207 | +## 配置选项 | ||
| 208 | + | ||
| 209 | +| 环境变量 | 说明 | 默认值 | | ||
| 210 | +|---------|------|--------| | ||
| 211 | +| `API_DIFF_FORMAT` | 输出格式 (text/json) | text | | ||
| 212 | +| `API_DIFF_STRICT` | 严格模式 (true/false) | false | | ||
| 213 | + | ||
| 214 | +## 存储位置 | ||
| 215 | + | ||
| 216 | +- **基线文件**:`.tmp/openAPI-temp/` | ||
| 217 | +- **临时备份**:`.tmp/openAPI-backup/` | ||
| 218 | + | ||
| 219 | +这些目录已添加到 `.gitignore`,不会被提交到 git。 | ||
| 220 | + | ||
| 221 | +## 常见问题 | ||
| 222 | + | ||
| 223 | +### Q: 首次运行显示"首次运行,已建立基线"? | ||
| 224 | + | ||
| 225 | +A: 这是正常的。首次运行时会建立 API 基线,从第二次开始才会检测变更。 | ||
| 226 | + | ||
| 227 | +### Q: 如何重置基线? | ||
| 228 | + | ||
| 229 | +A: 删除 `.tmp` 目录即可: | ||
| 230 | + | ||
| 231 | +```bash | ||
| 232 | +rm -rf .tmp | ||
| 233 | +``` | ||
| 234 | + | ||
| 235 | +### Q: 检测到破坏性变更怎么办? | ||
| 236 | + | ||
| 237 | +A: | ||
| 238 | +1. 仔细检查变更内容 | ||
| 239 | +2. 确认是否真的需要这个变更 | ||
| 240 | +3. 如果需要,搜索所有调用该接口的代码并更新 | ||
| 241 | +4. 如果是第三方接口变更,联系后端确认 | ||
| 242 | + | ||
| 243 | +### Q: 可以对比生成的 JS 文件吗? | ||
| 244 | + | ||
| 245 | +A: 当前版本不支持对比生成的 JS 文件,请对比 OpenAPI 文档。 | ||
| 246 | + | ||
| 247 | +### Q: 为什么有些参数没有检测到变更? | ||
| 248 | + | ||
| 249 | +A: 工具会自动过滤 `a` 和 `f` 参数(业务标识符),专注于业务参数。 | ||
| 250 | + | ||
| 251 | +## 最佳实践 | ||
| 252 | + | ||
| 253 | +1. **每次更新接口后运行检测**:及时发现问题 | ||
| 254 | +2. **仔细阅读破坏性变更报告**:不要忽略警告 | ||
| 255 | +3. **在 CI/CD 中集成**:自动化检测流程 | ||
| 256 | +4. **定期审查非破坏性变更**:确保接口设计合理 | ||
| 257 | +5. **保留接口变更历史**:便于追溯问题 | ||
| 258 | + | ||
| 259 | +## 技术实现 | ||
| 260 | + | ||
| 261 | +### 核心文件 | ||
| 262 | + | ||
| 263 | +- `scripts/apiDiff.js` - 核心对比逻辑 | ||
| 264 | +- `scripts/generateApiFromOpenAPI.js` - 集成调用 | ||
| 265 | +- `.claude/custom_skills/api-diff/skill.md` - Skill 文档 | ||
| 266 | + | ||
| 267 | +### 对比维度 | ||
| 268 | + | ||
| 269 | +1. **接口级别**:新增、删除、修改 | ||
| 270 | +2. **参数级别**:名称、类型、是否必填 | ||
| 271 | +3. **HTTP 方法**:GET ↔ POST | ||
| 272 | +4. **返回值**:结构、字段类型 | ||
| 273 | + | ||
| 274 | +### 检测逻辑 | ||
| 275 | + | ||
| 276 | +- 基于参数名称和类型对比 | ||
| 277 | +- 智能识别必填/可选参数变化 | ||
| 278 | +- 自动分类破坏性/非破坏性变更 | ||
| 279 | +- 支持模块级别和接口级别对比 | ||
| 280 | + | ||
| 281 | +## 贡献 | ||
| 282 | + | ||
| 283 | +如果发现问题或有改进建议,欢迎提交 Issue 或 Pull Request。 |
scripts/apiDiff.js
0 → 100644
This diff is collapsed. Click to expand it.
| ... | @@ -24,6 +24,7 @@ | ... | @@ -24,6 +24,7 @@ |
| 24 | const fs = require('fs'); | 24 | const fs = require('fs'); |
| 25 | const path = require('path'); | 25 | const path = require('path'); |
| 26 | const yaml = require('js-yaml'); | 26 | const yaml = require('js-yaml'); |
| 27 | +const { generateReport, parseOpenAPIPath } = require('./apiDiff'); | ||
| 27 | 28 | ||
| 28 | /** | 29 | /** |
| 29 | * 提取 Markdown 文件中的 YAML 代码块 | 30 | * 提取 Markdown 文件中的 YAML 代码块 |
| ... | @@ -401,6 +402,164 @@ function scanAndGenerate(openAPIDir, outputDir) { | ... | @@ -401,6 +402,164 @@ function scanAndGenerate(openAPIDir, outputDir) { |
| 401 | }); | 402 | }); |
| 402 | 403 | ||
| 403 | console.log('\n✅ API 文档生成完成!'); | 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 | + } | ||
| 404 | } | 563 | } |
| 405 | 564 | ||
| 406 | // 执行生成 | 565 | // 执行生成 |
| ... | @@ -411,4 +570,11 @@ console.log('=== OpenAPI 转 API 文档生成器 ===\n'); | ... | @@ -411,4 +570,11 @@ console.log('=== OpenAPI 转 API 文档生成器 ===\n'); |
| 411 | console.log(`输入目录: ${openAPIDir}`); | 570 | console.log(`输入目录: ${openAPIDir}`); |
| 412 | console.log(`输出目录: ${outputDir}\n`); | 571 | console.log(`输出目录: ${outputDir}\n`); |
| 413 | 572 | ||
| 573 | +// 备份当前的 OpenAPI 文档(用于下次对比) | ||
| 574 | +if (fs.existsSync(openAPIDir)) { | ||
| 575 | + console.log('💾 备份当前 OpenAPI 文档...'); | ||
| 576 | + backupOpenAPIDir(openAPIDir); | ||
| 577 | + console.log(''); | ||
| 578 | +} | ||
| 579 | + | ||
| 414 | scanAndGenerate(openAPIDir, outputDir); | 580 | scanAndGenerate(openAPIDir, outputDir); | ... | ... |
| ... | @@ -11,7 +11,6 @@ const Api = { | ... | @@ -11,7 +11,6 @@ const Api = { |
| 11 | * @param {string} params.name (可选) 姓名 | 11 | * @param {string} params.name (可选) 姓名 |
| 12 | * @param {string} params.avatar (可选) 头像 | 12 | * @param {string} params.avatar (可选) 头像 |
| 13 | * @param {string} params.mobile (可选) 手机号 | 13 | * @param {string} params.mobile (可选) 手机号 |
| 14 | - * @param {string} params.sms_code (可选) 短信验证码 | ||
| 15 | * @param {string} params.idcard (可选) 身份证 | 14 | * @param {string} params.idcard (可选) 身份证 |
| 16 | * @returns {Promise<{ | 15 | * @returns {Promise<{ |
| 17 | * code: number; // 状态码 | 16 | * code: number; // 状态码 |
| ... | @@ -24,6 +23,7 @@ export const editUserInfoAPI = (params) => fn(fetch.post(Api.EditUserInfo, param | ... | @@ -24,6 +23,7 @@ export const editUserInfoAPI = (params) => fn(fetch.post(Api.EditUserInfo, param |
| 24 | /** | 23 | /** |
| 25 | * @description: 查询我的信息 | 24 | * @description: 查询我的信息 |
| 26 | * @param {Object} params 请求参数 | 25 | * @param {Object} params 请求参数 |
| 26 | + * @param {boolean} params.include_profile (可选) 是否包含完整资料信息 | ||
| 27 | * @returns {Promise<{ | 27 | * @returns {Promise<{ |
| 28 | * code: number; // 状态码 | 28 | * code: number; // 状态码 |
| 29 | * msg: string; // 消息 | 29 | * msg: string; // 消息 | ... | ... |
-
Please register or login to post a comment