const express = require("express"); const playwright = require("playwright"); const app = express(); const port = 3000; // 用户提供的,用于初始化图形的 JS 对象 const graphicInitPayload = { type: "graphic:init", value: { tokens: { login: "cW_39372b3655c84d058161000372da8ce8__", anonymous: "a83f75a1fb5f2c701759320b8ead2042", visitor: null, }, options: { uid: "1701151465403707393", canEdit: true, mode: "thread", thread_id: "2svFUkJFEWZnrKZpM6gU7h", message_id: "4dbec04b-a3b9-4465-af04-8259bebfa3ea", language: "zh-Hans", inputText: '\n Query:介绍一下可琳威尔斯\n Answer:# 可琳·威克斯介绍\n\n可琳·威克斯(Corin Wickes)是米哈游开发的游戏《绝区零》中的一位A级物理强攻角色,于2024年7月4日在游戏1.0版本正式上线[1][4]。\n\n## 基本信息\n\n- **全名**:可琳·威克斯(Corin Wickes)[2]\n- **性别**:女[1][2]\n- **生日**:6月2日[1]\n- **星座**:双子座[1]\n- **身高**:141厘米[1]\n- **血型**:Rh阴性[1]\n- **所属组织**:维多利亚家政[1][2]\n- **稀有度**:A级[1][2][5]\n- **属性**:物理[1][5]\n- **特性**:强攻[1][5]\n- **配音演员**:沐霏(汉语)、五十岚裕美(日语)[1][11]\n\n## 角色特点\n\n可琳是维多利亚家政的女仆之一,性格外柔内刚、乖巧谦逊,拥有无法抛弃的责任心[1]。她非常认真负责,不惧任何脏活与累活,但同时极度缺乏自信,总是害怕被其他人讨厌,经常表现得慌慌张张[13]。\n\n根据维多利亚家政收到的客户反馈表显示,可琳的服务能力获得了四星评价,服务质量获得了三星半,服务态度获得了两星半[1]。客户在备注中提到,虽然最初对可琳畏畏缩缩的态度感到不放心,但实际上她完成任务的质量很好,只是不明白为什么她总是一副担惊受怕的样子[1]。\n\n## 战斗特点\n\n可琳在战斗中使用一把电锯造型的扫除工具[1][19],她的战斗技能包括:\n\n1. **普通攻击:扫除开始** - 向前方进行至多五段的斩击,造成物理伤害,在第三、第五段中连点或长按时,可以使用电锯持续斩击敌人[19]。\n\n2. **闪避:[离]** - 快速的冲刺闪避[19]。\n\n她的专属装备"安心家用轮锯"提供以下效果:位于后场时,装备者的能量自动回复提升0.45点/秒;"强化特殊技"命中敌人时,装备者造成的物理伤害提升3%(4.8%),最多叠加15层,持续1秒[1]。\n\n## 获取方式\n\n玩家可以通过常驻频段"热门卡司"以及商城"丽都迎新"礼盒获得可琳·威克斯[1]。\n\n作为《绝区零》中的一位受欢迎角色,可琳·威克斯以其独特的女仆形象和电锯武器设计吸引了许多玩家的喜爱[12][15]。', smart_art_id: "1934511766443233282", }, }, }; // 全局浏览器实例 let browser; /** * 核心渲染函数 * @returns {Promise} 从页面获取到的图形数据 */ async function renderGraphic() { const context = await browser.newContext(); // 将所有浏览器的 console 日志转发到 Node.js 的终端 const page = await context.newPage(); // page.on('console', msg => { // // 过滤掉一些不重要的 vite 日志,让输出更清晰 // const text = msg.text(); // if (text.includes('[vite]')) return; // console.log(`[Browser Console] ${msg.type().toUpperCase()}: ${text}`); // }); // 设置一个Promise,用于监听从页面回传的最终数据 // 我们将 exposeFunction 和 addInitScript 绑定到 context 而不是 page,这更可靠 const finalDataPromise = new Promise(async (resolve, reject) => { // 1. 在 Context 上暴露函数 await context .exposeFunction("onGraphicData", (data) => { console.log( "Node.js: onGraphicData function was called from the browser." ); resolve(data); }) .catch(reject); console.log( "Node.js: exposeFunction and addInitScript have been set up on the context." ); }); // await page.addInitScript(() => { // console.log("INIT SCRIPT: This script is running from page.addInitScript!"); // }); // 2. 在 Context 上添加初始化脚本 await context.addInitScript(() => { // 这个 console.log 现在应该可以被 Node.js 终端捕获到 console.log( "INIT SCRIPT: This script is running from context.addInitScript!" ); window.addEventListener("message", (event) => { // 为了调试,我们打印收到的消息类型 if (event.data && event.data.type) { console.log( `INIT SCRIPT: Message received in browser - Type: ${event.data.type}` ); } // 根据用户提供的返回数据格式,判断是否是我们需要的数据 if ( event.data && event.data.value && (event.data.value.svg || event.data.value.png) ) { // 如果是,则调用从Node.js注入的函数,将数据传递回来 // window.onGraphicData(event.data); } }); }); try { // 3. 导航到目标网页 console.log("Node.js: Navigating to page..."); await page.goto("http://localhost:5173/", { waitUntil: "domcontentloaded", }); console.log("Node.js: Page navigation complete."); // 4. 注入登录态和图形信息 console.log("Node.js: Posting graphic:init message..."); await page.evaluate((payload) => { window.postMessage(payload, "*"); }, graphicInitPayload); // 5. 等待图形加载完成的标志——`.editor-controls` 元素出现 console.log("Node.js: Waiting for selector .editor-controls..."); await page.waitForSelector(".editor-controls", { timeout: 30000 }); // 设置30秒超时 console.log("Node.js: Selector .editor-controls found."); // 6. 请求图片数据 console.log("Node.js: Posting graphic:handlessRequestData message..."); await page.evaluate(() => { window.postMessage({ type: "graphic:handlessRequestData" }, "*"); }); // 7. 等待 `finalDataPromise` 完成,以获取最终的图形数据 console.log("Waiting for graphic data from the page..."); const graphicData = await finalDataPromise; console.log("Graphic data received."); return graphicData; } finally { // 8. 确保页面和上下文被关闭,释放资源 await page.close(); await context.close(); } } app.get("/render", async (req, res) => { console.log("Received a request to /render"); try { const data = await renderGraphic(); // 返回从页面获取到的 value 对象 res.setHeader("Content-Type", "application/json"); res.json(data.value); } catch (error) { console.error("Failed to render graphic:", error); res .status(500) .json({ error: "Failed to render graphic", details: error.message }); } }); // 启动服务 const server = app.listen(port, async () => { try { // 启动 Playwright 浏览器实例 // 使用 headless: false 可以在本地看到浏览器操作,方便调试 browser = await playwright.chromium.launch({ headless: false }); console.log( `Playwright-based graphic rendering service listening at http://localhost:${port}` ); console.log( "Please make sure your frontend dev server is running on http://localhost:5173" ); console.log(`To render a graphic, access: http://localhost:${port}/render`); } catch (error) { console.error("Failed to launch browser:", error); process.exit(1); } }); // 优雅地关闭浏览器和服务器 const cleanup = async () => { console.log("Closing server and browser..."); if (browser) { await browser.close(); } server.close(() => { console.log("Server closed."); process.exit(0); }); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup);