@zhin.js/adapter-napcat 1.1.18 → 1.1.19
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 +39 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-napcat",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -45,19 +45,19 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"ws": "^8.21.1",
|
|
48
|
-
"@zhin.js/adapter": "1.1.
|
|
49
|
-
"@zhin.js/agent-feature": "1.1.
|
|
50
|
-
"@zhin.js/client": "1.1.
|
|
48
|
+
"@zhin.js/adapter": "1.1.26",
|
|
49
|
+
"@zhin.js/agent-feature": "1.1.11",
|
|
50
|
+
"@zhin.js/client": "1.1.22",
|
|
51
51
|
"@zhin.js/console-contract": "1.1.3",
|
|
52
|
-
"@zhin.js/core": "1.1.
|
|
53
|
-
"@zhin.js/feature-kit": "1.1.
|
|
54
|
-
"@zhin.js/host-http": "1.1.
|
|
52
|
+
"@zhin.js/core": "1.1.54",
|
|
53
|
+
"@zhin.js/feature-kit": "1.1.10",
|
|
54
|
+
"@zhin.js/host-http": "1.1.12",
|
|
55
55
|
"@zhin.js/im-contract": "1.1.9",
|
|
56
56
|
"@zhin.js/logger": "1.1.2",
|
|
57
|
-
"@zhin.js/page": "1.1.
|
|
58
|
-
"@zhin.js/permission": "1.1.
|
|
59
|
-
"@zhin.js/skill": "1.1.
|
|
60
|
-
"@zhin.js/tool": "1.1.
|
|
57
|
+
"@zhin.js/page": "1.1.11",
|
|
58
|
+
"@zhin.js/permission": "1.1.10",
|
|
59
|
+
"@zhin.js/skill": "1.1.11",
|
|
60
|
+
"@zhin.js/tool": "1.1.13"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^26.1.2",
|
|
@@ -65,15 +65,15 @@
|
|
|
65
65
|
"typescript": "^6.0.3",
|
|
66
66
|
"vitest": "^4.1.10",
|
|
67
67
|
"zod": "^4.4.3",
|
|
68
|
-
"@zhin.js/host-http": "1.1.
|
|
69
|
-
"zhin.js": "1.1.
|
|
68
|
+
"@zhin.js/host-http": "1.1.12",
|
|
69
|
+
"zhin.js": "1.1.19"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"zod": "^4.0.0",
|
|
73
|
-
"@zhin.js/adapter": "^1.1.
|
|
74
|
-
"@zhin.js/command": "^1.1.
|
|
75
|
-
"@zhin.js/core": "^1.1.
|
|
76
|
-
"zhin.js": "^1.1.
|
|
73
|
+
"@zhin.js/adapter": "^1.1.26",
|
|
74
|
+
"@zhin.js/command": "^1.1.12",
|
|
75
|
+
"@zhin.js/core": "^1.1.54",
|
|
76
|
+
"zhin.js": "^1.1.19"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@zhin.js/command": {
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"features": [
|
|
108
108
|
{
|
|
109
109
|
"package": "@zhin.js/adapter",
|
|
110
|
-
"api": "^
|
|
110
|
+
"api": "^2.0.0"
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"package": "@zhin.js/command",
|
package/plugin.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
-
import { createEndpointRuntimeState } from 'zhin.js/adapter';
|
|
3
2
|
import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
|
|
4
3
|
import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
|
|
5
|
-
import { napcatRuntimeStateToken } from "./lib/napcat-runtime-state.js";
|
|
6
4
|
import { createNapcatConsoleRpc } from "./lib/console.js";
|
|
7
5
|
export default definePlugin({
|
|
8
6
|
name: 'napcat',
|
|
@@ -10,7 +8,6 @@ export default definePlugin({
|
|
|
10
8
|
displayName: 'NapCat Adapter',
|
|
11
9
|
},
|
|
12
10
|
setup(context) {
|
|
13
|
-
context.resources.provide(napcatRuntimeStateToken, createEndpointRuntimeState());
|
|
14
11
|
context.resources.provide(pluginConsoleRpcToken, createNapcatConsoleRpc(context.resources, () => context.config.get()));
|
|
15
12
|
if (context.resources.has(permissionHostToken)) {
|
|
16
13
|
const host = context.resources.use(permissionHostToken);
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
|
|
2
|
+
import { httpHostToken } from '@zhin.js/host-http';
|
|
3
|
+
import { callNapCatHttpAction, resolveNapCatConfig, type NapCatEndpointConfig } from './protocol.js';
|
|
4
|
+
import { NapCatWsEndpoint } from './ws-endpoint.js';
|
|
5
|
+
import { NapCatWssEndpoint } from './wss-endpoint.js';
|
|
6
|
+
import { NapCatHttpEndpoint } from './http-endpoint.js';
|
|
7
|
+
import { probeNapcatIdentity } from './identity.js';
|
|
8
|
+
import type { NapcatClientEventMap } from './client.js';
|
|
9
|
+
|
|
10
|
+
type NapcatEndpoint = NapCatWsEndpoint | NapCatWssEndpoint | NapCatHttpEndpoint;
|
|
11
|
+
export class NapcatAdapter extends Adapter<NapcatEndpoint, NapCatEndpointConfig, NapcatClientEventMap> {
|
|
12
|
+
readonly #context: AdapterContext<NapCatEndpointConfig>;
|
|
13
|
+
constructor(context: AdapterContext<NapCatEndpointConfig>) { super(); this.#context = context; }
|
|
14
|
+
override get configurationIdentityFields(): readonly string[] { return ['id']; }
|
|
15
|
+
override get configurationIdentityProbeFields(): readonly string[] { return ['connection', 'access_token', 'url', 'http_url']; }
|
|
16
|
+
override get configurationUpdateFields(): readonly string[] {
|
|
17
|
+
return ['connection', 'url', 'path', 'http_url', 'post_path', 'access_token', 'reconnect_interval', 'heartbeat_interval', 'poll_interval'];
|
|
18
|
+
}
|
|
19
|
+
protected override validateConfiguration(config: NapCatEndpointConfig): void { resolveNapCatConfig(config); }
|
|
20
|
+
protected override async identifyConfiguration(input: NapCatEndpointConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
21
|
+
const config = resolveNapCatConfig(input);
|
|
22
|
+
if (config.connection === 'ws') return probeNapcatIdentity(config, signal);
|
|
23
|
+
if (config.connection === 'wss') return undefined;
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
const response = await callNapCatHttpAction({ ...config, signal }, 'get_login_info');
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
const data = response.data as { user_id?: unknown } | undefined;
|
|
28
|
+
if (data?.user_id === 0 || data?.user_id === '0' || (typeof data?.user_id === 'string' && !data.user_id.trim())) return undefined;
|
|
29
|
+
if ((typeof data?.user_id !== 'string' && typeof data?.user_id !== 'number') || !String(data.user_id).trim()) {
|
|
30
|
+
throw new Error('NapCat login identity is unavailable');
|
|
31
|
+
}
|
|
32
|
+
return `qq:${data.user_id}`;
|
|
33
|
+
}
|
|
34
|
+
protected createEndpoint(input: NapCatEndpointConfig, execution: EndpointCreationContext): NapcatEndpoint {
|
|
35
|
+
const config = resolveNapCatConfig(input), id = execution.canonicalId, signal = execution.signal;
|
|
36
|
+
switch (config.connection) {
|
|
37
|
+
case 'wss': return new NapCatWssEndpoint({ id, signal, config, http: this.#context.use(httpHostToken) });
|
|
38
|
+
case 'http': return new NapCatHttpEndpoint({ id, signal, config, http: this.#context.use(httpHostToken) });
|
|
39
|
+
case 'ws': return new NapCatWsEndpoint({ id, signal, config });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
declare module '@zhin.js/feature-kit' {
|
|
44
|
+
interface AdapterRegistry { readonly napcat: NapcatAdapter; }
|
|
45
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,16 +1,99 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { callNapCatHttpAction, readNapCatActionResponse, type NapCatEvent, type ResolvedNapCatConfig } from './protocol.js';
|
|
3
|
+
import { createNapCatRpc, type NapCatRpc } from './ws-transport.js';
|
|
4
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
5
|
+
import type { PendingRpcRequest } from 'zhin.js/adapter';
|
|
6
|
+
import type { NapCatWsSocket } from './ws-types.js';
|
|
3
7
|
|
|
4
8
|
export type NapcatApiCall = (
|
|
5
9
|
action: string,
|
|
6
10
|
params?: Record<string, unknown>,
|
|
7
11
|
) => Promise<unknown>;
|
|
8
12
|
|
|
13
|
+
/** Platform actions use one named payload; wire field names stay inside this Client. */
|
|
14
|
+
export interface NapcatMemberTitleRequest {
|
|
15
|
+
readonly groupId: number;
|
|
16
|
+
readonly userId: number;
|
|
17
|
+
readonly title: string;
|
|
18
|
+
/** Platform duration; omission retains the existing indefinite value (-1). */
|
|
19
|
+
readonly duration?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface NapcatProfileRequest {
|
|
22
|
+
readonly nickname: string;
|
|
23
|
+
readonly company?: string;
|
|
24
|
+
readonly email?: string;
|
|
25
|
+
readonly college?: string;
|
|
26
|
+
readonly personalNote?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface NapcatGroupFileRequest {
|
|
29
|
+
readonly groupId: number;
|
|
30
|
+
readonly file: string;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly folder?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface NapcatMiniAppRequest {
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly title: string;
|
|
37
|
+
readonly desc: string;
|
|
38
|
+
readonly picUrl: string;
|
|
39
|
+
readonly jumpUrl: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NapcatClientOptions {
|
|
43
|
+
readonly config: ResolvedNapCatConfig;
|
|
44
|
+
readonly httpAction?: typeof callNapCatHttpAction;
|
|
45
|
+
readonly signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
/** Transport-independent NapCat protocol Client produced by every Endpoint mode. */
|
|
10
|
-
export class NapcatClient {
|
|
11
|
-
|
|
49
|
+
export class NapcatClient extends EventEmitter<Record<string, [NapCatEvent]>> {
|
|
50
|
+
protected transportSocket?: NapCatWsSocket;
|
|
51
|
+
protected readonly rpc: NapCatRpc;
|
|
52
|
+
protected readonly protocolConfig: ResolvedNapCatConfig;
|
|
53
|
+
readonly #httpAction: typeof callNapCatHttpAction;
|
|
54
|
+
readonly #executionSignal?: AbortSignal;
|
|
12
55
|
|
|
13
|
-
|
|
56
|
+
constructor(options: NapcatClientOptions) {
|
|
57
|
+
super();
|
|
58
|
+
this.protocolConfig = options.config;
|
|
59
|
+
this.#httpAction = options.httpAction ?? callNapCatHttpAction;
|
|
60
|
+
this.#executionSignal = options.signal;
|
|
61
|
+
this.rpc = createNapCatRpc({ endpointId: options.config.id, signal: options.signal,
|
|
62
|
+
socket: () => this.actionSocket });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ingest(event: NapCatEvent): void {
|
|
66
|
+
this.emit('event', event);
|
|
67
|
+
const name = [event.post_type, event.message_type ?? event.notice_type ?? event.request_type, event.sub_type].filter(Boolean).join('.');
|
|
68
|
+
if (name && name !== 'event') this.emit(name, event);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected get actionSocket(): NapCatWsSocket | undefined { return this.transportSocket; }
|
|
72
|
+
|
|
73
|
+
/** Existing platform methods retain their native action/params calling convention. */
|
|
74
|
+
readonly callApi: NapcatApiCall = (action, params = {}) => this.requestApi({ action, params });
|
|
75
|
+
|
|
76
|
+
/** A semantic operation supplies its caller lifetime to the same native protocol owner. */
|
|
77
|
+
async requestApi(request: PendingRpcRequest): Promise<unknown> {
|
|
78
|
+
const signals = [this.#executionSignal, request.signal].filter((signal): signal is AbortSignal => !!signal);
|
|
79
|
+
const signal = signals.length ? AbortSignal.any(signals) : undefined;
|
|
80
|
+
if (signal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint operation ended', 'not_sent');
|
|
81
|
+
if (this.protocolConfig.connection === 'http') {
|
|
82
|
+
try {
|
|
83
|
+
const response = await this.#httpAction({ http_url: this.protocolConfig.http_url,
|
|
84
|
+
access_token: this.protocolConfig.access_token, signal }, request.action, request.params ?? {});
|
|
85
|
+
if (signal?.aborted) throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint operation cancelled', 'unknown');
|
|
86
|
+
return readNapCatActionResponse(response).data;
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
if (cause instanceof EndpointDeliveryError) throw cause;
|
|
89
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', cause instanceof Error ? cause.message : String(cause), 'unknown', { cause });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const response = await this.rpc.call({ ...request, signal });
|
|
93
|
+
return readNapCatActionResponse(response).data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async setTitle({ groupId, userId, title, duration = -1 }: NapcatMemberTitleRequest): Promise<boolean> {
|
|
14
97
|
await this.callApi('set_group_special_title', {
|
|
15
98
|
group_id: groupId,
|
|
16
99
|
user_id: userId,
|
|
@@ -24,7 +107,7 @@ export class NapcatClient {
|
|
|
24
107
|
deleteFriend(userId: number) { return this.callApi('delete_friend', { user_id: userId }); }
|
|
25
108
|
markMsgAsRead(messageId: number) { return this.callApi('mark_msg_as_read', { message_id: messageId }); }
|
|
26
109
|
ocrImage(image: string) { return this.callApi('ocr_image', { image }); }
|
|
27
|
-
setQQProfile(nickname
|
|
110
|
+
setQQProfile({ nickname, company, email, college, personalNote }: NapcatProfileRequest) {
|
|
28
111
|
return this.callApi('set_qq_profile', { nickname, company, email, college, personal_note: personalNote });
|
|
29
112
|
}
|
|
30
113
|
setGroupPortrait(groupId: number, file: string) { return this.callApi('set_group_portrait', { group_id: groupId, file }); }
|
|
@@ -35,7 +118,7 @@ export class NapcatClient {
|
|
|
35
118
|
sendGroupNotice(groupId: number, content: string, image?: string) { return this.callApi('_send_group_notice', { group_id: groupId, content, image }); }
|
|
36
119
|
getGroupNotice(groupId: number) { return this.callApi('_get_group_notice', { group_id: groupId }); }
|
|
37
120
|
deleteGroupNotice(groupId: number, noticeId: string) { return this.callApi('_del_group_notice', { group_id: groupId, notice_id: noticeId }); }
|
|
38
|
-
uploadGroupFile(groupId
|
|
121
|
+
uploadGroupFile({ groupId, file, name, folder }: NapcatGroupFileRequest) { return this.callApi('upload_group_file', { group_id: groupId, file, name, folder }); }
|
|
39
122
|
getGroupRootFiles(groupId: number) { return this.callApi('get_group_root_files', { group_id: groupId }); }
|
|
40
123
|
getGroupFileUrl(groupId: number, fileId: string, busid: number) { return this.callApi('get_group_file_url', { group_id: groupId, file_id: fileId, busid }); }
|
|
41
124
|
downloadFile(url: string, threadCount = 1, headers?: string[]) { return this.callApi('download_file', { url, thread_count: threadCount, headers }); }
|
|
@@ -59,21 +142,10 @@ export class NapcatClient {
|
|
|
59
142
|
sendPoke(userId: number, groupId?: number) { return this.callApi('send_poke', { user_id: userId, group_id: groupId }); }
|
|
60
143
|
ncGetUserStatus(userId: number) { return this.callApi('nc_get_user_status', { user_id: userId }); }
|
|
61
144
|
getGroupShutList(groupId: number) { return this.callApi('get_group_shut_list', { group_id: groupId }); }
|
|
62
|
-
getMiniAppArk(type
|
|
145
|
+
getMiniAppArk({ type, title, desc, picUrl, jumpUrl }: NapcatMiniAppRequest) { return this.callApi('get_mini_app_ark', { type, title, desc, picUrl, jumpUrl }); }
|
|
63
146
|
getAiCharacters(groupId: number) { return this.callApi('get_ai_characters', { group_id: groupId }); }
|
|
64
147
|
sendGroupAiRecord(groupId: number, characterId: string, text: string) {
|
|
65
148
|
return this.callApi('send_group_ai_record', { group_id: groupId, character: characterId, text });
|
|
66
149
|
}
|
|
67
150
|
}
|
|
68
151
|
export type NapcatClientEventMap = Record<string, NapCatEvent>;
|
|
69
|
-
|
|
70
|
-
declare module '@zhin.js/feature-kit' {
|
|
71
|
-
interface AdapterClientRegistry {
|
|
72
|
-
readonly napcat: {
|
|
73
|
-
readonly client: NapcatClient;
|
|
74
|
-
readonly events: NapcatClientEventMap;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const napcatClient = defineEndpointClient<NapcatClient, NapcatClientEventMap>('napcat');
|
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 { napcatRuntimeStateToken } from './napcat-runtime-state.js';
|
|
9
8
|
import { napcatConsoleSpec } from './console-spec.js';
|
|
10
9
|
|
|
11
10
|
export function createNapcatConsoleRpc(
|
|
12
|
-
|
|
11
|
+
_resources: AdapterConsoleResources,
|
|
13
12
|
getConfig: () => unknown,
|
|
14
13
|
): PluginConsoleRpc {
|
|
15
|
-
const state = resources.use(napcatRuntimeStateToken);
|
|
16
14
|
return createAdapterConsoleRpc({
|
|
17
15
|
...napcatConsoleSpec,
|
|
18
|
-
running: () => state.endpoints.values(),
|
|
19
16
|
getConfig,
|
|
20
|
-
}
|
|
17
|
+
});
|
|
21
18
|
}
|
|
@@ -7,23 +7,27 @@
|
|
|
7
7
|
* - group → {group_id:number, name: group_name ?? name}
|
|
8
8
|
* - 群成员列表保持 OneBot11 原生形状,仅保证数组
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
type PendingRpcRequest,
|
|
12
|
+
type EndpointFriend,
|
|
13
|
+
type EndpointGroup,
|
|
14
|
+
type EndpointManagement,
|
|
15
|
+
type EndpointManagementRequest,
|
|
16
|
+
type EndpointGroupRequest,
|
|
17
|
+
type EndpointGroupNoticeRequest,
|
|
14
18
|
} from 'zhin.js/adapter';
|
|
15
19
|
|
|
16
20
|
/** 管理面只依赖 endpoint 的 callApi(ws/wss/http 三种传输各自实现)。 */
|
|
17
21
|
export interface NapCatManagementCaller {
|
|
18
|
-
|
|
22
|
+
requestApi(request: PendingRpcRequest): Promise<unknown>;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export function createNapCatEndpointManagement(
|
|
22
26
|
endpoint: NapCatManagementCaller,
|
|
23
27
|
): EndpointManagement {
|
|
24
28
|
return Object.freeze<EndpointManagement>({
|
|
25
|
-
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
26
|
-
const data = await endpoint.
|
|
29
|
+
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
30
|
+
const data = await endpoint.requestApi({ action: 'get_friend_list', signal: request.signal });
|
|
27
31
|
return toArray(data).map((value) => {
|
|
28
32
|
const friend = asRecord(value);
|
|
29
33
|
return {
|
|
@@ -33,8 +37,8 @@ export function createNapCatEndpointManagement(
|
|
|
33
37
|
};
|
|
34
38
|
});
|
|
35
39
|
},
|
|
36
|
-
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
37
|
-
const data = await endpoint.
|
|
40
|
+
async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
|
|
41
|
+
const data = await endpoint.requestApi({ action: 'get_group_list', signal: request.signal });
|
|
38
42
|
return toArray(data).map((value) => {
|
|
39
43
|
const group = asRecord(value);
|
|
40
44
|
return {
|
|
@@ -43,17 +47,17 @@ export function createNapCatEndpointManagement(
|
|
|
43
47
|
};
|
|
44
48
|
});
|
|
45
49
|
},
|
|
46
|
-
async listGroupMembers(groupId:
|
|
47
|
-
const data = await endpoint.
|
|
50
|
+
async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
|
|
51
|
+
const data = await endpoint.requestApi({ action: 'get_group_member_list', params: {
|
|
48
52
|
group_id: toNumberId(groupId, 'group_id'),
|
|
49
|
-
});
|
|
53
|
+
}, signal });
|
|
50
54
|
return toArray(data);
|
|
51
55
|
},
|
|
52
|
-
async sendGroupNotice(groupId
|
|
53
|
-
await endpoint.
|
|
56
|
+
async sendGroupNotice({ groupId, content, signal }: EndpointGroupNoticeRequest): Promise<void> {
|
|
57
|
+
await endpoint.requestApi({ action: '_send_group_notice', params: {
|
|
54
58
|
group_id: toNumberId(groupId, 'group_id'),
|
|
55
59
|
content,
|
|
56
|
-
});
|
|
60
|
+
}, signal });
|
|
57
61
|
},
|
|
58
62
|
});
|
|
59
63
|
}
|
package/src/http-endpoint.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
|
4
4
|
*/
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
6
|
import {
|
|
7
|
-
Endpoint,
|
|
7
|
+
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
8
8
|
createRecallEndpointControl,
|
|
9
9
|
type EndpointControl,
|
|
10
10
|
type EndpointManagement,
|
|
@@ -38,43 +38,55 @@ import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
|
38
38
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
39
39
|
import { NapcatClient } from './client.js';
|
|
40
40
|
import { readRequestBody } from './webhook.js';
|
|
41
|
-
import { NapCatWsEndpoint } from './ws-endpoint.js';
|
|
42
41
|
import { verifyNapCatAccessToken } from './wss-auth.js';
|
|
43
42
|
|
|
44
43
|
export interface NapCatHttpEndpointOptions {
|
|
45
44
|
readonly id: CapabilityId;
|
|
45
|
+
readonly signal?: AbortSignal;
|
|
46
46
|
readonly http: HttpHost;
|
|
47
47
|
readonly config: NapCatHttpConfig;
|
|
48
48
|
readonly callHttpAction?: typeof callNapCatHttpAction;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export class NapCatHttpEndpoint extends
|
|
52
|
-
readonly
|
|
51
|
+
export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
|
|
52
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
53
53
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
54
54
|
|
|
55
55
|
readonly #options: NapCatHttpEndpointOptions;
|
|
56
56
|
readonly #inboundDeduper = new InboundMessageDeduper();
|
|
57
|
-
readonly management: EndpointManagement = createNapCatEndpointManagement(this
|
|
58
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
59
|
-
readonly content = createNapCatContentPort(
|
|
60
|
-
readonly #callHttpAction: typeof callNapCatHttpAction;
|
|
57
|
+
readonly #management: EndpointManagement = createNapCatEndpointManagement(this);
|
|
58
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
|
|
59
|
+
readonly #content = createNapCatContentPort(request => this.requestApi(request));
|
|
61
60
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
62
61
|
#open = false;
|
|
63
62
|
#started = false;
|
|
64
63
|
#stopped = false;
|
|
65
64
|
|
|
66
65
|
constructor(options: NapCatHttpEndpointOptions) {
|
|
67
|
-
super();
|
|
66
|
+
super({ config: options.config, httpAction: options.callHttpAction ?? callNapCatHttpAction, signal: options.signal });
|
|
68
67
|
this.#logger = getAdapterLogger('napcat', options.config.id);
|
|
69
68
|
this.#options = options;
|
|
70
|
-
|
|
69
|
+
const readTransportState = () => this.#transportState;
|
|
70
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
71
|
+
name: options.config.id,
|
|
72
|
+
start: () => this.#start(), open: () => this.#openEndpoint(),
|
|
73
|
+
close: () => this.#close(), stop: () => this.#stop(),
|
|
74
|
+
send: request => this.#send(request),
|
|
75
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
76
|
+
get transportState() { return readTransportState(); },
|
|
77
|
+
});
|
|
78
|
+
this.on('event', event => {
|
|
79
|
+
void this[endpointSpec].events.run(() => this.#admit(event)).catch((error: unknown) => {
|
|
80
|
+
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
get transportState(): EndpointTransportState {
|
|
85
|
+
get #transportState(): EndpointTransportState {
|
|
74
86
|
return this.#stopped ? 'stopped' : this.#started ? 'open' : 'idle';
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
async start(): Promise<void> {
|
|
89
|
+
async #start(): Promise<void> {
|
|
78
90
|
if (this.#started) return;
|
|
79
91
|
this.#stopped = false;
|
|
80
92
|
this.#started = true;
|
|
@@ -87,15 +99,15 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
87
99
|
}));
|
|
88
100
|
}
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
#openEndpoint(): void {
|
|
91
103
|
this.#open = true;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
close(): void {
|
|
106
|
+
#close(): void {
|
|
95
107
|
this.#open = false;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
async stop(): Promise<void> {
|
|
110
|
+
async #stop(): Promise<void> {
|
|
99
111
|
this.#stopped = true;
|
|
100
112
|
this.#open = false;
|
|
101
113
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
@@ -104,18 +116,10 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
104
116
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
119
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
108
120
|
const message = formatOutboundSegments(payload);
|
|
109
121
|
const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
|
|
110
|
-
const
|
|
111
|
-
{
|
|
112
|
-
http_url: this.#options.config.http_url,
|
|
113
|
-
access_token: this.#options.config.access_token,
|
|
114
|
-
},
|
|
115
|
-
action,
|
|
116
|
-
params,
|
|
117
|
-
);
|
|
118
|
-
const data = resp.data as { message_id?: number | string } | undefined;
|
|
122
|
+
const data = await this.requestApi({ action, params, signal }) as { message_id?: number | string } | undefined;
|
|
119
123
|
const rawId = data?.message_id;
|
|
120
124
|
if ((typeof rawId !== 'number' && typeof rawId !== 'string') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
|
|
121
125
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: platform returned no message_id', 'unknown');
|
|
@@ -130,25 +134,17 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
130
134
|
return messageId;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
137
|
+
async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
|
|
134
138
|
if (!messageId) return;
|
|
135
|
-
await this.
|
|
139
|
+
await this.requestApi({ action: 'delete_msg', params: { message_id: Number(messageId) }, signal });
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
#callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
139
|
-
return this.#callHttpAction(
|
|
140
|
-
{
|
|
141
|
-
http_url: this.#options.config.http_url,
|
|
142
|
-
access_token: this.#options.config.access_token,
|
|
143
|
-
},
|
|
144
|
-
action,
|
|
145
|
-
params,
|
|
146
|
-
).then((resp) => resp.data);
|
|
147
|
-
}
|
|
148
142
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
|
|
144
|
+
#admit(ev: NapCatEvent): void {
|
|
145
|
+
const nativeEvent = ev;
|
|
146
|
+
if (!this.#started) return;
|
|
147
|
+
void this[endpointSpec].events.platform(napCatPlatformEventName(ev), ev).catch((error) => {
|
|
152
148
|
this.#logger.warn(formatCompact({
|
|
153
149
|
op: 'napcat_platform_event_failed',
|
|
154
150
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -156,9 +152,9 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
156
152
|
});
|
|
157
153
|
if (!isMessageEvent(ev)) {
|
|
158
154
|
receiveNapCatSideEvent(
|
|
159
|
-
(name, payload) => this.emit(name, payload),
|
|
155
|
+
(name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
160
156
|
this.#options.config.id,
|
|
161
|
-
this
|
|
157
|
+
this,
|
|
162
158
|
ev,
|
|
163
159
|
this.#logger,
|
|
164
160
|
);
|
|
@@ -170,10 +166,12 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
170
166
|
if (Array.isArray(ev.message) || typeof ev.message === 'string') {
|
|
171
167
|
ev = { ...ev, message: normalizeMessage(ev.message) };
|
|
172
168
|
}
|
|
173
|
-
const conversation = napcatInboundConversation(String(this.#options.id), ev);
|
|
169
|
+
const conversation = { ...napcatInboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
174
170
|
const nickname = senderNickname(ev);
|
|
175
171
|
const mentioned = isNapCatBotMentioned(ev);
|
|
176
|
-
void this.
|
|
172
|
+
void this[endpointSpec].events.message({
|
|
173
|
+
event: nativeEvent,
|
|
174
|
+
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
177
175
|
conversation,
|
|
178
176
|
message: { conversation, id: msgId },
|
|
179
177
|
content: formatInboundContent(ev),
|
|
@@ -227,7 +225,11 @@ export class NapCatHttpEndpoint extends Endpoint<NapcatClient> {
|
|
|
227
225
|
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
228
226
|
return;
|
|
229
227
|
}
|
|
230
|
-
if (this.#open)
|
|
228
|
+
if (this.#open) {
|
|
229
|
+
void this[endpointSpec].events.run(() => this.ingest(ev)).catch((error: unknown) => {
|
|
230
|
+
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
231
233
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
232
234
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
233
235
|
} catch (error) {
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { buildWsConnectOptions, readNapCatActionResponse, type NapCatWsConfig } from './protocol.js';
|
|
3
|
+
import { createNapCatRpc } from './ws-transport.js';
|
|
4
|
+
|
|
5
|
+
/** Separate read-only connection: never changes credentials or state of the running Endpoint. */
|
|
6
|
+
export async function probeNapcatIdentity(config: NapCatWsConfig, signal: AbortSignal): Promise<string | undefined> {
|
|
7
|
+
signal.throwIfAborted();
|
|
8
|
+
const { url, headers } = buildWsConnectOptions(config);
|
|
9
|
+
const socket = new WebSocket(url, { headers });
|
|
10
|
+
const rpc = createNapCatRpc({ endpointId: config.id, signal, socket: () => socket });
|
|
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 response = await rpc.call({ action: 'get_login_info', signal });
|
|
20
|
+
readNapCatActionResponse(response);
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const data: unknown = response.data;
|
|
23
|
+
if (!data || typeof data !== 'object' || !('user_id' in data)
|
|
24
|
+
|| (typeof data.user_id !== 'string' && typeof data.user_id !== 'number') || !String(data.user_id).trim() || String(data.user_id) === '0') return undefined;
|
|
25
|
+
return `qq:${data.user_id}`;
|
|
26
|
+
} finally {
|
|
27
|
+
signal.removeEventListener('abort', abort); rpc.dispose(); socket.terminate();
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export {
|
|
|
27
27
|
type ParsedSendTarget,
|
|
28
28
|
type ResolvedNapCatConfig,
|
|
29
29
|
} from './protocol.js';
|
|
30
|
+
export { NapcatAdapter } from './adapter.js';
|
|
30
31
|
|
|
31
32
|
export {
|
|
32
33
|
InboundMessageDeduper,
|
|
@@ -36,9 +37,13 @@ export {
|
|
|
36
37
|
} from './napcat-inbound.js';
|
|
37
38
|
|
|
38
39
|
export {
|
|
39
|
-
napcatClient,
|
|
40
40
|
type NapcatClient,
|
|
41
41
|
type NapcatClientEventMap,
|
|
42
|
+
type NapcatClientOptions,
|
|
43
|
+
type NapcatMemberTitleRequest,
|
|
44
|
+
type NapcatProfileRequest,
|
|
45
|
+
type NapcatGroupFileRequest,
|
|
46
|
+
type NapcatMiniAppRequest,
|
|
42
47
|
} from './client.js';
|
|
43
48
|
|
|
44
49
|
export { parseOneBotGetMsgResponse } from './onebot-get-msg.js';
|
|
@@ -61,11 +66,10 @@ export {
|
|
|
61
66
|
export type { NapCatWsSocket, NapCatWsCreateOptions } from './ws-types.js';
|
|
62
67
|
|
|
63
68
|
export {
|
|
64
|
-
|
|
69
|
+
createNapCatRpc,
|
|
65
70
|
decodeWsPayload,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
startNapCatHeartbeat,
|
|
71
|
+
type NapCatRpc,
|
|
72
|
+
type NapCatRpcOptions,
|
|
69
73
|
} from './ws-transport.js';
|
|
70
74
|
|
|
71
75
|
export { verifyNapCatAccessToken } from './wss-auth.js';
|
package/src/onebot-get-msg.ts
CHANGED
|
@@ -39,7 +39,7 @@ export function parseOneBotGetMsgResponse(
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function createNapCatContentPort(
|
|
42
|
-
callApi: (
|
|
42
|
+
callApi: (request: PendingRpcRequest) => Promise<unknown>,
|
|
43
43
|
): EndpointContentPort {
|
|
44
44
|
return Object.freeze({
|
|
45
45
|
async resolve(reference: ConversationReference, context: EndpointContentResolveContext) {
|
|
@@ -51,7 +51,7 @@ export function createNapCatContentPort(
|
|
|
51
51
|
: Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
|
|
52
52
|
}
|
|
53
53
|
if (reference.kind === 'message') {
|
|
54
|
-
const parsed = parseOneBotGetMsgResponse(reference.message.id, await callApi('get_msg', { message_id: reference.message.id }));
|
|
54
|
+
const parsed = parseOneBotGetMsgResponse(reference.message.id, await callApi({ action: 'get_msg', params: { message_id: reference.message.id }, signal: context.signal }));
|
|
55
55
|
context.signal.throwIfAborted();
|
|
56
56
|
const value: ConversationMessage = Object.freeze({
|
|
57
57
|
ref: reference.message,
|
|
@@ -61,7 +61,7 @@ export function createNapCatContentPort(
|
|
|
61
61
|
});
|
|
62
62
|
return Object.freeze({ status: 'resolved' as const, reference, value });
|
|
63
63
|
}
|
|
64
|
-
const raw = await callApi('get_forward_msg', { message_id: reference.forwardId, id: reference.forwardId }) as Record<string, unknown>;
|
|
64
|
+
const raw = await callApi({ action: 'get_forward_msg', params: { message_id: reference.forwardId, id: reference.forwardId }, signal: context.signal }) as Record<string, unknown>;
|
|
65
65
|
context.signal.throwIfAborted();
|
|
66
66
|
const nodes = Array.isArray(raw.messages) ? raw.messages.slice(0, context.maxEntries) : [];
|
|
67
67
|
const value: readonly ForwardEntry[] = Object.freeze(nodes.map((node) => {
|
|
@@ -93,5 +93,5 @@ function toMillis(value: unknown): number {
|
|
|
93
93
|
const time = Number(value);
|
|
94
94
|
return Number.isFinite(time) ? (time < 10_000_000_000 ? time * 1000 : time) : Date.now();
|
|
95
95
|
}
|
|
96
|
-
import type { EndpointContentPort, EndpointContentResolveContext } from 'zhin.js/adapter';
|
|
96
|
+
import type { PendingRpcRequest, EndpointContentPort, EndpointContentResolveContext } from 'zhin.js/adapter';
|
|
97
97
|
import type { ConversationMessage, ConversationReference, ForwardEntry } from '@zhin.js/im-contract';
|