koishi-plugin-weibo-fetcher
Version:
一个微博内容获取与订阅的 Koishi 插件,支持链接实时解析和用户后台订阅推送。
334 lines (330 loc) • 15.7 kB
JavaScript
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,
inject: () => inject,
name: () => name
});
module.exports = __toCommonJS(src_exports);
var import_koishi = require("koishi");
var name = "koishi-plugin-weibo-fetcher";
var inject = {
required: ["puppeteer", "database"]
};
var logger = new import_koishi.Logger(name);
var WEIBO_URL_REGEX = /https?:\/\/((m\.)?weibo\.(cn|com))\/(\d+)\/(\w+)/g;
var Config = import_koishi.Schema.intersect([
import_koishi.Schema.object({
cookie: import_koishi.Schema.string().role("textarea").description("【非必要】微博 Cookie")
}).description("基础设置"),
import_koishi.Schema.object({
splitMessages: import_koishi.Schema.boolean().description("是否分条发送消息?开启后,文本、截图和每张图片/视频都会作为独立消息发送。").default(false),
showScreenshot: import_koishi.Schema.boolean().description("是否发送微博截图。").default(true),
sendText: import_koishi.Schema.boolean().description("是否发送提取的微博文本。").default(true),
sendMedia: import_koishi.Schema.boolean().description("是否发送微博中的图片和视频。").default(true),
useForward: import_koishi.Schema.boolean().description("是否使用合并转发的形式发送 (仅 QQ 平台效果最佳)(此选项开启时,“分条发送”将无效)。").default(false)
}).description("手动解析设置 - 当手动发送链接时生效"),
import_koishi.Schema.object({
sub_showLink: import_koishi.Schema.boolean().description("推送时, 是否在消息顶部附带原始微博链接。").default(true),
sub_showScreenshot: import_koishi.Schema.boolean().description("推送时, 是否发送微博截图。").default(true),
sub_sendText: import_koishi.Schema.boolean().description("推送时, 是否发送提取的微博文本。").default(true),
sub_sendMedia: import_koishi.Schema.boolean().description("推送时, 是否发送微博中的图片和视频。").default(true),
sub_useForward: import_koishi.Schema.boolean().description("推送时, 是否使用合并转发。").default(false)
}).description("订阅推送内容设置 - 当自动推送订阅时生效"),
import_koishi.Schema.object({
enableSubscription: import_koishi.Schema.boolean().description("**【总开关】是否启用订阅功能。** 开启后会显示详细设置。").default(false)
}).description("订阅设置"),
import_koishi.Schema.union([
import_koishi.Schema.object({
enableSubscription: import_koishi.Schema.const(false)
}),
import_koishi.Schema.object({
enableSubscription: import_koishi.Schema.const(true),
platform: import_koishi.Schema.string().description("用于执行推送的机器人平台 (例如: onebot)。").required(),
selfId: import_koishi.Schema.string().description("用于执行推送的机器人账号/ID (例如: 12345678)。").required(),
updateInterval: import_koishi.Schema.number().min(1).description("每隔多少分钟检查一次更新。").default(30),
test_authority: import_koishi.Schema.number().min(0).description("“测试微博推送”指令的最低权限等级。").default(2),
subscriptions: import_koishi.Schema.array(import_koishi.Schema.object({
uid: import_koishi.Schema.string().description("微博用户 UID (纯数字)"),
name: import_koishi.Schema.string().description("备注名 (仅用于后台显示)"),
channelIds: import_koishi.Schema.array(String).role("table").description("需要推送的频道/群号列表")
})).role("table").description("订阅列表")
})
]),
import_koishi.Schema.object({
logDetails: import_koishi.Schema.boolean().description("是否在控制台输出详细的调试日志。").default(false)
}).description("调试设置")
]);
async function getLatestWeiboPostUrlByPuppeteer(ctx, cookie, uid, log) {
const userHomepageUrl = `https://weibo.com/u/${uid}`;
log(`正在访问用户主页 (PC 版): ${userHomepageUrl}`);
const page = await ctx.puppeteer.page();
try {
if (cookie) {
const cookies = cookie.split(";").map((c) => {
const [name2, ...valueParts] = c.trim().split("=");
return { name: name2, value: valueParts.join("="), domain: ".weibo.com", path: "/" };
});
await page.setCookie(...cookies);
}
await page.goto(userHomepageUrl, { waitUntil: "networkidle2", timeout: 3e4 });
const articleSelector = 'article[class*="Feed_wrap"]';
await page.waitForSelector(articleSelector, { timeout: 2e4 });
const latestPostUrl = await page.evaluate(() => {
const articles = document.querySelectorAll('article[class*="Feed_wrap"]');
for (const article of articles) {
const pinnedElement = article.querySelector('[class*="title_title"]');
if (pinnedElement) {
continue;
}
const linkElement = article.querySelector('a[class*="head-info_time"]');
if (linkElement) {
return linkElement.href;
}
}
return null;
});
if (latestPostUrl) {
log(`成功获取到最新微博链接: ${latestPostUrl}`);
return latestPostUrl;
}
logger.warn(`[Puppeteer] 在 ${uid} 的主页上未能找到任何符合条件的微博链接.`);
return null;
} finally {
await page.close();
}
}
__name(getLatestWeiboPostUrlByPuppeteer, "getLatestWeiboPostUrlByPuppeteer");
async function processWeiboPost(ctx, config, url, options) {
const log = createLogStepper(`处理:${url}`);
log(`开始处理微博 (PC 版模式)`);
const page = await ctx.puppeteer.page();
const result = {
mediaBlocks: []
};
try {
log(`准备访问微博 PC 版页面: ${url}`);
if (config.cookie) {
log("检测到 Cookie 已配置,准备注入...");
const cookies = config.cookie.split(";").map((c) => {
const [name2, ...valueParts] = c.trim().split("=");
return { name: name2, value: valueParts.join("="), domain: ".weibo.com", path: "/" };
});
await page.setCookie(...cookies);
log("Cookie 注入成功。");
} else {
log("未配置 Cookie,将以游客身份尝试访问。", true);
}
await page.goto(url, { waitUntil: "networkidle2", timeout: 3e4 });
const articleSelector = 'article[class*="Feed_wrap"]';
await page.waitForSelector(articleSelector, { timeout: 2e4 });
const articleElement = await page.$(articleSelector);
if (!articleElement) throw new Error("无法在页面上定位到微博正文元素 (<article>)。");
if (options.showScreenshot) {
log("正在生成截图...");
const screenshotBuffer = await articleElement.screenshot();
result.mediaBlocks.push(import_koishi.h.image(screenshotBuffer, "image/png"));
log("截图成功。");
}
const postData = await articleElement.evaluate((article) => {
const author = article.querySelector('a[href^="/u/"] > span')?.textContent || "未知作者";
const textEl = article.querySelector('[class*="detail_text"]');
const text = textEl ? textEl.innerText : "";
const images = Array.from(article.querySelectorAll('[class*="picture_pic"] img')).map((img) => img.src);
const video = article.querySelector('[class*="FeedPlayer"] video')?.src || null;
return { author, text, images, video };
});
log(`提取到来自 [${postData.author.trim()}] 的文本,${postData.images.length} 张图片,${postData.video ? 1 : 0} 个视频。`);
let textContent = "";
if (options.showLink) textContent += `${url}
`;
if (options.sendText) {
const authorText = postData.author.trim();
if (authorText && authorText !== "未知作者") textContent += `✨ 微博用户: ${authorText}
`;
if (postData.text) textContent += `💡 微博正文: ${postData.text}`;
}
if (textContent.trim()) {
result.textBlock = (0, import_koishi.h)("p", textContent.trim());
}
if (options.sendMedia) {
for (const imageUrl of postData.images) {
try {
const response = await ctx.http("get", imageUrl, { headers: { "Referer": "https://weibo.com/" }, responseType: "arraybuffer" });
result.mediaBlocks.push(import_koishi.h.image(response.data, response.headers.get("content-type")));
} catch (error) {
log(`下载图片失败: ${imageUrl}, 错误: ${error.message}`, true);
}
}
if (postData.video) {
try {
const response = await ctx.http("get", postData.video, { headers: { "Referer": "https://weibo.com/" }, responseType: "arraybuffer" });
result.mediaBlocks.push(import_koishi.h.video(response.data, response.headers.get("content-type")));
} catch (error) {
log(`下载视频失败: ${postData.video}, 错误: ${error.message}`, true);
}
}
}
return result;
} finally {
await page.close();
}
}
__name(processWeiboPost, "processWeiboPost");
var logStepperPrefix = "";
var logStep = 1;
var KOISHI_WEIBO_LOG_DETAILS = false;
function createLogStepper(prefix) {
logStepperPrefix = prefix;
logStep = 1;
return (message, isWarning = false) => {
if (KOISHI_WEIBO_LOG_DETAILS) {
const logMessage = `[${logStepperPrefix}] [步骤 ${logStep++}] ${message}`;
if (isWarning) logger.warn(logMessage);
else logger.info(logMessage);
}
};
}
__name(createLogStepper, "createLogStepper");
function apply(ctx, config) {
logger.info("微博 Fetcher (Puppeteer 模式) 插件已启动。");
KOISHI_WEIBO_LOG_DETAILS = config.logDetails;
ctx.model.extend("weibo_subscriptions", { uid: "string", last_post_url: "string" }, { primary: "uid" });
async function sendPost(bot, channelId, postResult, useForward, splitMessages) {
if (useForward && bot.platform === "onebot") {
const elements = [];
if (postResult.textBlock) elements.push(postResult.textBlock);
elements.push(...postResult.mediaBlocks);
if (elements.length > 0) {
await bot.sendMessage(channelId, (0, import_koishi.h)("figure", {}, elements));
}
return;
}
if (splitMessages) {
if (postResult.textBlock) {
await bot.sendMessage(channelId, postResult.textBlock);
await (0, import_koishi.sleep)(500);
}
for (const media of postResult.mediaBlocks) {
await bot.sendMessage(channelId, media);
await (0, import_koishi.sleep)(500);
}
} else {
const elements = [];
if (postResult.textBlock) elements.push(postResult.textBlock);
elements.push(...postResult.mediaBlocks);
if (elements.length > 0) {
await bot.sendMessage(channelId, elements);
}
}
}
__name(sendPost, "sendPost");
ctx.middleware(async (session, next) => {
WEIBO_URL_REGEX.lastIndex = 0;
const match = WEIBO_URL_REGEX.exec(session.content);
if (!match) return next();
const statusMessage = await session.send((0, import_koishi.h)("quote", { id: session.messageId }) + "正在解析微博链接, 请稍候...");
try {
const postResult = await processWeiboPost(ctx, config, match[0], {
showLink: false,
// 手动解析默认不显示链接
showScreenshot: config.showScreenshot,
sendText: config.sendText,
sendMedia: config.sendMedia
});
if (!postResult.textBlock && postResult.mediaBlocks.length === 0) {
await session.send("未能获取到任何内容。");
} else {
await sendPost(session.bot, session.channelId, postResult, config.useForward, config.splitMessages);
}
} catch (error) {
logger.warn(`[手动解析] 失败:`, error);
await session.send("解析微博失败,可能是链接已失效或需要有效的 Cookie。");
} finally {
if (statusMessage?.[0]) {
try {
await session.bot.deleteMessage(session.channelId, statusMessage[0]);
} catch {
}
}
}
});
if (config.enableSubscription) {
const checkAndPushUpdates = /* @__PURE__ */ __name(async () => {
}, "checkAndPushUpdates");
const forcePushAllSubscriptions = /* @__PURE__ */ __name(async () => {
if (!config.enableSubscription) return "订阅功能未开启。";
const botKey = `${config.platform}:${config.selfId}`;
const bot = ctx.bots[botKey];
if (!bot || !bot.online) return `机器人 [${botKey}] 不在线,无法执行推送。`;
let pushCount = 0;
let failCount = 0;
for (const sub of config.subscriptions) {
if (!sub.uid || !sub.channelIds || sub.channelIds.length === 0) continue;
const log = createLogStepper(`强制推送:${sub.name}(${sub.uid})`);
log("开始处理...");
try {
const latestPostUrl = await getLatestWeiboPostUrlByPuppeteer(ctx, config.cookie, sub.uid, log);
if (latestPostUrl) {
log(`获取到最新链接: ${latestPostUrl}, 开始处理内容...`);
const postResult = await processWeiboPost(ctx, config, latestPostUrl, {
showLink: config.sub_showLink,
showScreenshot: config.sub_showScreenshot,
sendText: config.sub_sendText,
sendMedia: config.sub_sendMedia
});
log(`内容处理完毕,准备推送到 ${sub.channelIds.length} 个频道...`);
for (const channelId of sub.channelIds) {
await sendPost(bot, channelId, postResult, config.sub_useForward, true);
}
log(`推送完成。`);
pushCount++;
} else {
log("未能获取到最新链接,跳过。", true);
failCount++;
}
} catch (error) {
logger.warn(`[强制推送] 处理 [${sub.name}] 时发生错误:`, error);
failCount++;
}
await (0, import_koishi.sleep)(5 * import_koishi.Time.second);
}
return `强制推送任务完成。成功推送 ${pushCount} 个订阅,失败 ${failCount} 个。`;
}, "forcePushAllSubscriptions");
ctx.setInterval(checkAndPushUpdates, config.updateInterval * import_koishi.Time.minute);
ctx.command("测试微博推送", "强制将所有订阅用户的最新微博推送到目标群组", {
authority: config.test_authority
}).action(async ({ session }) => {
session.send("收到指令,即将开始为所有订阅强制推送最新内容,这可能需要一段时间,请稍候...");
const result = await forcePushAllSubscriptions();
return result;
});
}
}
__name(apply, "apply");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Config,
apply,
inject,
name
});