@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/src/protocol.ts
CHANGED
|
@@ -104,6 +104,22 @@ export interface NapCatActionResponse {
|
|
|
104
104
|
wording?: string;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/** OneBot 11: async/1 is submitted without a known result, not a rejection.
|
|
108
|
+
* https://github.com/botuniverse/onebot-11/blob/master/communication/http.md#响应
|
|
109
|
+
*/
|
|
110
|
+
export function readNapCatActionResponse(value: unknown): NapCatActionResponse {
|
|
111
|
+
if (!value || typeof value !== 'object' || !('status' in value) || !('retcode' in value)
|
|
112
|
+
|| !Number.isInteger(value.retcode)) throw new EndpointDeliveryError('delivery_unconfirmed', 'NapCat returned an invalid action response', 'unknown');
|
|
113
|
+
const response = value as NapCatActionResponse;
|
|
114
|
+
if (response.status === 'failed' && response.retcode !== 0 && response.retcode !== 1) {
|
|
115
|
+
throw new EndpointDeliveryError('platform_rejected',
|
|
116
|
+
`NapCat action failed [${response.retcode}]: ${response.message || response.wording || 'unknown'}`, 'rejected');
|
|
117
|
+
}
|
|
118
|
+
if (response.status !== 'ok' || response.retcode !== 0) throw new EndpointDeliveryError('delivery_unconfirmed',
|
|
119
|
+
'NapCat did not confirm the action result', 'unknown');
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
export interface NapCatWireSegment {
|
|
108
124
|
readonly type: string;
|
|
109
125
|
readonly data?: Record<string, unknown>;
|
|
@@ -430,6 +446,7 @@ export function buildWsConnectOptions(config: NapCatWsConfig): {
|
|
|
430
446
|
export interface NapCatHttpOptions {
|
|
431
447
|
readonly http_url: string;
|
|
432
448
|
readonly access_token?: string;
|
|
449
|
+
readonly signal?: AbortSignal;
|
|
433
450
|
}
|
|
434
451
|
|
|
435
452
|
/**
|
|
@@ -448,24 +465,19 @@ export async function callNapCatHttpAction(
|
|
|
448
465
|
headers.Authorization = `Bearer ${options.access_token}`;
|
|
449
466
|
}
|
|
450
467
|
const res = await fetch(url, {
|
|
451
|
-
signal: AbortSignal.timeout(30_000),
|
|
468
|
+
signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
|
|
452
469
|
method: 'POST',
|
|
453
470
|
headers,
|
|
454
471
|
body: JSON.stringify(params),
|
|
455
472
|
});
|
|
456
473
|
const text = await res.text();
|
|
457
|
-
if (res.status
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
data = JSON.parse(text) as NapCatActionResponse;
|
|
462
|
-
} catch {
|
|
463
|
-
throw new Error(`NapCat HTTP invalid response: ${text.slice(0, 200)}`);
|
|
464
|
-
}
|
|
465
|
-
if (data.status !== 'ok' && data.retcode !== 0) {
|
|
466
|
-
throw new EndpointDeliveryError(
|
|
467
|
-
'platform_rejected', `NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`, 'rejected',
|
|
468
|
-
);
|
|
474
|
+
if (res.status !== 200) {
|
|
475
|
+
const rejected = [400, 401, 403, 404, 406, 429].includes(res.status);
|
|
476
|
+
throw new EndpointDeliveryError(rejected ? 'platform_rejected' : 'delivery_unconfirmed',
|
|
477
|
+
`NapCat HTTP ${res.status}: ${text}`, rejected ? 'rejected' : 'unknown');
|
|
469
478
|
}
|
|
470
|
-
|
|
479
|
+
let data: unknown;
|
|
480
|
+
try { data = JSON.parse(text); }
|
|
481
|
+
catch (cause) { throw new EndpointDeliveryError('delivery_unconfirmed', 'NapCat returned invalid JSON', 'unknown', { cause }); }
|
|
482
|
+
return readNapCatActionResponse(data);
|
|
471
483
|
}
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
4
4
|
*/
|
|
5
5
|
import WebSocket from 'ws';
|
|
6
6
|
import {
|
|
7
|
-
Endpoint,
|
|
7
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
8
8
|
createRecallEndpointControl,
|
|
9
9
|
createEndpointLifecycle,
|
|
10
10
|
type EndpointConnectHandle,
|
|
@@ -38,12 +38,6 @@ import {
|
|
|
38
38
|
} from './protocol.js';
|
|
39
39
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
40
40
|
import {
|
|
41
|
-
callNapCatWsAction,
|
|
42
|
-
handleNapCatWsMessage,
|
|
43
|
-
rejectAllPending,
|
|
44
|
-
} from './ws-transport.js';
|
|
45
|
-
import {
|
|
46
|
-
type NapCatPendingAction,
|
|
47
41
|
type NapCatWsCreateOptions,
|
|
48
42
|
type NapCatWsSocket,
|
|
49
43
|
} from './ws-types.js';
|
|
@@ -52,6 +46,7 @@ import { NapcatClient } from './client.js';
|
|
|
52
46
|
|
|
53
47
|
export interface NapCatWsEndpointOptions {
|
|
54
48
|
readonly id: CapabilityId;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
55
50
|
readonly config: NapCatWsConfig;
|
|
56
51
|
readonly createWebSocket?: (
|
|
57
52
|
url: string,
|
|
@@ -59,25 +54,36 @@ export interface NapCatWsEndpointOptions {
|
|
|
59
54
|
) => NapCatWsSocket;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
|
-
export class NapCatWsEndpoint extends
|
|
63
|
-
readonly
|
|
57
|
+
export class NapCatWsEndpoint extends NapcatClient implements Endpoint {
|
|
58
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
64
59
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
65
60
|
|
|
66
61
|
readonly #options: NapCatWsEndpointOptions;
|
|
67
62
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
68
|
-
readonly management: EndpointManagement = createNapCatEndpointManagement(this
|
|
69
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
70
|
-
readonly content = createNapCatContentPort(
|
|
63
|
+
readonly #management: EndpointManagement = createNapCatEndpointManagement(this);
|
|
64
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
|
|
65
|
+
readonly #content = createNapCatContentPort(request => this.requestApi(request));
|
|
71
66
|
readonly #lifecycle: EndpointLifecycle;
|
|
72
|
-
#ws?: NapCatWsSocket;
|
|
73
|
-
#requestId = { value: 0 };
|
|
74
|
-
#pending = new Map<string, NapCatPendingAction>();
|
|
75
67
|
#open = false;
|
|
76
68
|
|
|
77
69
|
constructor(options: NapCatWsEndpointOptions) {
|
|
78
|
-
super();
|
|
70
|
+
super({ config: options.config, signal: options.signal });
|
|
79
71
|
this.#logger = getAdapterLogger('napcat', options.config.id);
|
|
80
72
|
this.#options = options;
|
|
73
|
+
const readTransportState = () => this.#transportState;
|
|
74
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
75
|
+
name: options.config.id,
|
|
76
|
+
start: () => this.#start(), open: () => this.#openEndpoint(),
|
|
77
|
+
close: () => this.#close(), stop: () => this.#stop(),
|
|
78
|
+
send: request => this.#send(request),
|
|
79
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
80
|
+
get transportState() { return readTransportState(); },
|
|
81
|
+
});
|
|
82
|
+
this.on('event', event => {
|
|
83
|
+
void this[endpointSpec].events.run(() => this.#admit(event)).catch((error: unknown) => {
|
|
84
|
+
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
81
87
|
this.#lifecycle = createEndpointLifecycle({
|
|
82
88
|
name: options.config.id,
|
|
83
89
|
heartbeat: { intervalMs: options.config.heartbeat_interval, watchdogMisses: 2 },
|
|
@@ -91,42 +97,46 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
91
97
|
});
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
get transportState(): EndpointTransportState {
|
|
100
|
+
get #transportState(): EndpointTransportState {
|
|
95
101
|
return this.#lifecycle.state;
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
|
|
104
|
+
protected override get actionSocket(): NapCatWsSocket | undefined {
|
|
105
|
+
return this.#lifecycle.state === 'open' ? this.transportSocket : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async #start(): Promise<void> {
|
|
99
109
|
if (this.#lifecycle.started) return;
|
|
100
110
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
#openEndpoint(): void {
|
|
104
114
|
this.#open = true;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
close(): void {
|
|
117
|
+
#close(): void {
|
|
108
118
|
this.#open = false;
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
async stop(): Promise<void> {
|
|
121
|
+
async #stop(): Promise<void> {
|
|
112
122
|
this.#open = false;
|
|
113
123
|
await this.#lifecycle.stop();
|
|
114
|
-
|
|
124
|
+
this.rpc.disconnect();
|
|
115
125
|
this.#inboundDeduper.clear();
|
|
116
|
-
if (this
|
|
126
|
+
if (this.transportSocket) {
|
|
117
127
|
try {
|
|
118
|
-
this
|
|
128
|
+
this.transportSocket.close();
|
|
119
129
|
} catch {
|
|
120
130
|
/* ignore */
|
|
121
131
|
}
|
|
122
|
-
this
|
|
132
|
+
this.transportSocket = undefined;
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
136
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
127
137
|
const message = formatOutboundSegments(payload);
|
|
128
138
|
const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
|
|
129
|
-
const data = await this.
|
|
139
|
+
const data = await this.requestApi({ action, params, signal }) as { message_id?: number | string } | undefined;
|
|
130
140
|
const rawId = data?.message_id;
|
|
131
141
|
if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
|
|
132
142
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
|
|
@@ -141,19 +151,18 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
141
151
|
return messageId;
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
154
|
+
async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
|
|
145
155
|
if (!messageId) return;
|
|
146
|
-
await this.
|
|
156
|
+
await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
return callNapCatWsAction(this.#lifecycle.state === 'open' ? this.#ws : undefined, this.#pending, this.#requestId, action, params);
|
|
151
|
-
}
|
|
159
|
+
|
|
152
160
|
|
|
153
161
|
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
154
|
-
admit(ev: NapCatEvent): void {
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
#admit(ev: NapCatEvent): void {
|
|
163
|
+
const nativeEvent = ev;
|
|
164
|
+
if (!this.#open && !this.#lifecycle.started) return;
|
|
165
|
+
void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
|
|
157
166
|
this.#logger.warn(formatCompact({
|
|
158
167
|
op: 'napcat_platform_event_failed',
|
|
159
168
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -161,9 +170,9 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
161
170
|
});
|
|
162
171
|
if (!isMessageEvent(ev)) {
|
|
163
172
|
receiveNapCatSideEvent(
|
|
164
|
-
(name, payload) => this.emit(name, payload),
|
|
173
|
+
(name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
165
174
|
this.#options.config.id,
|
|
166
|
-
this
|
|
175
|
+
this,
|
|
167
176
|
ev,
|
|
168
177
|
this.#logger,
|
|
169
178
|
);
|
|
@@ -175,11 +184,13 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
175
184
|
if (Array.isArray(ev.message) || typeof ev.message === 'string') {
|
|
176
185
|
ev = { ...ev, message: normalizeMessage(ev.message) };
|
|
177
186
|
}
|
|
178
|
-
const conversation = napcatInboundConversation(String(this.#options.id), ev);
|
|
187
|
+
const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
179
188
|
const content = formatInboundContent(ev);
|
|
180
189
|
const nickname = senderNickname(ev);
|
|
181
190
|
const mentioned = isNapCatBotMentioned(ev);
|
|
182
|
-
void this.
|
|
191
|
+
void this[endpointSpec].events.message({
|
|
192
|
+
event: nativeEvent,
|
|
193
|
+
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
183
194
|
conversation,
|
|
184
195
|
message: { conversation, id: msgId },
|
|
185
196
|
content,
|
|
@@ -217,10 +228,10 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
217
228
|
await new Promise<void>((resolve, reject) => {
|
|
218
229
|
let settled = false;
|
|
219
230
|
const ws = create(url, { headers });
|
|
220
|
-
this
|
|
231
|
+
this.transportSocket = ws;
|
|
221
232
|
handle.onForceClose(() => {
|
|
222
|
-
if (this
|
|
223
|
-
|
|
233
|
+
if (this.transportSocket === ws) {
|
|
234
|
+
this.rpc.disconnect();
|
|
224
235
|
}
|
|
225
236
|
try {
|
|
226
237
|
ws.close();
|
|
@@ -230,7 +241,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
230
241
|
});
|
|
231
242
|
|
|
232
243
|
ws.on('open', () => {
|
|
233
|
-
if (settled || this
|
|
244
|
+
if (settled || this.transportSocket !== ws || !this.#lifecycle.started) return;
|
|
234
245
|
settled = true;
|
|
235
246
|
if (!this.#options.config.access_token) {
|
|
236
247
|
this.#logger.warn(formatCompact({
|
|
@@ -258,22 +269,18 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
258
269
|
});
|
|
259
270
|
|
|
260
271
|
ws.on('pong', () => {
|
|
261
|
-
if (this
|
|
272
|
+
if (this.transportSocket === ws) this.#lifecycle.notifyHeartbeatAck();
|
|
262
273
|
});
|
|
263
274
|
ws.on('message', (data) => {
|
|
264
|
-
if (this
|
|
275
|
+
if (this.transportSocket !== ws || ws.readyState !== 1 || !this.#lifecycle.started) return;
|
|
265
276
|
this.#lifecycle.notifyHeartbeatAck();
|
|
266
|
-
|
|
267
|
-
endpointId: this.#options.config.id,
|
|
268
|
-
pending: this.#pending,
|
|
269
|
-
admit: (event) => this.admit(event),
|
|
270
|
-
});
|
|
277
|
+
this.rpc.receive(data, event => this.ingest(event));
|
|
271
278
|
});
|
|
272
279
|
|
|
273
280
|
ws.on('close', (code, reason) => {
|
|
274
|
-
if (this
|
|
275
|
-
this
|
|
276
|
-
|
|
281
|
+
if (this.transportSocket !== ws) return;
|
|
282
|
+
this.transportSocket = undefined;
|
|
283
|
+
this.rpc.disconnect();
|
|
277
284
|
const reasonStr = typeof reason === 'string'
|
|
278
285
|
? reason
|
|
279
286
|
: Buffer.isBuffer(reason)
|
|
@@ -300,7 +307,7 @@ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
|
|
|
300
307
|
});
|
|
301
308
|
|
|
302
309
|
ws.on('error', (err) => {
|
|
303
|
-
if (this
|
|
310
|
+
if (this.transportSocket !== ws) return;
|
|
304
311
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
305
312
|
this.#logger.warn(formatCompact({
|
|
306
313
|
op: 'ws_error',
|
package/src/ws-transport.ts
CHANGED
|
@@ -1,112 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
|
|
1
|
+
import { createPendingRpc, type PendingRpc, type PendingRpcRequest } from 'zhin.js/adapter';
|
|
3
2
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type NapCatActionResponse,
|
|
7
|
-
type NapCatEvent,
|
|
8
|
-
} from './protocol.js';
|
|
9
|
-
import {
|
|
10
|
-
WS_OPEN,
|
|
11
|
-
type NapCatPendingAction,
|
|
12
|
-
type NapCatWsSocket,
|
|
13
|
-
} from './ws-types.js';
|
|
3
|
+
import type { NapCatActionResponse, NapCatEvent } from './protocol.js';
|
|
4
|
+
import { WS_OPEN, type NapCatWsSocket } from './ws-types.js';
|
|
14
5
|
|
|
15
6
|
const logger = getLogger('napcat');
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
export interface NapCatRpcOptions {
|
|
8
|
+
readonly endpointId: string;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
/** Reads the lifecycle-selected socket; RPC does not open or reconnect it. */
|
|
11
|
+
readonly socket: () => NapCatWsSocket | undefined;
|
|
12
|
+
}
|
|
13
|
+
export interface NapCatRpc {
|
|
14
|
+
call(request: PendingRpcRequest): Promise<NapCatActionResponse>;
|
|
15
|
+
receive(data: unknown, ingest: (event: NapCatEvent) => void): void;
|
|
16
|
+
disconnect: PendingRpc<NapCatActionResponse>['disconnect'];
|
|
17
|
+
dispose(): void;
|
|
18
|
+
readonly pendingCount: number;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
options.pending.delete(resp.echo!);
|
|
40
|
-
clearTimeout(pending.timeout);
|
|
41
|
-
if (resp.status === 'ok') pending.resolve(resp.data);
|
|
42
|
-
else {
|
|
43
|
-
pending.reject(new EndpointDeliveryError(
|
|
44
|
-
'platform_rejected', `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`, 'rejected',
|
|
45
|
-
));
|
|
21
|
+
/** Native SDK and semantic ports share this Client's correlation and cancellation owner. */
|
|
22
|
+
export function createNapCatRpc(options: NapCatRpcOptions): NapCatRpc {
|
|
23
|
+
const pending = createPendingRpc<NapCatActionResponse>({
|
|
24
|
+
signal: options.signal, requestPrefix: 'req_', timeoutMs: 30_000,
|
|
25
|
+
transport: () => { const socket = options.socket(); return socket?.readyState === WS_OPEN ? socket : undefined; },
|
|
26
|
+
serialize: request => JSON.stringify({ action: request.action, params: request.params ?? {}, echo: request.id }),
|
|
27
|
+
});
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
call: pending.call, disconnect: pending.disconnect, dispose: pending.dispose,
|
|
30
|
+
get pendingCount() { return pending.pendingCount; },
|
|
31
|
+
receive(data: unknown, ingest: (event: NapCatEvent) => void) {
|
|
32
|
+
try {
|
|
33
|
+
const message = JSON.parse(decodeWsPayload(data)) as NapCatEvent | NapCatActionResponse;
|
|
34
|
+
if ('echo' in message && typeof message.echo === 'string') {
|
|
35
|
+
pending.accept({ id: message.echo, value: message as NapCatActionResponse }); return;
|
|
46
36
|
}
|
|
37
|
+
ingest(message as NapCatEvent);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.warn(formatCompact({ op: 'napcat_parse_failed', endpoint: options.endpointId,
|
|
40
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
options.admit(msg as NapCatEvent);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
logger.warn(formatCompact({
|
|
53
|
-
op: 'napcat_parse_failed',
|
|
54
|
-
endpoint: options.endpointId,
|
|
55
|
-
error: error instanceof Error ? error.message : String(error),
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function callNapCatWsAction(
|
|
61
|
-
ws: NapCatWsSocket | undefined,
|
|
62
|
-
pending: Map<string, NapCatPendingAction>,
|
|
63
|
-
requestId: { value: number },
|
|
64
|
-
action: string,
|
|
65
|
-
params: Record<string, unknown>,
|
|
66
|
-
): Promise<unknown> {
|
|
67
|
-
if (!ws || ws.readyState !== WS_OPEN) {
|
|
68
|
-
return Promise.reject(new EndpointDeliveryError('endpoint_disconnected', 'WebSocket is not connected', 'not_sent'));
|
|
69
|
-
}
|
|
70
|
-
const echo = `req_${++requestId.value}`;
|
|
71
|
-
const req: NapCatActionRequest = { action, params, echo };
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
const timeout = setTimeout(() => {
|
|
74
|
-
pending.delete(echo);
|
|
75
|
-
reject(new EndpointDeliveryError('endpoint_timeout', `API call timeout: ${action}`, 'unknown'));
|
|
76
|
-
}, 30_000);
|
|
77
|
-
pending.set(echo, { resolve, reject, timeout });
|
|
78
|
-
try {
|
|
79
|
-
ws.send(JSON.stringify(req));
|
|
80
|
-
} catch (error) {
|
|
81
|
-
pending.delete(echo);
|
|
82
|
-
clearTimeout(timeout);
|
|
83
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
84
|
-
}
|
|
42
|
+
},
|
|
85
43
|
});
|
|
86
44
|
}
|
|
87
45
|
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (existingTimer) clearInterval(existingTimer);
|
|
94
|
-
return setInterval(() => {
|
|
95
|
-
try {
|
|
96
|
-
ws?.ping?.();
|
|
97
|
-
} catch {
|
|
98
|
-
/* ignore */
|
|
99
|
-
}
|
|
100
|
-
}, intervalMs);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function rejectAllPending(
|
|
104
|
-
pending: Map<string, NapCatPendingAction>,
|
|
105
|
-
message = 'Connection closed',
|
|
106
|
-
): void {
|
|
107
|
-
for (const [, entry] of pending) {
|
|
108
|
-
clearTimeout(entry.timeout);
|
|
109
|
-
entry.reject(new EndpointDeliveryError('endpoint_disconnected', message, 'unknown'));
|
|
110
|
-
}
|
|
111
|
-
pending.clear();
|
|
46
|
+
export function decodeWsPayload(data: unknown): string {
|
|
47
|
+
if (typeof data === 'string') return data;
|
|
48
|
+
if (Buffer.isBuffer(data)) return data.toString();
|
|
49
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
50
|
+
return String(data ?? '');
|
|
112
51
|
}
|