@zhin.js/adapter-onebot12 1.1.18 → 1.1.20
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.
- package/CHANGELOG.md +43 -0
- package/README.md +24 -5
- package/adapters/onebot12/index.js +3 -32
- package/adapters/onebot12/index.ts +2 -38
- package/commands/onebot12/endpoint/definition.js +2 -2
- package/commands/onebot12/endpoint/definition.ts +2 -2
- package/lib/adapter.d.ts +21 -0
- package/lib/adapter.js +34 -0
- package/lib/client.d.ts +9 -11
- package/lib/client.js +23 -13
- package/lib/console-spec.js +2 -0
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/content-port.d.ts +2 -2
- package/lib/content-port.js +1 -1
- package/lib/endpoint-management.d.ts +5 -5
- package/lib/endpoint-management.js +17 -17
- package/lib/identity.d.ts +4 -0
- package/lib/identity.js +53 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +32 -4
- package/lib/protocol.js +54 -53
- package/lib/response.d.ts +5 -0
- package/lib/response.js +17 -0
- package/lib/side-event-dispatch.d.ts +8 -1
- package/lib/side-event-dispatch.js +2 -1
- package/lib/webhook.d.ts +5 -11
- package/lib/webhook.js +72 -37
- package/lib/ws-endpoint.d.ts +5 -11
- package/lib/ws-endpoint.js +79 -45
- package/lib/ws-transport.d.ts +18 -10
- package/lib/ws-transport.js +26 -45
- package/lib/ws-types.d.ts +0 -6
- package/lib/wss-endpoint.d.ts +5 -11
- package/lib/wss-endpoint.js +82 -48
- package/package.json +15 -15
- package/plugin.js +0 -4
- package/schema.json +15 -3
- package/src/adapter.ts +36 -0
- package/src/client.ts +26 -29
- package/src/console-spec.ts +2 -0
- package/src/console.ts +2 -5
- package/src/content-port.ts +3 -3
- package/src/endpoint-management.ts +25 -24
- package/src/identity.ts +52 -0
- package/src/index.ts +4 -1
- package/src/protocol.ts +71 -62
- package/src/response.ts +19 -0
- package/src/side-event-dispatch.ts +9 -7
- package/src/webhook.ts +70 -55
- package/src/ws-endpoint.ts +75 -63
- package/src/ws-transport.ts +37 -73
- package/src/ws-types.ts +0 -7
- package/src/wss-endpoint.ts +80 -66
- package/lib/onebot12-runtime-state.d.ts +0 -1
- package/lib/onebot12-runtime-state.js +0 -6
- package/src/onebot12-runtime-state.ts +0 -7
package/schema.json
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"endpoints": {
|
|
53
53
|
"type": "array",
|
|
54
54
|
"minItems": 1,
|
|
55
|
-
"description": "
|
|
55
|
+
"description": "多账号:每项明确选择 platform/userId,id 为本地账号名称,其余连接字段覆盖实例默认值",
|
|
56
56
|
"items": {
|
|
57
57
|
"type": "object",
|
|
58
58
|
"additionalProperties": false,
|
|
@@ -121,15 +121,27 @@
|
|
|
121
121
|
"id": {
|
|
122
122
|
"type": "string",
|
|
123
123
|
"description": "OneBot12 bot name"
|
|
124
|
+
},
|
|
125
|
+
"platform": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"minLength": 1,
|
|
128
|
+
"description": "Selected OneBot12 bot platform"
|
|
129
|
+
},
|
|
130
|
+
"userId": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"minLength": 1,
|
|
133
|
+
"description": "Selected OneBot12 bot user ID"
|
|
124
134
|
}
|
|
125
135
|
},
|
|
126
136
|
"required": [
|
|
127
|
-
"id"
|
|
137
|
+
"id",
|
|
138
|
+
"platform",
|
|
139
|
+
"userId"
|
|
128
140
|
]
|
|
129
141
|
},
|
|
130
142
|
"x-i18n": {
|
|
131
143
|
"en": {
|
|
132
|
-
"description": "
|
|
144
|
+
"description": "Each endpoint explicitly selects platform and userId. id is the local account name; other connection fields override instance defaults."
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
},
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
3
|
+
import { resolveOneBot12Config, type OneBot12EndpointConfig } from './protocol.js';
|
|
4
|
+
import { OneBot12WsEndpoint } from './ws-endpoint.js';
|
|
5
|
+
import { OneBot12WssEndpoint } from './wss-endpoint.js';
|
|
6
|
+
import { OneBot12WebhookEndpoint } from './webhook.js';
|
|
7
|
+
import { probeOnebot12Identity, probeOnebot12HttpIdentity } from './identity.js';
|
|
8
|
+
import type { Onebot12ClientEventMap } from './client.js';
|
|
9
|
+
|
|
10
|
+
export class Onebot12Adapter extends Adapter<OneBot12WsEndpoint | OneBot12WssEndpoint | OneBot12WebhookEndpoint, OneBot12EndpointConfig, Onebot12ClientEventMap> {
|
|
11
|
+
readonly #context: AdapterContext<OneBot12EndpointConfig>;
|
|
12
|
+
constructor(context: AdapterContext<OneBot12EndpointConfig>) { super(); this.#context = context; }
|
|
13
|
+
override get configurationIdentityFields(): readonly string[] { return ['id', 'platform', 'userId']; }
|
|
14
|
+
override get configurationIdentityProbeFields(): readonly string[] { return ['connection', 'access_token', 'url', 'api_url']; }
|
|
15
|
+
override get configurationUpdateFields(): readonly string[] {
|
|
16
|
+
return ['connection', 'url', 'path', 'api_url', 'access_token', 'reconnect_interval', 'heartbeat_interval'];
|
|
17
|
+
}
|
|
18
|
+
protected override validateConfiguration(config: OneBot12EndpointConfig): void { resolveOneBot12Config(config); }
|
|
19
|
+
protected override async identifyConfiguration(input: OneBot12EndpointConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
20
|
+
const config = resolveOneBot12Config(input);
|
|
21
|
+
if (config.connection === 'ws') return probeOnebot12Identity(config, signal);
|
|
22
|
+
if (config.connection === 'webhook') return probeOnebot12HttpIdentity(config, signal);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
protected createEndpoint(input: OneBot12EndpointConfig, execution: EndpointCreationContext): OneBot12WsEndpoint | OneBot12WssEndpoint | OneBot12WebhookEndpoint {
|
|
26
|
+
const config = resolveOneBot12Config(input);
|
|
27
|
+
const id = execution.canonicalId;
|
|
28
|
+
if (config.connection === 'webhook') return new OneBot12WebhookEndpoint({ id, signal: execution.signal, http: this.#context.use(httpHostToken), config });
|
|
29
|
+
if (config.connection === 'wss') return new OneBot12WssEndpoint({ id, signal: execution.signal, http: this.#context.use(httpHostToken), config });
|
|
30
|
+
return new OneBot12WsEndpoint({ id, signal: execution.signal, config });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '@zhin.js/feature-kit' {
|
|
35
|
+
interface AdapterRegistry { readonly onebot12: Onebot12Adapter; }
|
|
36
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { readOnebot12Response } from './response.js';
|
|
2
|
+
export { readOnebot12Response } from './response.js';
|
|
3
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
1
4
|
import {
|
|
2
5
|
OneBotV12Client,
|
|
3
6
|
type OneBotV12Event as ImHelperOneBot12Event,
|
|
4
7
|
type OneBotV12Response,
|
|
5
8
|
} from '@imhelper/onebot-v12';
|
|
6
|
-
import { EventFactory, type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
9
|
+
import { ProtocolError, EventFactory, type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
7
10
|
import {
|
|
8
|
-
defineEndpointClient,
|
|
9
11
|
forwardEndpointClientEvents,
|
|
10
12
|
type ClientEventPayloads,
|
|
11
13
|
} from 'zhin.js/adapter';
|
|
@@ -25,38 +27,44 @@ type Onebot12ClientEvents = ImHelperEventMap<
|
|
|
25
27
|
>;
|
|
26
28
|
export type Onebot12ClientEventMap = ClientEventPayloads<Onebot12ClientEvents>;
|
|
27
29
|
|
|
28
|
-
export function
|
|
30
|
+
export function onebot12ClientOptions(
|
|
29
31
|
config: ResolvedOneBot12Config,
|
|
30
32
|
request: Onebot12ApiCall,
|
|
31
|
-
): OneBotV12Client {
|
|
33
|
+
): ConstructorParameters<typeof OneBotV12Client>[0] {
|
|
32
34
|
const baseUrl = config.connection === 'ws'
|
|
33
35
|
? config.url.replace(/^ws(s?):/, 'http$1:')
|
|
34
36
|
: config.connection === 'webhook'
|
|
35
37
|
? config.api_url
|
|
36
38
|
: 'http://localhost';
|
|
37
|
-
return
|
|
39
|
+
return {
|
|
38
40
|
baseUrl,
|
|
39
|
-
|
|
41
|
+
platform: config.platform,
|
|
42
|
+
selfId: config.userId,
|
|
40
43
|
accessToken: config.access_token,
|
|
41
44
|
receiveMode: 'manual',
|
|
42
45
|
call: request,
|
|
43
|
-
}
|
|
46
|
+
};
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export
|
|
47
|
-
client: OneBotV12Client
|
|
48
|
-
action: string
|
|
49
|
-
params?: Record<string, unknown
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
export interface Onebot12ClientRequest {
|
|
50
|
+
readonly client: OneBotV12Client;
|
|
51
|
+
readonly action: string;
|
|
52
|
+
readonly params?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
/** Native methods retain ProtocolError; framework ports preserve the underlying delivery evidence. */
|
|
55
|
+
export async function callOnebot12Client<T = unknown>(request: Onebot12ClientRequest): Promise<T | undefined> {
|
|
56
|
+
try {
|
|
57
|
+
return readOnebot12Response<T>(await request.client.call<T>(request.action, request.params));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof ProtocolError) {
|
|
60
|
+
if (error.cause instanceof EndpointDeliveryError) throw error.cause;
|
|
61
|
+
if (error.response !== undefined) readOnebot12Response(error.response);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
56
64
|
}
|
|
57
|
-
return response.data;
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
|
|
60
68
|
const onebot12ClientEventNames = Object.freeze([
|
|
61
69
|
...EventFactory.getSupportedEventTypes<string>(),
|
|
62
70
|
'event',
|
|
@@ -68,14 +76,3 @@ export function forwardOnebot12ClientEvents(
|
|
|
68
76
|
): () => void {
|
|
69
77
|
return forwardEndpointClientEvents(client, onebot12ClientEventNames, receive);
|
|
70
78
|
}
|
|
71
|
-
|
|
72
|
-
declare module '@zhin.js/feature-kit' {
|
|
73
|
-
interface AdapterClientRegistry {
|
|
74
|
-
readonly onebot12: {
|
|
75
|
-
readonly client: OneBotV12Client;
|
|
76
|
-
readonly events: Onebot12ClientEventMap;
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const onebot12Client = defineEndpointClient<OneBotV12Client, Onebot12ClientEventMap>('onebot12');
|
package/src/console-spec.ts
CHANGED
|
@@ -7,6 +7,8 @@ export const onebot12ConsoleSpec = {
|
|
|
7
7
|
adapterKey: 'onebot12',
|
|
8
8
|
adapterDisplayName: 'OneBot 12',
|
|
9
9
|
fields: [
|
|
10
|
+
{ key: 'platform', required: true, description: 'Selected OneBot12 bot platform' },
|
|
11
|
+
{ key: 'userId', required: true, description: 'Selected OneBot12 bot user ID' },
|
|
10
12
|
{ key: 'url', description: 'OneBot 实现 WebSocket URL(connection: ws 必填)', descriptionI18n: { en: 'OneBot WebSocket URL (required for connection: ws)' } },
|
|
11
13
|
{ key: 'path', description: 'HTTP/WS 路径(webhook / reverse-wss)', descriptionI18n: { en: 'HTTP/WS path (webhook / reverse-wss)' } },
|
|
12
14
|
{ key: 'api_url', description: 'HTTP action 端点(connection: webhook 出站必填)', descriptionI18n: { en: 'HTTP action endpoint (required for outbound connection: webhook)' } },
|
package/src/console.ts
CHANGED
|
@@ -5,17 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createAdapterConsoleRpc, type AdapterConsoleResources } from 'zhin.js/adapter';
|
|
7
7
|
import type { PluginConsoleRpc } from 'zhin.js';
|
|
8
|
-
import { onebot12RuntimeStateToken } from './onebot12-runtime-state.js';
|
|
9
8
|
import { onebot12ConsoleSpec } from './console-spec.js';
|
|
10
9
|
|
|
11
10
|
export function createOnebot12ConsoleRpc(
|
|
12
|
-
|
|
11
|
+
_resources: AdapterConsoleResources,
|
|
13
12
|
getConfig: () => unknown,
|
|
14
13
|
): PluginConsoleRpc {
|
|
15
|
-
const state = resources.use(onebot12RuntimeStateToken);
|
|
16
14
|
return createAdapterConsoleRpc({
|
|
17
15
|
...onebot12ConsoleSpec,
|
|
18
|
-
running: () => state.endpoints.values(),
|
|
19
16
|
getConfig,
|
|
20
|
-
}
|
|
17
|
+
});
|
|
21
18
|
}
|
package/src/content-port.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { EndpointContentPort, EndpointContentResolveContext } from 'zhin.js/adapter';
|
|
1
|
+
import type { EndpointContentPort, EndpointContentResolveContext, PendingRpcRequest } from 'zhin.js/adapter';
|
|
2
2
|
import type { ConversationMessage, ConversationReference } from '@zhin.js/im-contract';
|
|
3
3
|
|
|
4
|
-
type CallApi = (
|
|
4
|
+
type CallApi = (request: PendingRpcRequest) => Promise<unknown>;
|
|
5
5
|
|
|
6
6
|
export function createOneBot12ContentPort(callApi: CallApi): EndpointContentPort {
|
|
7
7
|
return Object.freeze({
|
|
@@ -16,7 +16,7 @@ export function createOneBot12ContentPort(callApi: CallApi): EndpointContentPort
|
|
|
16
16
|
? Object.freeze({ status: 'unsupported' as const, code: 'onebot12_opaque_media' })
|
|
17
17
|
: Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
|
|
18
18
|
}
|
|
19
|
-
const row = await callApi('get_message', { message_id: reference.message.id }) as Record<string, unknown>;
|
|
19
|
+
const row = await callApi({ action: 'get_message', params: { message_id: reference.message.id }, signal: context.signal }) as Record<string, unknown>;
|
|
20
20
|
context.signal.throwIfAborted();
|
|
21
21
|
const message = (row.message && typeof row.message === 'object' ? row.message : row) as Record<string, unknown>;
|
|
22
22
|
const segments = normalizeSegments(message.message ?? message.content);
|
|
@@ -3,51 +3,54 @@
|
|
|
3
3
|
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
4
|
*
|
|
5
5
|
* 动作名对齐 OB12 规范:get_friend_list / get_group_list / get_group_member_list。
|
|
6
|
-
*
|
|
7
|
-
* - friend → {user_id:
|
|
8
|
-
* - group → {group_id:
|
|
6
|
+
* 投影仅归一化展示字段;OB12 账号与会话 ID 始终保留为不透明字符串:
|
|
7
|
+
* - friend → {user_id:string, nickname: user_name, remark: user_displayname ?? ''}
|
|
8
|
+
* - group → {group_id:string, name: group_name ?? name}
|
|
9
9
|
* - 群成员列表保持 OB12 原生形状,仅保证数组
|
|
10
10
|
*/
|
|
11
|
-
import
|
|
12
|
-
EndpointFriend,
|
|
13
|
-
EndpointGroup,
|
|
14
|
-
EndpointManagement,
|
|
11
|
+
import {
|
|
12
|
+
type EndpointFriend,
|
|
13
|
+
type EndpointGroup,
|
|
14
|
+
type EndpointManagement,
|
|
15
|
+
type EndpointManagementRequest,
|
|
16
|
+
type EndpointGroupRequest,
|
|
17
|
+
type PendingRpcRequest,
|
|
15
18
|
} from 'zhin.js/adapter';
|
|
16
19
|
|
|
17
20
|
/** 管理面只依赖 endpoint 的 callApi(ws/wss/webhook 三种传输各自实现)。 */
|
|
18
21
|
export interface OneBot12ManagementCaller {
|
|
19
|
-
callApi(
|
|
22
|
+
callApi(request: PendingRpcRequest): Promise<unknown>;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function createOneBot12EndpointManagement(
|
|
23
26
|
endpoint: OneBot12ManagementCaller,
|
|
24
27
|
): EndpointManagement {
|
|
25
28
|
return Object.freeze<EndpointManagement>({
|
|
26
|
-
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
27
|
-
const data = await endpoint.callApi('get_friend_list');
|
|
29
|
+
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
30
|
+
const data = await endpoint.callApi({ action: 'get_friend_list', signal: request.signal });
|
|
28
31
|
return toArray(data).map((value) => {
|
|
29
32
|
const friend = asRecord(value);
|
|
30
33
|
return {
|
|
31
|
-
user_id:
|
|
34
|
+
user_id: readOpaqueId(friend.user_id, 'user_id'),
|
|
32
35
|
nickname: String(friend.user_name ?? friend.nickname ?? ''),
|
|
33
36
|
remark: String(friend.user_displayname ?? friend.remark ?? ''),
|
|
34
37
|
};
|
|
35
38
|
});
|
|
36
39
|
},
|
|
37
|
-
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
38
|
-
const data = await endpoint.callApi('get_group_list');
|
|
40
|
+
async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
|
|
41
|
+
const data = await endpoint.callApi({ action: 'get_group_list', signal: request.signal });
|
|
39
42
|
return toArray(data).map((value) => {
|
|
40
43
|
const group = asRecord(value);
|
|
41
44
|
return {
|
|
42
|
-
group_id:
|
|
45
|
+
group_id: readOpaqueId(group.group_id, 'group_id'),
|
|
43
46
|
name: String(group.group_name ?? group.name ?? ''),
|
|
44
47
|
};
|
|
45
48
|
});
|
|
46
49
|
},
|
|
47
|
-
async listGroupMembers(groupId:
|
|
48
|
-
const data = await endpoint.callApi('get_group_member_list', {
|
|
50
|
+
async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
|
|
51
|
+
const data = await endpoint.callApi({ action: 'get_group_member_list', signal, params: {
|
|
49
52
|
group_id: groupId,
|
|
50
|
-
});
|
|
53
|
+
} });
|
|
51
54
|
return toArray(data);
|
|
52
55
|
},
|
|
53
56
|
});
|
|
@@ -63,11 +66,9 @@ function toArray(value: unknown): readonly unknown[] {
|
|
|
63
66
|
return Array.isArray(value) ? value : [];
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
/**
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
return n;
|
|
69
|
+
/** Platform IDs are addresses, including nonnumeric IDs and integers beyond Number precision. */
|
|
70
|
+
function readOpaqueId(value: unknown, label: string): string {
|
|
71
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
72
|
+
if (typeof value === 'number' && Number.isSafeInteger(value)) return String(value);
|
|
73
|
+
throw new TypeError(`onebot12 ${label} must be an opaque string or safe integer platform ID`);
|
|
73
74
|
}
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { buildWsConnectOptions, callOneBot12Action, type OneBot12ActionResponse, type OneBot12WebhookConfig, type OneBot12WsConfig } from './protocol.js';
|
|
3
|
+
import { createOneBot12Rpc } from './ws-transport.js';
|
|
4
|
+
|
|
5
|
+
/** Separate read-only connection: never changes credentials or state of the running Endpoint. */
|
|
6
|
+
export async function probeOnebot12Identity(config: OneBot12WsConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
7
|
+
signal.throwIfAborted();
|
|
8
|
+
const { url, headers } = buildWsConnectOptions(config);
|
|
9
|
+
const socket = new WebSocket(url, { headers });
|
|
10
|
+
const rpc = createOneBot12Rpc({ endpointId: config.id, signal, socket: () => socket, self: { platform: config.platform, user_id: config.userId }, });
|
|
11
|
+
let rejectOpen: (error: unknown) => void = () => undefined;
|
|
12
|
+
const abort = () => { rejectOpen(signal.reason); rpc.disconnect(); socket.terminate(); };
|
|
13
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
14
|
+
socket.on('message', data => rpc.receive(data, () => undefined));
|
|
15
|
+
socket.on('close', () => { rejectOpen(new Error('Identity connection closed')); rpc.disconnect(); });
|
|
16
|
+
try {
|
|
17
|
+
await new Promise<void>((resolve, reject) => { rejectOpen = reject; socket.once('open', resolve); socket.on('error', reject); });
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
const self = { platform: config.platform, user_id: config.userId };
|
|
20
|
+
return await identifySelectedBot({ selected: self, query: action => rpc.call({ action }), signal });
|
|
21
|
+
} finally {
|
|
22
|
+
signal.removeEventListener('abort', abort); rpc.dispose(); socket.terminate();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function probeOnebot12HttpIdentity(config: OneBot12WebhookConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
27
|
+
const self = { platform: config.platform, user_id: config.userId };
|
|
28
|
+
return identifySelectedBot({ selected: self, query: action => callOneBot12Action({ url: config.api_url, access_token: config.access_token, signal, self, action }), signal });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface OneBot12IdentityProbe {
|
|
32
|
+
readonly selected: Readonly<{ platform: string; user_id: string }>;
|
|
33
|
+
readonly query: (action: string) => Promise<OneBot12ActionResponse>;
|
|
34
|
+
readonly signal: AbortSignal;
|
|
35
|
+
}
|
|
36
|
+
async function identifySelectedBot({ selected, query, signal }: OneBot12IdentityProbe): Promise<string | undefined> {
|
|
37
|
+
const status = await query('get_status');
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
if (status.status !== 'ok' || status.retcode !== 0) throw new Error('OneBot12 identity status lookup failed');
|
|
40
|
+
const data = status.data;
|
|
41
|
+
if (!data || typeof data !== 'object' || !('bots' in data) || !Array.isArray(data.bots)) return undefined;
|
|
42
|
+
const match = data.bots.some((bot: unknown) => {
|
|
43
|
+
if (!bot || typeof bot !== 'object' || !('self' in bot) || !bot.self || typeof bot.self !== 'object') return false;
|
|
44
|
+
return 'platform' in bot.self && 'user_id' in bot.self && bot.self.platform === selected.platform && bot.self.user_id === selected.user_id;
|
|
45
|
+
});
|
|
46
|
+
if (!match) return undefined;
|
|
47
|
+
const account = await query('get_self_info');
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
if (account.status !== 'ok' || account.retcode !== 0) throw new Error('OneBot12 account identity lookup failed');
|
|
50
|
+
if (!account.data || typeof account.data !== 'object' || !('user_id' in account.data) || account.data.user_id !== selected.user_id) return undefined;
|
|
51
|
+
return `onebot12:${JSON.stringify([selected.platform, selected.user_id])}`;
|
|
52
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,9 @@ export {
|
|
|
21
21
|
type OneBot12EndpointConfig,
|
|
22
22
|
type OneBot12Event,
|
|
23
23
|
type OneBot12HttpOptions,
|
|
24
|
+
type OneBot12HttpRequest,
|
|
25
|
+
type OneBot12MediaUpload,
|
|
26
|
+
type OneBot12MediaSource,
|
|
24
27
|
type OneBot12Segment,
|
|
25
28
|
type OneBot12Self,
|
|
26
29
|
type OneBot12WebhookConfig,
|
|
@@ -50,7 +53,6 @@ export type { OneBot12WsSocket, OneBot12WsCreateOptions } from './ws-types.js';
|
|
|
50
53
|
export { verifyOneBotAccessToken } from './wss-auth.js';
|
|
51
54
|
export {
|
|
52
55
|
Onebot12Client,
|
|
53
|
-
onebot12Client,
|
|
54
56
|
type Onebot12ClientEventMap,
|
|
55
57
|
} from './client.js';
|
|
56
58
|
|
|
@@ -64,3 +66,4 @@ export type {
|
|
|
64
66
|
} from '@imhelper/onebot-v12';
|
|
65
67
|
export { ProtocolError } from '@imhelper/onebot-v12';
|
|
66
68
|
export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
|
|
69
|
+
export { Onebot12Adapter } from './adapter.js';
|
package/src/protocol.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { readOnebot12Response } from './response.js';
|
|
1
2
|
/**
|
|
2
3
|
* OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
5
|
* Spec: https://12.onebot.dev/
|
|
5
6
|
*/
|
|
6
|
-
import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
9
|
|
|
9
10
|
const logger = getLogger('onebot12');
|
|
10
11
|
|
|
11
12
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
12
13
|
export interface OneBot12EndpointConfig {
|
|
14
|
+
readonly platform: string;
|
|
15
|
+
readonly userId: string;
|
|
13
16
|
readonly connection?: 'ws' | 'webhook' | 'wss';
|
|
14
17
|
readonly id: string;
|
|
15
18
|
readonly access_token?: string;
|
|
@@ -21,6 +24,8 @@ export interface OneBot12EndpointConfig {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export interface OneBot12ConfigBase {
|
|
27
|
+
readonly platform: string;
|
|
28
|
+
readonly userId: string;
|
|
24
29
|
readonly context: 'onebot12';
|
|
25
30
|
readonly id: string;
|
|
26
31
|
readonly access_token?: string;
|
|
@@ -78,10 +83,10 @@ export interface OneBot12Segment {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
export interface OneBot12ActionRequest {
|
|
86
|
+
readonly self: OneBot12Self;
|
|
81
87
|
action: string;
|
|
82
88
|
params: Record<string, unknown>;
|
|
83
89
|
echo?: string;
|
|
84
|
-
self?: OneBot12Self;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
export interface OneBot12ActionResponse {
|
|
@@ -93,6 +98,8 @@ export interface OneBot12ActionResponse {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export interface OneBot12HttpOptions {
|
|
101
|
+
readonly self: OneBot12Self;
|
|
102
|
+
readonly signal?: AbortSignal;
|
|
96
103
|
readonly url: string;
|
|
97
104
|
readonly access_token?: string;
|
|
98
105
|
}
|
|
@@ -105,12 +112,14 @@ export interface OneBot12WireSegment {
|
|
|
105
112
|
export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedOneBot12Config {
|
|
106
113
|
const connection = config.connection ?? 'ws';
|
|
107
114
|
const id = requiredEndpointField(config.id, 'id');
|
|
115
|
+
const platform = requiredEndpointField(config.platform, 'platform');
|
|
116
|
+
const userId = requiredEndpointField(config.userId, 'userId');
|
|
108
117
|
const access_token = optionalEndpointField(config.access_token);
|
|
109
118
|
|
|
110
119
|
if (connection === 'ws') {
|
|
111
120
|
const url = requiredEndpointField(config.url, 'url');
|
|
112
121
|
return {
|
|
113
|
-
context: 'onebot12',
|
|
122
|
+
context: 'onebot12', platform, userId,
|
|
114
123
|
connection: 'ws',
|
|
115
124
|
id,
|
|
116
125
|
access_token,
|
|
@@ -124,7 +133,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
124
133
|
const path = requiredEndpointField(config.path, 'path');
|
|
125
134
|
const api_url = requiredEndpointField(config.api_url, 'api_url');
|
|
126
135
|
return {
|
|
127
|
-
context: 'onebot12',
|
|
136
|
+
context: 'onebot12', platform, userId,
|
|
128
137
|
connection: 'webhook',
|
|
129
138
|
id,
|
|
130
139
|
access_token,
|
|
@@ -136,7 +145,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
136
145
|
if (connection === 'wss') {
|
|
137
146
|
const path = requiredEndpointField(config.path, 'path');
|
|
138
147
|
return {
|
|
139
|
-
context: 'onebot12',
|
|
148
|
+
context: 'onebot12', platform, userId,
|
|
140
149
|
connection: 'wss',
|
|
141
150
|
id,
|
|
142
151
|
access_token,
|
|
@@ -150,7 +159,7 @@ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedO
|
|
|
150
159
|
|
|
151
160
|
function requiredEndpointField(
|
|
152
161
|
value: unknown,
|
|
153
|
-
field: 'id' | 'url' | 'path' | 'api_url',
|
|
162
|
+
field: 'id' | 'url' | 'path' | 'api_url' | 'platform' | 'userId',
|
|
154
163
|
): string {
|
|
155
164
|
const resolved = optionalEndpointField(value);
|
|
156
165
|
if (!resolved) {
|
|
@@ -398,11 +407,12 @@ export type OneBot12CallAction = (
|
|
|
398
407
|
) => Promise<unknown>;
|
|
399
408
|
|
|
400
409
|
/** upload_file 文件名:优先段 data.name/filename,URL/路径取 basename,再按 mime 给默认名。 */
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
data: Record<string, unknown
|
|
404
|
-
media: MediaRef
|
|
405
|
-
|
|
410
|
+
export interface OneBot12MediaSource {
|
|
411
|
+
readonly type: string;
|
|
412
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
413
|
+
readonly media: MediaRef;
|
|
414
|
+
}
|
|
415
|
+
function oneBot12UploadName({ type, data, media }: OneBot12MediaSource): string {
|
|
406
416
|
const named = data.name ?? data.filename;
|
|
407
417
|
if (typeof named === 'string' && named) return named;
|
|
408
418
|
if (media.kind === 'url') {
|
|
@@ -418,16 +428,13 @@ function oneBot12UploadName(
|
|
|
418
428
|
if (base) return base;
|
|
419
429
|
}
|
|
420
430
|
const ext = media.mime_type?.split('/')[1]?.split(';')[0];
|
|
421
|
-
return `${
|
|
431
|
+
return `${type}.${ext || 'bin'}`;
|
|
422
432
|
}
|
|
423
433
|
|
|
424
434
|
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
425
|
-
export function mediaRefToOneBot12UploadParams(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
media: MediaRef,
|
|
429
|
-
): Record<string, unknown> | undefined {
|
|
430
|
-
const name = oneBot12UploadName(segmentType, data, media);
|
|
435
|
+
export function mediaRefToOneBot12UploadParams(source: OneBot12MediaSource): Record<string, unknown> | undefined {
|
|
436
|
+
const { media } = source;
|
|
437
|
+
const name = oneBot12UploadName(source);
|
|
431
438
|
if (media.kind === 'url') return { type: 'url', name, url: media.value };
|
|
432
439
|
if (media.kind === 'path') {
|
|
433
440
|
const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
|
|
@@ -444,8 +451,7 @@ export function mediaRefToOneBot12UploadParams(
|
|
|
444
451
|
|
|
445
452
|
async function uploadOneMediaSegment(
|
|
446
453
|
item: unknown,
|
|
447
|
-
|
|
448
|
-
onUploadFailed?: (error: unknown) => void,
|
|
454
|
+
request: OneBot12MediaUpload,
|
|
449
455
|
): Promise<unknown> {
|
|
450
456
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
|
|
451
457
|
const segment = item as OneBot12WireSegment;
|
|
@@ -460,17 +466,18 @@ async function uploadOneMediaSegment(
|
|
|
460
466
|
if (media.kind === 'file') {
|
|
461
467
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: media.value } };
|
|
462
468
|
}
|
|
463
|
-
const params = mediaRefToOneBot12UploadParams(segment.type, data, media);
|
|
469
|
+
const params = mediaRefToOneBot12UploadParams({ type: segment.type, data, media });
|
|
464
470
|
if (!params) return item;
|
|
465
471
|
try {
|
|
466
|
-
const result = await callAction('upload_file', params) as { file_id?: unknown } | undefined;
|
|
472
|
+
const result = await request.callAction('upload_file', params) as { file_id?: unknown } | undefined;
|
|
467
473
|
if (result && typeof result.file_id === 'string' && result.file_id) {
|
|
468
474
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: result.file_id } };
|
|
469
475
|
}
|
|
470
476
|
throw new Error('upload_file 响应缺少 file_id');
|
|
471
477
|
} catch (error) {
|
|
478
|
+
if (request.signal?.aborted) throw error;
|
|
472
479
|
// 上传失败降级:保留原段,由 formatOutboundSegments 走扩展字段(url/data/path)
|
|
473
|
-
onUploadFailed?.(error);
|
|
480
|
+
request.onUploadFailed?.(error);
|
|
474
481
|
return item;
|
|
475
482
|
}
|
|
476
483
|
}
|
|
@@ -481,15 +488,17 @@ async function uploadOneMediaSegment(
|
|
|
481
488
|
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
482
489
|
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
483
490
|
*/
|
|
484
|
-
export
|
|
485
|
-
payload: unknown
|
|
486
|
-
callAction: OneBot12CallAction
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
+
export interface OneBot12MediaUpload {
|
|
492
|
+
readonly payload: unknown;
|
|
493
|
+
readonly callAction: OneBot12CallAction;
|
|
494
|
+
readonly signal?: AbortSignal;
|
|
495
|
+
readonly onUploadFailed?: (error: unknown) => void;
|
|
496
|
+
}
|
|
497
|
+
export async function uploadOneBot12MediaSegments(request: OneBot12MediaUpload): Promise<unknown> {
|
|
498
|
+
if (Array.isArray(request.payload)) {
|
|
499
|
+
return Promise.all(request.payload.map(item => uploadOneMediaSegment(item, request)));
|
|
491
500
|
}
|
|
492
|
-
return uploadOneMediaSegment(payload,
|
|
501
|
+
return uploadOneMediaSegment(request.payload, request);
|
|
493
502
|
}
|
|
494
503
|
|
|
495
504
|
/**
|
|
@@ -519,40 +528,34 @@ export function buildSendMessageParams(
|
|
|
519
528
|
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
|
520
529
|
* 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
|
|
521
530
|
*/
|
|
522
|
-
export
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
531
|
+
export interface OneBot12HttpRequest extends OneBot12HttpOptions {
|
|
532
|
+
readonly action: string;
|
|
533
|
+
readonly params?: Record<string, unknown>;
|
|
534
|
+
readonly echo?: string;
|
|
535
|
+
}
|
|
536
|
+
/** HTTP owns submission evidence and cancellation for both SDK and framework callers. */
|
|
537
|
+
export async function callOneBot12Action(request: OneBot12HttpRequest): Promise<OneBot12ActionResponse> {
|
|
538
|
+
if (request.signal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint operation cancelled', 'not_sent');
|
|
528
539
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const body: OneBot12ActionRequest = { action, params };
|
|
533
|
-
if (echo) body.echo = echo;
|
|
534
|
-
|
|
535
|
-
const res = await fetch(options.url, {
|
|
536
|
-
method: 'POST',
|
|
537
|
-
headers,
|
|
538
|
-
body: JSON.stringify(body),
|
|
539
|
-
signal: AbortSignal.timeout(30_000),
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
const text = await res.text();
|
|
543
|
-
if (res.status === 401) throw new Error(`OneBot12 鉴权失败: ${text}`);
|
|
544
|
-
if (res.status !== 200) throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
|
|
545
|
-
|
|
546
|
-
let data: OneBot12ActionResponse;
|
|
540
|
+
if (request.access_token) headers.Authorization = `Bearer ${request.access_token}`;
|
|
541
|
+
const body: OneBot12ActionRequest = { action: request.action, params: request.params ?? {}, self: request.self,
|
|
542
|
+
...(request.echo !== undefined ? { echo: request.echo } : {}) };
|
|
547
543
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
544
|
+
const response = await fetch(request.url, { method: 'POST', headers, body: JSON.stringify(body),
|
|
545
|
+
signal: request.signal ? AbortSignal.any([request.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
|
|
546
|
+
const text = await response.text();
|
|
547
|
+
if (response.status === 401) throw new EndpointDeliveryError('platform_rejected', `OneBot12 鉴权失败: ${text}`, 'rejected');
|
|
548
|
+
if (response.status !== 200) throw new EndpointDeliveryError('endpoint_http_failed', `OneBot12 HTTP ${response.status}: ${text}`, 'unknown');
|
|
549
|
+
let data: OneBot12ActionResponse;
|
|
550
|
+
try { data = JSON.parse(text) as OneBot12ActionResponse; }
|
|
551
|
+
catch { throw new EndpointDeliveryError('delivery_unconfirmed', `OneBot12 无效响应: ${text.slice(0, 200)}`, 'unknown'); }
|
|
552
|
+
readOnebot12Response(data);
|
|
553
|
+
return data;
|
|
554
|
+
} catch (cause) {
|
|
555
|
+
if (cause instanceof EndpointDeliveryError) throw cause;
|
|
556
|
+
throw new EndpointDeliveryError(request.signal?.aborted ? 'endpoint_inactive' : 'endpoint_transport_failed',
|
|
557
|
+
cause instanceof Error ? cause.message : String(cause), 'unknown', { cause });
|
|
554
558
|
}
|
|
555
|
-
return data;
|
|
556
559
|
}
|
|
557
560
|
|
|
558
561
|
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
@@ -573,3 +576,9 @@ export function buildWsConnectOptions(config: OneBot12WsConfig): {
|
|
|
573
576
|
safeUrl.searchParams.delete('access_token');
|
|
574
577
|
return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
|
|
575
578
|
}
|
|
579
|
+
|
|
580
|
+
/** Route only the explicitly selected bot; service-wide frames carry no account authority. */
|
|
581
|
+
export function matchesOneBot12Self(config: Pick<OneBot12ConfigBase, 'platform' | 'userId'>,
|
|
582
|
+
event: OneBot12Event): event is OneBot12Event & { readonly self: OneBot12Self } {
|
|
583
|
+
return event.self?.platform === config.platform && event.self?.user_id === config.userId;
|
|
584
|
+
}
|