You need to sign in or sign up before continuing.
apifox-sync.js 8.87 KB
#!/usr/bin/env node

/**
 * Apifox API 同步工具
 *
 * 功能:
 * 1. 从 Apifox 获取所有接口数据
 * 2. 生成 TypeScript/JavaScript API 接口代码
 * 3. 生成 Mock 数据
 * 4. 自动更新 src/api/ 目录
 *
 * 使用:
 * node scripts/apifox-sync.js
 */

const fs = require('fs');
const path = require('path');
const https = require('https');

// 配置
const CONFIG = {
  token: process.env.VITE_APIFOX_TOKEN,
  projectId: process.env.VITE_APIFOX_PROJECT_ID,
  baseUrl: 'api.apifox.com',
  outputDir: path.join(__dirname, '../src/api'),
  mockDir: path.join(__dirname, '../src/mocks')
};

// 颜色输出
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  red: '\x1b[31m',
  blue: '\x1b[34m'
};

function log(message, color = 'reset') {
  console.log(`${colors[color]}${message}${colors.reset}`);
}

// 读取 .env.apifox 文件
function loadEnv() {
  const envPath = path.join(__dirname, '../.env.apifox');

  if (!fs.existsSync(envPath)) {
    log('❌ 未找到 .env.apifox 文件', 'red');
    log('📝 请先创建 .env.apifox 文件并填写 Apifox 凭证:', 'yellow');
    log('');
    log('VITE_APIFOX_TOKEN=your_api_token_here', 'blue');
    log('VITE_APIFOX_PROJECT_ID=your_project_id_here', 'blue');
    process.exit(1);
  }

  const env = fs.readFileSync(envPath, 'utf-8');
  env.split('\n').forEach(line => {
    const [key, ...valueParts] = line.split('=');
    const value = valueParts.join('=');
    if (key && !key.startsWith('#') && value) {
      process.env[key.trim()] = value.trim();
    }
  });

  if (!process.env.VITE_APIFOX_TOKEN || !process.env.VITE_APIFOX_PROJECT_ID) {
    log('❌ .env.apifox 文件中缺少必要的配置', 'red');
    log('请确保填写了 VITE_APIFOX_TOKEN 和 VITE_APIFOX_PROJECT_ID', 'yellow');
    process.exit(1);
  }

  CONFIG.token = process.env.VITE_APIFOX_TOKEN;
  CONFIG.projectId = process.env.VITE_APIFOX_PROJECT_ID;

  log(`✅ 已加载配置,项目 ID: ${CONFIG.projectId}`, 'green');
}

// 发送 HTTPS 请求
function httpsRequest(options) {
  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let data = '';

      res.on('data', chunk => {
        data += chunk;
      });

      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          if (res.statusCode === 200) {
            resolve(json);
          } else {
            reject(new Error(`HTTP ${res.statusCode}: ${json.message || data}`));
          }
        } catch (err) {
          reject(new Error(`解析响应失败: ${err.message}`));
        }
      });
    });

    req.on('error', reject);
    req.end();
  });
}

// 获取 Apifox 项目所有接口
async function fetchApis() {
  log('\n📡 正在从 Apifox 获取接口数据...', 'blue');

  const options = {
    hostname: CONFIG.baseUrl,
    path: `/api/v1/projects/${CONFIG.projectId}/apis?pageSize=1000`,
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${CONFIG.token}`,
      'Content-Type': 'application/json'
    }
  };

  try {
    const response = await httpsRequest(options);
    const apis = response.data || [];

    log(`✅ 成功获取 ${apis.length} 个接口`, 'green');
    return apis;
  } catch (err) {
    log(`❌ 获取接口失败: ${err.message}`, 'red');
    throw err;
  }
}

// 获取接口详情
async function fetchApiDetail(apiId) {
  const options = {
    hostname: CONFIG.baseUrl,
    path: `/api/v1/projects/${CONFIG.projectId}/apis/${apiId}`,
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${CONFIG.token}`,
      'Content-Type': 'application/json'
    }
  };

  try {
    const response = await httpsRequest(options);
    return response.data;
  } catch (err) {
    log(`⚠️  获取接口详情失败 (${apiId}): ${err.message}`, 'yellow');
    return null;
  }
}

// 生成 API 接口代码
function generateApiCode(apis) {
  log('\n📝 正在生成 API 接口代码...', 'blue');

  let code = `/**
 * @description API 接口定义(从 Apifox 自动生成)
 * @generated by scripts/apifox-sync.js
 * @lastUpdated ${new Date().toISOString()}
 */

import { fn } from '@/api/fn';

`;

  // 按标签分组
  const groupedApis = {};
  apis.forEach(api => {
    const tags = api.attributes?.tags || ['default'];
    const tag = tags[0];
    if (!groupedApis[tag]) {
      groupedApis[tag] = [];
    }
    groupedApis[tag].push(api);
  });

  // 生成每个分组的接口
  Object.entries(groupedApis).forEach(([tag, items]) => {
    code += `\n// ==================== ${tag} ====================\n\n`;

    items.forEach(api => {
      const apiId = api.id;
      const name = api.attributes?.name || apiId;
      const method = (api.attributes?.method || 'GET').toLowerCase();
      const path = api.attributes?.path || '';

      // 生成接口函数名(将路径转换为驼峰命名)
      const functionName = pathToFunctionName(method, path);

      // 生成注释
      code += `/**\n`;
      code += ` * @description ${name}\n`;
      code += ` * @path ${method.toUpperCase()} ${path}\n`;
      if (api.attributes?.description) {
        code += ` * @remark ${api.attributes.description}\n`;
      }
      code += ` */\n`;

      // 生成接口函数
      code += `export const ${functionName} = (params = {}) => {\n`;
      code += `    return fn({\n`;
      code += `        method: '${method}',\n`;
      code += `        url: '${path}',\n`;
      code += `        data: params\n`;
      code += `    })\n`;
      code += `}\n\n`;
    });
  });

  // 写入文件
  const outputPath = path.join(CONFIG.outputDir, 'generated.js');
  fs.writeFileSync(outputPath, code, 'utf-8');

  log(`✅ 已生成 API 接口代码: ${outputPath}`, 'green');
  log(`   共 ${apis.length} 个接口,${Object.keys(groupedApis).length} 个分组`, 'green');

  return outputPath;
}

// 生成 Mock 数据
function generateMockData(apis) {
  log('\n🎭 正在生成 Mock 数据...', 'blue');

  // 确保 mocks 目录存在
  if (!fs.existsSync(CONFIG.mockDir)) {
    fs.mkdirSync(CONFIG.mockDir, { recursive: true });
  }

  let mockIndex = `/**
 * @description Mock 数据(从 Apifox 自动生成)
 * @generated by scripts/apifox-sync.js
 */

`;

  // 为每个接口生成 Mock 数据
  apis.forEach(api => {
    const path = api.attributes?.path || '';
    const method = (api.attributes?.method || 'GET').toLowerCase();
    const functionName = pathToFunctionName(method, path);
    const mockFileName = `${functionName}.mock.js`;

    // 从响应示例中提取 Mock 数据
    const responses = api.attributes?.responses || [];
    const successResponse = responses.find(r => r.code === 200) || responses[0];

    if (successResponse) {
      // 生成 Mock 文件
      const mockCode = `/**
 * @description Mock data for ${functionName}
 * @path ${method.toUpperCase()} ${path}
 */

export const mock${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data = ${JSON.stringify(successResponse, null, 2)};

export default mock${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data;
`;

      const mockPath = path.join(CONFIG.mockDir, mockFileName);
      fs.writeFileSync(mockPath, mockCode, 'utf-8');

      mockIndex += `export { default as mock${functionName.charAt(0).toUpperCase() + functionName.slice(1)}Data } from './${mockFileName}';\n`;
    }
  });

  // 写入索引文件
  const indexPath = path.join(CONFIG.mockDir, 'index.js');
  fs.writeFileSync(indexPath, mockIndex, 'utf-8');

  log(`✅ 已生成 Mock 数据: ${indexPath}`, 'green');

  return indexPath;
}

// 将 URL 路径转换为驼峰命名的函数名
function pathToFunctionName(method, path) {
  // 移除路径参数和查询参数
  let cleanPath = path
    .replace(/:\w+/g, '')
    .replace(/\?.*$/, '')
    .replace(/^\//, '')
    .replace(/\/$/, '');

  // 转换为驼峰命名
  const parts = cleanPath.split('/').filter(Boolean);
  let functionName = method;

  parts.forEach(part => {
    // 首字母大写
    const capitalized = part.charAt(0).toUpperCase() + part.slice(1);
    functionName += capitalized;
  });

  // 移除特殊字符
  functionName = functionName.replace(/[^a-zA-Z0-9]/g, '');

  return functionName;
}

// 主函数
async function main() {
  try {
    log('\n🚀 Apifox API 同步工具', 'bright');
    log('=' .repeat(50), 'bright');

    // 1. 加载配置
    loadEnv();

    // 2. 获取接口列表
    const apis = await fetchApis();

    // 3. 生成 API 接口代码
    generateApiCode(apis);

    // 4. 生成 Mock 数据
    generateMockData(apis);

    // 完成
    log('\n✅ 同步完成!', 'green');
    log('📦 生成的文件:', 'blue');
    log(`   - src/api/generated.js (API 接口)`, 'blue');
    log(`   - src/mocks/ (Mock 数据)`, 'blue');
    log('\n💡 提示: 将 src/api/generated.js 中的接口导入到 src/api/index.js 中使用', 'yellow');

  } catch (err) {
    log(`\n❌ 同步失败: ${err.message}`, 'red');
    process.exit(1);
  }
}

// 运行
main();