Commit Graph
316 Commits
Author SHA1 Message Date
DrSmoothl 2dbd321ef0 fix(protocol): decode nested group operators
Recognize both raw UTF-8 UIDs and nested OperatorInfo envelopes in GroupChange pushes while keeping self-kick and voluntary-leave semantics explicit.\n\nFixes #253.
2026-07-20 14:08:50 +08:00
DrSmoothl 297f0428cc fix(core): correlate AI voice send receipts
Wait for the canonical self group-message event after AI voice synthesis instead of interpreting undocumented response fields as a message sequence.

Correlate receipts by exact media UUID, buffer response races, and claim each semantic message identity only once across concurrent calls and duplicate multi-process pushes. Reject missing identifiers, invalid sequences, bounded-buffer overflow, and receipt timeouts with explicit diagnostics.

Keep fetchAiVoice focused on returning the media node and add regression coverage for race ordering, unrelated events, identical concurrent audio, duplicate pushes, malformed receipts, and timeout cleanup.

Fixes #250
2026-07-20 14:02:48 +08:00
DrSmoothl 810d72fa04 feat(onebot): implement collection list retrieval
Replace the get_collection_list placeholder with authenticated, read-only collection retrieval through the existing bridge API.

Add strict binary envelope validation, bounded pagination, accurate category filtering, preserved 64-bit identifiers, and explicit ticket, transport, service, and malformed-response failures. Cap item counts, page traversal, response bytes, per-request waits, and the total operation time.

Expose the compatible response model through Core, OneBot, the SDK, and the generated MCP catalog. Cover protocol framing, cursor behavior, filtered hasMore semantics, cleanup failures, bridge ticket acquisition, and Action validation with regression tests.

Verified with the live test bot without changing native components or adding any collection write behavior.
2026-07-19 20:22:16 +08:00
DrSmoothl 4e62fdc80b chore(release): v1.12.6 2026-07-19 19:38:13 +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 e6a69b05a7 feat(onebot): expose group folder upload metadata
Decode the folder modifier metadata returned by group file listings and project it as last upload time, uploader ID, and uploader name for both folder listing actions.

Regenerate the MCP catalog and add protocol, bridge, OneBot projection, and action schema coverage for the new fields.

Closes #248
2026-07-19 18:34:53 +08:00
DrSmoothl f3e0c4e45a fix(onebot): allow clearing the self signature
Accept explicit empty strings from both longNick and long_nick when callers need to clear the bot signature. Missing and non-string values remain rejected before reaching the bridge.

Preserve the QQ-compatible clear representation by omitting the empty profile value, and add action-level and wire-level regression coverage. Live verification confirmed successful clearing and readback on the test bot.

Fixes #247
2026-07-19 18:12:48 +08:00
DrSmoothl 4391b45c41 chore(release): v1.12.5 2026-07-17 18:47:51 +08:00
DrSmoothl 5fe6b31eb6 refactor(ci): generalize native smoke tests
Replace the feature-specific MP3 verification command with a workspace-wide native test entry point.

Keep native regression cases in package-owned test suites so future checks can expand without adding workflow steps.
2026-07-17 18:27:30 +08:00
DrSmoothl b3838d204f fix(protocol): produce valid MP3 records
Use the dedicated audio conversion path for record output so mono QQ voice records produce valid MP3 data.

Add release-target smoke coverage and preserve conversion and cleanup failures for diagnostics.

Fixes #239
2026-07-17 18:17:48 +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 6a1042e33f chore(release): v1.12.4 2026-07-16 20:29:00 +08:00
DrSmoothl c2d028af2e fix(protocol): preserve member operation high bits
Clear only the membership flag bit when normalizing group increase operation codes so unrelated high bits cannot collapse into the invitation value.

Add regression coverage for extended operation codes.
2026-07-16 20:17:38 +08:00
DrSmoothl 0e3bbc6906 fix(protocol): correct group membership notices
Derive group increase approval and invitation subtypes from the member-change operation code instead of actor identity.

Treat voluntary leave field 5 as structured metadata and attribute the operation to the departing member so valid notices are not discarded.

Add protocol and OneBot regression coverage for issue #237.
2026-07-16 19:41:48 +08:00
DrSmoothl b4f5e9b71b fix(media): support base64 data URLs for uploads
Parse RFC 2397 base64 data URLs through the shared binary source classifier so inline media cannot fall through to local file handling.

Reuse the same parsing path for buffered and disk-staged uploads, reject malformed or oversized inputs explicitly, and add regression coverage for both paths.

Refs #236
2026-07-16 10:05:56 +08:00
DrSmoothl ebd02c3f3d chore(release): v1.12.3 2026-07-15 21:31:59 +08:00
DrSmoothl f9d251d1fa fix(protocol): decode group member operators correctly
Handle member joins and ordinary removals using their direct operator identity while preserving the special nested form used when the bot is removed.

Reject malformed values with bounded diagnostic context and add regression coverage for #228 and related member-decrease events.
2026-07-14 20:08:29 +08:00
DrSmoothl a3467a8d89 chore(release): v1.12.2 2026-07-14 07:57:08 +08:00
DrSmoothl 5885187e7d test(group-notice): cover publishing options and readback
Verify endpoint selection, option validation, image metadata parsing, response merging, and OneBot forwarding. Cover the type-20 new-member path and fail-fast conflict behavior.
2026-07-14 03:39:27 +08:00
DrSmoothl 51eb1f733d feat(group-notice): support advanced announcement controls
Route regular and new-member announcements through their correct web endpoints. Validate option conflicts, preserve existing defaults, merge returned notice collections, and surface transport and upload failures.
2026-07-14 03:39:11 +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 8cd7d51d80 fix(protocol): preserve friend category metadata 2026-07-13 18:51:11 +08:00
DrSmoothl c04864dd21 chore(release): v1.12.1 2026-07-10 14:00:40 +08:00
DrSmoothl 01688c7567 fix(protocol): enforce message element contracts end to end
Replace the open message bag with a validated discriminated union and preflight complete messages before uploads or sends. Return BAD_REQUEST for invalid or unsendable segments instead of dropping fields, sending partial content, or silently changing private messages.
2026-07-10 13:47: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 b7d648ea0f fix(proton): harden generated protobuf decoders
Reject malformed varints, out-of-bounds fields, and invalid groups before they escape their parent frame. Aggregate unknown-field metadata and decode signed int32 values while preserving SnowLuma's existing outbound encoding bytes.
2026-07-10 13:40:53 +08:00
DrSmoothl 4425bee612 feat(protocol): make identity persistence availability-first 2026-07-10 03:06:00 +08:00
DrSmoothl 35958a7fc2 fix(onebot): use canonical group request flags 2026-07-10 02:42:07 +08:00
DrSmoothl 3c73d1d5b0 feat(onebot): reply into group temp sessions (临时会话) (#212)
send_private_msg / send_msg can now reply to a group temp-session message
by passing the source group_id. Inbound temp messages surface their
source group on sender.group_id; a reply is only made into a temp session
the bot has an inbound record of. Group temp replies route through
RoutingHead.grpTmp (field 3).
2026-07-10 02:29:18 +08:00
DrSmoothl b37b17f7d8 fix(protocol): guard identity.findGroup in group decoder (#212)
9b470d0 added ctx.identity.findGroup() to the group-message decoder for
the group_name cache fallback, but several suites mock a minimal identity
without findGroup, so decodeGroupMessage threw at runtime once the
typecheck gate (fixed in 5e73d69) let CI reach the test phase — forward /
packet-pipeline-stranger-resolve / msg-push-handler. Optional-chain the
call: the real IdentityService always has findGroup; only test mocks omit
it, and an absent cache simply yields no fallback name.
2026-07-09 23:13:40 +08:00
DrSmoothl 5e73d69663 fix(test): add groupName to GroupMessage test fixtures (#212 CI)
core's typecheck covers its own tests (protocol/onebot typecheck src
only), so the newly-required GroupMessage.groupName field failed the
Dev Build typecheck on test fixtures that construct group_message
events. Add groupName to every such fixture across core/onebot/protocol
tests.
2026-07-09 19:18:09 +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 7ab91c81e9 perf(highway): single-connection pipelined upload — supersede #211 concurrency
The #211 concurrency attempt (c17fff6) was disproven on a live endpoint.
QQ's highway server requires blocks to arrive in offset order and rejects
an out-of-order PUT with error_code=102902 — ~20% failure under a real
account even after deferring the finalize block. Parallel connections
can't guarantee arrival order, so that approach is unshippable.

Replace it with a single persistent connection that PUTs blocks in strict
offset order, reusing the socket across the whole file via HTTP keep-alive:
- In order: the server never sees an out-of-order block, so no 102902.
- One warm connection: the TCP congestion window stays open across the
  file instead of paying slow-start on a fresh connection per 1 MiB block.
  That per-block cold start was the real throughput cap behind #211.

Adaptive to QQ's edge nodes FIN'ing between chunks (the reason #118 went
one-connection-per-chunk): on a transport failure, or a peer half-close
after a response, drop the socket, reconnect, and re-send the SAME block
(re-PUTting an offset is idempotent). Worst case degrades to one
connection per block — no worse than #118, and still strictly in order.

Also fixes a latent bug on the reconnect path, surfaced by review: a
response truncated mid-stream (header arrived, Content-Length unmet, then
FIN) was resolved as a partial frame and then fatally rejected by
unpackHighwayFrame OUTSIDE the retry loop, turning a recoverable mid-body
FIN into a failed upload. A truncated response now rejects as a retryable
transport failure so the block is re-sent. Added an idle read timeout so
a silent peer can't hang the upload forever (the persistent socket widens
that exposure).

Verified live (tempserver, repeated 48 MiB group-file uploads): 1
connection per file, 0 failures across dense repeats, ~1.4x throughput
over the old per-chunk-connection serial path in a low-RTT datacenter —
the win is larger on high-RTT home links where per-block slow-start hurt
most (the original #211 report). Unit tests cover keep-alive reuse,
FIN-after-each-response degradation, mid-stream reconnect+resume, body-
truncation retry, in-order exactly-once coverage, and close-once.
2026-07-09 13:55:25 +08:00
DrSmoothl c17fff6e2f perf(core): parallelize highway chunk uploads (#211)
Group/private file uploads (and all highway media) topped out around
500 KB/s regardless of link speed. Two costs stacked up in
uploadHighwayHttp: chunks were sent strictly one at a time (throughput
capped at one 1 MiB block per round-trip), and each block opened a fresh
connection that paid TCP slow-start from a cold congestion window — so a
1 MiB block finished before the window ramped and never reached line
rate.

Drive the (already offset-addressed, self-contained) blocks through a
small worker pool instead of a sequential loop. Highway frames carry
their own dataOffset/chunkMd5/fileMd5, so blocks are independent and the
server reassembles by offset; a handful of connections overlap the
round-trip stalls and ramp their windows together. Concurrency is kept
modest (4) to stay friendly to the edge nodes' rate limiting, and the
one-connection-per-block invariant from #118 is preserved — parallelism
just puts several of those short-lived connections in flight at once.

Correctness preserved and covered by new tests:
- offset claim is synchronous (no await between read and advance of
  nextOffset), so every byte range is uploaded exactly once — no skip,
  no duplicate, ragged final block correct;
- concurrent ChunkSource reads are race-free (positional FileHandle
  reads into per-call buffers; read-only Buffer subarrays);
- first permanent block failure aborts the whole upload and rejects;
  peers stop claiming new blocks and drain the one in flight;
- workers never reject, so Promise.all settles all in-flight
  reads/sockets before the source is closed exactly once — no
  close-during-read, no masked upload error;
- 0-byte source still opens zero connections; peak memory bounded to
  concurrency x ~1 MiB, never the whole file.
2026-07-09 01:20:26 +08:00
DrSmoothl b6fb5bd530 chore(release): v1.12.0 2026-07-07 22:38:02 +08:00
DrSmoothl d9dfe513ee feat(onebot): report forced offline as notice/bot_offline
Register a handler for the StatusService.KickNT push (QQ NT's unified
forced-offline notification — kick / login-elsewhere / risk-control),
decode KickNTResponse and emit bot_offline {user_id, tag, message},
matching NapCat. Command + field layout (f4=tips_title, f3=tips_content)
RE-confirmed against wrapper.linux.node + Lagrange; not yet live-captured.
2026-07-07 15:25:35 +08:00
DrSmoothl eda1c32994 test(onebot): confirm profile_like decode against real captures
Validated the 0x210/39 ProfileLikeTip field layout byte-exact against three
real captured like pushes (detail.txt='赞了我的资料卡N次', f3=uin, f5=nick).
Drop the pending-validation caveat; test now uses the real txt shape.
2026-07-07 15:10:06 +08:00
DrSmoothl 447b40a259 feat(onebot): report profile like as notify/profile_like
Decode the multiplexed 0x210 subType-39 push: when body.msgContent is a
ProfileLikeTip with inner msgType==0/subType==203, emit notify/profile_like
{operator_id, operator_nick, times}. times is parsed from detail.txt.
Proto shape from NapCat's raw decoder; wiring + decode verified, but the
byte-exact wire is pending a real like capture to confirm.
2026-07-07 03:05:14 +08:00
DrSmoothl 0d4d58605f feat(onebot): report group special title grant as notify/title
Decode the 0x2DC subType-16 field13==6 push: eventParam carries the member
uin (f5) and a gray-tip template (f2) whose last rich token holds the title
text. Emit notify/title {group_id, user_id, title}, matching NapCat. Field
layout taken from a real on-wire sample, not ported blindly.
2026-07-07 02:53:38 +08:00
DrSmoothl d3d1d2512e feat(onebot): report group member card change as notice/group_card
Detect a group member's card change from message traffic (sender card vs
cached roster card) and emit group_card {group_id, user_id, card_new,
card_old}, matching NapCat's parseCardChangedEvent. Fires only on a real
change of a known member; first-contact and unchanged cards are suppressed.
2026-07-07 02:23:17 +08:00
DrSmoothl bfde0daab4 feat(onebot): report group name change as notify/group_name
Dispatch the 0x2DC subType-16 push by NotifyMessageBody.field13 (was
emoji-react only); field13==12 decodes eventParam as GroupNameChange and
emits notify/group_name {group_id, user_id=operator, name_new}, matching
NapCat. Emoji-react (field13==35) path unchanged.
2026-07-07 02:05:52 +08:00
DrSmoothl 852870db59 feat(onebot): report C2C input status as notify/input_status
Decode the 0x210/0x115 input-state sys_msg (RE'd from wrapper.linux.node
aio_input_state_worker) into a friend_input_status event and map it to a
notify/input_status notice, matching NapCat's onInputStatusPush.
event_type 1 = typing, 3 = recording voice; status_text synthesised to
match the client's own strings.
2026-07-07 01:26:31 +08:00
DrSmoothl d35e276730 chore(release): v1.11.4 2026-07-06 15:04:55 +08:00
DrSmoothl e86fd707ea fix(video): declare the cover's own dimensions on the thumbnail sub-file 2026-07-06 01:44:10 +08:00
DrSmoothl e1e0c6b02f fix(video): zero-trust metadata on fast-upload group video
The width/height=0 → 文件已过期 fix only covered the normal upload path, which
ffprobes real dimensions from the video bytes. The fast-upload (fingerprint /
forward) path has NO bytes to probe, so every field it ships is caller-supplied
and untrusted — a video that arrived on the wire without metadata caches 0s, and
re-forwarding it then shipped width/height=0 (receiver renders 文件已过期) and
time=0 (every client renders 00:00).

Harden videoPayloadFromFingerprint so no degenerate value reaches the wire:
- width/height fall back with `||` (not `??`, so a cached 0 is replaced too);
- duration falls back the same way (0 → 1, never 00:00);
- the fallback cover is now a real 720x1280 image, and the fallback video/cover
  dimensions are derived FROM it (detectImageFormat), so the declared video size,
  the declared cover size and the actual cover pixels are the same numbers by
  construction — never a 1x1 image lying about its size.

Real cached values always win; c2c is unaffected (the wire builder forces 0 for
c2c). Verified on-target by forwarding a zeroed video (renders, not expired).
2026-07-05 18:10:05 +08:00
DrSmoothl ac20956076 fix(video): fall back to portrait dims on fast-upload group video
The width/height=0 → 文件已过期 fix only covered the normal upload path, which
ffprobes real dimensions. The fast-upload (fingerprint / forward) path has no
bytes to probe and takes dimensions from cached metadata, which is 0 when the
source video arrived on the wire without dimensions (e.g. forwarded from another
client that itself sent 0x0). Re-forwarding such a video to a group then shipped
0x0 and the receiver rendered a stale "expired" placeholder.

videoPayloadFromFingerprint now substitutes a neutral 720x1280 portrait when a
dimension is absent or zero (|| not ??, so a cached 0 is replaced too). Real
cached dimensions always win; c2c is unaffected (the wire builder forces 0 there
regardless). A forwarded dimensionless video now renders and plays instead of
showing 文件已过期; verified on-target by forwarding a zeroed-dim video.
2026-07-05 15:39:35 +08:00
DrSmoothl 0e6e2ec51d chore(release): v1.11.3 2026-07-05 14:26:10 +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 5f90cc9b6e fix(forward): preserve group merged-forward sender nickname (#201)
get_forward_msg returned empty sender.nickname for group merged-forward nodes.
The reporter blamed a mis-parsed group_id + a failing member-list enrichment
(0xfe7_3 0x899_AUTHORITY_FAIL), but on-target capture shows a mislabeled proto
field: the group response-head's sender display name lives in field 4, which we
had labeled `groupName`. It's populated on live pushes ("星屿" / the group card
"墨梓柒111") AND on merged-forward nodes (per-node sender name), while field 2
(`memberName`) is empty on live/forward and only set by the group-history fetch,
and the real group name is field 7. Lagrange labels field 4 `MemberName`.

Relabel ResponseGrp: field 4 `memberCard` (sender display name = card||nick),
field 7 `groupName` (the real group name); keep field 2 `memberName` for the
group-history slot. The group decoder now resolves the sender name by priority:
field-2 name → local member cache (clean nick/card split) → field-4 memberCard.
The last step is what lets a forward node from a group the bot isn't in keep its
per-node sender name with no member-list lookup, so the forward-specific fallback
and the AUTHORITY_FAIL enrichment path are no longer needed for the common case.

Verified on-target across scenarios: live message senderNick unchanged; a plain
forward, a forward from a never-joined group (placeholder uin 1094950020, four
distinct senders 金橘雪梨/Crel/503/Xiing0), and a nested forward all keep their
nicknames; no member-list call fires. group_id stays the payload's value.
2026-07-05 14:20:36 +08:00
DrSmoothl e83eb80f2c feat(video): stream video uploads from disk instead of buffering (phase 4)
Wire the streaming primitives into the video send path. `loadVideo` now stages
the source onto a local disk path (stageSourceToDisk — hardlink/download),
hashes it in one streaming pass (hashFileStreaming), and returns a `fileSource`
instead of a full in-memory buffer; the main video sub-file PUTs through a
`FileChunkSource`, so a multi-GiB video never holds more than one 1 MiB chunk in
RAM. The thumbnail and the fingerprint (noByteFallback) fast path stay
byte-based and unchanged.

`runPuts` now sizes its fast-path / fast-only checks off `fileSource ?? bytes`
and resolves the highway session before opening the file handle (the handle's
owner, uploadHighwayHttp, must be the next call so its finally-close always
runs). A size+mtime guard re-checks the staged file before the PUT so a source
mutated mid-send fails cleanly rather than shipping bytes that disagree with the
already-sent hashes. The staged temp is released in every exit path.

Output is byte-identical to the buffered path (the OIDB fileInfo md5/sha1/size
and the uploaded bytes are unchanged); the old readFileSync-based
stageVideoSource is removed. Tests cover the disk path (fileSource not bytes,
streamed hash == buffered hash), cleanup on upload failure, and empty sources.
2026-07-05 03:23:19 +08:00