@zhin.js/adapter-napcat 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 +46 -0
- package/README.md +8 -5
- package/adapters/napcat/index.js +2 -29
- package/adapters/napcat/index.ts +3 -35
- package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +1 -1
- package/agents/napcat/skills/napcat-account/tools/set_profile/index.ts +1 -1
- package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.js +1 -1
- package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.ts +1 -1
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +1 -1
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +1 -1
- package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +1 -1
- package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts +1 -1
- package/commands/napcat/endpoint/definition.js +0 -2
- package/commands/napcat/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +23 -0
- package/lib/adapter.js +42 -0
- package/lib/client.d.ts +53 -16
- package/lib/client.js +53 -10
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint-management.d.ts +2 -2
- package/lib/endpoint-management.js +13 -13
- package/lib/http-endpoint.d.ts +5 -15
- package/lib/http-endpoint.js +44 -36
- package/lib/identity.d.ts +3 -0
- package/lib/identity.js +32 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -2
- package/lib/onebot-get-msg.d.ts +2 -2
- package/lib/onebot-get-msg.js +2 -2
- package/lib/protocol.d.ts +5 -0
- package/lib/protocol.js +23 -11
- package/lib/ws-endpoint.d.ts +6 -16
- package/lib/ws-endpoint.js +57 -48
- package/lib/ws-transport.d.ts +18 -12
- package/lib/ws-transport.js +28 -72
- package/lib/ws-types.d.ts +0 -5
- package/lib/wss-endpoint.d.ts +7 -15
- package/lib/wss-endpoint.js +56 -47
- package/package.json +18 -18
- package/plugin.js +0 -3
- package/src/adapter.ts +45 -0
- package/src/client.ts +91 -19
- package/src/console.ts +2 -5
- package/src/endpoint-management.ts +19 -15
- package/src/http-endpoint.ts +47 -45
- package/src/identity.ts +29 -0
- package/src/index.ts +9 -5
- package/src/onebot-get-msg.ts +4 -4
- package/src/protocol.ts +26 -14
- package/src/ws-endpoint.ts +61 -54
- package/src/ws-transport.ts +40 -101
- package/src/ws-types.ts +0 -6
- package/src/wss-endpoint.ts +60 -54
- package/lib/napcat-runtime-state.d.ts +0 -1
- package/lib/napcat-runtime-state.js +0 -6
- package/src/napcat-runtime-state.ts +0 -7
package/lib/ws-endpoint.js
CHANGED
|
@@ -3,32 +3,42 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
3
3
|
* NapCat WS client endpoint — outbound connect to NapCat.
|
|
4
4
|
*/
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
|
-
import {
|
|
6
|
+
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
7
7
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
8
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
9
9
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
10
10
|
import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
|
|
11
11
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
12
|
-
import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, } from './ws-transport.js';
|
|
13
12
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
14
13
|
import { NapcatClient } from './client.js';
|
|
15
|
-
export class NapCatWsEndpoint extends
|
|
16
|
-
|
|
14
|
+
export class NapCatWsEndpoint extends NapcatClient {
|
|
15
|
+
[endpointSpec];
|
|
17
16
|
#logger;
|
|
18
17
|
#options;
|
|
19
18
|
#inboundDeduper = new InboundMessageDeduper();
|
|
20
|
-
management = createNapCatEndpointManagement(this
|
|
21
|
-
control = createRecallEndpointControl((id) => this
|
|
22
|
-
content = createNapCatContentPort(
|
|
19
|
+
#management = createNapCatEndpointManagement(this);
|
|
20
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
|
|
21
|
+
#content = createNapCatContentPort(request => this.requestApi(request));
|
|
23
22
|
#lifecycle;
|
|
24
|
-
#ws;
|
|
25
|
-
#requestId = { value: 0 };
|
|
26
|
-
#pending = new Map();
|
|
27
23
|
#open = false;
|
|
28
24
|
constructor(options) {
|
|
29
|
-
super();
|
|
25
|
+
super({ config: options.config, signal: options.signal });
|
|
30
26
|
this.#logger = getAdapterLogger('napcat', options.config.id);
|
|
31
27
|
this.#options = options;
|
|
28
|
+
const readTransportState = () => this.#transportState;
|
|
29
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
30
|
+
name: options.config.id,
|
|
31
|
+
start: () => this.#start(), open: () => this.#openEndpoint(),
|
|
32
|
+
close: () => this.#close(), stop: () => this.#stop(),
|
|
33
|
+
send: request => this.#send(request),
|
|
34
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
35
|
+
get transportState() { return readTransportState(); },
|
|
36
|
+
});
|
|
37
|
+
this.on('event', event => {
|
|
38
|
+
void this[endpointSpec].events.run(() => this.#admit(event)).catch((error) => {
|
|
39
|
+
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
32
42
|
this.#lifecycle = createEndpointLifecycle({
|
|
33
43
|
name: options.config.id,
|
|
34
44
|
heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
|
|
@@ -41,39 +51,42 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
41
51
|
},
|
|
42
52
|
});
|
|
43
53
|
}
|
|
44
|
-
get transportState() {
|
|
54
|
+
get #transportState() {
|
|
45
55
|
return this.#lifecycle.state;
|
|
46
56
|
}
|
|
47
|
-
|
|
57
|
+
get actionSocket() {
|
|
58
|
+
return this.#lifecycle.state === 'open' ? this.transportSocket : undefined;
|
|
59
|
+
}
|
|
60
|
+
async #start() {
|
|
48
61
|
if (this.#lifecycle.started)
|
|
49
62
|
return;
|
|
50
63
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
51
64
|
}
|
|
52
|
-
|
|
65
|
+
#openEndpoint() {
|
|
53
66
|
this.#open = true;
|
|
54
67
|
}
|
|
55
|
-
close() {
|
|
68
|
+
#close() {
|
|
56
69
|
this.#open = false;
|
|
57
70
|
}
|
|
58
|
-
async stop() {
|
|
71
|
+
async #stop() {
|
|
59
72
|
this.#open = false;
|
|
60
73
|
await this.#lifecycle.stop();
|
|
61
|
-
|
|
74
|
+
this.rpc.disconnect();
|
|
62
75
|
this.#inboundDeduper.clear();
|
|
63
|
-
if (this
|
|
76
|
+
if (this.transportSocket) {
|
|
64
77
|
try {
|
|
65
|
-
this
|
|
78
|
+
this.transportSocket.close();
|
|
66
79
|
}
|
|
67
80
|
catch {
|
|
68
81
|
/* ignore */
|
|
69
82
|
}
|
|
70
|
-
this
|
|
83
|
+
this.transportSocket = undefined;
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
|
-
async send({ conversation, payload }) {
|
|
86
|
+
async #send({ conversation, payload, signal }) {
|
|
74
87
|
const message = formatOutboundSegments(payload);
|
|
75
88
|
const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
|
|
76
|
-
const data = await this.
|
|
89
|
+
const data = await this.requestApi({ action, params, signal });
|
|
77
90
|
const rawId = data?.message_id;
|
|
78
91
|
if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
|
|
79
92
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
|
|
@@ -87,26 +100,24 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
87
100
|
}));
|
|
88
101
|
return messageId;
|
|
89
102
|
}
|
|
90
|
-
async recallMessage(messageId) {
|
|
103
|
+
async #recallMessage(messageId, signal) {
|
|
91
104
|
if (!messageId)
|
|
92
105
|
return;
|
|
93
|
-
await this.
|
|
94
|
-
}
|
|
95
|
-
#callApi(action, params = {}) {
|
|
96
|
-
return callNapCatWsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
|
|
106
|
+
await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
|
|
97
107
|
}
|
|
98
108
|
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
99
|
-
admit(ev) {
|
|
100
|
-
|
|
109
|
+
#admit(ev) {
|
|
110
|
+
const nativeEvent = ev;
|
|
111
|
+
if (!this.#open && !this.#lifecycle.started)
|
|
101
112
|
return;
|
|
102
|
-
void this.
|
|
113
|
+
void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
|
|
103
114
|
this.#logger.warn(formatCompact({
|
|
104
115
|
op: 'napcat_platform_event_failed',
|
|
105
116
|
error: error instanceof Error ? error.message : String(error),
|
|
106
117
|
}));
|
|
107
118
|
});
|
|
108
119
|
if (!isMessageEvent(ev)) {
|
|
109
|
-
receiveNapCatSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, this
|
|
120
|
+
receiveNapCatSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, this, ev, this.#logger);
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
112
123
|
if (isSelfMessage(ev))
|
|
@@ -117,11 +128,13 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
117
128
|
if (Array.isArray(ev.message) || typeof ev.message === 'string') {
|
|
118
129
|
ev = { ...ev, message: normalizeMessage(ev.message) };
|
|
119
130
|
}
|
|
120
|
-
const conversation = napcatInboundConversation(String(this.#options.id), ev);
|
|
131
|
+
const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
121
132
|
const content = formatInboundContent(ev);
|
|
122
133
|
const nickname = senderNickname(ev);
|
|
123
134
|
const mentioned = isNapCatBotMentioned(ev);
|
|
124
|
-
void this.
|
|
135
|
+
void this[endpointSpec].events.message({
|
|
136
|
+
event: nativeEvent,
|
|
137
|
+
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
125
138
|
conversation,
|
|
126
139
|
message: { conversation, id: msgId },
|
|
127
140
|
content,
|
|
@@ -156,10 +169,10 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
156
169
|
await new Promise((resolve, reject) => {
|
|
157
170
|
let settled = false;
|
|
158
171
|
const ws = create(url, { headers });
|
|
159
|
-
this
|
|
172
|
+
this.transportSocket = ws;
|
|
160
173
|
handle.onForceClose(() => {
|
|
161
|
-
if (this
|
|
162
|
-
|
|
174
|
+
if (this.transportSocket === ws) {
|
|
175
|
+
this.rpc.disconnect();
|
|
163
176
|
}
|
|
164
177
|
try {
|
|
165
178
|
ws.close();
|
|
@@ -169,7 +182,7 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
169
182
|
}
|
|
170
183
|
});
|
|
171
184
|
ws.on('open', () => {
|
|
172
|
-
if (settled || this
|
|
185
|
+
if (settled || this.transportSocket !== ws || !this.#lifecycle.started)
|
|
173
186
|
return;
|
|
174
187
|
settled = true;
|
|
175
188
|
if (!this.#options.config.access_token) {
|
|
@@ -198,24 +211,20 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
198
211
|
resolve();
|
|
199
212
|
});
|
|
200
213
|
ws.on('pong', () => {
|
|
201
|
-
if (this
|
|
214
|
+
if (this.transportSocket === ws)
|
|
202
215
|
this.#lifecycle.notifyHeartbeatAck();
|
|
203
216
|
});
|
|
204
217
|
ws.on('message', (data) => {
|
|
205
|
-
if (this
|
|
218
|
+
if (this.transportSocket !== ws || ws.readyState !== 1 || !this.#lifecycle.started)
|
|
206
219
|
return;
|
|
207
220
|
this.#lifecycle.notifyHeartbeatAck();
|
|
208
|
-
|
|
209
|
-
endpointId: this.#options.config.id,
|
|
210
|
-
pending: this.#pending,
|
|
211
|
-
admit: (event) => this.admit(event),
|
|
212
|
-
});
|
|
221
|
+
this.rpc.receive(data, event => this.ingest(event));
|
|
213
222
|
});
|
|
214
223
|
ws.on('close', (code, reason) => {
|
|
215
|
-
if (this
|
|
224
|
+
if (this.transportSocket !== ws)
|
|
216
225
|
return;
|
|
217
|
-
this
|
|
218
|
-
|
|
226
|
+
this.transportSocket = undefined;
|
|
227
|
+
this.rpc.disconnect();
|
|
219
228
|
const reasonStr = typeof reason === 'string'
|
|
220
229
|
? reason
|
|
221
230
|
: Buffer.isBuffer(reason)
|
|
@@ -241,7 +250,7 @@ export class NapCatWsEndpoint extends Endpoint {
|
|
|
241
250
|
}
|
|
242
251
|
});
|
|
243
252
|
ws.on('error', (err) => {
|
|
244
|
-
if (this
|
|
253
|
+
if (this.transportSocket !== ws)
|
|
245
254
|
return;
|
|
246
255
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
247
256
|
this.#logger.warn(formatCompact({
|
package/lib/ws-transport.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export
|
|
1
|
+
import { type PendingRpc, type PendingRpcRequest } from 'zhin.js/adapter';
|
|
2
|
+
import type { NapCatActionResponse, NapCatEvent } from './protocol.js';
|
|
3
|
+
import { type NapCatWsSocket } from './ws-types.js';
|
|
4
|
+
export interface NapCatRpcOptions {
|
|
5
5
|
readonly endpointId: string;
|
|
6
|
-
readonly
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
readonly signal?: AbortSignal;
|
|
7
|
+
/** Reads the lifecycle-selected socket; RPC does not open or reconnect it. */
|
|
8
|
+
readonly socket: () => NapCatWsSocket | undefined;
|
|
9
|
+
}
|
|
10
|
+
export interface NapCatRpc {
|
|
11
|
+
call(request: PendingRpcRequest): Promise<NapCatActionResponse>;
|
|
12
|
+
receive(data: unknown, ingest: (event: NapCatEvent) => void): void;
|
|
13
|
+
disconnect: PendingRpc<NapCatActionResponse>['disconnect'];
|
|
14
|
+
dispose(): void;
|
|
15
|
+
readonly pendingCount: number;
|
|
16
|
+
}
|
|
17
|
+
/** Native SDK and semantic ports share this Client's correlation and cancellation owner. */
|
|
18
|
+
export declare function createNapCatRpc(options: NapCatRpcOptions): NapCatRpc;
|
|
19
|
+
export declare function decodeWsPayload(data: unknown): string;
|
package/lib/ws-transport.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
|
|
1
|
+
import { createPendingRpc } from 'zhin.js/adapter';
|
|
3
2
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
4
|
-
import { WS_OPEN
|
|
3
|
+
import { WS_OPEN } from './ws-types.js';
|
|
5
4
|
const logger = getLogger('napcat');
|
|
5
|
+
/** Native SDK and semantic ports share this Client's correlation and cancellation owner. */
|
|
6
|
+
export function createNapCatRpc(options) {
|
|
7
|
+
const pending = createPendingRpc({
|
|
8
|
+
signal: options.signal, requestPrefix: 'req_', timeoutMs: 30_000,
|
|
9
|
+
transport: () => { const socket = options.socket(); return socket?.readyState === WS_OPEN ? socket : undefined; },
|
|
10
|
+
serialize: request => JSON.stringify({ action: request.action, params: request.params ?? {}, echo: request.id }),
|
|
11
|
+
});
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
call: pending.call, disconnect: pending.disconnect, dispose: pending.dispose,
|
|
14
|
+
get pendingCount() { return pending.pendingCount; },
|
|
15
|
+
receive(data, ingest) {
|
|
16
|
+
try {
|
|
17
|
+
const message = JSON.parse(decodeWsPayload(data));
|
|
18
|
+
if ('echo' in message && typeof message.echo === 'string') {
|
|
19
|
+
pending.accept({ id: message.echo, value: message });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ingest(message);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
logger.warn(formatCompact({ op: 'napcat_parse_failed', endpoint: options.endpointId,
|
|
26
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
6
31
|
export function decodeWsPayload(data) {
|
|
7
32
|
if (typeof data === 'string')
|
|
8
33
|
return data;
|
|
@@ -12,72 +37,3 @@ export function decodeWsPayload(data) {
|
|
|
12
37
|
return new TextDecoder().decode(data);
|
|
13
38
|
return String(data ?? '');
|
|
14
39
|
}
|
|
15
|
-
export function handleNapCatWsMessage(data, options) {
|
|
16
|
-
try {
|
|
17
|
-
const raw = decodeWsPayload(data);
|
|
18
|
-
const msg = JSON.parse(raw);
|
|
19
|
-
if ('echo' in msg && typeof msg.echo === 'string') {
|
|
20
|
-
const resp = msg;
|
|
21
|
-
const pending = options.pending.get(resp.echo);
|
|
22
|
-
if (pending) {
|
|
23
|
-
options.pending.delete(resp.echo);
|
|
24
|
-
clearTimeout(pending.timeout);
|
|
25
|
-
if (resp.status === 'ok')
|
|
26
|
-
pending.resolve(resp.data);
|
|
27
|
-
else {
|
|
28
|
-
pending.reject(new EndpointDeliveryError('platform_rejected', `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`, 'rejected'));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
options.admit(msg);
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
logger.warn(formatCompact({
|
|
37
|
-
op: 'napcat_parse_failed',
|
|
38
|
-
endpoint: options.endpointId,
|
|
39
|
-
error: error instanceof Error ? error.message : String(error),
|
|
40
|
-
}));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export function callNapCatWsAction(ws, pending, requestId, action, params) {
|
|
44
|
-
if (!ws || ws.readyState !== WS_OPEN) {
|
|
45
|
-
return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket is not connected', 'not_sent'));
|
|
46
|
-
}
|
|
47
|
-
const echo = `req_${++requestId.value}`;
|
|
48
|
-
const req = { action, params, echo };
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const timeout = setTimeout(() => {
|
|
51
|
-
pending.delete(echo);
|
|
52
|
-
reject(new EndpointDeliveryError('endpoint_timeout', `API call timeout: ${action}`, 'unknown'));
|
|
53
|
-
}, 30_000);
|
|
54
|
-
pending.set(echo, { resolve, reject, timeout });
|
|
55
|
-
try {
|
|
56
|
-
ws.send(JSON.stringify(req));
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
pending.delete(echo);
|
|
60
|
-
clearTimeout(timeout);
|
|
61
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
|
|
66
|
-
if (existingTimer)
|
|
67
|
-
clearInterval(existingTimer);
|
|
68
|
-
return setInterval(() => {
|
|
69
|
-
try {
|
|
70
|
-
ws?.ping?.();
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* ignore */
|
|
74
|
-
}
|
|
75
|
-
}, intervalMs);
|
|
76
|
-
}
|
|
77
|
-
export function rejectAllPending(pending, message = 'Connection closed') {
|
|
78
|
-
for (const [, entry] of pending) {
|
|
79
|
-
clearTimeout(entry.timeout);
|
|
80
|
-
entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
|
|
81
|
-
}
|
|
82
|
-
pending.clear();
|
|
83
|
-
}
|
package/lib/ws-types.d.ts
CHANGED
|
@@ -10,8 +10,3 @@ export interface NapCatWsCreateOptions {
|
|
|
10
10
|
readonly headers?: Record<string, string>;
|
|
11
11
|
}
|
|
12
12
|
export declare const WS_OPEN = 1;
|
|
13
|
-
export interface NapCatPendingAction {
|
|
14
|
-
resolve: (value: unknown) => void;
|
|
15
|
-
reject: (err: Error) => void;
|
|
16
|
-
timeout: NodeJS.Timeout;
|
|
17
|
-
}
|
package/lib/wss-endpoint.d.ts
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
5
5
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
6
6
|
import type { CapabilityId } from 'zhin.js';
|
|
7
|
-
import { type
|
|
7
|
+
import { type NapCatWssConfig } from './protocol.js';
|
|
8
8
|
import { NapcatClient } from './client.js';
|
|
9
|
+
import { type NapCatWsSocket } from './ws-types.js';
|
|
9
10
|
export interface NapCatWssEndpointOptions {
|
|
10
11
|
readonly id: CapabilityId;
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
11
13
|
readonly http: HttpHost;
|
|
12
14
|
readonly config: NapCatWssConfig;
|
|
13
15
|
}
|
|
14
|
-
export declare class NapCatWssEndpoint extends
|
|
16
|
+
export declare class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
15
17
|
#private;
|
|
16
|
-
readonly
|
|
17
|
-
readonly management: EndpointManagement;
|
|
18
|
-
readonly control: EndpointControl;
|
|
19
|
-
readonly content: import("@zhin.js/adapter").EndpointContentPort;
|
|
18
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
20
19
|
constructor(options: NapCatWssEndpointOptions);
|
|
21
|
-
get
|
|
22
|
-
start(): Promise<void>;
|
|
23
|
-
open(): void;
|
|
24
|
-
close(): void;
|
|
25
|
-
stop(): Promise<void>;
|
|
26
|
-
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
27
|
-
recallMessage(messageId: string): Promise<void>;
|
|
28
|
-
admit(ev: NapCatEvent): void;
|
|
20
|
+
protected get actionSocket(): NapCatWsSocket | undefined;
|
|
29
21
|
}
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
2
2
|
/**
|
|
3
3
|
* NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
6
6
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
7
7
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
8
8
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
@@ -10,47 +10,60 @@ import { buildSendAction, formatInboundContent, formatOutboundSegments, isMessag
|
|
|
10
10
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
11
11
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
12
12
|
import { NapcatClient } from './client.js';
|
|
13
|
-
import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, } from './ws-transport.js';
|
|
14
13
|
import { verifyNapCatAccessToken } from './wss-auth.js';
|
|
15
|
-
export class NapCatWssEndpoint extends
|
|
16
|
-
|
|
14
|
+
export class NapCatWssEndpoint extends NapcatClient {
|
|
15
|
+
[endpointSpec];
|
|
17
16
|
#logger;
|
|
18
17
|
#options;
|
|
19
18
|
#inboundDeduper = new InboundMessageDeduper();
|
|
20
|
-
management = createNapCatEndpointManagement(this
|
|
21
|
-
control = createRecallEndpointControl((id) => this
|
|
22
|
-
content = createNapCatContentPort(
|
|
19
|
+
#management = createNapCatEndpointManagement(this);
|
|
20
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
|
|
21
|
+
#content = createNapCatContentPort(request => this.requestApi(request));
|
|
23
22
|
#connectionLifecycle;
|
|
24
|
-
#ws;
|
|
25
23
|
#wsRelease;
|
|
26
24
|
#connectionTask = Promise.resolve();
|
|
27
|
-
#requestId = { value: 0 };
|
|
28
|
-
#pending = new Map();
|
|
29
25
|
#open = false;
|
|
30
26
|
#started = false;
|
|
31
27
|
#stopped = false;
|
|
32
28
|
constructor(options) {
|
|
33
|
-
super();
|
|
29
|
+
super({ config: options.config, signal: options.signal });
|
|
34
30
|
this.#logger = getAdapterLogger('napcat', options.config.id);
|
|
35
31
|
this.#options = options;
|
|
32
|
+
const readTransportState = () => this.#transportState;
|
|
33
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
34
|
+
name: options.config.id,
|
|
35
|
+
start: () => this.#start(), open: () => this.#openEndpoint(),
|
|
36
|
+
close: () => this.#close(), stop: () => this.#stop(),
|
|
37
|
+
send: request => this.#send(request),
|
|
38
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
39
|
+
get transportState() { return readTransportState(); },
|
|
40
|
+
});
|
|
41
|
+
this.on('event', event => {
|
|
42
|
+
void this[endpointSpec].events.run(() => this.#admit(event)).catch((error) => {
|
|
43
|
+
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
36
46
|
this.#connectionLifecycle = createEndpointLifecycle({
|
|
37
47
|
name: `${options.config.id}:inbound`,
|
|
38
48
|
reconnect: false,
|
|
39
49
|
heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
|
|
40
50
|
});
|
|
41
51
|
}
|
|
42
|
-
get transportState() {
|
|
52
|
+
get #transportState() {
|
|
43
53
|
if (this.#stopped)
|
|
44
54
|
return 'stopped';
|
|
45
55
|
if (!this.#started)
|
|
46
56
|
return 'idle';
|
|
47
57
|
if (this.#connectionLifecycle.state === 'closed')
|
|
48
58
|
return 'closed';
|
|
49
|
-
if (this
|
|
59
|
+
if (this.transportSocket?.readyState === 1)
|
|
50
60
|
return 'open';
|
|
51
61
|
return this.#connectionLifecycle.state === 'connecting' ? 'connecting' : 'idle';
|
|
52
62
|
}
|
|
53
|
-
|
|
63
|
+
get actionSocket() {
|
|
64
|
+
return this.#connectionLifecycle.state === 'closed' ? undefined : this.transportSocket;
|
|
65
|
+
}
|
|
66
|
+
async #start() {
|
|
54
67
|
if (this.#started)
|
|
55
68
|
return;
|
|
56
69
|
this.#stopped = false;
|
|
@@ -74,53 +87,51 @@ export class NapCatWssEndpoint extends Endpoint {
|
|
|
74
87
|
path: this.#options.config.path,
|
|
75
88
|
}));
|
|
76
89
|
}
|
|
77
|
-
|
|
90
|
+
#openEndpoint() {
|
|
78
91
|
this.#open = true;
|
|
79
92
|
}
|
|
80
|
-
close() {
|
|
93
|
+
#close() {
|
|
81
94
|
this.#open = false;
|
|
82
95
|
}
|
|
83
|
-
async stop() {
|
|
96
|
+
async #stop() {
|
|
84
97
|
this.#stopped = true;
|
|
85
98
|
this.#open = false;
|
|
86
99
|
this.#wsRelease?.();
|
|
87
100
|
this.#wsRelease = undefined;
|
|
88
101
|
await this.#connectionTask;
|
|
89
102
|
await this.#connectionLifecycle.stop();
|
|
90
|
-
|
|
103
|
+
this.rpc.disconnect();
|
|
91
104
|
this.#inboundDeduper.clear();
|
|
92
|
-
this
|
|
105
|
+
this.transportSocket = undefined;
|
|
93
106
|
this.#started = false;
|
|
94
107
|
}
|
|
95
|
-
async send({ conversation, payload }) {
|
|
108
|
+
async #send({ conversation, payload, signal }) {
|
|
96
109
|
const message = formatOutboundSegments(payload);
|
|
97
110
|
const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
|
|
98
|
-
const data = await this.
|
|
111
|
+
const data = await this.requestApi({ action, params, signal });
|
|
99
112
|
const rawId = data?.message_id;
|
|
100
113
|
if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
|
|
101
114
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
|
|
102
115
|
}
|
|
103
116
|
return String(rawId);
|
|
104
117
|
}
|
|
105
|
-
async recallMessage(messageId) {
|
|
118
|
+
async #recallMessage(messageId, signal) {
|
|
106
119
|
if (!messageId)
|
|
107
120
|
return;
|
|
108
|
-
await this.
|
|
109
|
-
}
|
|
110
|
-
#callApi(action, params = {}) {
|
|
111
|
-
return callNapCatWsAction(this.#connectionLifecycle.state === 'closed' ? undefined : this.#ws, this.#pending, this.#requestId, action, params);
|
|
121
|
+
await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
|
|
112
122
|
}
|
|
113
|
-
admit(ev) {
|
|
114
|
-
|
|
123
|
+
#admit(ev) {
|
|
124
|
+
const nativeEvent = ev;
|
|
125
|
+
if (!this.#open && !this.#started)
|
|
115
126
|
return;
|
|
116
|
-
void this.
|
|
127
|
+
void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
|
|
117
128
|
this.#logger.warn(formatCompact({
|
|
118
129
|
op: 'napcat_platform_event_failed',
|
|
119
130
|
error: error instanceof Error ? error.message : String(error),
|
|
120
131
|
}));
|
|
121
132
|
});
|
|
122
133
|
if (!isMessageEvent(ev)) {
|
|
123
|
-
receiveNapCatSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, this
|
|
134
|
+
receiveNapCatSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, this, ev, this.#logger);
|
|
124
135
|
return;
|
|
125
136
|
}
|
|
126
137
|
if (isSelfMessage(ev))
|
|
@@ -131,10 +142,12 @@ export class NapCatWssEndpoint extends Endpoint {
|
|
|
131
142
|
if (Array.isArray(ev.message) || typeof ev.message === 'string') {
|
|
132
143
|
ev = { ...ev, message: normalizeMessage(ev.message) };
|
|
133
144
|
}
|
|
134
|
-
const conversation = napcatInboundConversation(String(this.#options.id), ev);
|
|
145
|
+
const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
135
146
|
const nickname = senderNickname(ev);
|
|
136
147
|
const mentioned = isNapCatBotMentioned(ev);
|
|
137
|
-
void this.
|
|
148
|
+
void this[endpointSpec].events.message({
|
|
149
|
+
event: nativeEvent,
|
|
150
|
+
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
138
151
|
conversation,
|
|
139
152
|
message: { conversation, id: msgId },
|
|
140
153
|
content: formatInboundContent(ev),
|
|
@@ -173,12 +186,12 @@ export class NapCatWssEndpoint extends Endpoint {
|
|
|
173
186
|
}
|
|
174
187
|
const socket = connection.socket;
|
|
175
188
|
await this.#connectionLifecycle.stop();
|
|
176
|
-
|
|
189
|
+
this.rpc.disconnect('Connection replaced');
|
|
177
190
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
178
|
-
this
|
|
191
|
+
this.transportSocket = socket;
|
|
179
192
|
lifecycleHandle.onForceClose(() => {
|
|
180
|
-
if (this
|
|
181
|
-
|
|
193
|
+
if (this.transportSocket === socket) {
|
|
194
|
+
this.rpc.disconnect();
|
|
182
195
|
}
|
|
183
196
|
try {
|
|
184
197
|
socket.close();
|
|
@@ -196,29 +209,25 @@ export class NapCatWssEndpoint extends Endpoint {
|
|
|
196
209
|
}
|
|
197
210
|
}, this.#options.config.heartbeat_interval);
|
|
198
211
|
socket.on('pong', () => {
|
|
199
|
-
if (this
|
|
212
|
+
if (this.transportSocket === socket)
|
|
200
213
|
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
201
214
|
});
|
|
202
215
|
socket.on('message', (data) => {
|
|
203
|
-
if (this
|
|
216
|
+
if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed')
|
|
204
217
|
return;
|
|
205
218
|
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
206
|
-
|
|
207
|
-
endpointId: this.#options.config.id,
|
|
208
|
-
pending: this.#pending,
|
|
209
|
-
admit: (event) => this.admit(event),
|
|
210
|
-
});
|
|
219
|
+
this.rpc.receive(data, event => this.ingest(event));
|
|
211
220
|
});
|
|
212
221
|
socket.on('error', (error) => {
|
|
213
|
-
if (this
|
|
222
|
+
if (this.transportSocket !== socket)
|
|
214
223
|
return;
|
|
215
224
|
this.#logger.warn(formatCompact({ op: 'ws_error', error: String(error) }));
|
|
216
225
|
});
|
|
217
226
|
socket.on('close', () => {
|
|
218
|
-
if (this
|
|
227
|
+
if (this.transportSocket !== socket)
|
|
219
228
|
return;
|
|
220
|
-
this
|
|
221
|
-
|
|
229
|
+
this.transportSocket = undefined;
|
|
230
|
+
this.rpc.disconnect();
|
|
222
231
|
lifecycleHandle.notifyClosed(new Error('NapCat reverse WebSocket closed'));
|
|
223
232
|
});
|
|
224
233
|
});
|