Separate account login state from receive-path health in get_status. A logged-in account remains online while good becomes false only after the receive path has been confirmed stale.
Apply the same health value to OneBot lifecycle and heartbeat events so API polling and event consumers observe consistent status semantics.
Regenerate the MCP action catalog with the clarified contract and add regression coverage for the online-but-degraded response.
Refs #233.
Route selected backup entries through owner-owned config parsers before any filesystem write. Reject unsupported paths, mismatched encodings, malformed base64, lossy normalization, invalid effective overlays, and decoded payloads larger than 32 MiB.
Treat WebUI state, private keys, and every OneBot config as a single sensitive group. Skip unselected sensitive entries without inspecting their contents, preserve recognized migrations, and assign restrictive modes to credential files.
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.
Replace the compatibility no-op with a real batched read action for current groups and observed incoming private conversations.
Exclude stale groups, group temporary sessions, and sent-message echoes from private targets, then expose the functional action through the generated MCP catalog.
Add action and message-store regressions for target selection and delegation.
Replace message-header sequence forwarding with a server-guided read flow for group and private conversations.
Batch read reports conservatively, validate per-conversation results and cursor advancement, and fail with actionable errors when the server does not apply an update.
Add regression coverage for mixed batches, pagination, already-current sessions, malformed responses, and private sequence-domain mismatches.
Validate the public OneBot data.data field before converting JSON segments to the internal message element shape. Invalid payloads still return retcode 1400, but now identify the correct public field without reaching the QQ bridge.
Add Action-level regression coverage for issue #220.
Serialize desired network plans, isolate per-adapter apply failures, restore prior bindings when possible, and report saved versus applied state. Quiesce Action and event ingress, await in-flight dispatches and transport closes, retain failed generations for retry, and propagate shutdown failures.
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.
Persist user saves as authoritative per-account snapshots while retaining legacy overlay compatibility. Validate deterministic topology errors before writing and replace configuration files atomically.
Compile canonical names, aliases, stream kinds, documentation, and raw reservations into one immutable registry. Reject duplicate, undeclared, mismatched, or late handler registration before requests can be dispatched.
Wait for asynchronous transports to flush bad-request frames before processStreamRequest returns. This prevents malformed Stream API requests from losing their terminal error response.
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).
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.
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.
Merged-forward nodes always stamped the current time. A custom forward can now
set each node's displayed timestamp via the OneBot `data.time` field (unix
seconds), matching the field the wire already carries per node.
parseForwardNodes reads `data.time` on custom/fake nodes and threads it onto the
node payload (the `time?` field already existed for the receive side);
buildForwardPushBody uses it for `contentHead.timestamp`, falling back to now
when absent. The value is validated to a plausible unix-seconds range — the wire
field is uint32, so a millisecond or out-of-range input is rejected (would
overflow) and defaults to now rather than corrupting the timestamp. The
message_id-reference branch (time from the quoted message) and the c2c-file
expireTime (real now) are unchanged. Nested inner nodes honour their own time.
Symmetric with the ws-client fix (#208): the reverse-WS server now pings each
attached client and terminates one that goes fully silent for ~90s (2 unanswered
pings), reaping a half-open client that would otherwise linger in the connection
set. Reuses startHeartbeat with the same conservative, activity-reset tolerance,
so a client with live traffic is never dropped. The per-connection stop is kept
on the ForwardConn entry and cleared in both close() and the socket 'close'
handler; no shared instance state, so the #97 overlap does not apply here.
A reverse-WS client could end up half-open after the peer restarted: still
writable (events kept flowing out) but no longer readable, so inbound action
frames silently never arrived and only a full restart recovered it (#208). The
OneBot app-level meta heartbeat is a one-way push and cannot detect this.
Add an RFC6455 ping/pong keepalive (startHeartbeat) wired into the ws-client
adapter. It pings every 30s and declares the link dead only after 2 unanswered
pings (~90s of total silence), then terminates the socket so the existing
reconnect path runs. Any inbound activity — a pong OR a message — resets the
counter, so a connection carrying real traffic is never reaped; the conservative
tolerance keeps the false-positive rate near zero (a GC pause or latency spike is
absorbed). The stop is per-socket and also held on the instance so close()/reload
halt it immediately rather than waiting for the deferred 'close'; a symmetric
identity guard on 'open' mirrors the #97 stale-socket guard on 'close'.
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.
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.
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.
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.
A fake forward node (send_group_forward_msg / send_private_forward_msg) whose
data.user_id was "0" or omitted got rejected with "forward node user_id/uin is
required", even though the protocol端 is logged in and the core forward builder
(bridge/apis/forward.ts) already defaults a zero sender to the bot's own uin.
Upstream frameworks (AstrBot, etc.) don't manage QQ uins and legitimately send
user_id "0"; NapCat and LLBot accept it. parseForwardNodes now falls back to
ref.selfId, keeping the throw only for the not-logged-in case (selfId 0).
Verified on-target (#203).
get_msg / get_group_msg_history returned an empty sender.nickname for the bot's
own group file / video sends. Those publish via OIDB (0x6d9_4 / c2c finalize)
rather than PbSendMsg, so the QQ server never echoes them back — and the echo is
what normally overwrites the synthetic self-sent store's placeholder nickname.
For a plain text send the echo arrives and fills it, which is why only file/video
surfaced empty.
Seed the synthetic event's sender.nickname from the bot's own
identity.nickname (we are the sender, so we know it). A normal text send's echo
still overwrites it with the same value. Verified on-target: a self-sent group
video now shows the bot nickname in history.
The 100 MB cap that re-routed larger videos to the file-upload pipeline
was a workaround for the width/height=0 → 已过期 bug (fixed in the prior
commit), not a real QQ size limit. Real-machine verified on tempserver:
88 MB / 149 MB / 299 MB / 497 MB videos all upload, render and play as
normal videos in a group. Raise MAX_VIDEO_SIZE to 1.5 GiB (matching the
file-upload ceiling); only videos above that still fall back to the file
path. Collapse the redundant MAX_VIDEO_SIZE_HARD (was also 1.5 GiB) into
the single constant and drop the now-dead 100 MB warn branches.
Note: the whole video is buffered in RAM for the Highway upload, so a
1.5 GiB send costs ~1.5 GiB+ of process memory — that, not a QQ limit,
is what bounds the ceiling.
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.
Round out the OneBot v11 group-profile fields the issue asked for.
Group (get_group_info / get_group_list, inherited by get_group_info_ex /
get_group_detail_info):
- group_create_time + group_memo come free from the 0xFE5_2 list
(createdTime / announcement), verified live (createdTime=1729505343,
announcement populated).
- group_level lives only in the 0x88D_0 detail; get_group_info fetches it
(memoized 5min) so a joined group doesn't hit the detail every call.
get_group_list leaves it 0 — a per-group detail fetch across the whole
list would be far too costly.
Member (get_group_member_info / get_group_member_list):
- Add area / unfriendly / title_expire_time / card_changeable. QQ NT does
not expose these, so they're fixed placeholders ('' / false / 0 / true),
matching go-cqhttp / NapCat. sex/age stay unknown/0 (real values need a
per-member profile fetch — out of scope here).
typecheck 12 packages; onebot 523 / core 472 green.
set_group_add_request could fail with "matching group request not found"
for a request SnowLuma had itself just reported. handleGroupAddRequest only
fetched the main inbox (0x10C8 subCommand 1); a spam-filtered join/invite
lives in the filtered inbox (subCommand 2) and was never searched, so its
flag couldn't be resolved to a sequence.
Fetch both inboxes and search the union. Each request keeps the `filtered`
flag its approval subCommand needs, so a filtered request is approved
through subCommand 2. A failing inbox fetch degrades to the other instead
of aborting. Regular MSF invite pushes don't carry the approval msgseq
(only the private-card path does, #125), so the fetch is the only source.
Regression tests: filtered-only request is found + approved via subCommand
2; "not found" only when neither inbox has it.
* feat(qzone): add set_qzone_msg_right to change a feed's visibility
Add updateQzoneMsgRight in the protocol layer, porting php-qzone's
updateRight: fetch the feed via emotion_cgi_msgdetail_v6, rebuild the
full publish-shaped payload (richval/pic_bo/con), then override
ugc_right/allow_uins and POST to emotion_cgi_update. Expose it through
QzoneApi.updateRight and the OneBot set_qzone_msg_right action, with the
same ugc_right/target_uins validation as send_qzone_msg. Covered by
protocol, core and onebot tests.
* fix(qzone): make qzoneParse more stable and fixed a test example