Commit Graph
44 Commits
Author SHA1 Message Date
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl 075a27cc46 fix(protocol): classify private file subcommand receipts
Drop download acknowledgements carried by the offline-file subcommand in both push and history paths, while retaining incoming file transfers.

Fixes #477
2026-09-26 16:09:31 +08:00
DrSmoothl 953b18b5cb fix(protocol): avoid duplicate text for animated faces
Recognize animated face compatibility labels consistently while preserving user-authored text.

Fixes #484
2026-09-26 15:59:07 +08:00
DrSmoothl 4379e4838d fix(protocol): restore forward image media and drop file receipts
Bot-built merged forwards left hollow NT images after long-msg fetch,
so get_forward_msg returned empty file/url. Decode now rebuilds identity
from md5, ignores stubs, and forward uploads keep a legacy image sibling.

C2C offline-file download receipts were reported as the peer sending a
file. Treat downloadFlag=2 as a receipt and drop it from the chat stream.
2026-09-12 13:56:55 +08:00
DrSmoothl a0f152229d refactor(protocol): decide message bubbles after decode
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.
2026-08-30 13:45:22 +08:00
DrSmoothl 39e6b45b81 fix(onebot): restore image links inside merged forwards
get_forward_msg returned image segments with empty url and file when
the long-msg payload omitted a download path. Rebuild the URL from
the file fingerprint the payload still carries.

Fixes #412
2026-08-21 18:26:15 +08:00
H3CoF6 8fbddf67bf Feat: 发送图文ark卡片 (#355)
* feat(onebot): implement send tuwen ark

* test: add unit tests for send_ark action

* test(onebot): update canonical action docs count expectation to 191

* refactor(protocol,onebot): update 0xdc2_34 ark implementation and test coverage

- Rename action send_ark -> send_tuwen_ark to distinguish from structmsg and contact-share arks.
- Clarify SSO command mapping (OidbSvcTrpcTcp.0xdc2_34) and default param behavior in comments/docs.
- Adjust peerType, field3, and previewUrl to pb_optional to preserve 0/empty values during proton encoding.
- Update OneBot handler, tests, and auto-generated MCP tool catalog specs.

* style(protocol): fix indentation in rich-body-decoder tests
2026-08-20 12:12:39 +08:00
DrSmoothl 5a10fc4c9c feat(onebot): report the invited account on group invite events
request.group.invite already identified the inviter. The event now
also reports the invited account when QQ provides it.

Closes #394
2026-08-19 00:23:40 +08:00
DrSmoothl 29e6baac72 fix(protocol): drop leftover picture copies in robot messages
Some group-bot pictures arrive twice: one working download and one
unusable leftover for older clients. Only the working picture is kept.

Fixes #389
2026-08-17 20:08:48 +08:00
DrSmoothl ecf0efc5e2 refactor(protocol): keep hasDecodableContent next to decodeRichBody
Move the body-shape predicate into RichBody so admission no longer
mirrors decode slots. C2C control stays a header check.
2026-08-16 23:22:28 +08:00
DrSmoothl 226b48fd7a refactor(protocol): keep Decoder a pure parse of MsgPush events
Decoder used to write Identity: decodeGroupAdmin patched member roles
and decodeFriendMessage stashed invite-card msgseq. That split
observation with IncomingPacketPipeline.rememberEventIdentity and put
pending-application state on Identity.

Decoder now only returns events. Invite-card groupUin/msgseq ride on
FriendMessage; the live pipeline writes them to ContactsApi through
an optional port. Role self-heal for #93 stays in
rememberEventIdentity. History and forward decode do not write.
Identity no longer stores invite-card sequences.

Live #93 and #125 behavior is unchanged.
2026-08-16 18:22:41 +08:00
DrSmoothl 925d9c0ac7 feat(flash-file): expose cover URL as flash_file.thumb
Incoming flash cards already carry a cover link; map it to
flash_file.data.thumb. Older cards without a cover leave it empty.

Fixes #367
2026-08-14 14:00:18 +08:00
DrSmoothl e7322e2e72 fix(protocol): decode current NT flash-transfer cards
Current clients put fileset identity on markdown extInfo and the
open_fileset scheme, not data.fileSetId. Empty message arrays were
the old scraper missing those fields.

Fixes #358
2026-08-13 18:18:56 +08:00
DrSmoothl 7d5ca5523b refactor(protocol): own inbound UID-to-UIN resolution in Identity
Inbound person lookup used to be split across decoders, the packet
pipeline, and Bridge closures. Missing identities were filled from
unrelated packet headers, and "who is this person" was coupled to
roster refresh and pending-request enrichment.

Identity now owns both directions of resolution. The hot path stays
synchronous and emits unresolved events on a miss. Only the paths
that already waited before dispatch perform a network lookup, and
that lookup is a profile fetch by UID rather than a member-list
refresh. Pending-request comments and approval flags stay outside
Identity.

Decoders no longer invent a UIN from a group routing header. Private
chat routing still uses a hop's own wire UIN when the hop is named
that way. Inbound misses stay availability-first: they do not throw
and do not mark Identity degraded.
2026-08-13 14:37:56 +08:00
DrSmoothl 4aba81ec6b feat(onebot): decode bot markdown and keyboards
Fixes #337
2026-08-09 13:06:35 +08:00
DrSmoothl 185ba8353b fix(message): preserve passive temporary session context
Keep passive replies associated with the correct conversation when the same contact is shared across groups. Active temporary conversations remain unsupported.

Fixes #308
2026-08-04 00:20:56 +08:00
DrSmoothl 0d14408b6f feat(protocol): add byte-complete packet tracing
Capture complete packet request, reply, and push payloads during temporary TRACE sessions while preserving the existing non-persistent logging boundary.

Correlate transport, parser, enrichment, drop, dispatch, timeout, and terminal outcomes with their real identifiers so exported logs can explain packet failures without relying on user descriptions.

Lock byte completeness, lazy disabled behavior, concurrency, context propagation, and truthful terminal outcomes with regression coverage.
2026-07-30 20:11:02 +08:00
DrSmoothl 5d1dbecf2e fix(protocol): scope system dedup to native routes
Derive QQ NT's peerUid_chatType_msg_seq_random identity from its reverse-engineered static routing table before event dispatch. Unknown routes and incomplete identities fail open, while C2C and group peers use their native namespaces.

Refines #266
2026-07-29 01:25:54 +08:00
DrSmoothl 5eb5aaf0a3 fix(protocol): suppress big-face compatibility text
Identify QQ's service-37 fallback by its byte-exact nested Text reserve so compatibility descriptions are removed without consuming user-authored sibling text or malformed payload fallbacks.

Fixes #289
2026-07-29 01:06:59 +08:00
DrSmoothl 3d7c4f5dc3 fix(protocol): align system message dedup identity
Match QQ NT's reverse-engineered peer, sequence, and random key so duplicate system events cannot bypass filtering through a different outer package type.

Fixes #266
2026-07-28 23:59:56 +08:00
DrSmoothl 52a3c49752 fix(protocol): avoid duplicate private voice elements
Prefer a successfully decoded modern voice record over the empty legacy voice placeholder that can accompany private message pushes.

Preserve legacy-only voice messages and add a protobuf round-trip regression covering the duplicate segment reported in #291.

Fixes #291
2026-07-28 22:49:06 +08:00
DrSmoothl 95debcf34b fix(protocol): harden history response decoding
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.
2026-07-25 23:43:30 +08:00
DrSmoothl e16fa78b55 fix(onebot): distinguish private poke conversations
Preserve the private conversation peer separately from the poke sender and target. Emit NapCat-compatible user_id and sender_id fields while keeping group notices unchanged.

Add MsgPush and event-converter regression coverage for self-pokes across multiple friend sessions.

Fixes #285.
2026-07-25 20:28:28 +08:00
DrSmoothl 4aa2cd1693 fix(onebot): correct private message history (#256)
Use the C2C roam cursor and canonical NT sequence for bidirectional history. Persist direction-aware recall tombstones so recalled messages cannot reappear.
2026-07-20 22:25:13 +08:00
DrSmoothl 4ba2b27398 fix(onebot): keep synthetic sequences out of history
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
2026-07-20 15:33:28 +08:00
DrSmoothl b92c8ad881 feat(protocol): decode online device snapshots
Decode Event 0x210 subtype 349 into typed online-device snapshots, including QQ-compatible low-byte client type normalization and one-entry-per-device-category projection.

Keep replacement snapshots outside system-message deduplication, reject malformed device identities observably, and add wire-level regressions for complete, packed, repeated, and invalid payloads. Live verification covered mobile account switching without requiring native changes.
2026-07-19 19:19:58 +08:00
DrSmoothl fadc7ce316 feat(onebot): expose bidirectional message history
Add reverse_order to group and private history actions, preserve latest-page behavior without an anchor, and keep local fallback results chronologically ordered.

Key self-sent private messages and reply targets by the conversation peer, reject cross-conversation anchors, and include request context when a server fetch falls back to local history.

Refs #229
2026-07-17 17:45:30 +08:00
DrSmoothl 4d93c727bb fix(protocol): retain C2C peer identities
Carry the stable sender identity through decoded private-message events and persist the UID/UIN pair at the receive boundary.

This keeps later conversation operations independent of stale message rows or fallible profile lookups, with regression coverage for decoding and identity indexing.
2026-07-13 23:01:54 +08:00
DrSmoothl 920f75718a fix(protocol): bound rich message decoding
Reject malformed nested payloads, cap compressed card expansion, and enforce per-message decode budgets. Preserve unknown-wire metadata in diagnostics so unsupported QQ elements remain observable without exhausting memory.
2026-07-10 13:41:19 +08:00
DrSmoothl 9b470d0412 feat(onebot): add group_name to group message events (#212)
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.
2026-07-09 18:50:54 +08:00
DrSmoothl 007dfaf6e2 fix(group): self-heal member group card from message field 4
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.
2026-07-05 14:20:36 +08:00
DrSmoothl 770b224d58 feat(onebot): decode incoming 闪传 flash-file messages (#199/#200)
Older QQ clients (≤9.9.30) deliver a 闪传 (flash transfer) file as a richui
markdown commonElem (svc=45, busId=FlashTransfer); SnowLuma had no decoder for
it, so the message decoded to 0 elements and was reported empty. (Newer
clients ≥9.9.31 send a plain text+link message that already decodes fine.)

Decode svc=45 markdown → parse the FlashTransfer card JSON → emit a flash_file
element, surfaced as the segment the issue asked for:
  { type: "flash_file", data: { title, file_set_id, scene_type } }

- Field names (fileSetId / sceneType / title) confirmed against QQ NT's
  flash-transfer manager in wrapper.node.i64. The card is built by the
  sender's client so the exact JSON nesting is unknown — values are pulled by
  a bounded recursive search rather than a pinned path (handles either shape).
- Non-FlashTransfer markdown is ignored; missing fields default (never empty).
- Manifest: D/S = yes, P/W = by-design-no (receive-only; sending is the
  existing send_flash_msg action). Log preview shows [闪传文件:title].
- Tests: card decode (nested + alias + defaults + non-flash) and the
  element→segment mapping.

Note: not end-to-end verified on a live message — the svc=45 format only comes
from ≤9.9.30 senders and no such client was available (every test sender here
is ≥9.9.31 → text format). Logic + field names are unit-tested / IDA-confirmed.

typecheck 12 包; protocol 596 / onebot 529 green.
2026-07-04 19:02:47 +08:00
DrSmoothl 3b023b3275 fix(protocol): collapse ark/card messages structurally, not by string match (#146)
Follow-up to the previous #146 fix, which dropped QQ's compat fallback text by
content-matching the "版本不支持 … 升级" family. RE of the QQ kernel
(wrapper.linux.node) showed that is not how QQ does it:
- the receiver binary contains NONE of these fallback strings, so QQ cannot be
  matching by content;
- the on-target wire is [lightApp, text(compat), generalFlags, extraInfo] and the
  text element has no distinguishing marker (no pbReserve / attr6Buf);
- QQ NT's kernel codec (msg_codec_mgr) collapses the message to a single ark
  element — the sibling text is never emitted. NapCat maps kernel elements 1:1
  with no ark-aware skip yet still surfaces only the card, confirming the kernel
  drops it upstream.

Switch to that structural rule: when a lightApp/richMsg card is present, drop
sibling plain text outright. It is version/locale-proof (won't break when Tencent
rewords the string) and matches QQ NT + NapCat output exactly. @ mentions /
replies / faces are not plain text and survive; QQ clients can't attach a caption
to a card anyway, so no QQ-origin message loses real text.

Verified live on a logged-in build: get_msg returns a single json segment.
2026-06-26 18:31:43 +08:00
DrSmoothl 319c7d607d fix(protocol): drop QQ ark-compat fallback text beside mini-app cards (#146)
A QQ mini-program / ark share (B站 video, QQ 小程序, …) arrives on the wire
as a lightApp/richMsg ark element followed by a plain text element carrying
QQ's graceful-degradation compat string "当前QQ版本不支持此应用,请升级". That
text is meant only for protocol-old clients that can't render the card; QQ NT
and the kernel-backed bridges (NapCat) surface just the card.

On-target capture (test group) confirmed the text element has no distinguishing
wire marker (no pbReserve / attr6Buf) — it is a bare text sibling of the ark.
SnowLuma was decoding both, so every mini-app card came through as the card
plus a spurious "当前QQ版本不支持" text segment.

Drop the compat string when it sits beside a card, matched narrowly (the
"版本不支持 … 升级/更新" family, <=40 chars) so genuine user captions sent
alongside a card are preserved.

Verified live on a logged-in build: get_msg now returns a single json segment.
2026-06-26 18:08:26 +08:00
DrSmoothl 1bdb8a40c4 fix(protocol): dedup duplicate system pushes (#137 official-robot double group_increase)
Inviting an official robot (e.g. 2854207029) into a group made QQ push the
group_member_increase notice twice, so SnowLuma emitted two
notice.group_increase events; a normal member produced one.

Root cause (RE of wrapper.linux.node): QQ NT dedups every received system
message in sys_msg_mgr.cc::ProcessRecvSysMsg (sub_37BADE0) by a global_key
`{peerUid}_{chatType}_{msg_seq}_{random}` (sub_37BC4E0) — an already-seen key
is dropped ("ignore dup msg") before any listener sees it. The per-message
discriminators msg_seq (attr 40003 = contentHead field 5) and random (attr
40002 = contentHead field 4) are exactly head.sequence and head.msgId
(msg_header_codec_helper.cc sub_37C8AD0). Kernel-based bots (NapCat/LLOneBot)
receive events after this dedup; SnowLuma reads the raw OlPush before it.

Mirror it: SysMsgDedup, a per-account bounded ring that drops a duplicate
system push by (msgType, subType, fromUin, sequence, msgId). Push-level (drops
the whole duplicate like NT), live path only (Bridge owns the tracker; the
forward re-parse passes none), system kinds only (chat messages excluded), and
never dedups a push lacking a server identity (sequence or msgId 0).
2026-06-23 20:36:24 +08:00
DrSmoothl e160837f8b feat(onebot): tiered reply backfill + quoted-file recovery for get_msg (#128)
Harvests the sound ideas from PR #128 while KEEPING origSeqs[0] as the c2c
reply sequence (on-target capture in #114/#124 proved origSeqs[0] == the quoted
message's head.sequence; pbReserve.friendSequence is a small unrelated counter
that makes reply.id miss). The PR's friendSequence override is intentionally
NOT taken.

- rich-body-decoder: enrich the reply element with replySenderUin/replyTime and
  the quoted message's own decoded elements (SrcMsg.elems, field 5); recover a
  c2c quoted FILE from sourceMsg (field 9 -> RichText.notOnlineFile) when elems
  carried none, so a quoted file survives into get_msg (#124).
- backfillReplyTarget: tiered fallback — Tier 0 local store, Tier 1 server fetch
  (correct seq = origSeqs[0]), Tier 2 reconstruct from the embedded quoted
  elements (no round-trip), Tier 3 minimal '[引用消息]' placeholder so get_msg
  never returns 'message not found'.
- to-segment: per-element try/catch (logged, not silent) so one bad element
  doesn't drop the whole message.
- actions/message: log get_msg misses.
- tests: fix request-actions getGroupInviteCardSequence mock (gap from #125);
  cover backfill Tier 2 (reconstruct) + Tier 3 (placeholder); flip the stale
  c2c reply test to assert origSeqs[0] (was asserting the disproven
  friendSequence).

Validated on-target (Docker): get_msg on group reply, private text reply, and a
private quoted FILE all return correct content (file with name/size/id); full
onebot + protocol suites green.
2026-06-22 21:25:05 +08:00
DrSmoothl e0d5b47488 feat(msg-push): filter C2C control pushes by c2c_cmd, matching QQ NT (#102)
Upgrades the blank-body heuristic to NT's exact discriminator. RE of
wrapper.linux.node (msg_header_codec_helper.cc::DecodeRoutingHead, sub_3B42650)
shows the official client classifies a C2C push by (svr_msg_type, c2c_cmd):
msgType ∈ {141,166,167} with c2c_cmd ∈ {1,73,75,129,131,133,135,192} is routed
through OnRecvSysMsg (long_cnn_msg_mgr.cc) as a system/control signal and never
shown as a chat bubble — the group-invite "[空消息]" phantom is one of these.

- proto-defs: ContentHead field 3 was mislabelled `divSeq`; it is `c2c_cmd`.
  Renamed (only other use was forward.ts, which zeroes it on a fake send).
- MsgPushHead now carries c2cCmd; new isC2cControlPush() in blank-filter.ts.
- parseMsgPush + both history fetches drop control pushes by (msgType, c2cCmd)
  regardless of body, so a control signal carrying stray content is caught too
  (not just the empty case). isBlankMessage stays as the fallback for
  content-less pushes we don't yet classify by cmd. Live path debug-logs a
  control push that carried elements, in case the table ever eats real content.

NOTE: the c2c_cmd set is taken from NT; we have not captured #24's actual cmd
(our diagnostic never logged field 3), so the blank fallback remains load-bearing
until a live capture confirms which cmd the invite phantom uses.
2026-06-16 01:34:38 +08:00
DrSmoothl 2ffb6192dd fix(msg-push): drop blank "[空消息]" pushes in c2c/group history too (#102)
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).
2026-06-16 01:24:31 +08:00
DrSmoothl d5af6b10ef fix(msg-push): drop genuinely-empty c2c push instead of surfacing "[空消息]" (#102)
A group invite makes QQ emit a content-less c2c message (msgType=166/0, body
richText.elems=[]) right after the invite card. The TRACE log from #102's
diagnostic confirmed the body is truly empty — no commonElem, no msgContent,
no ptt/file — so it's a phantom control push, not an element type we fail to
decode. It was reaching OneBot clients as a confusing "[空消息]".

parseMsgPush now drops a message-kind event that decodes to zero elements when
its body carries nothing decodable. When the body *did* carry content but still
decoded to nothing (a real missing decoder), the event is kept and a warning
fires so the gap gets noticed rather than silently swallowed — replacing the
temporary #102 diagnostic with the actual fix.
2026-06-16 00:54:28 +08:00
DrSmoothl cf8e62d2ac fix(onebot): resolve c2c reply target by srcMsg friendSequence, not origSeqs
Replying to a bot self-sent private message still failed get_msg on the quoted
id ("无法解析申请信息"). Root cause is the reply DECODE, not the keying: for a
c2c (friend) reply the canonical replied-to sequence is carried in
srcMsg.pbReserve.friendSequence (tag 8), while origSeqs[0] holds the per-sender
clientSequence. The decoder read origSeqs[0], so the reply resolved to a
sequence that never matches how the original message is keyed (by its server /
private sequence) — get_msg missed. Mirrors Lagrange's incoming-reply rule
`Sequence = reserve.FriendSequence ?? OrigSeqs[0]` (ForwardEntity.cs +
SourceMsg.PbPreserve.cs ProtoMember(8)).

- proto-defs: +SrcMsgPbReserve { receiverUid@7, friendSequence@8 }
- protocol: decodeRichBody reads friendSequence for c2c replies (group keeps
  origSeqs[0], the shared group seq); fall back to origSeqs[0] when absent
- tests: c2c→friendSequence, group→origSeqs[0], no-reserve→fallback

This supersedes the keying approach in 4571864 (self-sent c2c keyed by
clientSequence), which is fully reverted here: the message is keyed by its
private sequence as before, and the fix is purely on the decode side so it also
covers replies to *received* old c2c messages, not just self-sent ones.
2026-06-15 11:58:28 +08:00
DrSmoothl 8dc4ff48f8 feat(onebot): unify market faces to image segments + add send support
Market faces (商城表情) now decode to an OneBot `image` segment carrying
NapCat-style markers (emoji_id/emoji_package_id/key/summary) plus a gxh
gif URL, so clients that don't special-case `mface` still render the
sticker while aware clients can reproduce it.

Adds the previously-missing send path: a dedicated `mface` segment OR an
`image` segment carrying `emoji_id` rebuilds the wire `marketFace`
(faceId=hex(emoji_id), itemType 6 / subType 3 / 300×300 /
pbReserve.field8=1) — byte-matched against NapCat and Lagrange.Core.

- proto-defs: MarketFace.key bytes→string; +MarketFacePbReserve{field8@8}
- protocol: decode faceId→emojiId(hex)/tabId/key; makeMarketFaceElem +
  buildSendElems 'mface' case; format preview keeps the sticker name
- onebot: to-segment mface→image; message-parser mface + image(emoji_id)
- tests: wire decode + full proton round-trip; parse→build; segment shape

Reply/forward round-trips preserved (element keeps type 'mface' + text).
2026-06-14 20:47:50 +08:00
DrSmoothl 58c02d415f feat(onebot): fetch real private (c2c) history from server (#99)
Mirror the group-history fetch for private chats via SsoGetC2cMsg
(trpc.msg.register_proxy.RegisterProxy.SsoGetC2cMsg, fields per Lagrange:
flat friendUid/start/end request, messages@7 response). get_friend_msg_history
now resolves the anchor sequence + the friend's UID, fetches real history from
the server, converts + persists each message, and falls back to the local
store on unknown anchor / no uid / empty / error.

- protocol: fetchC2cMessageRange (decodes via the friend decoder).
- core: the group + c2c fetch now share a generic walkBackHistory loop
  (same throttle gate + caps); added MessageApi.getC2cHistory.
- onebot: getFriendHistory + a generalized persistHistoryEvent (keys private
  by sender uin, exactly like the live path — so reply to old private
  messages resolves too).

Tests: SsoGetC2cMsg request/response round-trip + friend-decode reuse.
2026-06-14 20:15:38 +08:00
DrSmoothl 987ac09896 feat(onebot): fetch real group history from server (#99)
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).
2026-06-14 19:11:03 +08:00
DrSmoothl c16064d7e3 refactor(packages): rename @snowluma/bridge → @snowluma/protocol
The package previously named "bridge" houses the protocol-byte layer
(OIDB cmds, msg-push decoders, highway uploaders, element-builder,
event bus). The name "bridge" suggested transport/lifecycle, which it
isn't — that role lives in core/hook and core/bridge/manager. Rename
to match what's actually inside.

This is a pure rename: zero logic changes, all 73 import sites updated
mechanically and TS-verified. core/onebot dependencies key updated.
ESLint override path adjusted for the moved oidb-services directory.

Rename detection threshold: git log --find-renames=20% recommended
when following history across this commit — content-replacement
(@snowluma/bridge → @snowluma/protocol) dropped per-file similarity
below git's default 50% on import-heavy files.
2026-05-23 23:31:03 +08:00