The rkey fallback endpoint is deployment infra (one self-hosted server serves
all accounts; fallback rkeys are fungible), so it doesn't belong in per-UIN
onebot_<uin>.json. Introduce config/snowluma.json — a generic home for small
all-accounts SnowLuma settings — and move rkey.fallbackServers there.
- New onebot/global-config.ts: load/save/normalize with section-merge + atomic
write; http(s)+host URL validation; default [] (off). Lives in onebot (not
core) because the onebot manager constructs instances and must not depend on
core.
- OneBotInstance takes globalSettings; manager loads it on session start and
exposes reloadGlobalSettings() to hot-push edits to every live instance via
applyGlobalSettings → RKeyCache.setFallbackServers.
- Remove rkey from per-UIN OneBotConfig + the WebUI per-UIN round-trip (never
released, so no migration). Register snowluma.json in the backup allowlist.
Reworks the per-UIN rkey from 12b0190. Phase 1 of the global-settings split.
On accounts where the native OIDB 0x9067_202 rkey fetch persistently returns
nothing, every image was served as a bare multimedia.nt.qq.com.cn URL the CDN
rejects with invalid rkey — SnowLuma had no fallback, unlike NapCat.
Add config.rkey.fallbackServers (default []: OFF; no third-party server is
contacted unless the operator configures one). When the native fetch leaves the
cache without a usable image rkey, each configured HTTP endpoint is tried in
turn for { group_rkey, private_rkey, expired_time } (NapCat rkey-server format;
an OneBot { retcode, data } wrapper is also accepted), populating the group/
private image rkey types. Values keep any leading &rkey=/?rkey= since
resolveImageUrl strips before re-appending.
WebUI has no form yet but round-trips rkey.fallbackServers verbatim so a hand-
edited value survives an unrelated config save. Part B of #156 (cooldown is A).
When the OIDB 0x9067_202 rkey fetch returns empty or fails, the cache stays
empty and resolveImageUrl hands out a bare multimedia.nt.qq.com.cn URL the CDN
rejects with {retcode:-5503010, invalid rkey}. The 30s refresh cooldown then
blacked out every image for 30s+ after each empty/failed fetch.
Use a short RKEY_EMPTY_COOLDOWN (10s) whenever no usable image rkey is cached,
keeping the 30s steady-state backoff only once we actually hold one — so a
transient empty fetch self-heals in seconds instead of 30s. (Part A of #156;
remote-server fallback follows.)
/get_forward_msg returned raw, rkey-less image/media URLs because
getForwardMessage converted forward nodes WITHOUT the URL resolvers the
normal receive path (to-message.ts) uses. Wire imageUrlResolver +
mediaUrlResolver into it so forwarded images/voice/video get signed
download URLs — exactly issue #74 (image url 缺少 rkey).
Also derive the image rkey scene from the appid baked into the URL
(1406 -> private, 1407 -> group) instead of the carrying message's
group/private context, falling back to isGroup only when the URL has no
recognised appid. A c2c image forwarded into a group keeps its original
c2c appid, so the old isGroup-based pick signed it with the group rkey ->
`invalid rkey` on download. Mirrors NapCat's getImageUrlFromParsedUrl;
Lagrange/NapCat both reuse one unified converter so this never drifts.
Fix the self-contradictory `c2c images` rkey test (its fixture used the
group appid 1407) and add cross-tests asserting appid outranks isGroup
both ways, plus the no-appid fallback.
Image/video/record download URLs are signed at receive-time with an rkey
(OIDB 0x9067_202, TTL-bounded). RKeyCache resolved synchronously: on a
cache miss or inside the expiry skew it returned the URL *without* an
rkey and only kicked off a fire-and-forget refresh (plus a 30s cooldown).
So during the expire→refresh window — and whenever the OIDB hiccuped —
SnowLuma emitted unsigned URLs, which the CDN rejects with
`{"retcode":-5503010,"retmsg":"invalid rkey"}`. Intermittent by nature.
Mirror NapCat's getImageUrl (`if (isExpired()) await refreshRkey()`):
make resolveImageUrl/resolveMediaUrl async and block on one refresh when
the cache can't serve a still-valid rkey. The refresh coalesces
concurrent callers onto a single OIDB round-trip, backs off for 30s after
a failure, and is bounded by a 5s timeout so a stuck packet send never
wedges message conversion (it degrades to whatever the cache holds).
Widen ImageUrlResolver to `string | Promise<string>` and await it in
to-segment so the image path can resolve asynchronously like media.