dev2
115
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3870052fb4 |
Simplify Hub channel management
Refactor Hub to treat each UIN as a single active Channel: channelsByUin_ now maps UIN -> Channel (no Set of IDs), primary selection logic and primaryByUin_/pickPrimary were removed, and addChannel replaces any existing channel for a UIN (disposing the previous one). Updated add/remove flows to update channelsById_ and channelsByUin_ accordingly and to bring cores online/offline in the simplified model. Also: removed extensive JSDoc/comment blocks from channel/adapter/channel-ctx/core/hub files to slim the public API surface, reordered/cleaned exports in packages/channel/src/index.ts, added a new hub-events.ts placeholder, and cleaned up a test helper comment. Disposal of replaced channels is wrapped with a try/catch and logged at debug level. |
||
|
|
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). |
||
|
|
adf0744f75 |
Rename bridge → channel and reorganize packages
Move/rename the transport/bridge code into a new @snowluma/channel layout and extract hook implementations into channel-specific packages. Many bridge sources were deleted and equivalent channel files added (adapter/ctx/interface/channel/hook/socket pieces moved/renamed), along with tests and configs relocated under packages/channel. RoadMap.md replaced with a detailed v3 architecture plan describing Channel/ChannelCtx/Core/CoreCtx/Hub boundaries and migration steps. Also apply related core package updates (account/core reshaping and package.json adjustments) to match the new architecture. |
||
|
|
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. |
||
|
|
5440cbc37e |
Refactor bridge system: adapters & transports
Introduce a transport-adapter layer and refactor the bridge abstractions to be transport-agnostic. Added BridgeAdapter and BridgeAdapterHost (packages/core/src/bridge/adapter.ts) plus concrete InjectBridge/InjectBridgeAdapter and ProtocolBridge/ProtocolBridgeAdapter implementations (new files). Made Bridge an abstract base exposing kind/id and abstract sendRawPacket, moved transport-specific PID/sender logic into InjectBridge. Reworked BridgeManager to register/start adapters, manage bridges by id/uin, pick a primary per-UIN (inject preferred over protocol), and provide a host API for adapters to add/remove bridges. Updated index bootstrapping to register adapters and wire the WebUI to the inject adapter's HookManager. Adjusted tests and OneBot code to remove PID tracking from OneBotInstance/OneBotManager and to instantiate InjectBridge in tests. Also exported new entry points in package.json for adapter/bridge modules. This decouples transports from manager logic and adds a stubbed protocol runtime for future pure-protocol support. |
||
|
|
456086599c |
Normalize imports, typings and formatting
Reorder and normalize imports, add missing type imports, and fix whitespace/formatting across multiple files. Notable updates: eslint.config.ts import ordering; core bridge APIs (group-album, misc, web) now import Json types; tests/highway pipeline import ordering and added type imports; protocol changes (element-builder, highway client/pipeline) reorganized imports and added proto type imports; many oidb service modules moved protobuf encode/decode imports for consistency; web group modules (album, essence, honor, notice, request-util) reordered imports, adjusted typings/indentation, and small whitespace fixes. These edits are stylistic and typing improvements and should not change runtime behavior. |
||
|
|
80ee12ecc2 |
Enforce no-explicit-any; allow in tests
Enable @typescript-eslint/no-explicit-any as an error in the ESLint config to tighten typing across the codebase, and add an override to disable this rule for test files. Also apply non-functional cleanup in packages/proton/src/runtime.ts: reorder/imports and adjust formatting/indentation and string alignment for readability — no behavioral changes. |
||
|
|
f53df5e313 |
refactor(packages): extract @snowluma/bridge (hook + QQ lifecycle)
Split the QQ NT client lifecycle / hook injection layer out of @snowluma/core into its own self-contained package. The new bridge package owns: - native hook addon loader (injector) - HookManager + per-PID HookSession orchestration - PipeWatcher for QQ process discovery - QqHookClient pipe protocol - qq-port-probe for early UIN sniffing The hook layer used to type its main collaborator as `BridgeManager` directly. To keep the new package free of any dependency on @snowluma/core, the cross-package coupling is now expressed via a 3-method `BridgeManagerSink` interface that lives in the bridge package. BridgeManager (in core) satisfies the interface structurally with no source changes; the top-level app/webui still inject it the same way. Pure file relocation otherwise: git mv preserves history at 100% on every file, the only logic edit is the interface introduction in hook-manager.ts and an updated `nativeSearchDirs()` offset (one fewer `..` since hook now lives at packages/bridge/src instead of packages/core/src/hook). |
||
|
|
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. |
||
|
|
4755a91cb3 |
fix(image): use Chinese summary defaults to match QQ ecosystem
Mobile QQ, Lagrange.Core, and NapCat all use the literal Chinese
bubble-preview strings `[图片]` / `[动画表情]`. SnowLuma was emitting
English `[image]` / `[sticker]` as fallback, so QQ clients showed
"[image]" in chat-list previews instead of the expected Chinese.
Cross-checked against:
dev/NapCatQQ/.../packet/message/element.ts:367-371
dev/Lagrange.Core/.../Message/Entity/ImageEntity.cs:186-192
Both branch the same way: subType=1 → `[动画表情]`, else → `[图片]`.
Changes:
- `bridge/highway/image-upload.ts`: both code paths (fingerprint +
load-from-source) now emit the Chinese defaults. Subtype branch
flipped to `subType === 1 ? '[动画表情]' : '[图片]'` to match the
upstream convention (our old check was `subType === 0` →
`[image]`, which would label any non-zero subType — not just
stickers — as `[sticker]`).
- `bridge/msg-push/rich-body-decoder.ts`: all three decode paths
(NotOnlineImage / CustomFace / commonElem image) now substitute
the Chinese default when the server omits `textSummary`. Empty-
string is also treated as missing (matches NapCat's `=== ''`).
- Tests updated to assert the Chinese defaults; new test pins
that a caller-supplied summary still overrides verdef.
The OneBot caller's `summary` field on the MessageElement still
wins when set, so downstream clients passing custom bubble text
(e.g. for accessibility) keep working.
|
||
|
|
c8f259ef4a |
test(highway): pin multi-image sequential semantics + richer 921 diag
User reported `highway upload error_code=921` when sending two images
in one OneBot message — image 1 fast-uploaded (server already had the
md5), image 2 (58 bytes) triggered the actual PUT and failed.
Verified the claim from code inspection vs NapCatQQ + Lagrange.Core:
- dev/NapCatQQ/.../highwayContext.ts:62-94 (checkAvailable + prepareUpload)
- dev/NapCatQQ/.../highwayHttpUploader.ts:8-43 (HTTP block upload)
- dev/NapCatQQ/.../highwayContext.ts:219-254 (uploadGroupImage)
- dev/Lagrange.Core/.../HighwayContext.cs:98-180 (UploadSrcByStreamAsync)
Findings:
1. The pipeline runs `runNtv2Upload` sequentially per image (await
inside `buildSendElems`); there's no concurrency or shared state.
2. Each call gets its own OIDB request, its own lazy session fetch,
its own buildHighwayExtend output. No aliasing.
3. The 921 comes from the highway PUT's response head (errorCode),
not from anything client-emitted being malformed.
Verdict: not reproducible from code inspection — almost certainly
server-side (most likely Tencent rejecting the 58-byte image as a
malformed payload; 58B is too small to even be a valid 1×1 PNG).
Hardening:
- Three regression tests pin the sequential two-image semantics so
a future state-leak bug fails loudly:
- extend bytes for image 2 must come from image 2's response,
not image 1's,
- each runNtv2Upload fetches its own non-null session,
- each OIDB request carries its own fileInfo + a unique
clientRandomId.
- The `error_code=N` message now carries segRetCode, cmdId, chunk
size, offset, and the first 8 bytes of the file md5 — without
these the user couldn't tell apart "image rejected" vs "session
expired" vs "rate-limited" on future reports.
If the user reports the same 921 again, the new diagnostic message
should show enough server-side context to triage server-vs-client.
|
||
|
|
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.)
|
||
|
|
2638d13d11 |
feat(profile): surface QQ level from OIDB 0xFE1_2 number-property 105
`fetchUserProfile` already requested key 105 in the keys[] array but the decode pass never read it, so callers got nothing. NapCat exposes the same value as `qq_level` on `get_stranger_info`; users coming from NapCat noticed the field was missing on the SnowLuma side. Cross-checked against LagrangeV2 Internal/Services/System/ FetchStrangerService.cs which explicitly labels key 105 = Level and uses `numbers[105]` directly. Changes: - UserProfileInfo: add `level: number`. - fetchUserProfile decode: `info.level = numMap.get(105) ?? 0`. - getStrangerInfo: emit both `qq_level` (NapCat-compat) and `level`. - Test/manager call sites updated for the new required field. |
||
|
|
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. |
||
|
|
9a30df9907 |
refactor(onebot): extract OneBot protocol translation into @snowluma/onebot
Phase 3 of the 4-package split (RoadMap #3). Moves the entire `packages/core/src/onebot/` subtree (43 source files + 20 test files spanning network adapters, action handlers, event converters, message parser, message store, media store, instance + manager) into the new `@snowluma/onebot` workspace package. The remaining `@snowluma/core` now hosts only the api-layer (Bridge class + 13 Api classes), the hook tooling, the WebUI server, and the runnable `index.ts`. Why --- - The OneBot adapter is the public-facing OneBot11 protocol surface; it has its own version cadence and its own consumers (third-party bots talking OneBot WS / HTTP). Splitting it lets that cadence move independently of the Api-on-ctx layer. - The two were sharing the `packages/core/` build pipeline only because they happened to live in the same directory. Nothing in the api-layer reaches into OneBot internals. - Mirrors NapCat / Lagrange.OneBot's repo split (their OneBot adapter is a separate assembly from the Core protocol library). Layout ------ Moved via `git mv` (history preserved): packages/core/src/onebot/* → @snowluma/onebot/src/<x> packages/core/tests/onebot/* (6 files) → @snowluma/onebot/tests/ packages/core/tests/{message-parser,message-store,event-pipeline, media-store,media-url-resolver,event-converter, http-server-hot-reload,http-server-content-type, api-handler-logging,message-id,group-msg-emoji-like, config,media-indexer,network-manager}.test.ts (14 files) → @snowluma/onebot/tests/ What stays in @snowluma/core ---------------------------- - `bridge/{bridge,bridge-interface,bridge-context,manager}.ts` - `bridge/apis/*` (the 13 typed Api classes) - `hook/*` (debugger/injector — tooling for the core protocol layer) - `webui/*` (admin UI server — application wiring, not the OneBot adapter itself) - `index.ts` (application entry that wires BridgeManager + OneBotManager + HookManager + WebUI together) Changes ------- - New package `packages/onebot/` (package.json with subpath exports mirroring legacy `core/src/onebot/<x>` 1:1, tsconfig.json, vitest.config.ts with `SNOWLUMA_LOG_LEVEL=debug` env so the action- dispatch logging tests see debug-level subscribers — copied verbatim from core's vitest.config.ts). - @snowluma/core grows new subpath exports for the api-layer files OneBot's type-only imports need: `./bridge`, `./bridge-interface`, `./bridge-context`, `./manager`, `./apis`. - `@snowluma/onebot` depends on `@snowluma/bridge`, `@snowluma/common`, `@snowluma/core`, `@snowluma/proto-defs`, `@snowluma/proton`, `@snowluma/websocket`. - `@snowluma/core` depends on `@snowluma/onebot` (for index.ts + webui/server.ts wiring). - 100+ import sites rewritten via sed: * OneBot → core: `'../bridge/bridge'` → `'@snowluma/core/bridge'`, `'../bridge/manager'` → `'@snowluma/core/manager'`, `'../bridge/bridge-interface'` → `'@snowluma/core/bridge-interface'`, `'@/bridge/bridge'` (vite path alias) → `'@snowluma/core/bridge'`. * Core → OneBot: `'./onebot/manager'` → `'@snowluma/onebot/manager'`, `'../onebot/config'` → `'@snowluma/onebot/config'`, etc. * Test relative paths: `'../../src/onebot/<x>'` → `'../src/<x>'` after the test file move (one level shallower). - New `packages/onebot/src/env.d.ts` declaring `__APP_VERSION__` and `__BUILD_WEBUI__` (the vite-injected globals `info.ts` reads). Workspace cycle --------------- core ↔ onebot is allowed by pnpm workspaces. The runtime direction is one-way: core → onebot (index.ts constructs OneBotManager, webui/server reads OneBot config). OneBot only reaches into core for TYPES (Bridge, BridgeInterface, BridgeManager, ClientKeyInfo, DownloadRKeyInfo); those imports use `import type` so they compile out at runtime and don't introduce a real cycle. Verification ------------ - Full workspace `pnpm -r typecheck`: all 11 projects clean. - `@snowluma/core` test suite: 302/302 pass (33 test files). - `@snowluma/onebot` test suite: 250/250 pass (20 test files; pre-existing `network-manager.test.ts` native-addon failure unchanged — same baseline as before the split). - Combined: 552/552 — identical pass count to the pre-Phase-3 baseline. - Vite build with stubbed native binary: clean, 2.84 MB bundle / 224 KB gzip (identical envelope). |
||
|
|
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. |
||
|
|
f4c6838e65 |
refactor(api): move 10 web methods to WebApi + finalize cleanup (#6 commit 12/13)
Final commit of the #6 NapCat-style Api refactor. Inlines the four files under `web-actions/` (cookies / group-essence / group-honor / group-notice) into `apis/web.ts::WebApi`. Both the `actions/` and `web-actions/` directories are now empty and deleted from the tree. Method renaming (Api namespace already says "web"): forceFetchClientKey → forceFetchClientKey (kept — unique name) getCookiesStr → getCookiesStr (kept — disambiguates from the new public getCookies) getCsrfToken → getCsrfToken getCredentials → getCredentials getGroupEssence → getEssence getGroupEssenceAll → getEssenceAll getGroupHonorInfo → getHonorInfo sendGroupNotice → sendNotice getGroupNotice → getNotice deleteGroupNotice → deleteNotice Plus a new public `getCookies(domain)` on WebApi so GroupAlbumApi can reach the cookie-acquisition flow through `this.ctx.apis.web.getCookies` rather than reaching into a private helper. Internal helpers (`getPSkey` / `getSKey` / `getBknFromSKey`) remain file-private since they're only used by the publicly-exposed methods above. Cleanup performed in this same commit: • `actions/shared.ts` → moved to `apis/shared.ts` (only file left in actions/; it's a small helper module used by 4 Api classes). • `actions/` directory deleted entirely. • `web-actions/` directory deleted entirely. • `BridgeInterface` slimmed to `BridgeContext + activePid` (the only non-Api field OneBot consumers reach into). The 70-line list of method signatures is gone — every method type now comes from the `ApiHub` definition in `bridge-context.ts`. • `bridge.ts` no longer imports/wraps any action functions. Its public class is just the per-account state holder + `apis: ApiHub`. Callers updated: `instance-context.ts`, `actions/extended.ts`, `actions/group-info.ts`. Tests under `tests/web-actions/` deleted — those tested file-private helpers (`getPSkey`/`getSKey`/`getBknFromSKey`) that are no longer reachable from outside; the public WebApi surface is exercised through OneBot integration tests in `extended-actions.test.ts`. 552/552 remaining tests pass. (Test count down 35 from the deleted web-actions tests; production behaviour unchanged.) This concludes the #6 refactor. The 12 Api hubs that the OneBot side can reach via `bridge.apis.*`: message / contacts / groupAdmin / groupFile / groupAlbum interaction / friend / profile / forward / misc / extras / web |
||
|
|
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`. |
||
|
|
aea2676c31 |
Add TypeScript ESLint configs and lint scripts
Add a repository .editorconfig and a root eslint.config.ts to standardize formatting and linting. Migrate packages/webui ESLint config to TypeScript (eslint.config.ts) with project-specific rules and tsconfigRootDir handling, and remove the old JS config. Update package.json to run ESLint directly (add lint and lint:fix) and add required ESLint/dev dependencies; also add jiti to packages/webui/package.json. Include a small formatting tweak to tsconfig.base.json. |
||
|
|
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.
|
||
|
|
cc574632f1 |
fix(test): declare vi.fn param types so mock.calls[0] infers tuple
`vi.fn(async () => undefined)` infers `mock.calls: [][]`, so destructuring `const [groupId, fileId] = ...mock.calls[0]!` fails under TS strict tuple checks (`error TS2493: Tuple type '[]' of length '0' has no element at index '0'`). Declare the param types inline so the call-args tuple is `[number, string]`. |
||
|
|
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>]`.
|
||
|
|
6de2e071be |
Forward image sub_type and summary fields
Include optional element.subType and element.summary in the image segment payload (mapped to sub_type and summary) so custom emoji/sticker metadata is preserved. Adds a unit test to verify these fields are forwarded. |
||
|
|
1db287d8cc |
fix(c2c-file): rewrite sendC2cFileMessage wire shape per Lagrange.Core V2
The c2c file send path was wrong on three independent axes — every
c2c file the bot tried to deliver was getting rejected by the QQ-NT
server. Aligned to `dev/Lagrange.Core` (V2 branch) which is the only
direct-protocol reference for c2c files; NapCat goes through QQ-NT
IPC and never builds this packet itself.
Three wire-shape fixes:
1. **Routing**: `routingHead.c2c { uin, uid }` → `routingHead.trans0x211
{ ccCmd: 4, uid }`. RoutingHead field 15 is the c2c-file scene's
slot. `MessagePacker.BuildPacketBase` makes this conditional on
`chain.HasTypeOf<FileEntity>()`: when a file is present, C2C goes
nil and Trans0X211 carries the routing.
2. **File payload location**: `messageBody.richText.notOnlineFile {...}`
→ `messageBody.msgContent = serialize(FileExtra{file: NotOnlineFile})`.
The server doesn't read c2c file metadata from `richText` at all —
`MessageBody.cs:12` even has the comment "Offline file is now put
here(?". Confirmed by `FileEntity.PackElement()` returning empty
and `PackMessageContent()` returning the FileExtra wrapper.
3. **contentHead.c2cCmd**: `11` → `0` (default/omitted). `11` was a
stale go-cqhttp value the QQ-NT server doesn't recognise on the
c2c-file dispatcher. Lagrange leaves it at the C# default (0).
Schema fixes (proton + legacy, both directions):
- `RoutingHead` gains `trans0x211?: pb<15, RoutingTrans0x211>`. The
new message has `toUin@1`, `ccCmd@2`, `uid@8`.
- `SendMessageBody` gains `msgContent?: pb<2, bytes>` (the receive-side
`MessageBody` already had it).
- `FileExtraInfo` deleted — was wrong on every tag (fileSize=1,
fileName=2, fileMd5=3, fileUuid=4, fileHash=5, none of which match
the wire). `FileExtra { file: pb<1, NotOnlineFile> }` now wraps the
same `NotOnlineFile` used on the receive path. This is why c2c file
PUSHES were also silently failing the four-field truthy check in
`extractMsgContent` — the schema couldn't decode anything.
- `NotOnlineFile` gains the three intake-validator fields the server
requires on send: `subcmd@9 = 1`, `dangerEvel@50 = 0`, `expireTime@55`
= now+7d.
Receive side (`rich-body-decoder.ts::extractMsgContent`): drop the
"all four fields must be truthy" guard (it was a workaround for the
broken schema) and accept any payload that decodes a fileUuid.
Tests:
- `bridge-private-routing.test.ts`: new regression that round-trips
`sendC2cFileMessage` through `protoDecode` and asserts trans0x211
routing, msgContent FileExtra wrapper, NotOnlineFile field tags,
c2cCmd=0. Catches each of the three wire-shape regressions
independently.
- `send-private-c2c-file.test.ts`: vi.fn arg signatures explicit for
CI typecheck.
647/647 tests pass.
|
||
|
|
f65fe959bb |
fix(send-private): split c2c file segments out to sendC2cFileMessage (was shipping "[空消息]")
User report: `send_private_msg` with a `{type:'file', file_id}` segment
fired `[ElemBuilder] file send via elems[] is group-only; use bridge.
sendC2cFileMessage for c2c` and the message went out as "[空消息]".
Root cause: c2c files live on `RichText.notOnlineFile` (parallel to the
elems array, see proto/proton/message.ts), not inside `elems[]`. The
element-builder explicitly drops c2c `{type:'file'}` elements with a
warning. `sendPrivateMessage` was passing the file straight to
`bridge.sendPrivateMessage(userId, [fileElement])` which then ran
through `buildSendElems`, lost the file at the drop, and shipped a
0-element wire send — rendered as "[空消息]" by every receiver.
Fix: split at the OneBot layer in `sendPrivateMessage`. Pull file
elements out of the batch and dispatch each via
`bridge.sendC2cFileMessage`; route the rest through the regular
elems[] path. Matches NapCat's `dev/NapCatQQ/.../SendMsg.ts:404-415`
where FILE / VIDEO / ARK / PTT each get their own sendMsg call and
never mix with the regular elems[] batch.
- File-only message: only the c2c file API is called (single roundtrip)
- Mixed text + file: two sends, text first then file
- File without file_id: dropped with a warn (was silently shipping
empty messages)
- UID resolution failure: surfaced as throw instead of empty send
Also rewrote the element-builder fallback warn from "use
sendC2cFileMessage for c2c" (now stale advice) to "BUG: c2c file
reached element-builder — should be split out at the OneBot layer".
Tests: 4 new regression cases under
`tests/onebot/send-private-c2c-file.test.ts`. 646/646 pass.
|
||
|
|
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.
|
||
|
|
0b8dc1f7af |
fix(video): populate NTV2 fileInfo.time with the real duration (receiver was rendering 00:00)
The NTV2 upload request for video carried `time: 0` on the main mp4 sub-file, mirroring NapCat's transformer. But NapCat lives on top of the desktop QQ-NT client's IPC layer, which patches the duration into the resulting MsgInfo before the wire send — we don't have that layer, so the `time: 0` we passed in ended up baked into the `VideoFile. fileTime` field of every video the bot sent. Receivers then rendered every video bubble as "00:00". Cross-checked acidify's `RichMediaUpload.kt::buildVideoUploadInfoList` (2026-04 refactor) — they're a protocol-direct client too and write `time = payload.videoDuration` for exactly this reason. We already have `video.duration` from `loadVideo` (probed via ffprobe), so it's a one-liner. Width/height kept at 0 deliberately — acidify fills them, NapCat zeros them, and our earlier comment recorded "QQ NT server treats non-zero values here as schema mismatch on c2c sends". Out of scope for this fix; revisit if a c2c regression surfaces. Test asserts `fileInfo.time === 10` for the main sub-file (matches FINGERPRINT.duration) and stays 0 on the thumb. |
||
|
|
8f839f4f30 |
fix(group-file): correct GroupFileExtra wire shape — server was rejecting with result=79
Two off-by-one bugs in the group-file chat-post element that combined to make the QQ-NT server reject every group file send with result=79 (visible as "group file uploaded but chat post failed" in the logs): 1. Inner GroupFileInfo fields were shifted one tag left: - fileSha was field 5, should be field 6 - extInfo was field 6, should be field 7 - fileMd5 was field 7, should be field 8 Field 5 is actually a uint32 placeholder (`field5` in both NapCat and acidify). With the bug, our fileSha bytes were landing where the server expected a varint and the rest of the message got skipped as garbage. 2. Outer GroupFileExtra was missing the magic field1=6 + fileName slots that NapCat's encoder writes (`packet/message/element.ts:589`). Acidify's schema also models them as required even though it serialises with defaults. The current server build rejects sends without these slots populated. Cross-referenced: - NapCat `GroupFileExtraInfo` (component.ts:146-155) — matches our new field numbers exactly - acidify `GroupFileExtra.Inner.Info` (proto/message/extra/GroupFileExtra.kt) — same field numbers; their schema uses generic `field5`/`field7` names but the wire layout is identical Changes: - Proton + legacy schemas: shift inner fields, add outer field1/ fileName/display - `makeGroupFileElem`: write field1=6 + fileName on the outer wrapper - Regression test asserts each inner tag lands at 50/58/66 (not the old 42/50/58) and verifies the new outer field1 + fileName bytes are present |
||
|
|
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 |