UNPKG

@onedeepai/video_generator

Version:

阿里云文生视频MCP服务器

345 lines (304 loc) 11.2 kB
import { McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import axios from 'axios'; import * as fs from 'fs'; import * as path from 'path'; // 视频生成模型配置 const MODEL = 'wanx2.1-t2v-turbo'; // 设置API密钥和API端点 const API_KEY = process.env.DASHSCOPE_API_KEY || ''; // 从环境变量读取API密钥 const VIDEO_GENERATION_API = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis'; const TASK_STATUS_API = 'https://dashscope.aliyuncs.com/api/v1/tasks/'; // 检查API密钥是否存在 if (!API_KEY) { console.warn('警告: DASHSCOPE_API_KEY环境变量未设置,视频生成功能将无法使用'); } // API响应类型定义 interface TaskResponse { task_id?: string; output?: { task_id?: string; task_status?: string; video_url?: string; result?: { video_url?: string; }; actual_prompt?: string; }; task_status?: string; status?: string; message?: string; } // 创建MCP服务器 const server = new McpServer({ name: "文生视频MCP服务器", version: "1.0.0", description: "基于阿里云的文本生成视频工具,使用环境变量 DASHSCOPE_API_KEY 设置API密钥" }); // 添加文生视频工具 server.tool("gen_video", { prompt: z.string().min(1, "提示词不能为空"), size: z.string().regex(/^\d+\*\d+$/, "尺寸格式应为宽*高,如1280*720").optional().default("1280*720"), duration: z.number().min(1, "时长不能小于1秒").max(60, "时长不能超过60秒").optional().default(5) }, async ({ prompt, size, duration }: { prompt: string; size: string; duration: number }) => { try { // 验证API密钥 if (!API_KEY || API_KEY.trim() === '') { return { content: [{ type: "text", text: "错误: 缺少API密钥,请设置DASHSCOPE_API_KEY环境变量" }] }; } // 发送视频生成请求 const response = await axios.post( VIDEO_GENERATION_API, { model: MODEL, input: { prompt, }, parameters: { size, duration, prompt_extend: true, // 启用提示词扩展 }, }, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable', }, } ); const responseData = response.data as TaskResponse; const taskId = responseData.task_id || responseData.output?.task_id; if (!taskId) { throw new Error('无法从响应中获取任务ID'); } // 每5秒检查一次任务状态,最多检查72次(最大6分钟) const maxAttempts = 72; const intervalMs = 5000; let attempts = 0; let result: TaskResponse | null = null; let errorCount = 0; const startTime = Date.now(); while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, intervalMs)); attempts++; try { // 查询任务状态 const statusResponse = await axios.get( `${TASK_STATUS_API}${taskId}`, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, } ); result = statusResponse.data; const status = result?.task_status || result?.output?.task_status; const waitedSeconds = Math.floor((Date.now() - startTime) / 1000); if (status === 'SUCCEEDED') { break; } else if (status === 'FAILED') { throw new Error(`任务失败: ${result?.message || '未知错误'}`); } else if (status === 'PENDING' || status === 'RUNNING') { // 任务还在运行中,继续等待 // 重置错误计数,因为我们成功获取了状态 errorCount = 0; } else if (!status) { // 无法获取状态,但继续尝试 } } catch (error: any) { errorCount++; const waitedSeconds = Math.floor((Date.now() - startTime) / 1000); // 连续多次查询失败,抛出异常 if (errorCount >= 3) { throw new Error(`查询任务状态连续失败${errorCount}次: ${error.message || '未知错误'}`); } } } if (!result) { throw new Error('无法获取任务结果'); } // 检查是否达到最大尝试次数 if (attempts >= maxAttempts) { const waitedSeconds = Math.floor((Date.now() - startTime) / 1000); return { content: [ { type: "text", text: `视频生成任务仍在处理中 • 已等待 ${waitedSeconds} 秒\n` + `您可以稍后使用任务ID查询结果\n` + `任务ID: ${taskId}` } ] }; } // 获取视频URL let videoUrl = result?.output?.video_url || result?.output?.result?.video_url; if (!videoUrl) { if (result?.status === 'SUCCEEDED' || result?.output?.task_status === 'SUCCEEDED') { return { content: [ { type: "text", text: `视频生成成功,但无法获取视频URL,请查看完整响应数据\n` + `任务ID: ${taskId}\n` + `完整响应数据:\n${JSON.stringify(result, null, 2)}` } ] }; } throw new Error('无法获取视频URL'); } // 获取扩展提示词(如果有) const actualPrompt = result.output?.actual_prompt; const waitedSeconds = Math.floor((Date.now() - startTime) / 1000); // 构建响应内容 const content: any[] = [ { type: "text", text: `✅ 视频生成成功! (用时 ${waitedSeconds} 秒)\n` + `任务ID: ${taskId}\n` + `参数信息:\n` + `- 提示词: ${prompt}\n` + `- 尺寸: ${size}\n` + `- 时长: ${duration}秒\n` }, // 单独添加一个只包含视频URL的文本框,方便点击 { type: "text", text: `${videoUrl}` } ]; // 如果有扩展提示词,添加到响应 if (actualPrompt) { content.push({ type: "text", text: `\n系统扩展生成的完整提示词:\n${actualPrompt}` }); } return { content }; } catch (error: any) { console.error('视频生成请求失败:'); let errorMessage = ''; if (axios.isAxiosError(error) && error.response) { console.error('API错误响应:', error.response.data); errorMessage = error.response.data.message || '未知错误'; // 展示完整错误数据 return { content: [{ type: "text", text: `生成视频失败: ${errorMessage}\n\n` + `API错误详情:\n${JSON.stringify(error.response.data, null, 2)}` }] }; } errorMessage = error.message || '未知错误'; return { content: [{ type: "text", text: `生成视频失败: ${errorMessage}` }] }; } } ); // 添加查询任务状态工具 server.tool("check_task", { taskId: z.string().min(1, "任务ID不能为空") }, async ({ taskId }: { taskId: string }) => { try { // 验证API密钥 if (!API_KEY || API_KEY.trim() === '') { return { content: [{ type: "text", text: "错误: 缺少API密钥,请设置DASHSCOPE_API_KEY环境变量" }] }; } // 查询任务状态 const response = await axios.get( `${TASK_STATUS_API}${taskId}`, { headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, } ); const result = response.data as TaskResponse; const status = result.task_status || result.output?.task_status; // 获取视频URL let videoUrl = result.output?.video_url || result.output?.result?.video_url; // 获取扩展提示词(如果有) const actualPrompt = result.output?.actual_prompt; let content: any[] = []; // 基于状态构建不同的响应 if (status === 'SUCCEEDED' && videoUrl) { content.push({ type: "text", text: `✅ 视频生成成功!\n` + `任务ID: ${taskId}\n` }); // 单独添加一个只包含视频URL的文本框,方便点击 content.push({ type: "text", text: `${videoUrl}` }); if (actualPrompt) { content.push({ type: "text", text: `\n系统扩展生成的完整提示词:\n${actualPrompt}` }); } } else if (status === 'SUCCEEDED' && !videoUrl) { content.push({ type: "text", text: `任务已完成,但无法获取视频URL\n完整响应数据:\n${JSON.stringify(result, null, 2)}` }); } else if (status === 'FAILED') { content.push({ type: "text", text: `❌ 任务失败\n失败原因: ${result.message || '未知错误'}` }); } else if (status === 'PENDING' || status === 'RUNNING') { content.push({ type: "text", text: `⏳ 任务正在运行中\n状态: ${status}\n任务ID: ${taskId}` }); } else { content.push({ type: "text", text: `⚠️ 无法识别任务状态\n状态值: ${status || '无状态'}\n完整响应数据:\n${JSON.stringify(result, null, 2)}` }); } return { content }; } catch (error: any) { // 处理错误 const errorMessage = error.response?.data?.message || error.message || '未知错误'; if (error.response?.status === 404) { return { content: [{ type: "text", text: `错误: 找不到指定的任务ID (${taskId})` }] }; } return { content: [{ type: "text", text: `查询任务状态失败: ${errorMessage}` }] }; } } ); // 初始化stdin/stdout传输 const transport = new StdioServerTransport(); // 启动服务器的异步函数 export async function startServer() { console.log("文生视频MCP服务器正在启动..."); await server.connect(transport); console.log("文生视频MCP服务器已启动,等待连接..."); } // 如果直接运行此文件,则启动服务器 if (require.main === module) { startServer().catch(error => { console.error("服务器启动错误:", error); process.exit(1); }); }