Skip to content

About

面向QQ机器人RAG的中文Vocaloid歌词上下文数据集,为MaiBot提供歌词与歌曲元数据。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

37 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

中V歌词识别 · 歌曲库 (cv_lyric_context)

中文 VOCALOID(洛天依、言和、乐正绫等)歌词识别 + 歌曲库插件,两件事:

  1. 认歌词 —— 入站消息命中歌词时,向 MaiBot 的 LLM 上下文注入歌曲信息, 让 bot 能自然接住歌词话题
  2. 管歌库 —— 内置 VCPedia 爬虫,可以从 VCPedia 同步 4000+ 首中V歌曲(含作词作曲等完整创作信息)到本地 SQLite, 既能扩充歌词识别的词库,也能用 /歌词 命令或聊天直接查歌

词库有三个来源,可以叠加使用:

来源 数据 怎么来
内置基础库 3412 首旧快照,57227 句歌词 自带
VCPedia 同步 4000+ 首,含完整创作信息与歌词 /歌词 同步(后台爬取)
歌词文件 你自己放的歌 丢进 lyrics_inbox/(插件数据目录下),/加歌

目录

安装

纯标准库实现,没有任何第三方依赖(网络请求走 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+(用宿主自带的解释器即可)

步骤:

  1. 下载本仓库:git clone https://github.com/orge-8/cv_lyric_context.git, 或直接下载 zip 解压。
  2. 把整个 cv_lyric_context/ 目录放进 MaiBot 的 plugins/ 下, 目录名保持 cv_lyric_context——Runner 按目录名加载插件, 里面有 _manifest.json 和 plugin.py,不要只挑文件拷。
  3. 完整重启 MaiBot。插件目录的变动不会热加载,必须重启进程。
  4. 启动日志里出现 中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
/歌词 歌曲 普通DISCO

Category:洛天依歌曲 递归后约 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 起还会过滤日常聊天文本,五条规则(只看清洗后的键):

  1. 单字重复 ≥ 4 次("哈哈哈哈…"“啦啦啦啦…”):基础词库仅 26 个去重键,全是语气段;
  2. 2~3 字片段重复 ≥ 3 次("对不起对不起对不起""我不想我不想我不想""hahahahahaha"): 这类句子恰是吵架/撒娇/笑声刷屏常用语;
  3. ASCII 低信息键("emmmmmmmm"):整个键只由一两种字符构成;
  4. 命中日常高频用语表(约 390 条,精确匹配,不做子串):问候/致谢/道歉/祝福/ 告别/情感/疑问/请求/情绪/日常/网络十一类通用口语,如「生日快乐」「新年快乐」 「不好意思」「好久不见」「在吗在吗」「求求你了」「相信自己」「马上就好」。 这些句子在歌词库里也有零星出处(生日快乐 出自《Come Back》、不好意思 出自《鸽子》),但群聊里的出现率高几个数量级,宁可丢掉这几十句歌词的识别能力;
  5. 片段整倍重复且片段在用语表内("我要回家"×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 时也会自动 扫一遍收件箱,不一定要用命令。导入的歌立即生效,不用再重载。

/歌词、/加歌 没反应怎么办

命令发完什么都没发生,按顺序查:

  1. 看日志里有没有 命令执行成功: lyrics_xxx —— 没有说明命令压根没触发, 跳到第 2、3 条;有说明触发了但消息没发出去,接着看 回复发送失败 或 命令载荷里没有 stream_id。
  2. [E_CAPABILITY_DENIED] … 未获授权能力: send.text —— manifest 的 capabilities 没写对。必须写点分的精确能力名 send.text, 写 send_message 这种粗粒度名字无效。改完要重启 MaiBot(manifest 在插件 加载前校验,热重载不生效)。
  3. 重启 MaiBot —— 新增的 @Command 组件要重新注册,热重载插件不一定生效。
  4. 到 WebUI 的「Bot 配置 → 命令」里看 lyrics_status 等在不在列表里 —— 1.2.0 起插件命令统一在这里管理(可配置放行用户/聊天流),没出现就是没注册上。

/歌词 同步、/歌词 补歌词、/歌词 重抓 有反应但被拒绝,属另一类问题—— 权限没配好,见配管理员与「同步失败怎么办」的前三条。 命令只回了 False(静默拒绝)时,说明 crawler.allow_sync_command 被关了。

日志里出现命令执行成功但 回复发送失败 时,命令本身已经跑完(比如导入已完成, 会有 歌词文件已入库: …),只是结果没发出来;此时插件会退回让 bot 自己接一句话, 不会让你完全看不到反馈。

不依赖命令的退路:歌词文件放进 lyrics_inbox/(插件数据目录下)后重载插件或重启 MaiBot,加载时会自动导入,日志里会有 歌词文件已入库: …。

歌名怎么来的

按顺序取,取到为止:

  1. 文件名(去掉扩展名和 (1) 这类副本后缀)——推荐,最省事
  2. LRC 的 [ti:歌名] 标签
  3. 文件第一个非空行——该行会被当作歌名,不计入歌词

文件名是 lyrics / 歌词 / 新建文本文档 这类通用名时,自动跳到第 2、3 条。

歌手和 P 主从哪来

按优先级,取到为止:

  1. LRC 标签 —— [ar:歌手] 填歌手,[by:] / [au:] / [re:] 填 P 主
  2. 文件名约定 —— 见下节,库里没有的歌用这个手工指定
  3. 从基础库反查 —— 歌名命中 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)

同名歌不会重复添加,新歌词会合并进已有条目。

也可以直接编辑 user_songs.json(在插件数据目录下)

手改、批量改时用这个:

[
  {
    "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 日志。详见「公司网络 / 安全软件导致证书错误」。

VCPedia 同步

同步下来的数据怎么用

两处,自动的:

  1. 扩充歌词识别词库 —— 插件启动时,以及每次同步完成后,都会读取 data/vcpedia_songs.db,日志里出现 外部歌曲库 vcpedia_songs.db: 新增 N 首歌 / 歌词库: 同步后重建索引,新增 N 首进入识别词库。 之后这些新歌的歌词句也能被识别命中,歌手/P主自动带上,不需要重启 MaiBot。 重建是幂等的:已有的歌跳过,只有新增的进索引(已有歌词的更新不会重索引)。
  2. 直接查歌 —— /歌词 搜索 /歌词 歌曲,或聊天里直接问(走 LLM 工具)。

规则:

  • 基础库已有的歌名不重复索引 —— 它的歌词已经在 song_lyric_keywords.txt 里了, VCPedia 只补新歌。所以日志里的"新增 N 首"通常小于库里的总条目数,这是对的。
  • VCPedia 的歌手/P主不覆盖基础库已有的值,只补空缺。
  • 没同步过、库是空的,都只是跳过,不影响歌词识别。
  • 词库索引从 v2.6.2 起在后台线程构建完成后一次性换入,插件加载不再阻塞事件循环; 外部库的歌词也不全量常驻内存(见「性能与内存」一节)。

原曲 B站标识(v2.14.0 新增)

song 表的 bilibili_aid / bilibili_bvid 两列从 v2.8.0 起就在结构里,但一直空着 (抓取侧没有解析代码)。v2.14.0 补上采集,让点歌类插件能按原曲精确取流。

取数规则(顺序即优先级):

  1. 首个 {{VOCALOID_Songbox}} 的 |bb_id= —— 主信息框即原曲。 同一页面可能有多个 Songbox,后续的属于重制版或翻唱,不能取: 例《权御天下》第 2 个是洛天依官方账号的重制版,《达拉崩吧》第 2/3 个是翻唱;
  2. 无 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 首」,再发一次即继续。

歌名匹配层级(v2.14.1)

点歌插件传来的是用户原话,而库里歌名形态很杂——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 是这套规则的核心指标——宁可少命中,不能播错版本。

get_song_bilibili 的 singers 字段(v2.14.3)

接口额外返回原曲演唱者名单(如 ["洛天依"])。调用方靠它区分「用户点的是原曲还是翻唱」:

查询 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:传说曲"

注意两件事:

  1. 改完配置必须重启 MaiBot(或重载插件)。WebUI 保存配置只写文件,不会推送给 插件运行器——改完直接 /歌词 同步 跑的还是旧分类。同步开始的消息里会回显 当前分类,跑之前对一眼。
  2. 换分类是增量不是替换。已有的歌不会删,新分类的歌追加进同一个 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,Runner 自动生成)

在哪改——三种方式,改的是同一份 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])

键 默认 说明
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])

前三行就是「谁能触发重任务命令」的开关(可视化模式里排在这一页签最前面):

键 默认 说明
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] / [emotion] / [integration])

这三节在可视化模式里都默认收起——配一次基本不用再动。

键 默认 说明
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 注入里最多给出几条可点播的「歌名 歌手」查询串

点歌联动(v2.7.0)

配合点歌插件 [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 留空并依赖模型自行判断
播了但放错歌 检查歌曲库里的歌手字段是否为空(/歌词 歌曲 看「演唱」);歌手为空时查询串只有歌名,命中率自然下降

氛围选歌(v2.5.0)

给曲库加了情绪标签层后,bot 可以根据聊天氛围主动推歌:

  • 标签固定 7 个:甜美、温柔、积极、帅气、搞怪、伤感、愤怒(一首歌可多标签)。
  • LLM 对话中判断用户想听歌/氛围合适时,会自己调用 recommend_cv_song 工具 (传入情绪标签),工具内完成:标签匹配 → 随机抽取 → 最近推荐去重 (默认 10 首窗口内不重复)→ 歌手归属校验(合唱、含其他虚拟歌手的歌 不进推荐池;歌手字段为空的放行但标注「歌手未知」)。
  • 歌词识别注入行为完全不受影响——校验只作用于推荐环节。

同步后自动标注(v2.6.0,默认开启)

/歌词 同步 的结果消息发出后,插件会自动给待标注的歌打情绪标签:

  • 新歌优先:按 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

第二步:导出并填进配置

  1. Win + R → certmgr.msc 回车
  2. 展开「受信任的根证书颁发机构」→「证书」 (找不到就去「中间证书颁发机构」里再找一遍)
  3. 按「颁发给」排序,搜第一步拿到的那个名字
  4. 右键 → 所有任务 → 导出 → 选 Base64 编码 X.509 (.CER)
  5. 存好后在 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 指向的文件不存在或格式非法时,会自动回退到系统证书并记日志,不会让插件起不来。

性能与内存(v2.6.2,v2.10.0 增补)

一轮针对常驻内存与事件循环阻塞的优化,行为完全不变 (新旧代码索引逐条对比一致: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 起)

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(v2.9.0 新增)

get_recent_songs — 只读查询最近命中过的歌

@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,按"声明要准"改为实际引入版本。

dashboard 自定义页面(v2.12.0 新增)

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_gates PASS 2 / SKIP 0 / FAIL 0 (check_plugin PASS 35 / WARN 4 / FAIL 0、pytest 24 passed);三层自检 pytest 24 + test_vcpedia_schema.py 103/103 + test_annotate_llm.py 7/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.db
assets/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)。

About

面向QQ机器人RAG的中文Vocaloid歌词上下文数据集,为MaiBot提供歌词与歌曲元数据。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages