Repository navigation
Releases
v1.1.1
v1.1.1
成绩图现在带歌曲封面;修掉三个"看着成功其实没生效"的问题。
本代码由 AI 生成。非常感谢 Derpyu520 开发了
qq-bridge——没有它这个插件没有任何意义。
查分部分的接口契约与 OAuth 设计参照 Yuri-YuzuChaN/maimaiDX(MIT)。
曲绘数据来自 CrazyKidCN/maimaiDX-CN-songs-database(未声明许可,由使用者自行下载,本仓库不分发)。
新增
成绩图带曲绘
曲绘是 SEGA 的游戏素材,本仓库是 MIT,不能打包。所以做法是:素材由你准备,插件只负责读。
没配就退回"曲名推导出的稳定色条",功能不受影响。
配好之后一屏 50 张卡的封面,实测对真实 B50 命中 49/50。
一个命令拿到素材:
#查分 sync
从公开的国服曲数据库同步到 maimai.jacketDir。要有耐心:GitHub 未认证接口每小时只有
60 次请求,而曲数据库有 1293 首,一次下不完。所以同步是增量可续的,反复执行即可;
也可以挂常驻进程让它自己下完:
node scripts/sync-jackets.mjs --dir E:/qqbridge-plus/jackets它会按额度重置时间自己等待,而不是空转。
两个目录可配
maimai:
jacketDir: 'E:/qqbridge-plus/jackets' # 曲绘目录(留空=不带封面)
imageDir: 'E:/qqbridge-plus/images' # 出图目录(留空=系统临时目录)imageDir 值得单独说:成绩图按内容哈希命名、不自动清理,长期跑会一直堆。
默认位置在系统临时目录,也就是系统盘——本机实测 C 盘只剩 3.8GB 而其它盘各有 100GB+。
让使用者能指到大盘,比事后清理现实得多。
修复
① 图生成了却从没发出去
桥接读的是响应里的 data.imageFile 字段,而插件的 /qqbx/action 端点只回 { ok, text }
——那个字段恒为 undefined,于是发图逻辑直接返回。
症状极具误导性:日志显示命令成功、文本正常送达、出图目录里躺着一个新鲜的 PNG,
但没有任何图片相关日志(成功和失败都没打),因为代码根本没走到 try 块。
改为从文本里取路径(插件本来就把路径写在文本里)。加结构化字段要同时改
runAction → /action 路由 → 桥接三处,为一个路径重构不值得。
② 查自己的成绩读不到数据
userB50 走的是 GET /player/records——那个端点返回扁平 records 数组(实测 863 条),
没有 charts 字段,而渲染器和文字摘要读的都是 charts.sd / charts.dx。
于是 863 条成绩一条都读不到,Rating 却是对的。这种"一半有值一半为零"最容易让人误判成
"这账号没上传成绩"。
而且还顺手自己按 ra 排序算 B50,口径也对不上:前 50 的 ra 之和 = 13149,而官方
rating = 13278,差 129。反推了八种切分没有一种相等——那些原始成绩里根本重算不出
服务端的 B50。
正路是 POST /query/player + { b50: true }(授权模式多带一个 bearer 令牌),B50 由
服务端算好放在 charts.sd / charts.dx 里。实测 sd=35 + dx=15,且前 50 的 ra 之和
恰好等于 rating。
③ 同步器卡在 1291/1293 永不归零
两个叠加原因:数据库里有曲名完全重复的条目;还有只有大小写不同的条目
(实测同时存在 Trust 与 TRUST)。后者在 Windows 上是同一个文件、在 Linux 上是两个
——只在 Windows 上测表现为"永远差 1",换到 Linux 会真的漏下一张。所以去重按
不区分大小写做,两个平台行为一致。
安全改进
绑定链接只走私聊
授权链接的语义是"为你自己的账号授权"。若发到群里,任何人点开、看到页面显示的绑定身份、
点同意,就把他自己的账号绑到了发起者名下——正是确认码机制要防的转发攻击,
只不过这次是机器人自己把链接放进了群里。所以 bind / confirm 无论从哪发起都只发私聊,
私聊失败绝不退回群发。
不泄漏本机目录结构
出了图之后,文本里那个本机路径对用户就没用了,却会暴露你电脑的目录结构。
现在桥接在转发给 QQ 前会把路径剥掉;工具面保留(模型需要它才能自己发图)。
这里踩过一个误伤,比漏剥严重得多:首版判据写成
[A-Za-z]:[\\/],把 URL 里的
https://当成了盘符路径(s:+//),于是绑定命令的授权链接整行被删掉
——用户拿不到链接,而日志里一切正常。修法是限定盘符为单个字母并加前后视。
教训:误伤 > 漏剥。配了 13 项独立测试专门守"不该动的一个字都不许动"。
安装
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"安装器会自动装好成绩图依赖,并在装完后实测渲染一张 PNG 才报成功——不只看"命令返回 0"
(N-API 预编译包在少数平台可能装到却加载失败,那种失败暴露得太晚)。
OAuth 应用申请、#查分 各子命令、桥接补丁等详见 README。
桥接侧有几处本地改动(出图通路、命令入口、强制私聊、路径剥离),不在 qq-bridge 官方版本里,
升级时请对照 docs/ 下两份补丁文档重新打上——其中 cfg.maimaiCommand 必须显式构造那一处
漏了会完全静默(命令毫无反应、日志无线索)。
版本与校验
| 包名 | dsh-plugin-qqbridge-plus@1.1.1 |
| 文件 | dsh-plugin-qqbridge-plus-1.1.1.tgz(33 个文件,125.8 KB / 解压 347.7 KB) |
| SHA-256 | 90b58e9a80625d0139f3ce79367aa1bd9b205704d416581b72b04ca66866a0e3 |
| integrity (npm) | sha512-llckI7OjltfkjmLmBuQwG5lDqRRrwmZc2eHJ5eJaPqPoLlyvCBYsrRP9YejcedSk1UIJUeUNkYfRqd9jtSMkHQ== |
| shasum (npm, SHA-1) | 0470b12d7078bd89d60fd36b325914b8537c36d9 |
别混:npm 的
shasum是 SHA-1,校验下载完整性请用上面的 SHA-256。
测试
| 套件 | 项数 |
|---|---|
插件侧(npm test,7 套) |
211 |
| 桥接侧纯函数 | 69 |
| 端到端(需两端运行) | 10 |
| 舞萌查分的测试用依赖注入拦住网络,不需要任何凭据就能跑。成绩图那套会在"没装画图库" | |
| 时自动改测降级路径(7 项),装了就测完整路径(21 项)——所以它在干净环境和本机都能通过。 | |
| 真实验证过:公开 B50 接口、曲库 1404 首、曲绘命中 49/50、真实渲染 1554 KB PNG、 | |
| 经 OneBot 网关把图发到 QQ(retcode 0)。 |
已知边界
- 曲绘需自行准备(版权原因,本仓库不分发)。未配则卡片只有色条。
- 未绑定用户只能用公开查询(按用户名,需对方自行公开成绩)。
- 成绩图不自动清理。放系统盘要留意余量,建议把
imageDir指到大盘。 - 桥接侧改动不在官方 qq-bridge 里,升级需按
docs/重新打补丁。
许可
MIT。第三方组件与合规边界
见 LICENSE 后半部分。
v1.1.1 — English
Score images now include song jackets; fixes three cases where things looked like they worked
but silently did not.
This code was generated by AI. Many thanks to
Derpyu520 for building
qq-bridge — without it this plugin would have no
reason to exist. The score-lookup part follows the API contract and OAuth design of
Yuri-YuzuChaN/maimaiDX (MIT).
Jacket data comes from
CrazyKidCN/maimaiDX-CN-songs-database
(no license declared; downloaded by the user, not redistributed here).
Added
Song jackets on score images
Jacket art is SEGA game material and this repository is MIT, so it cannot be bundled. The approach
is therefore: you supply the assets, the plugin only reads them. Without them, cards fall back
to a stable colour strip derived from the song title; nothing else changes.
With assets in place you get all 50 cards illustrated. Measured against a real B50: 49/50 hits.
One command fetches the assets:
#查分 sync
It syncs from a public CN song database into maimai.jacketDir. Be patient: GitHub's
unauthenticated API allows 60 requests per hour and the database has 1293 songs, so one run cannot
finish. Sync is incremental and resumable; run it repeatedly, or start the persistent process
and let it finish on its own:
node scripts/sync-jackets.mjs --dir E:/qqbridge-plus/jacketsIt waits for the quota to reset rather than busy-looping.
Two configurable directories
maimai:
jacketDir: 'E:/qqbridge-plus/jackets' # jackets (empty = no artwork)
imageDir: 'E:/qqbridge-plus/images' # render output (empty = system temp)imageDir deserves a note: score images are named by content hash and are never auto-deleted,
so they accumulate. The default is the system temp directory, i.e. the system drive — on the
machine tested here, C: had only 3.8 GB free while other drives had 100 GB+. Letting the user point
at a large drive is far more practical than asking them to clean up later.
Fixed
1. The image was rendered but never sent
The bridge read a data.imageFile field that the plugin's /qqbx/action endpoint never returns
(it only sends { ok, text }), so it was always undefined and the send path returned early.
The symptom is thoroughly misleading: the log shows the command succeeded, the text arrives, and a
fresh PNG sits in the output directory — yet there is no image-related log line at all (neither
success nor failure), because execution never reached the try block.
Fixed by extracting the path from the text (the plugin already puts it there). Adding a
structured field would mean changing runAction → the /action route → the bridge in three places;
a refactor is not worth it for one path.
2. Looking up your own scores returned nothing
userB50 used GET /player/records, which returns a flat records array (863 entries
measured) with no charts field — while the renderer and text summary both read
charts.sd / charts.dx. So all 863 scores were invisible, yet Rating was correct. That
"half populated, half zero" pattern is exactly what makes someone conclude the account has not
uploaded any scores.
It also recomputed B50 by sorting on ra, and that formula was wrong too: the sum of the top 50
ra values was 13149 while the official rating was 13278 — off by 129. Eight different splits
were tried and none matched, meaning the server's B50 cannot be recomputed from those raw records.
The right call is POST /query/player with { b50: true } (with a bearer token in OAuth mode);
the B50 is computed server-side into charts.sd / charts.dx. Verified: sd=35 + dx=15, and the
top-50 ra sum equals the rating exactly.
3. The syncer stuck at 1291/1293 forever
Two overlapping causes: the database has entries with identical titles, and entries differing
only in letter case (both Trust and TRUST exist). The latter is one file on Windows but
two files on Linux — testing only on Windows shows "always one short", while on Linux it would
genuinely skip a jacket. Dedup is therefore case-insensitive so both platforms behave the same.
Safety improvements
Binding links are private-message only
An auth link means "authorize your account". Sent to a group, anyone there could open it, see the
binding identity shown on the page, and consent — binding their own account to whoever started
the command. That is exactly the forward-the-link attack the confirmation code exists to prevent,
except the bot would have put the link in the group itself. So bind / confirm are always sent
privately regardless of where they are issued, and a failed private send never falls back to the
group.
No local path leakage
Once the image is sent, the local file path is useless to the user but exposes your machine's
directory layout. The bridge now strips it before forwarding to QQ; the tool surface keeps it
(the model needs it to send the image itself).
A false positive here was far worse than under-stripping: the first version used
[A-Za-z]:[\\/], which treated thes:ofhttps://as a drive letter, so the binding
command's entire auth link line was deleted — the user got no link while the log looked fine.
Fixed by restricting the drive letter to a single letter with lookarounds.
Lesson: a false positive beats under-stripping in severity. 13 dedicated te...