/** * 云雾AI API 适配器 · yunwu.ai * D136+ · 铸渊 ICE-GL-ZY001 · 2026-06-21 * * Base URL: https://yunwu.ai/v1 * 模型: kling-video, kling-omni-video, kling-video-extend, kling-motion-control * 兼容 OpenAI 格式 * * 密钥: 从 video-ai-system/.env 或环境变量 YUNWU_API_KEY 读取 */ const fs = require('fs'); const path = require('path'); const https = require('https'); const envPath = path.resolve(__dirname, '../.env'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf-8'); envContent.split('\n').forEach(line => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { const [key, ...vals] = trimmed.split('='); if (key && vals.length) process.env[key.trim()] = vals.join('=').trim(); } }); } const API_KEY = process.env.YUNWU_API_KEY || ''; const BASE_URL = 'yunwu.ai'; const BASE_PATH = '/v1'; 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(300000, () => { 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); }); } /** * 云雾AI视频生成 - 两层策略: * 1. chat/completions 提交 → 检查响应中是否有视频数据 * 2. 如果chat返回空 → 尝试 video/generations 端点 */ async function generateVideo({ prompt, duration = 5, outputPath, model = 'kling-video' }) { if (!API_KEY) { throw new Error('YUNWU_API_KEY 未设置。请写入 video-ai-system/.env 或环境变量。'); } console.log('[yunwu.ai] 策略1: chat/completions 提交...'); // ═══ 策略1: chat/completions ═══ try { const chatRes = await apiRequest('POST', `${BASE_PATH}/chat/completions`, { model, messages: [{ role: 'user', content: prompt }], max_tokens: 4096, }); // 检查响应中是否有视频URL或错误信息 if (chatRes.error) { console.log(`[yunwu.ai] chat返回错误: ${JSON.stringify(chatRes.error).substring(0,200)}`); } else if (chatRes.choices && chatRes.choices[0]?.message?.content) { const content = chatRes.choices[0].message.content; // 检查是否包含视频URL const urlMatch = content.match(/https?:\/\/[^\s]+\.mp4[^\s]*/); if (urlMatch) { const videoUrl = urlMatch[0]; const out = outputPath || path.join(OUT_DIR, `yunwu-${Date.now()}.mp4`); fs.mkdirSync(path.dirname(out), { recursive: true }); console.log(`[yunwu.ai] 下载视频...`); await downloadFile(videoUrl, out); console.log(`[yunwu.ai] ✅ ${path.basename(out)}`); return { videoPath: out, taskId: chatRes.id }; } } else if (chatRes.choices === null && chatRes.usage?.prompt_tokens > 0) { console.log(`[yunwu.ai] chat接收了提示词但未生成视频(上游Kling返回空)`); } } catch (e) { console.log(`[yunwu.ai] chat异常: ${e.message}`); } // ═══ 策略2: video/generations 端点 ═══ console.log('[yunwu.ai] 策略2: video/generations 端点...'); try { const videoRes = await apiRequest('POST', `${BASE_PATH}/video/generations`, { model, prompt, duration, aspect_ratio: '16:9', }); if (videoRes.code === 500 || videoRes.error) { throw new Error(videoRes.message || videoRes.error?.message || 'video endpoint failed'); } if (videoRes.data?.task_id) { const taskId = videoRes.data.task_id; console.log(`[yunwu.ai] 任务: ${taskId}`); for (let i = 0; i < 180; i++) { await new Promise(r => setTimeout(r, 2000)); const status = await apiRequest('GET', `${BASE_PATH}/video/generations/${taskId}`); if (status.data?.task_status === 'succeed' || status.data?.task_status === 'completed') { const videoUrl = status.data.video_url || status.data.task_result?.videos?.[0]?.url; if (videoUrl) { const out = outputPath || path.join(OUT_DIR, `yunwu-${taskId}.mp4`); fs.mkdirSync(path.dirname(out), { recursive: true }); await downloadFile(videoUrl, out); console.log(`[yunwu.ai] ✅ ${path.basename(out)}`); return { videoPath: out, taskId }; } throw new Error('任务完成但无视频URL'); } if (status.data?.task_status === 'failed') { throw new Error(`任务失败: ${JSON.stringify(status.data)}`); } } throw new Error('轮询超时'); } } catch (e) { console.log(`[yunwu.ai] video端点异常: ${e.message}`); } throw new Error('yunwu.ai: 所有策略均失败。可能需要检查模型通道配置。'); } module.exports = { generateVideo };