cang-ying/engines/yunwu-api-adapter.js
Guanghu Domestic Migration a2fa7d57d8 chore: import sanitized domestic snapshot for REPO-005
Source snapshot: 23037fa3a2e9b0597162735755e92fc763bf4d94
2026-07-17 15:55:48 +08:00

165 lines
5.9 KiB
JavaScript

/**
* 云雾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 };