Wan2.1-UMT5快速上手:Node.js环境下的API调用与前端界面开发
Wan2.1-UMT5快速上手Node.js环境下的API调用与前端界面开发你是不是刚接触Wan2.1-UMT5这个模型想自己动手搭个界面来试试效果或者你是个全栈开发者想快速了解怎么把这类AI模型服务集成到自己的项目里这篇文章就是为你准备的。我会带你从零开始在Node.js环境下一步步搭建一个能调用Wan2.1-UMT5服务的前端演示界面。整个过程不复杂你只需要对JavaScript和基础的Web开发有点了解就行。我们会搞定Node.js环境、用Express写个简单的后端代理、再写个能显示生成进度的前端页面。跟着做下来你就能拥有一个属于自己的、能实时看到模型工作状态的演示工具了。1. 环境准备搞定Node.js和项目初始化万事开头难但第一步其实很简单。我们先把“地基”打好。1.1 安装Node.js和npmNode.js是我们的运行环境npm是它的包管理器。如果你还没安装去Node.js官网下载最新的LTS长期支持版本安装就行它会自动包含npm。安装完成后打开你的终端或命令行工具输入下面两条命令来检查是否安装成功node -v npm -v如果这两条命令都输出了版本号比如v18.17.0和9.6.7那就说明安装没问题可以继续了。1.2 创建项目并安装依赖接下来我们创建一个新的项目文件夹并初始化它。在你喜欢的位置新建一个文件夹比如叫wan2-umt5-demo。打开终端进入这个文件夹。运行npm init -y。这个命令会快速创建一个package.json文件里面记录了项目的基本信息和依赖。安装我们需要的包。这次我们主要用两个express: 一个非常流行的Node.js Web框架用来快速搭建我们的后端服务器。node-fetch: 一个在Node.js环境中模拟浏览器fetchAPI的库方便我们向后端模型服务发送请求。在终端里运行安装命令npm install express node-fetch安装完成后你的package.json文件里dependencies部分应该能看到这两个包。你的项目“工具箱”就准备好了。2. 搭建后端代理服务器为什么需要后端代理主要是出于安全考虑。前端代码HTML/JS是暴露给用户的如果直接把模型服务的密钥或地址写在前端很容易泄露。所以我们让Node.js后端作为一个“中间人”前端把请求发给后端后端再转发给真正的模型服务最后把结果返回给前端。2.1 创建后端主文件在项目根目录下创建一个新文件命名为server.js。这个文件将是我们后端服务的全部代码。打开server.js我们开始写代码。我会逐段解释// 引入所需的模块 const express require(express); const fetch require(node-fetch); const path require(path); // 创建Express应用 const app express(); // 设置服务器监听的端口如果环境变量有指定就用指定的否则用3000 const PORT process.env.PORT || 3000; // 假设的Wan2.1-UMT5模型服务地址和密钥 // 注意这里需要替换成你实际可用的服务地址和API密钥 const MODEL_API_URL https://your-model-service.com/v1/generate; const API_KEY your-actual-api-key-here; // 中间件解析JSON格式的请求体 app.use(express.json()); // 中间件提供静态文件服务将‘public’文件夹下的文件如HTML, CSS, JS直接提供给前端 app.use(express.static(path.join(__dirname, public)));这段代码做了几件事引入了必要的模块创建了Express应用定义了端口和需要你后续替换的模型服务配置并设置了两项中间件。express.json()能让我们方便地处理前端发来的JSON数据express.static则指定了一个叫public的文件夹用来存放我们的前端页面这样我们访问服务器时就能直接看到页面了。2.2 创建代理API接口接下来我们添加一个关键的接口用来接收前端的请求并转发给模型服务。// 定义处理生成请求的API端点 app.post(/api/generate, async (req, res) { // 从前端请求中获取用户输入的文本 const { prompt } req.body; // 简单的输入验证 if (!prompt || prompt.trim().length 0) { return res.status(400).json({ error: 请输入有效的文本提示。 }); } console.log(收到生成请求提示词: ${prompt}); try { // 准备请求体这里根据实际模型服务的API文档来构造 const requestBody { prompt: prompt, max_tokens: 150, // 生成的最大token数可调整 temperature: 0.7, // 生成随机性值越高越有创意越低越稳定 // 可以添加其他模型支持的参数 }; // 向模型服务发起请求 const modelResponse await fetch(MODEL_API_URL, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, // 通常的API密钥传递方式 }, body: JSON.stringify(requestBody), }); // 检查模型服务是否返回成功 if (!modelResponse.ok) { const errorText await modelResponse.text(); throw new Error(模型服务错误: ${modelResponse.status} - ${errorText}); } // 解析模型服务返回的JSON数据 const result await modelResponse.json(); console.log(模型生成成功。); // 将结果返回给前端 res.json({ success: true, generated_text: result.choices?.[0]?.text || result.generated_text || 未收到有效生成内容。, // 返回完整的原始结果便于调试 raw_response: result }); } catch (error) { console.error(请求模型服务失败:, error); // 将错误信息返回给前端方便排查 res.status(500).json({ success: false, error: 服务器内部错误: ${error.message} }); } });这个/api/generate接口是整个后端逻辑的核心。它接收前端发来的prompt提示词构造一个符合模型服务要求的请求然后使用node-fetch发送出去。成功拿到结果后再整理一下格式返回给前端。如果中途出错也会捕获错误并返回给前端明确的提示。2.3 启动服务器最后我们让服务器运行起来。// 启动服务器开始监听指定端口 app.listen(PORT, () { console.log( 后端代理服务器已启动运行在 http://localhost:${PORT}); console.log( 请确保已将 MODEL_API_URL 和 API_KEY 替换为真实值。); });现在你的server.js文件就完成了。不过先别急着运行因为我们还没有前端页面而且MODEL_API_URL和API_KEY还是占位符。我们先把前端做好。3. 开发前端演示界面前端的目标是做一个简洁的页面一个输入框让用户写提示词一个按钮来触发生成一个区域来显示实时的生成进度和最终结果。3.1 创建项目结构在项目根目录下创建一个名为public的文件夹。这个文件夹就是我们之前在后端代码中指定的静态文件目录。在public文件夹里创建两个文件index.html: 我们的主页面。style.css: 页面的样式文件可选为了让页面好看点。3.2 编写HTML页面打开public/index.html写入以下内容!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleWan2.1-UMT5 前端演示/title link relstylesheet hrefstyle.css link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css /head body div classcontainer header h1i classfas fa-robot/i Wan2.1-UMT5 文本生成演示/h1 p classsubtitle在下方输入提示词体验模型的文本生成能力。生成过程将实时显示。/p /header main section classinput-section label forpromptInput请输入您的提示词/label textarea idpromptInput placeholder例如写一首关于春天的七言绝句... rows4/textarea div classbutton-group button idgenerateBtn classbtn-primary i classfas fa-bolt/i 开始生成 /button button idclearBtn classbtn-secondary i classfas fa-broom/i 清空 /button /div /section section classoutput-section h2i classfas fa-stream/i 生成进度与结果/h2 div classstatus-container !-- 状态指示器 -- div idstatusIndicator classstatus idle i classfas fa-clock/i span等待输入.../span /div !-- 进度条容器初始隐藏 -- div idprogressContainer classprogress-container styledisplay: none; div classprogress-bar div idprogressFill classprogress-fill/div /div span idprogressText0%/span /div /div !-- 结果显示区域 -- div classresult-container pre idresultOutput生成的内容将显示在这里.../pre /div div classraw-info details summary查看原始API响应用于调试/summary pre idrawResponseOutput{}/pre /details /div /section /main footer p本演示界面基于 Node.js Express 构建用于调用 Wan2.1-UMT5 模型服务。/p /footer /div script srcscript.js/script /body /html这个页面结构清晰有标题和说明、输入区、按钮、状态/进度显示区、结果展示区还有一个可折叠的原始响应查看区方便调试。3.3 添加一些基础样式为了让页面不那么“原始”我们加一点简单的CSS。打开public/style.css* { box-sizing: border-box; margin: 0; padding: 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; min-height: 100vh; } .container { max-width: 900px; margin: 0 auto; background: white; border-radius: 16px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); padding: 40px; border: 1px solid #eaeaea; } header { text-align: center; margin-bottom: 40px; border-bottom: 2px solid #f0f0f0; padding-bottom: 25px; } header h1 { color: #2c3e50; margin-bottom: 10px; font-size: 2.5rem; } header .subtitle { color: #7f8c8d; font-size: 1.1rem; } .input-section, .output-section { margin-bottom: 40px; } label { display: block; margin-bottom: 10px; font-weight: 600; color: #34495e; } textarea { width: 100%; padding: 18px; border: 2px solid #ddd; border-radius: 10px; font-size: 16px; resize: vertical; transition: border 0.3s; margin-bottom: 20px; } textarea:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); } .button-group { display: flex; gap: 15px; } button { padding: 14px 28px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; } .btn-primary { background-color: #3498db; color: white; } .btn-primary:hover:not(:disabled) { background-color: #2980b9; transform: translateY(-2px); } .btn-secondary { background-color: #95a5a6; color: white; } .btn-secondary:hover { background-color: #7f8c8d; } button:disabled { opacity: 0.6; cursor: not-allowed; transform: none !important; } .output-section h2 { color: #2c3e50; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .status-container { background-color: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 25px; } .status { display: flex; align-items: center; gap: 12px; font-weight: 600; padding: 12px; border-radius: 8px; } .status.idle { color: #7f8c8d; background-color: #ecf0f1; } .status.processing { color: #f39c12; background-color: #fef5e7; } .status.success { color: #27ae60; background-color: #eafaf1; } .status.error { color: #e74c3c; background-color: #fdedec; } .progress-container { display: flex; align-items: center; gap: 20px; margin-top: 15px; } .progress-bar { flex-grow: 1; height: 12px; background-color: #ddd; border-radius: 6px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #3498db, #2ecc71); width: 0%; border-radius: 6px; transition: width 0.5s ease; } #progressText { font-weight: bold; min-width: 40px; color: #2c3e50; } .result-container { background-color: #f8f9fa; border: 1px solid #eaeaea; border-radius: 10px; padding: 25px; margin-top: 20px; } .result-container pre { white-space: pre-wrap; word-wrap: break-word; font-size: 16px; line-height: 1.7; color: #2c3e50; font-family: Courier New, Courier, monospace; margin: 0; } .raw-info { margin-top: 20px; } .raw-info details { background-color: #f1f8ff; border: 1px solid #c8e1ff; border-radius: 8px; padding: 15px; } .raw-info summary { font-weight: 600; color: #0366d6; cursor: pointer; user-select: none; } .raw-info pre { background-color: #f6f8fa; padding: 15px; border-radius: 6px; overflow-x: auto; margin-top: 10px; font-size: 14px; color: #24292e; max-height: 300px; overflow-y: auto; } footer { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #95a5a6; font-size: 0.9rem; }3.4 编写前端交互逻辑这是最有趣的部分让页面动起来。在public文件夹下创建script.js文件。// 获取DOM元素 const promptInput document.getElementById(promptInput); const generateBtn document.getElementById(generateBtn); const clearBtn document.getElementById(clearBtn); const statusIndicator document.getElementById(statusIndicator); const progressContainer document.getElementById(progressContainer); const progressFill document.getElementById(progressFill); const progressText document.getElementById(progressText); const resultOutput document.getElementById(resultOutput); const rawResponseOutput document.getElementById(rawResponseOutput); // 更新状态显示 function updateStatus(status, message) { statusIndicator.className status ${status}; const icon statusIndicator.querySelector(i); const textSpan statusIndicator.querySelector(span); // 根据状态更新图标和文字 switch(status) { case idle: icon.className fas fa-clock; break; case processing: icon.className fas fa-spinner fa-spin; progressContainer.style.display flex; break; case success: icon.className fas fa-check-circle; progressContainer.style.display none; break; case error: icon.className fas fa-exclamation-circle; progressContainer.style.display none; break; } textSpan.textContent message; } // 更新进度条 function updateProgress(percentage) { const percent Math.min(100, Math.max(0, percentage)); // 限制在0-100之间 progressFill.style.width ${percent}%; progressText.textContent ${Math.round(percent)}%; } // 模拟进度更新因为真实API可能不返回流式进度这里用模拟展示效果 function simulateProgress() { let progress 0; const interval setInterval(() { progress Math.random() * 15 5; // 随机增量 if (progress 95) { progress 95; // 在收到真实结果前停在95% clearInterval(interval); } updateProgress(progress); }, 200); return interval; // 返回interval ID方便后续清除 } // 处理生成按钮点击事件 generateBtn.addEventListener(click, async () { const prompt promptInput.value.trim(); if (!prompt) { alert(请输入提示词后再试。); promptInput.focus(); return; } // 禁用按钮防止重复点击 generateBtn.disabled true; generateBtn.innerHTML i classfas fa-spinner fa-spin/i 生成中...; // 重置并更新UI状态 resultOutput.textContent 正在生成请稍候...; rawResponseOutput.textContent {}; updateStatus(processing, 正在请求模型生成...); updateProgress(0); // 开始模拟进度 const progressInterval simulateProgress(); try { // 向后端代理接口发送请求 const response await fetch(/api/generate, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ prompt: prompt }), }); // 清除模拟进度 clearInterval(progressInterval); const data await response.json(); if (data.success) { // 成功更新进度到100%显示结果 updateProgress(100); updateStatus(success, 生成成功); resultOutput.textContent data.generated_text; rawResponseOutput.textContent JSON.stringify(data.raw_response, null, 2); // 美化输出 } else { // 失败显示错误 updateProgress(0); updateStatus(error, 生成失败: ${data.error}); resultOutput.textContent 抱歉生成过程中出现错误${data.error}; } } catch (error) { // 网络或请求异常 clearInterval(progressInterval); updateProgress(0); updateStatus(error, 网络请求失败); resultOutput.textContent 请求发送失败${error.message}; console.error(Fetch error:, error); } finally { // 无论成功失败都恢复按钮状态 generateBtn.disabled false; generateBtn.innerHTML i classfas fa-bolt/i 开始生成; } }); // 处理清空按钮点击事件 clearBtn.addEventListener(click, () { promptInput.value ; resultOutput.textContent 生成的内容将显示在这里...; rawResponseOutput.textContent {}; updateStatus(idle, 等待输入...); updateProgress(0); progressContainer.style.display none; promptInput.focus(); }); // 初始焦点放在输入框 promptInput.focus();这段JavaScript代码控制了页面的所有交互点击“生成”按钮时它会收集输入框的内容禁用按钮防止重复提交然后向后端我们刚写的/api/generate接口发送请求。在等待响应的过程中它会模拟一个进度条来提升用户体验。收到响应后根据成功或失败更新页面状态和结果区域。“清空”按钮的功能则很简单就是重置所有输入和显示。4. 运行与测试现在所有代码都准备好了让我们把它跑起来看看。4.1 配置并启动后端服务还记得server.js里的MODEL_API_URL和API_KEY吗现在你需要把它们换成真实的值。这取决于你如何获取Wan2.1-UMT5的服务如果你使用的是某个云服务商提供的API请查看其文档找到接口地址和获取API密钥的方法。重要请务必妥善保管你的API密钥不要提交到公开的代码仓库。替换完成后在项目根目录下打开终端运行node server.js如果看到终端输出 后端代理服务器已启动运行在 http://localhost:3000说明后端启动成功。4.2 访问前端页面打开你的浏览器访问http://localhost:3000。你应该能看到我们刚刚制作的演示界面。4.3 进行测试在文本框中输入一段提示词比如“用幽默的口吻介绍Node.js”。点击“开始生成”按钮。观察页面变化按钮会变成加载状态状态指示器会显示“正在请求模型生成...”进度条会开始模拟前进。如果后端配置正确且模型服务可用几秒到十几秒后进度条会走到100%状态变为“生成成功”下方会显示出模型生成的文本。你可以点击“查看原始API响应”来展开调试信息看看后端返回的完整数据格式。试试“清空”按钮一切会重置。如果遇到错误比如状态显示“生成失败”或“网络请求失败”请打开浏览器的开发者工具按F12查看“控制台”(Console)和“网络”(Network)标签页。这里通常会有详细的错误信息能帮你判断是前端请求发送有问题还是后端代理或模型服务出了问题。5. 总结与后续跟着走完这一趟你应该已经成功搭建了一个连接Wan2.1-UMT5模型的前后端分离的演示应用。这个项目虽然小但包含了几个在真实项目中也很常见的模式用Node.js做后端代理、用Express提供API和静态服务、用Fetch API进行前后端通信、以及一个带有状态反馈的用户界面。这个demo还有很多可以完善和扩展的地方。比如你可以根据模型服务是否支持流式输出Server-Sent Events或WebSocket把模拟进度条换成真实的生成进度。也可以给前端加上历史记录功能或者尝试不同的UI框架让它更漂亮。后端则可以增加更多的错误处理、请求限流、或者缓存机制。最重要的是你现在有了一个可以实际运行和修改的代码基础。你可以把它作为起点去探索如何将更强大的AI能力集成到你自己的产品或者创意项目中去。动手去改一改代码加一点新功能这才是学习最快的方式。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。