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.
Stop exposing and retaining hook state for PIDs that are not currently running. listProcesses() no longer returns orphaned states; live-pipe handling ignores PIDs not present in the current process list. When a process is not running its state is immediately torn down (tearDownClient called) and removed, and bridgeManager.onPidDisconnected is notified if the state was logged in. Also reorganized runningSet creation so it can be reused for these checks.
Introduce NT SILK-based voice (PTT) upload pipeline and native FFmpeg addon wrapper.
- Add ffmpeg-addon wrapper (packages/core/src/bridge/highway/ffmpeg-addon.ts) to load platform/arch .node binaries, detect SILK files, and convert inputs to NT SILK via the native addon. Provides encodeSilk, isSilkFile and default temp dir helpers.
- Implement ptt-upload pipeline (packages/core/src/bridge/highway/ptt-upload.ts) to: resolve/fetch audio bytes, convert to NT SILK if needed, send OIDB upload request, perform highway HTTP upload when required, and produce Encodable MsgInfo bytes for commonElem.
- Wire record element support into element-builder: uploadPttMsgInfo is used to build a commonElem for 'record' sends; when SendContext is absent, record sends are skipped with a warning (graceful no-bridge path).
- Update message parsing to accept record segments from data.file, data.url or data.path and drop empty sources.
- Enable canSendRecord in OneBot instance context.
- Add tests covering record parsing and the graceful send path (packages/core/tests/message-parser.test.ts).
- Include prebuilt native ffmpeg addon binaries under packages/runtime/native/ffmpeg and update vite config to validate & copy the ffmpeg addon into the dist layout.
These changes enable sending voice messages by converting/encoding sources to the expected NT SILK format and uploading them through the existing highway/NTV2 flows.
Translate and consolidate verbose WebUI log messages to shorter English variants (dev mode, initial credentials, forced password change, password update, port fallback, and listening message). Remove a startup log emitted in core index.ts. In HookManager, preserve an existing state.method when detecting an already-injected PID (only set to 'reconnect' if not present) and remove a redundant injection success log to reduce noise.
Introduce a typed per-kind BridgeEventBus and migrate away from the legacy single-callback firehose. Bridge now exposes `events` and emits via the bus; BridgeManager/OneBotManager no longer wire a global event callback. Add a focused onebot event pipeline (event-pipeline.ts) that subscribes per-kind handlers on instance construction. Replace the in-memory MediaCache with a persistent MediaStore and update all lookups accordingly. Replace the old Http/Ws/HttpPost transport implementations with a unified OneBotNetworkManager and pluggable adapters (server/client/post), and update OneBotInstance to install and manage adapters from config. Remove several legacy files (http-transport, http-post-transport, ws-transport, media-cache) and update imports across the codebase. Also add tests for the bridge event bus, event pipeline, network manager, and rename media tests to reflect MediaStore usage. The bus dispatches handlers in parallel and reports handler errors via an onError hook to avoid one faulty listener blocking others.
Introduce an in-memory MediaCache to index recently seen image and record segments and wire a MediaSegmentSink into EventConverter to populate it. OneBotInstance now constructs MediaCache and registers a sink to remember image/record metadata; instance-context exposes get_image/get_record lookup handlers that consult the cache (and re-resolve record URLs via bridge OIDB when needed). ApiHandler actions get_image/get_record now accept file_id fallback and use the new context lookups. Removed the old findMediaInStore stub and added comprehensive tests for the cache and sink behavior.
Switch default merge_method from 'merge' to 'squash' so dev->main PRs collapse the dev branch's incremental commits into one squash commit on main, keeping main's history linear and release-tag-friendly. Affects: workflow_dispatch input default, decide-step initial value + workflow_dispatch fallback, both push-trigger branches (tag + commit-msg), and the case-statement fallback in the auto-merge step. CONTRIBUTING.md updated to reflect the new default.