bridge-oidb.ts used to expose three thin functions —
makeOidbRequest / sendOidbAndCheck / sendOidbAndDecode — and every
action file repeated the same five-step envelope assembly with long
positional argument lists. Collapse them into one `runOidb(bridge,
call)` taking an OidbCall object: cmd / oidbCmd / subCmd / request
{ schema, value, isUid? } / response? { schema }.
Behavioural contract is preserved:
- No `response`: verify success + gotResponse, peek envelope retCode
iff response data is present (fire-and-check semantics).
- With `response`: require responseData, decode through the base
schema, validate retCode, return the inner body typed as TResp.
Migration touches 51 call sites across 9 source files (bridge-contacts,
actions/{group-file, group-admin, profile, group-message, friend,
interaction, misc}, web-actions/cookies) plus 9 test files that used
to assert on positional `.mock.calls[0]![4]` indices and now read
through `.mock.calls[0]![1].request.value` / `.cmd` / `.oidbCmd`.
Net effect: callers now name what each piece is for, and adding
cross-cutting OIDB behaviour (retry / timeout / per-cmd logging) is a
single-site change inside runOidb instead of a sweep across 9 files.
QQInfo's in-memory state (self identity, friends, group rosters,
profile cache) and read methods (findFriend / findGroup /
findGroupMember / findUserProfile / nickname / uin / selfProfile) now
live directly on IdentityService. The QQInfo class is gone; its
interfaces stay in `qq-info.ts` as plain types shared across the
remember*/find* surface.
This eliminates a long-standing duplication: every observation used to
maintain both QQInfo's in-memory structures and IdentityService's
SQLite mirror in parallel, with the uidByUin/uinByUid Maps tracked
separately. With one Module owning all three layers, every remember*
hydrates one set of structures and `loadSnapshot` repopulates the
in-memory uid↔uin index from SQLite on startup — closing the cold-cache
gap that previously forced findUidByUin to fall through to a SQLite
query after every restart.
Knock-on changes:
- `Bridge` no longer holds a QQInfo; constructor takes an
`IdentityService`. The CmdParser signature on the cmd dispatch
table now passes `IdentityService` to parsers.
- `msg-push-handler.ts` drops its private `resolveUidToUin(qqInfo, …)`
helper that bypassed IdentityService's cascade — the parser hot
path now reads through `identity.findUinByUid(uid, groupId)` and
benefits from the O(1) `uinByUid` map even on freshly-loaded
sessions.
- `BridgeManager`'s session-started callback drops the QQInfo
parameter; OneBotInstance no longer carries a `qqInfo` field and
exposes a `nickname` getter for the WebUI listing.
- `contact-actions` and the per-theme bridge action files stop
importing QQInfo; they read everything via `bridge.identity.*`.
Pull the cache→DB→network-fetch fallback cascade for UIN→UID translation
into a single async `resolveUid` on IdentityService, and drop the
duplicated `resolveUserUid` helper that used to live in bridge-oidb. Bridge
injects itself as the IdentityFetcher post-construction so the network
fallback runs through the same OIDB profile fetch the helper used.
Each action module's `resolveUserUid(bridge, uin, groupId?)` call becomes
`bridge.resolveUserUid(uin, groupId?)` — same signature on Bridge, but
internally it now goes through `identity.resolveUid` so future changes
(retry, coalescing, alternative resolvers) only need to land in one place.
This is part 1 of the IdentityService deepening (RoadMap item 1). The
QQInfo bypass in msg-push-handler and the broader QQInfo absorption land
in the follow-up commit.
Distinguish "message not found" from "private chats don't support
emoji reactions". The latter previously masqueraded as "not a group
message", which sounded like a missing feature; clarify that the
limitation is on QQ's side, not ours.
Bumps version, pushes dev with chore(release): prefix to trigger
Promote, waits for the PR to merge, then tags main and pushes
the tag to kick off the release workflow.
set_msg_emoji_like silently no-op'd for unicode emoji because the
reaction request didn't tell the server which emoji namespace the
code belongs to (QQ face vs unicode codepoint). Derive the type
from the code length — short numeric ids are QQ faces, longer
codepoint strings are unicode.
The NTV2 upload-response decoder used stub schemas that omitted fields
QQ NT echoes back (notably video.fromScene/toScene, pic.fromScene/
toScene/oldFileId, the ptt scene/waveform set), so finalizeMediaMsgInfo
re-encoded the commonElem.pbElem with those fields missing. For c2c
video this triggered `send private message rejected: result=79` on
every send_private_msg [{type:'video'}] call — the upload itself
fast-pathed fine but the follow-up SendMessage was rejected by NT.
Drop the duplicate NTV2 placeholder schemas and route NTV2ExtBizInfo
through the element-side PicExtBizInfo / VideoExtBizInfo /
PttExtBizInfo, which are field-complete. Also zero out the main
video sub-file's width/height/time/videoFormat in the upload request
to mirror NapCat — those fields belong on the thumb, not the source
mp4, and non-zero values trip schema validation on c2c sends.
The session warmup only set qqInfo.nickname when QQ's friend-list
response happened to echo self back, which it omits for some
accounts / versions / multi-account warmup races. The WebUI online
sessions card then rendered a blank nickname row above the UIN.
Fall back to a direct user-profile fetch when the friend-list scan
misses self, and seed qqInfo.nickname with the UIN at session
creation so the panel never paints empty while warmup is in flight.
- WebUI sidebar/login display and OneBot get_version_info now read the
root package.json version via Vite `define`, removing hardcoded
literals in webui/src/types.ts and core/onebot/actions/info.ts.
- tools/bump-version.mjs (exposed as `pnpm bump <ver>`) syncs the
version field across the root and all five workspace packages.
- Bump everything to 1.7.8.
get_forward_msg now returns full OB11Message objects (self_id, time,
message_id, sender, etc.) instead of node-segment wrappers, and event
emission keeps the resId on incoming forward segments instead of
dropping it via the unknown-element fallback. Extends ForwardNodePayload
with optional context fields so download/upload paths preserve enough
state to synthesize the OB11Message shape NapCat clients expect.
With SNOWLUMA_HOOK_AUTOLOAD=1, the QQ window came up blank — no QR code,
login stuck. Cause: the native enumerator (get_all_main_process_ids)
returned the main QQ pid plus 2 zygote subprocesses because their
cmdline contains "qq" and they transiently inherit wrapper.node via
fork copy-on-write. Auto-load fired loadModuleManual for all three,
spawning an in-process resolver thread inside each zygote. Renderer
processes Electron later forked from those zygotes then inherited
those resolver threads + the partially-initialised hook state, which
breaks QQ's IPC and prevents the login UI from rendering.
Add `shouldAutoLoadPid` which reads /proc/<pid>/cmdline and returns
false when it contains `--type=` (Electron child process marker:
zygote / renderer / gpu-process / utility). Manual loadProcess() is
unaffected — operators picking PIDs from WebUI keep full control.
SNOWLUMA_HOOK_AUTOLOAD_ALL=1 disables the filter as an escape hatch.
Verified on tempserver: container restart with autoload=1 now shows
the QR-code login screen as expected, and only the main qq pid gets
the mojo.<pid>.*.sock pair (zygotes stay clean).
Headless / Docker deployments previously needed a human to click
"Load" in WebUI for every newly-discovered QQ process. Add an opt-in
HookManager.autoLoadOnDiscovery flag wired up through two layers:
- SNOWLUMA_HOOK_AUTOLOAD env var (highest priority)
- hookAutoLoad in runtime.json (persistent fallback)
When enabled, every 'process-discovered' event from PipeWatcher fires
session.load() fire-and-forget; failures stay scoped to the session via
existing error-state handling and don't break the watcher loop.
Default stays off, so non-Docker installs see no behaviour change.
Two small cleanups that came out of the round-2 architecture review
and are sub-five-minutes each — bundled because each one is too
small to justify its own commit.
#5 — flatten single-file directories
packages/core/src/bridge/builders/element-builder.ts
-> packages/core/src/bridge/element-builder.ts
packages/core/src/bridge/handlers/msg-push-handler.ts
-> packages/core/src/bridge/msg-push-handler.ts
Both dirs had exactly one file in them — a smell that says
"planned a sibling that never landed". Moving up one level
removes the wrapper directory.
Touch list:
- bridge.ts: 2 import paths
- bridge/actions/forward.ts: 2 import paths
- msg-push-handler.ts: internal ../X -> ./X for events,
proto/element, proto/message (the
dynamic-import-typeof references too)
- element-builder.ts: internal ../X -> ./X for bridge,
events, highway/*
- 4 test files: import paths (2 regular, 2 vi.mock)
#6 — drop IdentityService.hasWriteFailure
identity-service.ts had a `private writeFailed = false` field set
by the runWrite catch block and exposed via `get hasWriteFailure()`,
but a full-repo grep showed zero callers of the getter. It was a
public API stub for a feature that hadn't been wired (a webui
health indicator, presumably).
Removed:
- the field
- the getter
- the assignment in runWrite's catch block (the error log on
the next line is the actual observable behavior; nothing else
reads the flag)
If we want a "did any write fail this session" surface later,
it's trivial to add back with an explicit consumer in mind.
Behavior preserved end-to-end. No public API change outside the
moved files. 369 tests pass (unchanged). 3 stability runs.
The OneBotInstance constructor still carried two non-trivial closures
inside its converterCtx object — a 26-line mediaUrlResolver that
branched bridge URL fetches by element.type + isGroup, and a 60-line
mediaSegmentSink that fanned out to mediaStore.rememberImage / Record /
Video with subtly different field maps. Both closed over `this` and
were untestable without instantiating the whole instance.
Extracted into two small, focused modules:
onebot/media-url-resolver.ts (64 lines)
class MediaUrlResolver(bridge, rkeyCache)
.resolve(element, isGroup, sessionId): Promise<string>
Picks the right Bridge.fetchXxx based on element.type + isGroup,
mutates element.url in-place (preserved behavior), then applies
the cached RKey via rkeyCache.resolveMediaUrl. Bridge-fetch
errors are swallowed so the RKey pass always runs.
onebot/media-indexer.ts (90 lines)
class MediaIndexer(mediaStore)
.remember(mediaType, element, data, isGroup, sessionId): void
One method, dispatches to mediaStore.rememberImage / Record /
Video with the right field map per mediaType. Defaults are
preserved exactly (?? 0 for missing numbers, ?? '' for strings).
OneBotInstance constructor shrinks from 134 lines to ~50 lines (-62%).
The converterCtx is now visually obvious — four callbacks each one
line, delegating to the right collaborator:
const mediaUrlResolver = new MediaUrlResolver(this.bridge, this.rkeyCache);
const mediaIndexer = new MediaIndexer(this.mediaStore);
this.converterCtx = {
selfId: parseInt(this.uin, 10) || 0,
imageUrlResolver: (el, ig) => this.rkeyCache.resolveImageUrl(this.bridge, el, ig),
mediaUrlResolver: (el, ig, sid) => mediaUrlResolver.resolve(el, ig, sid),
messageIdResolver: (ig, sid, seq, name) => hashMessageIdInt32(...),
mediaSegmentSink: (mt, el, d, ig, sid) => mediaIndexer.remember(mt, el, d, ig, sid),
};
Adds 19 happy-path tests:
media-url-resolver.test.ts (12 tests):
- file element: group / c2c-with-hash / c2c-without-hash
- record element: group / c2c
- video element: group / c2c
- record without mediaNode: no bridge call
- element.url already set: short-circuit, only rkey runs
- bridge throws: swallowed, rkey still applied
- rkey is always called at the end
media-indexer.test.ts (7 tests):
- image: full field map, file falls back to fileId
- record: full field map, file falls back to fileName
- video: full field map, data.file overrides element fields
- record: file falls back to fileId when fileName also missing
- missing numeric / string fields default to 0 / ''
- only the matching rememberX is called
Behavior preserved end-to-end: same bridge methods, same field
mappings into MediaStore, same in-place element.url mutation, same
RKey-always-runs guarantee, same best-effort error swallowing.
The existing media-store integration test (which still wires the
sink through the real converter) continues to pass unchanged.
334 tests pass (was 317); 3 stability runs, same pre-existing
macOS-arm64 websocket addon failure, unrelated.
event-converter.ts (541 lines) was a class with a 16-case switch
mapping bridge events to OneBot wire JSON, plus a 13-case helper
mapping bridge elements to OneBot segments, plus four lazily-injected
resolver setters (image-url / media-url / message-id / media-segment
sink). Testing one case required instantiating the class and wiring
four setters; 22 of the 29 cases (10 notices + 2 requests + 13 element
types) had zero tests.
This commit kills the class and replaces it with a pure-function
family that takes a ConverterContext (a plain object holding selfId
+ the four resolver callbacks). Each per-event handler becomes a
small typed function that's trivial to call from a test with two
lines of setup.
New layout: packages/core/src/onebot/event-converter/
index.ts 133 ConverterContext + four resolver types +
convertEvent dispatcher + re-exports.
to-message.ts 112 friend / group / temp message (3 handlers)
to-notice.ts 189 group_member_join / leave / mute / admin /
friend_recall / group_recall / friend_poke /
group_poke / group_essence / group_file_upload /
friend_add (11 handlers)
to-request.ts 37 friend_request / group_invite (2 handlers)
to-segment.ts 145 13 element -> segment cases + elementsToJson +
elementsToOneBotSegments
utils.ts 111 parseSelfId / isSameActor / applyMessageIdResolver /
resolveReplyId / segmentsToRawMessage + CQ encoding
Caller migration (3 files, all inside onebot/):
instance.ts: replaces `new EventConverter()` + four setter
calls with a single `converterCtx: ConverterContext`
object built once at construction. The four
closures that used to be passed through setters
are now properties of the context.
instance-context.ts: `eventConverter: EventConverter` field renamed
to `converterCtx: ConverterContext`.
event-pipeline.ts: `ctx.eventConverter.convert(ctx.uin, event)` is
now `convertEvent(ctx.converterCtx, event)`.
External callers (modules/message-actions.ts) keep working unchanged:
elementsToOneBotSegments is re-exported from the new index.ts with
the same signature.
Adds 28 new tests covering every previously-untested case:
message kinds (already partly covered): 7 tests
- friend / group / temp_message peer + self post_type tagging
- messageIdResolver override
notice kinds (new): 11 tests
- all 11 cases, including the sub-type axes
(approve/invite, leave/kick/kick_me, ban/lift_ban,
set/unset, add/delete, etc.)
request kinds (new): 2 tests
- friend_request, group_invite (with sub_type default)
element segments (new): 13 tests
- text / face / image (with + without resolver) / at /
at-all / reply (with + without resolver) / record /
video / json / xml (with + without subType) / file /
mface / poke
mediaSegmentSink wiring (new): 1 test
Plus 3 tests in media-store.test.ts and 1 in event-pipeline.test.ts
adapted to the new API.
317 tests pass (was 285); 3 stability runs, same pre-existing
macOS-arm64 websocket addon failure, unrelated.
The three highway uploaders — image-upload, ptt-upload, video-upload —
each implemented the same NTV2 envelope assembly, OIDB response decode,
error handling, and Highway PUT loop, with the only real per-format
differences being the file-info fields, the extBizInfo blob, and the
Highway command id. The duplicated boilerplate had drifted slightly
between files and any future NTQQ protocol change would have needed
parallel edits in three places.
New: packages/core/src/bridge/highway/pipeline.ts (249 lines).
runNtv2Upload({ bridge, isGroup, targetIdOrUid, oidbCmd, serviceCmd,
requestId, businessType, uploadInfo, compatQmsgSceneType,
extBizInfo, uploads, label })
-> awaited upload object
Builds the full reqHead + upload envelope, sends through OIDB,
decodes the response, runs the per-sub-file Highway PUTs (caching
the Highway session across them — video uses two PUTs and now only
fetches the session once), and surfaces a typed fastOnlyError when
the server demands bytes but the caller passed a fingerprint.
finalizeMediaMsgInfo(upload, defaultPic?) -> Uint8Array
Encodes the outgoing commonElem MsgInfo. `defaultPic` is the
image-only fallback: image fills bizType + textSummary when the
server omits the pic ext-biz-info; ptt / video pass undefined.
hexToBytes(hex), makeClientRandomId(): shared helpers used by every
uploader's fingerprint payload + client-randomId generation.
MediaSubFileUpload models per-sub-file routing:
- source: 'top' -> upload.uKey + upload.ipv4s (main file)
- source: N -> upload.subFileInfos[N].uKey + .ipv4s (video thumb)
- cmdId, bytes, md5, sha1, subFileIndex, fastOnlyError
Each upload file shrinks to its format-specific logic only:
image-upload.ts: 237 -> 163 (-31%)
format detection, picFormat mapping, pic/reserveTroop/reserveC2c
extBizInfo, group/c2c command id choice, finalize with defaultPic.
ptt-upload.ts: 300 -> 205 (-32%)
silk transcoding, temp-file cleanup, NapCat-style bytesGeneralFlags
(different per group/c2c), voiceFormat honouring.
video-upload.ts: 584 -> 507 (-13%)
Sha1StreamState (per-1MB streaming SHA1 for the main Highway PUT),
ffmpeg-driven thumb extraction with FALLBACK_THUMB, the two-sub-file
OIDB body (main mp4 + thumb jpg with subFileType 100), and the
fastOnlyError asymmetry (main file has it; thumb always has bytes).
Video stays the largest because its load step is genuinely the most
work — the share-out gain here is in concentration, not line count.
Behavior preserved end-to-end. Same NTV2 OIDB cmd numbers (image
0x11C4/5, ptt 0x126E/D, video 0x11EA/9), same service-cmd strings,
same requestId (image=1, ptt=group:1/c2c:4, video=3), same
compatQmsgSceneType (image+ptt: isGroup?2:1; video: 2), same
NapCat-derived bytesGeneralFlags, same fastUpload fallback strings.
Adds 42 happy-path tests across 4 new files in tests/highway/ :
pipeline.test.ts 22 envelope construction, response decode,
OIDB error paths, group vs c2c scene,
Highway PUT trigger + fast-path skip,
sub-file routing, session caching,
fastOnlyError fire / silent-skip,
finalize with/without defaultPic, helpers.
image-upload.test.ts 7 group/c2c oidbCmd + cmdId mapping,
fingerprint payload shape, picFormat /
dimensions wiring, subType -> bizType.
ptt-upload.test.ts 6 group requestId=1 / c2c requestId=4,
per-scene bytesGeneralFlags, voiceFormat
default and override, fingerprint
preconditions, finalize-without-pic.
video-upload.test.ts 7 two-sub-file shape, thumb subFileInfos[0]
routing, fastOnlyError asymmetry,
uploadInfo[mp4+jpg] with subFileType 0+100,
FALLBACK_THUMB always has bytes.
Tests mock highway-client for transport and the pipeline module (for
per-uploader tests) so each suite hits exactly the contract it's
testing. Pipeline tests roundtrip a real OIDB envelope through
protobuf encode/decode to exercise the actual decode path.
285 tests pass (was 244). Same pre-existing macOS-arm64 websocket
addon failure, unrelated.
ApiActionContext had 81 optional method fields (\`name?: ...\`) plus 4
required ones — but every single optional field was wired in
buildApiContext (verified by diffing the two lists). The optionality
was dead: nothing else implemented this interface, no test ever
swapped fields out, and the action handlers all paid for the
fiction with defensive guards like:
if (!ctx.sendGroupMessage) {
return failedResponse(RETCODE.ACTION_FAILED, 'send_group_msg is not implemented');
}
— 80 such guards total across the action files (78 in the 3-line
form, 1 single-line, 1 \`if (!(ctx.a && ctx.b))\` compound). None of
them could ever fire.
This commit deletes the dead optionality. Two mechanical changes:
1. api-handler.ts: 81 \`name?: ...\` -> \`name: ...\` in ApiActionContext.
The MessageSendResult optionals (meta?, echoEvent?) are unrelated
and stay as-is. After: 85 required fields, 0 optionals.
2. actions/*.ts: drop the 80 guard blocks. Net -129 lines across
extended, group-admin, group-file, group-info, friend, message,
request. info.ts had none to begin with.
What this buys:
- The type system now enforces the wiring contract. If
buildApiContext forgets to wire a field, tsc fails at the call
site — the bug we were ostensibly catching at runtime in 80 places
is now caught at compile time in 0 places, for free.
- Action handler bodies shrink visibly (avg ~3 lines per handler).
extended.ts goes 1021 -> 935.
- AI / human readers no longer skim past dead code.
Caught one TS2774 ("This condition will always return true since
this function is always defined") on a compound guard in
extended.ts:186 that the regex missed; removed it manually. That
diagnostic is the type system actually doing its job — exactly the
point of the change.
244 tests pass (was 244). Same pre-existing macOS-arm64 websocket
addon failure, unrelated.
bridge-actions.ts was a single 1814-line file holding 55 unrelated
exported functions — admin/file/forward/interaction/recall/profile/
status/translate/sign/avatar all jumbled together. Each function
already takes `bridge: Bridge` as its first parameter, so the
"deepening" target (per-UIN state object threaded into themed action
functions) was structurally there — what was missing was a coherent
file-level grouping that AI tools and humans could navigate.
This commit is candidate #2 from the deepening list, A-only variant:
pure file split, no facade change, no caller change outside the
bridge module.
New layout under packages/core/src/bridge/actions/ :
shared.ts 82 private helpers (toInt, ensureRetCodeZero,
resolveSelfUid) and the MediaIndexNode type.
group-admin.ts 153 mute/kick/leave + setGroup{Admin/Card/Name/
SpecialTitle/AddOption/Search/Remark/AddRequest}
+ getGroupAtAllRemain. (14 fns)
group-file.ts 821 upload/list/url/delete/move/folder ops, both
group + private files, media-URL fetchers for
ptt/video, fetchGroupFileCount. Owns file-
specific helpers (normalizeDirectory,
buildGroupFileUploadExt, normalizeMediaNode,
fetchNtv2DownloadUrl, ...). (15 fns)
group-message 101 recall* / markRead* / setGroupEssence. (5 fns)
interaction.ts 61 poke / like / reaction / emoji-likes. (4 fns)
forward.ts 197 uploadForwardNodes, fetchForwardNodes +
in-memory resId cache. (2 fns)
friend.ts 53 setFriendAddRequest, deleteFriend,
setFriendRemark. (3 fns)
profile.ts 269 setOnlineStatus, setProfile, setSelfLongNick,
setInputStatus, setAvatar, fetchCustomFace,
getProfileLike, getUnidirectionalFriendList.
(8 fns)
misc.ts 173 translateEn2Zh, getMiniAppArk,
clickInlineKeyboardButton, sendGroupSign.
(4 fns)
bridge.ts updates its imports — one themed import block per action
module instead of one giant block from bridge-actions.ts. GroupFilesResult
and MediaIndexNode type imports now point at actions/group-file and
actions/shared. Public Bridge facade API is unchanged: every caller
outside `bridge/` keeps the same `bridge.X(...)` surface and the
delegated function it points at.
bridge-actions.ts is deleted. The new shared.ts holds the three
helpers that span themes; everything else lives co-located with the
functions that use it.
Adds 60 happy-path tests across tests/actions/ — one .test.ts per
themed file plus a shared `_helpers.ts` (mockBridge fixture):
group-admin 10 command + payload dispatch, UID resolution,
flag-driven sub-command selection (mute on/off,
add-request filtered/unfiltered).
group-file 13 fetchGroupFileCount, upload happy path with
and without highway, OIDB error bubbling, file
listing pagination + folder/file split, URL
builder shape, delete/move/folder dispatch,
NTV2 URL assembly.
group-message 5 recall (group/private), markRead, essence on/off,
transport failure propagation.
interaction 6 poke (group vs friend payload), like, reaction
on/off, emoji-likes cookie/isLast logic.
forward 5 empty-array rejection, dispatch + res_id return,
transport failure, cache reuse after upload.
friend 5 numeric flag → UID resolution path, opaque flag
passthrough, delete + best-effort cache refresh.
profile 9 status dispatch, setProfile no-op when undefined,
setAvatar highway cmd 90, getProfileLike +
getUnidirectionalFriendList JSON parsing.
misc 6 translate, mini-app type guard, click + sign
dispatch.
Tests use vi.mock on `bridge-oidb` (and `highway-client`/`highway-utils`
where relevant) so each action runs against a tiny fake Bridge, no
native pipe / real OIDB exchange needed. Previously these 55 functions
had zero unit-test coverage.
244 tests pass (was 184); only failing suite remains the pre-existing
macOS-arm64 missing websocket native addon, unrelated.
Captures protocol-level media fingerprints (md5/sha1/dims/format) on the
receive side and persists them alongside existing MediaStore entries, so a
forward can re-send the original message via QQ's md5 fast-upload path
without re-downloading the source CDN bytes. Avoids the stale-rkey trap
where the cached imageUrl 404s a few hours after receipt.
- MessageElement: add md5Hex / sha1Hex / pic|video|voiceFormat /
noByteFallback optional fields.
- MediaStore: extend CachedImage / CachedRecord with fingerprints, add
CachedVideo + rememberVideo / findVideo / updateVideoUrl. Per-row growth
~80-90 bytes; LRU cap stays at 4096 per type.
- msg-push-handler: extract fingerprints from NTV2 image/record/video and
legacy NotOnlineImage/CustomFace/Ptt paths.
- event-converter: invoke mediaSegmentSink for video too.
- highway image/ptt/video upload: add fast-only branch that skips
loadBinarySource/encodeSilk/stageVideoSource when noByteFallback is set,
and fails fast if the server still returns uKey. Video forward uses the
100-byte FALLBACK_THUMB as its subFile (original thumb not preserved in
onebot semantics).
- message-actions: add forwardSingleMessage which looks up the cached
event, enriches each media element with cached fingerprints, and
dispatches via the existing sendGroup/sendPrivateMessage path. Files
reject explicitly (separate upload protocol, out of scope).
- extended actions: wire forward_friend_single_msg / forward_group_single_msg
to the new context method, replacing the not-implemented stubs.
NtqqHandler was a 54-line "shallow module": half of its public surface
(registerCmd by service-cmd) had zero callers — cmdHandlers was a Map
that nothing ever wrote to. The remaining half (registerCmdAll) had
exactly one subscriber: BridgeManager.bind() wiring its private
onPacket. So NtqqHandler was, in effect, a one-listener event emitter
plus a five-line field-rename from QqHookPacket to PacketInfo.
The deletion test concentrates the change rather than scattering it:
- HookSession.handlePacket inlines the field rename and pushes
straight into a PacketSink dependency (PacketInfo => void).
HookSession's ctor now takes `onPacket?: PacketSink` instead of
`ntqq?: NtqqHandler` — the new shape is exactly what tests want
(a vi.fn() spy with no event-emitter scaffolding).
- HookManager's `ntqq: NtqqHandler` constructor dep becomes
`onPacket?: PacketSink`, defaulting to `bridgeManager.onPacket` so
the common-case wiring stays one line at the call site.
- BridgeManager.bind(ntqq) is deleted; BridgeManager.onPacket becomes
public (it was always the real entry point). The file header
comment now describes onPacket as the single packet entry rather
than "wires NtqqHandler".
- index.ts shrinks: no `new NtqqHandler()` and no `bridgeManager.bind`.
`new HookManager({ bridgeManager })` covers both wires.
- packages/core/src/protocol/ntqq-handler.ts deleted.
Behavior preserved: same field rename (Buffer.from(packet.body) for
the body copy that QqHookClient already does, kept to match
byte-for-byte), same login + real-uin filter, same logging scope is
gone (`createLogger('NTQQ')` had no remaining call sites). PacketSink
type alias added to protocol/types.ts so the dep type stays readable
across HookSession + HookManager.
Adds 5 unit tests for the inlined packet path that the deleted module
never had: PacketInfo shape, session-uin fallback when packet.uin is
empty, pre-login drop, non-real-uin drop, and a no-sink no-op.
184 tests pass (was 179); only failing suite is the pre-existing
macOS-arm64 missing websocket native addon, unrelated.
HookManager was a 515-line god class with four tangled responsibilities
(state machine, watcher loop, per-PID serialization, lifecycle plumbing)
and no tests — every code path needed a real QQ.exe to exercise.
Split into three modules:
- PipeWatcher: the only module that polls the OS for QQ.exe processes
and live SnowLuma named pipes. Emits diff events (process-discovered,
process-gone, pipe-up, pipe-down, tick). Native list functions are
constructor deps so tests can drive ticks with in-memory fakes.
- HookSession: per-PID lifecycle. Owns its own state machine, its own
promise chain (serializes load/unload/refresh/onPipeUp/onPipeDown so
rapid user clicks don't race), and its own QqHookClient. Emits
high-level events (login, disconnected, status-changed, disposed)
that the manager forwards instead of reaching into BridgeManager
directly. Injector and pipe-client factory are deps for testability.
- HookManager: shrinks to a thin orchestrator (227 lines, was 515).
Routes user commands and watcher events to sessions, forwards session
events to BridgeManager, retries stuck-in-connecting sessions on every
watcher tick (preserves the original tickWatcher reconcile pass).
Public API (listProcesses/loadProcess/unloadProcess/refreshProcess)
unchanged for WebUI callers; only the constructor moves to a deps
object: new HookManager({ ntqq, bridgeManager }).
Behavior preserved including all the edge cases the original code
handles: re-using a SnowLuma DLL that survived a process restart
(fast-path via pipeWatcher.isPipeLive), stale close events from a
replaced client, DLL-unload-failed fallback to "watcher will
reconnect", QQ.exe killed with a live pipe (pipe-down emitted before
process-gone so disconnect runs before dispose).
Adds 27 unit tests across two new vitest suites (hook-session.test.ts
and pipe-watcher.test.ts) covering load fast-path, login emission,
duplicate-login suppression, connect retry on failure, pipe up/down
cycles, unload while logged in, unload verification failure, user
command serialization, and process-gone handling — none of which
previously had test coverage.
* feat: implement more group admin action (kick members and set search and set add type)
* feat: implement some user extend func (set group mark \\ get custom emoji)
* feat: implement get send emoji his
* feat: implement get profile like
* feat: implement get at all remain
* feat: implement get undirectional friends
* feat: implement set self long nick
* feat: implement set input status
* feat: implement EN 2 ZH
* feat: implement get mini app ark
* feat: implement click inline button
* feat: implement set and send group sign
* feat: implement set qq avatar
* feat: group notice with pic and delete notice implement
* fix(group-notice): POST api and fix new bkn logic
Add a `force` flag to member cache refresh logic so events that affect the bot itself trigger an immediate refresh. When the event userUin matches the bot's UIN, `triggerMemberCacheRefresh` sets `force = true` and forwards it to `fetchGroupMemberList`. `fetchGroupMemberList` now accepts an optional `force` parameter (default false) and bypasses the TTL cache when `force` is true, ensuring up-to-date member data after join/leave events.