Commit Graph
47 Commits
Author SHA1 Message Date
DrSmoothl 056188d63d chore: resolve async SQLite merge conflicts
Integrate the current development branch, preserve the album comment fixes, and align the changes with repository formatting and dependency tooling.
2026-10-13 09:55:16 +08:00
手瓜一十雪 e699351f90 feat: adopt async SQLite and improve messaging and runtime diagnostics 2026-10-12 15:44:55 +08:00
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl 36c395a9b0 chore(deps): patch open npm Dependabot advisories
Raise patched floors for qs, fast-uri, hono, @hono/node-server,
postcss, ip-address, body-parser, and brace-expansion so the
lockfile no longer resolves vulnerable transitive versions.

Keep hono on the 4.12 line used by WebUI. Direct core ranges now
start at the patched releases; pnpm.overrides prevent lockfile
regeneration from rolling back.

Workspace typecheck is clean. Core 1085 tests and MCP 64 tests pass.
2026-09-03 16:48:06 +08:00
DrSmoothl 5c2f620c54 feat(webui): add optional TOTP 2FA for operator login (#351)
WebUI access was a single shared password. This adds optional
Authenticator TOTP as a second factor, with one-time recovery codes
and SSH as the last-resort disable path.

Login stays two HTTP posts with no pending token: a correct password
and enabled 2FA without a second factor returns { needsTotp: true };
the client then resubmits { password, totp } or { password, recoveryCode }.
The TOTP secret is wrapped with a password-derived AES-256-GCM key.
Eight XXXX-XXXX recovery codes are stored as SHA-256 hashes and each
code is consumed once. Enabling or disabling 2FA invalidates other
sessions; disabling still requires password plus TOTP or a recovery
code. SNOWLUMA_DEV_MODE hides the feature.

The login page uses a dedicated second-factor screen (Interior OTP
cells for the 6-digit code; the same cell UI for recovery codes,
grouped as XXXX-XXXX). Settings expose enroll/confirm/disable and
one-time recovery-code display.

Fixes #351
2026-08-13 01:33:57 +08:00
DrSmoothl 3c97e71650 fix(onebot): strengthen endpoint defaults
Use safer defaults for newly created service endpoints and guide operators toward suitable access credentials without disrupting existing deployments.

Apply the same policy at the save boundary, retain deliberate local-only setups and external client compatibility, and add regression coverage for editing, recovery, and trusted access paths.
2026-08-01 18:12:04 +08:00
DrSmoothl ea39200111 feat(webui): unify dialog feedback
Move confirmations, editors, and help surfaces onto one consistent interaction model.

Preserve accessible focus handling while making destructive and in-progress actions clearer.
2026-07-31 17:38:33 +08:00
DrSmoothl b099dbe8cd test: unify workspace test and coverage entrypoints 2026-07-31 14:06:10 +08:00
DrSmoothl 9544aa7761 chore(repo): standardize cross-platform development
Make line endings deterministic across macOS, Windows, and Linux while preserving the native checkout format required by Windows launchers.

Pin the tested application runtime and Node type baseline, add a shared startup preflight, and keep independently published packages on their existing compatibility ranges.

Align local scripts, pull request validation, complete package builds, dependency metadata, and contributor documentation around the same Node and pnpm contract. The root test command now covers every tested workspace package while a focused Core command remains available for local iteration.
2026-07-31 13:09:02 +08:00
DrSmoothl 87bab6666b fix(webui): preserve dashboard widget settings
Keep per-widget configuration when persisted overview layouts are migrated against the current widget catalogue. Account shortcuts, notes, links, filters, and other configurable widgets now retain their saved values after a page reload.

Add a WebUI Vitest entry point and regression coverage for restoring an account widget with saved configuration.

Fixes #243.
2026-07-19 15:26:22 +08:00
DrSmoothl c15625dbd8 feat(webui): add spring-based range sliders
Replace native range inputs with an accessible Radix-backed control that preserves keyboard and assistive-technology semantics.

Reproduce the elastic handle, track stretch, tick visibility, and reduced-motion behavior while keeping SnowLuma theme tokens. Adopt the component for interface scale and background image controls.
2026-07-18 23:38:00 +08:00
DrSmoothl 0503af2fcc fix(webui): export logs without terminal control codes
Keep terminal styling confined to interactive console output while storing plain structured log lines for subscribers and file persistence.

Sanitize downloads again at the WebUI boundary so cached legacy entries and fallback-formatted records cannot leak CSI, OSC, DCS, or C1 control sequences. Add regression coverage for colored output, hyperlinks, control strings, malformed sequences, and preserved multiline text.
2026-07-16 19:20:48 +08:00
DrSmoothl 4802dfe7aa perf(webui): virtualize the log viewer row list (#185)
The logs page rendered every filtered row, so a full 2000-line view mounted
~10k DOM nodes and cost ~66ms of layout per commit. Switch to a virtualized
list (@tanstack/react-virtual) that mounts only the visible window plus a small
overscan — a few dozen nodes regardless of how many lines the view holds.

Rows are dynamically measured (measureElement) so wrap mode's variable-height
lines need no fixed row-height assumption; the size cache is keyed by row id so
the maxLines front-drop doesn't mis-attribute heights across the index shift.
The column header moved just outside the scroll viewport so the virtual list is
the sole scroll content (no sticky-offset math). Auto-scroll now follows the
tail via scrollToIndex with a next-frame re-pin (freshly-appended rows carry the
size estimate until measured, which otherwise left wrap-mode tails short), and
re-runs on filter/level/wrap/maxLines changes, not just new logs. ScrollArea
gained an optional viewportRef so the virtualizer can drive off the real Radix
scroll element. The per-row entrance fade is dropped (rows mount/unmount on
scroll); divide-y became a per-row border.
2026-07-07 22:07:02 +08:00
DrSmoothl 1f2bf4d3f2 chore(deps): patch low-severity esbuild + @babel/core advisories
两个构建/测试期工具链的 transitive 依赖存在低危「任意文件读」告警(非运行时产物):
- esbuild 0.27.7 → 0.28.1(dev server 任意文件读,vite 接受 ^0.28.0,兼容)
- @babel/core 7.29.0 → 7.29.7(sourceMappingURL 任意文件读)

pnpm.overrides 钉住修复版下限(esbuild@<0.28.1 / @babel/core@<7.29.6),防止后续
lockfile 重生回退。全 12 包 typecheck 干净、webui vite build 通过、protocol 572 测试
全绿,工具链未受影响。
2026-07-02 15:16:40 +08:00
DrSmoothl cf7de9f7bd chore: update vite version to ^8.0.16 in pnpm-workspace.yaml 2026-06-26 19:59:58 +08:00
DrSmoothl 77b16faebb feat(common): extend RuntimeConfig for WebUI listener settings
Add webuiHost / webuiTls.enabled / trustProxy to RuntimeConfig as the shared
system-settings model for WebUI Wave A1 (HTTPS + self-config panel). Refactor
load into pure, tested normalizeRuntimeConfig + resolveRuntimeEnvOverrides
(env wins, in-memory only; SNOWLUMA_WEBUI_HOST added alongside the existing
PORT/TRUST_PROXY overrides). loadRuntimeConfig backfills older runtime.json
with the new defaults. Adds vitest to @snowluma/common (was typecheck-only).
2026-06-18 14:19:45 +08:00
DrSmoothl f07f309c51 feat(webui): free-form draggable/resizable dashboard grid (phase 4)
The overview becomes a gridstack-backed free grid — drag, resize, and show/
hide 9 widgets (the 5 top stat tiles split out of the old single block, plus
connections/alerts/host/sessions), persisted to config/ui.json's
layout.overviewBlocks as {id,x,y,w,h,visible}.

- gridstack (zero-dep, vanilla; chosen over react-grid-layout whose
  react-draggable uses the React-19-removed findDOMNode). React renders each
  widget into a gridstack item via a portal (context flows through); gridstack
  owns layout/drag/resize. Per-widget minW/minH prevent content collapse.
- Desktop = editable in place; narrow screens = auto single-column, read-only.
  Fixed cellHeight => no ResizeObserver => the narrow column-clamp never
  persists over the desktop coords.
- Edit mode shows a panel: widget show/hide + nav reorder (the phase-3 motion
  list); the grid below is live-editable. Reset + empty-state preserved.
- Server UiLayoutItem gains optional x/y/w/h (clamped); the stats->5-tiles
  migration + default placement live client-side (lib/dashboard-layout.ts).

Verified across 2 adversarial review rounds (gridstack+React lifecycle traced
against the library source: StrictMode-safe, no mount-persist loop, no
re-init on coord change, no narrow-visit data loss).
2026-06-13 21:17:23 +08:00
dependabot[bot]andsnowluma-bot[bot] d804a3b2e0 build(deps-dev): bump hono from 4.12.18 to 4.12.21 (#91)
Bumps [hono](https://github.com/honojs/hono) from 4.12.18 to 4.12.21.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.18...v4.12.21)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: snowluma-bot[bot] <283088347+snowluma-bot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 17:04:41 +08:00
DrSmoothl 69c8960ea9 feat(mcp): @snowluma/mcp — read-only MCP server for the OneBot action catalog
A new published package that exposes every OneBot action's docs + a ready-to-use
JSON Schema to LLM clients over stdio. Tools: list_actions / get_action /
search_actions / list_categories, plus the whole catalog as a resource.

The catalog is a build-time snapshot generated from @snowluma/onebot's live
action specs (collectActionDocs()) — so it auto-tracks action add/remove and the
published package carries zero runtime dependency on the private onebot package.

action-kit enrichment (the data source):
- Field.toJsonSchema() per coercer (uint→integer/minimum:1, messageId→integer/
  not-const-0, int→min/max, string→minLength/maxLength, array→items, enum→enum).
- describe() composes a per-action inputSchema; ActionDoc gains inputSchema +
  category. action-docs.ts tags each doc with its domain category.

Wiring: onebot exports ./action-kit + ./action-docs; root build:all includes mcp;
mcp-npm-publish.yml mirrors the sdk/ui publish workflows (triggers on changes
under packages/mcp/** or packages/onebot/**, since the catalog derives from it).

onebot typecheck + 318 tests green; mcp typecheck/build/smoke green; the 142
registered OneBot action names are unchanged.
2026-06-02 22:45:30 +08:00
手瓜一十雪 f2b7ef0056 Switch from @snowluma/sqlite to node:sqlite (sync)
Remove the in-tree @snowluma/sqlite wrapper and vendored better-sqlite3 prebuilts; migrate consumers to node:sqlite's synchronous API. Replaced imports and types (Database/Statement -> DatabaseSync/StatementSync), updated PRAGMA calls to use exec, and adjusted code in onebot (media/message/reaction stores) and protocol (identity service). Vite config no longer bundles sqlite native binaries and references to the sqlite workspace were removed. Also delete the packages/sqlite package and its native prebuild artifacts, and add a couple of unrelated AST improvements in proton: use a module-level shared wrapper cache and add an inheritance cache to avoid repeated scans. Minor import reordering/cleanup applied where needed.
2026-05-27 20:02:40 +08:00
DrSmoothl 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).
2026-05-23 23:40:57 +08:00
DrSmoothl c16064d7e3 refactor(packages): rename @snowluma/bridge → @snowluma/protocol
The package previously named "bridge" houses the protocol-byte layer
(OIDB cmds, msg-push decoders, highway uploaders, element-builder,
event bus). The name "bridge" suggested transport/lifecycle, which it
isn't — that role lives in core/hook and core/bridge/manager. Rename
to match what's actually inside.

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

Rename detection threshold: git log --find-renames=20% recommended
when following history across this commit — content-replacement
(@snowluma/bridge → @snowluma/protocol) dropped per-file similarity
below git's default 50% on import-heavy files.
2026-05-23 23:31:03 +08:00
DrSmoothl 99772851a7 chore(deps): add vitest coverage packages to devDependencies 2026-05-23 00:43:27 +08:00
DrSmoothl e088167476 fix(lockfile): rebase against origin/main to unblock PR #63 auto-merge
The previous commit (ee0f159) regenerated the lockfile starting from
dev's pre-fix state, which diverged structurally from main's lockfile
(main carried dependabot's hono 4.12.18 bump that dev never inherited).
GitHub's auto-merger saw the two lockfiles as touching overlapping
hunks and marked PR #63 CONFLICTING, blocking the bot's auto-merge
flow into main.

Re-seed the lockfile from origin/main (`git show origin/main:pnpm-lock.yaml`)
then run `pnpm install` again — this time the regeneration starts from
main's already-bumped state, so the only delta is the 6 lines removed
from `packages/onebot`'s importer entry (no snapshot churn). PR #63
becomes a clean fast-forward for the bot to merge.

No code or package.json change here; this is purely a lockfile rebase.
Verified: `pnpm install --frozen-lockfile` passes locally against the
new lockfile.
2026-05-21 17:50:15 +08:00
DrSmoothl 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 cb06624), this
left two distinct peer-dep resolutions of `@hono/node-server` in the
lockfile (`(hono@4.12.14)` for onebot, `(hono@4.12.18)` for core).
On the dev→main merge the auto-resolver kept main's snapshot list,
dropping the `(hono@4.12.14)` variant onebot still referenced — so
v1.9.1 shipped with an inconsistent lockfile and CI's
`pnpm install --frozen-lockfile` blew up with
`ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY: no entry for
'@hono/node-server@1.19.14(hono@4.12.14)'`.

Fix:
- Remove `hono` + `@hono/node-server` from onebot (verified zero source
  imports — only matches were `honour` / `WebHonorType` false hits).
- Bump core's `hono` pin to `^4.12.18` so dev matches what dependabot
  already landed on main (otherwise the drift comes back on next merge).
- Regenerate pnpm-lock.yaml — now resolves to a single
  `@hono/node-server@1.19.14(hono@4.12.21)` snapshot.
2026-05-21 17:43:10 +08:00
DrSmoothl 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.
2026-05-21 13:49:19 +08:00
DrSmoothl b69d8ac83c build(ci): refresh pnpm-lock.yaml for runtime better-sqlite3 dep
`pnpm install --frozen-lockfile` in CI rejected the lockfile after
998eba2 added `better-sqlite3` to packages/runtime/package.json
without bumping the lock. Running `pnpm install` regenerates the
`packages/runtime: { dependencies: { better-sqlite3: ... } }` block
plus the resolved version pin. No code change — purely the lockfile
catching up to the manifest.
2026-05-21 10:45:56 +08:00
DrSmoothl 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.
2026-05-21 10:34:37 +08:00
DrSmoothl 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.
2026-05-21 10:26:29 +08:00
DrSmoothl 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).
2026-05-21 08:52:00 +08:00
DrSmoothl f7a03ca05a refactor(bridge): extract protocol layer into @snowluma/bridge package
Phase 2 of the 4-package split (RoadMap #3). Moves ~35 files —
events / event-bus / identity-service / packet-pipeline / element-
builder / bridge-oidb / qq-info / msg-push/* / highway/* / web/* +
the bridge-side formatters — out of `packages/core/src/bridge/`
into the new `@snowluma/bridge` workspace package. Everything that
translates QQ wire bytes ↔ typed events/elements now lives there.

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

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

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

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

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

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

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

Phase 3 (next): OneBot + WebUI move into @snowluma/onebot. The
runnable `index.ts` stays in @snowluma/core as the application
entry, importing `OneBotManager` from `@snowluma/onebot`.
2026-05-21 08:41:34 +08:00
DrSmoothl 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.
2026-05-21 08:27:05 +08:00
DrSmoothl 6c7e56d5a1 refactor(proto-defs): extract wire-protobuf definitions into standalone package
Move `packages/core/src/bridge/proto/proton/*.ts` (13 files, ~3.3k
LOC) into a new `@snowluma/proto-defs` workspace package. The proton
spike landed in the previous commit means bare-specifier imports
(`@snowluma/proto-defs/oidb`, `…/message`, …) now resolve correctly
through proton's vite plugin, so call-site replacement still fires.

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

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

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

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

Note: proton's `dist/index.js` MUST be rebuilt for the vite plugin to
pick up the bare-specifier resolution. The previous commit's source
change is only effective after `pnpm -F @snowluma/proton build`.
2026-05-20 21:31:30 +08:00
手瓜一十雪 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.
2026-05-20 19:08:45 +08:00
DrSmoothl 3d739c255a feat(proton): proof-of-concept migration — longmsg parity test
Migrates a self-contained subset of bridge/proto/longmsg.ts to the new
@snowluma/proton compile-time DSL form and verifies wire-format parity
against the legacy runtime ProtoSchema encoder. End-to-end proof that
the toolchain works in core's vitest pipeline.

What's covered:
  - LongMsgUid / LongMsgSettings / SendLongMsgInfo / SendLongMsgReq /
    SendLongMsgResp / SendLongMsgRespResult / RecvLongMsgInfo /
    RecvLongMsgReq / RecvLongMsgResp / RecvLongMsgRespResult
  - The three schemas that pull in PushMsgBody (LongMsgContent /
    LongMsgAction / LongMsgResult) stay on the legacy API for now, since
    migrating them requires also migrating message.ts and element.ts.

Toolchain changes:
  - @snowluma/proton gets a tsup build step. The ./vite subpath now
    points at the built dist/index.js so it is loadable as plain Node
    ESM from vite.config.ts / vitest.config.ts (the source-only .ts
    layout can't be consumed from outside the Vite transform pipeline).
    The . (runtime) subpath stays at TS source — it's only ever
    imported from user code, which IS Vite-transformed.
  - prepare script ensures dist/ exists after pnpm install.
  - @snowluma/core depends on @snowluma/proton (workspace:*) and adds
    protobufVitePlugin() to its vitest config so test files get
    transformed.

Documented divergence:
  proton follows proto3 semantics: fields equal to their default value
  (0 / false / "" / empty bytes) are NOT emitted on the wire. The legacy
  runtime encoder always emits them. Both forms are round-trippable
  through a conforming proto3 reader. The parity test pins this
  behaviour explicitly.

8/8 parity tests pass.
2026-05-19 08:19:13 +08:00
DrSmoothl 194165f49d feat(proton): add @snowluma/proton — compile-time protobuf DSL
Vendors SnowLuma/protobuf-dsl into the monorepo as @snowluma/proton, with
edge-case fixes applied on top of the upstream source. Reference copy of
the unmodified upstream is kept under dev/protobuf-dsl/.

Edge cases fixed (each with a regression test):
  - Aliased pb / pb_repeated marker imports (`import { pb as P }`)
    — collector now resolves the marker name through the import resolver
  - Concrete interface with a generic-instantiated message field
    (`wrapped: pb<5, Wrapper<uint_32>>`) — field typeName now resolves
    to the mangled form, and the inner instantiation is queued for
    monomorphization
  - Generic template with a generic-instantiated field
    (`interface Outer<U> { x: pb<5, Wrapper<U>> }`) — the inner
    instantiation is now re-substituted and re-monomorphized at outer
    instantiation time
  - Wrapper-binding synthetic SourceFile was empty-text — getText()
    returned garbage, breaking mangled name computation downstream
  - Cross-file forwarded wrappers (chain → imported base) were not
    detected — parseFileForDefinitions now resolves imports recursively
    (value imports only, with cycle protection)
  - matchForwardedKnownWrapper did not propagate typePattern — chain
    treated <X> as the encoded type instead of Wrapper<X>
  - Synthetic-SF WeakMap was private to analyzer.ts — replacer could
    not see it. Moved to utils.ts so typeNodeToMangledName picks it up
    automatically.

Defensive guard: any field whose typeName resolves to neither a primitive
nor a known message now throws at analyze() time, instead of silently
leaking the WireType.Varint placeholder into codegen.

95/95 tests pass; tsc --noEmit clean.
2026-05-19 08:19:13 +08:00
手瓜一十雪 df691c4e8f Revert "feat: Add @snowluma/protobuf package"
This reverts commit 9f9db8d16f.
2026-05-18 20:43:32 +08:00
手瓜一十雪 9f9db8d16f feat: Add @snowluma/protobuf package
Introduce a new @snowluma/protobuf package (v0.0.4) that provides a lightweight protobuf-ts wrapper (NapProto/NapProtoMsg) with type helpers, ProtoField factory, and encode/decode utilities. Adds package.json, tsconfig.json, and the implementation at src/NapProto.ts. pnpm-lock.yaml was updated to include the @protobuf-ts/runtime dependency resolution.
2026-05-18 19:31:42 +08:00
DrSmoothl 3e943e8f0d feat(ui): refresh theme tokens + add Popover/StatusDot/EmptyState/Kbd
Visual overhaul to give @snowluma/ui a coherent, accessible identity.

Tokens (styles/theme.css)
- Deeper dark-mode surface ramp with more contrast between background /
  card / popover so dropdowns and dialogs read clearly in dark mode
- New radius scale (xs → 3xl) anchored at 0.625rem for tighter feel
- Layered shadow tokens (xs/sm/md/lg/xl) that adapt to mode
- Semantic success / warning / info / destructive colour + foreground
  pairs replacing ad-hoc lookups
- `--chrome-height` (52px) for consistent top-bar / sidebar header height
- Utilities: `hover-lift`, `surface-glass`, `surface-gloss`; consumer-
  scoped scrollbar styling so native scrollbars match
- Note: consumers must also `@import "tw-animate-css";`

Components
- Select: solid bg-popover + ring-1 ring-foreground/5; rotating chevron
  on open state; rounded-xl menu with shadow-xl (fixes the transparent
  dropdown bug)
- Dialog / AlertDialog: bg-popover + ring; backdrop-blur on overlay;
  slide-in motion
- Tooltip: ring + zoom-in motion, max-w-xs for long labels
- Button: explicit transition list with active:translate-y-px; success /
  warning variants; icon-xs size
- Input: hover border + focus ring + aria-invalid handling
- Card: tighter line-height + typography
- Badge: variants for success / warning / info / soft, size scale
- ScrollArea: hover-only mode by default, slightly thicker thumb

New components
- Popover (radix wrapper) — for menus / inline overlays
- StatusDot — semantic colored dot (replaces ad-hoc 🟢/🟡/🔴 emoji)
- EmptyState — uniform placeholder for "nothing here yet" surfaces
- Kbd — inline keyboard-key label
2026-05-17 13:56:11 +08:00
DrSmoothl a8661ea216 feat(ui): add @snowluma/ui shared UI library
Lifts the shadcn primitives + Tailwind 4 theme tokens from webui into a
dedicated workspace package consumable by both webui and SnowLumaDesktop.

- 14 primitives (Button, Dialog, AlertDialog, Input, Label, Checkbox,
  Select, Switch, Tabs, Card, Avatar, Badge, Progress, ScrollArea,
  Separator, Sheet, Skeleton, Tooltip) + sonner Toaster wrapper
- Tailwind 4 theme tokens exported via styles/theme.css
- tsup ESM build with declaration files
- publishConfig.access=public mirroring @snowluma/sdk

Consumers @import '@snowluma/ui/styles/theme.css' for tokens and add an
@source directive to scan the package for utility classes.
2026-05-17 00:23:37 +08:00
DrSmoothl a1a49944df refactor(webui): migrate routing to TanStack Router
The four-page switch lived as a string union (Page) plus an if/else
chain in AppInner, with TopBar reverse-importing Sidebar's NAV_ITEMS
to find the icon for the active tab. Adding a page touched four
places and there was no URL, no history, no deep-linking.

Move routing behind @tanstack/react-router with a layout route
(AppLayout) that owns the polling lists, processOps, selectedUin
and the unload-failed alert; child routes render through <Outlet/>
and read shared state from a new AppStateContext. AuthBoundary still
gates login / forced-password-change above the router. Sidebar uses
<Link> + useRouterState for active state; TopBar and MainLayout look
up the current path the same way.

Behaviour-preserving:
- LoginPage / ChangePasswordPage still mount above the router.
- AppLayout never unmounts during navigation, so selectedUin,
  processOps state, the polling state and the unload-failed dialog
  all survive page switches just like before.
- Logout calls history.replaceState('/') so the next login lands on
  the overview page, matching pre-router behaviour.
- The sidebar-active-pill layoutId animation and the page-transition
  AnimatePresence are preserved, keyed off pathname instead of the
  removed `active` prop.

New URLs (/, /config, /logs, /settings) work on refresh because the
backend already had SPA fallback in core/src/webui/server.ts. Bundle
grows ~84KB (gzip +27KB) for the router runtime.
2026-05-16 02:35:49 +08:00
DrSmoothl c343021318 feat: add SnowLuma TypeScript SDK 2026-05-10 01:26:35 +08:00
手瓜一十雪 19a564991c Add WebUI dev mode auth & dev script
Introduce a development-only WebUI auth mode controlled by SNOWLUMA_DEV_MODE. When enabled, a fixed in-memory password ("snowluma-dev") is used, config/webui.json is not read or written, and password changes are disabled. Expose WebuiAuth.isDevMode() and WebuiAuth.devPassword, add startup logs when dev mode is active, and wire up dev:mode npm scripts (root and packages/core) using cross-env to set the env flags.
2026-05-07 14:36:12 +08:00
DrSmoothl 1426d7adb2 feat: add settings page and theme toggle component
- Implemented a new SettingsPage component for user preferences including appearance, behavior, and account security settings.
- Added a ThemeToggle component to switch between light, dark, and system modes.
- Created reusable UI components: AlertDialog, Avatar, Badge, Button, Card, Dialog, Input, Label, Progress, ScrollArea, Separator, Sheet, Skeleton, Tooltip.
- Introduced utility functions for class name merging and formatting bytes/uptime.
- Added a custom hook for media query handling
2026-05-05 00:39:52 +08:00
手瓜一十雪 252a65b833 Add @snowluma/websocket TS package and integrate
Introduce an in-tree TypeScript WebSocket package (@snowluma/websocket): move JS websocket implementation into packages/websocket/src (client, server, websocket, extensions, native, index) and add package.json + tsconfig. Replace direct 'ws' dependency in core with the workspace package and update runtime manifest/description. Implement a native addon loader for platform-specific binaries, add typings/refinements across client/server/websocket (more strict types, handshake parsing, permessage-deflate plumbing, buffer/raw data handling), and wire WebSocketClient as the package default export while exposing internal types. Update vite config to bundle the new package and copy only platform-target native binaries into dist/native. Remove legacy JS entrypoints and native loader, and adjust imports/usages in core (ws-transport) to accept broader raw data types.
2026-05-03 15:49:42 +08:00
手瓜一十雪 39f1672b29 Migrate project to pnpm monorepo
Restructure the repository into a pnpm workspace under packages/ (core, webui, runtime). Move source files for core and webui into packages/*, add package manifests for @snowluma/core and @snowluma/runtime, and remove the old launcher package.json. Update root package.json to be a monorepo controller (scripts delegate to pnpm filters, set packageManager and Node engine). Configure build outputs to emit into the repo-root dist/ (vite config changes) so the existing release pipeline continues to work. Add .npmrc with mirrors, expand .gitignore for monorepo layout, add tsconfig base and package-level tsconfigs, and add vitest config for the core package. Adjust injector native search paths and update tests import paths to the new package layout. Small CI/workflow comment added to clarify build behavior and README updated with monorepo structure and common commands.
2026-05-03 14:02:29 +08:00
手瓜一十雪 b3ffbae005 Initial commit 2026-05-02 19:06:29 +08:00