koishi-plugin-bilibili-notify
Version:
Bilibili 动态推送、直播通知 Koishi 插件
1,232 lines (1,223 loc) • 52 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let node_path = require("node:path");
let _koishijs_plugin_console = require("@koishijs/plugin-console");
require("@koishijs/plugin-notifier");
let koishi = require("koishi");
let _bilibili_notify_api = require("@bilibili-notify/api");
let node_util = require("node:util");
let _bilibili_notify_internal = require("@bilibili-notify/internal");
let _bilibili_notify_push = require("@bilibili-notify/push");
let _bilibili_notify_storage = require("@bilibili-notify/storage");
let _bilibili_notify_subscription = require("@bilibili-notify/subscription");
let qrcode = require("qrcode");
qrcode = __toESM(qrcode);
//#region src/config.ts
const BilibiliNotifyConfigSchema = koishi.Schema.object({
advancedSub: koishi.Schema.boolean().default(false).description("这个开关决定是否使用高级订阅功能喔~如果主人想要超级灵活的订阅内容,就请开启并安装 bilibili-notify-advanced-subscription 呀 (๑•̀ㅂ•́)و♡"),
subs: koishi.Schema.array(koishi.Schema.object({
name: koishi.Schema.string().required().description("UP昵称"),
uid: koishi.Schema.string().required().description("UID"),
dynamic: koishi.Schema.boolean().default(true).description("动态"),
dynamicAtAll: koishi.Schema.boolean().default(false).description("动态@全体"),
live: koishi.Schema.boolean().default(true).description("直播"),
liveAtAll: koishi.Schema.boolean().default(true).description("直播@全体"),
liveEnd: koishi.Schema.boolean().default(true).description("下播通知"),
liveGuardBuy: koishi.Schema.boolean().default(false).description("上舰消息"),
superchat: koishi.Schema.boolean().default(false).description("SC消息"),
wordcloud: koishi.Schema.boolean().default(true).description("弹幕词云"),
liveSummary: koishi.Schema.boolean().default(true).description("直播总结"),
platform: koishi.Schema.string().required().description("平台名"),
target: koishi.Schema.string().required().description("群号/频道号")
})).role("table").description("在这里填写主人的订阅信息~UP 昵称、UID、roomid、平台、群号都要填正确,不然女仆会迷路哒 (;>_<)如果多个群聊/频道,请用英文逗号分隔哦~女仆会努力送到每一个地方的!"),
logLevel: koishi.Schema.number().min(1).max(3).step(1).default(1).description("这里可以设置日志等级喔~3 是最详细的调试信息,1 是只显示错误信息。主人可以根据需要选择合适的等级,让女仆更好地为您服务 (๑•̀ㅂ•́)و✧"),
userAgent: koishi.Schema.string().description("这里可以设置请求头的 User-Agent 哦~如果请求出现了 -352 的奇怪错误,主人可以试着在这里换一个看看 (;>_<)"),
loginHealthCheckMinutes: koishi.Schema.number().min(5).max(180).step(1).default(30).description("登录状态周期检测的间隔(分钟)。女仆会按这个频率悄悄帮主人确认账号还在线哦~如果发现失效会立刻汇报呢 (๑•̀ㅂ•́)و✧"),
master: koishi.Schema.intersect([koishi.Schema.object({ enable: koishi.Schema.boolean().default(false).description("要不要让笨笨女仆开启主人账号功能呢?(>﹏<)如果机器人遭遇了奇怪的小错误,女仆会立刻跑来向主人报告的!不、不过……如果没有私聊权限的话,女仆就联系不到主人了……请不要打开这个开关喔 (;´д`)ゞ") }).description("主人的特别区域……女仆会乖乖侍奉的!(>///<)"), koishi.Schema.union([koishi.Schema.object({
enable: koishi.Schema.const(true).required(),
platform: koishi.Schema.union([
"qq",
"qqguild",
"onebot",
"discord",
"red",
"telegram",
"satori",
"chronocat",
"lark"
]).description("主人想让女仆在哪个平台伺候您呢?请从这里选一个吧~(〃´-`〃)♡女仆会乖乖待在主人选的地方哒!"),
masterAccount: koishi.Schema.string().role("secret").required().description("请主人把自己的账号告诉女仆嘛……不然女仆会找不到主人哒 (つ﹏⊂)在 Q 群的话用 QQ 号就可以了~其他平台请用 inspect 插件告诉女仆主人的 ID 哦 (´。• ᵕ •。`) ♡"),
masterAccountGuildId: koishi.Schema.string().role("secret").description("如果是在 QQ 频道、Discord 这种地方……主人的群组 ID 也要告诉女仆喔 (;>_<)不然女仆会迷路找不到主人……请用 inspect 插件带女仆去看看嘛~(〃ノωノ)")
}), koishi.Schema.object({})])])
});
//#endregion
//#region src/data-server.ts
var BilibiliNotifyDataServer = class extends _koishijs_plugin_console.DataService {
biliData = {
status: _bilibili_notify_api.BiliLoginStatus.LOADING_LOGIN_INFO,
msg: "正在加载登录信息..."
};
constructor(ctx) {
super(ctx, "bilibili-notify");
ctx.on("bilibili-notify/login-status-report", (data) => {
this.biliData = data;
this.refresh();
});
}
async get() {
return this.biliData;
}
};
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
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);
var index_exports = {};
__export(index_exports, {
Config: () => Config,
apply: () => apply,
name: () => name
});
module.exports = __toCommonJS(index_exports);
var import_koishi = require("koishi");
var zh_CN_default = { commands: { help: {
description: "显示帮助信息",
shortcuts: { help: "帮助" },
options: {
help: "显示此信息",
authority: "显示权限设置",
showHidden: "查看隐藏的选项和指令"
},
messages: {
"not-found": "指令未找到。",
"hint-authority": "括号内为对应的最低权限等级",
"hint-subcommand": "标有星号的表示含有子指令",
"command-title": "指令:{0}",
"command-aliases": "别名:{0}。",
"command-examples": "使用示例:",
"command-authority": "最低权限:{0} 级。",
"subcommand-prolog": "可用的子指令有{0}:",
"global-prolog": "当前可用的指令有{0}:",
"global-epilog": "输入“{0}help 指令名”查看特定指令的语法和使用示例。",
"available-options": "可用的选项有:",
"available-options-with-authority": "可用的选项有(括号内为额外要求的权限等级):"
}
} } };
var en_US_default = { commands: { help: {
description: "Show help",
shortcuts: { help: "<></>" },
options: {
help: "show this message",
authority: "show authority requirements",
showHidden: "show hidden options and commands"
},
messages: {
"not-found": "Command not found.",
"hint-authority": "this minimum authority is marked in parentheses",
"hint-subcommand": "those marked with an asterisk have subcommands",
"command-title": "Command: {0}",
"command-aliases": "Aliases: {0}.",
"command-examples": "Examples:",
"command-authority": "Minimal authority: {0}.",
"subcommand-prolog": "Available subcommands{0}:",
"global-prolog": "Available commands{0}:",
"global-epilog": "Type \"{0}help <command>\" to see syntax and examples for a specific command.",
"available-options": "Available options:",
"available-options-with-authority": "Available options (parentheses indicate additional authority requirement):"
}
} } };
var Config = import_koishi.Schema.object({
shortcut: import_koishi.Schema.boolean().default(true).description("是否启用快捷调用。"),
options: import_koishi.Schema.boolean().default(true).description("是否为每个指令添加 `-h, --help` 选项。")
});
function executeHelp(session, name2) {
if (!session.app.$commander.get("help")) return;
return session.execute({
name: "help",
args: [name2]
});
}
__name(executeHelp, "executeHelp");
var name = "help";
function apply(ctx, config) {
ctx.i18n.define("zh-CN", zh_CN_default);
ctx.i18n.define("en-US", en_US_default);
function enableHelp(command) {
command[import_koishi.Context.current] = ctx;
command.option("help", "-h", {
hidden: true,
notUsage: true,
descPath: "commands.help.options.help"
});
}
__name(enableHelp, "enableHelp");
ctx.schema.extend("command", import_koishi.Schema.object({
hideOptions: import_koishi.Schema.boolean().description("是否隐藏所有选项。").default(false).hidden(),
hidden: import_koishi.Schema.computed(import_koishi.Schema.boolean()).description("在帮助菜单中隐藏指令。").default(false),
params: import_koishi.Schema.any().description("帮助信息的本地化参数。").hidden()
}), 900);
ctx.schema.extend("command-option", import_koishi.Schema.object({
hidden: import_koishi.Schema.computed(import_koishi.Schema.boolean()).description("在帮助菜单中隐藏选项。").default(false),
params: import_koishi.Schema.any().description("帮助信息的本地化参数。").hidden()
}), 900);
if (config.options !== false) {
ctx.$commander._commandList.forEach(enableHelp);
ctx.on("command-added", enableHelp);
}
ctx.before("command/execute", (argv) => {
const { command, options, session } = argv;
if (options["help"] && command._options.help) return executeHelp(session, command.name);
if (command["_actions"].length) return;
return executeHelp(session, command.name);
});
const $ = ctx.$commander;
function findCommand(target, session) {
const command = $.resolve(target, session);
if (command?.ctx.filter(session)) return command;
const data = ctx.i18n.find("commands.(name).shortcuts.(variant)", target).map((item) => ({
...item,
command: $.resolve(item.data.name, session)
})).filter((item) => item.command?.match(session));
const perfect = data.filter((item) => item.similarity === 1);
if (!perfect.length) return data;
return perfect[0].command;
}
__name(findCommand, "findCommand");
const createCollector = /* @__PURE__ */ __name((key) => (argv, fields) => {
const { args: [target], session } = argv;
const result = findCommand(target, session);
if (!Array.isArray(result)) {
session.collect(key, {
...argv,
command: result,
args: [],
options: { help: true }
}, fields);
return;
}
for (const { command } of result) session.collect(key, {
...argv,
command,
args: [],
options: { help: true }
}, fields);
}, "createCollector");
async function inferCommand(target, session) {
const result = findCommand(target, session);
if (!Array.isArray(result)) return result;
const expect = $.available(session).filter((name3) => {
return name3 && session.app.i18n.compare(name3, target);
});
for (const item of result) {
if (expect.includes(item.data.name)) continue;
expect.push(item.data.name);
}
const cache = /* @__PURE__ */ new Map();
const name2 = await session.suggest({
expect,
prefix: session.text(".not-found"),
suffix: session.text("internal.suggest-command"),
filter: /* @__PURE__ */ __name((name3) => {
const command = $.resolve(name3, session);
if (!command) return false;
return ctx.permissions.test(`command:${command.name}`, session, cache);
}, "filter")
});
return $.resolve(name2, session);
}
__name(inferCommand, "inferCommand");
const cmd = ctx.command("help [command:string]", {
authority: 0,
...config
}).userFields(["authority"]).userFields(createCollector("user")).channelFields(createCollector("channel")).option("showHidden", "-H").action(async ({ session, options }, target) => {
if (!target) {
const prefix = session.resolve(session.app.koishi.config.prefix)[0] ?? "";
const output = await formatCommands(".global-prolog", session, $._commandList.filter((cmd2) => cmd2.parent === null), options);
const epilog = session.text(".global-epilog", [prefix]);
if (epilog) output.push(epilog);
return output.filter(Boolean).join("\n");
}
const command = await inferCommand(target, session);
if (!command) return;
if (!await ctx.permissions.test(`command:${command.name}`, session)) return session.text("internal.low-authority");
return showHelp(command, session, options);
});
if (config.shortcut !== false) cmd.shortcut("help", {
i18n: true,
fuzzy: true
});
}
__name(apply, "apply");
function* getCommands(session, commands, showHidden = false) {
for (const command of commands) {
if (!showHidden && session.resolve(command.config.hidden)) continue;
if (command.match(session) && Object.keys(command._aliases).length) yield command;
else yield* getCommands(session, command.children, showHidden);
}
}
__name(getCommands, "getCommands");
async function formatCommands(path, session, children, options) {
const cache = /* @__PURE__ */ new Map();
children = Array.from(getCommands(session, children, options.showHidden));
children = (await Promise.all(children.map(async (command) => {
return [command, await session.app.permissions.test(`command:${command.name}`, session, cache)];
}))).filter(([, result]) => result).map(([command]) => command);
children.sort((a, b) => a.displayName > b.displayName ? 1 : -1);
if (!children.length) return [];
const prefix = session.resolve(session.app.koishi.config.prefix)[0] ?? "";
const output = children.map(({ name: name2, displayName, config }) => {
let output2 = " " + prefix + displayName.replace(/\./g, " ");
output2 += " " + session.text([`commands.${name2}.description`, ""], config.params);
return output2;
});
const hints = [];
const hintText = hints.length ? session.text("general.paren", [hints.join(session.text("general.comma"))]) : "";
output.unshift(session.text(path, [hintText]));
return output;
}
__name(formatCommands, "formatCommands");
function getOptionVisibility(option, session) {
if (session.user && option.authority > session.user.authority) return false;
return !session.resolve(option.hidden);
}
__name(getOptionVisibility, "getOptionVisibility");
function getOptions(command, session, config) {
if (command.config.hideOptions && !config.showHidden) return [];
if (!(config.showHidden ? Object.values(command._options) : Object.values(command._options).filter((option) => getOptionVisibility(option, session))).length) return [];
const output = [];
Object.values(command._options).forEach((option) => {
function pushOption(option2, name2) {
if (!config.showHidden && !getOptionVisibility(option2, session)) return;
let line = `${import_koishi.h.escape(option2.syntax)}`;
const description = session.text(option2.descPath ?? [`commands.${command.name}.options.${name2}`, ""], option2.params);
if (description) line += " " + description;
line = command.ctx.chain("help/option", line, option2, command, session);
output.push(" " + line);
}
__name(pushOption, "pushOption");
if (!("value" in option)) pushOption(option, option.name);
for (const value in option.variants) pushOption(option.variants[value], `${option.name}.${value}`);
});
if (!output.length) return [];
output.unshift(session.text(".available-options"));
return output;
}
__name(getOptions, "getOptions");
async function showHelp(command, session, config) {
const output = [session.text(".command-title", [command.displayName.replace(/\./g, " ") + command.declaration])];
const description = session.text([`commands.${command.name}.description`, ""], command.config.params);
if (description) output.push(description);
if (session.app.database) {
const argv = {
command,
args: [],
options: { help: true }
};
const userFields = session.collect("user", argv);
await session.observeUser(userFields);
if (!session.isDirect) {
const channelFields = session.collect("channel", argv);
await session.observeChannel(channelFields);
}
}
if (Object.keys(command._aliases).length > 1) output.push(session.text(".command-aliases", [Array.from(Object.keys(command._aliases).slice(1)).join(",")]));
session.app.emit(session, "help/command", output, command, session);
if (command._usage) output.push(typeof command._usage === "string" ? command._usage : await command._usage(session));
else {
const text = session.text([`commands.${command.name}.usage`, ""], command.config.params);
if (text) output.push(text);
}
output.push(...getOptions(command, session, config));
if (command._examples.length) output.push(session.text(".command-examples"), ...command._examples.map((example) => " " + example));
else {
const text = session.text([`commands.${command.name}.examples`, ""], command.config.params);
if (text) output.push(session.text(".command-examples"), ...text.split("\n").map((line) => " " + line));
}
output.push(...await formatCommands(".subcommand-prolog", session, command.children, config));
return output.filter(Boolean).join("\n");
}
__name(showHelp, "showHelp");
})))();
function biliCommands() {
const biliCom = this.ctx.command("bili", "bilibili-notify 插件相关指令", { permissions: ["authority:3"] });
biliCom.subcommand(".list", "展示订阅对象").usage("展示订阅对象").example("bili list").action(() => this.subList());
biliCom.subcommand(".private", "向管理员账号发送一条测试消息", { hidden: true }).usage("向管理员账号发送一条测试消息").example("bili private").action(async ({ session }) => {
const internals = this.getInternals(_bilibili_notify_internal.BILIBILI_NOTIFY_TOKEN);
if (!internals) return "插件尚未就绪";
await internals.push.sendPrivateMsg("测试消息");
await session?.send("已发送测试消息。如果未收到,可能是机器人不支持发送私聊消息或配置信息有误");
});
biliCom.subcommand(".ll", "展示当前正在直播的订阅对象").usage("展示当前正在直播的订阅对象").example("bili ll").action(async () => {
const internals = this.getInternals(_bilibili_notify_internal.BILIBILI_NOTIFY_TOKEN);
if (!internals) return "插件尚未就绪";
const subMap = this.subManager;
const liveUsers = (await internals.api.getTheUserWhoIsLiveStreaming())?.data?.live_users?.items ?? [];
const liveUidSet = new Set(liveUsers.map((u) => String(u.mid)));
let table = "";
for (const [uid, sub] of subMap) {
const onLive = sub.live && liveUidSet.has(uid);
table += `[UID:${uid}] 「${sub.uname}」 ${onLive ? "正在直播" : "未开播"}\n`;
}
return table || "没有订阅任何UP";
});
}
//#endregion
//#region src/commands/status.ts
function statusCommands() {
const statusCom = this.ctx.command("status", "插件状态相关指令", { permissions: ["authority:5"] });
statusCom.subcommand(".auth", "查看登录状态").usage("查看登录状态").example("status auth").action(() => {
const snap = this.getAuthSnapshot();
return `登录状态:${_bilibili_notify_api.BiliLoginStatus[snap.status] ?? `unknown(${snap.status})`}\n信息:${snap.msg || "(无)"}`;
});
statusCom.subcommand(".dyn", "查看动态监测运行状态").usage("查看动态监测运行状态").example("status dyn").action(() => {
if (this.ctx.get("bilibili-notify-dynamic")) return "动态监测正在运行";
return "动态插件未运行(请检查是否已安装并启用 koishi-plugin-bilibili-notify-dynamic)";
});
statusCom.subcommand(".live", "查看直播监测运行状态").usage("查看直播监测运行状态").example("status live").action(() => {
if (this.ctx.get("bilibili-notify-live")) return "直播监测正在运行";
return "直播插件未运行(请检查是否已安装并启用 koishi-plugin-bilibili-notify-live)";
});
statusCom.subcommand(".sm", "查看订阅管理对象").usage("查看订阅管理对象").example("status sm").action(() => {
this.ctx.logger.info("[status]", this.subManager);
return "查看控制台";
});
statusCom.subcommand(".bot", "查询当前拥有的机器人信息", { hidden: true }).usage("查询当前拥有的机器人信息").example("status bot").action(() => {
this.ctx.logger.debug("[status] 开始输出BOT信息");
for (const bot of this.ctx.bots) {
this.ctx.logger.debug("[status] --------------------------------");
this.ctx.logger.debug(`[status] 平台:${bot.platform}`);
this.ctx.logger.debug(`[status] 名称:${bot.user?.name}`);
this.ctx.logger.debug("[status] --------------------------------");
}
});
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}`);
});
}
//#endregion
//#region src/commands/sys.ts
function sysCommands() {
const sysCom = this.ctx.command("bn", "bilibili-notify 插件运行相关指令", { permissions: ["authority:5"] });
sysCom.subcommand(".restart", "重启插件(重新加载订阅并通知 live/dynamic 插件)").usage("重启插件").example("bn restart").action(async () => {
if (await this.restartPlugin()) return "主人~女仆成功重启插件啦~乖乖继续为主人服务呢 (>ω<)♡";
return "主人呜呜 (;>_<) 女仆重启插件失败啦~请主人检查一下再试哦 (>ω<)♡";
});
sysCom.subcommand(".stop", "停止插件").usage("停止插件").example("bn stop").action(() => {
if (this.disposePlugin()) return "主人~女仆已经停止插件啦~休息一下先 (>ω<)♡";
return "主人呜呜 (;>_<) 女仆停止插件失败啦~请主人检查一下再试哦 (>ω<)♡";
});
sysCom.subcommand(".start", "启动插件").usage("启动插件").example("bn start").action(async () => {
if (await this.registerPlugin()) return "主人~女仆成功启动插件啦~准备好乖乖为主人工作呢 (>ω<)♡";
return "主人呜呜 (;>_<) 女仆启动插件失败啦~请主人检查一下再试哦 (>ω<)♡";
});
}
//#endregion
//#region src/login-status.ts
const MESSAGES = {
loading: "正在加载登录信息...",
notLogin: "账号未登录,请点击「扫码登录」",
keyReset: "密钥已重置,cookie 已清除,请重新扫码登录",
authLost: "账号登录已失效,请在控制台重新扫码登录",
loggedIn: "已登录",
loginJustSucceeded: "登录成功,正在加载订阅...",
fetchAccountFailed: "账号已登录,但获取个人信息失败,请检查",
waitScan: "尚未扫码,请扫码",
waitConfirm: "已扫码,但尚未确认,请确认",
qrFetchFailed: "获取二维码失败,请重试",
qrRenderFailed: "生成二维码失败",
qrExpired: "二维码已超时(3分钟),请重新登录",
qrInvalidated: "二维码已失效,请重新登录",
noCookieAfterLogin: "登录成功但未获取到 cookie,请重试",
genericLoginFail: "登录失败,请重试"
};
/** 类型守卫:snapshot.data 是否长得像 UserCardInfo。 */
function looksLikeCardData(data) {
return typeof data === "object" && data !== null && "card" in data;
}
/**
* 集中管理登录态:所有变更都经过这里,再以 `bilibili-notify/login-status-report`
* 推到前端。心跳定时器在登录态下定期 probe,发现失效会同时广播
* `bilibili-notify/auth-lost`,恢复时广播 `bilibili-notify/auth-restored`。
*/
var LoginStatusController = class {
snapshot = {
status: _bilibili_notify_api.BiliLoginStatus.LOADING_LOGIN_INFO,
msg: MESSAGES.loading
};
healthTimer;
/**
* 标记"曾从已登录态掉线",下一次成功登录时翻转为已登录后才发 auth-restored。
* 之所以不再用"上一帧 status === NOT_LOGIN"判断:失效后用户走扫码流程,会经过
* LOGIN_QR / LOGGING_QR 等中间态,直接基于上一帧会漏掉这条恢复路径。
*/
needsRestore = false;
constructor(ctx, options) {
this.ctx = ctx;
this.options = options;
}
current() {
return { ...this.snapshot };
}
attachHealthCheck() {
this.detachHealthCheck();
if (this.options.healthCheckMs <= 0) return;
this.healthTimer = this.ctx.setInterval(() => void this.runHealthCheck(), this.options.healthCheckMs);
}
detachHealthCheck() {
this.healthTimer?.();
this.healthTimer = void 0;
}
reportLoggedIn(card, reasonKey = "loggedIn") {
const wasLoggedIn = this.snapshot.status === _bilibili_notify_api.BiliLoginStatus.LOGGED_IN;
const fallback = looksLikeCardData(this.snapshot.data) ? this.snapshot.data : void 0;
this.transition({
status: _bilibili_notify_api.BiliLoginStatus.LOGGED_IN,
msg: MESSAGES[reasonKey],
data: card ?? fallback
});
if (!wasLoggedIn && this.needsRestore) {
this.needsRestore = false;
this.ctx.emit("bilibili-notify/auth-restored");
}
}
reportLoggedOut(reasonKey = "notLogin") {
const wasLoggedIn = this.snapshot.status === _bilibili_notify_api.BiliLoginStatus.LOGGED_IN;
this.transition({
status: _bilibili_notify_api.BiliLoginStatus.NOT_LOGIN,
msg: MESSAGES[reasonKey]
});
if (wasLoggedIn) {
this.needsRestore = true;
this.ctx.emit("bilibili-notify/auth-lost");
}
}
/** Dispatch on `getMyselfInfo` result code. */
reportLoginCheck(code, card) {
if (code === 0) this.reportLoggedIn(card);
else if (code === -101) this.reportLoggedOut("authLost");
else this.reportTransientFailure(`code=${code}`);
}
/** Keep current status, only refresh msg. Logs at warn level. */
reportTransientFailure(detail) {
this.options.logger.warn(`[auth] 瞬时失败:${detail}`);
if (this.snapshot.status !== _bilibili_notify_api.BiliLoginStatus.LOGGED_IN) return;
this.transition({
...this.snapshot,
msg: MESSAGES.fetchAccountFailed
});
}
reportQrReady(base64) {
this.transition({
status: _bilibili_notify_api.BiliLoginStatus.LOGIN_QR,
msg: "",
data: base64
});
}
reportQrPending(reasonKey) {
this.transition({
status: _bilibili_notify_api.BiliLoginStatus.LOGGING_QR,
msg: MESSAGES[reasonKey]
});
}
reportQrFailure(reasonKey) {
this.transition({
status: _bilibili_notify_api.BiliLoginStatus.LOGIN_FAILED,
msg: MESSAGES[reasonKey]
});
}
/** Emit only when (status, msg, data) changes. */
transition(next) {
if (this.snapshot.status === next.status && this.snapshot.msg === next.msg && this.snapshot.data === next.data) return;
this.snapshot = next;
this.ctx.emit("bilibili-notify/login-status-report", next);
}
async runHealthCheck() {
if (this.snapshot.status === _bilibili_notify_api.BiliLoginStatus.LOGIN_QR || this.snapshot.status === _bilibili_notify_api.BiliLoginStatus.LOGGING_QR || this.snapshot.status === _bilibili_notify_api.BiliLoginStatus.NOT_LOGIN) return;
try {
const res = await this.options.probe();
this.reportLoginCheck(res.code);
} catch (e) {
this.options.logger.warn(`[auth] 心跳异常(保持当前状态):${e}`);
}
}
};
//#endregion
//#region src/server-manager.ts
const SERVICE_NAME = "bilibili-notify";
const LIVE_MASTER_KEYS = [
"live",
"liveAtAll",
"liveEnd",
"liveGuardBuy",
"superchat",
"wordcloud",
"liveSummary"
];
const LIVE_CUSTOM_KEYS = [
"customCardStyle",
"customLiveMsg",
"customGuardBuy",
"customLiveSummary",
"customSpecialDanmakuUsers",
"customSpecialUsersEnterTheRoom",
"specialUsers"
];
/** Diff two SubItem snapshots and return a typed SubChange array. */
function diffSubItems(prev, next) {
const result = [];
const liveChange = { scope: "live" };
for (const key of LIVE_MASTER_KEYS) if (prev[key] !== next[key]) liveChange[key] = next[key];
if (prev.uname !== next.uname) liveChange.uname = next.uname;
if (prev.roomId !== next.roomId) liveChange.roomId = next.roomId;
for (const key of LIVE_CUSTOM_KEYS) if (!(0, node_util.isDeepStrictEqual)(prev[key], next[key])) liveChange[key] = next[key];
if (Object.keys(liveChange).length > 1) result.push(liveChange);
const dynamicChange = { scope: "dynamic" };
if (prev.dynamic !== next.dynamic) dynamicChange.dynamic = next.dynamic;
if (prev.dynamicAtAll !== next.dynamicAtAll) dynamicChange.dynamicAtAll = next.dynamicAtAll;
if (Object.keys(dynamicChange).length > 1) result.push(dynamicChange);
if (!(0, node_util.isDeepStrictEqual)(prev.target, next.target)) result.push({
scope: "target",
target: next.target
});
return result;
}
var BilibiliNotifyServerManager = class extends koishi.Service {
static [koishi.Service.provide] = SERVICE_NAME;
serverLogger = this.ctx.logger(SERVICE_NAME);
selfCtx;
api = null;
push = null;
subMgr = null;
loginTimer;
subNotifier;
running = false;
storageMgr;
currentSubs = null;
auth;
authLostNotifiedAt = 0;
constructor(ctx, config) {
super(ctx, SERVICE_NAME);
this.selfCtx = ctx;
this.config = config;
this.serverLogger.level = config.logLevel;
}
/** For commands */
get subManager() {
return this.subMgr?.subManager ?? /* @__PURE__ */ new Map();
}
/** For commands: read the current login snapshot. */
getAuthSnapshot() {
return this.auth.current();
}
subList() {
const map = this.subManager;
if (!map.size) return "没有订阅任何UP";
let table = "";
for (const [uid, sub] of map) {
const flags = [sub.dynamic ? "已订阅动态" : "", sub.live ? "已订阅直播" : ""].filter(Boolean).join(" ");
table += `[UID:${uid}] 「${sub.uname}」 ${flags}\n`;
}
return table.trim();
}
async start() {
this.serverLogger.info("[start] 正在启动中...");
this.storageMgr = new _bilibili_notify_storage.StorageManager(this.ctx.baseDir, this.ctx);
await this.storageMgr.init();
this.auth = new LoginStatusController(this.selfCtx, {
healthCheckMs: this.config.loginHealthCheckMinutes * 6e4,
logger: this.serverLogger,
probe: () => {
if (!this.api) throw new Error("api not initialized");
return this.api.getMyselfInfo();
}
});
this.ctx.on("bilibili-notify/cookies-refreshed", async (data) => {
try {
await this.storageMgr.cookieStore.save(data);
this.serverLogger.debug("[cookie] Cookie 已自动刷新并保存");
} catch (e) {
this.serverLogger.error(`[cookie] 保存刷新后的 cookie 失败:${e}`);
}
});
this.ctx.on("bilibili-notify/plugin-error", (source, message) => {
this.serverLogger.warn(`[${source}] ${message}`);
});
sysCommands.call(this);
if (!await this.registerPlugin()) this.serverLogger.error("[module] 启动模块失败,请检查配置后重试");
}
stop() {
this.disposePlugin();
}
/**
* 向持有 BILIBILI_NOTIFY_TOKEN 的友好插件暴露 api / push / subs 实例。
* 第三方插件无法获取此令牌,因此无法访问内部实例。
*/
getInternals(token) {
if (token !== _bilibili_notify_internal.BILIBILI_NOTIFY_TOKEN || !this.api || !this.push) return null;
return {
api: this.api,
push: this.push,
subs: this.currentSubs,
addSub: (p) => this.addSub(p),
removeSub: (uid) => this.removeSub(uid),
updateSub: (p) => this.updateSub(p)
};
}
async addSub(params) {
if (this.config.advancedSub) return "订阅失败:高级订阅模式下不支持通过 AI 管理订阅,操作未执行";
if (!this.subMgr) return "订阅失败:插件未就绪,操作未执行";
const existing = this.config.subs?.find((s) => s.uid.split(",")[0].trim() === params.uid);
if (existing) return `订阅失败:UID ${params.uid} 已在订阅列表中(昵称:${existing.name})`;
const item = {
name: params.name,
uid: params.uid,
dynamic: params.dynamic ?? true,
dynamicAtAll: params.dynamicAtAll ?? false,
live: params.live ?? true,
liveAtAll: params.liveAtAll ?? false,
liveEnd: params.liveEnd ?? true,
liveGuardBuy: params.liveGuardBuy ?? false,
superchat: params.superchat ?? false,
wordcloud: params.wordcloud ?? true,
liveSummary: params.liveSummary ?? true,
platform: params.platform,
target: params.target
};
const addedSub = await this.subMgr.addEntry(item);
if (!addedSub) return `订阅失败:${params.name}(UID: ${params.uid})操作未执行,请查看日志`;
const newConfig = {
...this.config,
subs: [...this.config.subs ?? [], item]
};
this.config = newConfig;
this.selfCtx.emit("bilibili-notify/update-config", newConfig);
this.syncCurrentSubs();
this.updateSubNotifier();
this.selfCtx.emit("bilibili-notify/subscription-changed", [{
type: "add",
sub: addedSub
}]);
this.serverLogger.info(`[subscribe] 已添加订阅:${params.name}(UID: ${params.uid})`);
return `已成功订阅 ${params.name}(UID: ${params.uid})`;
}
async updateSub(params) {
if (this.config.advancedSub) return "更新订阅失败:高级订阅模式下不支持通过 AI 管理订阅,操作未执行";
if (!this.subMgr) return "更新订阅失败:插件未就绪,操作未执行";
const flatSubs = this.config.subs ?? [];
const idx = flatSubs.findIndex((s) => s.uid.split(",")[0].trim() === params.uid);
if (idx === -1) return `更新订阅失败:未找到 UID 为 ${params.uid} 的订阅,操作未执行`;
const existing = flatSubs[idx];
const rawPrev = this.subMgr.subManager.get(params.uid);
const prevSub = rawPrev ? structuredClone(rawPrev) : null;
const updatedItem = {
...existing,
...params.dynamic !== void 0 && { dynamic: params.dynamic },
...params.dynamicAtAll !== void 0 && { dynamicAtAll: params.dynamicAtAll },
...params.live !== void 0 && { live: params.live },
...params.liveAtAll !== void 0 && { liveAtAll: params.liveAtAll },
...params.liveEnd !== void 0 && { liveEnd: params.liveEnd },
...params.liveGuardBuy !== void 0 && { liveGuardBuy: params.liveGuardBuy },
...params.superchat !== void 0 && { superchat: params.superchat },
...params.wordcloud !== void 0 && { wordcloud: params.wordcloud },
...params.liveSummary !== void 0 && { liveSummary: params.liveSummary }
};
const nextSub = this.subMgr.updateEntry(updatedItem);
if (!nextSub) return `更新订阅失败:UID ${params.uid} 不在运行中的订阅管理器内,操作未执行`;
const newFlatSubs = [...flatSubs];
newFlatSubs[idx] = updatedItem;
const newConfig = {
...this.config,
subs: newFlatSubs
};
this.config = newConfig;
this.selfCtx.emit("bilibili-notify/update-config", newConfig);
this.syncCurrentSubs();
this.updateSubNotifier();
if (prevSub) {
const changes = diffSubItems(prevSub, nextSub);
if (changes.length) this.selfCtx.emit("bilibili-notify/subscription-changed", [{
type: "update",
uid: params.uid,
changes
}]);
}
this.serverLogger.info(`[update] 已更新订阅:${existing.name}(UID: ${params.uid})`);
return `已成功更新 ${existing.name}(UID: ${params.uid})的订阅设置`;
}
removeSub(uid) {
if (this.config.advancedSub) return "取消订阅失败:高级订阅模式下不支持通过 AI 管理订阅,操作未执行";
if (!this.subMgr) return "取消订阅失败:插件未就绪,操作未执行";
const flatItem = this.config.subs?.find((s) => s.uid.split(",")[0].trim() === uid);
if (!flatItem) return `取消订阅失败:未找到 UID 为 ${uid} 的订阅,操作未执行`;
const removedSub = this.subMgr.removeEntry(uid);
if (!removedSub) return `取消订阅失败:UID ${uid} 不在运行中的订阅管理器内,操作未执行`;
const newConfig = {
...this.config,
subs: (this.config.subs ?? []).filter((s) => s !== flatItem)
};
this.config = newConfig;
this.selfCtx.emit("bilibili-notify/update-config", newConfig);
this.syncCurrentSubs();
this.updateSubNotifier();
this.selfCtx.emit("bilibili-notify/subscription-changed", [{
type: "delete",
uid
}]);
this.serverLogger.info(`[unsubscribe] 已移除订阅:${removedSub.uname}(UID: ${uid})`);
return `已成功取消订阅 ${removedSub.uname}(UID: ${uid})`;
}
/** Rebuild currentSubs from the subManager (uid-keyed). */
syncCurrentSubs() {
if (!this.subMgr?.subManager.size) {
this.currentSubs = null;
return;
}
const result = {};
for (const [uid, sub] of this.subMgr.subManager) result[uid] = sub;
this.currentSubs = result;
}
/** Compute ops by diffing two subManager snapshots. Used for advanced-sub full reload. */
diffSubManagers(prev, next) {
const ops = [];
for (const [uid] of prev) if (!next.has(uid)) ops.push({
type: "delete",
uid
});
for (const [uid, sub] of next) {
const prevSub = prev.get(uid);
if (!prevSub) ops.push({
type: "add",
sub
});
else {
const changes = diffSubItems(prevSub, sub);
if (changes.length) ops.push({
type: "update",
uid,
changes
});
}
}
return ops;
}
async registerPlugin() {
if (this.running) return false;
try {
this.api = new _bilibili_notify_api.BilibiliAPI(this.selfCtx, {
logLevel: this.config.logLevel,
userAgent: this.config.userAgent
}, {
onCookiesRefreshed: (data) => {
this.selfCtx.emit("bilibili-notify/cookies-refreshed", data);
},
onAuthLost: () => {
this.handleAuthLost();
}
});
this.push = new _bilibili_notify_push.BilibiliPush(this.selfCtx, {
logLevel: this.config.logLevel,
master: this.config.master
});
await this.api.start();
this.serverLogger.debug("[module] BilibiliAPI 启动完成");
this.push.start();
this.serverLogger.debug("[module] BilibiliPush 启动完成");
this.subMgr = new _bilibili_notify_subscription.SubscriptionManager(this.api, this.push, this.selfCtx);
this.running = true;
this.registerConsoleEvents();
biliCommands.call(this);
statusCommands.call(this);
await this.initCookies();
this.serverLogger.debug(`[cookie] Cookie 加载完成,登录状态:${this.isLoggedIn() ? "已登录" : "未登录"}`);
if (!this.isLoggedIn()) {
this.serverLogger.info("[login] 账号未登录,请在控制台扫码登录");
this.auth.reportLoggedOut("notLogin");
return true;
}
await this.reportAccountInfo();
await this.loadInitialSubscriptions();
} catch (e) {
this.serverLogger.error(`[module] 注册模块失败:${e}`);
return false;
}
return true;
}
disposePlugin() {
if (!this.running && !this.api && !this.push) return false;
this.serverLogger.debug("[stop] 正在清理插件资源...");
this.running = false;
this.clearLoginTimer();
this.auth?.detachHealthCheck();
if (this.subNotifier) {
this.subNotifier.dispose();
this.subNotifier = void 0;
}
this.push?.stop();
this.api?.stop();
this.push = null;
this.api = null;
this.subMgr = null;
this.currentSubs = null;
this.serverLogger.debug("[stop] 插件资源清理完成");
return true;
}
async restartPlugin() {
if (!this.running) {
this.serverLogger.warn("[restart] 插件目前没有运行,请使用 bn start 启动插件");
return false;
}
this.disposePlugin();
return new Promise((resolve) => {
this.selfCtx.setTimeout(() => {
this.registerPlugin().then(resolve).catch((e) => {
this.serverLogger.error(`[restart] 重启插件失败:${e}`);
resolve(false);
});
}, 1e3);
});
}
async initCookies() {
if (!this.api) return;
this.serverLogger.debug("[cookie] 正在从磁盘加载 Cookie...");
let cookieData = null;
try {
cookieData = await this.storageMgr.cookieStore.load();
} catch (e) {
this.serverLogger.warn(`[cookie] 读取 cookie 文件失败: ${e}`);
}
if (cookieData) {
this.serverLogger.debug("[cookie] 找到 Cookie 文件,正在写入 jar...");
await this.api.loadCookies(cookieData);
} else {
this.serverLogger.debug("[cookie] 未找到 Cookie 文件,标记为待登录状态");
this.api.markLoginInfoLoaded();
}
}
isLoggedIn() {
const cookiesJson = this.api?.getCookiesJson();
if (!cookiesJson || cookiesJson === "[]") return false;
try {
return JSON.parse(cookiesJson).some((c) => c.key === "bili_jct");
} catch {
return false;
}
}
clearLoginTimer() {
if (this.loginTimer) {
this.loginTimer();
this.loginTimer = void 0;
}
}
async reportAccountInfo() {
if (!this.api) return;
let personalInfo;
try {
personalInfo = await this.api.getMyselfInfo();
} catch (e) {
this.serverLogger.warn(`[account] 获取个人信息异常: ${e}`);
this.auth.reportTransientFailure(e);
this.auth.attachHealthCheck();
return;
}
if (personalInfo.code !== 0) {
this.auth.reportLoginCheck(personalInfo.code);
if (personalInfo.code !== -101) this.auth.attachHealthCheck();
return;
}
let card;
try {
card = (await this.api.getUserCardInfo(personalInfo.data.mid.toString(), true)).data;
} catch (e) {
this.serverLogger.warn(`[account] 获取用户卡片失败: ${e}`);
}
this.auth.reportLoggedIn(card);
this.auth.attachHealthCheck();
}
async handleAuthLost() {
this.auth.reportLoggedOut("authLost");
const now = Date.now();
if (now - this.authLostNotifiedAt < 6e4) return;
this.authLostNotifiedAt = now;
try {
await this.push?.sendPrivateMsg("账号登录已失效,请在控制台重新扫码登录");
} catch (e) {
this.serverLogger.warn(`[auth] 失效通知私信失败:${e}`);
}
}
async loadInitialSubscriptions() {
if (this.config.advancedSub) {
this.serverLogger.info("[sub] 开启高级订阅,等待接收订阅配置...");
this.selfCtx.emit("bilibili-notify/ready-to-receive");
} else if (this.config.subs?.length) {
this.serverLogger.debug(`[sub] 从配置加载 ${this.config.subs.length} 个订阅项`);
const subs = _bilibili_notify_subscription.SubscriptionManager.fromFlatConfig(this.config.subs);
if (!this.subMgr) return;
await this.subMgr.loadSubscriptions(subs, { isReload: false });
this.syncCurrentSubs();
this.updateSubNotifier();
const ops = [...this.subMgr.subManager.values()].map((sub) => ({
type: "add",
sub
}));
if (ops.length) this.selfCtx.emit("bilibili-notify/subscription-changed", ops);
} else this.serverLogger.info("[sub] 初始化完毕,但未添加任何订阅");
}
updateSubNotifier() {
if (!this.subMgr) return;
if (this.subNotifier) this.subNotifier.dispose();
const subInfo = this.subList();
if (subInfo === "没有订阅任何UP") this.subNotifier = this.selfCtx.notifier.create(subInfo);
else {
const lines = subInfo.split("\n").filter(Boolean);
const content = (0, koishi.h)(koishi.h.Fragment, [(0, koishi.h)("p", "当前订阅对象:"), (0, koishi.h)("ul", lines.map((str) => (0, koishi.h)("li", str)))]);
this.subNotifier = this.selfCtx.notifier.create(content);
}
}
registerConsoleEvents() {
this.selfCtx.on("bilibili-notify/subscription-changed", async (_ops) => {
await this.selfCtx.sleep(5e3);
if (this.currentSubs) await this.warnMissingPlugins(this.currentSubs);
});
this.selfCtx.console.addListener("bilibili-notify/start-login", async () => {
this.serverLogger.info("[login] 触发登录事件");
await this.startLoginFlow();
});
this.selfCtx.console.addListener("bilibili-notify/reset-key", async () => {
this.serverLogger.info("[login] 触发重置密钥事件");
try {
await this.storageMgr.cookieStore.resetKey();
this.auth.reportLoggedOut("keyReset");
} catch (e) {
this.serverLogger.error(`[login] 重置密钥失败:${e}`);
}
});
this.selfCtx.console.addListener("bilibili-notify/request-cors", async (url) => {
let parsed;
try {
parsed = new URL(url);
} catch {
throw new Error("无效的 URL");
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") throw new Error("仅支持 http/https 协议");
const host = parsed.hostname.toLowerCase();
if (!(host === "bilibili.com" || host === "hdslb.com" || host.endsWith(".bilibili.com") || host.endsWith(".hdslb.com"))) throw new Error("仅允许 bilibili.com / hdslb.com 域名");
const buffer = await (await fetch(url)).arrayBuffer();
return `data:image/png;base64,${Buffer.from(buffer).toString("base64")}`;
});
if (this.config.advancedSub) this.selfCtx.on("bilibili-notify/advanced-sub", async (subs) => {
if (!Object.keys(subs).length) {
this.serverLogger.info("[sub] 订阅加载完毕,但未添加任何订阅");
return;
}
if (!this.subMgr) return;
const prevSubManager = new Map(this.subMgr.subManager);
await this.subMgr.loadSubscriptions(subs, { isReload: prevSubManager.size > 0 });
this.syncCurrentSubs();
this.updateSubNotifier();
const ops = this.diffSubManagers(prevSubManager, this.subMgr.subManager);
if (ops.length) this.selfCtx.emit("bilibili-notify/subscription-changed", ops);
});
}
async startLoginFlow() {
if (!this.api) return;
let qrContent;
try {
qrContent = await this.api.getLoginQRCode();
} catch (e) {
this.serverLogger.error(`[login] 获取登录二维码失败:${e}`);
return;
}
if (qrContent.code !== 0) {
this.auth.reportQrFailure("qrFetchFailed");
return;
}
qrcode.default.toBuffer(qrContent.data.url, {
errorCorrectionLevel: "H",
type: "png",
margin: 1,
color: {
dark: "#000000",
light: "#FFFFFF"
}
}, (err, buffer) => {
if (err) {
this.serverLogger.error(`[login] 生成二维码失败:${err}`);
this.auth.reportQrFailure("qrRenderFailed");
return;
}
this.auth.reportQrReady(`data:image/png;base64,${Buffer.from(buffer).toString("base64")}`);
});
this.clearLoginTimer();
let polling = true;
this.loginTimer = this.selfCtx.setInterval(async () => {
if (!polling) return;
polling = false;
try {
await this.pollLoginStatus(qrContent.data.qrcode_key);
} finally {
polling = true;
}
}, 1e3);
this.selfCtx.setTimeout(() => {
if (!this.loginTimer) return;
this.clearLoginTimer();
this.auth.reportQrFailure("qrExpired");
}, 180 * 1e3);
}
async pollLoginStatus(qrcodeKey) {
if (!this.api) return;
let loginContent;
try {
loginContent = await this.api.getLoginStatus(qrcodeKey);
} catch (e) {
this.serverLogger.error(`[login] 获取登录状态失败:${e}`);
return;
}
const code = loginContent?.data?.code;
if (code === 86101) {
this.auth.reportQrPending("waitScan");
return;
}
if (code === 86090) {
this.auth.reportQrPending("waitConfirm");
return;
}
if (code === 86038) {
this.clearLoginTimer();
this.auth.reportQrFailure("qrInvalidated");
return;
}
if (code === 0) {
this.clearLoginTimer();
const cookiesJson = this.api.getCookiesJson();
if (!cookiesJson || cookiesJson === "[]") {
this.serverLogger.error("[login] 登录成功但未获取到任何 cookie,放弃保存");
this.auth.reportQrFailure("noCookieAfterLogin");
return;
}
try {
const refreshToken = loginContent.data.refresh_token ?? "";
await this.storageMgr.cookieStore.save({
cookiesJson,
refreshToken
});
} catch (e) {
this.serverLogger.error(`[login] 保存 cookie 失败:${e}`);
}
this.auth.reportLoggedIn(void 0, "loginJustSucceeded");
await this.reportAccountInfo();
await this.loadInitialSubscriptions();
return;
}
if (loginContent?.code !== 0) {
this.clearLoginTimer();
this.auth.reportQrFailure("genericLoginFail");
}
}
async warnMissingPlugins(subs) {
if (!this.push) return;
const needDynamic = Object.values(subs).some((s) => s.dynamic);
const needLive = Object.values(subs).some((s) => s.live);
if (needDynamic && !this.selfCtx.get("bilibili-notify-dynamic")) {
const msg = "[bilibili-notify] 警告:有订阅开启了动态通知,但动态插件(koishi-plugin-bilibili-notify-dynamic)未运行,请检查是否已安装并启用该插件。";
this.serverLogger.warn(`[warn] ${msg}`);
await this.push.sendPrivateMsg(msg);
}
if (needLive && !this.selfCtx.get("bilibili-notify-live")) {
const msg = "[bilibili-notify] 警告:有订阅开启了直播通知,但直播插件(koishi-plugin-bilibili-notify-live)未运行,请检查是否已安装并启用该插件。";
this.serverLogger.warn(`[warn] ${msg}`);
await this.push.sendPrivateMsg(msg);
}
}
};
//#endregion
//#region src/index.ts
const inject = {
required: ["notifier", "console"],
optional: ["bilibili-notify-dynamic", "bilibili-notify-live"]
};
const name = "bilibili-notify";
const usage = `
<h1>Bilibili-Notify</h1>
<p>使用问题请加群咨询 801338523</p>
---
主人好呀~我是笨笨女仆小助手哒 (〃∀〃)♡
专门帮主人管理 B 站订阅和直播推送的!
女仆虽然笨笨的,但是会尽力不出错哦~
主人,只要按照女仆的提示一步一步设置,女仆就可以乖乖帮您工作啦!
首先呢~请主人仔细阅读订阅相关的 subs 的填写说明 (>ω<)b
【主人账号部分非必填】然后再告诉女仆您的 主人账号 (///▽///),并选择您希望女仆服务的平台~
接着,请认真填写 主人的 ID 和 群组 ID,确保信息完全正确~
这样女仆才能顺利找到您并准确汇报动态呢 (≧▽≦)
不用着急,女仆会一直在这里陪着您,一步一步完成设置~
主人只要乖乖填好这些信息,就能让女仆变得超级听话、超级勤快啦 (>///<)♡
想要重新登录的话,只需要点击控制台左侧的「扫码登录」哦~
主人~注意事项要仔细看呀 (>_<)♡
- 如果主人使用的是 onebot 机器人,平台名请填写 onebot,而不是 qq 哦~
- 如果需要更灵活的订阅配置,请安装 bilibili-notify-advanced-subscription 插件
乖乖遵守这些规则,女仆才能顺利帮主人工作呢 (*>ω<)b
---
`;
function apply(ctx, config) {
ctx.plugin(BilibiliNotifyDataServer);
ctx.plugin(BilibiliNotifyServerManager, config);
ctx.on("bilibili-notify/update-config", (newConfig) => {
ctx.scope.update(newConfig, false);
});
ctx.console.addEntry({
dev: (0, node_path.resolve)(__dirname, "../client/index.ts"),
prod: (0, node_path.resolve)(__dirname, "../dist")
});
}
const Config = BilibiliNotifyConfigSchema;
//#endregion
exports.BilibiliNotifyConfigSchema = BilibiliNotifyConfigSchema;
exports.Config = Config;
exports.apply = apply;
exports.inject = inject;
exports.name = name;
exports.usage = usage;