Add consistent indent rule to ESLint and normalize formatting/indentation across multiple files. Refactor RequestUtil: improve cookie extraction, redirect handling, and HttpGetJson/HttpGetText request flow; fix cookie string builder formatting. Fix uploadGroupNoticeImage promise/error handling and network request flow. Also apply minor style/refactor tweaks in bridge web APIs, onebot handlers, message-store SQL alignment, and Proton AST collector utilities to improve readability and correctness.
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.
`vite build` was emitting a single ~606 kB chunk for the whole SPA
(Vite even printed its >500 kB warning). Every route — overview,
config (with its full edit-dialog form surface), logs, settings —
was eagerly imported in `router/index.tsx`, so users paid the full
parse + execute cost before the login redirect even resolved.
That's the most plausible cause of the "loading node config is
somewhat slow" report.
Switch each route to `lazyRouteComponent(() => import('…'), '<Name>')`
so the page chunk is fetched on demand, and wrap `<Outlet />` in
`AppLayout` with a `<Suspense>` boundary backed by a small skeleton
fallback. `defaultPreload: 'intent'` is already set on the router,
so hovering a sidebar link warms the next chunk before the click —
navigation still feels instant after the first idle moment.
Build now emits:
index.js 606 kB → 284 kB (-53%)
overview-page 15 kB
config-page 25 kB
logs-page 5.6 kB
settings-page 5.8 kB
button (shared primitives) 164 kB
+ smaller per-component chunks Vite shook out
And the >500 kB warning is gone.
Bonus: replace the per-render
`list.filter(...).map(...).includes(item.name)` O(n²) duplicate-name
check in `NetworkTabView` with a one-shot `Map<name, count>` —
matters every time the user types in the edit dialog and the tab
underneath re-renders.
The adapter edit dialog now ships a dedicated TokenField for the
access-token slot instead of reusing the plain text Field. It:
- defaults to a password-style input so the value stays out of
casual screen reads / screenshots,
- exposes an Eye / EyeOff button that flips the input type rather
than masking the string, so paste / autocomplete still behave like
a native password field,
- exposes a Copy button that pushes the current value to
navigator.clipboard, flashing a check icon for ~1.4 s on success,
and disables itself when the value is empty,
- falls back to a transient textarea + document.execCommand('copy')
for non-secure-context environments (LAN HTTP outside localhost),
routed through a local alias so the deprecation warning on the
live signature doesn't fire at the call site.
A11y checkers (axe-core / jsx-a11y) flag the boolean form because the
ARIA spec defines aria-checked as the enum "true" | "false" | "mixed",
not a boolean. React serialized the boolean to the right wire value at
runtime, but the inspection layer keys off the JSX expression's static
type. Switching to the explicit ternary silences both.
The original page stacked four expanded network sections vertically with
inline editing, so the first view drowned in every field across every
adapter at once. Restructured to a sidebar + tabs + modal layout:
- Account sidebar collapses to a 56px avatar column with name/UIN
tooltips; expanded it keeps the original 260px nickname + UIN layout.
- Right pane swaps the stacked sections for a 5-tab strip (通用设置 /
HTTP API / HTTP 客户端 / WS 服务端 / WS 客户端) with a per-tab badge
count.
- Each network tab is a list of summary cards: name + projected key
info (host:port/path, url · role, etc.) + format/report/token badges
+ inline enable toggle + edit/delete buttons. No live-typed fields.
- Create and edit both route through a single NodeEditDialog with
kind-specific field strips; cancel truly discards the draft.
- Per-kind metadata (title, summary projector, defaultEntry factory)
lives in config/defaults.ts so adding a new adapter kind only needs
one descriptor entry.
Behavior preserved end-to-end: same per-UIN config payload, same
useOneBotInstanceConfig hook, same dirty-switch confirm flow, same
save endpoint.
Even the login page leaks "there's a SnowLuma admin panel at this
IP / hostname" to anyone scanning, and right now that page is
publicly indexable. Three overlapping signals so we don't depend on
any one being honoured:
1. Hono middleware on every response sends
X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex
Hardest for compliant bots to ignore — it's a header, not
content. noimageindex also covers logo.png / avatar/* from
showing up in image search.
2. A GET /robots.txt route returns "User-agent: * / Disallow: /"
for crawlers that read it before fetching pages. Sits before
the auth middleware so no token is required (otherwise the
crawler would get 401 here, which it'd then refuse to obey).
3. <meta name="robots"> (plus googlebot / bingbot) in index.html
as a fallback for the case where a reverse proxy strips the
X-Robots-Tag header.
The console / subscriber level was captured at module load
(`const MIN_LEVEL = resolveMinLevel()`), so any change to
SNOWLUMA_LOG_LEVEL after Node started had no effect — restart was
the only way to flip levels, which makes "go to debug for 30s to
see what's happening" pretty painful.
logger.ts now keeps `currentLevel` mutable and exports
setLogLevel() / getLogLevel(). The env var still seeds it on
startup; nothing about the boot-time behavior changes. File
transport remains unaffected — it always sees debug-and-up.
WebUI gets two new endpoints, both behind the existing auth
middleware:
GET /api/logs/level → { level, levels }
POST /api/logs/level body → { level: 'debug' } updates it
Logs page picks up a "服务端级别" row of pills next to the
existing client-side filter pills, so an operator can flip the
console firehose on/off in one click without touching env vars
or restarting the bot. The label clarifies that file logging
stays at debug regardless.
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.
Two regressions slipped into the ApiClient/ConfigPage refactor:
1. api.config.save() ran the POST response through normalizeOneBotConfig
and used it to overwrite local state. But POST /api/config/:uin
returns { success, reloaded, message } — no config body — so the
normaliser produced an empty config and wiped the user's edits as
soon as the save succeeded. Honour the "save returns canonical view"
contract by following the POST with a GET inside the client; the
round-trip stays hidden behind the seam.
2. selectedUin lived inside useOneBotInstanceConfig, so navigating away
from ConfigPage unmounted the hook and lost the user's selection.
Lift selectedUin to AppInner and make the hook externally controlled
via { selectedUin, onSelectedUinChange }. Clear it on logout.
App.tsx held three copy-pasted handlers (load / unload / refresh) and
three parallel processXxxPid state slots that were mutually exclusive
but unenforced by the type system. The unload-failed alert was a
separate path, and OverviewPage needed 11 props to drive the table.
Extract useHookProcessOps: one inflight Set for dedup, one Map<pid,
OpKind> for view state, one run() that branches on kind via a
message table, and ownership of the unload-failed alert. OverviewPage
takes a single HookProcessOps prop and queries statusOf(pid).
App.tsx 270 → 216 LOC; OverviewPage props 11 → 7.
App.tsx was the single source of truth for token, fetch policy, 401
handling, and per-UIN config edit state — fetchApi was drilled into
every page and LogsPage read localStorage directly to build its SSE
URL, giving two parallel truths for the bearer token.
Promote the closure to a deep ApiClient module (domain methods +
request() escape hatch) injected via ApiProvider/useApi, and lift
selectedUin/config/saveStatus into useOneBotInstanceConfig so
ConfigPage is self-contained. normalizeOneBotConfig becomes a pure
function. ConfigPage now guards UIN switches when the editor is
dirty and refetches the server-normalised config after save.
ConfigPage prop count drops from 7 to 1; App.tsx from 421 to 270 LOC.
- WebUI sidebar/login display and OneBot get_version_info now read the
root package.json version via Vite `define`, removing hardcoded
literals in webui/src/types.ts and core/onebot/actions/info.ts.
- tools/bump-version.mjs (exposed as `pnpm bump <ver>`) syncs the
version field across the root and all five workspace packages.
- Bump everything to 1.7.8.
Normalize OneBot WS role values to canonical PascalCase ('Api'|'Event'|'Universal') across core and web UI, and support auto-migration from legacy lowercase on-disk values. Updated types, defaults, serializers/parsers, and runtime checks in ws-transport, adjusted WebUI select options and default entries, and updated the client User-Agent string. Added a test to verify that legacy lowercase role values are migrated to the new canonical form on load.
Unify per-adapter network configuration into a single `networks` structure and add migration/normalization for legacy on-disk formats. Introduce per-adapter options (messageFormat, reportSelfMessage, enabled, name) and stronger typing in types.ts. Add event-filter utilities to prebuild two serialized payload variants and pick the right one per adapter to avoid repeated JSON.stringify calls. Update HTTP/WS transports to use adapter names/signatures for hot-reload, respect per-adapter options, and improve logging; WS transport also improves client/server lifecycle/reconnect handling. Mark self-sent messages as `message_sent` in the event converter and wire instance dispatch to use the new DispatchPayload. Misc: README edits and accompanying tests added/updated for the refactor.
Remove external GPU detection (no more exec/promisify or detectGpus) and stop returning GPU list from /api/system. Instead expose the Node.js process runtime info (pid, rss, heapTotal, heapUsed, external, arrayBuffers) from the server. Update types to replace gpus with runtime and update the Overview UI: remove GPU list, show PID and formatted runtime memory stats, and adjust imports/icons accordingly. This simplifies system probing (avoids spawning platform commands) and surfaces useful process memory metrics in the WebUI.
Previous behavior: when an uninstall operation failed, the system did not provide any error feedback to the user and would simply reconnect during a polling check.
Fixed behavior: if an uninstall fails, a failure notification is now displayed in the WebUI, informing the user that the uninstall has failed.
Introduce a background pipe-watcher and per-PID operation serialization in HookManager to reliably detect and reconnect to SnowLuma pipes, reduce races, and remove the previous connect retry loop. Add new status 'connecting' and a refreshProcess API endpoint to force a probe/reconcile for a PID. Implement QqHookClient.listLivePipes() (and isClosed/probePipe updates) to enumerate live mojo pipes efficiently. Simplify client teardown logic and improve state transitions/logging. Update web UI: add a Refresh button, track inflight process ops, show connecting status and updated action messages. Update shared types to include the new 'connecting' status.
Previous behavior: after editing an unreachable reverse-ws OneBot config entry, the new config took effect, but reconnect attempts could still target the old URL.
Fixed behavior: reconnect now only applies to currently active OneBot config entries; removed or changed old entries are no longer retried.
Previous behavior: after refreshing the WebUI, OneBot config entries for the same account were already loaded and active in backend, but appeared empty in the WebUI config page.
Fixed behavior: OneBot config API response shape is now unified and backward-compatible, so persisted OneBot config entries render correctly after WebUI refresh.
Previous behavior: no-token OneBot config entries could be automatically assigned a token during load/refresh.
Fixed behavior: fixed the issue where no-token config entries were auto-populated with tokens; OneBot config entry token values now strictly follow explicit user configuration (empty stays empty).
Previous behavior: after multiple reconnect failures for a reverse-ws OneBot config entry, multiple connections could be created concurrently when the target server came back.
Fixed behavior: added per-entry connection/reconnect deduplication so each OneBot config entry maintains only one active connection and one reconnect timer.
- 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
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.