From ed7d15f5adcdaf343d011c8a352f4ba2d73882b6 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 3 Jul 2026 13:53:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Bilibili=E5=8D=A1=E7=89=87=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=E9=A6=96=E8=A1=8C=E5=B0=81=E9=9D=A2=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 2 +- README.md | 4 +-- .../src/domain/bilibili-reply-formatter.ts | 30 +++++++++++++++++-- .../bilibili-video-client.spec.ts | 1 + 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/API.md b/API.md index d3dd9fc..5fd670b 100644 --- a/API.md +++ b/API.md @@ -535,7 +535,7 @@ Admin 入口为 `/qqbot/plugin-task`,用于分页查看任务、启停、修 | ------------------------ | -------- | -------------------------------------------------------------------- | | `bilibili-card.message` | message | 从 QQ/NapCat `share/json/xml/lightapp` 卡片和文本中提取 Bilibili 链接 | -插件会解析 `www.bilibili.com`、`m.bilibili.com` 和 `b23.tv`。短链通过插件平台受控 `resolveRedirect` host 能力限制跳转次数和超时;视频信息来自 Bilibili `x/web-interface/view`,回复为纯文本标题、UP 主、时长、播放/弹幕/点赞等摘要和标准视频链接。同一账号、同一会话、同一视频在 `QQBOT_BILIBILI_CARD_DEDUPE_TTL_MS` 内去重。 +插件会解析 `www.bilibili.com`、`m.bilibili.com` 和 `b23.tv`。短链通过插件平台受控 `resolveRedirect` host 能力限制跳转次数和超时;视频信息来自 Bilibili `x/web-interface/view`,回复首行使用视频封面 CQ image,随后输出标题、UP 主、时长、播放/弹幕/点赞等文本摘要和标准视频链接。同一账号、同一会话、同一视频在 `QQBOT_BILIBILI_CARD_DEDUPE_TTL_MS` 内去重。 可配置键: diff --git a/README.md b/README.md index e0b30aa..f702a70 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ | `qqbot` | QQBot 账号、NapCat 扫码登录、运行态 Profile、OneBot 反向 WS、在线命令、规则、权限、发送/接收日志和插件平台 | | `modules/qqbot/plugin-platform` | QQBot 插件 manifest 校验、版本安装、运行事件、定时任务、受控 SDK 和 CLI 脚手架 | | `qqbot/plugins/bangdream` | BanG Dream 查曲、查卡、查活动、试炼、玩家、卡池、抽卡模拟、档线、谱面出图 | -| `qqbot/plugins/bilibili-card` | 解析 QQ/NapCat Bilibili 卡片和短链,按账号事件绑定回复视频文字摘要 | +| `qqbot/plugins/bilibili-card` | 解析 QQ/NapCat Bilibili 卡片和短链,按账号事件绑定回复封面图和视频文字摘要 | | `qqbot/plugins/ff14-market` | XIVAPI + Universalis 物品解析和 FF14 市场查价 | | `qqbot/plugins/fflogs` | FFLogs v2 GraphQL 角色排名和指定高难最近记录查询 | | `minio` | Bucket 检查、上传、列表、临时 URL、代理下载、删除 | @@ -176,7 +176,7 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow - QQBot 在线命令和自动回复规则都有运行时保底冷却:默认命令 `5000ms`、规则 `30000ms`;即使数据库里旧数据冷却值更低,也按保底值判定,降低频繁触发风控的概率。 - QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。 - QQBot 插件平台统一使用 `plugin.json` manifest 描述插件 key、版本、操作、事件、权限、运行预算和包入口;CLI 负责 create/validate/pack/install-local,后端只暴露受控 SDK 能力并通过插件维度记录安装、配置、账号绑定和运行事件。 -- Bilibili Card 是事件型内置插件:`bilibili-card.message` 只在账号绑定后监听 QQ/NapCat `share/json/xml/lightapp` 卡片或文本里的 Bilibili 链接,`b23.tv` 短链通过平台 `resolveRedirect` 受控 host 能力解析,视频信息从 Bilibili `x/web-interface/view` 获取后回复纯文本摘要。 +- Bilibili Card 是事件型内置插件:`bilibili-card.message` 只在账号绑定后监听 QQ/NapCat `share/json/xml/lightapp` 卡片或文本里的 Bilibili 链接,`b23.tv` 短链通过平台 `resolveRedirect` 受控 host 能力解析,视频信息从 Bilibili `x/web-interface/view` 获取后回复首行封面图和文本摘要。 - QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息,WebUI 配置或请求错误留在 `lastError`。 - NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。`desktop-cn-v20` 镜像从 KT `NapCatQQ` fork 的 source-built `NapCat.Shell` 构建,不再在镜像内对上游 bundle 做字符串 patch,并修复非自动重试 QR failure 后下次 WebUI 登录动作不重置、QQCore 通过进程级 mountinfo 探针看到 Docker/宿主路径、扫码登录成功后 API 立即读不到 QQ 号、生产 native reset 缺少 `offline()` 时半登录态无法清理、runtime view native maps 取证假阴性、WebUI `RestartNapCat` 重启 worker 丢失快速登录账号参数,以及首次解包覆盖 API 预写 NapCat config 导致 bypass 开关回落默认关闭的问题。踢下线后的半登录态不能只靠旧 native reset 兜底;源 Docker 容器在线时 API 会先同容器 `RestartNapCat` 重建 NapCat worker,再继续登录流程,同一个更新登录 session 不能反复重启 worker。 - NapCat 账号新增/编辑支持可选 QQ 登录密码:Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。 diff --git a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts index 25877f9..0156fa1 100644 --- a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts +++ b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter.ts @@ -4,16 +4,18 @@ import type { } from './bilibili-card.types'; /** - * Formats a Bilibili video summary as plain text for QQBot replies. + * Formats a Bilibili video summary as a leading cover CQ image plus text for QQBot replies. * @param video - Normalized video info returned by the package-local Bilibili client. * @param config - Runtime config that controls the maximum displayed description length. - * @returns Concise plain text reply with a canonical Bilibili video URL. + * @returns Concise CQ-compatible reply with a cover image and canonical Bilibili video URL. */ export function formatBilibiliVideoReply( video: BilibiliVideoInfo, config: BilibiliCardRuntimeConfig, ) { + const coverImageSegment = buildBilibiliCoverImageSegment(video.pic); const lines = [ + ...(coverImageSegment ? [coverImageSegment] : []), 'Bilibili 视频解析', `标题:${video.title || '未知标题'}`, `UP:${video.ownerName || '未知UP主'}`, @@ -28,6 +30,30 @@ export function formatBilibiliVideoReply( return lines.join('\n'); } +/** + * Builds the leading CQ image segment from the Bilibili cover URL. + * @param pic - Cover URL returned by the Bilibili video API; used as the OneBot image `file` parameter. + * @returns CQ image segment placed before the text summary, or an empty string when no cover URL exists. + */ +function buildBilibiliCoverImageSegment(pic: string) { + const normalizedPic = `${pic || ''}`.trim(); + if (!normalizedPic) return ''; + return `[CQ:image,file=${escapeCqParam(normalizedPic)}]`; +} + +/** + * Escapes values embedded in a CQ segment parameter. + * @param value - Raw parameter value that may contain CQ delimiters or HTML entity characters. + * @returns Value safe to embed in a single CQ parameter. + */ +function escapeCqParam(value: string) { + return value + .replace(/&/gu, '&') + .replace(/\[/gu, '[') + .replace(/\]/gu, ']') + .replace(/,/gu, ','); +} + /** * Builds the canonical public Bilibili video URL used in replies. * @param video - Normalized video info containing either a BV id or an av id fallback. diff --git a/test/modules/qqbot/plugins/bilibili-card/bilibili-video-client.spec.ts b/test/modules/qqbot/plugins/bilibili-card/bilibili-video-client.spec.ts index 472ad4b..f359285 100644 --- a/test/modules/qqbot/plugins/bilibili-card/bilibili-video-client.spec.ts +++ b/test/modules/qqbot/plugins/bilibili-card/bilibili-video-client.spec.ts @@ -113,6 +113,7 @@ describe('Bilibili video client', () => { ), ).toBe( [ + '[CQ:image,file=https://i0.hdslb.com/bfs/archive/demo.jpg]', 'Bilibili 视频解析', '标题:夏祭', 'UP:UP主',