Keep custom titles and previews available when reading uploaded forwards,
including nested messages and refreshed sender names.
Reject malformed preview metadata before processing the request.
Refs #493
Preserve message history and the media information needed for reliable
forwarding. Retain available older data during upgrades and report storage
failures explicitly.
Fixes#462
Prevent ordinary messages and forwarded nodes from combining video content with visible sibling segments or additional videos. Invalid combinations now fail before parsing side effects, media work, or bridge sends, while exact empty text placeholders remain compatible with a single video.
Cover group, private, temporary-session, CQ string, nested forward, cached forward, and protocol-builder paths. Preserve the existing standalone window-shake behavior and keep valid single-video messages unchanged.
Replace the open message bag with a validated discriminated union and preflight complete messages before uploads or sends. Return BAD_REQUEST for invalid or unsendable segments instead of dropping fields, sending partial content, or silently changing private messages.
Merged-forward nodes always stamped the current time. A custom forward can now
set each node's displayed timestamp via the OneBot `data.time` field (unix
seconds), matching the field the wire already carries per node.
parseForwardNodes reads `data.time` on custom/fake nodes and threads it onto the
node payload (the `time?` field already existed for the receive side);
buildForwardPushBody uses it for `contentHead.timestamp`, falling back to now
when absent. The value is validated to a plausible unix-seconds range — the wire
field is uint32, so a millisecond or out-of-range input is rejected (would
overflow) and defaults to now rather than corrupting the timestamp. The
message_id-reference branch (time from the quoted message) and the c2c-file
expireTime (real now) are unchanged. Nested inner nodes honour their own time.
A fake forward node (send_group_forward_msg / send_private_forward_msg) whose
data.user_id was "0" or omitted got rejected with "forward node user_id/uin is
required", even though the protocol端 is logged in and the core forward builder
(bridge/apis/forward.ts) already defaults a zero sender to the bot's own uin.
Upstream frameworks (AstrBot, etc.) don't manage QQ uins and legitimately send
user_id "0"; NapCat and LLBot accept it. parseForwardNodes now falls back to
ref.selfId, keeping the throw only for the not-logged-in case (selfId 0).
Verified on-target (#203).
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).