@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +8 -5
  3. package/adapters/napcat/index.js +2 -29
  4. package/adapters/napcat/index.ts +3 -35
  5. package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +1 -1
  6. package/agents/napcat/skills/napcat-account/tools/set_profile/index.ts +1 -1
  7. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.js +1 -1
  8. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.ts +1 -1
  9. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +1 -1
  10. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +1 -1
  11. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +1 -1
  12. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts +1 -1
  13. package/commands/napcat/endpoint/definition.js +0 -2
  14. package/commands/napcat/endpoint/definition.ts +0 -2
  15. package/lib/adapter.d.ts +23 -0
  16. package/lib/adapter.js +42 -0
  17. package/lib/client.d.ts +53 -16
  18. package/lib/client.js +53 -10
  19. package/lib/console.d.ts +1 -1
  20. package/lib/console.js +2 -5
  21. package/lib/endpoint-management.d.ts +2 -2
  22. package/lib/endpoint-management.js +13 -13
  23. package/lib/http-endpoint.d.ts +5 -15
  24. package/lib/http-endpoint.js +44 -36
  25. package/lib/identity.d.ts +3 -0
  26. package/lib/identity.js +32 -0
  27. package/lib/index.d.ts +3 -2
  28. package/lib/index.js +2 -2
  29. package/lib/onebot-get-msg.d.ts +2 -2
  30. package/lib/onebot-get-msg.js +2 -2
  31. package/lib/protocol.d.ts +5 -0
  32. package/lib/protocol.js +23 -11
  33. package/lib/ws-endpoint.d.ts +6 -16
  34. package/lib/ws-endpoint.js +57 -48
  35. package/lib/ws-transport.d.ts +18 -12
  36. package/lib/ws-transport.js +28 -72
  37. package/lib/ws-types.d.ts +0 -5
  38. package/lib/wss-endpoint.d.ts +7 -15
  39. package/lib/wss-endpoint.js +56 -47
  40. package/package.json +18 -18
  41. package/plugin.js +0 -3
  42. package/src/adapter.ts +45 -0
  43. package/src/client.ts +91 -19
  44. package/src/console.ts +2 -5
  45. package/src/endpoint-management.ts +19 -15
  46. package/src/http-endpoint.ts +47 -45
  47. package/src/identity.ts +29 -0
  48. package/src/index.ts +9 -5
  49. package/src/onebot-get-msg.ts +4 -4
  50. package/src/protocol.ts +26 -14
  51. package/src/ws-endpoint.ts +61 -54
  52. package/src/ws-transport.ts +40 -101
  53. package/src/ws-types.ts +0 -6
  54. package/src/wss-endpoint.ts +60 -54
  55. package/lib/napcat-runtime-state.d.ts +0 -1
  56. package/lib/napcat-runtime-state.js +0 -6
  57. 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.18",
3
+ "version": "1.1.20",
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.25",
49
- "@zhin.js/agent-feature": "1.1.10",
50
- "@zhin.js/client": "1.1.21",
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",
53
- "@zhin.js/feature-kit": "1.1.9",
54
- "@zhin.js/host-http": "1.1.11",
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.10",
58
- "@zhin.js/permission": "1.1.9",
59
- "@zhin.js/skill": "1.1.10",
60
- "@zhin.js/tool": "1.1.12"
57
+ "@zhin.js/page": "1.1.12",
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.11",
69
- "zhin.js": "1.1.18"
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.25",
74
- "@zhin.js/command": "^1.1.11",
75
- "@zhin.js/core": "^1.1.53",
76
- "zhin.js": "^1.1.18"
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": "^1.0.0"
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 { defineEndpointClient } from 'zhin.js/adapter';
2
- import type { NapCatEvent } from './protocol.js';
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
- constructor(readonly callApi: NapcatApiCall) {}
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
- async setTitle(groupId: number, userId: number, title: string, duration = -1): Promise<boolean> {
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: string, company?: string, email?: string, college?: string, personalNote?: string) {
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: number, file: string, name: string, folder?: string) { return this.callApi('upload_group_file', { group_id: groupId, file, name, folder }); }
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: string, title: string, desc: string, picUrl: string, jumpUrl: string) { return this.callApi('get_mini_app_ark', { type, title, desc, picUrl, jumpUrl }); }
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
- resources: AdapterConsoleResources,
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
- }, resources);
17
+ });
21
18
  }
@@ -7,23 +7,27 @@
7
7
  * - group → {group_id:number, name: group_name ?? name}
8
8
  * - 群成员列表保持 OneBot11 原生形状,仅保证数组
9
9
  */
10
- import type {
11
- EndpointFriend,
12
- EndpointGroup,
13
- EndpointManagement,
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
- callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
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.callApi('get_friend_list');
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.callApi('get_group_list');
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: string): Promise<readonly unknown[]> {
47
- const data = await endpoint.callApi('get_group_member_list', {
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: string, content: string): Promise<void> {
53
- await endpoint.callApi('_send_group_notice', {
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
  }
@@ -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 Endpoint<NapcatClient> {
52
- readonly client = new NapcatClient((action, params) => this.#callApi(action, params));
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.client);
58
- readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
59
- readonly content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
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
- this.#callHttpAction = options.callHttpAction ?? callNapCatHttpAction;
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
- open(): void {
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 resp = await this.#callHttpAction(
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.client.callApi('delete_msg', { message_id: Number(messageId) });
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
- admit(ev: NapCatEvent): void {
150
- if (!this.#open) return;
151
- void this.emitPlatform(napCatPlatformEventName(ev), ev).catch((error) => {
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.client,
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.emit('message.receive', {
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) this.admit(ev);
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) {
@@ -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
- callNapCatWsAction,
69
+ createNapCatRpc,
65
70
  decodeWsPayload,
66
- handleNapCatWsMessage,
67
- rejectAllPending,
68
- startNapCatHeartbeat,
71
+ type NapCatRpc,
72
+ type NapCatRpcOptions,
69
73
  } from './ws-transport.js';
70
74
 
71
75
  export { verifyNapCatAccessToken } from './wss-auth.js';
@@ -39,7 +39,7 @@ export function parseOneBotGetMsgResponse(
39
39
  }
40
40
 
41
41
  export function createNapCatContentPort(
42
- callApi: (action: string, params?: Record<string, unknown>) => Promise<unknown>,
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';