codex/async-sqlite-node24
1191
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.)
|
||
|
|
c3675aef7c |
feat(emoji-like): back fetch_emoji_like / get_emoji_likes with a local cache
Two rounds of production wire-level probing established that the OIDB
SSO path cannot surface the reactor user list for an emoji:
- 0x9083_1 → 4-byte ack `18 01 20 01` (no list)
- 0x9084_1 → per-emoji summary (count + timestamp,
still no user IDs)
- 0x9082_3..5, 0x9083_0/2/3, 0x9084_2..5, 0x9085_1/2, 0x9087_1
→ "no privilege" — the cmds exist but
aren't in NTQQ's client capability
bitmap, so the server blocks anything
not registered at login. NTQQ itself
serves getMsgEmojiLikesList from a
wrapper-internal cache, not SSO.
Mirror that wrapper-cache approach on the OneBot side. SnowLuma
already receives GroupMsgEmojiLikeEvent push events for every
reaction add/remove (with operator uin + uid + emoji + msgSeq); we
just weren't persisting them. Now we do.
Changes:
* proto-defs: add Oidb0x9084 Req/Resp schema for the summary call.
RespEntry's lastReactionTime/count are optional because the
"available reactions catalog" tail entries omit both — only the
"used" subset carries metadata.
* core InteractionApi: revert the 9-candidate probe loop, restore a
single 0x9083_1 call as a legacy stub (returns empty list, with a
debug-level hex dump on empty responses), and add
fetchReactionSummary() that calls 0x9084_1 and yields the per-emoji
summary (count + last-reaction timestamp).
* onebot ReactionStore: new SQLite-backed cache, schema
reactions(group_id, msg_seq, emoji_id, emoji_type, operator_uin,
operator_uid, set_at), PK on (group_id, msg_seq, emoji_id,
operator_uin). Mirrors MessageStore design — prepared statements at
construction, WAL journal.
* onebot event-pipeline: subscribe group_msg_emoji_like, call
reactionStore.recordAdd / recordRemove before convertAndDispatch so
a fetch_emoji_like racing with the push still sees the new row.
* onebot instance / instance-context: own the ReactionStore, expose
it on OneBotInstanceContext, close it on dispose. New
fetchEmojiLikeUsers helper on ApiActionContext combines local-cache
reads with a 0x9084_1 cross-check so callers can detect cache gaps
(events missed before bot boot, etc).
* onebot actions: get_emoji_likes and fetch_emoji_like now read from
the cache. fetch_emoji_like's NapCat-style cookie becomes a numeric
offset for cache pagination (no real cursor needed against local
SQLite).
* tests: 7 new ReactionStore unit tests (CRUD + pagination +
cross-key isolation + summarizeMessage). Existing event-pipeline
test harness picks up a no-op ReactionStore stub.
|
||
|
|
65636953e7 |
chore(interaction): round-2 cmd probe — focus on 0x9084 family
Round-1 wire dump revealed:
- 0x9083_1 returns a 4-byte ack (`18 01 20 01`), no user data
- 0x9082_3/_4, 0x9083_0/_2/_3 all return "no privilege" (server-side
deny linked to iwiki.woa.com permission docs)
- 0x9084_1 returns 932 bytes — a real reaction *summary* on the
message: first entry carries (timestamp, count, type, emoji_id)
for the used emoji ("76" with count=3 in our test), followed by
a 28-entry catalog of available reaction emojis. NOT a user list,
but proves 0x9084 family has live handlers.
The "fetch users who reacted with emoji X" sibling is almost
certainly another subcmd of 0x9084. Probe the immediate neighborhood:
0x9084_2..5 first-choice candidates (sibling subcmds)
0x9085_1..2 exploratory adjacent cmd
0x9086_1, 0x9087_1 wider probe
0x9084_1 kept at end so we still capture the summary baseline
|
||
|
|
7e3e833f84 |
chore(interaction): probe 7 OIDB cmd candidates per fetch_emoji_like call
Replace the env-driven probe with an unconditional round-robin across
the candidate list every fetch_emoji_like invocation:
0x9083_1 baseline (returns 4-byte ack, known not to be fetch)
0x9082_3 sibling of set/unset reaction
0x9082_4
0x9083_0
0x9083_2
0x9083_3
0x9084_1 exploratory
For each candidate the cmd, response length, parsed user count,
request hex, and response hex are dumped at debug level. The first
candidate that surfaces users wins and is returned; failures inside
runOidb (server-side error code, no handler) are caught + continued
so a bad cmd in the middle doesn't tank the whole probe.
fetch_emoji_like is user-triggered ("view who reacted"), so the
extra SSO traffic on each call is acceptable for a debug build —
we only need one production hit to identify the real cmd.
|
||
|
|
a24272b25a |
chore(interaction): env-driven cmd/subcmd override for fetch_emoji_like probing
Wire dump from production confirmed 0x9083_1 is a real server endpoint (routed, errorCode=0, trace string echoes "OidbSvcTrpcTcp.0x9083_1") but its reply is always a 4-byte minimal ack `18 01 20 01` regardless of message / emoji / whether the caller just set a reaction — so the semantics of 0x9083_1 are not "fetch emoji-like users". The real cmd must be a sibling (0x9082_3, 0x9082_4, 0x9083_2, 0x9083_3, …) but we don't yet know which. Rather than rebuild for each candidate, accept an env override `SNOWLUMA_EMOJI_FETCH_CMD=0x9082_3`. Format is `0xNNNN_N`; a malformed value warns and falls back to the historic 0x9083_1. Wire name is recomputed from the parsed (cmd, subcmd) so the trpc routing matches the OIDB envelope cmd field. Also unconditionally dump req/resp hex at debug level while we're probing — once we land on a working tuple the dump can be tightened back to "only on empty users". The pre-existing log line is widened to include the wire name and the result count so deployment can scan multiple cmd values from a single log stream. |
||
|
|
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. |
||
|
|
ce78a3bbdf | fix(oidb): update sequence type in Oidb0x9083Req to uint_64 for compatibility | ||
|
|
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. |
||
|
|
08b7ab7f15 |
Merge pull request #63 from SnowLuma/dev
chore: promote dev → main (
v1.9.1
|
||
|
|
e088167476 |
fix(lockfile): rebase against origin/main to unblock PR #63 auto-merge
The previous commit (
|
||
|
|
ee0f159b7e |
fix(deps): drop dead hono devDeps from @snowluma/onebot, align core to ^4.12.18
`packages/onebot` declared `hono` and `@hono/node-server` as devDeps but
never imported either — they were leftover scaffolding from when onebot
was carved out of `@snowluma/core`. Combined with dependabot's hono
bump only landing on core's pin (4.12.14 → 4.12.18 in
|
||
|
|
4a92dd71ad |
Merge pull request #62 from SnowLuma/dev
chore: promote dev → main (
|
||
|
|
607a7b319b | chore(release): v1.9.1 | ||
|
|
a13e09131f |
Merge pull request #61 from SnowLuma/native/auto-update-native-03d4d0e
chore(native): replace native artifacts (native-03d4d0e) |
||
|
|
66f8a5eb2f |
chore(native): replace native artifacts (native-03d4d0e)
Source: SnowLuma/nnphook@03d4d0ede7 Build: https://github.com/SnowLuma/SnowLumaNative/releases/tag/native-03d4d0e |
||
|
|
d68535e3d1 |
Merge pull request #60 from SnowLuma/native/auto-update-native-117413b
chore(native): replace native artifacts (native-117413b) |
||
|
|
61f40703ca |
chore(native): replace native artifacts (native-117413b)
Source: SnowLuma/nnphook@117413b517 Build: https://github.com/SnowLuma/SnowLumaNative/releases/tag/native-117413b |
||
|
|
5f1d4400dc |
fix(build): inject createRequire banner so bundled CJS requires resolve
better-sqlite3's JS wrapper is pure CJS — its `require('fs')` /
`require('path')` / `require('bindings')` calls get inlined into the
ESM bundle as `__require("fs")` shims by rolldown. The shim's
fast-path checks `typeof require !== "undefined"` (true in CJS, false
in our `.mjs` output) and otherwise throws "Calling `require` for
\"fs\" in an environment that doesn't expose the `require` function".
The launcher crashed at startup the moment `@snowluma/sqlite` resolved
its `import BetterSqlite3 from 'better-sqlite3'`.
Inject `const require = createRequire(import.meta.url)` via
`rollupOptions.output.banner` so every inlined CJS require routes to
Node's CJS loader. No call-site changes; the only diff in the emitted
bundle is two new top-of-file import/const lines.
|
||
|
|
4e51b4bd7d |
build(sqlite): wrap better-sqlite3 with @snowluma/sqlite + vendor prebuilts
Replaces "runtime npm dependency that gets `npm install`-ed into
dist/" with "TS wrapper that loads the native `.node` from
`dist/native/` like every other native binding in the project".
Why the change
--------------
The previous approach (better-sqlite3 declared in
packages/runtime/package.json, materialized by `npm install
--omit=dev` in dist/) worked but produced an artifact that needed
runtime npm install — a step the rest of SnowLuma's native bindings
don't need (websocket / snowluma-hook / ffmpeg are all standalone
`.node` files copied into `dist/native/` by vite-plugin-cp). The
asymmetry was friction: dev mode required `pnpm dist:install`,
build artifacts carried a `dist/node_modules/` tree the other
modules didn't need, and Docker images had to special-case the
glibc-vs-musl ABI lookup that prebuild-install runs.
The new shape:
- `@snowluma/sqlite` (packages/sqlite/) — TS wrapper. Has
`better-sqlite3` as a devDep (for the JS-layer types + the
actual JS wrapper code that vite inlines). At runtime, calls
`createRequire().require()` against
`<dist|repo>/native/better-sqlite3-v<abi>-<platform>-<arch>.node`
and passes the resolved addon to `new Database(path, {
nativeBinding })`.
- `packages/runtime/native/better-sqlite3-v<abi>-<triplet>.node`
— prebuilt binaries vendored into the repo. Two ABIs ship side-
by-side (v127 = Node 22 LTS used by CI, v137 = Node 24 used by
dev boxes) so `process.versions.modules` picks the matching one
at load time.
- `tools/fetch-sqlite-prebuilts.mjs` — fetches all 5 platforms ×
N ABIs from WiseLibs's GitHub releases. Idempotent: existing
`.node` files skip. Accepts extra ABI args:
`node tools/fetch-sqlite-prebuilts.mjs 12.10.0 v127 v137 v115`.
What was unwound
----------------
- packages/runtime/package.json — dependencies removed (runtime
artifact is now self-contained, no `npm install` step needed).
- root package.json — `dist:install` script removed (no node_modules
to install).
- .github/workflows/dev-build.yml — `Bundle runtime dependencies`
step removed.
- .github/workflows/release.yml — kept the `Bundle runtime
dependencies` step (was a no-op already, but harmless safety net
in case a future runtime dep shows up).
- packages/core/vite.config.ts — `better-sqlite3` no longer
external; vite inlines the JS wrapper (+~33 KB to bundle, ~2.84
MB → 2.87 MB total).
Naming
------
Mirrors the existing `websocket-${triplet}.node` convention (flat
dir under `runtime/native/`, dash-separated `<platform>-<arch>`).
Adds a `v<abi>` segment because better-sqlite3 is NOT N-API — one
binary per Node ABI. Naming:
better-sqlite3-v127-darwin-arm64.node Node 22 / macOS arm
better-sqlite3-v127-darwin-x64.node Node 22 / macOS intel
better-sqlite3-v127-linux-arm64.node Node 22 / Linux arm
better-sqlite3-v127-linux-x64.node Node 22 / Linux x64
better-sqlite3-v127-win32-x64.node Node 22 / Windows x64
better-sqlite3-v137-... (5) Node 24 / same 5 hosts
Total: 10 binaries × ~1.9 MB = ~19 MB committed. Aligned with the
existing native-vendoring footprint (websocket ~2 MB × 5, snowluma-
hook ~10 MB × Windows + Linux, ffmpeg ~10-30 MB × 4).
What changes for consumers
--------------------------
Source-side: nothing. `import Database, { type Database, type
Statement } from '@snowluma/sqlite'` is a drop-in for the previous
`import Database from 'better-sqlite3'`. The wrapper re-exports the
same types and the `Database` callable factory has the same
overload set.
Runtime: `node` finds the binding at
`<dist|repo>/native/better-sqlite3-v${process.versions.modules}-
${process.platform}-${process.arch}.node`. If the ABI doesn't
match a vendored binary, the wrapper throws a clear error pointing
at `tools/fetch-sqlite-prebuilts.mjs` with the missing ABI/triplet.
Bumped better-sqlite3 from 11.10.0 → 12.10.0 because v11 only had
prebuilt up to Node ABI v127 (no v137 = no Node 24). v12.10.0
ships both. Both versions are API-compat (the breaking changes
between v11 and v12 were either internal or in error-handling
paths SnowLuma doesn't exercise).
Verification
------------
- pnpm -r typecheck (12 projects): clean
- @snowluma/core test: 302/302
- @snowluma/onebot test: 250/250 (552/552 combined, unchanged from
the perf-series baseline)
- Bench numbers preserved — wrapper indirection is sub-μs:
storeMeta ~11 μs, storeEvent ~17 μs, findMeta hit ~1.1 μs (all
within noise of the post-RoadMap-#5 measurements).
- Vite build clean, bundle = 2.87 MB / 230 KB gzip
- `dist/index.mjs` no longer contains a `node:sqlite` import (it
did before — media-store hadn't migrated; now neither it nor
message-store reach into the Node builtin).
Bonus side-fix: `packages/onebot/src/media-store.ts` also moved
off `node:sqlite` (caught by the post-build bundle grep — the
file was using `DatabaseSync` + `StatementSync` from the builtin
even though it had already been written to cache its prepared
statements). Its hot paths now go through the same wrapper.
|
||
|
|
4e34f8aeb0 |
Merge pull request #59 from SnowLuma/native/auto-update-native-c0dc4db
chore(native): replace native artifacts (native-c0dc4db) |
||
|
|
26777d0010 |
chore(native): replace native artifacts (native-c0dc4db)
Source: SnowLuma/nnphook@c0dc4db649 Build: https://github.com/SnowLuma/SnowLumaNative/releases/tag/native-c0dc4db |
||
|
|
b69d8ac83c |
build(ci): refresh pnpm-lock.yaml for runtime better-sqlite3 dep
`pnpm install --frozen-lockfile` in CI rejected the lockfile after
|
||
|
|
998eba2ba8 |
build(ci): wire better-sqlite3 native binding into release artifacts
Followup to
|
||
|
|
3f2b474120 |
RoadMap: tick off #5 after the perf series lands
Three perf commits delivered the wins: - |
||
|
|
a70f194218 |
perf(msg-push): drop Buffer.from() wrap on pkt.body (free allocation)
`PacketInfo.body` is typed as `Uint8Array` in `@snowluma/common/ protocol-types` and arrives as one — the `Buffer.from(uint8Array)` wrap that fed it into `protobuf_decode<PushMsg>(...)` was a no-op copy. `Buffer` is a `Uint8Array` subclass, and proton's decoder accepts the parent type directly, so the wrap added one allocation per incoming push packet without changing behavior. Bench delta (parseMsgPush, pure decode): group_message (text only) 0.29 μs/op → 0.25 μs/op (~1.2x) group_message (5 mixed) 0.50 μs/op → 0.47 μs/op (~1.07x) group_member_join 0.25 μs/op → 0.30 μs/op (within noise) Small absolute gain — but `parseMsgPush` runs once per incoming packet at the very front of the pipeline, so the saved allocation compounds with the per-message GC churn. The text/mixed group- message cases pick up cleanly; the join case sits in noise (it has a smaller body so the Buffer wrap was cheap relative to total). Verification: pnpm -r typecheck clean; 552/552 tests pass. |
||
|
|
0b6541ca36 |
perf(sqlite): swap node:sqlite for better-sqlite3 + cache prepared stmts
Replaces the experimental `node:sqlite` builtin with the stable
`better-sqlite3` wrapper across the two hot SQLite users — the
OneBot message-store and the bridge identity-service. While the API
surface is essentially identical (sync, prepared-statement-driven,
`Database`/`Statement` shape), better-sqlite3 (a) doesn't carry the
"ExperimentalWarning: SQLite is an experimental feature" baggage
that ships with `node:sqlite`, (b) has a more mature C++ binding
that consistently outperforms the builtin by ~30-50%, and (c)
encourages reusing `Statement` objects by design — exactly the win
the second half of this commit is after.
Why both at once
----------------
Caching prepared statements (was P1 in the RoadMap-#5 analysis) is
the largest single optimization opportunity on the SQLite path. The
old shape was the textbook SQLite anti-pattern:
// every call re-parsed the SQL
this.db.prepare(BIG_SQL_STRING).run(...);
Each `prepare(sql)` runs the full SQL parser + planner — for simple
inserts/updates/selects that dominates wall-clock cost. Doing both
the driver swap AND the statement caching in one commit lets the
benchmark numbers attribute the combined effect honestly; splitting
them would have double-charged the same disruption budget.
What changed code-wise
----------------------
**messageStore** — 8 distinct SQL templates, all now `readonly`
fields prepared once in the constructor:
- stmtStoreEvent — INSERT/UPDATE event row + JSON payload
- stmtStoreMeta — INSERT/UPDATE meta-only row
- stmtFindEvent — single-row SELECT by message_hash
- stmtFindMeta — single-row SELECT (full meta columns)
- stmtResolveReplyGroup, stmtResolveReplyPrivate
— 2 variants for the reply-id lookup
- stmtListEventsAnchored, stmtListEventsLatest
— 2 variants for /get_history_msg
**identity-service** — 25+ distinct statements. Used a Map-based
`pstmt(sql)` memoization helper rather than enumerating all 25 as
named fields (call sites stay readable; Map lookup is O(1) hash +
string-eq, negligible next to the SQL parse it replaces). The
helper short-circuits to the cached `Statement` on second-and-later
calls.
API differences from node:sqlite (handled in this commit):
- `import { DatabaseSync } from 'node:sqlite'`
→ `import Database from 'better-sqlite3'`
- `new DatabaseSync(path)` → `new Database(path)`
- `db.exec('PRAGMA journal_mode = WAL')`
→ `db.pragma('journal_mode = WAL')`
- `db.exec('BEGIN' / 'COMMIT' / 'ROLLBACK')` — kept as-is; both
drivers accept raw `exec()` for transaction control. The
`db.transaction()` higher-order wrapper is more idiomatic
but the existing manual control already works and changing
it would invalidate the `transaction()` helper's matched
try/catch shape.
vite build wiring:
- `node:sqlite` removed from the externals list
- `better-sqlite3` added — must stay external because it has a
native binding (`require('./build/Release/better_sqlite3.node')`)
that Vite can't bundle. Vendoring the prebuilt `.node` into
`packages/runtime/native/` is a follow-up packaging task; the
workspace `pnpm install` already places the binding under
`node_modules` so dev + tests work today.
Bench results (RoadMap #5 baseline → after)
-------------------------------------------
@snowluma/bridge — recv pipeline (with SQLite identity writes):
group_member_join 17.5 μs/op → 4.7 μs/op (~3.7x faster)
group_message text 0.42→0.42 μs (no path change)
group_message mixed 0.67→0.67 μs (no path change)
@snowluma/onebot — send pipeline:
messageStore.storeMeta 17.0 μs/op → 10.9 μs/op (~1.6x)
messageStore.storeEvent 23.2 μs/op → 18.2 μs/op (~1.3x)
messageStore.findMeta hit 5.4 μs/op → 1.3 μs/op (~4x)
messageStore.findMeta miss 3.7 μs/op → 0.7 μs/op (~5x)
messageStore.findEvent 3.8 μs/op → 1.7 μs/op (~2.3x)
Read paths gained the most because they're parse-bound (small SQL
+ short execution → the parser was most of the cost). Writes gained
less because the fsync/journal overhead on each commit still
dominates the parse savings. Combined send hot path (storeMeta +
storeEvent, called on every outbound message + the /get_msg-fix
self-event store) drops from ~40 μs to ~29 μs — about a 27%
reduction per message sent.
Verification
------------
- pnpm -r typecheck: all 11 projects clean
- @snowluma/onebot test suite: 250/250
- @snowluma/core test suite: 302/302
- Combined: 552/552 (unchanged baseline)
- Vite build (with stubbed native binary): clean, 2.84 MB / 223 KB
gzip (matches pre-Phase-2 envelope after removing the better-
sqlite3 inline-bundle attempt — externalizing it shaves the
~30 KB regression we saw mid-flight)
Follow-up (next commits in this series)
---------------------------------------
- Skip rememberGroupMemberIdentity DB write when the (uid, uin,
nickname, card) tuple is unchanged in the in-memory cache.
- Drop the `Buffer.from(pkt.body)` allocation in msg-push context.
- Synchronous fast-path for elementsToJson when no async resolver
is wired.
- Vendor `better-sqlite3` prebuilt `.node` into the runtime/native
layout for the production package step.
|
||
|
|
f3f5d27c65 |
perf(bench): bench harness for receive/send chains (RoadMap #5 baseline)
vitest-driven benches at `packages/bridge/bench/recv.bench.ts` and
`packages/onebot/bench/send.bench.ts`. Picks up proton's vite plugin
so call sites get the inlined codecs (running via tsx + proton
runtime-fallback would throw without a pre-built runtime map).
Run via `pnpm -F @snowluma/bridge bench` and
`pnpm -F @snowluma/onebot bench`.
What's measured
---------------
Receive chain:
- parseMsgPush: pure decode + dispatch (no SQLite, no event emit)
* group_message text ~ 0.29 μs/op (3.4M ops/sec)
* group_message 5 mixed ~ 0.50 μs/op (2.0M ops/sec)
* group_member_join ~ 0.25 μs/op (4.1M ops/sec)
- IncomingPacketPipeline.process: full pipeline (with SQLite via
rememberGroupMemberIdentity, BridgeEventBus emit)
* group_message text ~ 0.42 μs/op (2.4M ops/sec)
* group_message 5 mixed ~ 0.67 μs/op (1.5M ops/sec)
* group_member_join ~17.5 μs/op ( 57K ops/sec) ⚠
Send chain:
- parseMessage (OneBot input → MessageElement[])
* CQ string text ~ 0.31 μs/op
* CQ string mixed ~ 1.22 μs/op
* segment array text ~ 0.32 μs/op
* segment array 5 ~ 1.07 μs/op
- messageStore SQLite writes:
* storeMeta ~17.0 μs/op ( 59K ops/sec) ⚠
* storeEvent (~250B JSON) ~23.2 μs/op ( 43K ops/sec) ⚠
- messageStore reads:
* findMeta hit ~ 5.4 μs/op (186K ops/sec)
* findMeta miss ~ 3.7 μs/op (272K ops/sec)
* findEvent hit ~ 3.8 μs/op (262K ops/sec)
Headline finding
----------------
Pure decode/encode is already cheap (sub-μs). The real cost per
sent message is the SQLite write pair (storeMeta + storeEvent =
~40 μs). On receive, group_message itself does NOT trigger
identity writes; only join/leave/admin events do — but those go
through 4-6 db.prepare(sql).run/get(...) calls each, all re-parsing
SQL on every invocation (the canonical SQLite anti-pattern).
The follow-up commits in this series target:
1. Replace `node:sqlite` (experimental, API may shift) with
`better-sqlite3` (stable, sync, ~30-50% faster wire) + cache
prepared statements as readonly fields. Single commit, both
wins together — better-sqlite3's API encourages stmt caching
by design. Expected: storeMeta / storeEvent drop to ~3-5 μs,
group_member_join drops to ~3-5 μs.
2. Skip rememberGroupMemberIdentity DB write when the in-memory
uidByUin / uinByUid cache already has the mapping unchanged.
3. Drop the `Buffer.from(pkt.body)` allocation in msg-push
context (pkt.body is already Uint8Array; proton accepts it).
4. Make `elementsToJson` sync-fast-path when no async resolvers
are wired (saves per-element microtask overhead).
Each commit re-runs these benches and the message should quote the
before/after numbers so the deltas are auditable.
Note: tsx is NOT involved — vitest's bench mode owns the run, and
the proton vite plugin is configured in each `vitest.config.ts`.
Absolute numbers reflect the production code path.
|
||
|
|
070e39090a |
fix(hook): retry stranded 'disconnected' sessions when pipe is still up
Windows users reported "running a while then stops" with logs:
WARN [Hook] pipe error: PID=1312 err=read EPIPE
DEBUG [Bridge] session closed: UIN=752645868
INFO [OneBot] session closed: UIN=752645868
The user-side socket read fails with EPIPE (idle-timeout, transient
Windows named-pipe fault, or similar). The cascade:
1. hook-client's `socket.on('error')` → emits 'error'
→ hook-session logs "pipe error: PID=… err=read EPIPE"
2. socket then emits 'close' → bindClient's close handler runs
`tearDownClient()` and sets status to 'disconnected' (because
wasLoggedIn = true)
3. PipeWatcher's next tick polls `listLivePipes()` natively — and
the OS-level named pipe is STILL listed (DLL is still resident
in QQ.exe, it never closed its end). So no `pipe-down` →
`pipe-up` cycle ever fires.
4. hook-manager's `tick` reconcile loop only retried sessions
whose status was exactly 'connecting' (line 208 in
hook-manager.ts). A 'disconnected' session that should
reconnect was simply ignored.
Result: the session was stranded forever; QQ kept running, DLL
kept running, but SnowLuma never re-established its socket. The
only recovery was a full SnowLuma restart.
Fix: include `'disconnected'` in the retry predicate. `onPipeUp` is
idempotent (it checks `isClosed` / `!connected` internally and rebuilds
the socket pair from scratch) so we can safely call it from the
tick loop every ~1.5s until either the reconnect lands or the
watcher reports a genuine pipe-down.
No behavior change for sessions already in any other state — only
the 'disconnected' branch gains the auto-retry.
|
||
|
|
450b551701 |
RoadMap: tick off #6 after the OneBot-side ctx slim-down lands
#6 was the "API层挂到ctx" item — Core side delivered the 13 typed Api classes back in the e8d1af4...f4c6838 commit series; this turn finished the OneBot side via the 4 ctx slim-down commits ( |
||
|
|
d983635c8f |
refactor(onebot): inline ctx.bridge.apis.profile/web/groupAlbum direct calls
Remove the remaining ~20 passthrough fields from ApiActionContext spanning four Api areas — most were already dead (no caller) since extended.ts had migrated to direct `ctx.bridge.apis.X` paths in earlier #6 commits. profile area: - setProfile / setOnlineStatus / fetchCustomFace / setGroupAvatar ── all already dead - getProfileLike ── one live caller in extended.ts inlined web area: - getGroupHonorInfo / getGroupEssence / getGroupEssenceAll / sendGroupNotice / getGroupNotice / deleteGroupNotice / getCookiesStr / getCsrfToken / getCredentials / forceFetchClientKey ── all already dead groupAlbum area: - getGroupAlbumList / uploadImageToGroupAlbum / getGroupAlbumMediaList / commentGroupAlbumMedia / deleteGroupAlbumMedia / likeGroupAlbumMedia ── all 7 live callers in actions/group-album.ts inlined to `ctx.bridge.apis.groupAlbum.<m>(...)` friend area: - setFriendRemark ── already dead Behavior preserved: getProfileLike's `userId = 0 → self` semantics keep working because ProfileApi.getLike uses `isSelf = !userId` internally (0 and undefined are equivalent for that check); same default args at the action site (`start = 0`, `count = 10`). Group-album action handlers now wrap the typed Api return values (GroupAlbumMediaResult, GroupAlbumCommentResult) with `as unknown as JsonValue` for okResponse compatibility — the old ctx wrapper was typed `Promise<any>` which silently hid the cast; making the cast explicit doesn't change runtime. Part 4 of the OneBot-side slim-down. 552/552 tests still green. |
||
|
|
8bf2ff3f36 |
refactor(onebot): inline ctx.bridge.apis.friend/interaction/message direct calls
Remove 8 passthrough fields from ApiActionContext spanning three Api
areas:
friend area:
- handleFriendRequest (set_friend_add_request)
- handleDeleteFriend (delete_friend)
- setFriendRemark already kept the field name but now unused
interaction area:
- sendFriendPoke / sendGroupPoke (friend_poke, group_poke, send_poke)
- getEmojiLikes (was already dead — extended.ts used the direct
path; field removed)
message area:
- markGroupMsgAsRead / markPrivateMsgAsRead (already dead —
mark_msg_as_read sites use the module helper)
`handleGroupRequest` stays on ctx because `handleGroupAddRequest` is
a module helper that does the flag-parsing + group-request roster
lookup before routing to `bridge.apis.groupAdmin.setAddRequest` —
real composition, not a passthrough.
Behavior preserved: same flag parsing (`asString/asNumber/asBoolean`),
same `isGroup` flag flatten into `sendPoke(isGroup, peerUin, target?)`,
same default `block=false` for delete_friend.
Part 3 of the OneBot-side slim-down. 552/552 tests still green.
|
||
|
|
4c4ce60bf2 |
refactor(onebot): inline ctx.bridge.apis.groupFile direct calls
Remove 6 passthrough fields from ApiActionContext for the group-file area (`uploadGroupFile / uploadPrivateFile / getGroupFileUrl / createGroupFileFolder / getPrivateFileUrl / getGroupFileCount`). Action sites (`actions/group-file.ts` and one site in `actions/extended.ts`) now call `ctx.bridge.apis.groupFile.<method>` directly. `getGroupFiles` stays on the ctx because its module helper (`getGroupFiles(bridge, groupId, folderId)` in `modules/contact-actions.ts`) does identity-lookup enrichment of uploader/creator names — not a pure passthrough. Behavior preserved: same default values (`name = ''`, `folderId = '/'`, `busId = 102`, `uploadFile = true`) — they were baked into the ctx wrapper before, now they're explicit in the action site (`asString` returns `''`, `... || '/'`, `... || 102`, `asBoolean(..., true)`). The result-unwrap (`fileId` extraction from `UploadFileResult`) moves from the ctx wrapper into the action handler. Part 2 of the OneBot-side slim-down. 552/552 tests still green. |
||
|
|
cb698d2435 |
refactor(onebot): inline ctx.bridge.apis.groupAdmin direct calls
Remove the 6 pure pass-through fields from `ApiActionContext` for the group-admin area (`setGroupKick / setGroupKickMembers / setGroupBan / setGroupWholeBan / setGroupLeave / setGroupRemark`). All 5 call sites in `actions/group-admin.ts` now invoke `ctx.bridge.apis.groupAdmin.<m>` directly. `setGroupRemark` had zero remaining callers (extended.ts was already on the direct path) so the field is simply deleted. Part 1 of RoadMap #6 OneBot-side slim-down. Core side's typed Api hub (bridge.apis.X) has been in place since the #6 commit series; OneBot actions were still routing through a 90+ field bag-of- functions on `ApiActionContext`. Most of those fields were 1:1 shims like `setGroupKick: (g,u,r) => bridge.apis.groupAdmin.kickMember(g,u,r)` — pure indirection with no composition logic. This commit kills the group-admin chunk; the friend / interaction / profile / web / album / groupFile / message chunks land in follow-up commits. Behavior preserved byte-for-byte: same parameter parsing (`asNumber/asString/asBoolean`), same default values, same 1800s mute fallback, same error responses. 552/552 tests still green. |
||
|
|
a011b8f033 |
chore(sdk): point actions-sync test at @snowluma/onebot
Path follow-up to the 4-package split — the action registrations moved from `packages/core/src/onebot/actions/` to `packages/onebot/src/actions/` in Phase 3. The test was failing with ENOENT on the old path before reaching the actual assertion. Note that this test has a separate pre-existing failure (it imports a non-exported `SNOWLUMA_ACTIONS` symbol from the SDK); the path fix just clears the first of the two errors. |
||
|
|
72320121c3 |
RoadMap: tick off #3 after the 4-package split lands
Three phased commits delivered the split:
-
|
||
|
|
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. |
||
|
|
e1c9ede9de |
RoadMap: tick off #2/#4 and partial #6 after the dev-branch refactor
- #2 (Proton ts+map fallback for unbundled debug) — landed in `994633e feat(proton): resolve bare specifiers via TypeScript module resolution` + `6c7e56d refactor(proto-defs)`. Bare specifiers like `@snowluma/proto-defs/oidb` now resolve to the original `.ts` source through `ts.resolveModuleName` (Bundler mode), so tsx/node + vite share the same import surface. - #4 (proto definitions in their own package) — landed in `6c7e56d refactor(proto-defs)`. Moved 13 proto `.ts` files (~3.3k LOC) into the new `@snowluma/proto-defs` workspace package with per-file subpath exports; 44 call sites across core/tests rewritten. - #6 (NapCat-style Api on ctx) — half done. Core side fully landed through 13 sequential commits ( |
||
|
|
e7afef5d63 |
fix(proto): correct OidbGroupReaction field numbers (off-by-one)
`set_msg_emoji_like` was failing with "invalid ReqBody.EmojiType: value must be greater than 0" because `OidbGroupReaction` had every field number off by one — `type` was at field 4, but the server reads `EmojiType` at field 5. The mistake covered all four fields: field what was should be ───── ──────── ─── ───────── 2 groupUin 1 2 3 sequence 2 3 4 code 3 4 5 type 4 5 Cross-checked with Lagrange.Core V2's `OidbSvcTrpcTcp0x9082.cs` — their definition starts at field 2 and runs through 7. The OIDB envelope wraps the body at field 4, so the inner body's own fields must start at 2 to avoid collision with the wrapper. (The previous 1..4 layout happened to encode SOMETHING wire-valid, just not what the server's `OidbSvcTrpcTcp0x9082` validator expected.) Also added the trailing `field6` and `field7` unused booleans that Lagrange serialises as `false`. The server currently tolerates them missing; including them keeps SnowLuma byte-identical with Lagrange in case the validator gets stricter. The reaction-type heuristic stays the same (`code.length > 3 ? 2 : 1` — Lagrange uses the same `IsEmoji => Code.Length > 3` check in `GroupAddReactionEvent.cs`). |
||
|
|
0479924482 |
fix(msg): cache self-sent messages so /get_msg can find them
`/get_msg` with a `message_id` returned by `send_*_msg` was failing
with "message not found" because the send-side only called
`cacheMessageMeta` (used by recall/delete), never `storeEvent`
(used by /get_msg lookup).
The QQ server sometimes echoes self-sent messages back through the
receive channel (which would populate `storeEvent` via the normal
event pipeline), but it's not reliable — Lagrange and NapCat both
work around this by persisting a local copy at send time. Lagrange
writes into Realm DB inside `SendMessageOperation.cs`; NapCat caches
in-memory via its msg service.
Mirror them: synthesise a `post_type: 'message_sent'` event from the
parsed `MessageElement[]` (converted back to OneBot segments via the
existing `elementsToOneBotSegments`) and write it directly into
`messageStore.storeEvent` — bypassing `dispatchEvent` so we don't
broadcast the synthetic event over the WS bridge (the QQ server's
own echo is the source of truth for that).
Wired into all five outbound paths:
- sendPrivateMessage (modules/message-actions.ts)
- sendGroupMessage
- sendGroupForwardMessage (the wrapping ARK card)
- sendPrivateForwardMessage
- forwardSingleMessage (both branches)
The helper is defensive: tests stub `messageStore` as
`{ findEvent: () => null } as any` without `storeEvent`, so the
helper checks for the function and bails quietly when absent.
Failure inside the helper is logged but never thrown — recall +
the send return value matter more than /get_msg lookup.
|
||
|
|
e5e85d8d4b |
fix(file): consult upload cache when emitting transElem(24) in forwards
`send_forward_msg` with a `{type:'file', file_id}` segment to a GROUP
was sending a 0 B file (no name, no size, no md5/sha1) because
`makeGroupFileElem` read those fields directly off `MessageElement`
without falling back to `bridge.recallUploadedFile()`.
The c2c forward path in `apis/forward.ts::buildForwardPushBody`
already does this lookup (line ~126) — that's why forwarding to a
private chat worked but to a group didn't. Mirror it here.
OneBot11 callers typically thread only the file_id through (the
upload action returns just the id), so the cached metadata is the
only place size/md5/sha1/name come from. Without it the long-msg
payload encodes `GroupFileExtra { fileSize=0, fileName='', md5='',
fileSha='' }` and the receiver renders a "0 B" file bubble.
|
||
|
|
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. |