19 Commits
Author SHA1 Message Date
DrSmoothl 05f18614c1 fix(onebot): restore nested chat record forwarding
Restore expansion and cross-chat forwarding for nested chat records that use preview-only cards.

Return an error when nested content cannot be retrieved instead of producing an unusable message.

Refs #589
2026-10-27 09:23:48 +08:00
DrSmoothlandwanqing-last 9d8df59370 fix(protocol): decode XML forward identities from root attributes
Recognize forward cards with an omitted protocol service ID from the root msg attributes. Decode attribute references once so resource IDs and nested lookup keys survive XML escaping, while preserving ordinary XML when the root declaration is ambiguous.

Refs #547, #556, #558.

Co-authored-by: wanqing-last <2917149421@qq.com>
2026-10-20 13:49:53 +08:00
手瓜一十雪 bdf224cbc9 fix(protocol): preserve nested XML forward lookup keys 2026-10-18 18:59:22 +08:00
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +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 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 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 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 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 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 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 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 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 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