/** * Kling 官方 API 适配器 · api-beijing.klingai.com * D136+ · 铸渊 ICE-GL-ZY001 · 2026-06-21 * * 文档: https://klingai.com/document-api/guides/get-started/quick-start * Base URL: https://api-beijing.klingai.com * 端点: POST /v1/videos/text2video (提交) + GET /v1/videos/{task_id} (查询) * * 认证: Bearer (JWT三段式token, 在 klingai.com/dev/api-key 生成) * 模型ID: kling-v2-6 (kling-v2.6) / kling-v3 / kling-video-o1 等 * * 价格: 试用包70元/月 · kling-v2-6 约0.049/s (5秒≈0.25元) */ const fs = require('fs'); const path = require('path'); const https = require('https'); const { loadVideoAiEnv } = require('./env-loader'); // ═══ 从环境变量读取 ═══ loadVideoAiEnv(path.resolve(__dirname, '../.env')); const API_KEY = process.env.KLING_API_KEY || ''; const BASE_URL = 'api-beijing.klingai.com'; const POLL_INTERVAL = 2000; const MAX_POLL = 180; const JZAO_SHOTS = '/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/视频/zai-fu-fei-xiu-xian/ep01'; const LOCAL_SHOTS = path.resolve(__dirname, '../outputs/shots'); const OUT_DIR = fs.existsSync(JZAO_SHOTS) ? JZAO_SHOTS : LOCAL_SHOTS; function apiRequest(method, path_, body = null) { return new Promise((resolve, reject) => { const options = { hostname: BASE_URL, path: path_, method, headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }; const req = https.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve({ status: res.statusCode, ...JSON.parse(data) }); } catch (e) { resolve({ status: res.statusCode, raw: data }); } }); }); req.on('error', reject); req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); }); if (body) req.write(JSON.stringify(body)); req.end(); }); } function downloadFile(url, dest) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); const proto = url.startsWith('https') ? https : require('http'); proto.get(url, (res) => { if (res.statusCode >= 300 && res.statusCode < 400) { return downloadFile(res.headers.location, dest).then(resolve).catch(reject); } res.pipe(file); file.on('finish', () => { file.close(); resolve(dest); }); }).on('error', reject); }); } /** * 提交任务 + 轮询 + 下载 */ async function generateVideo({ prompt, duration = 5, outputPath, model = 'kling-v2-6' }) { if (!API_KEY) throw new Error('KLING_API_KEY 未设置。在 klingai.com/dev/api-key 生成。'); console.log('[Kling Official] 提交文生视频...'); const submit = await apiRequest('POST', '/v1/videos/text2video', { model, prompt, duration: Math.min(duration, 10), aspect_ratio: '16:9', mode: 'std', }); if (submit.code && submit.code !== 0) { throw new Error(`提交失败 [${submit.code}]: ${submit.message}`); } if (!submit.data?.task_id) { throw new Error(`提交失败: ${JSON.stringify(submit)}`); } const taskId = submit.data.task_id; console.log(`[Kling Official] 任务: ${taskId}`); // 轮询 → 查询端点 GET /v1/videos/text2video/{task_id} for (let i = 1; i <= MAX_POLL; i++) { await new Promise(r => setTimeout(r, POLL_INTERVAL)); const status = await apiRequest('GET', `/v1/videos/text2video/${taskId}`); if (status.data?.task_status === 'succeed') { const videoUrl = status.data.task_result?.videos?.[0]?.url; if (!videoUrl) throw new Error('任务完成但无视频URL'); const out = outputPath || path.join(OUT_DIR, `kling-official-${taskId}.mp4`); fs.mkdirSync(path.dirname(out), { recursive: true }); console.log(`[Kling Official] 下载中...`); await downloadFile(videoUrl, out); console.log(`[Kling Official] ✅ ${path.basename(out)}`); return { videoPath: out, taskId }; } if (status.data?.task_status === 'failed') { throw new Error(`生成失败: ${status.data.task_status_msg || JSON.stringify(status)}`); } if (i % 15 === 0) console.log(`[Kling Official] 生成中... ${status.data?.task_status} (${i}/${MAX_POLL})`); } throw new Error('轮询超时'); } module.exports = { generateVideo };