Commit Graph
126 Commits
Author SHA1 Message Date
snowluma-bot[bot] c2f22d8daf Merge pull request #29 from SnowLuma/dev
chore: promote dev → main (6abde40)
v1.7.8
2026-05-13 16:23:42 +08:00
DrSmoothl 6abde40b9c [merge] fix Linux auto load issue 2026-05-13 16:20:43 +08:00
DrSmoothl 6760008c5c chore: refresh prebuilt linux-x64 native binary 2026-05-13 16:18:21 +08:00
DrSmoothl 37b835c537 chore: refresh prebuilt linux-x64 native binary 2026-05-13 16:18:21 +08:00
DrSmoothl 9068ce3108 fix(core): skip Electron child processes during hook auto-load
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).
2026-05-13 00:57:48 +08:00
snowluma-bot[bot] 9216b54427 Merge pull request #28 from SnowLuma/dev
chore: promote dev → main (1b2dd83)
v1.7.7
2026-05-12 22:06:52 +08:00
DrSmoothl 1b2dd836b8 [merge] upd & refactor 2026-05-12 22:06:04 +08:00
DrSmoothl fbb7504afe feat(core): auto-inject hook on QQ process discovery
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.
2026-05-12 22:04:11 +08:00
DrSmoothl dd02535a73 chore(core): flatten single-file dirs + drop dead writeFailed surface
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.
2026-05-12 20:59:25 +08:00
DrSmoothl 069548918b refactor(core): split web-actions.ts into themed modules + barrel
web-actions.ts was a 330-line monolith with 14 exported functions
covering 4 distinct themes: cookie/credential primitives (the 8
functions every web call builds on), group honor leaderboards, group
essence (highlighted messages), and group notices. Same shape problem
the OIDB bridge-actions.ts split fixed three commits back, just smaller.

New layout under packages/core/src/bridge/web-actions/ :

  cookies.ts        (161 lines, 8 fns)
    forceFetchClientKey / getPSkey / getCookies / getSKey /
    getBknFromSKey / getCookiesStr / getCsrfToken / getCredentials
    — the ptlogin2-jump + bkn-derivation primitives.

  group-honor.ts    (45 lines, 1 fn)
    getGroupHonorInfo (talkative / performer / legend / emotion / ALL)

  group-essence.ts  (27 lines, 2 fns)
    getGroupEssence (paged) + getGroupEssenceAll

  group-notice.ts   (118 lines, 3 fns)
    sendGroupNotice (with optional image-from-URL-or-file upload),
    getGroupNotice (OneBot shape flatten), deleteGroupNoticeByFid

  index.ts          (27 lines)
    Barrel re-export. `bridge.ts` keeps its existing
    `from './web-actions'` import — TS resolves the directory's
    index.ts so no caller changes.

The lower-level web/* helpers (request-util, group-essence,
group-honor, group-notice) stay where they were — they're the
direct HTTP/cookie wrappers, distinct from these Bridge-level
orchestrators.

Behavior preserved end-to-end. Same ptlogin2 URL construction,
same fall-back-to-getPSkey-when-p_skey-missing path in getCookies,
same default keyIndex='19' / expireTime='1800', same djb2 bkn hash,
same NapCat-derived default 540x300 notice image dimensions, same
chinese error messages ('设置群公告失败', '获取公告失败').

Adds 35 happy-path tests across 4 new files in tests/web-actions/ :

  cookies         (13)  forceFetchClientKey response shape + defaults,
                        getPSkey pskeyItems folding, getCookies ptlogin2
                        URL params, p_skey fallback path (both success
                        and throwing), getSKey success / no-clientKey /
                        no-skey throws, getBknFromSKey determinism +
                        range, getCookiesStr formatting, getCsrfToken
                        composition, getCredentials full + empty paths.
  group-honor      (7)  TALKATIVE list vs empty list behaviour,
                        PERFORMER / LEGEND / EMOTION per-type dispatch,
                        ALL fetches all four types, groupId stringification.
  group-essence    (4)  pageStart/pageLimit forwarding, defaults,
                        null-result fallback envelope, getAll delegation.
  group-notice    (11)  text-only dispatch + defaults, options override,
                        http image fetch + upload + thread, fetch failure,
                        ec≠0 surfaces em, generic error fallback,
                        getGroupNotice feed flattening + pics mapping,
                        null-result throw, empty-feeds case,
                        deleteGroupNoticeByFid argument forwarding.

Tests use vi.mock on bridge-oidb / web/request-util / web/group-*
to isolate each theme from real HTTP, mirroring the pattern from
the bridge-actions split.

369 tests pass (was 334); 3 stability runs, same pre-existing
macOS-arm64 websocket addon failure, unrelated.
2026-05-12 20:49:36 +08:00
DrSmoothl 8a49f0c21d refactor(core): extract MediaUrlResolver + MediaIndexer from OneBotInstance
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.
2026-05-12 20:35:12 +08:00
DrSmoothl 49281726f1 refactor(core): split EventConverter god switch into themed pure functions
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.
2026-05-12 18:45:28 +08:00
DrSmoothl 269cd6f081 refactor(core): extract NTV2 upload pipeline shared by image/ptt/video
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.
2026-05-12 17:46:56 +08:00
DrSmoothl b32a8110af refactor(core): make ApiActionContext required, drop 80 dead defensive guards
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.
2026-05-12 17:09:56 +08:00
DrSmoothl 8506b60d07 refactor(core): split bridge-actions.ts (1814 lines) into themed action modules
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.
2026-05-12 16:51:34 +08:00
DrSmoothl e91a506bf5 feat(onebot): implement forward_{group,friend}_single_msg via md5 fast-upload
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.
2026-05-12 16:22:16 +08:00
DrSmoothl ef4a321175 refactor(core): drop NtqqHandler shim, route packets directly to BridgeManager
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.
2026-05-12 14:56:38 +08:00
DrSmoothl 4fc52578a5 refactor(core): split HookManager into PipeWatcher + HookSession
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.
2026-05-12 14:43:30 +08:00
DrSmoothl 5ce8aa60de refactor onebot action context modules 2026-05-12 14:40:48 +08:00
snowluma-bot[bot] fd9a239241 Merge pull request #27 from SnowLuma/dev
chore: promote dev → main (8636222)
v1.7.6
2026-05-11 15:32:46 +00:00
DrSmoothl 8636222d91 [merge] fix & upd 2026-05-11 23:30:08 +08:00
DrSmoothl 6f506ff52c fix onebot video file sending 2026-05-11 23:26:45 +08:00
DrSmoothl e917720d9d fix linux hook stale socket detection 2026-05-11 23:14:27 +08:00
DrSmoothl af80855d2d fix(core): persist identities from request events 2026-05-11 19:16:39 +08:00
DrSmoothl 9a30a40a1f feat(core): add persistent identity service 2026-05-11 19:09:05 +08:00
H3CoF6 df5e3a26ac implement some onebot actions (#26)
* 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
2026-05-11 16:00:25 +08:00
DrSmoothl bf0a89e221 fix(core): resolve group join member identity 2026-05-10 17:38:05 +08:00
snowluma-bot[bot] 5a4028d5dc Merge pull request #25 from SnowLuma/dev
chore: promote dev → main (6f6ce11)
v1.7.5
2026-05-10 10:17:23 +08:00
H3CoF6 6f6ce1144b 补全一些onebot action实现 2 (#24)
* 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
chore.onebot2
2026-05-10 10:14:41 +08:00
snowluma-bot[bot] d02e7cc3ab Merge pull request #23 from SnowLuma/dev
chore: promote dev → main (bec23c4)
2026-05-10 01:32:20 +08:00
DrSmoothl bec23c4dbb [merge] promote SDK changes 2026-05-10 01:29:57 +08:00
DrSmoothl c343021318 feat: add SnowLuma TypeScript SDK 2026-05-10 01:26:35 +08:00
DrSmoothl 9ab4128799 Implement video message upload 2026-05-09 23:06:55 +08:00
手瓜一十雪 95df5234fb Support forced group member cache refresh
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.
2026-05-09 19:54:20 +08:00
snowluma-bot[bot] 9938545788 Merge pull request #21 from SnowLuma/dev
chore: promote dev → main (a7b3809)
v1.7.3
2026-05-09 19:40:04 +08:00
手瓜一十雪 a7b3809a12 Only track hook states for running PIDs
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.
chore.trackpid
2026-05-09 19:38:52 +08:00
snowluma-bot[bot] 7149bc8689 Merge pull request #20 from SnowLuma/dev
chore: promote dev → main (c39b3f5)
v1.7.2
2026-05-09 19:33:40 +08:00
手瓜一十雪 c39b3f5c4e Add PTT (voice) upload support with FFmpeg addon
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.
chore.record
2026-05-09 19:32:13 +08:00
snowluma-bot[bot] 9802128421 Merge pull request #19 from SnowLuma/dev
chore: promote dev → main (2af273b)
v1.7.0
2026-05-09 16:40:37 +08:00
手瓜一十雪 2af273b9e2 Simplify WebUI logs and adjust hook reconnect
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.
chore.webui.log
2026-05-09 16:36:03 +08:00
手瓜一十雪 144e0fdd59 Add BridgeEventBus and network manager
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.
2026-05-09 16:30:06 +08:00
墨梓柒 a572d05c25 Merge pull request #18 from SnowLuma/dependabot/npm_and_yarn/hono-4.12.18
build(deps-dev): bump hono from 4.12.14 to 4.12.18
2026-05-09 16:03:21 +08:00
dependabot[bot] cb06624d12 build(deps-dev): bump hono from 4.12.14 to 4.12.18
Bumps [hono](https://github.com/honojs/hono) from 4.12.14 to 4.12.18.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.14...v4.12.18)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.18
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-09 08:01:11 +00:00
snowluma-bot[bot] 7b93839186 Merge pull request #17 from SnowLuma/dev
chore: promote dev → main (1565749)
2026-05-09 15:37:22 +08:00
手瓜一十雪 1565749105 Add MediaCache and media segment sink
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.
chore.mediacache.1565749
2026-05-09 15:33:13 +08:00
手瓜一十雪 e53a8a01b7 Revert "[merge] dev & ci: default promote-dev-to-main merge strategy to squash"
This reverts commit 3afbcfaae5.
2026-05-09 15:08:41 +08:00
手瓜一十雪 3afbcfaae5 [merge] dev & ci: default promote-dev-to-main merge strategy to squash
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.
2026-05-09 15:06:54 +08:00
snowluma-bot[bot] d0085ec052 Merge pull request #15 from SnowLuma/dev
chore: promote dev → main (c3b328f)
2026-05-09 15:04:19 +08:00
手瓜一十雪 c3b328f91d [merge] dev 2026-05-09 15:01:46 +08:00
手瓜一十雪 7942c47808 [merge] dev 2026-05-09 14:58:37 +08:00