@dsbot-channel/onebot 0.0.0-stage → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import type { AdapterFactory, AdapterMount } from '@dsbot-channel/core/adapter-registry.js';
2
+ import type { OneBotAdapterConfig } from './adapter.js';
3
+ /** How this link carries OneBot: one protocol per adapter, never two at once. */
4
+ export declare const ONEBOT_PROTOCOLS: readonly ["http", "ws-forward", "ws-reverse"];
5
+ export type OneBotProtocol = (typeof ONEBOT_PROTOCOLS)[number];
6
+ export declare const DEFAULT_ONEBOT_WS_PATH = "/onebot/ws";
7
+ export interface OneBotReconnectConfig {
8
+ initialMs: number;
9
+ maxMs: number;
10
+ factor: number;
11
+ }
12
+ export interface OneBotHeartbeatConfig {
13
+ intervalMs: number;
14
+ timeoutMs: number;
15
+ }
16
+ /** The two address kinds history may be opened for, each off unless the profile says so. */
17
+ export interface OneBotHistoryConfig {
18
+ group: boolean;
19
+ private: boolean;
20
+ }
21
+ /** Longest token the transport may carry for its OneBot callback. */
22
+ export declare const DEFAULT_ONEBOT_SEND_TIMEOUT_MS = 10000;
23
+ export declare const DEFAULT_ONEBOT_PORT = 7331;
24
+ export declare const DEFAULT_ONEBOT_PATH = "/onebot/event";
25
+ /** Reads the transport block an OneBot adapter is mounted with. */
26
+ export declare function parseOneBotConfig(mount: AdapterMount): OneBotAdapterConfig;
27
+ export declare const createOneBotAdapter: AdapterFactory;
28
+ export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
29
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
30
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
31
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
32
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
33
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
34
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
35
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
36
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
37
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
38
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
39
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
40
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
41
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
42
+ }>>, Schemastery.ObjectT<NoInfer<{
43
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
44
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
45
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
46
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
47
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
48
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
49
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
50
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
51
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
52
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
53
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
54
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
55
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
56
+ }>>, "plain">;
57
+ export declare const apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
58
+ export declare const plugin: {
59
+ name: string;
60
+ inject: string[];
61
+ Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
62
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
63
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
64
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
65
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
66
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
67
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
68
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
69
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
70
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
71
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
72
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
73
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
74
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
75
+ }>>, Schemastery.ObjectT<NoInfer<{
76
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
77
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
78
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
79
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
80
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
81
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
82
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
83
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
84
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
85
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
86
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
87
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
88
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
89
+ }>>, "plain">;
90
+ apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
91
+ };
92
+ declare const _default: {
93
+ name: string;
94
+ inject: string[];
95
+ Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
96
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
97
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
98
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
99
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
100
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
101
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
102
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
103
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
104
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
105
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
106
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
107
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
108
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
109
+ }>>, Schemastery.ObjectT<NoInfer<{
110
+ channel: import("@deepseek-ai/schemastery").default<string, string, "plain">;
111
+ botName: import("@deepseek-ai/schemastery").default<string, string, "plain">;
112
+ anchor: import("@deepseek-ai/schemastery").default<string, string, "plain">;
113
+ session: import("@deepseek-ai/schemastery").default<string, string, "plain">;
114
+ model: import("@deepseek-ai/schemastery").default<string, string, "plain">;
115
+ reply: import("@deepseek-ai/schemastery").default<any, any, "plain">;
116
+ turn: import("@deepseek-ai/schemastery").default<any, any, "plain">;
117
+ delivery: import("@deepseek-ai/schemastery").default<any, any, "plain">;
118
+ policy: import("@deepseek-ai/schemastery").default<any, any, "plain">;
119
+ pipeline: import("@deepseek-ai/schemastery").default<any, any, "plain">;
120
+ events: import("@deepseek-ai/schemastery").default<any, any, "plain">;
121
+ status: import("@deepseek-ai/schemastery").default<boolean, boolean, "plain">;
122
+ transport: import("@deepseek-ai/schemastery").default<any, any, "plain">;
123
+ }>>, "plain">;
124
+ apply: (ctx: import("@deepseek-ai/cordis").Context, config?: Record<string, unknown>) => void;
125
+ };
126
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ import { makeAdapterPlugin } from '@dsbot-channel/core/adapter-plugin.js';
2
+ import { ConfigError, assertBindHost } from '@dsbot-channel/core/config.js';
3
+ import { OneBotAdapter } from './adapter.js';
4
+ const FIELDS = new Set([
5
+ 'selfId',
6
+ 'protocol',
7
+ 'listen',
8
+ 'api',
9
+ 'event',
10
+ 'reconnect',
11
+ 'heartbeat',
12
+ 'accessToken',
13
+ 'sendTimeoutMs',
14
+ 'bots',
15
+ 'history',
16
+ 'fileFromUrl',
17
+ ]);
18
+ /** How this link carries OneBot: one protocol per adapter, never two at once. */
19
+ export const ONEBOT_PROTOCOLS = ['http', 'ws-forward', 'ws-reverse'];
20
+ export const DEFAULT_ONEBOT_WS_PATH = '/onebot/ws';
21
+ /** Longest token the transport may carry for its OneBot callback. */
22
+ export const DEFAULT_ONEBOT_SEND_TIMEOUT_MS = 10_000;
23
+ export const DEFAULT_ONEBOT_PORT = 7331;
24
+ export const DEFAULT_ONEBOT_PATH = '/onebot/event';
25
+ function refuse(message, field) {
26
+ throw new ConfigError(message, field === undefined ? ['adapters', 'transport'] : ['adapters', 'transport', field]);
27
+ }
28
+ function record(value, field) {
29
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
30
+ refuse(`transport.${field} must be an object`, field);
31
+ }
32
+ return value;
33
+ }
34
+ function requiredText(value, field) {
35
+ if (typeof value !== 'string' || value.length === 0)
36
+ refuse(`transport.${field} must be a non-empty string`, field);
37
+ return value;
38
+ }
39
+ function optionalText(value, field) {
40
+ if (value === undefined)
41
+ return undefined;
42
+ if (typeof value !== 'string' || value.length === 0)
43
+ refuse(`transport.${field} must be a non-empty string`, field);
44
+ return value;
45
+ }
46
+ function whole(value, field, atLeast) {
47
+ if (value === undefined)
48
+ return undefined;
49
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < atLeast) {
50
+ refuse(`transport.${field} must be a whole number of at least ${atLeast}`, field);
51
+ }
52
+ return value;
53
+ }
54
+ function parseProtocol(value) {
55
+ if (value === undefined)
56
+ return 'http';
57
+ if (typeof value !== 'string' || !ONEBOT_PROTOCOLS.includes(value)) {
58
+ refuse(`transport.protocol must be one of ${ONEBOT_PROTOCOLS.join(', ')}`, 'protocol');
59
+ }
60
+ return value;
61
+ }
62
+ function parseListen(value, defaultPath) {
63
+ const raw = value === undefined ? {} : record(value, 'listen');
64
+ const host = optionalText(raw.host, 'listen.host') ?? '127.0.0.1';
65
+ assertBindHost(host, ['adapters', 'transport', 'listen', 'host']);
66
+ const port = whole(raw.port, 'listen.port', 0) ?? DEFAULT_ONEBOT_PORT;
67
+ const path = optionalText(raw.path, 'listen.path') ?? defaultPath;
68
+ if (!path.startsWith('/'))
69
+ refuse('transport.listen.path must start with "/"', 'listen.path');
70
+ return { host, port, path };
71
+ }
72
+ function parseSocketUrl(value, field) {
73
+ const url = requiredText(value, field);
74
+ let parsed;
75
+ try {
76
+ parsed = new URL(url);
77
+ }
78
+ catch {
79
+ refuse(`transport.${field} must be an absolute ws(s) url`, field);
80
+ }
81
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
82
+ refuse(`transport.${field} must be an absolute ws(s) url`, field);
83
+ }
84
+ return url;
85
+ }
86
+ function parseReconnect(value) {
87
+ if (value === false)
88
+ return false;
89
+ const raw = value === undefined ? {} : record(value, 'reconnect');
90
+ const initialMs = whole(raw.initialMs, 'reconnect.initialMs', 1) ?? 500;
91
+ const maxMs = whole(raw.maxMs, 'reconnect.maxMs', 1) ?? 30_000;
92
+ const factor = raw.factor;
93
+ if (factor !== undefined && (typeof factor !== 'number' || !Number.isFinite(factor) || factor < 1)) {
94
+ refuse('transport.reconnect.factor must be a number of at least 1', 'reconnect.factor');
95
+ }
96
+ if (initialMs > maxMs)
97
+ refuse('transport.reconnect.initialMs must not exceed maxMs', 'reconnect.initialMs');
98
+ return { initialMs, maxMs, factor: factor ?? 2 };
99
+ }
100
+ function parseHeartbeat(value) {
101
+ if (value === undefined)
102
+ return undefined;
103
+ const raw = record(value, 'heartbeat');
104
+ const intervalMs = whole(raw.intervalMs, 'heartbeat.intervalMs', 0) ?? 0;
105
+ const timeoutMs = whole(raw.timeoutMs, 'heartbeat.timeoutMs', 1) ?? 0;
106
+ if (intervalMs > 0 && timeoutMs === 0) {
107
+ refuse('transport.heartbeat.timeoutMs is required when intervalMs is above 0', 'heartbeat.timeoutMs');
108
+ }
109
+ if (intervalMs > 0 && timeoutMs <= intervalMs) {
110
+ refuse('transport.heartbeat.timeoutMs must be above intervalMs', 'heartbeat.timeoutMs');
111
+ }
112
+ return { intervalMs, timeoutMs };
113
+ }
114
+ function parseApi(value, timeoutMs, socket) {
115
+ if (value === undefined)
116
+ refuse('transport.api is required', 'api');
117
+ const raw = record(value, 'api');
118
+ const url = requiredText(raw.url, 'api.url');
119
+ let parsed;
120
+ try {
121
+ parsed = new URL(url);
122
+ }
123
+ catch {
124
+ refuse(`transport.api.url must be an absolute ${socket ? 'ws(s)' : 'http(s)'} url`, 'api.url');
125
+ }
126
+ const allowed = socket ? ['ws:', 'wss:'] : ['http:', 'https:'];
127
+ if (!allowed.includes(parsed.protocol)) {
128
+ refuse(`transport.api.url must be an absolute ${socket ? 'ws(s)' : 'http(s)'} url`, 'api.url');
129
+ }
130
+ return { url: url.replace(/\/$/, ''), timeoutMs };
131
+ }
132
+ function parseHistory(value) {
133
+ const raw = value === undefined ? {} : record(value, 'history');
134
+ const unknown = Object.keys(raw).filter((key) => key !== 'group' && key !== 'private');
135
+ if (unknown.length > 0)
136
+ refuse(`transport.history names a field this adapter does not read: ${unknown.join(', ')}`, 'history');
137
+ const flag = (field) => {
138
+ const one = raw[field];
139
+ if (one === undefined)
140
+ return false;
141
+ if (typeof one !== 'boolean')
142
+ refuse(`transport.history.${field} must be true or false`, `history.${field}`);
143
+ return one;
144
+ };
145
+ return { group: flag('group'), private: flag('private') };
146
+ }
147
+ function parseFileFromUrl(value) {
148
+ if (value === undefined)
149
+ return false;
150
+ if (typeof value !== 'boolean')
151
+ refuse('transport.fileFromUrl must be true or false', 'fileFromUrl');
152
+ return value;
153
+ }
154
+ function parseBots(value) {
155
+ if (value === undefined)
156
+ return [];
157
+ if (!Array.isArray(value))
158
+ refuse('transport.bots must be a list of ids', 'bots');
159
+ return value.map((item, index) => requiredText(item, `bots[${index}]`));
160
+ }
161
+ /** Reads the transport block an OneBot adapter is mounted with. */
162
+ export function parseOneBotConfig(mount) {
163
+ const raw = mount.transport;
164
+ const unknown = Object.keys(raw).filter((key) => !FIELDS.has(key));
165
+ if (unknown.length > 0)
166
+ refuse(`transport names a field this adapter does not read: ${unknown.join(', ')}`);
167
+ const protocol = parseProtocol(raw.protocol);
168
+ const sendTimeoutMs = whole(raw.sendTimeoutMs, 'sendTimeoutMs', 1) ?? DEFAULT_ONEBOT_SEND_TIMEOUT_MS;
169
+ const only = (field, why) => {
170
+ if (raw[field] !== undefined)
171
+ refuse(why, field);
172
+ };
173
+ let api;
174
+ let event;
175
+ let reconnect;
176
+ let heartbeat;
177
+ let listen;
178
+ if (protocol === 'http') {
179
+ only('event', 'transport.event is only read by ws-forward');
180
+ only('reconnect', 'transport.reconnect is only read by ws-forward');
181
+ only('heartbeat', 'transport.heartbeat is only read by a websocket protocol');
182
+ api = parseApi(raw.api, sendTimeoutMs, false);
183
+ listen = parseListen(raw.listen, DEFAULT_ONEBOT_PATH);
184
+ }
185
+ else if (protocol === 'ws-forward') {
186
+ only('listen', 'transport.listen is not read by ws-forward, which connects out');
187
+ api = parseApi(raw.api, sendTimeoutMs, true);
188
+ event = raw.event === undefined ? undefined : { url: parseSocketUrl(raw.event, 'event.url') };
189
+ reconnect = parseReconnect(raw.reconnect);
190
+ heartbeat = parseHeartbeat(raw.heartbeat);
191
+ }
192
+ else {
193
+ only('api', 'transport.api is not read by ws-reverse, which answers on a socket');
194
+ only('event', 'transport.event is only read by ws-forward');
195
+ only('reconnect', 'transport.reconnect is only read by ws-forward');
196
+ heartbeat = parseHeartbeat(raw.heartbeat);
197
+ listen = parseListen(raw.listen, DEFAULT_ONEBOT_WS_PATH);
198
+ }
199
+ return {
200
+ channel: mount.channel,
201
+ botName: mount.botName,
202
+ ...(mount.status === undefined ? {} : { status: mount.status }),
203
+ ...(optionalText(raw.selfId, 'selfId') === undefined ? {} : { selfId: raw.selfId }),
204
+ protocol,
205
+ ...(listen === undefined ? {} : { listen }),
206
+ ...(api === undefined ? {} : { api }),
207
+ ...(event === undefined ? {} : { event }),
208
+ ...(reconnect === undefined ? {} : { reconnect }),
209
+ ...(heartbeat === undefined ? {} : { heartbeat }),
210
+ sendTimeoutMs,
211
+ history: parseHistory(raw.history),
212
+ fileFromUrl: parseFileFromUrl(raw.fileFromUrl),
213
+ ...(optionalText(raw.accessToken, 'accessToken') === undefined ? {} : { accessToken: raw.accessToken }),
214
+ bots: parseBots(raw.bots),
215
+ };
216
+ }
217
+ export const createOneBotAdapter = (mount, options) => new OneBotAdapter(parseOneBotConfig(mount), options);
218
+ const adapter = makeAdapterPlugin({
219
+ name: 'dsbot-channel/onebot',
220
+ kind: 'onebot',
221
+ factory: createOneBotAdapter,
222
+ check: parseOneBotConfig,
223
+ });
224
+ export const Config = adapter.Config;
225
+ export const apply = adapter.apply;
226
+ export const plugin = adapter.plugin;
227
+ export default adapter.plugin;
@@ -0,0 +1,60 @@
1
+ import type { ConnectionHealth } from '@dsbot-channel/core/adapter-status.js';
2
+ /** Kept always on: a half-open socket is the failure that leaves no line in a log. */
3
+ export declare const ONEBOT_PING_INTERVAL_MS = 30000;
4
+ export declare const ONEBOT_PONG_TIMEOUT_MS = 10000;
5
+ export interface LivenessOptions {
6
+ intervalMs: number;
7
+ timeoutMs: number;
8
+ }
9
+ /** The timers a link runs on; a test hands in its own so a window can be walked deterministically. */
10
+ export interface LinkClock {
11
+ setTimer: (callback: () => void, ms: number) => unknown;
12
+ clearTimer: (handle: unknown) => void;
13
+ }
14
+ /** What an action call answers: the payload of a `status: ok` reply, or why none came. */
15
+ export type ActionAnswer = {
16
+ ok: true;
17
+ frame: Record<string, unknown>;
18
+ } | {
19
+ ok: false;
20
+ code: 'timeout' | 'unreachable' | 'rejected';
21
+ reason: string;
22
+ };
23
+ export interface ForwardOptions {
24
+ apiUrl: string;
25
+ /** The event socket; absent means events and actions share the api socket. */
26
+ eventUrl?: string | undefined;
27
+ token?: string | undefined;
28
+ reconnect: {
29
+ initialMs: number;
30
+ maxMs: number;
31
+ factor: number;
32
+ } | false;
33
+ heartbeat?: {
34
+ intervalMs: number;
35
+ timeoutMs: number;
36
+ } | undefined;
37
+ timeoutMs: number;
38
+ /** Protocol-level ping/pong; always on in production, shortened in tests. */
39
+ liveness?: LivenessOptions | undefined;
40
+ /** Timer source for liveness; real timers unless a test hands in its own. */
41
+ clock?: LinkClock | undefined;
42
+ maxFrameBytes: number;
43
+ onEvent: (frame: Record<string, unknown>) => void;
44
+ onMismatch: () => void;
45
+ warn: (message: string) => void;
46
+ now?: (() => number) | undefined;
47
+ }
48
+ /**
49
+ * The forward half of OneBot over WebSocket: this side dials out, sends actions with an
50
+ * `echo`, and reads events off the same socket or a second one.
51
+ */
52
+ export declare class ForwardLink {
53
+ #private;
54
+ constructor(options: ForwardOptions);
55
+ start(): void;
56
+ close(): void;
57
+ health(): ConnectionHealth;
58
+ /** Sends one action and waits for the frame that carries its echo. */
59
+ request(action: string, params: Record<string, unknown>, timeoutMs: number | undefined): Promise<ActionAnswer>;
60
+ }