qqbridge-plus —— 舞萌DX 查分 + 聊天拓展
基于 DeepSeek Harness 与 qq-bridge 的插件:以水鱼(舞萌DX)查分为主,另附一些聊天拓展功能。
拆开说:
- 水鱼查分 —— 在 QQ 里
#查分查舞萌DX 成绩、出 B50 成绩图(带曲绘)。 走 OAuth 授权,不限管理员,也不依赖桥接:桥接没装好,查分照样能用。 - 聊天拓展 —— 把 qq-bridge 的运维能力接进 AI 的工具面:余额与自动关机、
推理档位随计费峰谷自动切换、节假日表、开机/关机提示词、
#功能开关。
English | 中文
装它能得到什么
装好以后,你可以直接问 AI"机器人余额多少""现在什么状态",不用再去翻日志;
群里的人也能发 #查分 查舞萌DX 成绩、拿到一张带曲绘的 B50 成绩图。
管理员在 QQ 里发一句 #功能 balance off 就能关掉某项查询能力,改完立刻生效,
不用编辑文件也不用重启。
具体包括:
| 查询 | 内容 |
|---|---|
| 运行状态 | 当前模式、DSH 是否就绪、白名单、管理员、每个会话的未读和唤醒情况 |
| 余额 | 余额多少、数据多久前更新的、告警阈值、真要关机还有多久 |
| 推理档位 | 现在用的是哪一档。分时段功能会在计费高峰切到便宜档、空闲切回好档,省点钱 |
| 节假日表 | 表还够不够用;缺年份的时候可以顺手去国务院通知里抓一份来试算 |
| 开机 / 关机提示词 | 哪些人格配了、会发到哪里 |
| 管理命令 | #余额 #关机 #功能 #帮助 的触发词和权限规则 |
| 舞萌DX 查分 | 在 QQ 里 #查分 查 B50、出成绩图(带曲绘);不依赖桥接,也不限管理员 |
| 桥接配置 | 配置内容,令牌已经打码 |
插件内部是怎么走的
计费峰谷、节假日解析、唤醒过滤、余额告警这些判定,插件里一个都没有重写,
全是直接 import qq-bridge 自己的纯模块。插件只负责取数、拼文本。
qqbridge-plus ──import──▶ qq-bridge/src/wake-filters.mjs (计费峰谷 / 节假日 / 唤醒过滤 / 余额决策)
──import──▶ qq-bridge/src/whale-balance.js (余额账本读取)
──HTTP────▶ qq-bridge 控制台 127.0.0.1:3100 (运行态:模式、会话、生效档位)
◀──HTTP──── qq-bridge 转发来的 QQ 管理命令
这么做是因为 qq-bridge 才是 QQ 侧的权威:SnowLuma 连接、白名单、会话映射、计时器
都在它进程里。判定逻辑再写一份出来,两份迟早会对不上。开发时真撞上过一次:
余额读到 NaN 的时候,一边判成 recovered、另一边判成 none,
而这一步会直接决定要不要关机。功能键的清单同理,桥接那边不另存一份,
否则插件加了功能、桥接反而先把合法命令拒掉。
桥接没在跑的时候插件不会报错,工具会告诉你"控制台不可达"以及怎么排查。
怎么装
1. 前提
| 需要 | 说明 |
|---|---|
| Node.js ≥ 18 | DSH 与 qq-bridge 都要求它 |
| git | 用于克隆本仓库(手动安装可以不装,直接下 zip 也行) |
| DeepSeek Harness | 插件宿主。必须先启动过一次,否则 profile 目录还不存在 |
| qq-bridge | 本插件直接 import 它的纯模块并调它的端点,必须先装好 |
qq-bridge 的位置不用提前告诉安装器:它默认去插件目录的隔壁找
qq-bridge, 找不到时用--bridge-dir指定。
2. 安装
方式 A:自动安装(推荐)
先干跑看一眼它要改什么(第一次建议这么做):
node "%USERPROFILE%\dsh-plugins\qqbridge-plus\scripts\install.mjs" ^
--dir "%USERPROFILE%\dsh-plugins\qqbridge-plus" --dry-run
确认没问题就去掉 --dry-run。完整流程(克隆 + 安装):
Windows(cmd 或 PowerShell)
git clone https://github.com/xueyingneko/qqbridge-plus.git "%USERPROFILE%\dsh-plugins\qqbridge-plus"
node "%USERPROFILE%\dsh-plugins\qqbridge-plus\scripts\install.mjs" --dir "%USERPROFILE%\dsh-plugins\qqbridge-plus"
Linux / macOS / Git Bash
git clone https://github.com/xueyingneko/qqbridge-plus.git ~/dsh-plugins/qqbridge-plus
node ~/dsh-plugins/qqbridge-plus/scripts/install.mjs --dir ~/dsh-plugins/qqbridge-plus
qq-bridge 不在隔壁时:
node "...\scripts\install.mjs" --dir "...\qqbridge-plus" --bridge-dir "D:\path\to\qq-bridge"
要装进 web profile 而不是 desktop:加 --profile web。
已经在仓库目录里,也可以直接用包装脚本(它会自动认出当前目录):
scripts\install.cmd --dry-run
安装器做的事,每一步都幂等,重复跑不会重复写入:
- 克隆仓库,或对已有仓库执行
git pull; - 在插件目录装唯一的 npm 依赖
@deepseek-ai/schemastery; - 把包注册进 profile 的
package.json:dependencies加link:项,dsh.profile.bundles加包名; - 往 profile 的
cordis.patch.yml追加插件 entry(已经存在就跳过); - 让包管理器在 profile 里建好
node_modules链接(优先 pnpm,以匹配 profile 自己的 lockfile); - 检查 qq-bridge 是否就位,并打印后续步骤。
改 profile 文件之前会先备份成 package.json.bak-<时间戳>,写入用临时文件加 rename,
避免写一半被别的进程读到。
卸载:从 profile 的 package.json 里删掉 dependencies 与 dsh.profile.bundles 中的
dsh-plugin-qqbridge-plus,再删掉 cordis.patch.yml 里带
# === dsh-plugin-qqbridge-plus (managed by install.mjs) === 标记的那条 entry,重启 DSH。
方式 B:手动安装
要做精细控制(例如自己管软链接、或多 profile 同时装)就手动来。
第一步,加入 profile:把包加进 ~/.dsh/profiles/<profile>/package.json 的
dependencies(用 link: 指向本仓库),同时加进 dsh.profile.bundles:
{
"dependencies": {
"dsh-plugin-qqbridge-plus": "link:/abs/path/to/qqbridge-plus"
},
"dsh": {
"profile": {
"bundles": ["...", "dsh-plugin-qqbridge-plus"]
}
}
}
然后在 profile 目录跑一次 pnpm install(或 npm install)把链接建出来。
第二步,在 profile 的 cordis.patch.yml 加一条 entry:
- id: qqbridge-plus
name: dsh-plugin-qqbridge-plus
config:
bridgeDir: '/abs/path/to/qq-bridge' # ← 改成你的 qq-bridge 路径
consoleBase: 'http://127.0.0.1:3100'
personaSection: true
sectionOrder: 7
features:
status: true
balance: true
schedule: true
holidays: true
greeting: true
commands: true
config: true
id 必须写 qqbridge-plus(DSH 用 entry id 定位设置的命名空间),name 必须写包名。
第二步不能省。包内自带的 cordis.patch.yml 只描述"这个 bundle 有哪些行",
真正让 loader 建出 entry 的是 profile 层的 patch。只做第一步的话,
插件管理器会报 installed=true、行配置也齐全,但工具根本不存在,而且没有任何报错。
第三步,重启 DSH。
3. 验证装上了
重启 DSH 之后,任选一种确认:
问 AI"机器人状态",它应该能调到
qqbridge工具;或看桥接目录下的
state/plugin-apply.json,里面有本次装配的trace:{ "trace": [ { "phase": "apply-called" }, { "phase": "tool-registered" }, { "phase": "http-api-registered" }, { "phase": "apply-returned" } ] }只到
apply-called就没了,说明装配中途失败;完全没有这个文件,说明插件没被加载。
4. 让 QQ 侧也能改开关(可选)
在 qq-bridge 的 config.json 里加:
"featureCommand": {
"triggers": ["#功能", "#开关"],
"baseUrl": "http://127.0.0.1:19387"
},
"helpCommand": { "triggers": ["#帮助", "#help"] }
baseUrl 指向 DSH web,插件端点挂在它上面。改完重启 qq-bridge。
在 QQ 里使用
管理命令(仅管理员)
#帮助 列出全部管理命令(触发词按当前配置渲染)
#功能 查看全部开关(标 * 的是在 QQ 里改过的)
#功能 balance off 关闭某个功能(也接受 关 / 禁用 / off / 0)
#功能 balance 开 开启(也接受 启用 / on / 1)
#功能 reset 清空运行时覆盖,回到配置文件的值
- 触发词可配(
featureCommand.triggers/helpCommand.triggers)。 - 帮助文本里的触发词是从配置读出来拼的,所以改过触发词之后它也会跟着变, 不会给你一份对不上号的说明。
- 只有管理员(qq-bridge 的
ownerQQ)能用;没配ownerQQ时这些命令对谁都不可用。
查分(所有人)
#查分 这台机器上查分能不能用
#查分 柚子 直接写用户名 = 查公开 B50
#查分 b50 柚子 同上,显式写子操作
#查分 b50 查我自己(需先完成绑定)
#查分 b50 qq 12345678 查指定 QQ(对方需先绑定)
#查分 song 11823 查某首歌的定数
#查分 search ztn 模糊搜曲,支持简写
#查分 bind 发起绑定,授权链接**私聊**发你
#查分 confirm 123456 用确认码收尾绑定
- 不限管理员——谁都能查自己的成绩。但"查自己的"必须先完成一次绑定, 而绑定要求你本人在水鱼页面上点「同意授权」。
- 触发词可配(
maimaiCommand.triggers,默认#查分/#maimai)。 - 句首的全角空格、零宽字符会被自动清掉——它们肉眼看不出,却足以让
#查分匹配失败。 bind/confirm的链接与确认码只走私聊,群里最多留一句"已私发"。 这不是多余的谨慎:链接谁都能转发,确认码只出现在点同意那个人的浏览器里。
AI 能调什么
一个工具 qqbridge,10 个 action:
| action | 作用 |
|---|---|
status |
总览:模式 / 就绪 / 管理员 / 白名单 / 余额 / 档位 / 各会话 |
balance |
余额、观测新鲜度、阈值、自动关机倒计时 |
schedule |
计费峰谷与档位;testAt=[时间戳…] 可试算任意时刻 |
holidays |
节假日表覆盖;fetch=true 现场抓取解析试算(不落盘) |
greeting |
开机 / 关机提示词配置、目标、各人格卡覆盖 |
commands |
#余额 #关机 触发词与权限口径 |
config |
桥接配置摘要(令牌已脱敏);key="role.balanceAlarm" 取子段 |
features |
功能开关:查看,或用 set/enabled 修改,或 reset |
maimai |
舞萌DX 查分(见下一节) |
firstRun |
重看首次运行引导 |
舞萌DX 查分
接入水鱼查分器。这部分与 qq-bridge 无关——桥接没装好也能用。
qqbridge action=maimai, sub=status 这台机器上查分能不能用
qqbridge action=maimai, sub=b50, username=某人 公开查询(对方需自行公开成绩)
qqbridge action=maimai, sub=b50, qq=<QQ号> 查自己的(需先完成 OAuth 绑定)
qqbridge action=maimai, sub=song, songId=11823 查某首歌的定数
qqbridge action=maimai, sub=search, query=ztn 模糊搜曲(支持简写)
qqbridge action=maimai, sub=bind, qq=<QQ号> 发起绑定,拿到授权链接
qqbridge action=maimai, sub=confirm, qq=<QQ号>, code=<确认码> 用确认码收尾绑定
sub=b50 默认会出一张成绩图(1080 宽 PNG),写在系统临时目录下并把路径交给你;
不想要图就加 image=false。
为什么不支持"按 QQ 号随便查"
水鱼已经弃用开发者 token(DIVINGFISH_TOKEN)。原因是它能按 QQ 号读取任意用户的成绩,
而那些用户从未对机器人授权、也无法撤销——用它做出来的"查分",本质是一个无需同意就能
窥探他人成绩的工具。所以本插件只实现 OAuth:
| 环节 | 做法 |
|---|---|
| 机器人持有的凭据 | 只有你自己申请的应用 clientId / clientSecret |
| QQ 号 | 只以 sha256("<clientId>:<QQ号>") 摘要外发,号码本身不离开机器人 |
| 用户令牌 | 只在内存缓存(5 分钟,提前 30 秒失效),不落盘 |
| 绑定 | 用户发 bind 拿到链接 → 本人点「同意授权」→ 把页面给的一次性确认码发回来 |
| 撤销 | 用户随时可在 https://auth.diving-fish.com/apps 撤销 |
| 未绑定用户 | 仍可用公开查询(按用户名) |
那个确认码不是多余的手续:绑定链接谁都能转发,而确认码只出现在点同意那个人的浏览器里。
少了它,别人可以拿你的 clientId 造一条链接发给受害者、骗对方授权从而绑上别人的账号。
配置
maimai:
clientId: '你的应用 ID' # https://auth.diving-fish.com/apps 申请
clientSecret: '你的应用密钥'
proxy: false # 境外服务器可开
申请应用时,接入方式选「bot / 工具」(设备码绑定 + 换票,无需回调地址)——这正是本插件的流程。
部署形态选「由你自己部署运行」(机密客户端,用 client_secret);
需要勾的权限只有三项(全部只读):
| 权限 | 为什么需要 |
|---|---|
profile |
账号基本信息 |
prober.profile.read |
Rating、姓名框等汇总资料 |
prober.records.read |
B50 的逐条成绩记录(少这项查询会 403) |
不要勾任何 write 权限——本插件没有任何写入路径。而且水鱼会因此把申请转人工审核,
只会拖慢自己。chunithm.*(中二节奏)也与本插件无关。
留空时插件不会静默失败,而是明确告诉你去申请(sub=status 会报告当前状态)。
成绩图依赖 @napi-rs/canvas
安装器会自动装上(第 2 步),装完还会实测渲染一张 PNG 才报成功——不只看"命令返回 0"。 之所以要实测:它是原生模块(N-API 预编译),在少数平台/架构上可能装到却加载失败, 只报"install 成功"会让人以为出图可用,直到真去查分时才发现。
手动装也一样:
cd <插件目录> && npm install
万一它没装上(或平台不支持),插件其余部分照常可用——查分、搜曲、开关都不受影响,
只是不出图,sub=status 会明确告诉你去装什么。这是刻意的:画图是附加价值,
不该让主功能一起挂掉。
依赖只有这一个。
@deepseek-ai/schemastery(Config 校验用)刻意不列为依赖: 它由 DSH 宿主提供,插件代码会用多路径回退去宿主 profile 里找,找不到还能降级成Config = undefined(跳过校验),本机实测两条路都能跑。把它列成依赖会连带拉进 cosmokit 等一整套,安装从数十秒变成数分钟,而且万一它被装进某个与宿主共享的目录, 还可能干扰宿主依赖树(这个坑真踩过)。
曲库搜索
搜索用子序列匹配,所以简写也能命中:ztn → Zitronectar。排序按匹配质量
(完全相等 > 前缀 > 包含 > 艺人 > 子序列),查询短于 3 个字符时不走子序列兜底,避免误报。
关于曲绘
成绩图的歌曲封面是 SEGA 的游戏素材,本仓库是 MIT,不能把它打包进来。所以这里的 做法是:素材由你自己准备,插件只负责读。没配就没有封面,卡片退回"曲名推导出的稳定色条" (同一首歌颜色恒定),功能不受影响。
配好之后效果:一屏 50 张卡的封面。实测对真实 B50 命中 49/50。
怎么弄到曲绘
用一个命令从公开的国服曲数据库同步到本机:
#查分 sync 下载到 maimai.jacketDir 指定的目录
=== 曲绘同步 ===
目录:E:/qqbridge-plus/jackets
曲数据库共 1293 首,本地已有 0 张
本轮下载 52 张(3.1 MB)
还差 1241 张,再执行一次即可继续(增量,已下载的不会重下)。
(GitHub 接口额度 6/60,每小时重置)
那个曲数据库没有声明许可(未声明即保留所有权利)。所以我既没有把它打包进本仓库, 也没有替你分发——下载动作由你自己的机器发起、文件落在你自己的磁盘上,本插件只是读取方。 需要商用或再分发时,请自行确认素材授权。 来源:https://github.com/CrazyKidCN/maimaiDX-CN-songs-database
要有耐心:GitHub 未认证接口每小时只有 60 次请求,而曲数据库有 1293 首。一次下不完, 所以同步是增量可续的——反复执行即可,已下载的不会重下。想让它自己慢慢下完,可以挂 常驻进程(它会自己按额度重置时间等待,而不是空转):
node scripts/sync-jackets.mjs --dir E:/qqbridge-plus/jackets
两个实测过的坑,都写进代码注释了:①
raw.githubusercontent.com在部分网络下恒返回 502,所以走api.github.com;② Node 直连 GitHub 可能报UNABLE_TO_VERIFY_LEAF_SIGNATURE(系统证书库里有一层 Node 自带 CA 集不认的链),插件会在同步时自动合并系统 CA, 无需加命令行参数。
目录布局
文件名含曲目 id 最可靠(11576.png);也认 by-title/<曲名>.png(同步器用的就是这种)。
匹配不区分大小写——曲数据库里同时存在 Trust 与 TRUST,在 Windows 上它们是同一个
文件、在 Linux 上是两个。
开关
每个功能一个布尔开关,false 表示关闭,支持 '*' 兜底,默认全开。
生效优先级:
QQ / 运行时覆盖文件 > 组合行配置 > 代码默认(全开)
也就是"在 QQ 里改过的"压过"配置里写的"。覆盖写在 qq-bridge/state/plugin-features.json,
用临时文件加 rename 的方式原子写入,避免另一个进程读到半个 JSON;DSH 重启后仍然保留,
#功能 reset 可以清空。状态文件损坏时按"没有覆盖"处理并记录原因,不会让插件起不来。
八个键,默认全开:
| 键 | 对应能力 |
|---|---|
status |
总览 |
balance |
余额与自动关机 |
schedule |
计费峰谷与推理档位 |
holidays |
节假日表 |
greeting |
开机/关机提示词 |
commands |
管理命令现状 |
config |
桥接配置摘要 |
maimai |
舞萌DX 查分 |
关闭之后的表现
action 仍然留在枚举里,调用时返回一句明确的话,而不是把 action 删掉让人猜:
⛔ 功能「余额与自动关机」已在插件配置里关闭。 当前开启的功能:status(总览)、schedule(计费峰谷与推理档位)…不从枚举里删掉的原因:工具 schema 在
defineTool()时就序列化好了,之后无法随配置增删。 与其造成"配置改了但枚举没变"的假象,不如让关闭状态看得出来。status里对应的段落会写明原因:⛔ 已关闭(features.balance=false)。系统提示词按开关生成,已关闭的功能不会出现在里面,免得模型白白尝试。
开关名拼错会告警,并写进
plugin-apply.json的 trace。这类开关最容易出的问题就是 把schedule写成scheduler,看起来"设了却没生效"。
三条硬规则
都是开发时踩出来的,改代码前建议先读一遍。
1. inject 里要列出所有会被访问的服务
Cordis 规定访问未在 inject 中声明的服务属性会直接抛错:
Error: cannot get property "systemPrompt" without inject
最初只声明了 tools,想用 if (ctx.systemPrompt) 做可选依赖兜底,
结果读取属性这一步就抛错了,兜底代码根本执行不到。而这类错误在插件管理器里
只表现为 [failed] 或 "did not activate",不显示原因。
2. 不要导出 Config,除非它是真正的 schemastery schema
loader 看到 Config 导出会拿它做校验,并且期望它来自 @deepseek-ai/schemastery。
给一个普通对象会在 apply 之前就失败(现象是 entry 建出来了、apply 一行没跑)。
而 schemastery 是裸包名依赖,插件目录如果没自己的 node_modules 就解析不到,
整个模块会在导入期加载失败。
还有一条实测出来的边界:volatile 字段会让 loader 把配置求值成 {}。
DSH 设置页要可写就要求字段带 volatile,但实测(原始证据留在 lib/last-apply-config.json)
loader 求值后所有带 volatile 的字段都变成了 {},于是 sectionOrder 不再是数字,
order must be a finite number 直接让插件不出现;就算绕过这一点,
在设置页点"保存"也会把配置写坏。所以本插件的设置面板做成只读,
改开关交给 QQ 命令和配置文件。测试里有一条断言守着这件事。
3. defineTool 要用模块顶层的静态导入
lib/index.js 里用的是写死绝对路径的静态导入,和官方插件同一种做法。
最初为了"稳妥"改成运行时动态 import() 加 7 条回退路径,结果出现了最难查的一种失败:
某条路径报了 import 成功,但链上的 .then 永远不执行——不报错也不进 catch——
工具就这么一直没注册上。
纯 node 子进程读不到 app.asar 内部,所以这个模块没法在普通 node 里导入。
这是 Electron 的 asar 特性,不是缺陷;测试请用 test/ 下不导入该文件的用例。
排查用的自检:装配过程会写 qq-bridge/state/plugin-apply.json,里面的 trace 数组
记录了走到哪一步。宿主 console 在 GUI 里看不到,loader 启动失败又不给原因,
这个文件是唯一不用猜的判据。
版本配套
插件和 qq-bridge 属于跨仓库依赖。qq-bridge 启动时会报出两边的版本:
[bridge] 版本:qq-bridge 0.2.0
[bridge] 插件:dsh-plugin-qqbridge-plus 1.1.1
两边都只以各自 package.json 的 version 为准,不另外维护一份。
这条也是踩出来的:曾在一个模块里硬写了版本常量,和 package.json 对不上,
等于没有可信的版本号。
测试
npm test # 插件侧全部 7 套:190 项
npm run test:maimai # 只跑舞萌查分的 4 套:99 项
node ../qq-bridge/scripts/test-feature-command.mjs # 桥接侧纯函数:28 项
node ../qq-bridge/scripts/e2e-feature-command.mjs # 端到端:10 项(需两端都在运行)
CI 只跑不依赖 qq-bridge 的那几套,需要桥接在跑的两套留在本地跑—— 写一个必然失败的 CI 不如没有 CI。
舞萌查分的测试用依赖注入拦住了网络,所以不需要任何凭据就能跑; 成绩图那套还会在"没装画图库"时自动改测降级路径(7 项),装了就测完整路径(21 项)。
几条关键的安全与正确性断言:
| 断言 | 为什么重要 |
|---|---|
config 摘要不得含未脱敏的 accessToken |
模型上下文会长期保存 |
| 拼错的开关键不得关掉任何功能 | "设了没生效"最难查 |
任何字段都不得带 volatile |
会让 loader 把配置求值成 {},保存即损坏 |
| 端点无令牌时必须拒绝或不注册 | 它能放宽整个工具面 |
#功能xyz 不得被当成命令 |
误判会吞掉普通发言,表现为"机器人忽然不理人" |
源码里不得出现 developer-token |
它能按 QQ 号读任意人成绩,用户无法撤销 |
绑定请求必须带 handoff=code,且请求体不含明文 QQ 号 |
少了确认码就能被转发链接骗授权 |
展示打码、client_secret 不得出现在任何输出里 |
输出会进模型上下文与 QQ 聊天记录 |
出图路由必须挡掉 ../ 与非 .png |
否则变成任意文件读取 |
| 出图失败不得让查分本身失败 | 图是附加价值 |
许可
MIT。第三方组件与合规边界(含 SnowLuma 许可说明、数据出机清单)见 LICENSE 后半部分。
No comments yet. Be the first to write one.