UNPKG

jimeng-ai-mcp

Version:

火山引擎即梦AI多模态生成服务MCP工具

697 lines (696 loc) 32.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JimengClient = void 0; const axios_1 = __importDefault(require("axios")); const crypto_1 = __importDefault(require("crypto")); /** * 即梦AI客户端 * 使用火山引擎V4签名算法实现 */ class JimengClient { /** * 创建即梦AI客户端实例 */ constructor(config = {}) { this.accessKey = config.accessKey || process.env.JIMENG_ACCESS_KEY || ''; this.secretKey = config.secretKey || process.env.JIMENG_SECRET_KEY || ''; this.endpoint = config.endpoint || 'https://visual.volcengineapi.com'; this.host = config.host || 'visual.volcengineapi.com'; this.region = config.region || 'cn-north-1'; this.service = config.service || 'cv'; this.debug = config.debug || false; this.timeout = config.timeout || 30000; this.retries = config.retries || 3; // 验证必要的配置 if (!this.accessKey || !this.secretKey) { throw new Error('缺少必要的配置: accessKey 和 secretKey'); } if (this.debug) { console.log('JimengClient 初始化完成:'); console.log('- 端点:', this.endpoint); console.log('- 区域:', this.region); console.log('- 服务:', this.service); console.log('- AccessKey:', this.accessKey); console.log('- SecretKey:', this.secretKey.substring(0, 3) + '...(已隐藏)'); } } /** * 辅助函数:生成签名密钥 */ getSignatureKey(key, dateStamp, regionName, serviceName) { const kDate = crypto_1.default.createHmac('sha256', key).update(dateStamp).digest(); const kRegion = crypto_1.default.createHmac('sha256', kDate).update(regionName).digest(); const kService = crypto_1.default.createHmac('sha256', kRegion).update(serviceName).digest(); const kSigning = crypto_1.default.createHmac('sha256', kService).update('request').digest(); return kSigning; } /** * 格式化查询参数 */ formatQuery(parameters) { const sortedKeys = Object.keys(parameters).sort(); return sortedKeys.map(key => `${key}=${parameters[key]}`).join('&'); } /** * 火山引擎V4签名算法 */ signV4Request(reqQuery, reqBody) { const t = new Date(); const currentDate = t.toISOString().replace(/[:\-]|\.\d{3}/g, ''); const datestamp = currentDate.substring(0, 8); const method = 'POST'; const canonicalUri = '/'; const canonicalQuerystring = reqQuery; const signedHeaders = 'content-type;host;x-content-sha256;x-date'; const payloadHash = crypto_1.default.createHash('sha256').update(reqBody).digest('hex'); const contentType = 'application/json'; const canonicalHeaders = [ `content-type:${contentType}`, `host:${this.host}`, `x-content-sha256:${payloadHash}`, `x-date:${currentDate}` ].join('\n') + '\n'; const canonicalRequest = [ method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, payloadHash ].join('\n'); if (this.debug) { console.log('规范请求字符串:\n' + canonicalRequest); } const algorithm = 'HMAC-SHA256'; const credentialScope = `${datestamp}/${this.region}/${this.service}/request`; const stringToSign = [ algorithm, currentDate, credentialScope, crypto_1.default.createHash('sha256').update(canonicalRequest).digest('hex') ].join('\n'); if (this.debug) { console.log('待签名字符串:\n' + stringToSign); } const signingKey = this.getSignatureKey(this.secretKey, datestamp, this.region, this.service); const signature = crypto_1.default.createHmac('sha256', signingKey).update(stringToSign).digest('hex'); if (this.debug) { console.log('签名值:', signature); } const authorizationHeader = `${algorithm} Credential=${this.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`; const headers = { 'X-Date': currentDate, 'Authorization': authorizationHeader, 'X-Content-Sha256': payloadHash, 'Content-Type': contentType, 'Host': this.host }; const requestUrl = `${this.endpoint}?${canonicalQuerystring}`; return { headers, requestUrl }; } /** * 生成图像 */ async generateImage(params) { let lastError = null; let retryCount = 0; while (retryCount <= this.retries) { try { // 验证必要的参数 if (!params.prompt) { throw new Error('缺少必要的参数: prompt'); } // 查询参数 const queryParams = { 'Action': 'CVProcess', 'Version': '2022-08-31' }; const formattedQuery = this.formatQuery(queryParams); // 请求体参数 const bodyParams = { req_key: params.req_key || "jimeng_high_aes_general_v21_L", prompt: params.prompt, return_url: params.return_url !== undefined ? params.return_url : true, width: params.width || 512, height: params.height || 512, negative_prompt: params.negative_prompt }; // 移除undefined值 Object.keys(bodyParams).forEach(key => { if (bodyParams[key] === undefined) { delete bodyParams[key]; } }); const formattedBody = JSON.stringify(bodyParams); if (this.debug) { console.log('请求体:', formattedBody); } // 生成签名和请求头 const { headers, requestUrl } = this.signV4Request(formattedQuery, formattedBody); if (this.debug) { console.log('请求URL:', requestUrl); console.log('请求头:', JSON.stringify(headers, null, 2)); } // 发送请求 const response = await axios_1.default.post(requestUrl, bodyParams, { headers: headers, timeout: this.timeout, validateStatus: null // 允许任何状态码 }); if (this.debug) { console.log('响应状态码:', response.status); console.log('响应头:', JSON.stringify(response.headers, null, 2)); console.log('响应数据:', JSON.stringify(response.data, null, 2)); } // 处理响应 if (response.status !== 200) { throw new Error(`HTTP错误! 状态码: ${response.status}`); } // 检查API错误 if (response.data.ResponseMetadata && response.data.ResponseMetadata.Error) { const error = response.data.ResponseMetadata.Error; throw new Error(`API错误: ${error.Message || '未知错误'}`); } // 返回结果 if (response.data.data && response.data.data.image_urls && response.data.data.image_urls.length > 0) { return { success: true, image_urls: response.data.data.image_urls, raw_response: response.data }; } else { return { success: false, error: '未生成图像或响应格式不正确', raw_response: response.data }; } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (this.debug) { console.error(`尝试 #${retryCount + 1} 失败:`, lastError.message); } retryCount++; // 如果已经达到最大重试次数,返回错误 if (retryCount > this.retries) { if (this.debug) { console.error(`已达到最大重试次数 (${this.retries}),放弃重试`); } return { success: false, error: lastError.message }; } // 指数退避策略 const waitTime = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); if (this.debug) { console.log(`等待 ${waitTime}ms 后重试...`); } await new Promise(resolve => setTimeout(resolve, waitTime)); } } // 不应该运行到这里 return { success: false, error: '未知错误' }; } /** * 生成视频 - 文生视频 (异步方式: 提交任务) */ async submitVideoTask(params) { let lastError = null; const retries = 1; // 只重试一次 let retryCount = 0; while (retryCount <= retries) { try { // 验证必要的参数 if (!params.prompt) { throw new Error('缺少必要的参数: prompt'); } // 查询参数 - 使用异步提交任务API const queryParams = { 'Action': 'CVSync2AsyncSubmitTask', 'Version': '2022-08-31' }; const formattedQuery = this.formatQuery(queryParams); // 请求体参数 const bodyParams = { req_key: params.req_key || "jimeng_vgfm_t2v_l20", prompt: params.prompt }; const formattedBody = JSON.stringify(bodyParams); // 总是开启调试信息以便排查问题 console.log('提交任务请求体:', formattedBody); // 生成签名和请求头 const { headers, requestUrl } = this.signV4Request(formattedQuery, formattedBody); // 打印请求信息以便调试 console.log('提交任务请求URL:', requestUrl); console.log('提交任务请求头:', JSON.stringify(headers, null, 2)); // 发送请求 const response = await axios_1.default.post(requestUrl, bodyParams, { headers: headers, timeout: this.timeout, validateStatus: null // 允许任何状态码 }); // 打印响应信息以便调试 console.log('提交任务响应状态码:', response.status); console.log('提交任务响应数据:', JSON.stringify(response.data, null, 2)); // 处理响应 if (response.status !== 200) { // 特殊处理429错误 if (response.status === 429) { throw new Error(`API并发限制错误: 请求过于频繁,请稍后再试。详细信息: ${JSON.stringify(response.data)}`); } throw new Error(`HTTP错误! 状态码: ${response.status},详细信息: ${JSON.stringify(response.data)}`); } // 检查API错误 if (response.data.ResponseMetadata && response.data.ResponseMetadata.Error) { const error = response.data.ResponseMetadata.Error; throw new Error(`API错误: ${error.Message || '未知错误'}, 错误码: ${error.Code || '无代码'}`); } // 返回结果 - 任务ID if (response.data.data && response.data.data.task_id) { return { success: true, task_id: response.data.data.task_id, raw_response: response.data }; } else { return { success: false, error: '提交任务失败或响应格式不正确', raw_response: response.data }; } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (this.debug) { console.error(`尝试提交任务 #${retryCount + 1} 失败:`, lastError.message); } retryCount++; // 如果已经达到最大重试次数,返回错误 if (retryCount > retries) { if (this.debug) { console.error(`已达到最大重试次数 (${retries}),放弃重试`); } // 如果是429错误,给出更友好的提示 if (lastError.message.includes('429') || lastError.message.includes('并发限制')) { return { success: false, error: '请求频率受限,请等待几分钟后再尝试提交视频生成任务。火山引擎对视频生成API有严格的并发限制。' }; } return { success: false, error: lastError.message }; } // 视频API调用等待时间设为固定的60秒(1分钟),符合QPS=1的限制 const waitTime = 60000; // 60秒 = 1分钟 console.log(`请求受限,等待 ${waitTime / 1000} 秒后重试...`); console.log(`将在 ${new Date(Date.now() + waitTime).toLocaleTimeString()} 重试`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } // 不应该运行到这里 return { success: false, error: '未知错误' }; } /** * 查询视频生成任务结果 */ async getVideoTaskResult(taskId, reqKey = "jimeng_vgfm_t2v_l20") { let lastError = null; const retries = 1; // 只重试一次 let retryCount = 0; while (retryCount <= retries) { try { // 查询参数 - 使用查询结果API const queryParams = { 'Action': 'CVSync2AsyncGetResult', 'Version': '2022-08-31' }; const formattedQuery = this.formatQuery(queryParams); // 请求体参数 const bodyParams = { req_key: reqKey, task_id: taskId }; const formattedBody = JSON.stringify(bodyParams); // 总是开启调试信息以便排查问题 console.log('查询结果请求体:', formattedBody); // 生成签名和请求头 const { headers, requestUrl } = this.signV4Request(formattedQuery, formattedBody); // 开启调试信息 console.log('查询结果请求URL:', requestUrl); console.log('查询结果请求头:', headers); // 发送请求 const response = await (0, axios_1.default)({ url: requestUrl, method: 'POST', headers: headers, data: formattedBody, }); console.log('查询结果响应状态码:', response.status); console.log('查询结果响应数据:', JSON.stringify(response.data, null, 2)); // 处理响应 if (response.status === 200) { const data = response.data; // 处理服务器内部错误和其他业务错误 if (data.code !== 10000) { // 处理特定错误代码 if (data.code === 50411) { // 内容安全检查未通过 return { success: false, status: 'FAILED', error: `内容安全检查未通过: ${data.message}`, raw_response: data }; } // 其他业务错误 throw new Error(`服务器返回业务错误: ${data.message} (错误码: ${data.code})`); } // 处理状态 const taskData = data.data; const taskStatus = taskData.status; let normalizedStatus = ''; // 标准化状态值 switch (taskStatus) { case 'in_queue': normalizedStatus = 'PENDING'; break; case 'processing': normalizedStatus = 'RUNNING'; break; case 'done': normalizedStatus = 'SUCCEEDED'; break; case 'fail': normalizedStatus = 'FAILED'; break; default: normalizedStatus = taskStatus.toUpperCase(); } // 解析视频URL - 从两个可能的位置获取 let videoUrls = []; // 1. 从resp_data中解析视频URLs(需要先将字符串解析为JSON对象) if (taskData.resp_data && typeof taskData.resp_data === 'string') { try { const respData = JSON.parse(taskData.resp_data); if (respData.urls && Array.isArray(respData.urls)) { videoUrls = respData.urls; } } catch (e) { console.error('解析resp_data时出错:', e); } } // 2. 如果存在video_url字段,添加到videoUrls if (taskData.video_url && typeof taskData.video_url === 'string') { videoUrls.push(taskData.video_url); } // 返回任务状态和视频URL return { success: true, status: normalizedStatus, video_urls: videoUrls, raw_response: data }; } else { throw new Error(`HTTP错误! 状态码: ${response.status},详细信息: ${JSON.stringify(response.data)}`); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (this.debug) { console.error(`尝试查询结果 #${retryCount + 1} 失败:`, lastError.message); } retryCount++; // 如果已经达到最大重试次数,返回错误 if (retryCount > retries) { if (this.debug) { console.error(`已达到最大重试次数 (${retries}),放弃重试`); } return { success: false, error: lastError.message }; } // 查询结果API调用使用较短的重试时间 const waitTime = 5000; // 5秒 console.log(`查询失败,等待 ${waitTime / 1000} 秒后重试...`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } // 不应该运行到这里 return { success: false, error: '未知错误' }; } /** * 生成视频 - 文生视频 (保留原方法作为兼容) * @deprecated 使用异步方式生成视频,请使用 submitVideoTask 和 getVideoTaskResult 代替 */ async generateVideo(params) { console.log('警告: generateVideo 方法已过时,请使用 submitVideoTask 和 getVideoTaskResult 代替'); // 提交任务 const taskResult = await this.submitVideoTask(params); if (!taskResult.success || !taskResult.task_id) { return { success: false, error: taskResult.error || '提交任务失败' }; } console.log(`任务提交成功,任务ID: ${taskResult.task_id}`); console.log('开始轮询任务结果...'); // 轮询查询任务结果 const maxAttempts = 30; // 最多等待30次 const pollingInterval = 5000; // 5秒轮询一次 for (let i = 0; i < maxAttempts; i++) { console.log(`轮询任务结果 (${i + 1}/${maxAttempts})...`); // 查询任务结果 const result = await this.getVideoTaskResult(taskResult.task_id, params.req_key); if (result.success) { // 根据任务状态处理 if ((result.status === 'SUCCEEDED' || result.status === 'done') && result.video_urls && result.video_urls.length > 0) { console.log('视频生成成功!'); return { success: true, video_urls: result.video_urls, raw_response: result.raw_response, task_id: taskResult.task_id }; } else if (result.status === 'FAILED') { return { success: false, error: '视频生成任务失败', raw_response: result.raw_response, task_id: taskResult.task_id }; } else if (result.status === 'PENDING' || result.status === 'RUNNING') { console.log(`任务仍在进行中,状态: ${result.status},等待 ${pollingInterval / 1000} 秒后重试...`); // 任务仍在进行中,继续等待 await new Promise(resolve => setTimeout(resolve, pollingInterval)); continue; } } // 查询失败或状态异常,等待后重试 console.log('查询任务结果失败或状态异常,等待后重试...'); await new Promise(resolve => setTimeout(resolve, pollingInterval)); } // 超过最大尝试次数 return { success: false, error: '轮询任务结果超时,请使用任务ID手动查询结果', task_id: taskResult.task_id }; } /** * 提交图生视频任务 - 图片生成视频 */ async submitI2VTask(params) { var _a; let lastError = null; const retries = 1; // 只重试一次 let retryCount = 0; while (retryCount <= retries) { try { // 准备图片URL数组 let imageUrls = []; if (params.image_urls && params.image_urls.length > 0) { // 优先使用image_urls数组 imageUrls = params.image_urls; } else if (params.image_url) { // 如果没有提供image_urls但提供了image_url,则将其转换为数组 imageUrls = [params.image_url]; } else { throw new Error('缺少必要的参数: image_url 或 image_urls'); } // 查询参数 - 使用异步提交任务API const queryParams = { 'Action': 'CVSync2AsyncSubmitTask', 'Version': '2022-08-31' }; const formattedQuery = this.formatQuery(queryParams); // 请求体参数 - 默认使用图生视频模型 const bodyParams = { req_key: params.req_key || "jimeng_vgfm_i2v_l20", image_urls: imageUrls, // 必须指定aspect_ratio参数,不能使用keep_ratio aspect_ratio: params.aspect_ratio || "16:9", // 如果有提示词则添加 ...(params.prompt ? { prompt: params.prompt } : {}) }; const formattedBody = JSON.stringify(bodyParams); // 调试信息 console.log('提交任务请求体:', formattedBody); // 生成签名和请求头 const { headers, requestUrl } = this.signV4Request(formattedQuery, formattedBody); // 调试信息 console.log('提交任务请求URL:', requestUrl); console.log('提交任务请求头:', headers); // 发送请求 const response = await (0, axios_1.default)({ url: requestUrl, method: 'POST', headers: headers, data: formattedBody }); // 调试信息 console.log('提交任务响应状态码:', response.status); console.log('提交任务响应数据:', JSON.stringify(response.data, null, 2)); // 处理响应 if (response.status === 200) { if (response.data.status === 10000 || response.data.code === 10000) { // 从响应中提取任务ID const taskId = (_a = response.data.data) === null || _a === void 0 ? void 0 : _a.task_id; if (!taskId) { throw new Error('服务器未返回任务ID'); } return { success: true, task_id: taskId, raw_response: response.data }; } else { throw new Error(`API错误: ${response.data.message || '未知错误'}, 错误码: ${response.data.code || response.data.status || '无'}`); } } else { throw new Error(`HTTP错误! 状态码: ${response.status},详细信息: ${JSON.stringify(response.data)}`); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // 提取错误信息,检查是否是由于图片格式问题 const errorMsg = lastError.message || ''; if (errorMsg.includes('Image Decode Error') || errorMsg.includes('image format unsupported') || errorMsg.includes('image url')) { // 图片格式错误,提供更友好的错误信息 return { success: false, error: `图片格式不支持或无法访问。请确保提供的是可公开访问的JPEG或PNG格式图片URL。详细错误: ${errorMsg}` }; } console.error(`提交图生视频任务尝试 #${retryCount + 1} 失败: ${lastError.message}`); if (retryCount < retries) { // 计算重试等待时间 - 固定为60秒以避免API限流 const waitSeconds = 60; const nextRetryTime = new Date(Date.now() + waitSeconds * 1000); const timeString = nextRetryTime.toLocaleTimeString(); console.log(`提交任务失败,将在 ${timeString} 重试...`); // 等待指定时间 await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000)); retryCount++; } else { console.log(`已达到最大重试次数 (${retries}),放弃重试`); break; } } } return { success: false, error: lastError ? lastError.message : '提交任务失败,已达到最大重试次数' }; } /** * 生成视频 - 图片生成视频 (一步到位方式) */ async generateI2VVideo(params) { console.log('生成图生视频中...(内部会自动提交任务并轮询结果)'); // 提交任务 const taskResult = await this.submitI2VTask(params); if (!taskResult.success || !taskResult.task_id) { return { success: false, error: taskResult.error || '提交任务失败' }; } console.log(`任务提交成功,任务ID: ${taskResult.task_id}`); console.log('开始轮询任务结果...'); // 轮询查询任务结果 - 复用文生视频的查询结果方法 const maxAttempts = 30; // 最多等待30次 const pollingInterval = 5000; // 5秒轮询一次 for (let i = 0; i < maxAttempts; i++) { console.log(`轮询任务结果 (${i + 1}/${maxAttempts})...`); // 查询任务结果 const result = await this.getVideoTaskResult(taskResult.task_id, params.req_key || "jimeng_vgfm_i2v_l20"); if (result.success) { // 根据任务状态处理 if ((result.status === 'SUCCEEDED' || result.status === 'done') && result.video_urls && result.video_urls.length > 0) { console.log('视频生成成功!'); return { success: true, video_urls: result.video_urls, raw_response: result.raw_response, task_id: taskResult.task_id }; } else if (result.status === 'FAILED') { return { success: false, error: '视频生成任务失败', raw_response: result.raw_response, task_id: taskResult.task_id }; } else if (result.status === 'PENDING' || result.status === 'RUNNING' || result.status === 'in_queue') { console.log(`任务仍在进行中,状态: ${result.status},等待 ${pollingInterval / 1000} 秒后重试...`); // 任务仍在进行中,继续等待 await new Promise(resolve => setTimeout(resolve, pollingInterval)); continue; } } // 查询失败或状态异常,等待后重试 console.log('查询任务结果失败或状态异常,等待后重试...'); await new Promise(resolve => setTimeout(resolve, pollingInterval)); } // 超过最大尝试次数 return { success: false, error: '轮询任务结果超时,请使用任务ID手动查询结果', task_id: taskResult.task_id }; } } exports.JimengClient = JimengClient;