UNPKG

koishi-plugin-steaminfo-xiaoheihe

Version:

通过小黑盒网站搜索并生成截图获取 steam 游戏信息

231 lines (229 loc) 11.6 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, name: () => name, using: () => using }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var name = "steaminfo-xiaoheihe"; var using = ["puppeteer"]; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ cookies: import_koishi.Schema.string().description("【选填】【建议填写】登录小黑盒后获取的Cookie。获取方式请查看readme。").role("secret") }).description("身份验证"), import_koishi.Schema.object({ waitTimeout: import_koishi.Schema.number().description("页面加载的超时时间(毫秒)。").default(6e4), renderDelay: import_koishi.Schema.number().description("在内容框架出现后,额外等待渲染的时间(毫秒)。").default(5e3), deviceScaleFactor: import_koishi.Schema.number().min(1).max(3).step(0.5).description("截图清晰度(设备缩放因子)。设置为 2 可让文字更加清晰。").default(2), imageType: import_koishi.Schema.union(["jpeg", "png"]).description("截图的图片格式。PNG 无损但文件较大,JPEG 有损但文件较小。").default("jpeg"), imageQuality: import_koishi.Schema.number().min(1).max(100).description("图片质量,仅在格式为 jpeg 时生效。").default(95) }).description("高级设置"), import_koishi.Schema.object({ showGameTitle: import_koishi.Schema.boolean().description("是否在截图前显示“游戏名”文本。").default(true), showOnlineCount: import_koishi.Schema.boolean().description("是否在截图前显示“当前在线”文本。").default(true) }).description("显示设置"), import_koishi.Schema.object({ debug: import_koishi.Schema.boolean().description("启用后,将在控制台输出详细的调试日志。").default(false) }).description("调试") ]); function apply(ctx, config) { const logger = ctx.logger("xiaoheihe"); const log = /* @__PURE__ */ __name((message) => { if (config.debug) { logger.info(message); } }, "log"); ctx.command("xiaoheihe <game:text>", "小黑盒游戏页面截图").alias("小黑盒").action(async ({ session }, game) => { if (!game) return "请输入要搜索的游戏名称。"; let page; let tempMessageId; try { const tempMessage = [ import_koishi.segment.quote(session.messageId), "请求已收到,正在为您生成游戏截图,请稍候..." ]; const sentMessageIds = await session.send(tempMessage); tempMessageId = sentMessageIds[0]; log(`收到截图请求,游戏名称: "${game}"。临时消息ID: ${tempMessageId}`); page = await ctx.puppeteer.page(); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); if (config.cookies) { const cookies = config.cookies.split(";").map((pair) => { const parts = pair.trim().split("="); const name2 = parts.shift(); const value = parts.join("="); return { name: name2, value, domain: ".xiaoheihe.cn" }; }); await page.setCookie(...cookies); log(`成功注入 ${cookies.length} 个 Cookie。`); } else { log("未配置Cookie,以游客身份访问。"); } await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: config.deviceScaleFactor }); log(`视口已设置为 1280x800,缩放因子为 ${config.deviceScaleFactor}。`); const searchUrl = `https://www.xiaoheihe.cn/app/search?q=${encodeURIComponent(game)}`; log(`准备导航到搜索页面: ${searchUrl}`); await page.goto(searchUrl, { waitUntil: "load", timeout: config.waitTimeout }); let finalUrl; let navigationCompleted = false; const listGameSelector = 'a[href*="/app/topic/game/"]'; log(`[Plan A] 尝试寻找列表页的游戏链接: "${listGameSelector}"`); try { await page.waitForSelector(listGameSelector, { timeout: 5e3 }); const gamePageHref = await page.$eval(listGameSelector, (el) => el.getAttribute("href")); finalUrl = `https://www.xiaoheihe.cn${gamePageHref}`; log(`[Plan A] 成功!获取到链接: ${finalUrl}`); } catch (e) { log(`[Plan A] 失败。切换到 Plan B...`); try { const communityLinkSelector = ".search-topic__topic-name"; log(`[Plan B] 尝试寻找社区链接: "${communityLinkSelector}"`); await page.waitForSelector(communityLinkSelector, { timeout: 5e3 }); await Promise.all([ page.waitForNavigation({ waitUntil: "load", timeout: config.waitTimeout }), page.click(communityLinkSelector) ]); const gameTabSelector = ".slide-tab__tab-label"; await page.waitForSelector(gameTabSelector, { timeout: 1e4 }); await Promise.all([ page.waitForNavigation({ waitUntil: "load", timeout: config.waitTimeout }), page.click(gameTabSelector) ]); navigationCompleted = true; log(`[Plan B] 成功到达最终游戏页面: ${page.url()}`); } catch (e2) { log(`[Plan B] 失败。切换到 Plan C...`); try { const singleGameCardSelector = ".search-result__game .game-rank__game-card"; log(`[Plan C] 尝试寻找并点击独立游戏卡片: "${singleGameCardSelector}"`); await page.waitForSelector(singleGameCardSelector, { timeout: 5e3 }); await page.click(singleGameCardSelector); navigationCompleted = true; log(`[Plan C] 点击成功!页面将原地跳转。`); } catch (e3) { if (config.debug) logger.warn("在搜索页未能找到游戏链接,所有方案均失败。"); const screenshot = await page.screenshot({ fullPage: true }); await session.send(["未能找到游戏专题链接。这是当前页面的截图:", import_koishi.segment.image(screenshot)]); return; } } } if (!navigationCompleted) { log(`正在导航到: ${finalUrl}`); await page.goto(finalUrl, { waitUntil: "load", timeout: config.waitTimeout }); } const mainContentSelector = ".game-detail-page-detail"; log(`等待核心内容 "${mainContentSelector}" 出现...`); await page.waitForSelector(mainContentSelector, { timeout: config.waitTimeout }); log("核心内容容器已出现!"); let extractedTitle = game; let onlineInfo = "获取失败"; try { const titleSelector = ".game-name p.name"; const onlineNumberSelector = ".data-list .data-item:first-child .editor p"; const onlineUnitSelector = ".data-list .data-item:first-child .editor p + p"; const onlineLabelSelector = ".data-list .data-item:first-child > .p2"; log("等待标题和数据项出现..."); await Promise.all([ page.waitForSelector(titleSelector, { timeout: 1e4 }), page.waitForSelector(onlineNumberSelector, { timeout: 1e4 }), page.waitForSelector(onlineLabelSelector, { timeout: 1e4 }) ]); log("标题和数据项均已出现,开始提取..."); const title = await page.$eval(titleSelector, (el) => el.textContent.trim()); const number = await page.$eval(onlineNumberSelector, (el) => el.textContent.trim()); const label = await page.$eval(onlineLabelSelector, (el) => el.textContent.trim()); let unit = ""; try { unit = await page.$eval(onlineUnitSelector, (el) => el.textContent.trim()); } catch { } extractedTitle = title; onlineInfo = `${label}${number}${unit}`; log(`成功提取到标题: "${extractedTitle}"`); log(`成功提取到在线信息: "${onlineInfo}"`); } catch { log(`无法从页面提取标题或在线人数,将使用用户输入的游戏名。`); } log(`额外等待 ${config.renderDelay} 毫秒以确保内容渲染完成...`); await new Promise((resolve) => setTimeout(resolve, config.renderDelay)); const selectorsToHide = [ ".game-detail-section-comment", ".game-detail-section-similar-games", ".publish-score-wrapper" ]; const selectorToModify = ".game-detail-section-footer"; log(`准备隐藏 ${selectorsToHide.length} 个元素,并修正 1 个悬浮元素的位置...`); await page.evaluate((toHide, toModify) => { for (const selector of toHide) { const element2 = document.querySelector(selector); if (element2) element2.style.display = "none"; } const floatingElement = document.querySelector(toModify); if (floatingElement) floatingElement.style.position = "static"; }, selectorsToHide, selectorToModify); const element = await page.$(mainContentSelector); if (!element) throw new Error("无法定位到已等待的核心内容元素"); log(`正在执行最终的精准截图... 格式: ${config.imageType}`); const imageBuffer = await element.screenshot({ type: config.imageType, quality: config.imageType === "jpeg" ? config.imageQuality : void 0 }); log("截图成功!"); const message = []; const textLines = []; if (config.showGameTitle) textLines.push(`游戏名:${extractedTitle}`); if (config.showOnlineCount && onlineInfo !== "获取失败") textLines.push(onlineInfo); const textContent = textLines.join("\n"); if (textContent) message.push(textContent); message.push(import_koishi.segment.image(imageBuffer)); await session.send(message); } catch (error) { logger.error("截图过程中发生严重错误:", error); let errorMsg = `截图失败,请检查控制台错误日志。`; if (error.name === "TimeoutError") { errorMsg = `截图失败,页面加载超时。可能是小黑盒服务器繁忙或您的网络不稳定。`; } await session.send(errorMsg); } finally { if (page) { await page.close(); log("浏览器页面已关闭。"); } if (tempMessageId) { try { await session.bot.deleteMessage(session.channelId, tempMessageId); log(`临时消息 ${tempMessageId} 已撤回。`); } catch (e) { logger.warn(`撤回临时消息失败: ${e.message}`); } } } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name, using });