Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
123 lines
6.1 KiB
Markdown
123 lines
6.1 KiB
Markdown
# Reverse WebSocket connections
|
|
|
|
`@snowluma/sdk/node` can accept OneBot reverse WebSocket connections. SnowLuma
|
|
connects **to your SDK program** and sends events; your program sends actions
|
|
back over that same connection. The ordinary `createWebSocketClient()` continues
|
|
to dial a SnowLuma WebSocket server. Neither SDK entry acts as a QQ implementation
|
|
for NoneBot/AstrBot; SnowLuma's `networks.wsClients` already serves that use case.
|
|
|
|
```ts
|
|
import { text } from '@snowluma/sdk';
|
|
import { createReverseWebSocketServer } from '@snowluma/sdk/node';
|
|
|
|
const server = createReverseWebSocketServer({
|
|
host: '127.0.0.1',
|
|
port: 3002,
|
|
path: '/onebot/v11/ws',
|
|
accessToken: process.env.ONEBOT_ACCESS_TOKEN,
|
|
// Optional: restrict which accounts may connect, in addition to the token.
|
|
selfIds: ['10001'],
|
|
});
|
|
|
|
server.on('error', (error) => console.error('Reverse connection failed', error));
|
|
server.on('rejected', ({ status, reason }) => console.error('Handshake rejected', status, reason));
|
|
server.on('connection', (bot) => {
|
|
console.log('Connected account', bot.selfId);
|
|
bot.onMessage(async (_event, context) => {
|
|
await context.reply(text('received'));
|
|
});
|
|
});
|
|
server.on('disconnect', ({ client, code }) => console.log('Disconnected', client.selfId, code));
|
|
await server.listen();
|
|
// Await server.close() when shutting down your application.
|
|
```
|
|
|
|
Configure the matching SnowLuma account with an entry in `networks.wsClients`:
|
|
|
|
```json
|
|
{
|
|
"name": "sdk",
|
|
"url": "ws://127.0.0.1:3002/onebot/v11/ws",
|
|
"accessToken": "same-token-as-the-sdk-server",
|
|
"role": "Universal",
|
|
"messageFormat": "array",
|
|
"reportSelfMessage": false
|
|
}
|
|
```
|
|
|
|
Use the address of the SDK host if the two processes run on different machines.
|
|
The listener binds only to loopback by default. Authentication accepts a Bearer
|
|
header or `access_token` query parameter. Empty/omitted tokens mean no token
|
|
check. Account and role headers are still mandatory. No credentials or frame
|
|
bodies are included in handshake rejection events.
|
|
|
|
## Sessions and channels
|
|
|
|
`connection` supplies a `SnowLumaReverseWebSocketSession` with `selfId` (string),
|
|
`canCallActions`, `canReceiveEvents` and the existing forward client's helpers:
|
|
`onMessage`, middleware, `waitForMessage`, `getStatus`, message sends, `request`,
|
|
`requestStream`, upload and download helpers. Signed message IDs, response errors,
|
|
timeouts, cancellation and stream limits retain their existing meaning.
|
|
|
|
`server.getClient(selfId)` and the read-only `server.clients` snapshot expose live
|
|
sessions. Each account owns its pending requests and event handlers. Events must
|
|
match the account declared by `X-Self-ID`; mismatches close the session and report
|
|
an error. Duplicate active channels receive HTTP 409 and do not replace a healthy
|
|
session. An optional account allowlist rejects unknown accounts with HTTP 403.
|
|
|
|
Universal mode carries both actions and events. For separate channels, configure
|
|
one SnowLuma `Api` connection and one `Event` connection using the same account
|
|
and server address. `API` is also accepted in the wire header. Both share one SDK
|
|
session, so an event handler's `context.reply()` uses the API channel. `connection`
|
|
fires when the first channel arrives; `channel` fires for each accepted channel:
|
|
|
|
```ts
|
|
server.on('channel', ({ client, role }) => {
|
|
console.log(client.selfId, role, client.canCallActions, client.canReceiveEvents);
|
|
});
|
|
```
|
|
|
|
Wait for `canCallActions` before sending actions in split mode. Calling an action
|
|
on an Event-only session fails explicitly. Closing either half ends the entire
|
|
session and its sibling channel. SnowLuma owns reconnect timing; the next
|
|
connection creates a new session and emits `connection` again, where handlers
|
|
should be registered anew. Old session objects cannot reconnect themselves.
|
|
Pending requests, stream iterators and event waits fail on disconnect. Actions
|
|
are never automatically replayed because the remote side may already have
|
|
executed them. This transport does not persist events missed while disconnected.
|
|
|
|
## Limits, errors and ownership
|
|
|
|
The defaults are 100 MiB per incoming message, 16 MiB of outgoing buffered bytes
|
|
per channel, 128 simultaneous channels, a 30-second action timeout and a
|
|
30-second ping interval. Configure `maxPayloadBytes`, `maxBufferedBytes`,
|
|
`maxConnections`, `requestTimeoutMs` and `heartbeatIntervalMs` as needed. A zero
|
|
request timeout waits indefinitely; a zero heartbeat interval disables pings.
|
|
Other limits must be positive integers. All timers are cleaned up on close.
|
|
Oversized inbound messages close with 1009; outbound capacity failures reject
|
|
the action without enqueueing it. A peer missing the next ping check is terminated
|
|
(normally within two heartbeat intervals).
|
|
|
|
Register a server `error` handler: unhandled errors follow Node's EventEmitter
|
|
error contract and throw. Session transport, parsing and asynchronous event-handler
|
|
failures are forwarded there. Invalid peer JSON and cross-account/channel frames
|
|
close the connection; startup bind errors reject `listen()` directly.
|
|
|
|
To reuse an HTTP or HTTPS server, pass `server` instead of `host`/`port`, after
|
|
that server has started listening. Only the configured upgrade path belongs to
|
|
the SDK. Ordinary requests and other upgrade paths remain with their existing
|
|
handlers. `close()` releases SDK connections and listeners without closing the
|
|
external server. TLS configuration remains with that HTTPS server or your proxy;
|
|
there is no automatic downgrade. Internally owned listeners are closed by the
|
|
SDK. Closing connections have a bounded handshake grace period before termination.
|
|
|
|
Only the Node subpath imports the `ws` runtime dependency; browser-facing SDK
|
|
entry points retain their existing imports. TypeScript Node consumers should
|
|
include their normal `@types/node` development dependency and enable `"types": ["node"]` in their TypeScript configuration.
|
|
|
|
Validation uses real local WebSockets and SnowLuma's `WsClientAdapter` with the
|
|
actual action and stream handlers, isolated from QQ accounts. It covers Universal
|
|
and split channels, identity/authentication, message replies, multi-account echoes,
|
|
disconnect cleanup, limits, heartbeat, external-server ownership and published
|
|
NodeNext/bundler package consumers.
|