hookehuyr

feat: 添加 API 生成脚本并更新打卡接口

- 新增 OpenAPI 转 API 生成脚本 (scripts/)
- 更新打卡 API 端点 (map → map_activity)
- 添加 .gitignore 规则忽略 CLAUDE.md 和 .claude/
- 新增 npm run api:generate 命令

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -14,3 +14,6 @@ cypress.json ...@@ -14,3 +14,6 @@ cypress.json
14 src/test 14 src/test
15 .idea 15 .idea
16 map 16 map
17 +
18 +CLAUDE.md
19 +.claude/
......
...@@ -28,7 +28,8 @@ ...@@ -28,7 +28,8 @@
28 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar && npm run remove_dist", 28 "dev_upload": "npm run build_tar && npm run scp-dev && npm run dec-dev && npm run remove_tar && npm run remove_dist",
29 "oa_upload": "npm run build_tar && npm run scp-oa && npm run dec-oa && npm run remove_tar", 29 "oa_upload": "npm run build_tar && npm run scp-oa && npm run dec-oa && npm run remove_tar",
30 "xys_upload": "npm run build_tar && npm run scp-xys && npm run dec-xys && npm run remove_tar", 30 "xys_upload": "npm run build_tar && npm run scp-xys && npm run dec-xys && npm run remove_tar",
31 - "walk_upload": "npm run build_tar && npm run scp-walk && npm run dec-walk && npm run remove_tar" 31 + "walk_upload": "npm run build_tar && npm run scp-walk && npm run dec-walk && npm run remove_tar",
32 + "api:generate": "node scripts/generateApiFromOpenAPI.js"
32 }, 33 },
33 "dependencies": { 34 "dependencies": {
34 "@amap/amap-jsapi-loader": "^1.0.1", 35 "@amap/amap-jsapi-loader": "^1.0.1",
......
1 +/**
2 + * API 对比工具
3 + *
4 + * 功能:
5 + * 1. 对比两个 OpenAPI 文档的差异
6 + * 2. 检测破坏性变更
7 + * 3. 生成详细的变更报告
8 + *
9 + * 使用方式:
10 + * node 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 scripts/apiDiff.js <oldPath> <newPath>');
355 + console.error('示例:');
356 + console.error(' node scripts/apiDiff.js docs/api-specs/user/ docs/api-specs/user-new/');
357 + console.error(' node 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');
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 {string} text - 英文文本
64 + * @returns {string} - 中文文本
65 + */
66 +function translateToChinese(text) {
67 + if (!text) return '';
68 +
69 + // 常见技术术语翻译字典(优先级从高到低)
70 + const dictionary = {
71 + // 短语和完整句子
72 + 'mini program authorization': '小程序授权',
73 + 'login flow design': '登录流程设计',
74 +
75 + // 短语
76 + 'mini program': '小程序',
77 + 'mini-program': '小程序',
78 + 'wechat': '微信',
79 + 'weixin': '微信',
80 + 'openid': 'OpenID',
81 +
82 + // 动词和常用词
83 + 'authorize': '授权',
84 + 'authorization': '授权',
85 + 'login': '登录',
86 + 'logout': '登出',
87 + 'register': '注册',
88 + 'call': '调用',
89 + 'return': '返回',
90 + 'using': '使用',
91 + 'bound to': '绑定到',
92 + 'according to': '根据',
93 +
94 + // 名词
95 + 'user': '用户',
96 + 'code': '授权码',
97 + 'token': '令牌',
98 + 'session': '会话',
99 + 'request': '请求',
100 + 'response': '响应',
101 + 'interface': '接口',
102 + 'api': '接口',
103 + 'account': '账号',
104 + 'openid': 'OpenID',
105 +
106 + // 描述性词汇
107 + 'first': '首先',
108 + 'then': '然后',
109 + 'if': '如果',
110 + 'else': '否则',
111 + 'need': '需要',
112 + 'should': '应该',
113 + 'must': '必须',
114 +
115 + // 状态
116 + 'success': '成功',
117 + 'fail': '失败',
118 + 'error': '错误',
119 + 'empty': '空',
120 + 'non-empty': '非空',
121 +
122 + // 属性
123 + 'avatar': '头像',
124 + 'name': '姓名',
125 + 'id': 'ID',
126 + 'info': '信息',
127 + 'data': '数据',
128 + 'flow': '流程',
129 + 'design': '设计',
130 +
131 + // 其他
132 + 'internal': '内部',
133 + 'automatically': '自动',
134 + 'specify': '指定',
135 + 'testing': '测试',
136 + 'used for': '用于',
137 + 'bound with': '绑定',
138 + };
139 +
140 + let translated = text;
141 +
142 + // 按照字典进行替换(优先匹配长词)
143 + const sortedKeys = Object.keys(dictionary).sort((a, b) => b.length - a.length);
144 + sortedKeys.forEach(key => {
145 + const regex = new RegExp(key, 'gi');
146 + translated = translated.replace(regex, dictionary[key]);
147 + });
148 +
149 + return translated;
150 +}
151 +
152 +/**
153 + * 格式化描述文本(翻译+格式化)
154 + * @param {string} description - 原始描述
155 + * @returns {string} - 格式化后的描述
156 + */
157 +function formatDescription(description) {
158 + if (!description) return '';
159 +
160 + // 移除 markdown 格式符号(如 # 标题)
161 + let formatted = description
162 + .replace(/^#+\s*/gm, '') // 移除标题符号
163 + .replace(/\*\*(.*?)\*\*/g, '$1') // 移除加粗
164 + .replace(/\*(.*?)\*/g, '$1') // 移除斜体
165 + .replace(/`([^`]+)`/g, '$1') // 移除行内代码
166 + .trim();
167 +
168 + // 先进行整句翻译(常见句式)
169 + formatted = translateSentences(formatted);
170 +
171 + return formatted;
172 +}
173 +
174 +/**
175 + * 翻译常见句式
176 + * @param {string} text - 文本
177 + * @returns {string} - 翻译后的文本
178 + */
179 +function translateSentences(text) {
180 + if (!text) return '';
181 +
182 + // 常见句式翻译(按优先级排序,长的先匹配)
183 + const sentences = {
184 + // 完整句子
185 + '# 登录流程设计': '# 登录流程设计',
186 + '# Login Flow Design': '# 登录流程设计',
187 +
188 + // 常见句式
189 + 'Authorize mini program first': '先进行小程序授权',
190 + 'If user is empty, call login API': '如果返回 user 为空,则需要调用登录接口',
191 + 'If user is not empty, no need to call login API': '如果返回 user 非空,则不需要调用登录接口',
192 + 'the authorization API will automatically login using the account bound to openid': '授权接口内部按照 openid 绑定的账号,自动登录',
193 + 'Specify an openid for testing': '指定一个 openid 用来测试',
194 + 'User information bound to openid': 'openid 绑定的用户信息',
195 + '0=fail, 1=success': '0=失败,1=成功',
196 + };
197 +
198 + let translated = text;
199 +
200 + // 按长度排序(长句优先)
201 + const sortedKeys = Object.keys(sentences).sort((a, b) => b.length - a.length);
202 + sortedKeys.forEach(key => {
203 + const regex = new RegExp(escapeRegExp(key), 'gi');
204 + translated = translated.replace(regex, sentences[key]);
205 + });
206 +
207 + // 最后进行单词级别的补充翻译
208 + translated = translateWords(translated);
209 +
210 + return translated;
211 +}
212 +
213 +/**
214 + * 转义正则表达式特殊字符
215 + * @param {string} string - 字符串
216 + * @returns {string} - 转义后的字符串
217 + */
218 +function escapeRegExp(string) {
219 + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
220 +}
221 +
222 +/**
223 + * 单词级别的翻译(补充)
224 + * @param {string} text - 文本
225 + * @returns {string} - 翻译后的文本
226 + */
227 +function translateWords(text) {
228 + const dictionary = {
229 + 'mini program': '小程序',
230 + 'wechat': '微信',
231 + 'openid': 'OpenID',
232 + 'user': '用户',
233 + 'authorization': '授权',
234 + 'login': '登录',
235 + 'avatar': '头像',
236 + 'name': '姓名',
237 + };
238 +
239 + let translated = text;
240 + Object.entries(dictionary).forEach(([key, value]) => {
241 + const regex = new RegExp(key, 'gi');
242 + translated = translated.replace(regex, value);
243 + });
244 +
245 + return translated;
246 +}
247 +
248 +/**
249 + * 解析对象属性,生成字段描述
250 + * @param {object} properties - 属性对象
251 + * @param {number} indent - 缩进级别
252 + * @returns {string} - 字段描述字符串
253 + */
254 +function parseProperties(properties, indent = 0) {
255 + if (!properties) return '';
256 +
257 + const lines = [];
258 + const prefix = ' '.repeat(indent);
259 +
260 + Object.entries(properties).forEach(([key, value]) => {
261 + const type = value.type || 'any';
262 + const desc = value.description || value.title || '';
263 + const required = value.required ? '' : ' (可选)';
264 +
265 + // 基本类型
266 + if (type !== 'object' && type !== 'array') {
267 + lines.push(`${prefix}${key}: ${type}${required} - ${desc}`);
268 + }
269 + // 对象类型
270 + else if (type === 'object' && value.properties) {
271 + lines.push(`${prefix}${key}: {`);
272 + lines.push(`${prefix} // ${desc}`);
273 + lines.push(parseProperties(value.properties, indent + 2));
274 + lines.push(prefix + '}');
275 + }
276 + // 数组类型
277 + else if (type === 'array' && value.items) {
278 + const itemType = value.items.type || 'any';
279 + if (itemType === 'object' && value.items.properties) {
280 + lines.push(`${prefix}${key}: Array<{`);
281 + lines.push(`${prefix} // ${desc}`);
282 + lines.push(parseProperties(value.items.properties, indent + 2));
283 + lines.push(prefix + '}>');
284 + } else {
285 + lines.push(`${prefix}${key}: Array<${itemType}>${required} - ${desc}`);
286 + }
287 + }
288 + });
289 +
290 + return lines.join('\n');
291 +}
292 +
293 +/**
294 + * 从 requestBody 中提取参数
295 + * @param {object} requestBody - requestBody 对象
296 + * @returns {Array} - 参数数组
297 + */
298 +function extractRequestParams(requestBody) {
299 + if (!requestBody || !requestBody.content) {
300 + return [];
301 + }
302 +
303 + // 获取内容类型(可能是 application/x-www-form-urlencoded 或 application/json)
304 + const content = requestBody.content['application/x-www-form-urlencoded'] ||
305 + requestBody.content['application/json'];
306 +
307 + if (!content || !content.schema || !content.schema.properties) {
308 + return [];
309 + }
310 +
311 + const params = [];
312 + Object.entries(content.schema.properties).forEach(([key, value]) => {
313 + params.push({
314 + name: key,
315 + type: value.type || 'any',
316 + description: value.description || '',
317 + example: value.example || '',
318 + required: content.schema.required?.includes(key) || false,
319 + });
320 + });
321 +
322 + return params;
323 +}
324 +
325 +/**
326 + * 生成 JSDoc 参数注释
327 + * @param {Array} parameters - parameters 数组(GET 请求)
328 + * @param {Array} bodyParams - requestBody 参数数组(POST 请求)
329 + * @param {string} method - HTTP 方法
330 + * @returns {string} - JSDoc 参数注释
331 + */
332 +function generateParamJSDoc(parameters, bodyParams, method) {
333 + const lines = [' * @param {Object} params 请求参数'];
334 +
335 + // POST 请求使用 body 参数
336 + if (method === 'POST' && bodyParams && bodyParams.length > 0) {
337 + // 过滤掉 a、f 和 t 参数(这些参数已硬编码到 URL 中)
338 + const filteredParams = bodyParams.filter(p => p.name !== 'a' && p.name !== 'f' && p.name !== 't');
339 +
340 + filteredParams.forEach((param) => {
341 + const type = param.type || 'any';
342 + const desc = param.description || '';
343 + const required = param.required ? '' : ' (可选)';
344 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
345 + });
346 + }
347 + // GET 请求使用 query 参数
348 + else if (method === 'GET' && parameters && parameters.length > 0) {
349 + // 只保留 query 参数,过滤 header 参数和 a、f、t 参数
350 + const queryParams = parameters.filter(p =>
351 + p.in === 'query' &&
352 + p.name !== 'a' &&
353 + p.name !== 'f' &&
354 + p.name !== 't'
355 + );
356 +
357 + queryParams.forEach((param) => {
358 + const type = param.schema?.type || 'any';
359 + const desc = param.description || '';
360 + const required = param.required ? '' : ' (可选)';
361 + lines.push(` * @param {${type}} params.${param.name}${required} ${desc}`);
362 + });
363 + }
364 +
365 + return lines.join('\n');
366 +}
367 +
368 +/**
369 + * 递归生成属性字段的 JSDoc 注释
370 + * @param {object} properties - 属性对象
371 + * @param {number} indent - 缩进级别(空格数)
372 + * @returns {string} - JSDoc 注释
373 + */
374 +function generatePropertiesJSDoc(properties, indent = 0) {
375 + const lines = [];
376 + const prefix = ' '.repeat(indent);
377 +
378 + Object.entries(properties).forEach(([key, value]) => {
379 + const type = value.type || 'any';
380 + const desc = value.description || value.title || '';
381 +
382 + // 处理嵌套对象
383 + if (type === 'object' && value.properties) {
384 + lines.push(`${prefix}${key}: {\n`);
385 + // 递归处理嵌套对象的属性
386 + lines.push(generatePropertiesJSDoc(value.properties, indent + 2));
387 + lines.push(`${prefix}};\n`);
388 + }
389 + // 处理数组(元素是对象)
390 + else if (type === 'array' && value.items && value.items.properties) {
391 + lines.push(`${prefix}${key}: Array<{\n`);
392 + // 递归处理数组元素的属性
393 + lines.push(generatePropertiesJSDoc(value.items.properties, indent + 2));
394 + lines.push(`${prefix}}>;\n`);
395 + }
396 + // 处理简单数组
397 + else if (type === 'array' && value.items) {
398 + const itemType = value.items.type || 'any';
399 + lines.push(`${prefix}${key}: Array<${itemType}>; // ${desc}\n`);
400 + }
401 + // 处理基本类型
402 + else {
403 + lines.push(`${prefix}${key}: ${type}; // ${desc}\n`);
404 + }
405 + });
406 +
407 + return lines.join('');
408 +}
409 +
410 +/**
411 + * 生成 JSDoc 返回值注释
412 + * @param {object} responseSchema - 响应 schema
413 + * @returns {string} - JSDoc 返回值注释
414 + */
415 +function generateReturnJSDoc(responseSchema) {
416 + if (!responseSchema || !responseSchema.properties) {
417 + return ' * @returns {Promise<{code:number,data:any,msg:string}>} 标准返回';
418 + }
419 +
420 + const { code, msg, data } = responseSchema.properties;
421 +
422 + let returnDesc = ' * @returns {Promise<{\n';
423 + returnDesc += ' * code: number; // 状态码\n';
424 + returnDesc += ' * msg: string; // 消息\n';
425 +
426 + if (data) {
427 + const dataType = data.type || 'any';
428 + const dataDesc = data.description || data.title || '';
429 +
430 + // 处理对象类型的 data
431 + if (dataType === 'object' && data.properties) {
432 + returnDesc += ' * data: {\n';
433 + // 使用递归函数处理 data 的所有属性
434 + returnDesc += generatePropertiesJSDoc(data.properties, 4);
435 + returnDesc += ' * };\n';
436 + }
437 + // 处理数组类型的 data(元素是对象)
438 + else if (dataType === 'array' && data.items && data.items.properties) {
439 + returnDesc += ' * data: Array<{\n';
440 + returnDesc += generatePropertiesJSDoc(data.items.properties, 4);
441 + returnDesc += ' * }>;\n';
442 + }
443 + // 处理简单数组类型
444 + else if (dataType === 'array' && data.items) {
445 + const itemType = data.items.type || 'any';
446 + returnDesc += ` * data: Array<${itemType}>;\n`;
447 + }
448 + // 其他类型
449 + else {
450 + returnDesc += ` * data: ${dataType};\n`;
451 + }
452 + } else {
453 + returnDesc += ' * data: any;\n';
454 + }
455 +
456 + returnDesc += ' * }>}';
457 +
458 + return returnDesc;
459 +}
460 +
461 +/**
462 + * 解析 OpenAPI 文档并提取 API 信息
463 + * @param {object} openapiDoc - 解析后的 OpenAPI 对象
464 + * @param {string} fileName - 文件名(用作 API 名称)
465 + * @returns {object} - 提取的 API 信息
466 + */
467 +function parseOpenAPIDocument(openapiDoc, fileName) {
468 + try {
469 + const path = Object.keys(openapiDoc.paths)[0];
470 + const method = Object.keys(openapiDoc.paths[path])[0];
471 + const apiInfo = openapiDoc.paths[path][method];
472 +
473 + // 提取 query 参数
474 + const parameters = apiInfo.parameters || [];
475 + const queryParams = {};
476 + let actionValue = '';
477 + let typeValue = ''; // t 参数
478 +
479 + // 提取 body 参数(用于 POST 请求)
480 + const requestBody = apiInfo.requestBody;
481 + const bodyParams = extractRequestParams(requestBody);
482 +
483 + // 对于 POST 请求,从 requestBody 中提取 action 和 type
484 + if (requestBody && bodyParams.length > 0) {
485 + const actionParam = bodyParams.find(p => p.name === 'a');
486 + if (actionParam) {
487 + actionValue = actionParam.example || '';
488 + }
489 + const typeParam = bodyParams.find(p => p.name === 't');
490 + if (typeParam) {
491 + typeValue = typeParam.example || '';
492 + }
493 + }
494 +
495 + // 对于 GET 请求,从 query 参数中提取 action 和 type
496 + if (parameters.length > 0) {
497 + parameters.forEach((param) => {
498 + if (param.in === 'query') {
499 + queryParams[param.name] = param.example || param.schema?.default || '';
500 +
501 + // 提取 action 参数(通常是 'a' 参数)
502 + if (param.name === 'a' && !actionValue) {
503 + actionValue = param.example || '';
504 + }
505 + // 提取 type 参数('t' 参数)
506 + if (param.name === 't' && !typeValue) {
507 + typeValue = param.example || '';
508 + }
509 + }
510 + });
511 + }
512 +
513 + // 提取响应结构
514 + const responseSchema = apiInfo.responses?.['200']?.content?.['application/json']?.schema;
515 +
516 + return {
517 + summary: apiInfo.summary || fileName,
518 + description: apiInfo.description || '',
519 + method: method.toUpperCase(),
520 + action: actionValue,
521 + type: typeValue, // 保存 t 参数
522 + queryParams,
523 + parameters, // 保存完整的参数信息用于生成 JSDoc(GET 请求)
524 + bodyParams, // 保存 requestBody 参数用于生成 JSDoc(POST 请求)
525 + responseSchema, // 保存响应结构用于生成 JSDoc
526 + fileName,
527 + };
528 + } catch (error) {
529 + console.error(`解析 OpenAPI 文档失败: ${error.message}`);
530 + return null;
531 + }
532 +}
533 +
534 +/**
535 + * 生成 API 文件内容
536 + * @param {string} moduleName - 模块名称
537 + * @param {Array} apis - API 信息数组
538 + * @returns {string} - 生成的文件内容
539 + */
540 +function generateApiFileContent(moduleName, apis) {
541 + const imports = `import { fn, fetch } from '@/api/fn';\n\n`;
542 + const apiConstants = [];
543 + const apiFunctions = [];
544 +
545 + apis.forEach((api) => {
546 + // 生成常量名(帕斯卡命名)
547 + const constantName = toPascalCase(api.fileName);
548 + // 生成函数名(驼峰命名 + API 后缀)
549 + const functionName = toCamelCase(api.fileName) + 'API';
550 +
551 + // 构建 URL,包含 a 和 t 参数
552 + let url = '/srv/?';
553 + const params = [];
554 + if (api.action) params.push(`a=${api.action}`);
555 + if (api.type) params.push(`t=${api.type}`);
556 + url += params.join('&');
557 +
558 + // 添加常量定义
559 + apiConstants.push(
560 + ` ${constantName}: '${url}',`
561 + );
562 +
563 + // 生成详细的 JSDoc 注释
564 + const paramJSDoc = generateParamJSDoc(api.parameters, api.bodyParams, api.method);
565 + const returnJSDoc = generateReturnJSDoc(api.responseSchema);
566 +
567 + // 添加函数定义
568 + const fetchMethod = api.method === 'GET' ? 'fetch.get' : 'fetch.post';
569 +
570 + // 格式化描述
571 + const formattedDesc = formatDescription(api.description);
572 +
573 + // 生成 JSDoc 注释(包含描述)
574 + const comment = `/**
575 + * @description ${api.summary}
576 + * @remark ${formattedDesc}
577 +${paramJSDoc}
578 +${returnJSDoc}
579 + */`;
580 +
581 + apiFunctions.push(`${comment}\nexport const ${functionName} = (params) => fn(${fetchMethod}(Api.${constantName}, params));`);
582 + });
583 +
584 + return `${imports}const Api = {\n${apiConstants.join('\n')}\n}\n\n${apiFunctions.join('\n\n')}\n`;
585 +}
586 +
587 +/**
588 + * 扫描目录并处理所有 OpenAPI 文档
589 + * @param {string} openAPIDir - OpenAPI 文档目录
590 + * @param {string} outputDir - 输出目录
591 + */
592 +function scanAndGenerate(openAPIDir, outputDir) {
593 + if (!fs.existsSync(openAPIDir)) {
594 + console.error(`OpenAPI 目录不存在: ${openAPIDir}`);
595 + return;
596 + }
597 +
598 + // 确保输出目录存在
599 + if (!fs.existsSync(outputDir)) {
600 + fs.mkdirSync(outputDir, { recursive: true });
601 + }
602 +
603 + // 扫描第一级目录(模块)
604 + const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
605 + .filter(dirent => dirent.isDirectory())
606 + .map(dirent => dirent.name);
607 +
608 + console.log(`找到 ${modules.length} 个模块: ${modules.join(', ')}`);
609 +
610 + modules.forEach((moduleName) => {
611 + const moduleDir = path.join(openAPIDir, moduleName);
612 + const apiFiles = fs.readdirSync(moduleDir)
613 + .filter(file => file.endsWith('.md') && file !== 'CLAUDE.md');
614 +
615 + if (apiFiles.length === 0) {
616 + console.log(`模块 ${moduleName} 中没有找到 .md 文件`);
617 + return;
618 + }
619 +
620 + console.log(`\n处理模块: ${moduleName}`);
621 + console.log(`找到 ${apiFiles.length} 个 API 文档`);
622 +
623 + const apis = [];
624 +
625 + apiFiles.forEach((fileName) => {
626 + const filePath = path.join(moduleDir, fileName);
627 + const content = fs.readFileSync(filePath, 'utf8');
628 + const yamlContent = extractYAMLFromMarkdown(content);
629 +
630 + if (!yamlContent) {
631 + console.warn(` ⚠️ ${fileName}: 未找到 YAML 代码块`);
632 + return;
633 + }
634 +
635 + try {
636 + const openapiDoc = yaml.load(yamlContent);
637 + const apiName = path.basename(fileName, '.md');
638 + const apiInfo = parseOpenAPIDocument(openapiDoc, apiName);
639 +
640 + if (apiInfo) {
641 + apis.push(apiInfo);
642 + console.log(` ✓ ${apiName}: ${apiInfo.summary}`);
643 + }
644 + } catch (error) {
645 + console.error(` ✗ ${fileName}: 解析失败 - ${error.message}`);
646 + }
647 + });
648 +
649 + // 生成并保存 API 文件
650 + if (apis.length > 0) {
651 + const fileContent = generateApiFileContent(moduleName, apis);
652 + const outputPath = path.join(outputDir, `${moduleName}.js`);
653 + fs.writeFileSync(outputPath, fileContent, 'utf8');
654 + console.log(` 📝 生成文件: ${outputPath}`);
655 + }
656 + });
657 +
658 + console.log('\n✅ API 文档生成完成!');
659 +
660 + // 对比新旧 API
661 + console.log('\n🔍 开始检测 API 变更...\n');
662 + compareAPIChanges(openAPIDir);
663 +}
664 +
665 +/**
666 + * 备份 OpenAPI 文档目录
667 + * @param {string} sourceDir - 源目录
668 + * @returns {string} - 备份目录路径
669 + */
670 +function backupOpenAPIDir(sourceDir) {
671 + const backupBaseDir = path.resolve(__dirname, '../.tmp');
672 + const backupDir = path.join(backupBaseDir, 'api-specs-backup');
673 +
674 + // 创建备份目录
675 + if (!fs.existsSync(backupBaseDir)) {
676 + fs.mkdirSync(backupBaseDir, { recursive: true });
677 + }
678 +
679 + // 删除旧备份
680 + if (fs.existsSync(backupDir)) {
681 + fs.rmSync(backupDir, { recursive: true, force: true });
682 + }
683 +
684 + // 复制目录
685 + copyDirectory(sourceDir, backupDir);
686 +
687 + return backupDir;
688 +}
689 +
690 +/**
691 + * 递归复制目录
692 + * @param {string} src - 源路径
693 + * @param {string} dest - 目标路径
694 + */
695 +function copyDirectory(src, dest) {
696 + if (!fs.existsSync(dest)) {
697 + fs.mkdirSync(dest, { recursive: true });
698 + }
699 +
700 + const entries = fs.readdirSync(src, { withFileTypes: true });
701 +
702 + for (const entry of entries) {
703 + const srcPath = path.join(src, entry.name);
704 + const destPath = path.join(dest, entry.name);
705 +
706 + if (entry.isDirectory()) {
707 + copyDirectory(srcPath, destPath);
708 + } else {
709 + fs.copyFileSync(srcPath, destPath);
710 + }
711 + }
712 +}
713 +
714 +/**
715 + * 对比新旧 API 变更
716 + * @param {string} openAPIDir - OpenAPI 文档目录
717 + */
718 +function compareAPIChanges(openAPIDir) {
719 + const backupDir = path.resolve(__dirname, '../.tmp/api-specs-backup');
720 + const tempDir = path.resolve(__dirname, '../.tmp/api-specs-temp');
721 +
722 + // 检查是否存在临时备份(上一次的版本)
723 + if (!fs.existsSync(tempDir)) {
724 + console.log('ℹ️ 首次运行,已建立基线。下次运行将检测 API 变更。');
725 + // 将当前备份移动到临时目录,作为下次对比的基线
726 + if (fs.existsSync(backupDir)) {
727 + fs.renameSync(backupDir, tempDir);
728 + }
729 + return;
730 + }
731 +
732 + // 扫描模块
733 + const modules = fs.readdirSync(openAPIDir, { withFileTypes: true })
734 + .filter(dirent => dirent.isDirectory())
735 + .map(dirent => dirent.name);
736 +
737 + let hasChanges = false;
738 + const moduleReports = [];
739 +
740 + modules.forEach((moduleName) => {
741 + const moduleDir = path.join(openAPIDir, moduleName);
742 + const tempModuleDir = path.join(tempDir, moduleName);
743 +
744 + // 如果临时备份中不存在该模块,说明是新增模块
745 + if (!fs.existsSync(tempModuleDir)) {
746 + console.log(`\n📦 新增模块: ${moduleName}`);
747 + // 解析新增模块的接口
748 + try {
749 + const newDocs = parseOpenAPIPath(moduleDir);
750 + if (newDocs && newDocs.length > 0) {
751 + // 显示新增接口信息
752 + console.log(` 包含 ${newDocs.length} 个新增接口:`);
753 + newDocs.forEach(doc => {
754 + const path = Object.keys(doc.paths || {})[0] || '';
755 + const method = Object.keys(doc.paths?.[path] || {})[0] || '';
756 + const apiInfo = doc.paths?.[path]?.[method];
757 + const summary = apiInfo?.summary || doc.info?.title || '未命名接口';
758 + console.log(` • ${method?.toUpperCase()} ${path} - ${summary}`);
759 + });
760 + }
761 + } catch (error) {
762 + console.error(` ⚠️ 解析模块失败: ${error.message}`);
763 + }
764 + hasChanges = true;
765 + return;
766 + }
767 +
768 + // 读取当前和临时备份的文档
769 + const currentFiles = fs.readdirSync(moduleDir).filter(f => f.endsWith('.md') && f !== 'CLAUDE.md');
770 + const tempFiles = fs.readdirSync(tempModuleDir).filter(f => f.endsWith('.md') && f !== 'CLAUDE.md');
771 +
772 + // 检查是否有文件变更
773 + const hasNewFiles = currentFiles.some(f => !tempFiles.includes(f));
774 + const hasRemovedFiles = tempFiles.some(f => !currentFiles.includes(f));
775 + const hasModifiedFiles = currentFiles.some(f => {
776 + if (!tempFiles.includes(f)) return false;
777 + const currentContent = fs.readFileSync(path.join(moduleDir, f), 'utf8');
778 + const tempContent = fs.readFileSync(path.join(tempModuleDir, f), 'utf8');
779 + return currentContent !== tempContent;
780 + });
781 +
782 + if (hasNewFiles || hasRemovedFiles || hasModifiedFiles) {
783 + hasChanges = true;
784 + moduleReports.push({ moduleName, moduleDir, tempModuleDir });
785 + }
786 + });
787 +
788 + // 检查删除的模块
789 + const tempModules = fs.existsSync(tempDir)
790 + ? fs.readdirSync(tempDir, { withFileTypes: true })
791 + .filter(dirent => dirent.isDirectory())
792 + .map(dirent => dirent.name)
793 + : [];
794 +
795 + const deletedModules = tempModules.filter(m => !modules.includes(m));
796 + if (deletedModules.length > 0) {
797 + hasChanges = true;
798 + console.log(`\n❌ 删除模块: ${deletedModules.join(', ')}`);
799 + }
800 +
801 + if (!hasChanges) {
802 + console.log('✅ 未检测到 API 变更');
803 + // 更新基线
804 + if (fs.existsSync(backupDir)) {
805 + fs.rmSync(tempDir, { recursive: true, force: true });
806 + fs.renameSync(backupDir, tempDir);
807 + }
808 + return;
809 + }
810 +
811 + // 逐个模块对比
812 + console.log('');
813 + moduleReports.forEach(({ moduleName, moduleDir, tempModuleDir }) => {
814 + try {
815 + const oldDocs = parseOpenAPIPath(tempModuleDir);
816 + const newDocs = parseOpenAPIPath(moduleDir);
817 + const report = generateReport(oldDocs, newDocs, 'text');
818 +
819 + console.log(report);
820 + console.log('');
821 + } catch (error) {
822 + console.error(`⚠️ 模块 ${moduleName} 对比失败: ${error.message}`);
823 + }
824 + });
825 +
826 + // 更新基线:将当前备份作为下次对比的基准
827 + console.log('📝 更新 API 基线...');
828 + if (fs.existsSync(tempDir)) {
829 + fs.rmSync(tempDir, { recursive: true, force: true });
830 + }
831 + if (fs.existsSync(backupDir)) {
832 + fs.renameSync(backupDir, tempDir);
833 + }
834 +}
835 +
836 +// 执行生成
837 +const openAPIDir = path.resolve(__dirname, '../docs/api-specs');
838 +const outputDir = path.resolve(__dirname, '../src/api');
839 +
840 +console.log('=== OpenAPI 转 API 文档生成器 ===\n');
841 +console.log(`输入目录: ${openAPIDir}`);
842 +console.log(`输出目录: ${outputDir}\n`);
843 +
844 +// 备份当前的 OpenAPI 文档(用于下次对比)
845 +if (fs.existsSync(openAPIDir)) {
846 + console.log('💾 备份当前 OpenAPI 文档...');
847 + backupOpenAPIDir(openAPIDir);
848 + console.log('');
849 +}
850 +
851 +scanAndGenerate(openAPIDir, outputDir);
1 /* 1 /*
2 * @Date: 2025-09-04 16:44:18 2 * @Date: 2025-09-04 16:44:18
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-04 16:53:10 4 + * @LastEditTime: 2026-02-09 16:33:11
5 * @FilePath: /map-demo/src/api/checkin.js 5 * @FilePath: /map-demo/src/api/checkin.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
8 import { fn, fetch } from '@/api/fn'; 8 import { fn, fetch } from '@/api/fn';
9 9
10 const Api = { 10 const Api = {
11 - IS_CHECKED: '/srv/?f=walk&a=map&t=is_checked', 11 + IS_CHECKED: '/srv/?f=walk&a=map_activity&t=is_checked',
12 - CHECKIN: '/srv/?f=walk&a=map&t=checkin', 12 + CHECKIN: '/srv/?f=walk&a=map_activity&t=checkin',
13 }; 13 };
14 14
15 /** 15 /**
......
...@@ -7,5 +7,6 @@ ...@@ -7,5 +7,6 @@
7 7
8 | ID | Time | T | Title | Read | 8 | ID | Time | T | Title | Read |
9 |----|------|---|-------|------| 9 |----|------|---|-------|------|
10 +| #4119 | 2:13 PM | 🔵 | page_details data source identified in checkin info component | ~328 |
10 | #3978 | 11:52 AM | 🔵 | Code duplication identified in src/views directory structure | ~320 | 11 | #3978 | 11:52 AM | 🔵 | Code duplication identified in src/views directory structure | ~320 |
11 </claude-mem-context> 12 </claude-mem-context>
...\ No newline at end of file ...\ No newline at end of file
......