/** * CapCut API 基础设施层 * * 提供: 配置加载、API 封装、CLI 解析、工具函数 * 无业务逻辑,纯基础设施。 */ const axios = require('axios') const path = require('path') const fs = require('fs') const { execFile } = require('child_process') const US = 1_000_000 let _config = null function getConfig() { if (_config) return _config const configPath = path.join(__dirname, '..', '..', '..', 'config.json') if (!fs.existsSync(configPath)) { console.error('缺少配置文件: skills/config.json') console.error('请运行 node setup.js 生成配置') process.exit(1) } const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) if (!config.jianyingDraftPath || !config.capcutMateDir || !config.capcutMateApiBase) { console.error('config.json 需要填写 jianyingDraftPath、capcutMateDir 和 capcutMateApiBase') process.exit(1) } _config = config return _config } const BASE_URL = getConfig().capcutMateApiBase async function api(endpoint, data = {}, timeout = 60000) { const url = `${BASE_URL}/${endpoint}` const method = endpoint === 'get_draft' ? 'get' : 'post' try { const res = method === 'get' ? await axios.get(url, { params: data, timeout }) : await axios.post(url, data, { timeout }) if (res.data.code !== undefined && res.data.code !== 0) { throw new Error(`API [${endpoint}] 返回错误: ${res.data.message}`) } return res.data } catch (err) { if (err.response) { throw new Error(`API [${endpoint}] HTTP ${err.response.status}: ${JSON.stringify(err.response.data)}`) } throw err } } function parseArgs(argv) { const args = {} for (let i = 0; i < argv.length; i++) { if (argv[i].startsWith('--')) { const key = argv[i].slice(2) const value = argv[i + 1] if (value && !value.startsWith('--')) { args[key] = value i++ } else { args[key] = true } } } return args } function getResolution(format) { const map = { '9:16': { width: 1080, height: 1920 }, '16:9': { width: 1920, height: 1080 }, '1:1': { width: 1080, height: 1080 }, '4:3': { width: 1440, height: 1080 }, } return map[format] || map['9:16'] } function getAudioDurationSec(filePath) { return new Promise((resolve) => { execFile('ffprobe', [ '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath ], (err, stdout) => { if (err) { resolve(null); return } const dur = parseFloat(stdout.trim()) resolve(dur > 0 ? dur : null) }) }) } module.exports = { US, getConfig, BASE_URL, api, parseArgs, getResolution, getAudioDurationSec }