UNPKG

koishi-plugin-tieba-parse

Version:

一个用于解析百度贴吧链接,并生成帖子截图、提取内容的 Koishi 插件。

205 lines (203 loc) 10.4 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 = "tieba-parser-final"; var using = ["puppeteer"]; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ width: import_koishi.Schema.number().default(800).description("截图的默认宽度(像素)。"), screenshotHeight: import_koishi.Schema.number().default(0).description("设置截图的最大高度(像素)。设置为 0 则代表不限制高度,截取帖子第一页的所有内容。"), showTitle: import_koishi.Schema.boolean().default(true).description("是否在截图上方显示帖子标题。"), extractFirstPostText: import_koishi.Schema.boolean().default(true).description("是否在截图前提取并发送1楼的纯文本内容。"), extractFirstPostImages: import_koishi.Schema.boolean().default(true).description("是否在截图下方提取并发送1楼的全部图片。"), extractFirstPostVideo: import_koishi.Schema.boolean().default(true).description("是否提取并发送1楼的视频(若存在)。") }).description("解析设置"), import_koishi.Schema.object({ cookie: import_koishi.Schema.string().role("textarea").description("请通过 /tieba.login 指令获取。") }).description("登录信息"), import_koishi.Schema.object({ debugMode: import_koishi.Schema.boolean().default(false).description("启用调试模式。开启后,将在后台控制台输出详细的操作日志。") }).description("调试") ]); var TIEBA_REG = /(tieba\.baidu\.com\/p\/(\d+))|(jump\.bdimg\.com\/p\/(\d+))/; function formatCookie(cookies) { return cookies.map((c) => `${c.name}=${c.value}`).join("; "); } __name(formatCookie, "formatCookie"); function parseCookie(cookieString) { if (!cookieString) return []; return cookieString.split(";").map((pair) => { const parts = pair.split("="); const name2 = parts.shift().trim(); const value = parts.join("=").trim(); return { name: name2, value, domain: ".baidu.com" }; }); } __name(parseCookie, "parseCookie"); function apply(ctx, config) { const logger = ctx.logger("tieba-parser"); ctx.command("tieba.login", "获取贴吧 Cookie").action(async ({ session }) => { let page; try { await session.send("正在获取登录二维码,请稍候..."); page = await ctx.puppeteer.page(); const loginUrl = "https://passport.baidu.com/v2/?login&tpl=tb&u=https%3A%2F%2Ftieba.baidu.com"; await page.goto(loginUrl); const qrCodeElement = await page.waitForSelector(".tang-pass-qrcode-img"); const qrCodeImage = await qrCodeElement.screenshot({ type: "png" }); await session.send([ import_koishi.h.image(qrCodeImage, "image/png"), (0, import_koishi.h)("p", "请在2分钟内使用【百度贴吧】App扫描二维码登录。") ]); await page.waitForNavigation({ timeout: 12e4 }); const cookies = await page.cookies("https://baidu.com", "https://tieba.baidu.com"); const cookieString = formatCookie(cookies); if (!cookieString || !cookieString.includes("BDUSS")) { return "登录失败:未能获取到关键的登录凭证 (BDUSS),请重试。"; } return "登录成功!\n请将以下 Cookie 完整复制并粘贴到插件的【登录信息】配置项中:\n" + cookieString; } catch (error) { logger.error("扫码登录失败!\n" + error.stack); return "登录失败或超时,请重试。"; } finally { if (page) await page.close(); } }); ctx.middleware(async (session, next) => { const prefixes = Array.isArray(ctx.options.prefix) ? ctx.options.prefix : [ctx.options.prefix]; const commandPrefixes = prefixes.filter((p) => p && typeof p === "string"); if (commandPrefixes.some((p) => session.content.startsWith(p))) { return next(); } const match = TIEBA_REG.exec(session.content); if (!match) return next(); const postId = match[2] || match[4]; const targetUrl = `https://tieba.baidu.com/p/${postId}`; if (config.debugMode) logger.info("匹配到贴吧链接,ID: %s", postId); const pinger = await session.send([ (0, import_koishi.h)("quote", { id: session.messageId }), "识别到贴吧链接,正在为您生成内容..." ]); const pingerId = pinger?.[0]; let page; try { if (config.debugMode) logger.info("准备启动 Puppeteer 页面..."); page = await ctx.puppeteer.page(); if (config.cookie) { await page.setCookie(...parseCookie(config.cookie)); if (config.debugMode) logger.info("已设置全局 Cookie。"); } await page.setViewport({ width: config.width, height: 1080 }); await page.goto(targetUrl, { waitUntil: "networkidle2" }); if (config.debugMode) logger.info("页面已导航至: %s", targetUrl); const extractedData = await page.evaluate((cfg) => { const data = { postTitle: "", firstPostText: "", imageUrls: [], videoUrl: "" }; if (cfg.showTitle) { data.postTitle = document.title.replace(/_百度贴吧$/, "").trim(); } const firstPost = document.querySelector(".l_post"); if (!firstPost) return data; const contentElement = firstPost.querySelector(".d_post_content_main .p_content"); if (contentElement) { if (cfg.extractFirstPostText) { data.firstPostText = contentElement.innerText.trim(); } if (cfg.extractFirstPostImages) { const imageElements = contentElement.querySelectorAll("img.BDE_Image"); data.imageUrls = Array.from(imageElements).map((img) => img.src); } } if (cfg.extractFirstPostVideo) { const videoElement = firstPost.querySelector(".d_post_content_main video"); if (videoElement) { data.videoUrl = videoElement.src; } } return data; }, { showTitle: config.showTitle, extractFirstPostText: config.extractFirstPostText, extractFirstPostImages: config.extractFirstPostImages, extractFirstPostVideo: config.extractFirstPostVideo }); const { postTitle, firstPostText, imageUrls, videoUrl } = extractedData; if (config.debugMode) logger.info("数据提取完成: 标题=%s, 文本长度=%d, 图片数=%d, 视频=%s", !!postTitle, firstPostText.length, imageUrls.length, !!videoUrl); await page.evaluate(async () => { let lastHeight = -1; let currentHeight = 0; let tries = 0; while (lastHeight < currentHeight && tries < 15) { window.scrollTo(0, document.body.scrollHeight); lastHeight = currentHeight; await new Promise((resolve) => setTimeout(resolve, 500)); currentHeight = document.body.scrollHeight; tries++; } }); await page.evaluate(() => window.scrollTo(0, 0)); await new Promise((resolve) => setTimeout(resolve, 100)); await page.addStyleTag({ content: `#com_userbar, .tb-header, .right_section, .core_reply_wrapper, .app_download_wrap, .see-more-wrap, .tb_rich_poster_container, .footer, .j_user_sign, .quick_reply_button, .share_btn_wrapper, .celebrity, .post-client-promotion, .lottery-exp-wrap, .simple-card, .vip-red-name-honour-wrap, .bawu-button-wrapper, .video_header_wrap, .fix_bar_wrap { display: none !important; } .pb_content { width: auto !important; }` }); const contentArea = await page.$("#j_p_postlist"); if (!contentArea) throw new Error("无法找到帖子内容区域 #j_p_postlist。"); const boundingBox = await contentArea.boundingBox(); if (!boundingBox) throw new Error("无法获取帖子内容的边界框。"); const clip = { x: boundingBox.x, y: boundingBox.y, width: boundingBox.width, height: config.screenshotHeight > 0 ? config.screenshotHeight : boundingBox.height }; clip.height = Math.min(clip.height, boundingBox.height); const imageBuffer = await page.screenshot({ clip }); const mainMessage = []; if (postTitle) mainMessage.push((0, import_koishi.h)("p", postTitle)); if (firstPostText) mainMessage.push(firstPostText); mainMessage.push(import_koishi.h.image(imageBuffer, "image/png")); if (imageUrls.length > 0) mainMessage.push(...imageUrls.map((url) => import_koishi.h.image(url))); await session.send(mainMessage); if (videoUrl) { await session.send(import_koishi.h.video(videoUrl)); } return; } catch (error) { logger.error("贴吧解析过程中发生严重错误!\n" + error.stack); return `解析失败,可能是帖子不存在或网络问题。请管理员检查后台日志以获取详细错误信息。`; } finally { if (page) { await page.close(); if (config.debugMode) logger.info("Puppeteer 页面已关闭。"); } if (pingerId) { try { await session.bot.deleteMessage(session.channelId, pingerId); if (config.debugMode) logger.info("已撤回“正在生成”的提示消息。"); } catch (e) { if (config.debugMode) logger.warn("撤回提示消息失败,可能缺少权限。", e); } } } }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name, using });