190 lines
7.9 KiB
JavaScript
190 lines
7.9 KiB
JavaScript
const express = require("express");
|
||
const playwright = require("playwright");
|
||
|
||
const app = express();
|
||
const port = 3004;
|
||
// const proxyUrl = "https://smartart-dev.felo.ai";
|
||
const proxyUrl = "http://localhost:5173/";
|
||
|
||
// 用户提供的,用于初始化图形的 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<object>} 从页面获取到的图形数据
|
||
*/
|
||
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(proxyUrl, {
|
||
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");
|
||
console.log(data);
|
||
|
||
res.json(data);
|
||
} 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 ${proxyUrl}`
|
||
);
|
||
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);
|