中文 VOCALOID(洛天依、言和、乐正绫等)歌词识别 + 歌曲库插件,两件事:
- 认歌词 —— 入站消息命中歌词时,向 MaiBot 的 LLM 上下文注入歌曲信息, 让 bot 能自然接住歌词话题
- 管歌库 —— 内置 VCPedia 爬虫,可以从 VCPedia
同步 4000+ 首中V歌曲(含作词作曲等完整创作信息)到本地 SQLite,
既能扩充歌词识别的词库,也能用
/歌词命令或聊天直接查歌
词库有三个来源,可以叠加使用:
| 来源 | 数据 | 怎么来 |
|---|---|---|
| 内置基础库 | 3412 首旧快照,57227 句歌词 | 自带 |
| VCPedia 同步 | 4000+ 首,含完整创作信息与歌词 | /歌词 同步(后台爬取) |
| 歌词文件 | 你自己放的歌 | 丢进 lyrics_inbox/(插件数据目录下),/加歌 |
- 安装
- 配管理员(重任务命令的开关) —— 装完先看这一节
- 快速开始
- 命令
- 工作原理
- 数据
- 添加新歌:丢歌词文件进收件箱
- 数据流向与隐私
- VCPedia 同步
- 配置详解
- 点歌联动
- 氛围选歌
- 性能与内存
- 故障排查
- 知识库结构
- 跨插件只读 API
- dashboard 自定义页面
- 本地测试
- 代码结构
- 更新日志 —— 各版本的变更与根因记录(独立文件)
纯标准库实现,没有任何第三方依赖(网络请求走 urllib,数据库走内置 sqlite3),
不需要 pip install,也没有 requirements.txt。
| 项 | 要求 |
|---|---|
| MaiBot 宿主 | 1.0.0 ~ 1.3.99(实测到 1.3.8;可视化配置页签需 1.3.6+) |
插件 SDK(maibot_sdk) |
2.1.0 ~ 2.13.99(实测到 2.13.0) |
| Python | 3.9+(用宿主自带的解释器即可) |
步骤:
- 下载本仓库:
git clone https://github.com/orge-8/cv_lyric_context.git, 或直接下载 zip 解压。 - 把整个
cv_lyric_context/目录放进 MaiBot 的plugins/下, 目录名保持cv_lyric_context——Runner 按目录名加载插件, 里面有_manifest.json和plugin.py,不要只挑文件拷。 - 完整重启 MaiBot。插件目录的变动不会热加载,必须重启进程。
- 启动日志里出现
中V歌词识别已加载: N 句 / M 首就是装好了。
首次启动时 Runner 会自动在插件目录下生成 config.toml。
/歌词、/歌词 状态、/歌词 搜索、/歌词 歌曲、/歌词 取消 开箱可用;
同步 / 补歌词 / 重抓这类重任务命令默认全部关闭,先按下一节填好管理员 QQ 号。
/歌词 同步、/歌词 补歌词、/歌词 重抓 会向 VCPedia 连续发起成百上千次请求,
所以设计成 默认关闭、按 QQ 号授权:留空 = 谁都触发不了,填了谁 = 只有谁能触发。
配置项叫「管理员 QQ 号」,在 dashboard 的这个位置:
MaiBot dashboard → 插件 → 中V歌词识别 · 歌曲库 → 配置(可视化模式)
→ 页签「同步与权限」
→ 管理员 QQ 号(谁能触发同步) ← 在这里填,多个用英文逗号分隔
→ 允许命令触发重任务 ← 一般保持开启
→ 本机控制台免白名单 ← 一般保持开启
对应的配置文件键(源代码模式或直接改 TOML 时用):
[crawler]
allow_sync_command = true # 总开关,关掉后白名单里的人也不能触发
sync_admin_ids = "你的QQ号" # 管理员名单,多个用英文逗号分隔
allow_local_operator = true # 本机控制台不受白名单限制配好后在 QQ 里发 /歌词 同步 100 验证:能开始抓取说明生效;
如果回「重任务命令默认关闭」,说明 QQ 号没填对(注意是全角逗号、空格、多打的引号),
或者热更新还没落到位——保存后等 1~2 秒;仍不行就重新加载一次插件(配置改动会触发
宿主自动加载,但个别版本对插件配置文件的监听有延迟)。
- 只读命令不需要管理员:
/歌词、/歌词 状态、/歌词 搜索、/歌词 歌曲、/歌词 取消、/加歌任何人都能用。- 本机控制台默认免校验。如果你希望本机也必须过白名单, 把「本机控制台免白名单」关掉(
allow_local_operator = false)。- 改完怎么生效:宿主监听插件配置文件,保存后自动推给运行中的插件(通常 1 秒内)。 如果命令行为没变,重新加载该插件或完整重启 MaiBot 兜底。
- dashboard 的「歌词曲库」页面按钮不受这份白名单约束:那里由 dashboard 登录态 把关(
lyrics.start_sync/lyrics.start_refetch只看plugin.enabled与allow_sync_command)。任何人都能登录 dashboard 时,请先给 dashboard 设密码。- 可视化配置页要 1.3.6+ 的宿主才有页签;更早的宿主会平铺所有配置节, 节标题与字段完全一致,不影响填写。
# 1. 在 QQ 里发(先小批量试跑,确认数据正常再跑全量)
/歌词 同步 100
# 2. 同步完会自动重建识别词库,新歌立刻可被识别,不用重启
# 3. 试试
/歌词 搜索 普通DISCO
/歌词 歌曲 普通DISCOCategory:洛天依歌曲 递归后约 4000 个词条,按默认 0.8 秒间隔全量同步约 1 小时。
谁能用 一列按默认配置:只读命令对所有人开放,重任务命令需要 先在「管理员 QQ 号」里登记。
| 命令 | 说明 | 谁能用 |
|---|---|---|
/歌词 |
显示用法 | 所有人 |
/歌词 状态 |
本地歌曲数量、空歌词条数、上次同步时间、同步进度,附解析器自检结果 | 所有人 |
/歌词 搜索 <关键词> |
按歌名 / 歌手 / P主搜索 | 所有人 |
/歌词 歌曲 <歌名> |
查看歌曲详情与歌词 | 所有人 |
/歌词 取消 |
中止正在进行的同步或批量补歌词 | 所有人 |
/加歌 |
导入 lyrics_inbox/ 里的歌词文件 |
所有人 |
/歌词 同步 [数量] |
后台增量同步,可限定本次抓取数量(如 /歌词 同步 100) |
管理员 |
/歌词 补歌词 [数量] |
批量重抓歌词为空的条目并回填(后台执行,非歌曲页自动跳过;不填数量用 sync_batch_limit) |
管理员 |
/歌词 补BV [数量] |
只回填 B站原曲标识(bilibili_aid/bilibili_bvid),不动歌词与创作者;供点歌插件按原曲取流 |
管理员 |
/歌词 重抓 <歌名> |
重新抓取单个词条,刷新歌词 / 简介(常规同步不更新已入库的歌) | 管理员 |
「补歌词」和「补BV」筛的对象不同,别混:前者只挑歌词为空的条目, 后者挑缺 B站标识的条目(v2.14.0 之前入库的全部历史条目都缺)。
「补歌词」会给确认无歌词的条目(非歌曲页、无歌词章节)打上时间戳,在
crawler.refill_cooldown_days 天内跳过,避免它们堵在队首被反复重抓;抓取失败的不打,
下次仍会重试。存量见 /歌词 状态 的「待补 / 近期已确认无歌词」。
「补BV」跑完会回报「库里仍缺 N 首」,再发一次即可续跑——已填过的条目不再进队列,
所以第二遍只处理真正没有 bb_id 的那部分。它只写两列(bilibili_aid /
bilibili_bvid),不会覆盖已同步的歌词、创作者与分类。
聊天里直接问也行,LLM 会自动调用 search_vcpedia_song / get_vcpedia_lyrics。
被拒绝时怎么办:同步 / 补歌词 / 重抓会回复「重任务命令默认关闭」, 提示里带着配置页签的路径;按提示把 QQ 号填进「管理员 QQ 号」即可,无需重启。
用户消息 "某句歌词"
│
├─ 监听入站消息 ──> 清洗文本(全半角/标点/大小写)
│ 新版: HookHandler chat.receive.after_process
│ 旧版: EventHandler ON_MESSAGE(回退)
│ 在 57227 句关键词表中 O(1) 精确匹配
│ 整句未命中 -> 子串匹配兜底(v2.10.0):
│ 按 N 字滑窗(默认 6,0=关闭)查约 30 万个
│ 歌词片段,连续共享 >= N 字即命中——接得住
│ 「记岔开头/只记得半句」("可是我并不喜欢
│ 吃鱼" -> 《我的悲伤是水做的》)
│ 命中 -> 按会话登记 (时间戳, 歌词, 歌名)
│
└─ HookHandler (maisaka.planner.before_request + maisaka.replyer.before_request,
均 BLOCKING)
把 TTL 内的命中同时注入到规划器与回复器两条 LLM 链路:
· 规划器: 决定调用哪些工具前先看到歌词语境
(优先推荐/检索用户正在聊的歌),prompt/messages/
items 三种载荷结构都能追加
· 回复器: LLM 请求前,把命中整理成 system 内容注入
· 新版运行时传 items -> 追加 SystemMessageItem 快照
(Context Item schema v1)
· 旧版运行时传 messages -> 追加 {"role": "system"}
两条链路各自幂等(含【歌词识别】标记就不重复叠加)
歌词文件 "某歌.txt" -> 放进 lyrics_inbox/(插件数据目录下)
│ (/加歌 命令或插件加载时自动扫描)
├─ 解析: 去 LRC 时间轴与元数据标签行,歌名取文件名 / [ti:] / 首行
├─ 过滤: 含汉字少于 2 个的句子丢弃
├─ 补元数据: 文件写了用文件的,没写就反查基础库与 VCPedia 库
├─ 合并写入同目录的 user_songs.json(同名歌合并歌词)
├─ 立即并入内存词库(不用重载插件)
└─ 归档: 成功 -> imported/,失败 -> failed/
/歌词 同步 -> VCPedia(Anubis PoW 反爬)
├─ 解 PoW 挑战换 auth cookie(缓存约一周,失效自动重解)
├─ MediaWiki api.php list=categorymembers 全量分页枚举,支持递归子分类
├─ 词条取 wikitext,解析出演唱/P主/作词/作曲/编曲/混音/调教/母带/PV/曲绘/年份/简介/歌词
├─ 写入 data/vcpedia_songs.db(增量,已入库的跳过)
└─ 同步完自动重建识别词库(也可随时用 /歌词 搜索 查询)
注入的 system 内容形如:
【歌词识别】用户最近在会话中发送了以下歌词原文:
- 「某句歌词」 出自《歌名》(演唱:洛天依,P主:某P,年份:2020,作词:A,作曲:B,编曲:C,调教:D,混音:E,PV:F,曲绘:G)
前后歌词:上一句 / 上一句 / 「被识别的这一句」 / 下一句 / 下一句
用户可能在引歌词、玩歌词接龙或聊这首歌。请在回复中自然地运用这些歌曲信息……
- 命中行的前后歌词对接龙/续唱最有用——模型终于知道下一句是什么了
- 创作信息(年份/词/曲/编/调教/混音/PV/曲绘)来自歌曲库,库里没有的歌自动跳过
- 三类内容都有独立配置开关,见下方
plugin.inject_*;接龙用不到的信息可以关掉省 token
插件平时只被动识别 + 注入上下文,不会自己发消息;bot 的回复仍由 MaiBot 主体生成,
只是"知道"了歌词背后的歌。唯一的例外是命令——发 /歌词 ... 或 /加歌 会收到一条回复。
只读素材(随仓库分发,在插件目录 assets/ 下;插件只会读它,不会写)
| 文件 | 说明 |
|---|---|
assets/knowledge_db.db |
内置基础库,3412 首中V歌曲(歌名、P主、歌手) |
assets/song_lyric_keywords.txt |
歌词句 -> 歌名 关键词表,57227 句(匹配源) |
用户数据(全部落在宿主分配给本插件的插件数据目录,即 ctx.paths.data_dir)
| 路径 | 说明 |
|---|---|
lyrics_inbox/ |
歌词文件收件箱,含 imported/ failed/ 两个归档子目录 |
user_songs.json |
歌词文件收件箱导入的自定义歌单 |
vcpedia_songs.db |
VCPedia 同步下来的歌(SQLite,songs + sync_meta 表) |
anubis_cookies.txt |
反爬 cookie,失效自动重解,可安全删除 |
recent_songs.json |
最近命中过的歌(有界环形 30 条,v2.9.0 新增,供跨插件只读查询) |
为什么用户数据不在插件目录里:整目录更新或重装插件会把
assets/覆盖掉, 用户的歌单和收件箱会一起丢。所以这些路径全部由数据目录推导,插件源码目录保持只读。 实际路径可在 WebUI 插件详情页看到;本文档里lyrics_inbox/之类的相对路径都指它。2.8.2 及以前放在
assets/lyrics_inbox/、assets/user_songs.json的存量数据, 2.8.3 起在插件加载时自动搬迁到数据目录并打日志(数据目录已有同名内容时不覆盖, 搬不动只记 warning 不阻断加载),不需要手工处理。
song_lyric_keywords.txt 加载时会过滤含汉字少于 2 个的句子(纯数字/纯英文),
避免圆周率类歌曲的数字串误命中。
2.8.4 起还会过滤日常聊天文本,五条规则(只看清洗后的键):
- 单字重复 ≥ 4 次("哈哈哈哈…"“啦啦啦啦…”):基础词库仅 26 个去重键,全是语气段;
- 2~3 字片段重复 ≥ 3 次("对不起对不起对不起""我不想我不想我不想""hahahahahaha"): 这类句子恰是吵架/撒娇/笑声刷屏常用语;
- ASCII 低信息键("emmmmmmmm"):整个键只由一两种字符构成;
- 命中日常高频用语表(约 390 条,精确匹配,不做子串):问候/致谢/道歉/祝福/
告别/情感/疑问/请求/情绪/日常/网络十一类通用口语,如「生日快乐」「新年快乐」
「不好意思」「好久不见」「在吗在吗」「求求你了」「相信自己」「马上就好」。
这些句子在歌词库里也有零星出处(
生日快乐出自《Come Back》、不好意思出自《鸽子》),但群聊里的出现率高几个数量级,宁可丢掉这几十句歌词的识别能力; - 片段整倍重复且片段在用语表内("我要回家"×4):4 字以上整句重复多是歌曲
专属歌词(
水煮包子×3 保留),只有片段本身是日常用语时才算刷屏。
为什么不用「整句能否被常用词拼出」这类通用判定?实测那样会拦掉 800+ 键,
其中大半是真歌词(你说你想、请听我说、请来找我)——误伤率不可接受。
所以第 4 条只做精确匹配:子串匹配会误伤「我喜欢你的笑容」,而「我讨厌你」
被拦、「我讨厌的你」保留。
群聊里这类文本命中只会把歌曲上下文无端注进与歌无关的闲聊(真机实录:群友单纯 发 9 连哈,bot 被注入带着提了一嘴《躲在医院厕所雾化的二人》)。在真机曲库 (7672 首 / 24.2 万行歌词)上合计过滤约 300 个去重键(0.16%),逐条人工过目, 无一是辨识度歌词;4 字片段 2 连("不能打架不能打架")刻意保留——实词密度高、 日常复现率低。过滤器吞吐约 24 万行/秒,全量建索引仍在 2 秒内、且跑在后台线程。
旧位置(assets/lyrics_inbox/、assets/user_songs.json)仍保留在 .gitignore 里,
以防回滚到老版本时被误提交。
只需要一个歌词文件,.txt 或 .lrc 都行,放进插件数据目录下的 lyrics_inbox/:
<插件数据目录>/
└── lyrics_inbox/
├── 普通朋友.txt <- 放这里
├── 千本樱.lrc <- LRC 也行
├── imported/ <- 导入成功后自动归档到这里
└── failed/ <- 导入失败的文件放这里,不会反复重试
找不到数据目录时,在 QQ 里发一次 /加歌——收件箱为空时插件会把完整路径回给你。
然后在 QQ 里发 /加歌(/导入歌词、/扫描歌词 同义),插件扫描收件箱、
把歌写进同目录的 user_songs.json,并回复导入结果:
歌词导入完成:成功 2 个,失败 0 个。
- 《普通朋友》 入库 32 句,歌名取自文件名
- 《千本樱》 入库 48 句,歌名取自LRC标签
当前自定义歌单共 2 首。
plugin.auto_import_inbox 默认为 true,所以重载插件或重启 MaiBot 时也会自动
扫一遍收件箱,不一定要用命令。导入的歌立即生效,不用再重载。
命令发完什么都没发生,按顺序查:
- 看日志里有没有
命令执行成功: lyrics_xxx—— 没有说明命令压根没触发, 跳到第 2、3 条;有说明触发了但消息没发出去,接着看回复发送失败或命令载荷里没有 stream_id。 [E_CAPABILITY_DENIED] … 未获授权能力: send.text—— manifest 的capabilities没写对。必须写点分的精确能力名send.text, 写send_message这种粗粒度名字无效。改完要重启 MaiBot(manifest 在插件 加载前校验,热重载不生效)。- 重启 MaiBot —— 新增的
@Command组件要重新注册,热重载插件不一定生效。 - 到 WebUI 的「Bot 配置 → 命令」里看
lyrics_status等在不在列表里 —— 1.2.0 起插件命令统一在这里管理(可配置放行用户/聊天流),没出现就是没注册上。
/歌词 同步、/歌词 补歌词、/歌词 重抓 有反应但被拒绝,属另一类问题——
权限没配好,见配管理员与「同步失败怎么办」的前三条。
命令只回了 False(静默拒绝)时,说明 crawler.allow_sync_command 被关了。
日志里出现命令执行成功但 回复发送失败 时,命令本身已经跑完(比如导入已完成,
会有 歌词文件已入库: …),只是结果没发出来;此时插件会退回让 bot 自己接一句话,
不会让你完全看不到反馈。
不依赖命令的退路:歌词文件放进 lyrics_inbox/(插件数据目录下)后重载插件或重启
MaiBot,加载时会自动导入,日志里会有 歌词文件已入库: …。
按顺序取,取到为止:
- 文件名(去掉扩展名和
(1)这类副本后缀)——推荐,最省事 - LRC 的
[ti:歌名]标签 - 文件第一个非空行——该行会被当作歌名,不计入歌词
文件名是 lyrics / 歌词 / 新建文本文档 这类通用名时,自动跳到第 2、3 条。
按优先级,取到为止:
- LRC 标签 ——
[ar:歌手]填歌手,[by:]/[au:]/[re:]填 P 主 - 文件名约定 —— 见下节,库里没有的歌用这个手工指定
- 从基础库反查 —— 歌名命中
knowledge_db.db(3412 首,歌手 3405 条、P主 3340 条) 就自动补上缺失的字段
《珍珠》的例子:歌词文件里什么都不写,因为库里有这首歌,导入后自动变成
演唱:洛天依,P主:洛天依官方账号。
改文件名就能指定歌手和 P 主,不用碰文件内容:
| 文件名 | 歌名 | 歌手 | P主 |
|---|---|---|---|
珍珠.txt |
珍珠 | — | — |
珍珠 - 洛天依.txt |
珍珠 | 洛天依 | — |
珍珠 - 洛天依 - 某P.txt |
珍珠 | 洛天依 | 某P |
珍珠【某P】.txt |
珍珠 | — | 某P |
珍珠 - 洛天依【某P】.txt |
珍珠 | 洛天依 | 某P |
【洛天依】珍珠.txt |
珍珠 | 洛天依 | — |
珍珠 - 洛天依、言和.txt |
珍珠 | 洛天依、言和 | — |
规则:
- 分隔符
-(半角减号两侧都要空格)或全角-—(不要求空格) - 括号
【】[]标注 P 主;但括号在最开头时算歌手(中V 常见的【歌姬】歌名写法) - 半角减号两侧无空格时不拆,所以
X-02.txt、光 -Hikari-.txt这类歌名不会被拆坏 - 如果完整文件名能在库里查到,就完全不拆分,直接用原名 + 库的元数据
规则细节:
- 只补空缺,写了就不覆盖,库里有也不覆盖手填值
user_songs.json里留空的字段不会冲掉库里的信息(早期版本会,已修)- 库里的歌手可能带换行(如
言和\n洛天依),注入时会压成「演唱:言和、洛天依」 - 库里没有的歌就留空,注入时不加空括号
想改已经导入过的歌,直接编辑数据目录下 user_songs.json 的 singers / uploader 字段。
- 自动剥掉 LRC 时间轴(
[00:12.34],一行多个也能剥) - 跳过
[ti:][ar:][al:][by:][offset:]等元数据标签行 ([ar:]/[by:]/[au:]/[re:]的内容会取作歌手/P主,见上一节) - 空行去掉,文件内重复行只留一句
- 含汉字少于 2 个的行(纯数字/纯英文)会被过滤,防止圆周率类歌曲误命中
- 编码自动识别:UTF-8(含 BOM)/ GBK / Big5
- 单个文件最多入库
plugin.max_lines_per_song行(默认 2000)
同名歌不会重复添加,新歌词会合并进已有条目。
手改、批量改时用这个:
[
{
"name": "歌名",
"singers": "洛天依",
"uploader": "某P",
"lyrics": ["第一句歌词", "第二句歌词"]
}
]lyrics 也支持直接写字符串,用 \n 换行。
生效方式:插件在 on_load 时读取,所以在 MaiBot 里重载插件
(WebUI 关掉再打开)或重启 MaiBot 即可,无需重装。同名歌词句以自定义歌优先。
聊天内容不出本地。 歌词识别全程在进程内做关键词匹配,不落盘、不上传。
| 方向 | 内容 | 去向与条件 |
|---|---|---|
| 入站 | 消息正文 | 只在本进程内存里匹配;不写文件、不发网络 |
| 出站 | 歌名 / 分类名 / 词条名 | 仅在你主动触发同步或使用查询工具时,请求 crawler.base_url(默认 https://vcpedia.cn,MediaWiki 公开站点)。请求里不含聊天记录、QQ 号、群号、图片 |
| 出站 | 歌名 + 歌词 + 固定 prompt | emotion.annotate_on_sync 与 recommend_cv_song 走宿主的 llm.generate 能力,由你自己配置的模型服务商处理;插件不直连任何模型 API、不持有 API key |
| 本地落盘 | 收件箱、自定义歌单、曲库、cookie 缓存 | 全部在宿主给的插件数据目录(ctx.paths.data_dir)下;anubis_cookies.txt 只用于通过 VCPedia 的反爬校验,可随时删除 |
其余边界:
- 插件源码目录只读:
assets/里只有随包分发的素材(曲库元数据、关键词表), 插件不往那儿写任何用户数据——整目录更新/重装插件会覆盖那些文件。 用户的收件箱与歌单统一落在插件数据目录,路径由ctx.paths.data_dir推导。 - 插件不读宿主的数据库或日志,也不读其它插件的文件。
sqlite3只用在自己数据目录下的运行时曲库、随仓库分发的只读素材库assets/knowledge_db.db,以及你在plugin.extra_song_dbs里显式指定的外部曲库上——素材库与外部曲库一律以mode=ro只读打开。 - 不修改宿主的任何文件,也不改动其它插件或适配器的组件开关状态;配置读写交给宿主的配置管理层,插件不自己写回
config.toml。 - 仓库内不含任何凭据。情绪标注只走 MaiBot 自己的 LLM 通道(宿主配好模型任务即可),插件不持有任何 API key;
config.toml由宿主运行时生成,同样不入库。 verify_ssl = false默认关闭(默认值true);只有自己搭了中间人代理、又不想配ca_bundle时才该打开,插件会在打开时打 warning 日志。详见「公司网络 / 安全软件导致证书错误」。
两处,自动的:
- 扩充歌词识别词库 —— 插件启动时,以及每次同步完成后,都会读取
data/vcpedia_songs.db,日志里出现外部歌曲库 vcpedia_songs.db: 新增 N 首歌/歌词库: 同步后重建索引,新增 N 首进入识别词库。 之后这些新歌的歌词句也能被识别命中,歌手/P主自动带上,不需要重启 MaiBot。 重建是幂等的:已有的歌跳过,只有新增的进索引(已有歌词的更新不会重索引)。 - 直接查歌 ——
/歌词 搜索/歌词 歌曲,或聊天里直接问(走 LLM 工具)。
规则:
- 基础库已有的歌名不重复索引 —— 它的歌词已经在
song_lyric_keywords.txt里了, VCPedia 只补新歌。所以日志里的"新增 N 首"通常小于库里的总条目数,这是对的。 - VCPedia 的歌手/P主不覆盖基础库已有的值,只补空缺。
- 没同步过、库是空的,都只是跳过,不影响歌词识别。
- 词库索引从 v2.6.2 起在后台线程构建完成后一次性换入,插件加载不再阻塞事件循环; 外部库的歌词也不全量常驻内存(见「性能与内存」一节)。
song 表的 bilibili_aid / bilibili_bvid 两列从 v2.8.0 起就在结构里,但一直空着
(抓取侧没有解析代码)。v2.14.0 补上采集,让点歌类插件能按原曲精确取流。
取数规则(顺序即优先级):
- 首个
{{VOCALOID_Songbox}}的|bb_id=—— 主信息框即原曲。 同一页面可能有多个 Songbox,后续的属于重制版或翻唱,不能取: 例《权御天下》第 2 个是洛天依官方账号的重制版,《达拉崩吧》第 2/3 个是翻唱; - 无 Songbox 时取正文首个
{{BilibiliVideo|id=...}}—— 正文按「原曲 → 翻唱」 顺序书写,首个即原曲(《普通DISCO》没有 Songbox,首个 id=2129461)。
bb_id 可能是 avNNN 或 BVxxxxxxxxxx,解析结果按形态落到对应列
(实测 av 与 BV 是同一视频的两种写法:BV1zs411S7sz 与 aid=2129461 的 cid
都是 3307690)。解析来源记在返回值里(songbox / bilibili_video),
后者属推断值,做覆盖率统计时要能分开看。
两条工程约定:
- 单调累积:重抓时若本次解析不到
bb_id(页面模板改动、词条被改成消歧义页), 保留旧值而不清空。这是与 9 个创作者字段「整体覆盖」语义的唯一分歧点—— BV 号本身不会变,把已采集的抹掉没有收益。 - 块切分按
{{ }}深度:Songbox 参数里有嵌套模板(再生 = {{bilibiliCount|id=9372087}}), 朴素找最近的}}会把块切在最内层,第二个 Songbox 的bb_id就会串位。
对 12 首知名曲目实测:现存页面 100% 命中(11 首来自 Songbox,2 首来自 BilibiliVideo)。 冷门曲目可能没有 Songbox,覆盖率需在全量重抓后按实际数据统计。
重抓已有的歌才会回填这两列——只跑增量同步不会补历史数据。 存量条目用
/歌词 补BV专门回填(只写这两列、不动歌词与创作者), 可分批续跑:跑完会回报「库里仍缺 N 首」,再发一次即继续。
点歌插件传来的是用户原话,而库里歌名形态很杂——safe_song_name 不转小写,
只做精确匹配会大量漏命中。bilibili_ref() 逐级放宽:
| 层级 | 规则 | 例子 |
|---|---|---|
exact |
精确 / safe_name 精确 / 忽略大小写 |
普通disco → 普通DISCO;普通DISCO! → 普通DISCO |
base |
去末尾括号补充(双向) | Binary Life ↔ Binary Life(二元生命) |
prefix |
剥掉尾部 token(点歌常传「歌名 歌手」) | 普通DISCO 洛天依 → 普通DISCO |
逐层查、命中即返回(v2.14.4)。不能把所有条件并成一个 OR:真机库里同时存在
Say No 与 SAY NO!,两者归一化键都是 SAY NO,并起来查 SAY NO! 会命中两条
而被判成歧义——可用户输入完全等于其中一条。分层后精确命中优先,只有同一层内
出现多候选才算歧义(如 ABC 与 abc 共存时查 Abc,两者精确度相同,不猜)。
每一级都要求唯一命中:命中 0 条或多条都返回 None。多条时不猜——
九尾妖狐(PoKeR) 与 九尾妖狐(肯德龟) 这类同名不同版本,猜错就是播错歌,
让调用方回退平台搜索才是安全边界。
用真实 3412 首曲库压测(assets/knowledge_db.db):
| 查询形态 | 命中率 | 误命中 |
|---|---|---|
| 原样 | 3412/3412 = 100% | 0 |
| 转小写 | 3412/3412 = 100% | 0 |
| 去括号后缀 | 3408/3412 = 99.9% | 0 |
| 加歌手后缀 | 3412/3412 = 100% | 0 |
那 4 条未命中正是预期的歧义场景(九尾妖狐×2、清明×2),属设计行为。
误命中 0 是这套规则的核心指标——宁可少命中,不能播错版本。
接口额外返回原曲演唱者名单(如 ["洛天依"])。调用方靠它区分「用户点的是原曲还是翻唱」:
| 查询 | match |
singers |
调用方应如何处置 |
|---|---|---|---|
世末积雨云 |
exact |
["洛天依"] |
采纳原曲 |
世末积雨云 洛天依 |
prefix |
["洛天依"] |
采纳——额外 token 是原曲歌手,用户要的就是原曲 |
普通DISCO 三无 |
prefix |
["洛天依", "言和"] |
不采纳——三无不是原曲歌手,用户在点名翻唱版 |
没有这个字段时,调用方只能在「误杀原曲歌手」与「误播原曲」之间二选一。
真机 2026-10-22 两种都发生过:世末积雨云 洛天依 被误杀后,网易云返回了
「洛天依 ACE Studio Cover」翻唱版,而原曲其实是 COP 投稿的 av1579079。
站点全站启用 Anubis PoW 反爬(明文请求含 api.php 一律 403),
插件内置解题逻辑与 cookie 缓存,无需手工配置。
实现上参考了 mohobot 的
music_knowledge/vcpedia.py(MIT),并修正了它两处会失败的地方:
pass-challenge的response必须传真实 hash。mohobot 传固定值1, 在 Anubis 1.27 会返回invalid response.redir必须用重定向后的最终 URL。站点根路径会 301 到/首页, challenge 是在/首页下发的,传初始 URL 会导致校验失败
请保持 crawler.request_interval 不要太小,别把站爬崩了。
crawler.categories 可以改,父分类会自动递归子分类:
[crawler]
categories = "Category:洛天依歌曲,Category:殿堂曲,Category:传说曲"注意两件事:
- 改完配置必须重启 MaiBot(或重载插件)。WebUI 保存配置只写文件,不会推送给
插件运行器——改完直接
/歌词 同步跑的还是旧分类。同步开始的消息里会回显 当前分类,跑之前对一眼。 - 换分类是增量不是替换。已有的歌不会删,新分类的歌追加进同一个
data/vcpedia_songs.db。想同时保留多个歌手就写成逗号分隔的列表,别来回换。
plugin.extra_song_dbs 可以额外接别的 SQLite(多个用英文逗号分隔)。
库里只要有 songs(name, singers, uploader, lyrics) 四列就能用(多出的列忽略):
[plugin]
# 相对路径按本插件的数据目录解析(即宿主给这个插件的目录)
extra_song_dbs = "我的歌库.db"
# 也可以直接写绝对路径
# extra_song_dbs = "D:/songlibs/我的歌库.db"留空则只加载内置爬虫同步下来的库。相对路径不允许越出插件数据目录,
写成 ../../ 之类越界的会被拒绝并记日志。
在哪改——三种方式,改的是同一份 config.toml:
| 入口 | 怎么进 | 适合 |
|---|---|---|
| 可视化模式(推荐,宿主 1.3.6+) | dashboard → 插件 → 中V歌词识别 → 配置 | 日常改配置,字段下方灰字就是填写说明 |
| 源代码模式 | 同一页面切到源代码 | 批量改、整段复制 |
| 直接改文件 | plugins/cv_lyric_context/config.toml |
脚本化部署 |
可视化模式的三个页签:
| 页签 | 管什么 |
|---|---|
| 识别与注入 | 命中判定、注入内容、搜索条数、歌词文件导入 |
| 同步与权限 | 管理员 QQ 号、爬取分类、限速、超时、证书 |
| 扩展功能 | 氛围选歌、情绪标注、插件联动 |
旧宿主(< 1.3.6)没有页签,三个节平铺在一页,节标题与字段一致。 下面的
crawler.sync_admin_ids对应 TOML 的[crawler]节键名, 在可视化模式里就是「同步与权限」页签下的「管理员 QQ 号」。 保存后宿主会热更新到插件;plugin.subspan_min_chars、plugin.meme_phrases这类影响词库结构的字段在下一次重建时生效。
| 键 | 默认 | 说明 |
|---|---|---|
plugin.enabled |
true | 是否启用 |
plugin.min_line_len |
4 | 参与匹配的歌词句最短字数(过滤过短误报),也用于歌词文件导入 |
plugin.subspan_min_chars |
6 | 子串匹配滑窗长度:整句未命中时按 N 字滑窗找连续共享片段,接得住「记岔开头/只记得半句」(0 = 关闭)。越小接得越短、误报越多;修改后自动整库重建词库 |
plugin.meme_phrases |
空 | 追加的二次元/网络梗过滤词(空格或逗号分隔):这些词在群聊里是高频梗,不再按歌词识别(Ciallo、空尼奇瓦、前方高能等已内置);适合补充本社群专属梗词,改动后重启生效 |
plugin.ttl_seconds |
600 | 命中后多久内注入有效(秒) |
plugin.max_inject |
3 | 单次注入最多携带的歌曲数(歌名去重) |
plugin.inject_context_lines |
2 | 注入命中歌词的前后各几行(0 表示只注入命中的那一句) |
plugin.inject_basic_credits |
true | 注入年份与作词/作曲/编曲 |
plugin.inject_full_staff |
true | 注入调教/混音/PV/曲绘等完整 STAFF |
plugin.auto_import_inbox |
true | 插件加载时自动导入 lyrics_inbox/ 里的歌词文件 |
plugin.max_lines_per_song |
2000 | 单个歌词文件最多入库的行数 |
plugin.extra_song_dbs |
空 | 额外的歌曲库(SQLite)路径(相对插件数据目录或绝对路径),留空只用内置爬虫同步下来的库 |
plugin.max_results |
5 | 搜索歌曲时最多返回几条 |
plugin.detail_lyric_lines |
30 | /歌词 歌曲 展示的歌词行数(0 = 不展示) |
plugin.lyric_preview_chars |
120 | 工具返回歌词时的预览字数 |
前三行就是「谁能触发重任务命令」的开关(可视化模式里排在这一页签最前面):
| 键 | 默认 | 说明 |
|---|---|---|
crawler.sync_admin_ids |
空 | 管理员 QQ 号:允许触发 /歌词 同步、/歌词 补歌词、/歌词 重抓 的 QQ 号(英文逗号分隔)。留空 = 谁都不能触发,见配管理员 |
crawler.allow_sync_command |
true | 是否允许用命令触发重任务。关掉后即使白名单里有人也不能触发 |
crawler.allow_local_operator |
true | 是否放行本机控制台(宿主标记 is_local_operator 的调用);关掉后本机也要走白名单 |
crawler.base_url |
https://vcpedia.cn |
VCPedia 站点根地址 |
crawler.categories |
Category:洛天依歌曲 |
爬取分类,多个用英文逗号分隔 |
crawler.category_depth |
2 | 子分类递归深度 |
crawler.request_interval |
0.8 | 两次请求的最小间隔(秒),请保持礼貌爬取 |
crawler.timeout |
20 | 单次请求超时(秒) |
crawler.max_fail |
30 | 连续失败达到该次数时提前中止同步 |
crawler.sync_batch_limit |
0 | 单次同步最多抓取多少首,0 表示不限 |
crawler.refill_cooldown_days |
7 | /歌词 补歌词 跳过多少天内已确认无歌词的条目,0 表示每次都重抓 |
crawler.verify_ssl |
true | 是否校验 SSL 证书(见下) |
crawler.ca_bundle |
空 | CA 证书文件路径(PEM),用于有 TLS 中间人的网络 |
这三节在可视化模式里都默认收起——配一次基本不用再动。
| 键 | 默认 | 说明 |
|---|---|---|
recommend.recommend_enabled |
true | 是否启用 recommend_cv_song 推荐工具 |
recommend.target_singers |
洛天依 | 目标歌手白名单(英文逗号分隔),推荐优先返回他们演唱的歌 |
recommend.known_virtual_singers |
洛天依,言和,… | 已知虚拟歌手名单;候选歌歌手列表里出现「名单中非目标歌手」即被过滤 |
recommend.allow_unknown_singer |
true | 歌手字段为空的歌是否允许推荐(放行但标注「歌手未知」) |
recommend.recommend_count |
3 | LLM 未指定数量时默认推荐几首 |
recommend.soft_exclude_size |
10 | 最近推荐软排除窗口,窗口内不重复推荐(0 = 不去重) |
emotion.annotate_on_sync |
true | 同步完成后自动为待标注的歌打情绪标签(新歌优先) |
emotion.annotate_on_sync_limit |
30 | 每轮同步后最多标注多少首(防打爆 LLM 配额) |
emotion.llm_task |
utils | 标注用的模型任务名/模型标识(utils / planner / replyer / 具体模型名);留空走默认路由,若未配好会 fallback 向量模型报 400 |
emotion.annotate_timeout_ms |
20000 | 标注单首歌的 LLM 超时(毫秒) |
emotion.annotate_budget_seconds |
180 | 每轮标注总时间预算(秒),超时即停下轮继续 |
integration.play_tool_enabled |
true | 在规划器注入里提示可用的点歌工具(见下节) |
integration.play_tool_name |
search_and_play_music |
点歌工具名;留空则只给「可点播查询」串、不指定工具,便于换用其它点歌插件 |
integration.play_tool_hint |
空 | 追加在点歌提示末尾的自定义说明(如本群点歌限额) |
integration.max_play_candidates |
2 | 注入里最多给出几条可点播的「歌名 歌手」查询串 |
配合点歌插件 [github.cateye.music-request](search_and_play_music 工具)使用。
链路是经 LLM 规划器的松耦合,不硬依赖:
用户在群里贴歌词
→ 本插件识别出歌名/歌手
→ maisaka.planner.before_request 注入:歌名 + 「可点播查询:歌名 歌手」
→ 用户说「放一下」
→ 规划器调用 search_and_play_music(query="歌名 歌手")
→ 点歌插件把歌发到当前会话
为什么必须由本插件给出**「歌名 歌手」**:只给歌名时点歌插件容易搜到翻唱或同名曲;
歌词本身也不能直接当 query(搜不到)。所以注入里为前 max_play_candidates 首歌
各生成一条查询串,并明确告诉规划器「query 填这一整串,不要只填一句歌词、不要自己另猜歌名」。
行为约束(写进注入文案):只在用户明确想听时调用,不主动放歌、不为同一首歌连续调用。
若不想参与联动,把 integration.play_tool_enabled 设为 false 即可,
注入会退回原来的形态(只提示 recommend_cv_song / cv_song_search)。
排障:
| 现象 | 处置 |
|---|---|
| 贴了歌词但 bot 不放歌 | 先确认注入是否命中:真机日志里应有 已向规划器注入歌曲信息(prompt/items) |
| 注入有了但仍不放歌 | 规划器可能没选中该工具。点歌工具默认在 deferred 池,注入文案已提示先 tool_search 检索;也可把 integration.play_tool_name 留空并依赖模型自行判断 |
| 播了但放错歌 | 检查歌曲库里的歌手字段是否为空(/歌词 歌曲 看「演唱」);歌手为空时查询串只有歌名,命中率自然下降 |
给曲库加了情绪标签层后,bot 可以根据聊天氛围主动推歌:
- 标签固定 7 个:甜美、温柔、积极、帅气、搞怪、伤感、愤怒(一首歌可多标签)。
- LLM 对话中判断用户想听歌/氛围合适时,会自己调用
recommend_cv_song工具 (传入情绪标签),工具内完成:标签匹配 → 随机抽取 → 最近推荐去重 (默认 10 首窗口内不重复)→ 歌手归属校验(合唱、含其他虚拟歌手的歌 不进推荐池;歌手字段为空的放行但标注「歌手未知」)。 - 歌词识别注入行为完全不受影响——校验只作用于推荐环节。
/歌词 同步 的结果消息发出后,插件会自动给待标注的歌打情绪标签:
- 新歌优先:按 id 倒序取队列,刚同步进来的歌先标,标完立刻能被
recommend_cv_song推荐 - 每轮最多
emotion.annotate_on_sync_limit(默认 30)首,总预算 180 秒,超时剩下的下轮继续 - 任何失败都不影响同步:单首失败只记 warning;连续 3 次失败熔断本轮, 日志会直接给出三选一处置(配 model_config / 改 llm_task / 关开关)
- 同步回执之外会单独补一条「(顺带完成 N/M 首情绪标注)」
- 关闭:
emotion.annotate_on_sync = false,热重载立即生效
llm_task 怎么填:默认 utils;若真机日志出现
plugin.org.mai-mai.cv-lyric-context ... embedding ... 400 Field required: input.contents,
说明该任务被路由到了向量模型——三选一:① model_config.toml 给任务
plugin.org.mai-mai.cv-lyric-context 配文本模型(根治);② emotion.llm_task 改
planner / replyer 或具体模型名;③ 关掉 annotate_on_sync。
排空历史存量:同步后自动标注走新歌优先,让新歌即插即用;早于该机制入库的
约 4000 首老数据,靠多次同步(或临时把 emotion.annotate_on_sync_limit 调大)慢慢排空。
失败的歌不写标签、下轮仍进队列,天然断点续跑;不标注也不影响歌词识别与推荐之外的任何功能。
看覆盖进度:发「/歌词 状态」,或看 dashboard「歌词曲库」页的「已标注情感」与
「待标注情感」两项统计。
同步报 CERTIFICATE_VERIFY_FAILED: self-signed certificate in certificate chain,
说明你的网络出口做了 TLS 中间人(代理或安全软件把站点证书换成了自签的)。两种解法:
第一步:搞清楚是哪张证书
在跑 MaiBot 的那台机器上打开 https://vcpedia.cn → 地址栏锁图标 → 「连接是安全的」→ 证书图标 →「详细信息」/「证书路径」, 看这条链最顶层那张是谁:
- 最顶层是系统内置的公共 CA(
DigiCert、ISRG Root X1之类)→ 链路正常, 证书报错另有原因; - 最顶层是代理 / 安全软件 / 公司网关的名字(
TestCorp Proxy Root CA之类)→ 链路里被插了一层,那张就是你要找的根证书。
不想开浏览器,可以用 openssl 反查签发者(Git Bash 自带):
# 被中间人替换时,这里会显示代理 CA 的名字而不是公共 CA
openssl s_client -connect vcpedia.cn:443 -servername vcpedia.cn </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer第二步:导出并填进配置
Win + R→certmgr.msc回车- 展开「受信任的根证书颁发机构」→「证书」 (找不到就去「中间证书颁发机构」里再找一遍)
- 按「颁发给」排序,搜第一步拿到的那个名字
- 右键 → 所有任务 → 导出 → 选 Base64 编码 X.509 (.CER)
- 存好后在
config.toml里填(可视化模式:同步与权限 页签 → 「CA 证书文件路径」):
[crawler]
ca_bundle = "D:/certs/proxy-root-ca.cer".cer 和 .pem 内容一样(都是 Base64 PEM),改不改后缀都行。
如果代理是本机软件,直接去它那儿拿更快:
| 软件 | 位置 |
|---|---|
| Fiddler | Tools → Options → HTTPS → Actions → Export Root Certificate to Desktop |
| Charles | Help → SSL Proxying → Save Charles Root Certificate |
| mitmproxy | ~/.mitmproxy/mitmproxy-ca-cert.pem |
| Clash Verge | 设置里一般有「系统代理 CA」;找不到就用上面的 openssl 命令反查 |
临时方案:关掉校验
[crawler]
verify_ssl = false这会跳过证书链校验,存在被中间人窃听的风险。只在确认那个代理是你自己的 (公司网关、本机安全软件)时才用,公网上不要开。插件关闭校验时会打一条 warning 日志。
默认行为:
crawler.verify_ssl的默认值是true——即开箱即用就是完整证书链校验, 不会静默降级。只有用户显式把它设为false才会走ssl.CERT_NONE分支,且那一刻 日志里必有 warning(已关闭 SSL 证书校验(verify_ssl=false)…)。优先用上面的ca_bundle(只追加信任代理根证书,系统根证书库仍然生效)。
ca_bundle 指向的文件不存在或格式非法时,会自动回退到系统证书并记日志,不会让插件起不来。
一轮针对常驻内存与事件循环阻塞的优化,行为完全不变 (新旧代码索引逐条对比一致:58139 句关键词 / 3412 首元数据):
- 加载不再阻塞事件循环:词库索引(解析 5.7MB 关键词文件 + 构建约 6 万条索引 + 读两个 SQLite 库)改到后台线程构建,完成后一次性原子换入,同进程其他插件不再被卡。
- 歌词不再全量常驻:只有真实多行(或单行可展示)的歌词进内存索引; 基础库里无换行的整段 blob(超长、永远不可能命中)与 VCPedia 库的全量歌词 不再常驻,改走 200 首上限的 LRU 按需查库。同步规模越大,省得越多。
- 推荐查询瘦身:
recommend_cv_song的候选查询从SELECT *改为只取 歌名/歌手/情绪/简介四列,并把标签过滤下推给 SQL,不再把全库歌词读进内存。 - 注入路径:命中行定位从每行最多 3 次文本归一化降为 1 次(单趟扫描,全等优先); 歌曲创作信息查询加有界缓存,不再每首歌新开一次数据库连接。
- 其他:同步入库复用单个连接(仍逐首提交,状态计数依旧实时); Anubis PoW 求解复用前缀哈希;加载期汉字计数改为提前退出; 关键词里的重复歌名字符串去重;禁用/卸载插件时释放全部核心索引。
- 子串索引开销(v2.10.0):滑窗 6 字时子串索引约 30 万个片段, 构建耗时 +0.3s(实测 0.97s,基线约 0.7s),常驻内存 +40~60MB; 归属歌数超过 3 的通用片段不登记,卸载/禁用时随索引一起释放。 滑窗设为 0(关闭子串匹配)即回到 v2.9.0 的内存占用。
绝大部分问题看一行日志就能定位,所以先查「日志速查」,再对症处理。
爬取侧的常见故障另外收在「同步失败怎么办」;
命令发出去没反应,见前面「/歌词、/加歌 没反应怎么办」一节。
首次触发时各打印一行字段名诊断,日常运行打印命中与注入结果:
| 日志 | 含义 |
|---|---|
中V歌词识别已加载: N 句 / M 首 / 子串片段 K(滑窗 W 字,0=关闭) |
插件已加载、词库就绪(含 v2.10.0 子串索引规模) |
[诊断] chat.receive.after_process 字段: [...] |
入站 hook 的实际字段名 |
歌词命中: 「…」-> 《…》 (会话=…) |
整句精确匹配成功 |
歌词命中(子串): 「…」-> 《…》 (会话=…) |
整句没记对,靠滑窗片段兜底命中(v2.10.0);片段即「」里那截 |
梗过滤表: 并入 config 自定义梗词 N 条 |
meme_phrases 里的自定义梗词已并入过滤表(v2.11.0);内置梗/日常用语照常拦截 |
[诊断] import_lyrics 字段: [...] |
/加歌 命令首次触发,列出载荷里的实际字段名 |
收到歌词导入命令: raw=… stream_id=… |
命令已触发;stream_id=<空> 说明取不到会话,回复发不出去 |
歌词导入结果已回复: sent=True/False |
结果是否发出去;False 时插件会退回让 bot 自己接话 |
命令载荷里没有 stream_id,无法回复结果 |
命令触发了但回不了话,把字段列表反馈给开发者 |
[诊断] replyer.before_request 字段: [...] |
回复器注入 hook 的实际字段名 |
[诊断] planner.before_request 字段: [...] |
规划器注入 hook 的实际字段名 |
已向 LLM 上下文注入歌曲信息(extra_prompt,N/M 字) |
回复器注入成功;M 明显大于 N 说明被预算截断 |
已向规划器注入歌曲信息(prompt/messages/items) |
规划器注入成功 |
歌词注入未生效:replyer.before_request 载荷里没有 extra_prompt |
hook 契约不匹配,把字段名反馈给开发者 |
歌词命中但 extra_prompt 预算已满 |
别的插件已占满共享通道,本轮跳过 |
歌词命中但 planner 请求载荷中没有 prompt/messages/items |
规划器侧无可用载荷,把字段名反馈给开发者 |
歌词收件箱导入: 成功 N 个 / 失败 M 个 |
本次加载扫过收件箱的结果 |
歌词文件已入库: xxx.txt -> 《歌名》(新增 N 句) |
某个歌词文件导入成功 |
歌词收件箱导入失败: … |
扫描/写盘异常,看完整堆栈 |
歌词库已加载: 本地 N 首歌曲,上次同步 … |
内置 VCPedia 库就绪 |
歌词库: 解析器自检通过(新版:…) |
进程里装的是新解析器,未闭合 <poem> 的页面能解析 |
歌词库: 解析器自检失败——旧版:… |
WARNING。磁盘文件可能是新的,但进程里的模块还是旧的,完整重启 MaiBot |
外部歌曲库 vcpedia_songs.db: 新增 N 首歌 |
爬到的歌接进了识别词库 |
VCPedia: 同步失败: … |
爬取异常,多为反爬拦截、限流或证书问题 |
hook 会话 … 无命中 类情况 |
会话 ID 与消息侧不一致(本插件两侧都取 session_id,一般不会出现) |
| 现象 | 原因与处理 |
|---|---|
| 回「重任务命令默认关闭」 | 没有配置管理员 QQ 号。按配管理员在「同步与权限」页签填上自己的 QQ 号,保存后通常 1 秒内热更新生效 |
| 回「这个命令只有管理员能触发」 | 已配白名单,但你的号不在名单里。注意英文逗号分隔、前后不要空格、不要带引号 |
| 回「未能识别发送者 QQ 号」 | 宿主没把发送者 ID 传进命令载荷。日志里有 [诊断] 同步命令未能识别发送者字段,把字段列表反馈给开发者;临时可在本机控制台(若 allow_local_operator = true)触发 |
CERTIFICATE_VERIFY_FAILED: self-signed certificate in certificate chain |
网络出口有 TLS 中间人。配 crawler.ca_bundle 指到代理根证书,或临时 verify_ssl = false。详见上一节 |
pass-challenge 返回 403(invalid response.) |
站点 Anubis 版本升级改变了校验方式,需对照前端 main.mjs 调整 |
| 同步全量失败、日志刷 403/超时 | 请求过密被临时限流。调大 crawler.request_interval,等一段时间再试 |
本地 getaddrinfo failed / 连不上站 |
网络环境的 DNS 拒绝解析该域名,换 DNS 或网络环境 |
未找到 Anubis challenge 脚本 |
站点可能已关掉反爬,或页面结构变了;看返回的状态码判断 |
| 库里没有某首歌 | 该曲不在配置的分类下;换 crawler.categories 或确认歌名 |
| 歌词为空 | 词条本身没有歌词章节(相声、纯音乐、器乐等非歌曲条目) |
/歌词 重抓 回来「歌词 0 行」 |
先发「/歌词 状态」看解析器自检:不通过=进程里是旧模块,完整重启 MaiBot;通过=该页面结构特殊,按上面的日志速查对照解析器各级输出,必要时把该页面 URL 反馈给开发者 |
| 想抽查剩下那些空歌词条目 | 发「/歌词 状态」看缺歌词总数;dashboard「歌词曲库」页的缺歌词清单区块可列出名字(会标出哪些还没确认过),挑几首用「/歌词 搜索 <歌名>」逐个确认是真的没歌词还是解析漏了 |
| 命令发完没反应 | 见「/歌词 没反应怎么办」 |
v2.8.0 之前,曲库是一张 21 列的宽表 songs:创作者被平铺成 lyricist /
composer / arranger / mixer / tuner / mastering / pv / illustrator
八个文本列,歌姬、分类、情绪各是一个分隔串。这种结构表达不了
「一人兼多角色」「一个角色多人」「角色未知」,也没法回答「这位 P 主还写过什么」。
现在改为规范化实体 + 一个同名兼容视图(思路参考中V档案馆 CVSA 的关系模型):
| 表 | 作用 |
|---|---|
song |
歌曲主体:名称、年份、时长、B站 aid/bvid、派生曲 original_song_id、软删除 |
credit_role |
角色字典:UP主 / 作词 / 作曲 / 编曲 / 混音 / 调校 / 母带 / PV / 曲绘 |
artist |
创作者档案(去重,带 aliases 别名) |
song_credit |
(歌曲, 创作者, 角色) 三元关系 —— 取代原来 8 个平铺列 |
singer / song_singer |
歌姬档案与演唱关系;engine / voicebank 允许为空(只知道谁唱、不知道用什么声库是常态) |
lyrics |
歌词多版本:language + plain_text + 预留 ttml(逐字时间轴)/ lrc + is_translated |
tag / song_tag |
分类与情绪统一为标签(kind 区分,position 保序),分类支持 parent_id 成树 |
song_external_link |
网易云 / B站 / VOCADB 等多平台外链 |
song_annotation |
情绪标注时间戳 |
song_revision |
元信息修订记录(歌名 / 年份 / 简介变更时可回溯) |
兼容视图:songs 这个名字仍然可用,列名与顺序与旧 21 列完全一致,
所以 SELECT * FROM songs WHERE singers LIKE ? 这类既有语句无需改动。
视图用相关标量子查询实现(不是 GROUP BY 派生表),这样 WHERE id = ? 能下推到
基表索引 —— 实测单行读取 0.5ms,比派生表写法快约 30 倍。
视图只读:写入一律走 SongStore。若让 UPDATE songs 反写关系表,重建
song_singer 时会把单独存放的 engine 抹掉,所以不提供写代理。
旧库在插件启动时自动迁移(首次打开 SongStore 时完成),旧表改名为
songs_legacy_v1 保留以备回退,迁移过程逐字段校验零丢失,耗时约 1.7s、库体积增约 4%。
真机部署时迁移会在首次启动自动发生。稳妥起见可以先备份一份运行时曲库文件再重启。
除了原有方法,SongStore 新增了几个关系型查询:
| 方法 | 用途 |
|---|---|
credits_of(歌名) |
取某首歌的分工:{"lyricist": ["词作A"], "composer": ["曲作B"], ...} |
singers_of(歌名) |
取演唱者及其引擎/声库(未知为 None) |
songs_by_singer(歌姬) |
反查某位歌姬唱过的歌 |
songs_by_artist(创作者, 角色) |
反查某位创作者参与的歌,可限定角色 |
tags_of(歌名, kind) |
取分类或情绪标签(保序) |
role_breakdown() / stats() |
数据完整度体检 |
@API("get_recent_songs", version="1", public=True)
async def api_get_recent_songs(self, limit: int = 10) -> dict参数:limit(默认 10,钳到 1..100,最新在前)。
返回(结构永远完整的纯 dict,任何情况下不抛异常):
| 字段 | 类型 | 说明 |
|---|---|---|
schema_version |
int |
恒为 1 |
reason |
str |
空 = 数据正常;否则说明降级原因(见下) |
active |
bool |
插件已启用且 on_load 完成(记录已就绪) |
songs |
list |
[{"name": str, "artist": str, "at": float}],最新在前 |
reason 的取值:""(正常)/ 插件尚未完成启动或已停用(最近歌曲记录未就绪)
(active=false,不要用它的数据)/ 暂无歌曲命中记录。
数据来源:recent_songs.json——歌词命中那一刻登记一条(有界环形 30 条,
超出丢最旧),字段只有 name / artist / at。artist 取歌曲库 singers 列
(聚合的歌手/P主,形如 某P主、某歌手),库里查不到就是空串(不猜)。
写盘是节流的(首次立即落盘,此后默认 30s 窗口,卸载时强制落盘),
热路径上不做同步 IO;因此刚命中后的极短时间内,API 读到的可能是内存态中
尚未落盘、但已经存在的条目(API 读内存,不读文件)。
与既有内存态的区别:_hits 是会话内 TTL(注入上下文用,不落盘),
_recent_recommends 只记最近推荐(软排除用,不落盘)。本记录回答的是
"最近有人在群里聊到/点了哪首歌",跨重启保留。
纪律承诺(有测试守着):本 API 只读内存态——零网络、零写盘、不触发落盘
(tests/test_cross_plugin_api.py 断言调用前后文件 mtime/内容不变、
内存记录不变、dirty 标记不被制造)。调用方可按任意频率轮询。
本版同时抬高
sdk.min_version:2.0.0 → 2.1.0。理由:@API组件在 SDK 2.1.0 才引入(实测 2.0.0 / 2.0.1 的maibot_sdk没有API,2.1.0 起可用)。 此前声明的 2.0.0 是低估——本版开始真正用@API,按"声明要准"改为实际引入版本。
webui.json 声明一个声明式页面(协议 v2,declarative_basic),在 dashboard 的
**顶部工作区 tab「歌词曲库」**里用:
| 区块 | 内容 | 绑定的 API |
|---|---|---|
| 看板 | 曲库总数 / 缺歌词 / 歌词覆盖率 / 已标注情感 | lyrics.stats |
| 曲库状态 | 上次同步、爬取分类、待标注数、配置的单次批量 | lyrics.stats / lyrics.task |
| 后台任务 | 触发增量同步、批量补歌词、取消;进度条 + 已完成/新增/失败 | lyrics.task / lyrics.start_sync / lyrics.start_refetch / lyrics.cancel |
| 曲库检索 | 关键词 + 范围(歌名/歌手/UP主 或 歌词片段),点行看歌词预览 | lyrics.search |
| 缺歌词清单(默认收起) | 可按「跳过最近 N 天已确认无歌词」过滤 | lyrics.empty |
| 最近命中 | 群里聊到过的歌(recent_songs.json) |
lyrics.recent |
| 单曲档案 | 按歌名取完整档案(创作者分工 / 标签 / 歌词预览) | lyrics.song |
页面位置必须是 workspace:sidebar 页只在 dashboard 的「设置」工作区才进左侧栏菜单
(Layout.tsx:127-135 的过滤),用户停在别的插件工作区时整个消失 —— identity-guard
v1.1.0 真机踩过。顶栏最多直显 2 个工作区 tab,超出的在「更多」菜单里。
几个刻意的取舍(详见 docs/decisions-lyrics-webui.md):
- 触发不另做鉴权:
lyrics.start_sync/lyrics.start_refetch不看sync_admin_ids, 改由 dashboard 登录态把关;但尊重plugin.enabled与crawler.allow_sync_command—— 配置里写了「关闭同步命令」,页面上也一律拒绝。⚠️ 代价:能登录 dashboard 的人就能触发对外网的重任务。 - 页面不继承「0 = 不限」:命令侧
0是全量同步(首次建库需要),页面上误点一下就是 小时级任务,所以条数口径为「显式值 >sync_batch_limit> 保底 50」,夹到 500, 替换原因会在返回结果里说明。 - 单条写入不做:改歌词/改情感用
/歌词 重抓 <歌名>;页面的单条编辑要和导入链路、 标注覆盖规则对齐,边界多收益低。 - 条数参数在声明里是 string:声明式页面的输入框只能给字符串,声明成 integer 会被宿主 判不合规(422,前端只显示「插件 API 执行失败」)。数字转换与范围夹取在插件侧完成。
2026-10-17 优化轮(仍属 v2.12.0,细节见 docs/decisions-lyrics-webui.md):
- 页面补
poll_interval_seconds: 3——auto_refresh只管交互触发重查,没有它后台任务 进度不会自动刷新(前端seconds < 3直接不轮询);任务区新增「任务类型 / 已用时」。 - 输入框长 label 精简(协议没有 hint/placeholder,说明下沉为旁挂 text 节点)。
- 配置页:19 个字段改手工 label(自动推导的 12 字硬切会产出 dangling 括号 / 「的」收尾),
11 个格式敏感字段补 placeholder 示例,爬取/选歌/标注/联动四个配套节默认收起
(v2.13.0 起
crawler不再收起——它含管理员配置,收起等于藏起来)。
各版本的变更摘要与根因记录见 CHANGELOG.md。
要求:宿主 1.3.6+(协议 v2 起点;1.3.5 及更早会拒绝声明并连累整个插件加载失败),
且 dashboard 前端包含插件页面路由。@API 组件自 SDK 2.1.0 起可用(本插件已声明该下限)。
# 1. pytest 套件(tests/ 下的 WebUI 配置显示 + 跨插件 API 契约用例,需要 pytest)
pytest -q
# 2. 知识库结构回归(103 项断言,纯标准库,不依赖 MaiBot SDK)
python test_vcpedia_schema.py
# 放到 MaiBot 插件工作区里时也可以指定其它副本
python test_vcpedia_schema.py /path/to/cv_lyric_context
# 3. LLM 调用参数回归(7 项断言,纯标准库)
python test_annotate_llm.py覆盖:三种历史老库形态(7/18/21 列)的自动迁移、逐字段零丢失、公开 API 语义、
情绪标签排序、关系型查询,以及新版存储层独有能力(upsert(conn=) 连接复用、
open_writer、LIKE 通配符转义、情绪查询只返回 4 列)。
v2.9.0 门禁实测(2026-10-05):devkit
run_gatesPASS 2 / SKIP 0 / FAIL 0 (check_pluginPASS 35 / WARN 4 / FAIL 0、pytest 24 passed);三层自检 pytest 24 +test_vcpedia_schema.py103/103 +test_annotate_llm.py7/7 全绿。 本仓库没有tests/smoke_test.py(自检形态就是上面三条),插件中心自查器check_submission.py报 1 个 FAIL:命中vcpedia_client.py的ssl.CERT_NONE。 该代码是 v2.x 起就有的用户显式开关(crawler.verify_ssl,默认 true 即默认开启校验, 关闭时打 warning 日志),本版未改动它。全检加固(2026-10-05):命中记录的降级不再静默——取歌手失败、写盘失败、 加载期坏文件被备份重建,三处都会打 warning(带歌名/路径/原因), 免得"API 里 songs/artist 一直为空"在真机上查不出线索。
根目录的两个
test_*.py是脚本式自检,不是 pytest 套件。 它们用check()记录失败而不assert,被 pytest 收集会「断言全灭也显示通过」,所以用例函数 一律case_*命名(pytest 收集不到),pytest.ini也把默认范围收紧到tests/。 请按上面的命令用python跑,不要用pytest test_vcpedia_schema.py。
注意:本仓库除
test_vcpedia_schema.py外没有更上层的test_context.py全流程模拟测试——那个夹具在插件开发工作区里。改动核心逻辑(词库索引、 上下文注入、内存治理)后仍建议在开发工作区跑一轮全流程回归。
插件运行时(MaiBot 只加载 plugin.py,其余由它 import)
| 文件 | 职责 |
|---|---|
plugin.py |
入口:配置模型、生命周期、歌词识别与注入、/加歌 |
lyrics_import.py |
歌词文件收件箱的解析与导入(纯标准库,可单独测) |
vcpedia_mixin.py |
VCPedia 歌曲库能力:/歌词 系列命令 + 三个 LLM 工具(search_vcpedia_song、get_vcpedia_lyrics、recommend_cv_song) |
vcpedia_client.py |
Anubis PoW 解题 + MediaWiki API 取 wikitext |
vcpedia_sync.py |
同步流程:分类枚举、词条解析、入库、熔断 |
vcpedia_wikitext_parser.py |
wikitext -> 结构化创作信息 |
vcpedia_text_clean.py |
wiki 标记清洗({{color}}、{{ruby}}、<ref>、[[链接|文本]]) |
vcpedia_schema.py |
知识库结构:规范化实体表 + 角色字典 + songs 兼容视图 + 老库自动迁移 |
vcpedia_store.py |
歌曲库 SQLite 读写(对外 API 不变,内部写规范化表;含关系型查询) |
assets/knowledge_db.dbassets/song_lyric_keywords.txt |
内置歌曲元数据库与歌词关键词表(只读素材) |
辅助模块与自检脚本(运行时模块与维护脚本的边界见下表;脚本式自检用例一律 case_* 命名,
不被 pytest 收集,按 python <脚本> 跑而不是 pytest <脚本>)
| 文件 | 职责 |
|---|---|
test_vcpedia_schema.py |
知识库结构回归自检(103 项断言,纯标准库,脚本式;用例函数 case_* 命名,不被 pytest 收集) |
test_annotate_llm.py |
LLM 调用参数回归自检(7 项断言,纯标准库,脚本式) |
pytest.ini |
pytest 配置:默认只收集 tests/ 下的套件 |
singer_check.py |
歌手归属校验纯函数(推荐池过滤用,可独立单测) |
recent_songs.py |
最近命中歌曲的落盘记录(有界环形 + 节流原子写;纯标准库,可脱机单测) |
emotion_annotate.py |
情绪标注纯函数件:prompt 构造 + 标签解析(插件内同步标注用) |
VCPediaMixin 以多继承混入主类(class CVLyricContextPlugin(VCPediaMixin, MaiBotPlugin)),
实测 MaiBot SDK 能正确注册继承来的 @Command / @Tool,不用把代码复制进 plugin.py。
- 若你的 MaiBot 版本中
chat.receive.*系列 hook 不存在(老版本),插件会退回到ON_MESSAGE事件;若该事件在版本里也未派发,则无法识别入站消息,需要换用对应 版本的消息 hook(可看日志里打印的字段名确认)。 - 幂等保护:同一请求中若已注入过(内容含
【歌词识别】标记),重试时不会重复叠加。 规划器与回复器两条链路各自独立判重,互不影响。 - 注入覆盖两条链路:规划器(
maisaka.planner.before_request,影响工具调用决策) 与回复器(maisaka.replyer.before_request的extra_prompt,影响最终回复)。若某条链路 没有日志出现,说明该版本 MaiBot 未触发对应 hook 点。 - 回复器不改写
items/messages:自拼的 UserMessageItem 没有宿主框语义, 会被当成用户发言(v2.15.0 真机事故)。inject_position只影响规划器。 - 同一会话内相同文本 10 秒内重复到达只登记一次,避免两套监听重复计数。
- 同步是增量且可中止的:已入库的歌会跳过,
/歌词 取消可随时停。
MIT。VCPedia 解析与清洗逻辑参考自 mohobot(MIT License)。