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
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
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
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
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
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
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
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.
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).
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.
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.
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`.
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).
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`.
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.
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
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.
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.
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.