Commit Graph
1191 Commits
Author SHA1 Message Date
DrSmoothl de598242ab feat(webui): apply Apple-style polish to processes + config pages
Carry the logs/settings visual language across the remaining pages:

- Processes: sort control becomes a segmented radiogroup; action banner
  gains a spinner; cards round to xl with a hover tint; calmer empty state.
- Config: save status collapses from bordered pills into one inline
  dot/spinner indicator; node summary rows round to xl with hover and
  fold the three flag badges into a single muted "数组 · 不上报自身 · 已设
  Token" line; empty states round to xl; account rail rounds to xl.

Presentational only — all state wiring and handlers unchanged.
2026-06-13 23:25:11 +08:00
DrSmoothl 3d480fb018 feat(webui): redesign settings page with macOS-style two-pane UX
Rework the settings page around the macOS System Settings layout, reusing
existing UI tokens (no new deps):

- Two-pane: a sticky left nav rail (w-52) on lg, a wrapping tab strip on
  mobile. Active section mirrors the app sidebar's selected style.
- Grouped inset lists: each Card is a header above a hairline-divided list
  of label-left / control-right rows (SettingRow: auto / inline / stack),
  replacing the stacked Field + Separator spam.
- Refined segmented control: recessed track with a raised pill on the
  selected segment (font-semibold + full ring so it survives high-contrast
  / OLED); exposed as a proper radiogroup (role=radio + aria-checked) and
  named via the row label through a small context + useId.
- Per-section fade transition; section heading anchors the detail pane.

Wide controls (时制, 深色强度, fonts, radius scale, poll interval, default
route) stack so they don't crowd their labels; password-saved confirmation
keeps its success color. All settings preserved 1:1.
2026-06-13 23:15:38 +08:00
DrSmoothl 5b253ea1d5 feat(webui): redesign logs page with Apple-style UX
Rework the run-log viewer around progressive disclosure and a calmer,
content-first hierarchy, reusing existing UI tokens (no new deps):

- Toolbar: live status dot (red when disconnected) replaces the badge;
  real search field with inline icon + clear; icon-only actions with
  tooltips, grouped (toggles in a track · divider · data actions).
- One always-visible segmented level filter (iOS-style raised pill +
  colored dot) instead of strikethrough pills.
- Collapse the server-level switch + highlight-rule editor into a single
  animated "选项" panel; server level also surfaced in the subtitle. The
  control area drops from three rows to one.
- Log stream: subtle sticky column header (时间/级别/模块/消息) aligned to
  rows via a sm:contents trick, dot-style level indicators, hairline row
  separators, and distinct empty / no-match states.

a11y: radiogroup on color swatches, aria-pressed on level toggles,
unique remove-button labels, focus rings on all controls.
2026-06-13 23:00:22 +08:00
DrSmoothl a61e0a253d feat(webui): in-place dashboard edit overlays + inline sidebar reorder
Rework the layout edit mode from a separate panel to in-place affordances:
- Each dashboard widget gets a semi-transparent floating overlay in edit mode:
  a "拖动" grip bar (the whole card is the drag target — forgiving +
  discoverable), an eye to show/hide, and a gear that opens the per-widget
  settings in a dialog. Hidden widgets show as re-enableable "已隐藏" ghosts;
  the bottom-right resize grip is now clearly visible.
- The sidebar nav becomes a drag-to-reorder list directly in the sidebar
  (show/hide + locked pinned items + a 完成 button); editing force-expands the
  sidebar (and disables the collapse toggle). Edit mode is a shared
  LayoutContext flag so one "编辑布局" toggle drives both the grid and the nav.
- The separate editor panel (overview-layout-editor) is removed; the gear
  opens config in a dialog instead.

Also: stat-tile typography tightened (leading-tight, smaller value, overflow
clip) so text no longer hugs the edge on small/short tiles. Verified by an
adversarial review round (no MUST-FIX).
2026-06-13 21:45:41 +08:00
DrSmoothl 056fff415c feat(webui): per-page preferences (phase 7)
A `pages` section in config/ui.json (3rd section-merge alongside appearance +
layout):
- defaultRoute: the page to open after login (chosen in Settings → 数据与格式;
  DefaultRouteRedirect navigates there once, only from '/', respecting
  deep-links + later navigation).
- logs: persisted visible levels, max lines, auto-scroll, line-wrap, and
  keyword→colour highlight rules — edited live on the logs page; maxLines is
  read via a ref so it never re-subscribes the SSE.
- processes: pid / name / status sort (persisted, default from prefs).
- config: remembers the last-used tab.

"重置全部" now also clears pages. The server normalizer is total and caps the
logs fields (levels subset, maxLines 100-5000, <=20 highlight rules, trimmed
keywords). Verified by an adversarial review round (normalizePages total
against 30 adversarial inputs; zero MUST/SHOULD-FIX).
2026-06-13 21:17:24 +08:00
DrSmoothl 7c2923dd04 feat(webui): custom CSS + config export/import/reset (phase 6)
A "高级" Settings tab adds:
- Custom CSS (appearance.customCss, <=50KB) applied post-auth only, as the
  last <style> so it overrides everything. It never reaches the login page:
  the server strips it from /api/ui/public, applyAppearance gates injection on
  a bearer token (so the localStorage cache can't leak it pre-auth), and the
  login page clears any lingering <style> on mount. `?safe-mode=1` (sticky for
  the tab session) disables custom CSS so a broken rule can't lock the
  operator out.
- Export / import the whole UI config as JSON (import is normalized
  server-side, then reload); reset-all behind a confirm dialog.

POST /api/ui and the import file are size-capped (256KB) on top of the
per-field caps. Verified across 2 adversarial review rounds — the pre-auth
cached-CSS leak/lockout vector found in round 1 is closed across the boot /
logout / 401-expiry / re-login paths.
2026-06-13 21:17:23 +08:00
DrSmoothl 3258300539 feat(webui): per-widget config — alerts levels/count, sessions sort/filter (phase 5)
Overview widgets can carry an opaque `config` (stored per grid item). v1:
- alerts: how many entries + which log levels (was fixed at 5 warn/error);
  the alert row colour now uses the full 6-level palette instead of a
  warn/error binary.
- sessions: sort (recent / uin / nickname) + a nickname/uin filter.

A gear in the layout editor opens an inline config form per configurable
widget; edits apply live (no grid re-init — config isn't in the grid key) and
persist via the section-merge save. Config is parsed client-side with total,
clamping parsers (count 1-50, known levels only, filter <=100 chars); the
server stores it opaquely within a 4KB bound. Widgets with no config render
exactly as before. Verified across 2 adversarial review rounds.
2026-06-13 21:17:23 +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
DrSmoothl a5335687ec fix(protocol): fetch group-join verify comment regardless of uin cache (#98)
The verify text a user types when requesting to join a group is never in the
push — it lives on the OIDB pending-request queue. The fetch
(resolveGroupJoinRequest) was gated behind the UID→UIN stranger-resolve, so
when the requester's uin was already cached the whole async path (and thus the
comment fetch) was skipped, leaving the OneBot request.group `comment` empty.

Decouple the two: every uid-bearing group_invite now goes through the async
enrich path (needsGroupInviteEnrich → dispatchGroupInvite), which ALWAYS
fetches the comment and resolves the UIN profile only when needed (the
fromUin === groupId cache-pollution self-heal is preserved). Matches
Lagrange's unconditional FetchGroupRequests and NapCat's notify.postscript.

Rewrites the cache-hit test (which had pinned the buggy behavior) into a
regression asserting the comment is fetched + applied on a cache hit.
346/346 protocol tests pass. Verified by adversarial review.
2026-06-13 12:03:04 +08:00
DrSmoothl 69ed8fc722 feat(webui): customizable dashboard + nav layout (phase 3)
The "C" half — reorder + show/hide the overview cards and the sidebar nav,
persisted to config/ui.json's layout section.

- LayoutContext loads the layout (authed GET /api/ui) and persists edits via
  the section-merge POST /api/ui {layout} (debounced). reconcileLayoutItems
  keeps stored order/visibility, appends new catalogue entries (forward-compat),
  and forces pinned items visible.
- Overview is refactored into a block registry (stats/connections/alerts/host/
  sessions). A 「编辑布局」 mode (motion Reorder — zero new deps) edits both the
  cards and the nav in place, with per-item show/hide and a reset; an all-hidden
  dashboard shows an empty state with 恢复默认.
- Anti-self-lock: PINNED_NAV = ['/', '/settings'] can be reordered but never
  hidden — the overview hosts the layout editor and settings hosts
  account/appearance, so the user can never strand themselves.
- api.ui.save now takes Partial<UiConfig> (the section-merge contract).

A late initial GET can't clobber an in-flight edit (dirtyRef); a pending edit
flushes on unmount. Verified across 2 adversarial review rounds.
2026-06-13 01:15:00 +08:00
DrSmoothl ac1ffc46d3 feat(webui): appearance customization — theme/accent/background/fonts/a11y (phase 2)
Wires the Phase 1 store into a full appearance layer.

ThemeContext is rewritten to load/save UiAppearance from the server (with a
localStorage anti-flash cache + a one-time migration of the legacy per-browser
theme keys), apply ~12 dimensions to <html>, and theme the login page pre-auth.

New Settings controls (外观 / 数据与格式): free-hex accent (+ presets + apply
scope), dark intensity (soft / OLED black), sidebar style, background
(solid / gradient / uploaded image with opacity + blur), font presets, UI
scale, reduce-motion, high-contrast, default-collapsed sidebar, 12h/24h time
format (wired into logs + overview timestamps).

- index.html gains a pre-paint bootstrap script that applies the cached theme
  before the bundle mounts, eliminating the dark/OLED/scale FOUC.
- server saveUiConfig is now a section-level merge so appearance-only and
  layout-only saves never clobber each other.
- reduce-motion uses Framer MotionConfig; the background renders on a fixed
  layer revealed by transparent app roots; custom-accent hex and ui-scale are
  validated/clamped before use (localStorage-tamper hardening).

Verified across 2 adversarial review rounds (FOUC + CSS-injection + impure
state-updater + ref-staleness all fixed).
2026-06-13 01:15:00 +08:00
DrSmoothl 0b5087b410 feat(webui): UI customization config store + endpoints (phase 1)
Backend foundation for the WebUI appearance + layout customization feature.

- core/src/webui/ui-config.ts: config/ui.json store with TOTAL normalization
  (any input -> safe config), atomic write, lazy memoized cache, and the
  background-image lifecycle (magic-number sniff for PNG/JPEG/WebP).
- server.ts: GET/POST /api/ui (bearer), GET /api/ui/public (unauth cosmetic
  subset for pre-login theming), POST/DELETE /api/ui/background (upload with
  Content-Length pre-check + 5MB cap + magic-number validation), and unauth
  GET /ui-asset/background (serves the uploaded bytes with sniffed mime +
  nosniff). SPA fallback excludes /ui-asset/.
- webui: `ui` API-client namespace + mirror types (no UI wiring yet).

Server-managed background fields (hasImage/imageMime/imageVersion) are taken
from the trusted source per path — the file on load, the on-disk config on
save — so a client save can never forge them, and a wallpaper survives a
restart. Verified across 3 adversarial review rounds.
2026-06-13 01:15:00 +08:00
DrSmoothl 043ce779e6 fix(onebot): ignore close events from stale ws-client sockets (#97)
A hot reload that changes the binding signature (url / role / accessToken
/ reconnectIntervalMs) drives `await this.close()` then `await this.open()`
back-to-back. `WsClientAdapter.close()` is synchronous and `WebSocket.close()`
just schedules the close frame, so the OLD socket's `'close'` event fires
AFTER the new socket has already been assigned to `this.socket`.

The close handler did not check the event's origin, so it would null out
`this.socket`, drop `connected`, and schedule an unwanted reconnect against
the freshly-opened connection. Against a single-connection backend that
kicks duplicates this snowballed into a reconnect storm where the bot and
the backend repeatedly kicked each other off; against a permissive backend
it still left the new connection in a degraded state (events couldn't be
sent until the bogus reconnect rebuilt the socket).

Add a one-line identity guard at the top of the close handler — when the
event arrives from a socket that is no longer the current one, do nothing.
The explicitlyClosed / isEnabled checks are kept as the second line of
defence for the normal (current-socket) close path.

Add three regression tests under tests/ws-client-stale-close.test.ts that
mock `@snowluma/websocket` (same approach as adapter-reload-state-machine.
test.ts, which avoids the native addon dependency): the reload race
scenario from the issue, a real-current-socket close that MUST still drive
reconnect, and a close() race against an in-flight close event.
2026-06-12 21:55:32 +08:00
DrSmoothl e9ae2ec751 feat(log): show ARK card title in JSON segment preview (#96)
The JSON segment preview was a fixed "[JSON]" placeholder, hiding both
real card titles and QQ's server-side fallback prompts (e.g. cardId
-896954183 "当前QQ版本不支持此应用,请升级", which is what #96 reported
seeing). Best-effort extract a title from the common ARK fields —
meta.{detail_1,detail,news,mannounce,music,video}.title, single-section
meta, then root.prompt / root.desc — and render as "[卡片:...]"; fall
back to "[JSON]" on parse failure or an unrecognised schema. Title is
truncated at 30 chars so a long prompt cannot blow up the log line.

OneBot segment shape is unchanged: downstream consumers still get the
full payload at data.data; this is purely a log-preview readability fix.
The helper is pure best-effort and never throws — every failure path
falls through to the existing "[JSON]" output.
2026-06-11 20:47:54 +08:00
DrSmoothl a3a36718d6 chore(release): v1.9.9 2026-06-11 16:42:23 +08:00
游手好闲的言柒 d2a67cb560 fix(onebot): add canonical fields to file segment and fix log rendering (#94)
* fix(onebot): support inline file url/path in send_group/private_msg

* test(onebot): update file segment parser test for inline url/path support

* fix(onebot): add canonical fields to file segment and fix log rendering
2026-06-11 15:40:47 +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 14e02bce3c fix(events): keep member role cache in sync on group admin change
get_group_member_info serves straight from the in-memory member cache
on its default no_cache=false path — a deliberate choice, since clients
like OlivaDice/MaiBot query it once per inbound message and a per-call
OIDB refetch trips Tencent risk-control. But the group-admin push
decoder only emitted the notice and never updated the cached role, so a
member promoted to admin kept reading back as `member` and permission
gates rejected their commands. Patch the cached role in the decoder on
set/unset, only touching members we already know and never downgrading
the owner.

Also fix promote/demote misdetection: proton materializes an absent
embedded-message field as an empty `{}` rather than undefined, so the
old `extraEnable !== undefined` test was always true — every demote was
reported as a promotion with a fallback userUin. Key off whichever
sub-message actually carries an adminUid instead.

Fixes #93
2026-06-09 16:30:10 +08:00
DrSmoothl d508814dfa chore(release): v1.9.8 2026-06-09 14:22:43 +08:00
snowluma-bot[bot] ac049e5cc1 chore(native): replace native artifacts (native-886e60a) (#90)
Source: SnowLuma/nnphook@886e60a271
Build:  https://github.com/SnowLuma/SnowLumaNative/releases/tag/native-886e60a

Co-authored-by: snowluma-bot[bot] <283088347+snowluma-bot[bot]@users.noreply.github.com>
2026-06-08 00:13:38 +08:00
DrSmoothl 515bac4b3c chore(release): v1.9.7 2026-06-07 22:29:37 +08:00
DrSmoothl 6a2fd9e8f3 fix(bridge): don't crash the daemon when the hook pipe closes mid-send
When the control pipe drops while a send() is awaiting its ack,
rejectControlPending() rejects both the ack and the reply deferreds. The
reply is only awaited after the ack, so an ack-first rejection leaves the
reply promise rejected with NO awaiter — an unhandled rejection that, under
Node's default policy, kills the process. In Docker supervisor restarts it
(every session drops for ~1s); on a bare Windows run it just dies, surfacing
as "connection pipe closed" with the bot offline after hours of uptime.

Attach a no-op .catch() to each deferred at creation so a rejection can never
go unhandled (the real await still observes the value/rejection). Add a
regression test that drops the pipe mid-send and asserts no unhandled
rejection escapes — it fails without the guard.

Also install last-resort process guards (unhandledRejection / uncaughtException)
in the app entry: log and keep serving instead of exiting, so any other stray
rejection cannot take the daemon down and its stack is captured. The bridge's
watcher reconnects the dropped pipe on the next tick.
2026-06-07 22:15:07 +08:00
墨梓柒 ac39d04170 Merge branch 'main' into dev 2026-06-07 21:20:39 +08:00
DrSmoothl 174150c5e1 chore(release): v1.9.6 2026-06-07 21:19:00 +08:00
DrSmoothl 56dc934106 feat(webui): notify when a newer release is available
Advisory, read-only update check: the core polls GitHub's latest stable release, compares it against the running build, and surfaces a notice in the sidebar footer plus a detailed card (changelog + download link + platform-matched asset hint) in Settings → About.

SnowLuma never downloads or applies anything itself — cross-platform self-update is a minefield (Windows locks loaded .node/.dll, Docker bakes the app into image layers, bare installs have no supervisor), so the user downloads and applies manually.

The backend caches results for 6h to stay under GitHub's unauthenticated rate limit, degrades silently on failure, and can be disabled via SNOWLUMA_UPDATE_CHECK=0.
2026-06-05 16:08:38 +08:00
DrSmoothl 28db1d1079 fix(onebot): distinguish group disband from kick_me (#87)
group_decrease collapsed GroupChange.decreaseType into a single isKick boolean, so a group disband (decreaseType 129) was indistinguishable from the bot being kicked — both surfaced as kick_me.

Preserve the protocol reason as a leaveType discriminator (leave | kick | disband) on GroupMemberLeave: decreaseType 129 -> disband, 0/130 -> leave, otherwise kick. The OneBot converter emits disband, or splits kick into kick/kick_me by selfId as before. leave/kick/kick_me are unchanged.
2026-06-05 11:49:14 +08:00
DrSmoothl 966855469d ci: run the full test suite on pull requests
The repo had no PR test gate, so broken tests landed on dev unnoticed. Add a
pull_request workflow that runs `pnpm -r --no-bail --if-present test` across all
packages on ubuntu-latest (pnpm 10.28.0, Node 22), with per-PR concurrency.
2026-06-04 23:36:33 +08:00
DrSmoothl 9374b8ac64 fix(protocol): invokeOidb rejects on packet send failure (success=false)
invokeOidb only threw on success=false when an OIDB errorCode was set (the
block added in #69); a send failure with no errorCode silently resolved with an
undecoded result. Surface it — throw errorMessage || 'packet send failed',
matching runOidb and the oidb-service tests that were already red on dev.
2026-06-04 23:36:22 +08:00
ikun两年半 34ea2ce438 refactor(webui): 移除冗余的手动保存按钮喵,通用设置改为 debounce 自动保存喵 (#83)
* refactor(webui): 移除冗余的手动保存按钮喵,通用设置改为 debounce 自动保存喵

* fix(webui): 修复保存按钮状态喵,允许在自动保存时手动触发喵
2026-06-04 18:03:17 +08:00
墨梓柒 9dedcf93a8 Dev (#82)
* test(sdk): remove obsolete actions-sync guard

c49ae1c removed the SDK's hardcoded action-name list (src/actions.ts / SNOWLUMA_ACTIONS) and updated exports, but left the test that guarded it. The test imports SNOWLUMA_ACTIONS from ../src -- now undefined ("not iterable") -- and its registerAction('...') enumeration of onebot is itself stale after the declarative action-kit migration. Nothing else references SNOWLUMA_ACTIONS; the list it kept in sync no longer exists, so the guard is dead. Removing it makes the sdk suite green (16 tests).

* fix(mcp): add repository metadata so the provenance publish validates

npm publish --provenance rejected @snowluma/mcp@1.9.5 with E422 because package.json had no repository.url to match the GitHub repo asserted in the provenance statement. Add repository/homepage/bugs (mirroring @snowluma/sdk and @snowluma/ui, directory packages/mcp) and refresh the now-inaccurate 'read-only' description. 1.9.5 was never successfully published, so re-running the publish workflow will ship it.
2026-06-03 22:19:31 +08:00
DrSmoothl 0cc7ef2f30 fix(mcp): add repository metadata so the provenance publish validates
npm publish --provenance rejected @snowluma/mcp@1.9.5 with E422 because package.json had no repository.url to match the GitHub repo asserted in the provenance statement. Add repository/homepage/bugs (mirroring @snowluma/sdk and @snowluma/ui, directory packages/mcp) and refresh the now-inaccurate 'read-only' description. 1.9.5 was never successfully published, so re-running the publish workflow will ship it.
2026-06-03 22:17:58 +08:00
DrSmoothl d2097f77c8 test(sdk): remove obsolete actions-sync guard
c49ae1c removed the SDK's hardcoded action-name list (src/actions.ts / SNOWLUMA_ACTIONS) and updated exports, but left the test that guarded it. The test imports SNOWLUMA_ACTIONS from ../src -- now undefined ("not iterable") -- and its registerAction('...') enumeration of onebot is itself stale after the declarative action-kit migration. Nothing else references SNOWLUMA_ACTIONS; the list it kept in sync no longer exists, so the guard is dead. Removing it makes the sdk suite green (16 tests).
2026-06-03 22:17:58 +08:00
snowluma-bot[bot] 46cbd29956 Merge pull request #81 from SnowLuma/dev
chore: promote dev → main (4fcaba2)
v1.9.5
2026-06-03 13:56:10 +00:00
DrSmoothl 4fcaba2f3e chore(release): v1.9.5 2026-06-03 21:55:37 +08:00
DrSmoothl 4427ecd569 docs(mcp): document execution mode (modes, env, read/write safety)
Covers the docs vs execution split, the four SNOWLUMA_MCP_* env vars, the query_action / invoke_action tools, read/write-mode example client configs, and the per-action safety model (default-write classification, the enforced mode gate, unknown-action rejection, envelope passthrough, and the readOnly/destructive annotations).
2026-06-03 21:55:05 +08:00
DrSmoothl 6d92a28e2b feat(mcp): execution mode — query/invoke OneBot actions with a read/write safety gate
@snowluma/mcp gains execution alongside the read-only catalog: when SNOWLUMA_MCP_ENDPOINT is set it exposes query_action (read-only actions, readOnlyHint) and, in write mode, invoke_action (any known action, destructiveHint), proxied to a running OneBot instance over HTTP. Without an endpoint it degrades to docs-only. SNOWLUMA_MCP_MODE (docs|read|write) gates tool visibility AND is re-checked in execute(), so an unlisted tool cannot be called; unknown/legacy actions are non-invokable.

Read vs write is classified precisely at the source: defineAction/preset gain readOnly (default false = write, the safe default), flowed through describe() into the generated catalog snapshot. 58 actions are marked read-only by what their run() actually does -- name-traps like get_ai_record (publishes to the group), request_decrypt_key (returns a DB key), and download_file (arbitrary URL + disk write) stay writes.

The HTTP call is a ~20-line fetch bridge rather than @snowluma/sdk: the SDK's published dist is bundler-only ESM (extensionless imports) that native node cannot load, and the bin stays a plain tsc build.

onebot 332 tests pass (readOnly is metadata-only, no behavior change); mcp gains 21 tests for mode gating / read-write routing / envelope passthrough / error mapping. Full workspace typecheck clean; native node smoke verified.
2026-06-03 21:53:02 +08:00
DrSmoothl 18f7d847ac refactor(onebot): lift the network adapter reload state machine into the base class
The four NetworkAdapters each carried a byte-identical reload() state machine (sigChanged/wasEnabled -> Reopened/Closed/Opened/Normal). Lift it into IOneBotNetworkAdapter as a template method; per-adapter variation is now expressed via three hooks: bindingSignature(), willEnable() (default enabled!==false; clients add && !!url), and onConfigReplaced() (post-swap side effects).

Also delete four redundant 'override get isActive()' (identical to the base getter), lift the duplicated uin-logger setup into the base ctor, and drop http-post's now-redundant signature_ cache.

NetworkReloadType is a test-observability seam -- the sole production caller (instance.applyConfigDiff) discards it -- so a new base-level test exercises all transitions directly. Behaviour-preserving: +94/-157 in src, full workspace typecheck clean, 332 onebot tests pass.
2026-06-03 20:53:36 +08:00
游手好闲的言柒 d3092a1806 fix(onebot): support inline file url/path in send_group/private_msg (#80)
file 段现接受 file/url/path(无需先 upload_group_file/upload_private_file);群走 groupFile.upload()、私聊走 uploadPrivate()(内部已 publish/sendC2c),file_id 老路径不变。含 parser 与群/私聊文件发送测试。
2026-06-03 20:00:58 +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
DrSmoothl fc21d87a9b feat(onebot): fetch_ptt_text via async 0x210/61 result push (#79)
Live testing showed the voice-to-text text is delivered by an async push
(Event 0x210 subType 61), not the request response — so the previous
poll-by-resend never got it. Captured a real push and decoded its protobuf:
outer { f1, f2:item }; the item echoes the request msgId at field 1 (the
correlation key) and the recognised text at field 8.

- proto-defs: PttTransPush / PttTransPushItem (live-verified).
- protocol: Event0x210SubType.PttTransResult = 61 + decoder → internal
  `ptt_trans_result` event { msgId, text } (replaces the temp hex dump).
- core: translatePttToText now sends ONE trigger and returns the inline text
  only if present (already-transcribed); empty = pending.
- onebot: ptt-trans-waiter (keyed by selfUin:msgId), event-pipeline subscribes
  `ptt_trans_result` to resolve it; fetch_ptt_text registers the waiter,
  fires the trigger, and awaits the push (20s timeout).

Decoded one real push as text "12311123。" end to end.
2026-06-02 22:01:07 +08:00
DrSmoothl e036e70acc chore(protocol): temp-dump 0x210 subType-61 ptt-trans push body (#79)
Live testing showed the voice-to-text text is delivered via an async push
Event 0x210 subType 61 (0x3D), not the sync pttTrans.TransC2CPttReq response
(which is a fixed ~137B ack). That push has no static sys_msg_0x210_0x3d
handler — its protobuf is registered dynamically by the ptt-trans subsystem,
so RE through the std::function callback chain is impractical.

Dump the raw subType-61 body (fromUin + len + hex) once so the wire format
can be decoded from ground truth, then implement the real decoder +
request/await correlation. Temporary — to be replaced by the actual decoder.
2026-06-02 21:28:44 +08:00
DrSmoothl 92cfaf24b8 feat(log): trace the inbound receive chain (symmetric to outbound)
Sending got full-chain trace via the api-handler chokepoint; receiving had
none. Wrap convertAndDispatch — the single path every inbound message/notice
takes (decode → convert → dispatch) — as the receive-side mirror:

- When trace is live, run the chain inside a request id so the whole receive
  (incl. any rkey-fetch packets the conversion triggers) is correlated under
  one [req#N]; otherwise skip the wrap entirely (zero overhead).
- trace the raw inbound event on entry (deep render) and the converted
  post_type + elapsed on exit (or "dropped").

Reuses the existing trace/ALS/renderParamsVerbose machinery; no new mechanism.
2026-06-02 21:15:12 +08:00
DrSmoothl 15e4e261c5 feat(log): memory-only trace level + request correlation + chokepoint tracing
Debugging from user-supplied logs was impossible: a successful action left
one terse, over-summarized debug line and nothing about what it sent QQ or
what QQ returned. Add full-chain tracing that stays off disk so it scales to
hundreds of groups.

common:
- New `trace` level (below debug). Never written to file — it lives only in
  an in-memory ring + the WebUI stream, so the high-volume full chain costs no
  disk. `emit` early-returns for trace before format/render (and before any
  lazy producer) when trace isn't active → ~free at the default level.
- Dedicated O(1) circular ring for trace (default 5000, SNOWLUMA_TRACE_BUFFER),
  separate from the normal 1000-entry buffer (also switched off array .shift);
  getRecentLogs merges both by id.
- request-context.ts: AsyncLocalStorage request id. The logger auto-stamps
  `[req#N]` on every line in a request's async chain — across packages, zero
  signature threading — and records it on the entry.
- log-summary.renderParamsVerbose: deep render of params (nested structure,
  per-string cap, total budget, token/password/secret redaction).

chokepoints (auto-cover every action without touching 100+ handlers):
- onebot api-handler: wrap each action in a request id (only when trace is
  live); trace the full params on entry and status+elapsed on exit; the terse
  debug breadcrumb still persists to file, failures still warn.
- core bridge.sendRawPacket: trace on success (cmd, sizes, elapsed); warn on
  errorCode!=0 / no response (cmd, code, message) so disk keeps real problems.
- protocol highway: fold the bare console.log lines into the logger (trace).

webui: trace as the lowest filterable level; render the `[req#N]` tag and let
search match it. Server needs no change — LOG_LEVELS, getRecentLogs and the
SSE stream carry it through.
2026-06-02 21:01:19 +08:00
DrSmoothl 7336352cd3 feat(onebot): voice-to-text action fetch_ptt_text (#79)
Adds NapCat-compatible voice-to-text. New `fetch_ptt_text` action (aliases
`get_ptt_text` / `get_record_text`) takes a message_id, resolves the cached
`record` fingerprint (uuid + md5 + duration/size/format), drives QQ's native
`pttTrans.TransC2CPttReq` / `pttTrans.TransGroupPttReq`, and returns { text }.

The response carries the text inline once transcription is done; an empty
text means the server is still transcribing (there is no dedicated push
command — confirmed via RE), so the bridge re-sends with a short backoff
until the text lands, mirroring NapCat's fire-then-re-read shape.

Layers:
- proto-defs/ptt-trans: PttTransReq/Resp + per-scene items.
- core ExtrasApi.translatePttToText: encode -> sendRawPacket -> decode -> poll.
- onebot media-actions.fetchPttText + api-handler/instance-context wiring.
- onebot extended: register the action + aliases.

The commands + request/response field tags were reverse-engineered from
QQ-NT wrapper.node (im_core/msg/ptt/ptt_trans_worker.cc). The numeric tags
are solid; the length-delimited fields (uuid/md5) and the response text tag
are RE-derived and want one live capture to confirm before relying on them.
2026-06-02 20:47:06 +08:00
DrSmoothl 9b92f3825d refactor(onebot): declarative action-kit for param coercion + validation
Introduce packages/onebot/src/action-kit.ts — a zero-dependency,
type-inferring declarative layer for OneBot action params. Each action
declares its param contract (f.* coercers + optional cross-field rules)
and a `run` handler that receives typed, already-validated params.
Validation failures now return BAD_REQUEST naming the offending field
instead of falling through to ApiHandler's INTERNAL_ERROR catch.

- 141/142 actions migrated to defineAction/groupAction/groupUserAction
  across info/message/friend/group-*/request/extended; only
  .handle_quick_operation stays legacy (it needs the ApiHandler instance,
  which run() does not receive).
- f.messageId() distinguishes signed/non-zero message ids from positive
  group/user ids (f.uint()); int({min:0}).default(1800) fixes duration=0
  (unmute), which the old `|| 1800` silently swallowed.
- run(p, ctx, raw) exposes the raw params as an escape hatch for the
  irregular tail (alias-key fallbacks, arbitrary objects).
- action-docs.ts renders self-generated API docs from describe().
- ApiHandler dispatch / try-catch / response-wrapping unchanged (narrow seam).

Registered action name set is byte-identical before/after (142, verified
by runtime enumeration). Typecheck clean; onebot suite green (the
pre-existing websocket native-addon test failure is unrelated).
2026-06-02 15:21:46 +08:00
snowluma-bot[bot] 74c727b9e3 Merge pull request #77 from SnowLuma/dev
chore: promote dev → main (a1f372b)
v1.9.4
2026-06-01 08:22:22 +00:00
DrSmoothl a1f372bc9d chore(release): v1.9.4 2026-06-01 16:21:54 +08:00
DrSmoothl b23607d51d feat(webui): add About tab to settings with Star CTA
New "关于" mini-tab in the settings page showcasing SnowLuma: logo +
name + version badge (__APP_VERSION__) + tagline, a short blurb, and a
prominent GitHub Star call-to-action linking to SnowLuma/SnowLuma, plus
secondary links (repo / releases / issues).

Static CTA — no GitHub API fetch, so no rate-limit / loading state.
Reuses /logo.png (same asset as sidebar/login) and the existing Card +
motion + accent-color design language; responsive centered layout.
2026-06-01 16:07:49 +08:00
墨梓柒 a51bf60d9a Merge pull request #76 from tt-P607/fix/highway-multi-chunk-keepalive
fix(protocol): one TCP connection per highway chunk
2026-06-01 15:04:05 +08:00
DrSmoothl 003e8c2deb feat(onebot): built-in #sl status command (toggleable, non-plugin)
A hardcoded `#sl` status command — deliberately NOT a plugin system.
Matched at the instance dispatch choke point: a pure-text message equal
to `#sl` (exact, trimmed, case-insensitive — no startsWith) replies with
SnowLuma version / platform / uptime in the same conversation.

- Anyone can trigger; the per-UIN `statusCommand.enabled` toggle is the
  only gate (default on). Per-conversation cooldown (default 5s,
  configurable) prevents flood/风控 from repeated triggers.
- `swallow` (default false / pass-through): when set, a matched `#sl` is
  not forwarded to downstream adapters — still cached, logged, replied.
- Config on OneBotConfig.statusCommand (per-UIN): defaulted + clamped in
  config.ts, mirrored/normalized in the WebUI; edits hot-reload via the
  shared instance context.
- WebUI: a "内置命令" section in the 通用设置 tab (enable / swallow /
  cooldown). Node enable toggle extracted into a shared ui/ToggleSwitch.

Pure matcher / cooldown / uptime / text helpers live in
modules/status-command with unit tests; config defaults + clamp covered.
2026-06-01 14:58:13 +08:00