dev2
44
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
af8d5ab06b |
Split channel packages and rename Account→Core
Reorganize package layout and rename core/account types: - Move channel hook & socket sources/tests into packages/channel-hook and packages/channel-socket (many files renamed). - Remove ChannelManager from packages/channel and drop its export from package.json (multi-account host folded into Hub). - Update tests to use HookSink instead of BridgeManagerSink. - In @snowluma/core: move account/apis/* up to src/apis, rename account-context→core-context, account-interface→core-ctx, account→core, and add hub.ts; delete AccountManager. - Update constructors and type usages to accept CoreContext/CoreCtx and adapt identifiers accordingly. This is a refactor to clarify responsibilities (split transport implementations, fold multi-account manager into Hub, and rebrand the account layer as Core). |
||
|
|
652bafd75a |
Extract bridge transport into @snowluma/bridge
Move transport-layer bridge code out of @snowluma/core into a dedicated @snowluma/bridge package and introduce a clear account layer in core. Adds Bridge, BridgeInterface, InjectBridge, ProtocolBridge (stub), adapters, and manager files under packages/bridge and updates package.json exports. In @snowluma/core the former bridge surface is replaced by Account, AccountContext, AccountInterface and AccountManager; apis were moved/renamed to account/apis and now consume AccountContext. Adjustments include packet handling plumbing (use deliverPacket / setPacketHandler), updated sendRawPacket wiring, uploaded-file cache and lifecycle changes (InjectBridge.dispose, ProtocolBridge returns a structured not-implemented result). Tests and imports were updated accordingly. |
||
|
|
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. |
||
|
|
cd9403f27a |
fix(forward): align resid+uniseq for nested forwards
Multi-layer 合并转发 was uploading correctly but the outer's
LongMsgResult piggyback was *unreachable* by the receiver because
two things were missing:
1. The forward preview element was emitted as the old XML
`richMsg serviceID=35 m_resid=…` shape, which carries the
inner resId but has NO uniseq field.
2. The piggyback `actionCommand: <uuid>` slot generated a fresh
UUID that was never reflected into the preview either.
Result: QQ-NT receivers saw the inner preview's resid, scanned the
outer's actions[] for a matching uniseq, found none, and either
fell back to a fresh server fetch per layer or dropped the layer
entirely.
Cross-checked against:
- dev/Lagrange.Core/Lagrange.Core/Message/Entity/MultiMsgEntity.cs:43-115
- dev/NapCatQQ/packages/napcat-core/helper/forward-msg-builder.ts:52-122
- dev/NapCatQQ/packages/napcat-onebot/action/msg/SendMsg.ts:208-347
Both upstream impls use the modern `com.tencent.multimsg` LightApp
JSON whose `meta.detail.uniseq` is the round-trip key that links
the inner preview to the matching `actionCommand` in the outer's
LongMsgResult — no separate fetch needed.
Changes:
- `element-builder.makeForwardElem` now emits a deflated
`com.tencent.multimsg` LightApp JSON (LightAppElem, field 51)
instead of the XML RichMsg (field 12). `forwardUuid` on the
MessageElement is baked into `meta.detail.uniseq` and
`extra.filename`. Auto-generates a UUID when absent.
- `MessageElement.forwardUuid?: string` added for the round-trip.
- `forward.uploadRecursive` threads `inner.uuid` into the outer's
preview element AND uses the same uuid for the outer's
piggyback actionCommand — closing the receiver-side lookup loop.
- `forward.fetch` now walks ALL actions (not just `MultiMsg`),
indexes piggybacks by uuid, and pre-populates the cache for
each inner resId discovered in the outer's preview bubbles.
Follow-up `fetch(innerResId)` hits the cache instead of the wire.
- `rich-body-decoder` parses LightApp JSON; when `app ===
com.tencent.multimsg`, emits `{type: 'forward', resId,
forwardUuid}` so the OneBot layer can walk in. Legacy XML
serviceID=35 path stays as a fallback for mobile QQ senders.
Wire-byte tests cover:
- send: lightApp.data is 0x01-prefixed deflated JSON with the
right meta + extra fields
- send/nested: outer LongMsgResult carries `MultiMsg` AND a
uuid-keyed piggyback; the piggyback uuid matches the
preview's `meta.detail.uniseq` baked into the same outer
msgBody
- send/3-deep: outermost action[] has 1 + 2 entries (own
MultiMsg + 2 piggybacks hoisted from deeper levels)
- recv: lightApp `com.tencent.multimsg` → forward element with
both resId and forwardUuid; non-multimsg LightApp falls
through to {type:'json'}; legacy XML path still emits
{type:'forward'}.
641 tests pass (was 628 + 13 new); typecheck clean; lint clean.
|
||
|
|
521808a6eb |
refactor(oidb): migrate group-file.ts OIDB cmds to namespaces
Splits the 16 OIDB cmds from GroupFileApi into self-contained
namespaces under @snowluma/bridge/oidb-services/group-file/. Adds
@snowluma/bridge/oidb-services/shared with `toInt` / `ensureRetCodeZero`
helpers so namespaces can validate sub-responses without an
@snowluma/core dependency.
Boundaries:
- Single-OIDB methods (publish / getCount / list-page / *url /
delete / move / *folder) become one-line forwarders.
- 3-stage methods (`upload` / `uploadPrivate`) keep their
orchestration on the facade — OIDB preflight goes through the
new namespace, but highway PUT + publish/sendC2cFile + upload-
cache bookkeeping stay where they are.
- `list` keeps its pagination loop on the facade; the page-fetch
namespace is one call.
- The NTV2-rich-media URL fetchers (group/private × ptt/video)
share a deserialize helper (`parseNtv2DownloadUrl`) so all four
namespaces only differ in their scene block.
Wire-byte equivalence preserved. The 0x6D6 family shares one
OidbGroupFileReq envelope; each namespace populates a different
slot (file / download / delete / move) and proton monomorphizes
per call site as before.
|
||
|
|
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.
|
||
|
|
80afa31f5c |
refactor(oidb): migrate extras.ts OIDB cmds to namespaces
5 logical OIDB cmds → 4 namespace files (group todo's 3 sub-cmds
collapse into one namespace with a dynamic resolver):
- GroupTodo (0xF90_1/2/3) — set/complete/cancel via the
new `action` discriminator
- GetStrangerStatus (0xFE1_2) — uinForm=true; preserves the
(status*10, ext_status) decode
quirk verbatim
- FetchAiVoiceList (0x929D_0)
- FetchAiVoice (0x929B_0) — single poll; retry loop
stays on the facade so callers
can plug their own back-off
Facade keeps the 6 public methods (setGroupTodo / completeGroupTodo /
cancelGroupTodo / getStrangerStatus / fetchAiVoiceList / fetchAiVoice)
as one-line forwarders; only `getStrangerStatus` wraps in try/catch
to preserve its "return null on transport failure" contract, and
`fetchAiVoice` keeps the polling loop.
30 new namespace-level tests; extras.test.ts moved onto the
sendRawPacket assertion model.
Verification:
- typecheck green
- @snowluma/bridge: 24 files / 195 tests
- @snowluma/core: 302 tests
Wire bytes unchanged.
|
||
|
|
58e8081420 |
refactor(oidb): migrate misc.ts OIDB cmds to namespaces
3 OIDB cmds migrated; the 1 non-OIDB cmd (getMiniAppArk via
LightAppSvc) stays on the facade unchanged.
- TranslateEnToZh (0x990_2)
- ClickInlineKeyboardButton (0x112E_1)
- SendGroupSign (0xEB7_1) — preserves historic UPPERCASE
EB7 wire name via the new
`wireName` override (the only
cmd in the codebase with this
quirk; every other cmd uses
the default lowercase).
Tests:
- misc.test.ts moved to bridge.sendRawPacket assertion model
- 18 new namespace-level tests covering serialize, deserialize edge
cases, magic constants, BigInt widening, and the EB7 case quirk
Verification:
- typecheck green
- @snowluma/bridge: 18 files / 153 tests
- @snowluma/core: 302 tests
Wire bytes unchanged.
|
||
|
|
f78431242c |
refactor(oidb): migrate friend.ts to namespaces
3 OIDB cmds moved to self-contained namespaces:
- HandleFriendRequest (0xB5D_44) — digit-only flag still
auto-resolves via resolveUserUid
- DeleteFriend (0x126B_0) — verbatim magic constants in the
nested field2 sub-message
(130/109/8/8/50 — server-side
discriminator, MUST stay byte-
equal to NTQQ wire)
- SetFriendRemark (0xB6E_2)
Cache invalidation (fetchFriendList after deletion) stays in the
facade — that side-effect needs `ctx.apis.contacts`, which namespaces
deliberately can't reach.
Tests migrated to assert against bridge.sendRawPacket. 15 new
namespace-level tests covering each invoke branch + serialize edge
cases.
Verification:
- typecheck green
- @snowluma/bridge: 15 files / 133 tests
- friend.test.ts: 5/5
Wire bytes unchanged.
|
||
|
|
a38f7ea26e |
refactor(oidb): migrate profile.ts OIDB cmds to namespaces
5 of profile.ts's methods were thin OIDB wrappers — migrated to the
new namespace pattern. The 5 non-OIDB methods (setOnlineStatus /
setDiyOnlineStatus / setAvatar / setGroupAvatar / fetchCustomFace)
stay on the facade unchanged — they hit `trpc.qq_new_tech.*`,
Highway, or `Faceroam.OpReq` directly and aren't in scope for the
OIDB-namespace refactor.
New namespaces (each with focused Pick<BridgeContext, …> deps):
- SetProfile (0x112A_2) — needs identity for uin
- SetSelfLongNick (0x112A_2) — same cmd different proto, ditto
- SetInputStatus (0xCD4_1) — needs resolveUserUid
- GetLike (0x7ED_12) — needs identity + resolveUserUid
- GetUnidirectionalFriendList (0xE17_0) — needs identity; non-standard
wire name handled via the new
wireName override on OidbCallSpec
Infrastructure tweak:
- OidbCallSpec gains an optional `wireName(cmd, sub) → string`. Most
cmds use the default `OidbSvcTrpcTcp.0xNNNN_N` scheme; 0xe17_0
routes through `MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0`.
Test mock updates:
- profile.test.ts moves the migrated cmds onto bridge.sendRawPacket
mock (the new path bypasses the bridge-oidb runOidb mock). Non-OIDB
cmds (setOnlineStatus, setAvatar, etc.) keep their existing mocks.
- 36 new namespace-level tests in @snowluma/bridge covering serialize
branches, deserialize edge cases, deps resolution, and e2e wire
routing.
Verification:
- pnpm -r typecheck: green
- @snowluma/bridge: 12 files / 118 tests
- @snowluma/core: 33 files / 302 tests
Wire shape unchanged across migrated cmds — same bytes produced.
|
||
|
|
ff2b902cbd |
refactor(oidb): migrate interaction.ts remaining 3 cmds to namespaces
Completes the InteractionApi facade — every method is now a one-line
forwarder. New namespaces:
- SendPoke (0xED3_1) — group + friend variants discriminated by
which of groupUin/friendUin is set
- SendLike (0x7E5_104) — thumbs-up to a target user
- SetEssence (0xEAC_1/_2) — set/unset essence message, dynamic subcmd
Facade methods reduced to thin forwarders, mirror-fixed comment block
removed (the namespaces now own the wire-shape commentary).
Tests: 28 new in @snowluma/bridge covering each namespace's serialize
edge cases + dynamic subcmd resolution + e2e wire name + envelope body
shape. Cleaned the legacy `vi.mock('bridge-oidb')` setup out of
interaction.test.ts — the post-migration path doesn't route through
the mocked symbols anymore.
Wire behavior unchanged: every cmd produces the same bytes as before,
verified by encode round-trip in the unit tests.
|
||
|
|
0238bcb280 |
refactor(oidb): namespace-per-cmd service layer — vertical slice (3 cmds)
Introduce a new way to organise OIDB cmd implementations:
- one TypeScript namespace per (cmd, subcmd) tuple
- the namespace's exported members ARE the OidbCallSpec (structural
typing — no `implements` clause)
- a single invokeOidb() template method drives every call:
envelope build → encode → send → errorCode check → decode →
deserialize transformation
- each namespace declares its own minimal `Deps` capability via
Pick / intersection on BridgeContext (Interface Segregation):
most cmds need only OidbSender (the `sendRawPacket` method)
The facade Api classes (InteractionApi etc.) stay for now, with the
migrated methods reduced to one-line forwarders that hand off to the
namespace. Callers still write `bridge.apis.X.Y(...)`; the OIDB
wire-encoding work just moved to a self-contained file per cmd.
Vertical slice scope: 3 cmds (the reaction line)
- SetReaction (0x9082_1 / _2; dynamic subcmd)
- FetchReactionSummary (0x9084_1)
- GetEmojiLikes (0x9083_1; legacy stub)
Infrastructure:
- new packages/bridge/src/oidb-service.ts:
OidbSender (sendRawPacket-only capability),
OidbCallSpec<TReq, TResp, TParams, TResult>,
invokeOidb(), buildOidbRequest(), parseOidbResponse(), OidbError
- runOidb() refactored to accept OidbSender instead of BridgeContext
(structurally compatible — no caller changes anywhere)
- new OidbEmpty placeholder type in proto-defs/oidb.ts for cmds whose
response carries no body (proton needs a real message interface,
not `void` / `never`)
- empty-body responses substituted with {} in invokeOidb so
deserialize always receives a defined object
Tests (65 in @snowluma/bridge, all green):
- oidb-service.test.ts (20): every helper branch — static + dynamic
subcmd, uinForm flag, error code propagation as OidbError, empty
body substitution, sender failure modes (success=false /
gotResponse=false / both with and without errorMessage),
timeoutMs propagation
- set-reaction.test.ts: const declarations, dynamic subcmd both
ways, type=1/2 boundary on code length, e2e envelope inspection
- fetch-reaction-summary.test.ts: BigInt serialization, catalog-tail
filtering, default emojiId/Type/lastReactionTime
- get-emoji-likes.test.ts: BigInt serialization, default count /
emojiType, base64 cookie round-trip, uin=0 filtering, isLast
computation, e2e
Existing interaction.test.ts moved off the now-bypassed runOidb mock
onto sendRawPacket mock (the new invokeOidb path routes there).
Workspace-wide typecheck: green. Workspace test totals:
proton 99/99, bridge 65/65, core 302/302, onebot 257/257
(1 pre-existing onebot suite failure, network-manager native addon
load — unrelated to this change.)
|
||
|
|
737b121dfe |
fix(oidb): correct 0x9083 field layout so fetch_emoji_like returns users
Two bugs collapsed fetch_emoji_like into a perma-empty list with status=ok and no error output: 1. Req fields 4 and 5 were swapped (emojiType on field 4 / emojiId on field 5). The sibling 0x9082 OidbGroupReaction has field 4 = code (string) and field 5 = type (uint), and 0x9083 mirrors that layout — verified against LagrangeDev/LagrangeV2 `Internal/Packets/Service/SetGroupReaction.cs`. With our swap the wire types also mismatched (varint vs length-delim), so the server protobuf decoder dropped both fields and returned an empty list without ever surfacing an error. 2. Resp `inner.userInfo` was declared as a single message, but the server emits one entry per liker (repeated). Even after fixing the req, multi-user responses would have collapsed into the last entry. Map the decoded array on the InteractionApi side and filter uin=0 placeholders. Test now mocks two users to exercise the repeated path. |
||
|
|
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`. |
||
|
|
46b302f738 |
refactor(common): extract leaf utilities into @snowluma/common package
Phase 1 of the 4-package split called out in RoadMap #3. Moves the "infrastructure" leaves — files with zero business dependencies that @snowluma/bridge, @snowluma/core, and @snowluma/onebot all consume — into a dedicated workspace package. Mirrors what #4 (proto-defs) did for wire-protobuf definitions. Why --- - Cleaner layering: these were never bridge- or onebot-specific. Logger / hex codec / runtime config sit at the bottom of every module's import graph; giving them their own package makes that graph readable and prevents future leaks the other direction. - Sets up #3's bridge + onebot extractions: those need a clean leaf to depend on. Without this, every cross-package import would have to re-export through core, which defeats the split. - Mirrors @snowluma/proto-defs layout — per-file subpath exports (`@snowluma/common/logger`, `@snowluma/common/hex`, …) keep import sites unambiguous and let bundlers tree-shake aggressively. Layout ------ Moved (via `git mv` — history preserved): packages/core/src/utils/logger.ts → @snowluma/common/logger packages/core/src/utils/hex.ts → @snowluma/common/hex packages/core/src/utils/log-file-transport.ts → @snowluma/common/log-file-transport packages/core/src/utils/log-summary.ts → @snowluma/common/log-summary packages/core/src/protocol/packet-sender.ts → @snowluma/common/packet-sender packages/core/src/protocol/types.ts → @snowluma/common/protocol-types packages/core/src/common/runtime.ts → @snowluma/common/runtime Not moved (deferred to Phase 2): `packages/core/src/utils/event-format.ts` — it has reverse imports on `bridge/events`, `bridge/identity-service`, `onebot/message-store`, `onebot/types`. It moves into @snowluma/bridge in Phase 2, with the MessageStore parameter decoupled via a structural type so bridge doesn't end up depending on onebot. Changes ------- - New package `packages/common/` (package.json, tsconfig.json — no src/index.ts barrel, callers use the subpath exports directly to avoid pulling in `node:fs` when only the hex codec is needed). - 7 `.ts` files moved via `git mv`. - 50+ import sites rewritten via sed across `packages/core/src` and `packages/core/tests`. Old shapes (`../utils/logger`, `../../protocol/types`, `./common/runtime`, `../src/utils/hex`) all collapse to `@snowluma/common/<sub>`. - `@snowluma/core` depends on `@snowluma/common` (workspace:*). - `packages/core/src/{protocol,common}/` directories removed (empty). Verification ------------ - core typecheck: clean - common typecheck: clean - Full workspace `pnpm -r typecheck`: all 9 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 — same size envelope as before) Note: `utils/event-format.ts` is the only file remaining under the old `utils/` namespace. It moves into @snowluma/bridge alongside the events / identity-service / packet-pipeline trio in Phase 2. |
||
|
|
22add4ad1c |
refactor(api): move Tier-2 extras to ExtrasApi (#6 commit 11/13)
Inlines `actions/extras.ts` into `apis/extras.ts::ExtrasApi`. Covers
the napcat-parity Tier-2 surface: group todo CRUD (0xF90_1/2/3),
stranger online/ext status decode (0xFE1_2), and AI voice list +
synthesis (0x929D / 0x929B).
`StrangerStatus`, `AiVoiceChatType`, `AiVoiceCategory`, `AiVoiceItem`
are now exported from `apis/extras.ts` and re-exported through
`bridge.ts` for backwards compatibility (the OneBot side imports
them via `@snowluma/core`).
No renaming — the names already read cleanly under `apis.extras.X`.
The `fetchAiVoiceList` / `fetchAiVoice` chatType parameter is widened
to `AiVoiceChatType | number` because the OneBot side passes a raw
number from JSON. The wire encoder is happy with any integer; the
strict enum just gives autocompletion at call sites that have it.
Callers updated: `actions/extended.ts` (7 sites).
Tests rewritten:
- `extras.test.ts` → uses `new ExtrasApi(bridge as any).method(...)`
and the it.each dispatch switches to method-name strings.
- `extended-actions.test.ts` APIS_ROUTING gains the extras block.
587/587 tests pass.
|
||
|
|
e5ac2a61b1 |
refactor(api): move 4 misc methods to MiscApi (#6 commit 10/13)
Inlines `actions/misc.ts` into `apis/misc.ts::MiscApi`. The Api holds the methods that didn't naturally group into any other area: translation, mini-app ARK card builder, inline-keyboard callback, group sign-in. No renaming — these names are already descriptive at the call site under `apis.misc.X`. Callers updated: `actions/extended.ts` (4 sites). Tests rewritten to use `new MiscApi(bridge as any).method(...)`. 587/587 tests pass. |
||
|
|
d953b9824c |
refactor(api): move 2 forward methods to ForwardApi (#6 commit 9/13)
Inlines `actions/forward.ts` into `apis/forward.ts::ForwardApi`. The
file is the largest unmoved one — 475 lines covering long-message
upload with NapCat-style piggyback for nested {type:"node"} chains,
plus the cached fetch side.
The module-scoped `forwardResCache` Map stays at file scope rather
than as an instance field. Forward res_ids are globally unique and
an outbound upload often gets queried back through `fetch` on a
different Bridge instance (different OneBot connection hitting the
same account) — NapCat's behaviour relies on the cache being shared
across the process.
Method renaming (Api namespace already says "forward"):
uploadForwardNodes → upload
fetchForwardNodes → fetch
The private `uploadForwardNodesRecursive` becomes
`this.uploadRecursive(...)` — same body, threads through `this.ctx`.
Callers updated: `modules/message-actions.ts` (4 sites).
Tests rewritten:
- `forward.test.ts` → `new ForwardApi(bridge as any).upload/fetch(...)`
- `forward-file.test.ts` → same pattern
- `forward-nested.test.ts` → mocks `apis.forward.upload` instead of
flat `bridge.uploadForwardNodes`
587/587 tests pass.
|
||
|
|
62c5590fe1 |
refactor(api): move 10 profile methods to ProfileApi (#6 commit 8/13)
Inlines `actions/profile.ts` into `apis/profile.ts::ProfileApi`. The
file covers personal profile (nickname / long-nick / personal note),
online status (regular + DIY with custom face/wording), avatars
(personal + group via highway), input-status, likes (received from
others), unidirectional friend list, and custom face roaming.
Method renaming (Api namespace already says "profile"):
getProfileLike → getLike (only obvious shortening; rest keep
their `set*`/`fetch*` prefixes because
they're already minimal at the call
site under `apis.profile.X`)
Callers updated: `instance-context.ts`, `actions/group-admin.ts`,
`actions/extended.ts`. The extended-actions test gained a sizable
`APIS_ROUTING` block covering profile/interaction/friend (all of the
late-stage Api hubs need auto-promotion now), so legacy tests that
stub `setDiyOnlineStatus`/`setOnlineStatus`/`deleteFriend`/etc as flat
methods keep working.
Tests rewritten to use `new ProfileApi(bridge as any).method(...)`.
587/587 tests pass.
|
||
|
|
2ec72d59f9 |
refactor(api): move 3 friend methods to FriendApi (#6 commit 7/13)
Inlines `actions/friend.ts` into `apis/friend.ts::FriendApi`. The roster READ side (fetchFriendList) stays on ContactsApi where it naturally lives next to fetchGroupList — the friend/group rosters share the same pipeline. Method renaming (Api namespace already says "friend"): setFriendAddRequest → handleRequest (accept/reject UID-or-flag) deleteFriend → delete (with cache refresh) setFriendRemark → setRemark `delete` still does the post-OIDB best-effort `apis.contacts.fetchFriendList()` refresh to evict the stale entry. Callers updated: `instance-context.ts`, `actions/extended.ts`. Tests rewritten to use `new FriendApi(bridge as any).method(...)`. 587/587 tests pass. |
||
|
|
73f1cd3c80 |
refactor(api): move interaction methods to InteractionApi (#6 commit 6/13)
Folds the entire `actions/interaction.ts` (sendPoke / sendLike / setGroupReaction / getEmojiLikes) plus the lone-survivor `setGroupEssence` from `actions/group-message.ts` into `apis/interaction.ts::InteractionApi`. Both action files deleted — the recall + markRead halves of group-message were already absorbed into MessageApi back in commit 1. Skipping the planned standalone GroupMessageApi commit: with only one method left (`setGroupEssence`), giving it its own Api class would be pure overhead. Essence is conceptually an "interaction" anyway (mark a message as essence is parallel to "react with an emoji"). Method renaming (Api namespace already says "interaction"): sendPoke → sendPoke (no rename — already minimal) sendLike → sendLike setGroupReaction → setReaction setGroupEssence → setEssence getEmojiLikes → getEmojiLikes (no rename — read-side, fine) Callers updated: `instance-context.ts`, `modules/message-actions.ts`, `actions/extended.ts`. Tests `interaction.test.ts` rewritten to use `new InteractionApi(bridge as any).method(...)` and gained a `setEssence` test. `group-message.test.ts` rewritten to target MessageApi.recall* / markRead* methods that were already moved. 587/587 tests pass. |
||
|
|
dad501ebc0 |
refactor(api): move 17 group-file methods to GroupFileApi (#6 commit 4/13)
Inlines `actions/group-file.ts` into `apis/group-file.ts::GroupFileApi`,
deletes the old action file, removes the matching 17 facade methods
off `Bridge`, and slims `BridgeInterface` accordingly.
Method renaming follows the same "strip the redundant suffix" rule we
applied to GroupAdminApi:
uploadGroupFile → upload
uploadPrivateFile → uploadPrivate
sendGroupFileMessage → publish (OIDB 0x6d9_4)
fetchGroupFiles → list
fetchGroupFileCount → getCount
fetchGroupFileUrl → getUrl
fetchPrivateFileUrl → getPrivateUrl
fetchGroupPttUrlByNode → getPttUrl
fetchPrivatePttUrlByNode → getPrivatePttUrl
fetchGroupVideoUrlByNode → getVideoUrl
fetchPrivateVideoUrlByNode → getPrivateVideoUrl
deleteGroupFile → delete
moveGroupFile → move
createGroupFileFolder → createFolder
deleteGroupFileFolder → deleteFolder
renameGroupFileFolder → renameFolder
`upload`'s atomic upload+publish flow now calls `this.publish(...)`
directly instead of going through `bridge.sendGroupFileMessage`, and
`uploadPrivate` calls `this.ctx.apis.message.sendC2cFile(...)`.
Callers updated: `instance-context.ts`, `media-url-resolver.ts`,
`actions/group-file.ts` (onebot), `actions/extended.ts`,
`modules/{contact,media,message}-actions.ts`.
Tests: `group-file.test.ts` rewritten to use
`new GroupFileApi(bridge as any).method(...)`. The "verify publish was
called" assertions now use `vi.spyOn(api, 'publish').mockResolvedValue()`
since `publish` is an instance method on the same Api. `_helpers.ts`
gains `mockGroupFileApi()` for cross-Api callers and drops the flat
`sendGroupFileMessage` shim. `media-url-resolver.test.ts` and
`send-group-file.test.ts` switched to `bridge.apis.groupFile.*`; the
extended-actions `APIS_ROUTING` map now carries a `[area, newName]`
tuple so renamed methods auto-promote.
587/587 tests pass. The network-manager file remains as the only
failing test file (pre-existing infrastructure issue unrelated to #6).
|
||
|
|
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).
|
||
|
|
6bf53890dc |
refactor(api): move contacts methods to ContactsApi (#6 commit 2/13)
Second of 13 commits decomposing Bridge. Moves the six "fetch the
roster / a profile / outstanding requests / download rkeys" methods
off Bridge and onto a new `apis.contacts.*`.
Methods moved
-------------
* fetchFriendList — OIDB 0xfd4_1 + identity.rememberFriends
* fetchGroupList — OIDB 0xfe5_2 + identity.rememberGroups
* fetchGroupMemberList — OIDB 0xfe7_3 + identity.rememberGroupMembers
(includes the 60s TTL + per-group inflight
coalesce cache that prevents Tencent
risk-control from triggering)
* fetchUserProfile — OIDB 0xfe1_2 + identity.rememberUserProfile
* fetchGroupRequests — OIDB 0x10c0_{1,2}
* fetchDownloadRKeys — OIDB 0x9067_202
Cache fields moved
------------------
The two member-list throttle maps (`memberListInflight_`,
`memberListLastFetch_`) move from Bridge to `ContactsApi` as private
fields. Same TTL, same semantics — just owned by the Api class that
exposes the method.
Constructor ordering
--------------------
`buildApiHub(this)` now runs BEFORE `identity.setFetcher(...)` in the
Bridge constructor because the `fetchProfile` / `fetchGroupMemberList`
callbacks installed there reach into `this.apis.contacts.*`. With
eager construction this is safe; the alternative (pass a thunk that
forwards once apis is built) was strictly worse for readability.
bridge-contacts.ts removed
--------------------------
The file existed solely as the previous module-function delegate
target; ContactsApi inlines every function body so there's nothing
left to keep.
Caller-site updates
-------------------
* `bridge/actions/friend.ts` — `bridge.fetchFriendList()` →
`bridge.apis.contacts.fetchFriendList()`.
* `onebot/manager.ts`, `onebot/instance-rkey.ts`,
`onebot/modules/contact-actions.ts`, `modules/request-actions.ts`,
`actions/extended.ts` — same shape rewrite (6 method names).
Test plumbing
-------------
* `tests/actions/_helpers.ts` — `mockApiHub()` grows a `contacts`
field built by `mockContactsApi()`.
* `tests/bridge-member-identity.test.ts::RefreshingBridge` — was
`override async fetchGroupMemberList()`. After the move the method
lives on `this.apis.contacts`, not `this`. Switched to patching the
method on the constructed ContactsApi instance directly.
* `tests/actions/friend.test.ts` — `bridge.fetchFriendList` →
`bridge.apis.contacts.fetchFriendList`; the "refresh throws" branch
overrides via `bridge.apis.contacts.fetchFriendList = vi.fn(…)`.
* `tests/onebot/contact-actions.test.ts`, `extended-actions.test.ts`,
`request-actions.test.ts` — local `fakeBridge` helpers gained an
auto-promotion shim that rewrites flat method stubs (the pre-#6
test API) into the new `apis.<area>.method` shape. Keeps existing
test setup readable while the production code uses the new path.
Tests: 587/587 pass (same baseline; pre-existing network-manager
native-addon failure unchanged).
|
||
|
|
3e5dca1bd4 |
refactor(api): introduce MessageApi + BridgeContext + ApiHub skeleton (#6 commit 1/13)
First of 13 commits decomposing the Bridge god-object into typed Api
classes following NapCat's `ctx.apis.XxxApi.method()` pattern. This
commit lays the foundation (BridgeContext, ApiHub, eager construction)
plus moves the message-send/recall/mark-read methods onto MessageApi.
Subsequent commits will move the other 12 areas (group admin, files,
profile, …) similarly.
Architecture
------------
* `bridge-context.ts` — new interface, the surface Api classes see.
Carries identity / events / apis / sendRawPacket / resolveUserUid /
next{Message,Client}Sequence helpers / upload meta cache. Replaces
the previous `BridgeInterface` for Api-class consumption (the
legacy interface still exists, just slimmed; full removal happens
in commit 13).
* `apis/index.ts` — `ApiHub` interface + `buildApiHub(ctx)` factory.
Each commit grows `ApiHub` by one entry; today only `message` is
populated.
* `apis/message.ts` — `MessageApi` with seven methods inlined from
the previous Bridge implementation:
sendGroup ← Bridge.sendGroupMessage
sendPrivate ← Bridge.sendPrivateMessage
sendC2cFile ← Bridge.sendC2cFileMessage
recallGroup ← actions/group-message.ts::recallGroupMessage
recallPrivate ← actions/group-message.ts::recallPrivateMessage
markGroupRead ← actions/group-message.ts::markGroupMessageRead
markPrivateRead ← actions/group-message.ts::markPrivateMessageRead
(Names trimmed: the `apis.message` prefix already signals the
context, so `apis.message.sendGroupMessage` would be redundant.)
Bridge changes
--------------
* Removed the 7 methods listed above (moved to MessageApi).
* Removed unused imports + `SEND_MSG_CMD` constant + `buildSendElems`
+ `protobuf_encode/decode` (now used only inside MessageApi).
* Added `readonly apis: ApiHub` field, built eagerly in constructor.
* Promoted `nextMessageRandom` / `nextClientSequence` from private →
public (Api classes need them to build SendMessageRequest packets).
* `BridgeInterface` now also lists `apis: ApiHub` + the two
generators; the 7 deleted method signatures are gone.
Caller-site updates (~9 files)
------------------------------
* `bridge/actions/group-file.ts` — `bridge.sendC2cFileMessage(...)`
→ `bridge.apis.message.sendC2cFile(...)`.
* `onebot/instance-context.ts` — markGroupMsgAsRead / markPrivateMsgAsRead
bindings now route through `apis.message.markGroup/PrivateRead`.
* `onebot/modules/message-actions.ts` — every send/recall site
rerouted; `typeof ref.bridge.sendXxx` type refs also updated.
* `onebot/actions/extended.ts` — same.
Tests (~5 files)
----------------
* `tests/actions/_helpers.ts` — `mockBridge()` grows an `apis` field
built by `mockApiHub()`; the previous flat `sendGroupMessage` etc.
vi.fn stubs are replaced with `mockMessageApi()`.
* All test files using direct `bridge.sendXxx` / `bridge.recallXxx` /
`bridge.markXxxAsRead` calls or stubs updated to `bridge.apis.message.*`.
* No behaviour changes — 587/587 pass (same baseline; pre-existing
network-manager native-addon failure unchanged).
|
||
|
|
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`. |
||
|
|
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. |
||
|
|
5d83aedd83 |
fix(typecheck): narrow ctx explicitly and type buildSendElems mock params
CI tsc flagged two strict-mode issues in
|
||
|
|
d1aade6f87 |
fix(forward): keep file segments in long-msg payload via forwardFake flag
Previous fix split file segments off at the OneBot live-send layer
(c2c → sendC2cFileMessage, group → sendGroupFileMessage) and made
element-builder drop any `{type:'file'}` segment that reached it.
That broke forward messages: send_forward_msg encodes each node
through buildSendElems for the long-msg upload, so file segments
inside a forward node hit the drop-with-warn path and the forward
bubble shipped without the file.
Forward upload is NOT a live PbSendMsg — it stores the gzipped
protobuf verbatim through `trpc.group.long_msg_interface.MsgService.
SsoSendLongMsg`, and the recipient decodes file entities through the
normal msg-push path (transElem(24) → GroupFileExtra; msgContent →
FileExtra { file: NotOnlineFile }). So `result=79` doesn't apply here.
Add a `forwardFake?: true` flag on SendContext; when set:
* group file → element-builder emits transElem(24) again (restored
makeGroupFileElem, exactly the receive-side wire shape).
* c2c file → element-builder still skips; forward-builder pulls the
segment off the node and writes `body.msgContent` as
FileExtra { file: NotOnlineFile }, with file metadata hydrated
from the upload cache when inline fields are missing (same
pattern as send_private_msg).
Mirrors NapCat's PacketMsgFileElement.{buildElement,buildContent}
split (dev/napcatQQ/.../packet/message/element.ts:530-610) and
Lagrange.Core V2's MultiMsgUploadService.cs.
|
||
|
|
c83c333e73 |
fix(file): route group publish through OIDB 0x6d9_4 and cache upload meta for c2c resend
The chat-publish step after upload_group_file used to ship a PbSendMsg
with a transElem(elemType=24) — but that wire shape is RECEIVE-side
only, and current QQ-NT rejects it on send with `result=79`. Lagrange.
Core V2 uses a dedicated `OidbSvcTrpcTcp.0x6d9_4` for the publish
(`GroupSendFileService.cs`); mirror that. Send_group_msg with a
`{type:'file', file_id}` segment now routes through the same path
after the OneBot layer splits file segments out (matching the c2c
file split pattern).
Separately, send_private_msg with just `{file_id}` was shipping a 0 B
file because the wire packet for c2c file send needs fileSize/fileMd5/
fileName/fileHash but the OneBot caller only echoes the file_id from
upload_private_file. Add a bounded (1024-entry, FIFO) bridge cache
populated at upload time so the send-message path can rehydrate the
full tuple; inline segment fields still take precedence so callers
that track the metadata themselves can bypass the cache. Parser now
also accepts `file_hash`/`fileHash` on file segments.
Also: file segments no longer get a `[空消息]` log line — the format
string now shows `[文件:<name-or-id>]`.
|
||
|
|
cec38b4c1b |
fix(forward): piggyback every nested level onto the outermost long-msg upload (NapCat model)
Cross-referenced `dev/NapCatQQ/.../SendMsg.uploadForwardedNodesPacket`
(208-347): every layer of a nested forward attaches its own msgBody to
the *outer* long-msg upload under a uuid `actionCommand` slot. Receiver
walks the whole tree from one server fetch instead of resolving each
layer's res_id separately.
`dev/Lagrange.Core` (V2 branch on `linwenxuan04/v2`,
`MultiMsgUploadService.cs:23-31`) only writes a single MultiMsg action
— that's why NapCat-style receivers couldn't follow our previous
nested-forward sends past the first level.
Refactor:
1. `ForwardNodePayload` gets an optional `innerForward?:
ForwardNodePayload[]` field. `parseForwardNodes` no longer eagerly
uploads inner chains — it just attaches them here.
2. `uploadForwardNodes` now drives the whole recursion. The new
`uploadForwardNodesRecursive`:
- walks each node, recursively uploads any `innerForward` first
- replaces that node's `elements` with a real `{type:'forward'}`
preview pointing at the inner res_id
- piggybacks the inner level's `{uuid, msgBody}` + every deeper
level it surfaced into this level's `myInnerActions`
- encodes the long-msg payload as
`[{MultiMsg, ownMsgBody}, ...innerActions.map(uuid+msgBody)]`
- returns `{resId, msgBody, uuid, innerActions}` so an outer call
can keep climbing
Top-level public signature unchanged: `Promise<string>`.
3. New `deriveInnerSource` / `previewLinesFromNodes` /
`previewFromElements` helpers synthesise the ARK preview metadata
the inner bubble shows before the user taps to expand. They handle
text/image/record/video/file/forward/face element previews.
4. `parseForwardNodes`: when `content` is an all-node array, attach
the parsed inner chain to `node.innerForward` and let the upload
pipeline take over. `elements = []` placeholder is replaced by
`uploadForwardNodes` once it has the real res_id.
Tests:
- `actions/forward.test.ts`:
- `uploadForwardNodes recursively uploads nested innerForward chains
(NapCat piggyback model)` — asserts two SsoSendLongMsg roundtrips
(inner first, then outer) and the outer res_id propagates up
- `flat sends still take a single roundtrip` — backwards-compat
- `onebot/forward-nested.test.ts`: rewritten — mock now sees exactly
one `uploadForwardNodes` call with `innerForward` attached, instead
of two sequential calls; depth-limit + mixed-content tests
unchanged.
|
||
|
|
04a905460b |
fix(file-upload): read upload host from rtpMediaPlatformUploadAddress (NT 2026 rollout)
The 2026 QQ-NT server has stopped populating *every* legacy host field (uploadIp / uploadDomain / uploadIpList / uploadHttpsDomain / uploadDns). The host now arrives in the new field 210 `rtpMediaPlatformUploadAddress` — a repeated `IPv4` message carrying `inIP`/`inPort` (LAN) and `outIP`/`outPort` (WAN). Coworker's debugging hint "host 好像是 fix64" was the 8-12-byte length-delimited IPv4 message looking like a fixed-width field on the wire. Cross-referenced acidify (https://github.com/LagrangeDev/acidify) — the only NT-protocol fork that's tracked this change (refactored in their 2026-04 protobuf rewrite). They read `inIP`+`inPort` exclusively because that's the address the highway PUT actually needs to reach (same DC as the OIDB endpoint). LagrangeGo / Lagrange.Core / NapCat still ship the old schema and would fail the same way against this server build. Changes: - Proton schema: add `IPv4` message (fields 1-5: outIP/outPort/inIP/ inPort/iPType, all int32 varint) + field 210 on the upload response - Consumer: prefer `rtpMediaPlatformUploadAddress[0].inIP`+`inPort` before falling back through the legacy host chain - New helper `int32ToIpv4Dotted` reverses acidify's `Int.toIpString()` (little-endian byte order, `>>>` for unsigned shift so high-bit IPs don't render as negative octets) - Diagnostic warn-log now dumps the rtp address array too - Regression test: 192.168.1.1 (LE-packed 0x0101A8C0 = 16885952) + inPort=8080 should reach highway PUT |
||
|
|
87ac6ffedb |
fix(file-upload): fall back through parallel host fields when uploadIp is empty
User report: `upload_private_file` was failing with "private file upload host is invalid" on a fresh upload (boolFileExist=false). The OIDB call succeeded but `uploadIp` (field 60) came back empty, and we treated that single field as the only source of truth. Cross-referenced LagrangeGo's `OidbSvcTrpcTcp0xE37_800.proto` + NapCat's `Oidb.0XE37_800.ts`: the server has six parallel host slots on this response (uploadIp / uploadDomain / uploadIpList[0] / uploadHttpsDomain / uploadDns / uploadLanip). LagrangeGo + Lagrange.Core only read field 60 and would hit the same dead-end — we walk the chain instead. Also log the full slot set when we genuinely can't find a host so future reports can name which field the server actually populated. Adds two regression tests: one proves the fallback to `uploadDomain` goes to highway successfully, one proves we still throw when every slot is empty. |
||
|
|
e637647877 |
fix(bridge): publish uploaded files as chat messages (the "empty message" bug)
`upload_group_file` / `upload_private_file` used to do only the
upload half of the napcat-equivalent flow:
OIDB 0x6D6_0 alloc → highway PUT → return fileId
The fileId was real, the bytes were on QQ's CDN, the logs said
"uploaded" — but the chat showed nothing because no message was
ever sent referring to the upload. NapCat's `UploadGroupFile`
action (`dev/napcatQQ/.../UploadGroupFile.ts:54-58`) does the
upload AND immediately calls `MsgApi.sendMsg(peer, [fileElement])`
in the same action; that second step is what makes the file
visible in the chat.
Add the missing send step. Two paths because the wire shape
differs:
- **Group files** ride on `Elem.transElem` (type 24) in the
`RichText.elems` array. Add a `makeGroupFileElem()` helper +
`case 'file':` in `element-builder.buildSendElems`, then
`bridge.sendGroupMessage(groupId, [fileElement])` after a
successful upload. The TransElem wraps a `GroupFileExtra`
protobuf in `0x01 | BE16(len) | payload`, byte-symmetric with
the receive-side decoder at `rich-body-decoder.ts:171-185`.
- **C2C (private) files** live on `RichText.notOnlineFile`
(field 3 on RichText, parallel to `elems`), so they can't go
through `buildSendElems` at all. Add `Bridge.sendC2cFileMessage`
that builds the `SendMessageRequest` with `notOnlineFile`
directly and dispatches via `sendRawPacket`. The decoder side
that lifts these back to `{type:'file'}` segments has lived
at `rich-body-decoder.ts:425-433` the whole time.
Behavioural detail: both upload paths call the chat-publish step
*only* when `uploadFile=true` (the default). `uploadFile=false`
keeps the upload-only semantics for callers that just want a
fileId without dropping a message in the chat. If the chat post
fails for any reason (server rejection / network), the action
still returns the fileId with a warning log — the file is on QQ's
side, callers can re-publish if they want.
Regression coverage in `tests/actions/group-file.test.ts`:
- publishes the file as `{type:'file'}` via sendGroupMessage on
group upload
- skips the post when uploadFile=false
- returns success even when the chat post fails
- same trio for private upload going through sendC2cFileMessage
- and confirms private path does NOT route through the
`sendPrivateMessage` elems[] pipeline
core: 618/618 tests pass (+5), typecheck clean. Production build
contains 8 substituted call sites for the new GroupFileExtra +
SendMessageRequest paths under proton.
|
||
|
|
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
|
||
|
|
b3df633c87 |
fix(bridge): correctness pass on highway, hook, and identity service
* highway/utils detectImageFormat: JPEG SOF marker check now excludes 0xC4 (DHT), 0xC8 (JPG), and 0xCC (DAC). The old `(marker & 0xFC) === 0xC0` matched DHT et al. and then read Huffman table bytes as image dimensions, producing junk width/height for many real JPEGs. * loadBinarySource takes an optional maxBytes; defaults to 1 GiB to defend against an authenticated client triggering a multi-GB load into memory. Group/private file uploads pass FILE_UPLOAD_MAX_BYTES (4 GiB) because QQ's file protocol legitimately supports that size. HTTP fetches also pick up a 60s timeout and a Content-Length precheck. * highway-client.tcpConnect detaches its error/timeout listeners after a successful connect so subsequent upload IO errors don't reject an already-resolved promise. readHttpResponseBody uses local listener refs + a central detach(), eliminating MaxListenersExceededWarning during multi-chunk uploads. * hook/qq-hook-client.nextRequestId now wraps inside uint32 explicitly and skips id 0 / in-flight ids. The previous `nextRequestId++ >>> 0` loses precision past Number.MAX_SAFE_INTEGER (postfix increment evaluates before the shift) and could collide live request ids on very long-running processes. * identity-service.rememberUidUin removes stale partner entries before inserting a new (uid, uin) pair. The old code left orphan uidByUin / uinByUid entries after a uid/uin re-bind, so the two maps could disagree. |
||
|
|
97ad2e22bb |
feat(onebot): implement set_group_portrait via highway cmdId 3000
Port the Lagrange.Core protocol path for group avatar upload —
napcat's NTQQ GroupService.setHeader has no oidb wrapper, but
Lagrange.Core/Internal/Context/Logic/Implementation/OperationLogic.cs
spells out the wire shape: same highway HTTP endpoint as personal
avatar, but with cmdId=3000 and a GroupAvatarExtra proto carried as
the `extend` blob (type=101, groupUin, field3={field1=1}, field5=3,
field6=1 — all four are protocol-prescribed constants).
Adds:
- GroupAvatarExtraSchema in proto/oidb-action.ts
- bridge.setGroupAvatar(groupId, source) wrapping the highway upload
- BridgeInterface.setGroupAvatar
- OneBot set_group_portrait action (was a stub returning
'not yet implemented'; accepts the same file-source variants as
set_qq_avatar)
5 new tests: 3 bridge-action (cmdId, extra-blob round-trip, empty-file
guard) + 2 OneBot (forwarding, param validation) — also tightens the
existing setAvatar test to assert the extend blob stays empty.
|
||
|
|
f7457e3e59 |
feat(onebot): add set_diy_online_status
DIY online status reuses the existing StatusService.SetStatus wire call. napcat hardcodes status=10 / extStatus=2000 and threads the faceId / wording / faceType through a customExt sub-message (field 4 of SetStatus, per napcat packet/transformer/proto/action/action.ts). Implementation: - Extend SetStatusReqSchema with optional customExt field. Encoder skips undefined fields so the existing set_online_status path stays byte-identical on the wire (verified by a new round-trip test). - New bridge.setDiyOnlineStatus(faceId, wording, faceType) wraps the same dispatchSetStatus helper as setOnlineStatus. - OneBot action set_diy_online_status accepts napcat-style payloads (face_id / face_type can be string or number; wording defaults to ''). 9 new tests across actions/profile and onebot/extended-actions. |
||
|
|
ee62f5aa0b |
feat(onebot): add napcat-parity actions (Tier 1 + Tier 2)
Wires 16 OneBot actions that napcat exposes but SnowLuma previously either lacked or stubbed: Tier 1 (pure glue over existing Bridge capabilities) - send_packet / .send_packet: real impl over Bridge.sendRawPacket - bot_exit, nc_get_packet_status, nc_get_rkey - get_group_ignored_notifies / get_group_ignore_add_request: project fetchGroupRequests(filtered=true) into napcat shapes (was empty stub) - delete_group_folder alias for delete_group_file_folder Tier 2 (new oidb proto + Bridge method) - set_/complete_/cancel_group_todo (oidb 0xF90_1/2/3) - nc_get_user_status (oidb 0xFE1_2 with napcat's status decode) - get_ai_characters / get_ai_record / send_group_ai_record (oidb 0x929D / 0x929B; AI voice reuses the existing fetchGroupPttUrlByNode) BridgeInterface now exposes sendRawPacket as the deliberate escape hatch for send_packet — every other surface method already wraps it. 46 new tests: 13 bridge-action (mocked runOidb dispatch checks) + 33 OneBot-action (ApiHandler.handle end-to-end). |
||
|
|
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.
|
||
|
|
ac10c663f1 |
refactor(identity): absorb QQInfo into IdentityService
QQInfo's in-memory state (self identity, friends, group rosters,
profile cache) and read methods (findFriend / findGroup /
findGroupMember / findUserProfile / nickname / uin / selfProfile) now
live directly on IdentityService. The QQInfo class is gone; its
interfaces stay in `qq-info.ts` as plain types shared across the
remember*/find* surface.
This eliminates a long-standing duplication: every observation used to
maintain both QQInfo's in-memory structures and IdentityService's
SQLite mirror in parallel, with the uidByUin/uinByUid Maps tracked
separately. With one Module owning all three layers, every remember*
hydrates one set of structures and `loadSnapshot` repopulates the
in-memory uid↔uin index from SQLite on startup — closing the cold-cache
gap that previously forced findUidByUin to fall through to a SQLite
query after every restart.
Knock-on changes:
- `Bridge` no longer holds a QQInfo; constructor takes an
`IdentityService`. The CmdParser signature on the cmd dispatch
table now passes `IdentityService` to parsers.
- `msg-push-handler.ts` drops its private `resolveUidToUin(qqInfo, …)`
helper that bypassed IdentityService's cascade — the parser hot
path now reads through `identity.findUinByUid(uid, groupId)` and
benefits from the O(1) `uinByUid` map even on freshly-loaded
sessions.
- `BridgeManager`'s session-started callback drops the QQInfo
parameter; OneBotInstance no longer carries a `qqInfo` field and
exposes a `nickname` getter for the WebUI listing.
- `contact-actions` and the per-theme bridge action files stop
importing QQInfo; they read everything via `bridge.identity.*`.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |