Commit Graph
27 Commits
Author SHA1 Message Date
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl a71377ff17 feat(onebot): add actions for group category tags
get_group_tags lists the tags under one group category. add_group_tag
and delete_group_tag add or remove a tag by name. Callers pass the
category number as group_class. An empty name is rejected.

Refs #506
2026-10-05 23:51:18 +08:00
DrSmoothl 5441203b76 fix(onebot): explain when member permission changes need the owner
Admins were getting an empty protocol error when changing group member
permissions. Report that only the group owner can make that change.

Fixes #411
2026-08-22 19:25:23 +08:00
DrSmoothl 6782fd9cb3 fix(protocol): treat a successful group kick as success
QQ reports a per-member kick outcome in the command body. A zero
outcome means the member was removed; treat that as success instead
of a false failure. Still reject a non-zero outcome so callers do
not see success when the kick did not take effect.

Fixes #413
2026-08-22 11:12:58 +08:00
DrSmoothl e6aeef02b4 fix(protocol): apply member permission updates on the native group setting command
set_group_member_permissions was still rejected after the previous
command switch. Linux QQ writes these capabilities with the same
group-detail command as the other admin settings.

Closes #392
2026-08-19 21:46:44 +08:00
DrSmoothl 0a924634b2 fix(protocol): return group join question and answer in admin settings
get_group_admin_settings reported the join mode correctly but left the
question and answer blank. The detail request now asks for those
strings the same way the official client does.

Closes #393
2026-08-19 00:15:32 +08:00
DrSmoothl b088ffaff0 fix(protocol): apply member permission updates on the correct group setting command
set_group_member_permissions was still rejected after the privilege
flag read was corrected. Album, temporary-session, and create-group
writes use a different 0x89A variant than invite policy.

Closes #392
2026-08-18 21:57:11 +08:00
DrSmoothl 86669d9451 fix(core): apply member permissions from the current privilege flag
set_group_member_permissions now writes only the member-capability
bits. An omitted privilege value is treated as all allowed.

Fixes #388
2026-08-17 20:00:11 +08:00
DrSmoothl 096ad0acfa fix(core): accept history visibility when the read-back flag matches
Setting new members to see history no longer fails after the server
has already applied it.

Fixes #387
2026-08-17 19:40:22 +08:00
DrSmoothl cd6ada0caa feat(onebot): accept join question and answer on set_group_add_option
add_type 4 takes group_question and group_answer. add_type 5 (and 55)
takes group_question. get_group_admin_settings also returns both.

Fixes #383
2026-08-17 19:12:43 +08:00
DrSmoothl b1fa11ca05 feat(onebot): add get_group_admin_settings
Bots can now read the current join option, robot-add option, member
invite policy, member permissions, history visibility, and search
settings that previously only had setters.

Fixes #385
2026-08-17 19:06:14 +08:00
DrSmoothl 1e0c91e74d feat(onebot): configure group member permissions
Fixes #335
2026-08-09 12:56:05 +08:00
DrSmoothl e08613062e feat(onebot): configure group history visibility
Fixes #336
2026-08-09 12:53:45 +08:00
DrSmoothl cd6fcadea4 feat(onebot): configure group invite policy
Add validated group member invite policy updates with post-write verification.

Fixes #334
2026-08-09 11:46:04 +08:00
DrSmoothl 0ed85e7745 fix(onebot): report rejected group kick operations
Treat group member removal as successful only when QQ accepts the operation. Preserve the rejection reason so callers no longer receive a false success when a target cannot be removed.

Apply the same behavior to single-member and batch removal actions, while keeping accepted operations unchanged. Add regression coverage for both successful and rejected responses.

Fixes #298
2026-07-30 21:42:18 +08:00
DrSmoothl b7d648ea0f fix(proton): harden generated protobuf decoders
Reject malformed varints, out-of-bounds fields, and invalid groups before they escape their parent frame. Aggregate unknown-field metadata and decode signed int32 values while preserving SnowLuma's existing outbound encoding bytes.
2026-07-10 13:40:53 +08:00
DrSmoothl 4e0ec92c23 fix(core): emit group_decrease + drop cache on set_group_leave (#133)
The server never pushes a member-decrease back to the member that left of its
own accord, so set_group_leave produced no notice.group_decrease — downstream
kept a 'zombie' group in its cache, and a later set_group_add_request failed
with 'matching group request not found'.

bridge.leave() now, after the 0x1097_1 OIDB succeeds, synthesizes a self
group_member_leave (user_id = operator_id = self, sub_type 'leave') through the
event bus — converted to notice.group_decrease by the existing path — and
calls identity.forgetGroup() so get_group_list / member lookups drop it
immediately (in-memory + DB inactive).

Kick / disband already arrive via the GroupMemberDecrease push (PkgType 34).
Limitation: a MANUAL leave in the NTQQ client isn't covered — SL is packet-
based with no kernel callback, and a voluntary self-leave isn't pushed back;
only kernel-based clients (NapCat/LLOneBot) catch that.

Validated by unit tests (bridge leave emits the event + forgets the group).
2026-06-22 23:07:44 +08:00
DrSmoothl c16064d7e3 refactor(packages): rename @snowluma/bridge → @snowluma/protocol
The package previously named "bridge" houses the protocol-byte layer
(OIDB cmds, msg-push decoders, highway uploaders, element-builder,
event bus). The name "bridge" suggested transport/lifecycle, which it
isn't — that role lives in core/hook and core/bridge/manager. Rename
to match what's actually inside.

This is a pure rename: zero logic changes, all 73 import sites updated
mechanically and TS-verified. core/onebot dependencies key updated.
ESLint override path adjusted for the moved oidb-services directory.

Rename detection threshold: git log --find-renames=20% recommended
when following history across this commit — content-replacement
(@snowluma/bridge → @snowluma/protocol) dropped per-file similarity
below git's default 50% on import-heavy files.
2026-05-23 23:31:03 +08:00
DrSmoothl 8d22868777 refactor(oidb): migrate group-admin.ts OIDB cmds to namespaces
Splits the 14 OIDB cmds from GroupAdminApi into self-contained
namespaces under @snowluma/bridge/oidb-services/group-admin/. The
facade becomes one-line forwarders; per-cmd capability is narrowed
to OidbSender (+ resolveUserUid / identity where the cmd actually
needs it).

Notes:
  - 0x89A_0 hosts MuteAll, SetAddOption, SetSearch, SetGroupName —
    same (cmd, subcmd) tuple, four different proto bodies; each gets
    its own namespace so proton can monomorphize independently.
  - 0x8A0_1 splits KickMember (single) and KickMembers (batch) for
    the same reason.
  - SetAddRequest carries uinForm=true and dynamic subCommand
    (filtered ? 2 : 1) on the same namespace.
  - GetAtAllRemain needs the bot's own uin → declares
    Pick<BridgeContext, 'identity'> in its Deps.

Wire-byte equivalence preserved; corner case for getAtAllRemain
empty-envelope path now returns {false, 0, 0} instead of throwing —
the new infrastructure normalises empty bodies and the OneBot action
ergonomics are unchanged.
2026-05-23 00:06:40 +08:00
DrSmoothl f7a03ca05a refactor(bridge): extract protocol layer into @snowluma/bridge package
Phase 2 of the 4-package split (RoadMap #3). Moves ~35 files —
events / event-bus / identity-service / packet-pipeline / element-
builder / bridge-oidb / qq-info / msg-push/* / highway/* / web/* +
the bridge-side formatters — out of `packages/core/src/bridge/`
into the new `@snowluma/bridge` workspace package. Everything that
translates QQ wire bytes ↔ typed events/elements now lives there.

Why
---
- `packages/core/src/bridge/` was actually two layers: a low-level
  "protocol decoder + I/O" set (msg-push, highway, packet-pipeline,
  identity, element-builder) AND the Api-class host (bridge.ts +
  apis/*). The Api layer can't move without the protocol layer
  moving first, but the two had grown into one directory.
- Bridge-internal consumers (highway uploaders, OIDB envelope
  helper, element-builder, msg-push parsers) only ever needed
  `bridge.identity` + `bridge.sendRawPacket` + the upload-metadata
  cache. Splitting them out lets that minimal surface live in the
  bridge package as `BridgeContext` while the api-layer's extended
  context (with `apis: ApiHub`) stays in @snowluma/core.

What stays in @snowluma/core (apis layer):
  - bridge.ts          ── the god-class that composes the protocol
                          building blocks + mounts apis
  - bridge-interface.ts ── public façade for OneBot consumers
  - manager.ts          ── per-uin BridgeManager hub
  - apis/*              ── the 13 typed Api classes
  - bridge-context.ts (NEW) ── extends @snowluma/bridge's slim
                              BridgeContext with `apis: ApiHub` so
                              the 13 Api classes keep their typed
                              `this.ctx.apis.X.method()` access.

Layout
------
Moved via `git mv` (history preserved):
  events / event-bus / identity-service / packet-pipeline /
  element-builder / bridge-oidb / qq-info / msg-push/* /
  highway/* / web/*           → @snowluma/bridge/<sub>
  utils/event-format.ts        → @snowluma/bridge/format.ts

  (the last one renamed because the destination directory only has
  bridge-related files; `format.ts` reads cleaner than `event-format`.
  Listed as a rename in git's `RM` output — content unchanged.)

Layering fixes (functionality preserved, just type signatures)
--------------------------------------------------------------
- `element-builder.SendContext.bridge` → typed as `BridgeContext`
  instead of `Bridge`. Reads only `bridge.identity` and the
  upload-cache (both on BridgeContext); the concrete `Bridge`
  instance still passes via free upcast.
- `bridge-oidb.runOidb` signature widened from `Bridge` to
  `BridgeContext` — same reason.
- `highway/{highway-client,pipeline,image-upload,ptt-upload,
  video-upload}.ts` — same.
- `format.ts::formatReply` parameter typed as a new local
  `ReplyEventLookup` structural type (`{ findEvent(id): JsonObject |
  null }`) instead of `MessageStore` from @snowluma/onebot. The
  OneBot `MessageStore` is a structural superset; consumers pass
  it through unchanged.
- JSON value types moved to a new `@snowluma/common/json` subpath
  (consumed by format.ts) — `onebot/types.ts` re-exports them so
  external `import { JsonValue } from '<…>/onebot/types'` paths
  keep working.
- `UploadedFileMeta` interface moved from `core/src/bridge/bridge.ts`
  into `@snowluma/bridge/bridge-context.ts` — element-builder + the
  forward-builder both read the upload cache through BridgeContext,
  so the type has to live in @snowluma/bridge. `bridge.ts` keeps
  its re-export for back-compat.

Changes
-------
- New package `packages/bridge/` (package.json with per-file subpath
  exports mirroring legacy `core/src/bridge/<x>` layout 1:1,
  tsconfig.json, typecheck script).
- 35 `.ts` files moved via `git mv` (3 files counted by git as
  RM — rename + modify because Bridge → BridgeContext type swap).
- 150+ import sites rewritten across `packages/core/src` and
  `packages/core/tests`: `'../bridge/<x>'`, `'../../bridge/<x>'`,
  `'../src/bridge/<x>'`, `'@/bridge/web/<x>'`, and vitest
  `vi.mock(...)` literals all collapse to `@snowluma/bridge/<x>`.
- `@snowluma/core` depends on `@snowluma/bridge` (workspace:*).
- `@snowluma/bridge` depends on `@snowluma/common`,
  `@snowluma/proto-defs`, `@snowluma/proton`.
- `packages/core/src/utils/` directory removed (empty).

Verification
------------
- Full workspace `pnpm -r typecheck`: all 10 projects clean.
- core test suite: 552 passed / 552 total (pre-existing
  websocket-darwin-arm64.node addon failure unchanged — same
  baseline as the previous refactor commits).
- Vite build (with stubbed native binary): clean — 143 modules,
  2.84 MB bundle / 224 KB gzip, identical envelope to the
  pre-split build.

Phase 3 (next): OneBot + WebUI move into @snowluma/onebot. The
runnable `index.ts` stays in @snowluma/core as the application
entry, importing `OneBotManager` from `@snowluma/onebot`.
2026-05-21 08:41:34 +08:00
DrSmoothl 0b91e4db8d refactor(api): move 14 group-admin methods to GroupAdminApi (#6 commit 3/13)
Third of 13 commits. Folds the previous `actions/group-admin.ts`
plus the matching Bridge facade methods into a typed `apis.groupAdmin`
hub.

Methods (14, all renamed to drop the redundant `Group` prefix once
the `apis.groupAdmin` namespace makes it implicit):

  muteGroupMember     → apis.groupAdmin.muteMember
  muteGroupAll        → apis.groupAdmin.muteAll
  setGroupAddOption   → apis.groupAdmin.setAddOption
  setGroupSearch      → apis.groupAdmin.setSearch
  setGroupAddRequest  → apis.groupAdmin.setAddRequest
  kickGroupMember     → apis.groupAdmin.kickMember
  kickGroupMembers    → apis.groupAdmin.kickMembers
  leaveGroup          → apis.groupAdmin.leave
  setGroupAdmin       → apis.groupAdmin.setAdmin
  setGroupCard        → apis.groupAdmin.setCard
  setGroupName        → apis.groupAdmin.setName
  setGroupSpecialTitle→ apis.groupAdmin.setSpecialTitle
  setGroupRemark      → apis.groupAdmin.setRemark
  getGroupAtAllRemain → apis.groupAdmin.getAtAllRemain

actions/group-admin.ts deleted. The corresponding `*_` alias imports
and `async xxx() { return xxx_(this, …); }` delegate methods on
Bridge are gone.

Caller updates
--------------
* onebot/instance-context.ts — 6 sites (the multi-method binding for
  the OneBot action layer).
* onebot/actions/extended.ts — 8 sites including `getAtAllRemain`.
* onebot/actions/group-admin.ts — all 6 onebot action handlers.
* onebot/modules/request-actions.ts — `setGroupAddRequest`.

Test updates
------------
* tests/actions/group-admin.test.ts — was importing the deleted
  `* as admin` module and calling `admin.muteGroupMember(bridge, ...)`.
  Switched to `new GroupAdminApi(bridge as any).muteMember(...)` —
  same wire validation, just constructs the real Api class once per
  call (the Api is stateless so it's effectively free).
* tests/onebot/request-actions.test.ts — was stubbing
  `setGroupAddRequest` flat on bridge. Updated to
  `apis: { groupAdmin: { setAddRequest } } as any`.

Tests: 587/587 pass (network-manager native-addon failure unchanged).
2026-05-20 22:34:54 +08:00
DrSmoothl 6c7e56d5a1 refactor(proto-defs): extract wire-protobuf definitions into standalone package
Move `packages/core/src/bridge/proto/proton/*.ts` (13 files, ~3.3k
LOC) into a new `@snowluma/proto-defs` workspace package. The proton
spike landed in the previous commit means bare-specifier imports
(`@snowluma/proto-defs/oidb`, `…/message`, …) now resolve correctly
through proton's vite plugin, so call-site replacement still fires.

Why
---
- Lets downstream consumers share the same wire
  definitions without copy-pasting them — multiple workspace packages
  can now depend on `@snowluma/proto-defs` directly.
- Trims `@snowluma/core`'s surface: the bridge no longer owns the
  protocol schema, only its consumers.
- Sets up future #5 (`message-parsing perf`) and #6 (`Api-on-ctx
  refactor`) — both want a thinner core.

Layout
------
Per-file subpath exports (mirrors the legacy `proto/proton/<sub>`
shape 1:1). The barrel `index.ts` intentionally skips `highway` and
`oidb-actions/media` because the two files share six NTV2-prefixed
type names — `export *` would silently drop one side. Subpath imports
are unambiguous, which is also why every call site in core uses them.

Changes
-------
- New package `packages/proto-defs/` (package.json, tsconfig.json,
  src/index.ts).
- 13 `.ts` files moved via `git mv` to preserve history.
- 44 import sites across 26 source files + 15 tests rewritten via sed
  from `./[…/]proto/proton/<sub>` → `@snowluma/proto-defs/<sub>`.
- `@snowluma/core` depends on `@snowluma/proto-defs` (workspace:*).

Verification
------------
- proto-defs typecheck: clean
- core typecheck: clean
- core test suite: 587 passed (same baseline as before the refactor;
  one failure is the pre-existing websocket native addon, unrelated)
- proton test suite: 99 passed (8 files including the new
  cross-package.test.ts from the spike)
- Full workspace `pnpm -r typecheck`: all 8 projects clean

Note: proton's `dist/index.js` MUST be rebuilt for the vite plugin to
pick up the bare-specifier resolution. The previous commit's source
change is only effective after `pnpm -F @snowluma/proton build`.
2026-05-20 21:31:30 +08:00
手瓜一十雪 5ea1c3a961 Remove bridge proto schemas and parity test
Delete proto schema modules under packages/core/src/bridge/proto (action.ts, element.ts, highway.ts, longmsg.ts, message.ts, notify.ts, oidb-action.ts, oidb.ts) and the related test packages/core/tests/proton-parity.test.ts. These files contained protobuf schema definitions for the bridge layer; remove them as part of a cleanup/refactor. Update any imports or references to these modules to their new locations or replacements.
2026-05-20 18:23:03 +08:00
DrSmoothl 5802812392 feat(proton): finish OIDB migration — runOidb on protobuf_encode<OidbBase<T>>
Closes the OIDB phase of the proton migration. Every consumer that
talked to the OIDB service through the old runtime
`makeOidbBaseSchema()` factory now goes through the compile-time
generic `OidbBase<T>` instead — proton monomorphizes the codec per
call-site instantiation.

@snowluma/proton — two analyzer fixes uncovered by the migration:

1. Skip queuing call-site type-args that reference an *enclosing
   generic function's* type parameter for monomorphization. Before
   this, a wrapper like

     export function encodeOidbEnv<T>(env: OidbBase<T>): Uint8Array {
       return protobuf_encode<OidbBase<T>>(env);
     }

   would cause the analyzer to mint a literal `OidbBase__T` registry
   entry with `body.typeName = 'T'`, which the post-walk wire-type
   guard then tripped on. The enclosing-scope type params are tracked
   on a stack as the walker descends into FunctionDeclaration,
   FunctionExpression, ArrowFunction, MethodDeclaration nodes; matching
   call sites are skipped (the wrapper-binding path is still
   responsible for materialising concrete codecs at downstream call
   sites).

2. Register *explicitly* empty interfaces (`interface X {}`) as
   zero-field proto messages. OIDB has a handful of these
   ('this call has no meaningful response body' placeholders —
   Oidb0xeb7Resp, Oidb0xcd4Resp, Oidb0x112aResp, Oidb0x8a0Resp,
   Oidb0xf16Resp, OidbClientKeyReq). Proto3 wire allows zero-field
   messages, and downstream code does `protobuf_decode<OidbBase<X>>`
   where X is one of these — the wire-type guard would otherwise trip
   on the missing `X` registration. Interfaces that have members but
   no pb-marked fields are still NOT registered.

@snowluma/core — the OIDB-layer refactor itself:

bridge-oidb.ts ditches the runtime ProtoSchema factory and exposes:
  - makeOidbEnvelope<T>(oidbCmd, subCmd, body, isUid)   pure helper
  - encodeOidbEnv<T>(env)                               pass-through
  - decodeOidbEnv<T>(bytes)                             pass-through
  - runOidb(bridge, cmd, envelopeBytes)                 non-generic raw

`encodeOidbEnv` / `decodeOidbEnv` are pure pass-through generic
wrappers — proton wrapper-binds them and substitutes every call site
with the inlined `protobuf_encode_OidbBase__X` / `protobuf_decode_…`
function. `runOidb` is non-generic (proton leaves it alone) so it can
be mocked in tests. `makeOidbEnvelope` is plain TS — also mockable for
introspection.

56 `runOidb` call sites across 11 files migrated to the new API:
  - bridge-contacts.ts, actions/extras.ts, actions/friend.ts,
    actions/group-admin.ts, actions/group-file.ts,
    actions/group-message.ts, actions/interaction.ts,
    actions/misc.ts, actions/profile.ts, web-actions/cookies.ts,
    highway/pipeline.ts

pipeline.ts also drops `makeOidbBaseSchema(NTV2UploadRichMediaReqSchema)`
and the residual `protoEncode({...}, EncodableMediaMsgInfoSchema)` —
both go through proton now.

Tests:
  - 8 test files migrated from `vi.mocked(decodeOidbEnv).mockReturnValueOnce`
    to `vi.mocked(runOidb).mockResolvedValueOnce(protobuf_encode<OidbBase<X>>({body: …}))`
    — the proton substitution means the old mock layer is bypassed at
    build time, so the only mockable point is the bytes layer at `runOidb`.
  - 2 new parity tests pinning `OidbBase<T>` ↔ legacy
    `makeOidbBaseSchema(InnerSchema)` byte equivalence.

Verification:
  - @snowluma/proton: 95/95 tests pass, tsc --noEmit clean
  - @snowluma/core:   605/605 tests pass (network-manager.test.ts
    fails to load due to a missing darwin-arm64 native binary in this
    environment — pre-existing), tsc --noEmit clean
2026-05-19 08:19:14 +08:00
DrSmoothl 59cdfa98a2 refactor(oidb): replace 3 positional helpers with one named runOidb
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.
2026-05-15 01:03:25 +08:00
DrSmoothl 030336a25c refactor(identity): consolidate UID resolution behind IdentityService
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.
2026-05-15 00:32:59 +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