koishi-plugin-bilibili-notify
Version:
Koishi bilibili notify plugin
1,316 lines (1,309 loc) • 204 kB
JavaScript
import "node:module";
import { Schema, Service, Universal, h } from "koishi";
import "@koishijs/plugin-notifier";
import "@koishijs/plugin-help";
import { GuardLevel, startListen } from "blive-message-listener";
import QRCode from "qrcode";
import { CronJob } from "cron";
import { DateTime } from "luxon";
import { Jieba } from "@node-rs/jieba";
import { dict } from "@node-rs/jieba/dict";
import "koishi-plugin-puppeteer";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import md5 from "md5";
import crypto from "node:crypto";
import http from "node:http";
import https from "node:https";
import axios from "axios";
import { JSDOM } from "jsdom";
import { Cookie, CookieJar } from "tough-cookie";
import OpenAI from "openai";
//#region rolldown:runtime
var __defProp = Object.defineProperty;
var __export = (all, symbols) => {
let target = {};
for (var name$2 in all) {
__defProp(target, name$2, {
get: all[name$2],
enumerable: true
});
}
if (symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
//#endregion
//#region src/config.ts
const BAConfigSchema = Schema.object({
require: Schema.object({}).description("必填设置"),
key: Schema.string().pattern(/^[0-9a-f]{32}$/).role("secret").required().description("请输入一个32位小写字母的十六进制密钥(例如:9b8db7ae562b9864efefe06289cc5530),使用此密钥将你的B站登录信息存储在数据库中,请一定保存好此密钥。如果你忘记了此密钥,必须重新登录。你可以自行生成,或到这个网站生成:https://www.sexauth.com/"),
master: Schema.intersect([Schema.object({ enable: Schema.boolean().default(false).description("是否开启主人账号功能,如果您的机器人没有私聊权限请不要开启此功能。开启后如果机器人运行错误会向您进行报告") }).description("主人账号"), Schema.union([Schema.object({
enable: Schema.const(true).required(),
platform: Schema.union([
"qq",
"qqguild",
"onebot",
"discord",
"red",
"telegram",
"satori",
"chronocat",
"lark"
]).description("请选择您的私人机器人平台"),
masterAccount: Schema.string().role("secret").required().description("主人账号,在Q群使用可直接使用QQ号,若在其他平台使用,请使用inspect插件获取自身ID"),
masterAccountGuildId: Schema.string().role("secret").description("主人账号所在的群组ID,只有在QQ频道、Discord这样的环境才需要填写,请使用inspect插件获取群组ID")
}), Schema.object({})])]),
basicSettings: Schema.object({}).description("基本设置"),
userAgent: Schema.string().description("设置请求头User-Agen,请求出现-352时可以尝试修改,UA获取方法可参考:https://blog.csdn.net/qq_44503987/article/details/104929111"),
ai: Schema.intersect([Schema.object({ enable: Schema.boolean().default(false).description("是否开启AI功能") }), Schema.union([Schema.object({
enable: Schema.const(true).required(),
apiKey: Schema.string().role("secret").required().description("API KEY"),
baseURL: Schema.string().required().description("API 访问地址"),
model: Schema.string().default("gpt-3.5-turbo").description("AI模型"),
persona: Schema.string().description("AI系统角色设定").default("你是一个风趣幽默的主播助理,你的任务是根据提供的直播数据生成一段有趣且富有创意的直播总结。请确保你的回答简洁明了,避免使用过于复杂的语言或长句子。请注意,你的回答必须与提供的数据相关,并且不能包含任何虚构的信息。如果你无法根据提供的数据生成总结,请礼貌地说明你无法完成任务。")
}), Schema.object({ enable: Schema.const(false) })])]),
subTitle: Schema.object({}).description("订阅配置"),
advancedSub: Schema.boolean().default(false).description("是否开启高级订阅,高级订阅是独立于本插件的一个专门用于订阅配置的插件,用于高度自定义订阅消息。若开启高级订阅,请打开该选项并下载插件 bilibili-notify-advanced-subscription"),
subs: Schema.array(Schema.object({
name: Schema.string().required().description("备注"),
uid: Schema.string().required().description("UID和roomid"),
dynamic: Schema.boolean().default(true).description("动态"),
dynamicAtAll: Schema.boolean().default(false).description("动态At全体"),
live: Schema.boolean().default(true).description("直播"),
liveAtAll: Schema.boolean().default(true).description("直播At全体"),
liveGuardBuy: Schema.boolean().default(false).description("上舰消息"),
superchat: Schema.boolean().default(false).description("SC消息"),
wordcloud: Schema.boolean().default(true).description("弹幕词云"),
liveSummary: Schema.boolean().default(true).description("直播总结"),
platform: Schema.string().required().description("平台名"),
target: Schema.string().required().description("群号/频道号")
})).role("table").description("输入订阅信息,自定义订阅内容; UID和roomid,如果经常在初始化插件遇到风控问题,请补充直播间房间号,使用英文逗号分隔例如,1234567,114514 群号/频道号格式:频道号,频道号 使用英文逗号分隔,例如 1234567,2345678"),
dynamic: Schema.object({}).description("动态推送设置"),
dynamicUrl: Schema.boolean().default(false).description("发送动态时是否同时发送链接。注意:如果使用的是QQ官方机器人不能开启此项!"),
dynamicCron: Schema.string().default("*/2 * * * *").description("动态监测时间,请填入cron表达式,请勿填入过短时间"),
dynamicVideoUrlToBV: Schema.boolean().default(false).description("如果推送的动态是视频动态,且开启了发送链接选项,开启此选项则会将链接转换为BV号以便其他用途"),
pushImgsInDynamic: Schema.boolean().default(false).description("是否推送动态中的图片,默认不开启。开启后会单独推送动态中的图片,该功能容易导致QQ风控"),
live: Schema.object({}).description("直播推送设置"),
wordcloudStopWords: Schema.string().description("词云生成时的停用词,多个停用词请使用英文逗号分隔,例如:哔哩哔哩,弹幕,直播,词云"),
liveSummary: Schema.array(String).default([
"🔍【弹幕情报站】本场直播数据如下:",
"🧍♂️ 总共 -dmc 位-mdn上线",
"💬 共计 -dca 条弹幕飞驰而过",
"📊 热词云图已生成,快来看看你有没有上榜!",
"👑 本场顶级输出选手:",
"🥇 -un1 - 弹幕输出 -dc1 条",
"🥈 -un2 - 弹幕 -dc2 条,萌力惊人",
"🥉 -un3 - -dc3 条精准狙击",
"🎖️ 特别嘉奖:-un4 & -un5",
"你们的弹幕,我们都记录在案!🕵️♀️"
]).role("table").description("自定义直播总结语,开启弹幕词云自动发送。变量解释:-dmc代表总弹幕发送人数,-mdn代表主播粉丝牌子名,-dca代表总弹幕数,-un1到-un5代表弹幕发送条数前五名用户的用户名,-dc1到-dc5代表弹幕发送条数前五名的弹幕发送数量,数组每一行代表换行"),
customGuardBuyImg: Schema.intersect([Schema.object({ enable: Schema.boolean().default(false).description("是否开启自定义上舰图片功能").experimental() }), Schema.union([Schema.object({
enable: Schema.const(true).required(),
captainImgUrl: Schema.string().default("https://s1.hdslb.com/bfs/static/blive/live-pay-mono/relation/relation/assets/captain-Bjw5Byb5.png").description("舰长图片链接"),
supervisorImgUrl: Schema.string().default("https://s1.hdslb.com/bfs/static/blive/live-pay-mono/relation/relation/assets/supervisor-u43ElIjU.png").description("提督图片链接"),
governorImgUrl: Schema.string().default("https://s1.hdslb.com/bfs/static/blive/live-pay-mono/relation/relation/assets/governor-DpDXKEdA.png").description("总督图片链接")
}), Schema.object({})])]),
restartPush: Schema.boolean().default(true).description("插件重启后,如果订阅的主播正在直播,是否进行一次推送,默认开启"),
pushTime: Schema.number().min(0).max(12).step(.5).default(1).description("设定间隔多长时间推送一次直播状态,单位为小时,默认为一小时"),
customLiveStart: Schema.string().default("-name开播啦,当前粉丝数:-follower\\n-link").description("自定义开播提示语,-name代表UP昵称,-follower代表当前粉丝数,-link代表直播间链接(如果使用的是QQ官方机器人,请不要使用),\\n为换行。例如-name开播啦,会发送为xxxUP开播啦"),
customLive: Schema.string().default("-name正在直播,目前已播-time,累计观看人数:-watched\\n-link").description("自定义直播中提示语,-name代表UP昵称,-time代表开播时长,-watched代表累计观看人数,-link代表直播间链接(如果使用的是QQ官方机器人,请不要使用),\\n为换行。例如-name正在直播,会发送为xxxUP正在直播xxx"),
customLiveEnd: Schema.string().default("-name下播啦,本次直播了-time,粉丝数变化-follower_change").description("自定义下播提示语,-name代表UP昵称,-follower_change代表本场直播粉丝数变,-time代表开播时长,\\n为换行。例如-name下播啦,本次直播了-time,会发送为xxxUP下播啦,直播时长为xx小时xx分钟xx秒"),
followerDisplay: Schema.boolean().default(true).description("粉丝数变化和累积观看本场直播的人数是否显示在推送卡片中"),
hideDesc: Schema.boolean().default(false).description("是否隐藏UP主直播间简介,开启后推送的直播卡片将不再展示简介"),
style: Schema.object({}).description("美化设置"),
removeBorder: Schema.boolean().default(false).description("移除推送卡片边框"),
cardColorStart: Schema.string().pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).default("#F38AB5").description("推送卡片的开始渐变背景色,请填入16进制颜色代码,参考网站:https://webkul.github.io/coolhue/"),
cardColorEnd: Schema.string().pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).default("#F9CCDF").description("推送卡片的结束渐变背景色,请填入16进制颜色代码,参考网站:https://colorate.azurewebsites.net/"),
cardBasePlateColor: Schema.string().pattern(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).default("#FFF5EE").description("推送卡片底板颜色,请填入16进制颜色代码"),
cardBasePlateBorder: Schema.string().pattern(/\d*\.?\d+(?:px|em|rem|%|vh|vw|vmin|vmax)/).default("15px").description("推送卡片底板边框宽度,请填入css单位,例如1px,12.5rem,100%"),
enableLargeFont: Schema.boolean().default(false).description("是否开启动态推送卡片大字体模式,默认为小字体。小字体更漂亮,但阅读比较吃力,大字体更易阅读,但相对没这么好看"),
font: Schema.string().description("推送卡片的字体样式,如果你想用你自己的字体可以在此填写,例如:Microsoft YaHei"),
filter: Schema.intersect([Schema.object({ enable: Schema.boolean().default(false).description("是否开启动态屏蔽功能") }).description("屏蔽设置"), Schema.union([Schema.object({
enable: Schema.const(true).required(),
notify: Schema.boolean().default(false).description("动态被屏蔽是否发送提示"),
regex: Schema.string().description("正则表达式屏蔽"),
keywords: Schema.array(String).description("关键字屏蔽,一个关键字为一项"),
forward: Schema.boolean().default(false).description("是否屏蔽转发动态"),
article: Schema.boolean().default(false).description("是否屏蔽专栏")
}), Schema.object({})])]),
debug: Schema.object({}).description("调试设置"),
dynamicDebugMode: Schema.boolean().default(false).description("动态调试模式,开启后会在控制台输出动态推送的详细信息,用于调试")
});
//#endregion
//#region src/utils/index.ts
/**
* 高阶函数:为函数添加锁机制
* @param {Function} fn - 需要包装的原始函数
* @returns {Function} 带锁功能的函数
*/
function withLock(fn) {
const isAsync = fn.constructor.name === "AsyncFunction";
let locked = false;
if (isAsync) return (...args) => {
if (locked) return;
locked = true;
Promise.resolve(fn(...args)).catch((err) => {
console.error("Execution error:", err);
throw err;
}).finally(() => {
locked = false;
});
};
return (...args) => {
if (locked) return;
locked = true;
try {
fn(...args);
} catch (err) {
console.error("Execution error:", err);
throw err;
} finally {
locked = false;
}
};
}
async function withRetry(fn, maxAttempts = 3, delayMs = 1e3) {
let attempt = 0;
while (attempt < maxAttempts) try {
return await fn();
} catch (error) {
attempt++;
if (attempt >= maxAttempts) throw error;
await new Promise((resolve$1) => setTimeout(resolve$1, delayMs * attempt));
}
}
function replaceButKeep(oldObj, newObj, keepKeys) {
const result = { ...newObj };
for (const key of keepKeys) result[key] = oldObj[key];
return result;
}
//#endregion
//#region src/type/index.ts
let LiveType = /* @__PURE__ */ function(LiveType$1) {
LiveType$1[LiveType$1["NotLiveBroadcast"] = 0] = "NotLiveBroadcast";
LiveType$1[LiveType$1["StartBroadcasting"] = 1] = "StartBroadcasting";
LiveType$1[LiveType$1["LiveBroadcast"] = 2] = "LiveBroadcast";
LiveType$1[LiveType$1["StopBroadcast"] = 3] = "StopBroadcast";
LiveType$1[LiveType$1["FirstLiveBroadcast"] = 4] = "FirstLiveBroadcast";
return LiveType$1;
}({});
let PushType = /* @__PURE__ */ function(PushType$1) {
PushType$1[PushType$1["Live"] = 0] = "Live";
PushType$1[PushType$1["Dynamic"] = 1] = "Dynamic";
PushType$1[PushType$1["DynamicAtAll"] = 2] = "DynamicAtAll";
PushType$1[PushType$1["StartBroadcasting"] = 3] = "StartBroadcasting";
PushType$1[PushType$1["LiveGuardBuy"] = 4] = "LiveGuardBuy";
PushType$1[PushType$1["WordCloudAndLiveSummary"] = 5] = "WordCloudAndLiveSummary";
PushType$1[PushType$1["Superchat"] = 6] = "Superchat";
return PushType$1;
}({});
const PushTypeMsg = {
[PushType.Live]: "直播推送",
[PushType.Dynamic]: "动态推送",
[PushType.DynamicAtAll]: "动态推送+At全体",
[PushType.StartBroadcasting]: "开播推送",
[PushType.LiveGuardBuy]: "上舰推送",
[PushType.WordCloudAndLiveSummary]: "弹幕词云和直播总结推送",
[PushType.Superchat]: "SC推送"
};
//#endregion
//#region src/stop_words.ts
const stopwords = new Set([
",",
"。",
"!",
"?",
":",
";",
"“",
"”",
"‘",
"’",
"(",
")",
"、",
"……",
"——",
"-",
"_",
".",
",",
"(",
")",
"【",
"】",
"而且",
"但是",
"如果",
"虽然",
"因为",
"所以",
"但是",
"那么",
"那么就",
"今天",
"昨天",
"后天",
"明天",
"现在",
"刚刚",
"刚才",
"一直",
"一直在",
"目前",
"以前",
"以后",
"以前的",
"the",
"and",
"to",
"of",
"a",
"is",
"in",
"on",
"for",
"with",
"this",
"that",
"you",
"觉得",
"表示",
"发现",
"认为",
"看到",
"听说",
"了解",
"知道",
"说明",
"指出",
"讨论",
"讨论一下",
"看看",
"想想",
"说说",
"讲讲",
"一个",
"一些",
"这个",
"那个",
"每个",
"什么",
"东西",
"事情",
"这些",
"那些",
"这种",
"那种",
"怎么说",
"怎么会",
"怎么可能",
"不可能",
"有点像",
"真的很",
"特别是",
"有时候",
"每次都",
"一点点",
"哪里有",
"太离谱",
"太搞笑",
"太真实",
"为了",
"因为",
"所以",
"但是",
"而且",
"然后",
"如果",
"虽然",
"然而",
"不过",
"并且",
"即使",
"由于",
"那么",
"除非",
"比如",
"比如说",
"现在",
"刚刚",
"刚才",
"以前",
"以后",
"一直",
"从来",
"目前",
"最近",
"已经",
"后来",
"之前",
"某天"
]);
var stop_words_default = stopwords;
//#endregion
//#region src/command_register.ts
var ComRegister = class ComRegister {
static inject = [
"bilibili-notify",
"bilibili-notify-api",
"bilibili-notify-live",
"bilibili-notify-generate-img",
"database"
];
qqRelatedBotList = [
"qq",
"onebot",
"red",
"satori",
"chronocat"
];
logger;
config;
loginTimer;
num = 0;
rebootCount = 0;
subNotifier;
ctx;
subManager;
dynamicTimelineManager;
liveAPIManager;
liveWSManager;
pushArrMap;
loginDBData;
privateBot;
dynamicJob;
liveAPIJob;
_jieba = Jieba.withDict(dict);
stopwords;
reciveSubTimes = 0;
groupInfo = null;
constructor(ctx, config) {
this.ctx = ctx;
this.init(config);
const statusCom = ctx.command("status", "插件状态相关指令", { permissions: ["authority:5"] });
statusCom.subcommand(".dyn", "查看动态监测运行状态").usage("查看动态监测运行状态").example("status dyn").action(() => {
if (this.dynamicJob?.isActive) return "动态监测正在运行";
return "动态监测未运行";
});
statusCom.subcommand(".sm", "查看订阅管理对象").usage("查看订阅管理对象").example("status sm").action(async () => {
this.logger.info(this.subManager);
return "查看控制台";
});
statusCom.subcommand(".bot", "查询当前拥有的机器人信息", { hidden: true }).usage("查询当前拥有的机器人信息").example("status bot 查询当前拥有的机器人信息").action(() => {
this.logger.info("开始输出BOT信息");
for (const bot of ctx.bots) {
this.logger.info("--------------------------------");
this.logger.info(`平台:${bot.platform}`);
this.logger.info(`名称:${bot.user.name}`);
this.logger.info("--------------------------------");
}
});
statusCom.subcommand(".env", "查询当前环境的信息", { hidden: true }).usage("查询当前环境的信息").example("status env 查询当前环境的信息").action(async ({ session }) => {
await session.send(`Guild ID:${session.event.guild.id}`);
await session.send(`Channel ID: ${session.event.channel.id}`);
});
const biliCom = ctx.command("bili", "bili-notify插件相关指令", { permissions: ["authority:3"] });
biliCom.subcommand(".login", "登录B站之后才可以进行之后的操作").usage("使用二维码登录,登录B站之后才可以进行之后的操作").example("bili login").action(async ({ session }) => {
this.logger.info("调用bili login指令");
let content;
try {
content = await ctx["bilibili-notify-api"].getLoginQRCode();
} catch (_) {
return "bili login getLoginQRCode() 本次网络请求失败";
}
if (content.code !== 0) return await session.send("出问题咯!");
QRCode.toBuffer(content.data.url, {
errorCorrectionLevel: "H",
type: "png",
margin: 1,
color: {
dark: "#000000",
light: "#FFFFFF"
}
}, async (err, buffer) => {
if (err) return await session.send("二维码生成出错,请重新尝试");
await session.send(h.image(buffer, "image/jpeg"));
});
if (this.loginTimer) this.loginTimer();
let flag = true;
this.loginTimer = ctx.setInterval(async () => {
try {
if (!flag) return;
flag = false;
let loginContent;
try {
loginContent = await ctx["bilibili-notify-api"].getLoginStatus(content.data.qrcode_key);
} catch (e) {
this.logger.error(e);
return;
}
if (loginContent.code !== 0) {
this.loginTimer();
return await session.send("登录失败请重试");
}
if (loginContent.data.code === 86038) {
this.loginTimer();
return await session.send("二维码已失效,请重新登录");
}
if (loginContent.data.code === 0) {
const encryptedCookies = ctx["bilibili-notify-api"].encrypt(ctx["bilibili-notify-api"].getCookies());
const encryptedRefreshToken = ctx["bilibili-notify-api"].encrypt(loginContent.data.refresh_token);
await ctx.database.upsert("loginBili", [{
id: 1,
bili_cookies: encryptedCookies,
bili_refresh_token: encryptedRefreshToken
}]);
this.loginDBData = (await this.ctx.database.get("loginBili", 1))[0];
await this.ctx["bilibili-notify-api"].loadCookiesFromDatabase();
await this.checkIfLoginInfoIsLoaded();
this.loginTimer();
ctx["bilibili-notify-api"].disposeNotifier();
await session.send("登录成功,请重启插件");
}
} finally {
flag = true;
}
}, 1e3);
});
biliCom.subcommand(".list", "展示订阅对象").usage("展示订阅对象").example("bili list").action(() => {
return this.subShow();
});
biliCom.subcommand(".private", "向主人账号发送一条测试消息", { hidden: true }).usage("向主人账号发送一条测试消息").example("bili private 向主人账号发送一条测试消息").action(async ({ session }) => {
await this.sendPrivateMsg("Hello World");
await session.send("已发送消息,如未收到则说明您的机器人不支持发送私聊消息或您的信息填写有误");
});
biliCom.subcommand(".ll").usage("展示当前正在直播的订阅对象").example("bili ll").action(async () => {
const { data: { live_users } } = await ctx["bilibili-notify-api"].getTheUserWhoIsLiveStreaming();
const subLiveUsers = [];
if (live_users?.items) for (const [uid, sub] of this.subManager) {
let onLive = false;
for (const user of live_users.items) if (user.mid.toString() === uid && sub.live) {
onLive = true;
break;
}
subLiveUsers.push({
uid: Number.parseInt(uid),
uname: sub.uname,
onLive
});
}
let table = "";
if (subLiveUsers.length === 0) table += "当前没有正在直播的订阅对象";
else for (const user of subLiveUsers) table += `[UID:${user.uid}] 「${user.uname}」 ${user.onLive ? "正在直播" : "未开播"}\n`;
return table;
});
biliCom.subcommand(".dyn <uid:string> [index:number]", "手动推送一条动态信息", { hidden: true }).usage("手动推送一条动态信息").example("bili dyn 233 1 手动推送UID为233用户空间的第一条动态信息").action(async ({ session }, uid, index) => {
const i = index && index - 1 || 0;
const item = (await this.ctx["bilibili-notify-api"].getUserSpaceDynamic(uid)).data.items[i];
const buffer = await withRetry(async () => {
return await this.ctx["bilibili-notify-generate-img"].generateDynamicImg(item);
}, 1).catch(async (e) => {
if (e.message === "直播开播动态,不做处理") {
await session.send("直播开播动态,不做处理");
return;
}
if (e.message === "出现关键词,屏蔽该动态") {
await session.send("已屏蔽该动态");
return;
}
if (e.message === "已屏蔽转发动态") {
await session.send("已屏蔽转发动态");
return;
}
if (e.message === "已屏蔽专栏动态") {
await session.send("已屏蔽专栏动态");
return;
}
this.logger.error(`dynamicDetect generateDynamicImg() 推送卡片发送失败,原因:${e.message}`);
});
buffer && await session.send(h.image(buffer, "image/jpeg"));
});
biliCom.subcommand(".wc").action(async ({ session }) => {
const img = h.image(await this.ctx["bilibili-notify-generate-img"].generateWordCloudImg([
["摆烂", 91],
["可以", 82],
["可以", 72],
["可以", 42],
["dog", 40],
["dog", 40],
["不是", 37],
["不是", 37],
["就是", 27],
["就是", 27],
["吃瓜", 16],
["吃瓜", 16],
["吃瓜", 16],
["cj", 8],
["cj", 8],
["cj", 8],
["没有", 8],
["没有", 8],
["没有", 8],
["有点", 8],
["有点", 8],
["喜欢", 7],
["喜欢", 7],
["空调", 7],
["空调", 7],
["空调", 7],
["感觉", 7],
["感觉", 7],
["感觉", 7],
["时候", 6],
["时候", 6],
["怎么", 6],
["怎么", 6],
["痛车", 6],
["痛车", 6],
["一下", 6],
["一下", 6],
["还是", 6],
["还是", 6],
["麻麻", 6],
["麻麻", 6],
["下午", 5],
["下午", 5],
["开始", 5],
["开始", 5],
["一部", 5],
["一部", 5],
["这样", 5],
["这样", 5],
["上次", 5],
["上次", 5],
["游戏", 5],
["游戏", 5],
["这边", 5],
["这边", 5],
["问号", 5],
["问号", 5],
["好看", 5],
["好看", 5],
["哈哈哈", 5],
["哈哈哈", 5],
["角色", 5],
["角色", 5],
["味道", 5],
["味道", 5],
["233333", 4],
["233333", 4],
["老规矩", 4],
["老规矩", 4],
["鸣潮", 4],
["鸣潮", 4],
["养生", 4],
["养生", 4],
["划掉", 4],
["划掉", 4],
["排队", 4],
["排队", 4],
["cos", 4],
["cos", 4],
["的话", 4],
["的话", 4],
["我们", 4],
["主要", 4],
["www", 4],
["直接", 4],
["不好", 4],
["学校", 4],
["一样", 4],
["初中", 4],
["毕业", 4]
], "词云测试"), "image/jpg");
const top5DanmakuMaker = [
["张三", 60],
["李四", 48],
["王五", 45],
["赵六", 27],
["田七", 25]
];
const summary = this.config.liveSummary.join("\n").replace("-dmc", "114").replace("-mdn", "特工").replace("-dca", "514").replace("-un1", `${top5DanmakuMaker[0][0]}`).replace("-dc1", `${top5DanmakuMaker[0][1]}`).replace("-un2", `${top5DanmakuMaker[1][0]}`).replace("-dc2", `${top5DanmakuMaker[1][1]}`).replace("-un3", `${top5DanmakuMaker[2][0]}`).replace("-dc3", `${top5DanmakuMaker[2][1]}`).replace("-un4", `${top5DanmakuMaker[3][0]}`).replace("-dc4", `${top5DanmakuMaker[3][1]}`).replace("-un5", `${top5DanmakuMaker[4][0]}`).replace("-dc5", `${top5DanmakuMaker[4][1]}`).replaceAll("\\n", "\n");
await session.send(h("message", [img, h.text(summary)]));
});
biliCom.subcommand(".cap").action(async ({ session }) => {
const { code: userInfoCode, data: userInfoData } = await withRetry(async () => {
return {
code: 0,
data: await this.ctx["bilibili-notify-api"].getUserInfo("114514")
};
}).then((content) => content.data);
if (userInfoCode !== -352 || !userInfoData.v_voucher) return "不满足验证条件,不需要执行该命令,如果提示风控可以尝试重启插件";
const { data } = await ctx["bilibili-notify-api"].v_voucherCaptcha(userInfoData.v_voucher);
if (!data.geetest) return "当前风控无法通过该验证解除,或许考虑人工申诉?";
await session.send("请到该网站进行验证操作:https://kuresaru.github.io/geetest-validator/");
await session.send("请手动填入 gt 和 challenge 后点击生成进行验证,验证完成后点击结果,根据提示输入对应validate");
await session.send(`gt:${data.geetest.gt}`);
await session.send(`challenge:${data.geetest.challenge}`);
await session.send("请直接输入validate");
const validate = await session.prompt();
const seccode = `${validate}|jordan`;
const { data: validateCaptchaData } = await ctx["bilibili-notify-api"].validateCaptcha(data.geetest.challenge, data.token, validate, seccode);
if (validateCaptchaData?.is_valid !== 1) return "验证不成功!";
await this.ctx.sleep(10 * 1e3);
const { code: validCode, data: validData } = await ctx["bilibili-notify-api"].getUserInfo("114514", validateCaptchaData.grisk_id);
if (validCode === -352 && validData.v_voucher) return "验证不成功!";
await session.send("验证成功!请重启插件");
});
biliCom.subcommand(".ai").action(async () => {
this.logger.info("开始生成AI直播总结");
const res = await this.ctx["bilibili-notify-api"].chatWithAI(`请你生成直播总结,用这样的风格,多使用emoji并且替换示例中的emoji,同时要对每个人进行个性化点评,一下是风格参考:
🔍【弹幕情报站】本场直播数据如下:
🧍♂️ 总共 XX 位 (这里用medalName) 上线
💬 共计 XXX 条弹幕飞驰而过
📊 热词云图已生成,快来看看你有没有上榜!
👑 本场顶级输出选手:
🥇 XXX - 弹幕输出 XX 条,(这里进行吐槽)
🥈 XXX - 弹幕 XX 条,(这里进行吐槽)
🥉 XXX - 弹幕 XX 条,(这里进行吐槽)
🎖️ 特别嘉奖:XXX(这里进行吐槽) & XXX(这里进行吐槽)。
别以为发这么点弹幕就能糊弄过去,本兔可是盯着你们的!下次再偷懒小心被我踹飞!🐰🥕
以下是直播数据:${JSON.stringify({
medalName: "特工",
danmakuSenderCount: "56",
danmakuCount: "778",
top5DanmakuSender: [
["张三", 71],
["李四", 67],
["王五", 57],
["赵六", 40],
["田七", 31]
],
top10Word: [
["摆烂", 91],
["可以", 82],
["dog", 40],
["不是", 37],
["就是", 27],
["吃瓜", 16],
["cj", 8],
["没有", 8],
["有点", 8],
["喜欢", 7],
["空调", 7]
],
liveStartTime: "2025-07-21 12:56:05",
liveEndTime: "2025-07-21 15:40:30"
})}`);
this.logger.info("AI 生成完毕,结果为:");
this.logger.info(res.choices[0].message.content);
});
biliCom.subcommand(".img").action(async ({ session }) => {
const guardImg = ComRegister.GUARD_LEVEL_IMG[GuardLevel.Jianzhang];
const buffer = await this.ctx["bilibili-notify-generate-img"].generateBoardingImg(guardImg, {
guardLevel: GuardLevel.Jianzhang,
face: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSESgEED4WoyK9O5FFgrV8cHZPM4w4JgleZQ&s",
uname: "恶魔兔",
isAdmin: 1
}, {
masterName: "籽岷",
masterAvatarUrl: "https://img.touxiangkong.com/uploads/allimg/20203301251/2020/3/BjEbyu.jpg"
});
await session.send(h.image(buffer, "image/jpeg"));
});
}
async init(config) {
this.logger = this.ctx.logger("bilibili-notify-core");
this.logger.info("初始化插件中...");
this.config = config;
this.privateBot = this.ctx.bots.find((bot) => bot.platform === config.master.platform);
if (!this.privateBot) this.ctx.notifier.create({ content: "您未配置私人机器人,将无法向您推送机器人状态!" });
this.loginDBData = (await this.ctx.database.get("loginBili", 1, ["dynamic_group_id"]))[0];
await this.checkIfLoginInfoIsLoaded();
if (!await this.checkIfIsLogin()) {
this.logger.info("账号未登录,请登录");
return;
}
this.mergeStopWords(config.wordcloudStopWords);
this.initAllManager();
this.registeringForEvents();
if (config.advancedSub) {
this.logger.info("开启高级订阅,等待加载订阅...");
this.ctx.emit("bilibili-notify/ready-to-recive");
} else if (config.subs && config.subs.length > 0) {
const subs = this.configSubsToSubscription(config.subs);
await this.initAsyncPart(subs);
} else this.logger.info("初始化完毕,未添加任何订阅!");
}
registeringForEvents() {
this.ctx.on("dispose", () => {
if (this.loginTimer) this.loginTimer();
if (this.dynamicJob) this.dynamicJob.stop();
if (this.liveAPIJob) this.liveAPIJob.stop();
for (const [roomId, timer] of this.liveWSManager) {
this.ctx["bilibili-notify-live"].closeListener(roomId);
if (timer) timer();
}
});
if (this.config.advancedSub) this.ctx.on("bilibili-notify/advanced-sub", async (subs) => {
if (Object.keys(subs).length === 0) {
this.logger.info("初始化完毕,未添加任何订阅!");
return;
}
if (this.reciveSubTimes >= 1) await this.ctx["bilibili-notify"].restartPlugin();
else {
this.processUname(subs);
await this.initAsyncPart(subs);
}
this.reciveSubTimes++;
});
}
processUname(subs) {
for (const uname of Object.keys(subs)) subs[uname].uname = uname;
}
async initAsyncPart(subs) {
this.ctx["bilibili-notify-live"].clearListeners();
this.logger.info("获取到订阅信息,开始加载订阅...");
const groupInfoResult = await this.getGroupInfo();
if (groupInfoResult.code !== 0) {
this.logger.error("获取分组信息失败,插件初始化失败!");
return;
}
this.groupInfo = groupInfoResult.data;
const { code, message } = await this.loadSubFromConfig(subs);
if (code !== 0) {
this.logger.error(message);
this.logger.error("订阅对象加载失败,插件初始化失败!");
await this.sendPrivateMsg("订阅对象加载失败,插件初始化失败!");
return;
}
this.initManagerAfterLoadSub();
this.checkIfDynamicDetectIsNeeded();
this.updateSubNotifier();
this.logger.info("插件初始化完毕!");
}
mergeStopWords(stopWordsStr) {
if (!stopWordsStr || stopWordsStr.trim() === "") {
this.stopwords = new Set(stop_words_default);
return;
}
const additionalStopWords = stopWordsStr.split(",").map((word) => word.trim()).filter((word) => word !== "");
this.stopwords = new Set([...stop_words_default, ...additionalStopWords]);
}
initManagerAfterLoadSub() {
for (const [uid, sub] of this.subManager) {
if (sub.dynamic) this.dynamicTimelineManager.set(uid, Math.floor(DateTime.now().toSeconds()));
if (sub.live) this.liveAPIManager.set(uid, {
roomId: sub.roomId,
live: false,
liveRoomInfo: void 0,
masterInfo: void 0,
watchedNum: "0",
liveStartTime: "",
liveStartTimeInit: false,
push: 0
});
}
}
initAllManager() {
this.subManager = /* @__PURE__ */ new Map();
this.dynamicTimelineManager = /* @__PURE__ */ new Map();
this.liveAPIManager = /* @__PURE__ */ new Map();
this.liveWSManager = /* @__PURE__ */ new Map();
this.pushArrMap = /* @__PURE__ */ new Map();
}
configSubsToSubscription(sub) {
const subs = {};
sub.forEach((s) => {
const target = [{
channelArr: s.target.split(",").map((channelId) => ({
channelId,
dynamic: s.dynamic,
dynamicAtAll: s.dynamicAtAll,
live: s.live,
liveAtAll: s.liveAtAll,
liveGuardBuy: s.liveGuardBuy,
superchat: s.superchat,
wordcloud: s.wordcloud,
liveSummary: s.liveSummary
})),
platform: s.platform
}];
const [uid, roomid] = s.uid.split(",");
subs[s.name] = {
uname: s.name,
uid,
roomid,
dynamic: s.dynamic,
live: s.live,
target,
customCardStyle: { enable: false },
customLiveMsg: { enable: false },
customLiveSummary: { enable: false },
customGuardBuyImg: { enable: false }
};
});
return subs;
}
getBot(pf, selfId) {
if (!selfId || selfId === "") return this.ctx.bots.find((bot) => bot.platform === pf);
return this.ctx.bots.find((bot) => bot.platform === pf && bot.selfId === selfId);
}
async sendPrivateMsg(content) {
if (this.config.master.enable) {
if (this.privateBot?.status !== Universal.Status.ONLINE) {
this.logger.error(`${this.privateBot.platform} 机器人未初始化完毕,无法进行推送`);
return;
}
if (this.config.master.masterAccountGuildId) await this.privateBot.sendPrivateMessage(this.config.master.masterAccount, content, this.config.master.masterAccountGuildId);
else await this.privateBot.sendPrivateMessage(this.config.master.masterAccount, content);
}
}
async sendPrivateMsgAndRebootService() {
if (this.rebootCount >= 3) {
this.logger.error("已重启插件三次,请检查机器人状态后使用指令 bn start 启动插件");
await this.sendPrivateMsg("已重启插件三次,请检查机器人状态后使用指令 bn start 启动插件");
await this.ctx["bilibili-notify"].disposePlugin();
return;
}
this.rebootCount++;
this.logger.info("插件出现未知错误,正在重启插件");
if (await this.ctx["bilibili-notify"].restartPlugin()) this.logger.info("重启插件成功");
else {
this.logger.error("重启插件失败,请检查机器人状态后使用指令 bn start 启动插件");
await this.sendPrivateMsg("重启插件失败,请检查机器人状态后使用指令 bn start 启动插件");
await this.ctx["bilibili-notify"].disposePlugin();
}
}
async sendPrivateMsgAndStopService() {
await this.sendPrivateMsg("插件发生未知错误,请检查机器人状态后使用指令 bn start 启动插件");
this.logger.error("插件发生未知错误,请检查机器人状态后使用指令 bn start 启动插件");
await this.ctx["bilibili-notify"].disposePlugin();
}
async sendMessageWithRetry(bot, channelId, content) {
withRetry(async () => await bot.sendMessage(channelId, content), 1).catch(async (e) => {
if (e.message === "this._request is not a function") {
this.ctx.setTimeout(async () => {
await this.sendMessageWithRetry(bot, channelId, content);
}, 2e3);
return;
}
this.logger.error(`发送群组ID:${channelId}消息失败!原因: ${e.message}`);
await this.sendPrivateMsg(`发送群组ID:${channelId}消息失败,请查看日志`);
});
}
preInitConfig(subs) {
for (const sub of Object.values(subs)) {
if (sub.customLiveMsg.enable) {
if (!sub.customLiveMsg.customLiveStart.trim()) sub.customLiveMsg.customLiveStart = this.config.customLiveStart;
if (!sub.customLiveMsg.customLiveEnd.trim()) sub.customLiveMsg.customLiveEnd = this.config.customLiveEnd;
if (!sub.customLiveMsg.customLive.trim()) sub.customLiveMsg.customLive = this.config.customLive;
} else {
sub.customLiveMsg.enable = false;
sub.customLiveMsg.customLiveStart = this.config.customLiveStart;
sub.customLiveMsg.customLiveEnd = this.config.customLiveEnd;
sub.customLiveMsg.customLive = this.config.customLive;
}
if (sub.customGuardBuyImg.enable) {
if (!sub.customGuardBuyImg.captainImgUrl.trim()) sub.customGuardBuyImg.captainImgUrl = this.config.customGuardBuyImg.captainImgUrl;
if (!sub.customGuardBuyImg.supervisorImgUrl.trim()) sub.customGuardBuyImg.supervisorImgUrl = this.config.customGuardBuyImg.supervisorImgUrl;
if (!sub.customGuardBuyImg.governorImgUrl.trim()) sub.customGuardBuyImg.governorImgUrl = this.config.customGuardBuyImg.governorImgUrl;
} else if (this.config.customGuardBuyImg.enable) {
sub.customGuardBuyImg.enable = true;
sub.customGuardBuyImg.captainImgUrl = this.config.customGuardBuyImg.captainImgUrl;
sub.customGuardBuyImg.supervisorImgUrl = this.config.customGuardBuyImg.supervisorImgUrl;
sub.customGuardBuyImg.governorImgUrl = this.config.customGuardBuyImg.governorImgUrl;
}
if (sub.customLiveSummary.enable) {
if (!sub.customLiveSummary.liveSummary.trim()) sub.customLiveSummary.liveSummary = this.config.liveSummary.join("\n");
} else {
sub.customLiveSummary.enable = false;
sub.customLiveSummary.liveSummary = this.config.liveSummary.join("\n");
}
const dynamicArr = [];
const dynamicAtAllArr = [];
const liveArr = [];
const liveAtAllArr = [];
const liveGuardBuyArr = [];
const superchatArr = [];
const wordcloudArr = [];
const liveSummaryArr = [];
for (const platform of sub.target) for (const channel of platform.channelArr) {
const target = `${platform.platform}:${channel.channelId}`;
const conditions = [
["dynamic", dynamicArr],
["dynamicAtAll", dynamicAtAllArr],
["live", liveArr],
["liveAtAll", liveAtAllArr],
["liveGuardBuy", liveGuardBuyArr],
["superchat", superchatArr],
["wordcloud", wordcloudArr],
["liveSummary", liveSummaryArr]
];
for (const [key, arr] of conditions) if (channel[key]) arr.push(target);
}
this.pushArrMap.set(sub.uid, {
dynamicArr,
dynamicAtAllArr,
liveArr,
liveAtAllArr,
liveSummaryArr,
liveGuardBuyArr,
superchatArr,
wordcloudArr
});
}
this.logger.info("初始化推送群组/频道信息:");
this.logger.info(this.pushArrMap);
}
async pushMessage(targets, content) {
const t = {};
for (const target of targets) {
const [platform, channleId] = target.split(":");
if (!t[platform]) t[platform] = [channleId];
else t[platform].push(channleId);
}
for (const platform of Object.keys(t)) {
const bots = [];
for (const bot of this.ctx.bots) if (bot.platform === platform) bots.push(bot);
let num = 0;
const sendMessageByBot = async (channelId, botIndex = 0, retry = 3e3) => {
if (!bots[botIndex]) {
this.logger.warn(`${platform} 没有配置对应机器人,无法进行推送!`);
return;
}
if (bots[botIndex].status !== Universal.Status.ONLINE) {
if (retry >= 3e3 * 2 ** 5) {
this.logger.error(`${platform} 机器人未初始化完毕,无法进行推送,已重试5次,放弃推送`);
await this.sendPrivateMsg(`${platform} 机器人未初始化完毕,无法进行推送,已重试5次,放弃推送`);
return;
}
this.logger.error(`${platform} 机器人未初始化完毕,无法进行推送,${retry / 1e3}秒后重试`);
await this.ctx.sleep(retry);
await sendMessageByBot(channelId, botIndex, retry * 2);
return;
}
try {
await bots[botIndex].sendMessage(channelId, content);
num++;
await this.ctx.sleep(500);
} catch (e) {
this.logger.error(e);
if (bots.length > 1) await sendMessageByBot(channelId, botIndex++);
}
};
for (const channelId of t[platform]) await sendMessageByBot(channelId);
this.logger.info(`成功推送消息 ${num} 条`);
}
}
async broadcastToTargets(uid, content, type) {
const record = this.pushArrMap.get(uid);
if (!record) return;
if (!(type === PushType.StartBroadcasting && record.liveAtAllArr?.length > 0 || type === PushType.Dynamic && (record.dynamicArr?.length > 0 || record.dynamicAtAllArr?.length > 0) || (type === PushType.Live || type === PushType.StartBroadcasting) && record.liveArr?.length > 0 || type === PushType.LiveGuardBuy && record.liveGuardBuyArr?.length > 0 || type === PushType.Superchat && record.superchatArr?.length > 0 || type === PushType.WordCloudAndLiveSummary && (record.wordcloudArr?.length > 0 || record.liveSummaryArr?.length > 0))) return;
this.logger.info(`本次推送对象:${uid},推送类型:${PushTypeMsg[type]}`);
if (type === PushType.StartBroadcasting && record.liveAtAllArr?.length > 0) {
this.logger.info("推送 @全体:", record.liveAtAllArr);
const atAllArr = structuredClone(record.liveAtAllArr);
await withRetry(() => this.pushMessage(atAllArr, h.at("all")), 1);
}
if (type === PushType.Dynamic && record.dynamicArr?.length > 0) {
if (record.dynamicAtAllArr?.length > 0) {
this.logger.info("推送动态 @全体:", record.dynamicAtAllArr);
const dynamicAtAllArr = structuredClone(record.dynamicAtAllArr);
await withRetry(() => this.pushMessage(dynamicAtAllArr, h.at("all")), 1);
}
this.logger.info("推送动态:", record.dynamicArr);
const dynamicArr = structuredClone(record.dynamicArr);
await withRetry(() => this.pushMessage(dynamicArr, h("message", content)), 1);
}
if ((type === PushType.Live || type === PushType.StartBroadcasting) && record.liveArr?.length > 0) {
this.logger.info("推送直播:", record.liveArr);
const liveArr = structuredClone(record.liveArr);
await withRetry(() => this.pushMessage(liveArr, h("message", content)), 1);
}
if (type === PushType.LiveGuardBuy && record.liveGuardBuyArr?.length > 0) {
this.logger.info("推送直播守护购买:", record.liveGuardBuyArr);
const liveGuardBuyArr = structuredClone(record.liveGuardBuyArr);
await withRetry(() => this.pushMessage(liveGuardBuyArr, h("message", content)), 1);
}
if (type === PushType.Superchat && record.superchatArr?.length > 0) {
this.logger.info("推送SC:", record.superchatArr);
const superchatArr = structuredClone(record.superchatArr);
await withRetry(() => this.pushMessage(superchatArr, h("message", content)), 1);
}
if (type === PushType.WordCloudAndLiveSummary) {
const wordcloudArr = structuredClone(record.wordcloudArr);
const liveSummaryArr = structuredClone(record.liveSummaryArr);
const wordcloudAndLiveSummaryArr = wordcloudArr.filter((item) => liveSummaryArr.includes(item));
const wordcloudOnlyArr = wordcloudArr.filter((item) => !liveSummaryArr.includes(item));
const liveSummaryOnlyArr = liveSummaryArr.filter((item) => !wordcloudArr.includes(item));
if (wordcloudAndLiveSummaryArr.length > 0) {
this.logger.info("推送词云和直播总结:", wordcloudAndLiveSummaryArr);
const msgs = content.filter(Boolean);
if (msgs.length > 0) await withRetry(() => this.pushMessage(wordcloudAndLiveSummaryArr, h("message", msgs)), 1);
}
if (content[0] && wordcloudOnlyArr.length > 0) {
this.logger.info("推送词云:", wordcloudOnlyArr);
await withRetry(() => this.pushMessage(wordcloudOnlyArr, h("message", content[0])), 1);
}
if (content[1] && liveSummaryOnlyArr.length > 0) {
this.logger.info("推送直播总结:", liveSummaryOnlyArr);
await withRetry(() => this.pushMessage(liveSummaryOnlyArr, h("message", content[1])), 1);
}
}
}
dynamicDetect() {
const handler = async () => {
const currentPushDyn = {};
const content = await withRetry(async () => {
return await this.ctx["bilibili-notify-api"].getAllDynamic();
}, 1).catch((e) => {
this.logger.error(`dynamicDetect getAllDynamic() 发生了错误,错误为:${e.message}`);
});
if (!content) return;
if (content.code !== 0) switch (content.code) {
case -101:
this.logger.error("账号未登录,插件已停止工作,请登录");
await this.sendPrivateMsg("账号未登录,插件已停止工作,请登录");
await this.ctx["bilibili-notify"].disposePlugin();
break;
case -352:
this.logger.error("账号被风控,插件已停止工作,请输入指令 bili cap 根据提示解除风控");
await this.sendPrivateMsg("账号被风控,插件已停止工作,请输入指令 bili cap 根据提示解除风控");
await this.ctx["bilibili-notify"].disposePlugin();
break;
default:
this.logger.error(`获取动态信息错误,错误码为:${content.code},错误为:${content.message},请排除错误后输入指令 bn start 重启插件`);
await this.sendPrivateMsg(`获取动态信息错误,错误码为:${content.code},错误为:${content.message},请排除错误后输入指令 bn start 重启插件`);
await this.ctx["bilibili-notify"].disposePlugin();
break;
}
const items = content.data.items;
for (const item of items) {
if (!item) continue;
const postTime = item.modules.module_author.pub_ts;
const uid = item.modules.module_author.mid.toString();
const name$2 = item.modules.module_author.name;
if (this.dynamicTimelineManager.has(uid)) {
if (this.dynamicTimelineManager.get(uid) < postTime) {
const sub = this.subManager.get(uid);
const buffer = await withRetry(async () => {
return await this.ctx["bilibili-notify-generate-img"].generateDynamicImg(item, sub.customCardStyle.enable ? sub.customCardStyle : void 0);
}, 1).catch(async (e) => {
if (e.message === "直播开播动态,不做处理") return;
if (e.message === "出现关键词,屏蔽该动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}发布了一条含有屏蔽关键字的动态`), PushType.Dynamic);
return;
}
if (e.message === "已屏蔽转发动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}转发了一条动态,已屏蔽`), PushType.Dynamic);
return;
}
if (e.message === "已屏蔽专栏动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}投稿了一条专栏,已屏蔽`), PushType.Dynamic);
return;
}
this.logger.error(`dynamicDetect generateDynamicImg() 推送卡片发送失败,原因:${e.message}`);
await this.sendPrivateMsgAndStopService();
});
if (!buffer) continue;
let dUrl = "";
if (this.config.dynamicUrl) if (item.type === "DYNAMIC_TYPE_AV") if (this.config.dynamicVideoUrlToBV) {
const bv = item.modules.module_dynamic.major.archive.jump_url.match(/BV[0-9A-Za-z]+/);
dUrl = bv ? bv[0] : "";
} else dUrl = `${name$2}发布了新视频:https:${item.modules.module_dynamic.major.archive.jump_url}`;
else dUrl = `${name$2}发布了一条动态:https://t.bilibili.com/${item.id_str}`;
let aigc = "";
if (this.config.ai.enable) {
this.logger.info("正在生成AI动态推送内容...");
if (item.type === "DYNAMIC_TYPE_AV") {
const title = item.modules.module_dynamic.major.archive.title;
const desc = item.modules.module_dynamic.major.archive.desc;
aigc = (await this.ctx["bilibili-notify-api"].chatWithAI(`请你根据以下视频标题和简介,帮我写一份简短的动态播报,标题:${title},简介:${desc}`)).choices[0].message.content;
}
if (item.type === "DYNAMIC_TYPE_DRAW" || item.type === "DYNAMIC_TYPE_WORD") {
const title = item.modules.module_dynamic.major.opus.title;
const desc = item.modules.module_dynamic.major.opus.summary.text;
aigc = (await this.ctx["bilibili-notify-api"].chatWithAI(`请你根据以下图文动态的标题和内容,帮我写一份简短的动态播报,标题:${title},内容:${desc}`)).choices[0].message.content;
}
this.logger.info("AI动态推送内容生成完毕!");
}
this.logger.info("推送动态中...");
await this.broadcastToTargets(uid, h("message", [
h.image(buffer, "image/jpeg"),
h.text(aigc),
h.text(dUrl)
]), PushType.Dynamic);
if (this.config.pushImgsInDynamic) {
if (item.type === "DYNAMIC_TYPE_DRAW") {
const pics = item.modules?.module_dynamic?.major?.opus?.pics;
if (pics) {
const picsMsg = h("message", { forward: true }, pics.map((pic) => h.img(pic.url)));
await this.broadcastToTargets(uid, picsMsg, PushType.Dynamic);
}
}
}
if (!currentPushDyn[uid]) currentPushDyn[uid] = item;
this.logger.info("动态推送完毕!");
}
}
}
for (const uid in currentPushDyn) {
const postTime = currentPushDyn[uid].modules.module_author.pub_ts;
this.dynamicTimelineManager.set(uid, postTime);
}
};
return withLock(handler);
}
debug_dynamicDetect() {
const handler = async () => {
const currentPushDyn = {};
this.logger.info("开始获取动态信息...");
const content = await withRetry(async () => {
return await this.ctx["bilibili-notify-api"].getAllDynamic();
}, 1).catch((e) => {
this.logger.error(`dynamicDetect getAllDynamic() 发生了错误,错误为:${e.message}`);
});
if (!content) return;
if (content.code !== 0) switch (content.code) {
case -101:
this.logger.error("账号未登录,插件已停止工作,请登录");
await this.sendPrivateMsg("账号未登录,插件已停止工作,请登录");
await this.ctx["bilibili-notify"].disposePlugin();
break;
case -352:
this.logger.error("账号被风控,插件已停止工作,请输入指令 bili cap 根据提示解除风控");
await this.sendPrivateMsg("账号被风控,插件已停止工作,请输入指令 bili cap 根据提示解除风控");
await this.ctx["bilibili-notify"].disposePlugin();
break;
default:
this.logger.error(`获取动态信息错误,错误码为:${content.code},错误为:${content.message},请排除错误后输入指令 bn start 重启插件`);
await this.sendPrivateMsg(`获取动态信息错误,错误码为:${content.code},错误为:${content.message},请排除错误后输入指令 bn start 重启插件`);
await this.ctx["bilibili-notify"].disposePlugin();
break;
}
this.logger.info("获取动态信息成功!开始处理动态信息...");
const items = content.data.items;
for (const item of items) {
if (!item) continue;
const postTime = item.modules.module_author.pub_ts;
const uid = item.modules.module_author.mid.toString();
const name$2 = item.modules.module_author.name;
this.logger.info(`获取到动态信息,UP主:${name$2},UID:${uid},动态发布时间:${DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`);
if (this.dynamicTimelineManager.has(uid)) {
this.logger.info("订阅该UP主,判断动态时间线...");
const timeline = this.dynamicTimelineManager.get(uid);
this.logger.info(`上次推送时间线:${DateTime.fromSeconds(timeline).toFormat("yyyy-MM-dd HH:mm:ss")}`);
if (timeline < postTime) {
this.logger.info("需要推送该条动态,开始推送...");
const sub = this.subManager.get(uid);
this.logger.info("开始渲染推送卡片...");
const buffer = await withRetry(async () => {
return await this.ctx["bilibili-notify-generate-img"].generateDynamicImg(item, sub.customCardStyle.enable ? sub.customCardStyle : void 0);
}, 1).catch(async (e) => {
if (e.message === "直播开播动态,不做处理") return;
if (e.message === "出现关键词,屏蔽该动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}发布了一条含有屏蔽关键字的动态`), PushType.Dynamic);
return;
}
if (e.message === "已屏蔽转发动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}转发了一条动态,已屏蔽`), PushType.Dynamic);
return;
}
if (e.message === "已屏蔽专栏动态") {
if (this.config.filter.notify) await this.broadcastToTargets(uid, h("message", `${name$2}投稿了一条专栏,已屏蔽`), PushType.Dynamic);
return;
}
this.logger.error(`dynamicDetect generateDynamicImg() 推送卡片发送失败,原因:${e.message}`);
await this.sendPrivateMsgAndStopService();
});
if (!buffer) continue;
this.logger.info("渲染推送卡片成功!");
let dUrl = "";
if (this.config.dynamicUrl) {
this.logger.info("需要发送动态链接,开始生成链接...");
if (item.type === "DYNAMIC_TYPE_AV") if (this.config.dynamicVideoUrlToBV) {
const bv = item.modules.module_dynamic.major.archive.jump_url.match(/BV[0-9A-Za-z]+/);
dUrl = bv ? bv[0] : "";
} else dUrl = `${name$2}发布了新视频:https:${item.modules.module_dynamic.major.archive.jump_url}`;
else dUrl = `${name$2}发布了一条动态:https://t.bilibili.com/${item.id_str}`;
this.logger.info("动态链接生成成功!");
}
this.logger.info("推送动态中...");
await this.broadcastToTargets(uid, h("message", [h.image(buffer, "image/jpeg"), h.text(dUrl)]), PushType.Dynamic);
if (this.config.pushImgsInDynamic) {
this.logger.info("需要发送动态中的图片,开始发送...");
if (item.type === "DYNAMIC_TYPE_DRAW") {
const pics = item.modules?.module_dynamic?.major?.opus?.pics;
if (pics) {
const picsMsg = h("message", { forward: true }, pics.map((pic) => h.img(pic.url)));
await this.broadcastToTargets(uid, picsMsg, PushType.Dynamic);
}
}
this.logger.info("动态中的图片发送完毕!");
}
if (!currentPushDyn[uid]) currentPushDyn[uid] = item;
this.logger.info("动态推送完毕!");
}
}
}
this.logger.info("动态信息处理完毕!");
for (const uid in currentPushDyn) {
const postTime = currentPushDyn[uid].modules.module_author.pub_ts;
this.dynamicTimelineManager.set(uid, postTime);
this.logger.info(`更新时间线成功,UP主:${uid},时间线:${DateTime.fromSeconds(postTime).toFormat("yyyy-MM-dd HH:mm:ss")}`);
}
this.logger.info(`本次推送动态数量:${Object.keys(currentPushDyn).length}`);
};
return withLock(handler);
}
async getMasterInfo(uid, masterInfo, liveType) {
const { data } = await this.ctx["bilibili-notify-api"].getMasterInfo(uid);
let liveOpenFollowerNum;
let liveEndFollowerNum;
let liveFollowerChange;
if (liveType === LiveType.StartBroadcasting || liveType === LiveType.FirstLiveBroadcast) {
liveOpenFollowerNum = data.follower_num;
liveEndFollowerNum = data.follower_num;
liveFollowerChange = 0;
}
if (liveType === LiveType.StopBroadcast || liveType === LiveType.LiveBroadcast) {
liveOpenFollowerNum = masterInfo.liveOpenFollowerNum;
liveEndFollowerNum = data.follower_num;
liveFollowerChange = liveEndFollowerNum - masterInfo.liveOpenFollowerNum;
}
return {
username: data.info.uname,
userface: data.info.face,
roomId: data.room_id,
liveOpenFollowerNum,
liveEndFollowerNum,
liveFollowerChange,
medalName: data.medal_name
};
}
async getLiveRoomInfo(roomId) {
const data = await withRetry(async () => await this.ctx["bilibili-notify-api"].getLiveRoomInfo(roomId)).then((content) => content.data).catch((e) => {
this.logger.error(`liveDetect getLiveRoomInfo 发生了错误,错误为:${e.message}`);
});
if (!data) {
await this.sendPrivateMsgAndStopService();
return;
}
return data;
}
async sendLiveNotifyCard(liveType, liveData, liveInfo, uid, liveNotifyMsg) {
const buffer = await withRetry(async () => {
return await this.ctx["bilibili-notify-generate-img"].generateLiveImg(liveInfo.liveRoomInfo, liveInfo.masterInfo.username, liveInfo.masterInfo.userface, liveData, liveType, liveInfo.cardStyle.enable ? liveInfo.cardStyle : void 0);
}, 1).catch((e) => {
this.logger.error(`liveDetect generateLiveImg() 推送卡片生成失败,原因:${e.message}`);
});
if (!buffer) return await this.sendPrivateMsgAndStopService();
const msg = h("message", [h.image(buffer, "image/jpeg"), h.text(liveNotifyMsg || "")]);
return await this.broadcastToTargets(uid, msg, liveType === LiveType.StartBroadcasting ? PushType.StartBroadcasting : PushType.Live);
}
async segmentDanmaku(danmaku, danmakuWeightRecord) {
this._jieba.cut(danmaku, true).filter((word) => word.length >= 2 && !this.stopwords.has(word)).map((w) => {
danmakuWeightRecord[w] = (danmakuWeightRecord[w] || 0) + 1;
});
}
addUserToDanmakuMaker(username, danmakuMakerRecord) {
danmakuMakerRecord[username] = (danmakuMakerRecord[username] || 0) + 1;
}
static GUARD_LEVEL_IMG = {
[GuardLevel.Jianzhang]: "https://s1.hdslb.com/bfs/static/blive/live-pay-mono/relation/relation/assets/captain-Bjw5Byb5.png",
[GuardLevel.Tidu]: "https://s1.hdslb.com/bfs/static/blive/live-pay-mono/relation/relation/assets/supervisor-u43ElIjU.png",
[GuardLevel.Zongdu]: