Skip to main content
Glama
Qihoo360

360 AI Cloud Drive MCP Server

by Qihoo360

video-download

Save videos directly to 360 AI Cloud Drive by providing URLs. This tool enables users to download multiple videos at once, supporting efficient file management. Ensure sufficient timeout settings for larger downloads.

Instructions

下载视频到云盘。注意:此操作可能需要较长时间,建议客户端设置更长的超时时间(建议300秒以上)。

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlsYes视频URL,多个URL使用英文竖线'|'分隔

Implementation Reference

  • The registerVideoDownloadTool function registers the "video-download" tool on the MCP server. It defines the tool name, description, input schema (urls: string), and the full handler logic including API calls to create download tasks, progress polling with notifications, error handling, and formatted response with cloud storage links.
    export function registerVideoDownloadTool(server: McpServer) { server.tool( "video-download", "下载视频到云盘。注意:此操作可能需要较长时间,建议客户端设置更长的超时时间(建议300秒以上)。", { urls: z.string().describe("视频URL,多个URL使用英文竖线'|'分隔") }, async ({ urls }, mcpReq) => { // 参数验证 if (!urls) { return { content: [{ type: "text", text: "❌ 参数错误: 必须提供urls参数" }] }; } const httpContext = gethttpContext(mcpReq, server); const transportAuthInfo = httpContext.authInfo; try { let authInfo: AuthInfo; try { // 获取鉴权信息 authInfo = await getAuthInfo({ method: 'File.createCloudDownload' }, transportAuthInfo); console.error('authInfo==video-download', authInfo); authInfo.request_url = getConfig(transportAuthInfo?.ecsEnv).request_url; } catch (authError) { console.error("获取鉴权信息失败:", authError); throw new Error("获取鉴权信息失败"); } // 调用创建下载任务API const createResult = await createVideoDownload(authInfo, urls); console.error('createResult==video-download', JSON.stringify(createResult, null, 2)); if (createResult && createResult.errno === 0) { const taskData = createResult.data; if (!taskData || taskData.length === 0) { return { content: [{ type: "text", text: "❌ 创建下载任务失败: 未获取到任务信息" }] }; } // 提取所有任务ID const taskIds = taskData.map((task: any) => task.task_id).join(','); // 🔄 恢复一体化下载体验:自动轮询直到完成 console.error(`✅ 任务创建成功,开始轮询进度,任务ID: ${taskIds}`); // 使用配置的轮询参数 const timeoutConfig = TOOL_TIMEOUT_CONFIG.VIDEO_DOWNLOAD; // 轮询任务状态直到完成 try { const finalResult = await pollDownloadProgress( taskIds, transportAuthInfo, async (notification) => { await mcpReq.sendNotification(notification); }, timeoutConfig.pollInterval, timeoutConfig.maxPollAttempts ); if (!finalResult?.data || !Array.isArray(finalResult.data)) { // 轮询超时且没有获取到任务数据,提供基本的任务信息 let timeoutText = "❌ 轮询超时或获取最终结果失败\n\n"; timeoutText += `📋 任务信息:\n${taskData.map((task: any) => `• 任务ID: ${task.task_id}\n• URL: ${task.url}` ).join('\n\n')}\n\n`; timeoutText += `💡 请稍后在云盘传输列表中查询任务状态: ${taskIds}\n\n`; // 为每个任务生成默认查看路径 timeoutText += '📁 您也可以在以下路径查看文件(如果任务已完成):\n'; taskData.forEach((task: any, index: number) => { if (task.qid) { const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.default_path ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid}&owner_qid=${task.qid}`; timeoutText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); return { content: [{ type: "text", text: timeoutText }] }; } // 分析最终结果 const completedTasks: any[] = []; const failedTasks: any[] = []; finalResult.data.forEach((task: any) => { const numericStatus = Number(task.status); if (numericStatus === 4) { // 任务完成(上传成功),添加云盘链接 if (task.upload_info && task.upload_info.data && task.upload_info.data.path && task.upload_info.data.nid && task.qid) { const dirPath = task.upload_info.data.path.substring(0, task.upload_info.data.path.lastIndexOf('/') + 1); const href = `https://www.yunpan.com/file/index#/fileManage/my/file/${encodeURIComponent(dirPath)}?focus_nid=${task.upload_info.data.nid}&owner_qid=${task.qid}`; task.upload_info.data.href = href; } completedTasks.push(task); } else if (numericStatus === 5) { // 任务失败 failedTasks.push(task); } }); // 格式化最终结果 let resultText: string; const hasFinishedTasks = completedTasks.length > 0 || failedTasks.length > 0; if (hasFinishedTasks) { resultText = `🎬 视频下载完成!\n\n`; } else { resultText = `⏳ 视频下载轮询超时\n\n`; } if (completedTasks.length > 0) { resultText += `✅ 成功下载 ${completedTasks.length} 个视频:\n`; completedTasks.forEach((task, index) => { resultText += `${index + 1}. **视频${task.task_id}**\n`; resultText += ` 📹 原始链接: ${task.url}\n`; if (task.upload_info?.data?.href) { const fileSize = task.upload_info.data.size ? formatBytes(task.upload_info.data.size) : '未知大小'; resultText += ` 📁 [点击查看云盘文件](${task.upload_info.data.href})\n`; resultText += ` 📂 云盘路径: ${task.upload_info.data.path}\n`; resultText += ` 📦 文件大小: ${fileSize}\n`; resultText += ` 🔑 文件NID: ${task.upload_info.data.nid}\n`; resultText += ` 👤 用户QID: ${task.qid}\n`; } resultText += '\n'; }); } if (failedTasks.length > 0) { resultText += `❌ 下载失败 ${failedTasks.length} 个视频:\n`; failedTasks.forEach((task, index) => { // ❗️ 关键改造: 使用更健壮的逻辑链来查找最具体的错误信息 const failureReason = task.progress?.error || task.upload_info?.errmsg || task.progress?.status_desc || task.errmsg || '未知错误'; resultText += `${index + 1}. **视频${task.task_id}**: ${task.url}\n`; resultText += ` - **失败原因**: ${failureReason}\n`; }); resultText += '\n'; } if (completedTasks.length === 0 && failedTasks.length === 0) { resultText += '部分任务仍在处理中(待开始/下载中/下载成功但未上传完成)\n'; resultText += '💡 请稍后在云盘传输列表中查询任务状态\n'; // resultText += `📝 任务ID列表: ${taskIds}\n`; // 🔗 为超时的任务生成默认查看路径 const timeoutTasks = finalResult.data.filter((task: any) => { const status = Number(task.status); return status !== 4 && status !== 5; // 不是成功也不是失败的任务 }); if (timeoutTasks.length > 0) { resultText += '\n📁 您可以在以下路径查看文件(如果任务已完成):\n'; timeoutTasks.forEach((task: any, index: number) => { if (task.qid) { // 生成默认查看路径,类似 href 但指向目录 const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.default_path ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid || ''}&owner_qid=${task.qid}`; resultText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); } } // 为结构化数据确定最终状态 const finalStatus = (completedTasks.length === 0 && failedTasks.length === 0) ? 'timeout' : (failedTasks.length > 0) ? (completedTasks.length > 0 ? 'partial_success' : 'all_failed') : 'all_success'; // 混合模式返回 return { content: [ // 1. 给机器看的结构化数据 (序列化为JSON字符串) { type: "text", name: "x-download-result-json", text: JSON.stringify({ status: finalStatus, completedTasks: completedTasks, failedTasks: failedTasks, taskIds: taskIds }) }, // 2. 给人看的预格式化文本 { type: "text", name: "x-download-result-display", text: resultText } ] }; } catch (pollError: any) { console.error('轮询过程出错:', pollError); let errorText = `✅ 任务创建成功,但轮询过程中出现问题: ${pollError.message}\n\n`; errorText += `📋 任务信息:\n${taskData.map((task: any) => `• 任务ID: ${task.task_id}\n• URL: ${task.url}` ).join('\n\n')}\n\n`; errorText += `💡 请稍后在云盘传输列表中查询任务状态: ${taskIds}\n\n`; // 为每个任务生成默认查看路径 errorText += '📁 您可以在以下路径查看文件(如果任务已完成):\n'; taskData.forEach((task: any, index: number) => { if (task.qid) { const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.dirPath ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid || ''}&owner_qid=${task.qid}`; errorText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); return { content: [ { type: "text", text: errorText } ] }; } } else { throw new Error(createResult?.errmsg || "API请求失败"); } } catch (error: any) { return { content: [ { type: "text", text: `下载视频时发生错误: ${error.message}`, } ], }; } }, ); }
  • Core handler function that executes the video download: authenticates, creates cloud download tasks for given URLs, polls progress extensively with real-time notifications, handles completion/failure, generates cloud disk links for successful downloads.
    async ({ urls }, mcpReq) => { // 参数验证 if (!urls) { return { content: [{ type: "text", text: "❌ 参数错误: 必须提供urls参数" }] }; } const httpContext = gethttpContext(mcpReq, server); const transportAuthInfo = httpContext.authInfo; try { let authInfo: AuthInfo; try { // 获取鉴权信息 authInfo = await getAuthInfo({ method: 'File.createCloudDownload' }, transportAuthInfo); console.error('authInfo==video-download', authInfo); authInfo.request_url = getConfig(transportAuthInfo?.ecsEnv).request_url; } catch (authError) { console.error("获取鉴权信息失败:", authError); throw new Error("获取鉴权信息失败"); } // 调用创建下载任务API const createResult = await createVideoDownload(authInfo, urls); console.error('createResult==video-download', JSON.stringify(createResult, null, 2)); if (createResult && createResult.errno === 0) { const taskData = createResult.data; if (!taskData || taskData.length === 0) { return { content: [{ type: "text", text: "❌ 创建下载任务失败: 未获取到任务信息" }] }; } // 提取所有任务ID const taskIds = taskData.map((task: any) => task.task_id).join(','); // 🔄 恢复一体化下载体验:自动轮询直到完成 console.error(`✅ 任务创建成功,开始轮询进度,任务ID: ${taskIds}`); // 使用配置的轮询参数 const timeoutConfig = TOOL_TIMEOUT_CONFIG.VIDEO_DOWNLOAD; // 轮询任务状态直到完成 try { const finalResult = await pollDownloadProgress( taskIds, transportAuthInfo, async (notification) => { await mcpReq.sendNotification(notification); }, timeoutConfig.pollInterval, timeoutConfig.maxPollAttempts ); if (!finalResult?.data || !Array.isArray(finalResult.data)) { // 轮询超时且没有获取到任务数据,提供基本的任务信息 let timeoutText = "❌ 轮询超时或获取最终结果失败\n\n"; timeoutText += `📋 任务信息:\n${taskData.map((task: any) => `• 任务ID: ${task.task_id}\n• URL: ${task.url}` ).join('\n\n')}\n\n`; timeoutText += `💡 请稍后在云盘传输列表中查询任务状态: ${taskIds}\n\n`; // 为每个任务生成默认查看路径 timeoutText += '📁 您也可以在以下路径查看文件(如果任务已完成):\n'; taskData.forEach((task: any, index: number) => { if (task.qid) { const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.default_path ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid}&owner_qid=${task.qid}`; timeoutText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); return { content: [{ type: "text", text: timeoutText }] }; } // 分析最终结果 const completedTasks: any[] = []; const failedTasks: any[] = []; finalResult.data.forEach((task: any) => { const numericStatus = Number(task.status); if (numericStatus === 4) { // 任务完成(上传成功),添加云盘链接 if (task.upload_info && task.upload_info.data && task.upload_info.data.path && task.upload_info.data.nid && task.qid) { const dirPath = task.upload_info.data.path.substring(0, task.upload_info.data.path.lastIndexOf('/') + 1); const href = `https://www.yunpan.com/file/index#/fileManage/my/file/${encodeURIComponent(dirPath)}?focus_nid=${task.upload_info.data.nid}&owner_qid=${task.qid}`; task.upload_info.data.href = href; } completedTasks.push(task); } else if (numericStatus === 5) { // 任务失败 failedTasks.push(task); } }); // 格式化最终结果 let resultText: string; const hasFinishedTasks = completedTasks.length > 0 || failedTasks.length > 0; if (hasFinishedTasks) { resultText = `🎬 视频下载完成!\n\n`; } else { resultText = `⏳ 视频下载轮询超时\n\n`; } if (completedTasks.length > 0) { resultText += `✅ 成功下载 ${completedTasks.length} 个视频:\n`; completedTasks.forEach((task, index) => { resultText += `${index + 1}. **视频${task.task_id}**\n`; resultText += ` 📹 原始链接: ${task.url}\n`; if (task.upload_info?.data?.href) { const fileSize = task.upload_info.data.size ? formatBytes(task.upload_info.data.size) : '未知大小'; resultText += ` 📁 [点击查看云盘文件](${task.upload_info.data.href})\n`; resultText += ` 📂 云盘路径: ${task.upload_info.data.path}\n`; resultText += ` 📦 文件大小: ${fileSize}\n`; resultText += ` 🔑 文件NID: ${task.upload_info.data.nid}\n`; resultText += ` 👤 用户QID: ${task.qid}\n`; } resultText += '\n'; }); } if (failedTasks.length > 0) { resultText += `❌ 下载失败 ${failedTasks.length} 个视频:\n`; failedTasks.forEach((task, index) => { // ❗️ 关键改造: 使用更健壮的逻辑链来查找最具体的错误信息 const failureReason = task.progress?.error || task.upload_info?.errmsg || task.progress?.status_desc || task.errmsg || '未知错误'; resultText += `${index + 1}. **视频${task.task_id}**: ${task.url}\n`; resultText += ` - **失败原因**: ${failureReason}\n`; }); resultText += '\n'; } if (completedTasks.length === 0 && failedTasks.length === 0) { resultText += '部分任务仍在处理中(待开始/下载中/下载成功但未上传完成)\n'; resultText += '💡 请稍后在云盘传输列表中查询任务状态\n'; // resultText += `📝 任务ID列表: ${taskIds}\n`; // 🔗 为超时的任务生成默认查看路径 const timeoutTasks = finalResult.data.filter((task: any) => { const status = Number(task.status); return status !== 4 && status !== 5; // 不是成功也不是失败的任务 }); if (timeoutTasks.length > 0) { resultText += '\n📁 您可以在以下路径查看文件(如果任务已完成):\n'; timeoutTasks.forEach((task: any, index: number) => { if (task.qid) { // 生成默认查看路径,类似 href 但指向目录 const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.default_path ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid || ''}&owner_qid=${task.qid}`; resultText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); } } // 为结构化数据确定最终状态 const finalStatus = (completedTasks.length === 0 && failedTasks.length === 0) ? 'timeout' : (failedTasks.length > 0) ? (completedTasks.length > 0 ? 'partial_success' : 'all_failed') : 'all_success'; // 混合模式返回 return { content: [ // 1. 给机器看的结构化数据 (序列化为JSON字符串) { type: "text", name: "x-download-result-json", text: JSON.stringify({ status: finalStatus, completedTasks: completedTasks, failedTasks: failedTasks, taskIds: taskIds }) }, // 2. 给人看的预格式化文本 { type: "text", name: "x-download-result-display", text: resultText } ] }; } catch (pollError: any) { console.error('轮询过程出错:', pollError); let errorText = `✅ 任务创建成功,但轮询过程中出现问题: ${pollError.message}\n\n`; errorText += `📋 任务信息:\n${taskData.map((task: any) => `• 任务ID: ${task.task_id}\n• URL: ${task.url}` ).join('\n\n')}\n\n`; errorText += `💡 请稍后在云盘传输列表中查询任务状态: ${taskIds}\n\n`; // 为每个任务生成默认查看路径 errorText += '📁 您可以在以下路径查看文件(如果任务已完成):\n'; taskData.forEach((task: any, index: number) => { if (task.qid) { const defaultPath = `https://www.yunpan.com/file/index#/fileManage/my/file${task.dirPath ? encodeURIComponent(task.default_path) : '/AI为我下载/'}?focus_nid=${task.upload_info?.data?.nid || ''}&owner_qid=${task.qid}`; errorText += `${index + 1}. [查看云盘文件](${defaultPath})\n`; } }); return { content: [ { type: "text", text: errorText } ] }; } } else { throw new Error(createResult?.errmsg || "API请求失败"); } } catch (error: any) { return { content: [ { type: "text", text: `下载视频时发生错误: ${error.message}`, } ], }; } },
  • Zod schema for tool input: single string parameter 'urls' supporting multiple URLs separated by '|'.
    { urls: z.string().describe("视频URL,多个URL使用英文竖线'|'分隔") },
  • In the main tools index, calls registerVideoDownloadTool to add the video-download tool to the server.
    registerVideoDownloadTool(server);
  • Key helper function for polling download task progress, sending real-time notifications, and determining completion.
    async function pollDownloadProgress( taskIds: string, transportAuthInfo: any, // ⚠️ stdio模式下此参数可能为undefined sendNotification: (notification: any) => Promise<void>, interval = TOOL_TIMEOUT_CONFIG.VIDEO_DOWNLOAD.pollInterval, maxAttempts = TOOL_TIMEOUT_CONFIG.VIDEO_DOWNLOAD.maxPollAttempts ): Promise<any> { let attempts = 0; let result; // 兼容stdio模式:即使transportAuthInfo未定义,也尝试从环境变量获取ecsEnv const ecsEnv = transportAuthInfo?.ecsEnv || process.env.ECS_ENV || 'prod'; // 🔒 使用固定的传输认证信息,避免并发请求中API key串用 console.error(`[轮询初始化] 固定使用API Key: ${transportAuthInfo?.apiKey}... (避免并发串用)`); // 发送初始进度通知 await safeSendNotification(sendNotification, { method: "notifications/progress", params: { progressToken: Date.now(), progress: 0, total: 100, message: "开始轮询下载进度..." } }, "轮询初始化"); while (attempts < maxAttempts) { attempts++; // 🔧 基于固定的传输认证信息重新生成API认证(保证认证时效性) let currentAuthInfo: AuthInfo; try { // 检查固定的认证信息是否仍然有效 // 兼容stdio模式:transportAuthInfo可以为undefined,getAuthInfo会从环境变量中回退 // if (!transportAuthInfo) { // console.error(`[轮询第${attempts}次] 固定认证信息无效,退出轮询`); // break; // } // 基于固定的传输认证信息重新生成API认证(刷新签名等时效信息) const extraParams = { task_id: taskIds }; currentAuthInfo = await getAuthInfo({ method: 'File.getDownloadProgress', extraParams: extraParams }, transportAuthInfo); // transportAuthInfo在stdio模式下为undefined,getAuthInfo会从环境变量回退 currentAuthInfo.request_url = getConfig(ecsEnv).request_url; console.error(`[轮询第${attempts}次] 使用固定API Key生成新签名: ${transportAuthInfo?.apiKey}...`); } catch (authError) { console.error(`[轮询第${attempts}次] 基于固定认证生成签名失败:`, authError); if (attempts >= maxAttempts) { throw authError; } // 认证失败时等待后重试 await new Promise(resolve => setTimeout(resolve, interval)); continue; } try { result = await queryDownloadProgress(currentAuthInfo, taskIds); console.error('result==pollDownloadProgress', JSON.stringify(result, null, 2)); if (result.errno !== 0) { throw new Error(result.errmsg || '查询下载进度失败'); } if (!result.data || !Array.isArray(result.data)) { console.error(`轮询第${attempts}次: 未获取到任务数据`); await new Promise(resolve => setTimeout(resolve, interval)); continue; } // 📊 计算总体进度并发送通知 const statusCounts = { '1': 0, '2': 0, '3': 0, '4': 0, '5': 0 }; let totalTasks = result.data.length; result.data.forEach((task: any) => { const status = String(task.status); if (statusCounts.hasOwnProperty(status)) { statusCounts[status as keyof typeof statusCounts]++; } }); // 计算进度百分比:完成的任务 / 总任务数 const completedTasks = statusCounts['4'] + statusCounts['5']; // 成功 + 失败都算完成 const progressPercent = Math.round((completedTasks / totalTasks) * 100); // 🔔 发送实时进度通知 await safeSendNotification(sendNotification, { method: "notifications/progress", params: { progressToken: Date.now(), progress: progressPercent, total: 100, message: `下载进度 ${progressPercent}% (${completedTasks}/${totalTasks}) - 待开始:${statusCounts['1']}, 下载中:${statusCounts['2']}, 下载成功:${statusCounts['3']}, 上传成功:${statusCounts['4']}, 失败:${statusCounts['5']}` } }, `轮询第${attempts}次-进度`); // 📢 发送状态日志通知 await safeSendNotification(sendNotification, { method: "notifications/message", params: { level: "info", data: `轮询第${attempts}次: 任务状态分布 - 待开始:${statusCounts['1']}, 下载中:${statusCounts['2']}, 下载成功:${statusCounts['3']}, 上传成功:${statusCounts['4']}, 失败:${statusCounts['5']}` } }, `轮询第${attempts}次-状态`); // 检查所有任务是否都已进入最终状态(4=上传成功, 5=失败) const allTasksAreFinished = result.data.every((task: any) => { const status = Number(task.status); return status === 4 || status === 5; }); if (allTasksAreFinished) { console.error(`轮询第${attempts}次: 所有任务已进入最终状态(成功或失败),结束轮询。`); // 📢 发送完成通知 await safeSendNotification(sendNotification, { method: "notifications/message", params: { level: "info", data: `所有下载任务已完成!成功:${statusCounts['4']}, 失败:${statusCounts['5']}` } }, "任务完成"); return result; } console.error(`轮询第${attempts}次: 任务状态分布 - 待开始:${statusCounts['1']}, 下载中:${statusCounts['2']}, 下载成功:${statusCounts['3']}, 上传成功:${statusCounts['4']}, 失败:${statusCounts['5']}`); // ✅ 优化:仅在还有下一次尝试时才打印"等待"日志并执行等待 if (attempts < maxAttempts) { console.error(`轮询第${attempts}次: 任务进行中,等待${interval/1000}秒后继续查询...`); await new Promise(resolve => setTimeout(resolve, interval)); } else { // 这是最后一次尝试,只打印结束信息,不再等待 console.error(`轮询第${attempts}次: 已达到最大尝试次数,结束轮询。`); // 📢 发送超时通知 await safeSendNotification(sendNotification, { method: "notifications/message", params: { level: "warning", data: `轮询超时,但任务可能仍在后台处理中,请稍后查看云盘` } }, "轮询超时"); } } catch (error) { console.error(`轮询第${attempts}次查询失败:`, error); // 📢 发送错误通知 await safeSendNotification(sendNotification, { method: "notifications/message", params: { level: "error", data: `轮询第${attempts}次查询失败: ${error}` } }, `轮询第${attempts}次-错误`); if (attempts >= maxAttempts) { throw error; } await new Promise(resolve => setTimeout(resolve, interval)); } } // 即使超时,也返回最后的结果数据,让调用方能够处理 if (result && result.data) { console.error(`轮询超时,但返回最后获取到的任务数据供处理`); } return result; }

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Qihoo360/ecs_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server