Live push and history now share one keep-or-drop decision. Control
signals and empty bodies stay hidden; undecodable content is kept and
logged on both paths.
Share packet response validation across group and private history fetches so transport failures and empty responses cannot masquerade as empty pages.
Reject malformed group records and mismatched private-peer responses while preserving explicit QQ control-message filters. Mark fetched contexts as historical so old invite cards cannot overwrite live approval state.
Add wire-level regression coverage for failed responses, malformed entries, peer mismatches, ordering, and live-only identity observations. Refs #280.
Distinguish QQ-authoritative message sequences from local message IDs so group files and reply placeholders cannot become history anchors or be reused by protocol actions.
Filter QQ null roam records and migrate previously stored synthetic rows.
Fixes#254
Group message events (message + message_sent) now carry group_name — a
non-standard but common OneBot extension. The name comes from the
message's own wire field (ResponseGrp field 7, accurate even right after
a rename), falling back to the identity group cache, else '' when neither
is available. It's resolved in the group-message decoder, where both the
wire head and the identity cache are already at hand, so the converter
just passes it through (no ConverterContext change).
Verified live (tempserver): real inbound group messages from a non-self
account carry group_name set to the actual group name.
A member's cached group card could stay stale for the lifetime of a long-running
bot: nothing refreshes it on a card change (no push), and the member-list refetch
only fires on join/leave/admin events (+60s TTL), never periodically — so a quiet
group freezes at warmup. get_group_msg_history / live events then showed an empty
or outdated sender.card even though the current card was in the message.
Every group message carries the sender's current display name in grp.memberCard
(field 4 = group card if set, else base nickname). The group decoder now prefers
it over the possibly-stale cache when it differs from the base nickname (a real
group card); and packet-pipeline self-heals it back into the in-memory member
cache, gated so it writes only on an actual change. This keeps active members'
cards fresh for all cache consumers off message traffic alone — no extra fetch,
no rate-limit exposure. Verified on-target: a member's card change now reflects
in the next message without a member-list refetch.
Known, accepted edge: a member changing only their base nickname (no card) is
mislabeled as a card until the next member-list refetch corrects it — field 4
alone can't distinguish the two, and the mislabeled value is still the member's
own current name.
The #102 fix dropped content-less control pushes only in parseMsgPush (live).
History fetches (fetchC2cMessageRange / fetchGroupMessageRange) decoded straight
into the result, so the same phantom resurfaced as "[空消息]" on backfill. RE of
wrapper.linux.node confirms QQ NT filters blanks on BOTH paths — live/roam C2C
via c2c_roam_msg_mgr.cc::FilterBlankMsgAndRetryFetch and group roam via
group_roam_msg_worker.cc::FilterBlankSeqsMsg.
Extracts the blank test into blank-filter.ts (bodyHasDecodableContent +
isBlankMessage) and applies it in all three message-producing paths. A message
that decoded to 0 elements but whose body carried content is a missing decoder,
not a blank — isBlankMessage returns false there so it's kept (live path still
warns).
get_group_msg_history was served only from the local observed-message store,
so it could never return more than what SnowLuma saw live (the "only 100
messages" symptom). It now fetches real history from the server.
- protocol: new SsoGetGroupMsg packet (trpc.msg.register_proxy.RegisterProxy
.SsoGetGroupMsg, sequence-range fetch; fields per Lagrange). Each returned
PushMsgBody is decoded by the existing group decoder via a factored-out
buildContextFromMessage.
- core: MessageApi.getGroupHistory walks backward in small sequence windows
with rate-limiting + data-volume guards — a process-wide throttle (≥300ms
between sends), 30-seq chunks, ≤12 requests, ≤200 messages per call — so a
hammering client can't make us flood the server and get kicked.
- onebot: get_group_msg_history resolves the anchor sequence (requested
message_id or latest observed), fetches from the server, converts + returns,
and persists each fetched message. Falls back to the local store on
unknown anchor / empty / error.
Persisting fetched history also fixes replying to pre-injection messages:
the reply path resolves message_id→sequence via the store, which now contains
the fetched messages.
Tests: SsoGetGroupMsg request/response round-trip + decode reuse (protocol);
walk-back chunking/dedup/cap/floor (core).