@zhin.js/adapter-napcat 3.0.2 → 4.0.1

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 (183) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +43 -95
  3. package/adapters/napcat.ts +39 -0
  4. package/agent/tools/ai_tts.ts +2 -2
  5. package/agent/tools/del_group_notice.ts +2 -2
  6. package/agent/tools/delete_essence_msg.ts +2 -2
  7. package/agent/tools/delete_friend.ts +2 -2
  8. package/agent/tools/download_file.ts +2 -2
  9. package/agent/tools/forward_single_msg.ts +2 -2
  10. package/agent/tools/get_ai_characters.ts +2 -2
  11. package/agent/tools/get_essence_list.ts +2 -2
  12. package/agent/tools/get_friend_msg_history.ts +2 -2
  13. package/agent/tools/get_group_file_url.ts +2 -2
  14. package/agent/tools/get_group_info_ex.ts +2 -2
  15. package/agent/tools/get_group_msg_history.ts +2 -2
  16. package/agent/tools/get_group_notice.ts +2 -2
  17. package/agent/tools/get_group_root_files.ts +2 -2
  18. package/agent/tools/get_group_shut_list.ts +2 -2
  19. package/agent/tools/get_mini_app_ark.ts +2 -2
  20. package/agent/tools/get_user_status.ts +2 -2
  21. package/agent/tools/group_sign.ts +2 -2
  22. package/agent/tools/mark_msg_as_read.ts +2 -2
  23. package/agent/tools/ocr_image.ts +2 -2
  24. package/agent/tools/send_forward_msg.ts +2 -2
  25. package/agent/tools/send_group_notice.ts +2 -2
  26. package/agent/tools/send_like.ts +2 -2
  27. package/agent/tools/send_poke.ts +2 -2
  28. package/agent/tools/set_avatar.ts +2 -2
  29. package/agent/tools/set_emoji_reaction.ts +2 -2
  30. package/agent/tools/set_essence_msg.ts +2 -2
  31. package/agent/tools/set_group_portrait.ts +2 -2
  32. package/agent/tools/set_online_status.ts +2 -2
  33. package/agent/tools/set_profile.ts +2 -2
  34. package/agent/tools/set_signature.ts +2 -2
  35. package/agent/tools/set_title.ts +2 -2
  36. package/agent/tools/translate.ts +2 -2
  37. package/agent/tools/upload_group_file.ts +2 -2
  38. package/lib/http-endpoint.d.ts +26 -0
  39. package/lib/http-endpoint.js +147 -0
  40. package/lib/index.d.ts +11 -0
  41. package/lib/index.js +10 -0
  42. package/lib/napcat-agent-deps.d.ts +51 -0
  43. package/lib/napcat-agent-deps.js +33 -0
  44. package/lib/napcat-inbound.d.ts +28 -0
  45. package/lib/{src/napcat-inbound.js → napcat-inbound.js} +28 -13
  46. package/lib/onebot-get-msg.d.ts +14 -0
  47. package/lib/{src/onebot-get-msg.js → onebot-get-msg.js} +0 -1
  48. package/lib/protocol.d.ts +151 -0
  49. package/lib/protocol.js +217 -0
  50. package/lib/webhook.d.ts +2 -0
  51. package/lib/webhook.js +14 -0
  52. package/lib/ws-endpoint.d.ts +61 -0
  53. package/lib/ws-endpoint.js +357 -0
  54. package/lib/ws-transport.d.ts +13 -0
  55. package/lib/ws-transport.js +75 -0
  56. package/lib/ws-types.d.ts +17 -0
  57. package/lib/ws-types.js +1 -0
  58. package/lib/wss-auth.d.ts +2 -0
  59. package/lib/wss-auth.js +16 -0
  60. package/lib/wss-endpoint.d.ts +25 -0
  61. package/lib/wss-endpoint.js +157 -0
  62. package/package.json +44 -33
  63. package/plugin.ts +13 -0
  64. package/schema.json +48 -0
  65. package/src/http-endpoint.ts +197 -0
  66. package/src/index.ts +66 -81
  67. package/src/napcat-agent-deps.ts +81 -15
  68. package/src/napcat-inbound.ts +34 -10
  69. package/src/onebot-get-msg.ts +8 -2
  70. package/src/protocol.ts +373 -0
  71. package/src/webhook.ts +16 -0
  72. package/src/ws-endpoint.ts +452 -0
  73. package/src/ws-transport.ts +105 -0
  74. package/src/ws-types.ts +20 -0
  75. package/src/wss-auth.ts +17 -0
  76. package/src/wss-endpoint.ts +205 -0
  77. package/client/NapCatManagement.tsx +0 -113
  78. package/client/index.tsx +0 -11
  79. package/client/tsconfig.json +0 -7
  80. package/client/utils/api.ts +0 -30
  81. package/dist/index.js +0 -27
  82. package/lib/agent/tools/ai_tts.js +0 -21
  83. package/lib/agent/tools/ai_tts.js.map +0 -1
  84. package/lib/agent/tools/del_group_notice.js +0 -22
  85. package/lib/agent/tools/del_group_notice.js.map +0 -1
  86. package/lib/agent/tools/delete_essence_msg.js +0 -21
  87. package/lib/agent/tools/delete_essence_msg.js.map +0 -1
  88. package/lib/agent/tools/delete_friend.js +0 -20
  89. package/lib/agent/tools/delete_friend.js.map +0 -1
  90. package/lib/agent/tools/download_file.js +0 -19
  91. package/lib/agent/tools/download_file.js.map +0 -1
  92. package/lib/agent/tools/forward_single_msg.js +0 -24
  93. package/lib/agent/tools/forward_single_msg.js.map +0 -1
  94. package/lib/agent/tools/get_ai_characters.js +0 -19
  95. package/lib/agent/tools/get_ai_characters.js.map +0 -1
  96. package/lib/agent/tools/get_essence_list.js +0 -19
  97. package/lib/agent/tools/get_essence_list.js.map +0 -1
  98. package/lib/agent/tools/get_friend_msg_history.js +0 -20
  99. package/lib/agent/tools/get_friend_msg_history.js.map +0 -1
  100. package/lib/agent/tools/get_group_file_url.js +0 -21
  101. package/lib/agent/tools/get_group_file_url.js.map +0 -1
  102. package/lib/agent/tools/get_group_info_ex.js +0 -19
  103. package/lib/agent/tools/get_group_info_ex.js.map +0 -1
  104. package/lib/agent/tools/get_group_msg_history.js +0 -21
  105. package/lib/agent/tools/get_group_msg_history.js.map +0 -1
  106. package/lib/agent/tools/get_group_notice.js +0 -19
  107. package/lib/agent/tools/get_group_notice.js.map +0 -1
  108. package/lib/agent/tools/get_group_root_files.js +0 -19
  109. package/lib/agent/tools/get_group_root_files.js.map +0 -1
  110. package/lib/agent/tools/get_group_shut_list.js +0 -19
  111. package/lib/agent/tools/get_group_shut_list.js.map +0 -1
  112. package/lib/agent/tools/get_mini_app_ark.js +0 -22
  113. package/lib/agent/tools/get_mini_app_ark.js.map +0 -1
  114. package/lib/agent/tools/get_user_status.js +0 -18
  115. package/lib/agent/tools/get_user_status.js.map +0 -1
  116. package/lib/agent/tools/group_sign.js +0 -20
  117. package/lib/agent/tools/group_sign.js.map +0 -1
  118. package/lib/agent/tools/mark_msg_as_read.js +0 -19
  119. package/lib/agent/tools/mark_msg_as_read.js.map +0 -1
  120. package/lib/agent/tools/ocr_image.js +0 -18
  121. package/lib/agent/tools/ocr_image.js.map +0 -1
  122. package/lib/agent/tools/send_forward_msg.js +0 -21
  123. package/lib/agent/tools/send_forward_msg.js.map +0 -1
  124. package/lib/agent/tools/send_group_notice.js +0 -23
  125. package/lib/agent/tools/send_group_notice.js.map +0 -1
  126. package/lib/agent/tools/send_like.js +0 -20
  127. package/lib/agent/tools/send_like.js.map +0 -1
  128. package/lib/agent/tools/send_poke.js +0 -20
  129. package/lib/agent/tools/send_poke.js.map +0 -1
  130. package/lib/agent/tools/set_avatar.js +0 -19
  131. package/lib/agent/tools/set_avatar.js.map +0 -1
  132. package/lib/agent/tools/set_emoji_reaction.js +0 -20
  133. package/lib/agent/tools/set_emoji_reaction.js.map +0 -1
  134. package/lib/agent/tools/set_essence_msg.js +0 -21
  135. package/lib/agent/tools/set_essence_msg.js.map +0 -1
  136. package/lib/agent/tools/set_group_portrait.js +0 -22
  137. package/lib/agent/tools/set_group_portrait.js.map +0 -1
  138. package/lib/agent/tools/set_online_status.js +0 -20
  139. package/lib/agent/tools/set_online_status.js.map +0 -1
  140. package/lib/agent/tools/set_profile.js +0 -23
  141. package/lib/agent/tools/set_profile.js.map +0 -1
  142. package/lib/agent/tools/set_signature.js +0 -19
  143. package/lib/agent/tools/set_signature.js.map +0 -1
  144. package/lib/agent/tools/set_title.js +0 -23
  145. package/lib/agent/tools/set_title.js.map +0 -1
  146. package/lib/agent/tools/translate.js +0 -18
  147. package/lib/agent/tools/translate.js.map +0 -1
  148. package/lib/agent/tools/upload_group_file.js +0 -23
  149. package/lib/agent/tools/upload_group_file.js.map +0 -1
  150. package/lib/src/adapter.js +0 -102
  151. package/lib/src/adapter.js.map +0 -1
  152. package/lib/src/agent-prompt.js +0 -106
  153. package/lib/src/agent-prompt.js.map +0 -1
  154. package/lib/src/endpoint-base.js +0 -315
  155. package/lib/src/endpoint-base.js.map +0 -1
  156. package/lib/src/endpoint-http.js +0 -106
  157. package/lib/src/endpoint-http.js.map +0 -1
  158. package/lib/src/endpoint-ws-client.js +0 -139
  159. package/lib/src/endpoint-ws-client.js.map +0 -1
  160. package/lib/src/endpoint-ws-server.js +0 -137
  161. package/lib/src/endpoint-ws-server.js.map +0 -1
  162. package/lib/src/index.js +0 -58
  163. package/lib/src/index.js.map +0 -1
  164. package/lib/src/napcat-agent-deps.js +0 -19
  165. package/lib/src/napcat-agent-deps.js.map +0 -1
  166. package/lib/src/napcat-inbound.js.map +0 -1
  167. package/lib/src/onebot-get-msg.js.map +0 -1
  168. package/lib/src/routes.js +0 -61
  169. package/lib/src/routes.js.map +0 -1
  170. package/lib/src/segment-mapper.js +0 -2
  171. package/lib/src/segment-mapper.js.map +0 -1
  172. package/lib/src/types.js +0 -6
  173. package/lib/src/types.js.map +0 -1
  174. package/plugin.yml +0 -3
  175. package/src/adapter.ts +0 -109
  176. package/src/agent-prompt.ts +0 -115
  177. package/src/endpoint-base.ts +0 -362
  178. package/src/endpoint-http.ts +0 -95
  179. package/src/endpoint-ws-client.ts +0 -137
  180. package/src/endpoint-ws-server.ts +0 -136
  181. package/src/routes.ts +0 -61
  182. package/src/segment-mapper.ts +0 -1
  183. package/src/types.ts +0 -266
@@ -0,0 +1,75 @@
1
+ import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
2
+ import { formatCompact, getLogger } from '@zhin.js/logger';
3
+ import { WS_OPEN, } from './ws-types.js';
4
+ const logger = getLogger('napcat');
5
+ export function decodeWsPayload(data) {
6
+ if (typeof data === 'string')
7
+ return data;
8
+ if (Buffer.isBuffer(data))
9
+ return data.toString();
10
+ if (data instanceof ArrayBuffer)
11
+ return new TextDecoder().decode(data);
12
+ return String(data ?? '');
13
+ }
14
+ export function handleNapCatWsMessage(data, options) {
15
+ try {
16
+ const raw = decodeWsPayload(data);
17
+ const msg = JSON.parse(raw);
18
+ if ('echo' in msg && typeof msg.echo === 'string') {
19
+ const resp = msg;
20
+ const pending = options.pending.get(resp.echo);
21
+ if (pending) {
22
+ options.pending.delete(resp.echo);
23
+ clearTimeout(pending.timeout);
24
+ if (resp.status === 'ok')
25
+ pending.resolve(resp.data);
26
+ else {
27
+ pending.reject(new Error(`API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`));
28
+ }
29
+ }
30
+ return;
31
+ }
32
+ options.admit(msg);
33
+ }
34
+ catch (error) {
35
+ logger.warn(formatCompact({
36
+ op: 'napcat_parse_failed',
37
+ endpoint: options.endpointName,
38
+ error: error instanceof Error ? error.message : String(error),
39
+ }));
40
+ }
41
+ }
42
+ export function callNapCatWsAction(ws, pending, requestId, action, params) {
43
+ if (!ws || ws.readyState !== WS_OPEN) {
44
+ return Promise.reject(new Error('WebSocket is not connected'));
45
+ }
46
+ const echo = `req_${++requestId.value}`;
47
+ const req = { action, params, echo };
48
+ return new Promise((resolve, reject) => {
49
+ const timeout = setTimeout(() => {
50
+ pending.delete(echo);
51
+ reject(new Error(`API call timeout: ${action}`));
52
+ }, 30_000);
53
+ pending.set(echo, { resolve, reject, timeout });
54
+ ws.send(JSON.stringify(req));
55
+ });
56
+ }
57
+ export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
58
+ if (existingTimer)
59
+ clearInterval(existingTimer);
60
+ return setInterval(() => {
61
+ try {
62
+ ws?.ping?.();
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ }, intervalMs);
68
+ }
69
+ export function rejectAllPending(pending, message = 'Connection closed') {
70
+ for (const [, entry] of pending) {
71
+ clearTimeout(entry.timeout);
72
+ entry.reject(new Error(message));
73
+ }
74
+ pending.clear();
75
+ }
@@ -0,0 +1,17 @@
1
+ /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
+ export interface NapCatWsSocket {
3
+ readonly readyState: number;
4
+ send(data: string): void;
5
+ close(code?: number, reason?: string): void;
6
+ ping?(): void;
7
+ on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
8
+ }
9
+ export interface NapCatWsCreateOptions {
10
+ readonly headers?: Record<string, string>;
11
+ }
12
+ export declare const WS_OPEN = 1;
13
+ export interface NapCatPendingAction {
14
+ resolve: (value: unknown) => void;
15
+ reject: (err: Error) => void;
16
+ timeout: NodeJS.Timeout;
17
+ }
@@ -0,0 +1 @@
1
+ export const WS_OPEN = 1;
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare function verifyNapCatAccessToken(accessToken: string | undefined, request: IncomingMessage): boolean;
@@ -0,0 +1,16 @@
1
+ export function verifyNapCatAccessToken(accessToken, request) {
2
+ if (!accessToken)
3
+ return true;
4
+ const auth = request.headers.authorization ?? '';
5
+ if (auth === `Bearer ${accessToken}`)
6
+ return true;
7
+ try {
8
+ const url = new URL(request.url ?? '/', 'http://localhost');
9
+ if (url.searchParams.get('access_token') === accessToken)
10
+ return true;
11
+ }
12
+ catch {
13
+ /* ignore */
14
+ }
15
+ return false;
16
+ }
@@ -0,0 +1,25 @@
1
+ import type { EndpointInstance } from '@zhin.js/adapter';
2
+ import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { HttpHost } from '@zhin.js/host-http';
4
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
5
+ import { type NapCatEvent, type NapCatWssConfig } from './protocol.js';
6
+ export interface NapCatWssEndpointOptions {
7
+ readonly id: CapabilityId;
8
+ readonly gateway: MessageGateway;
9
+ readonly http: HttpHost;
10
+ readonly config: NapCatWssConfig;
11
+ }
12
+ export declare class NapCatWssEndpoint implements EndpointInstance {
13
+ #private;
14
+ constructor(options: NapCatWssEndpointOptions);
15
+ start(): Promise<void>;
16
+ open(): void;
17
+ close(): void;
18
+ stop(): Promise<void>;
19
+ send({ target, payload }: {
20
+ readonly target: string;
21
+ readonly payload: unknown;
22
+ }): Promise<string>;
23
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
24
+ admit(ev: NapCatEvent): void;
25
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
3
+ */
4
+ import { clearInterval } from 'node:timers';
5
+ import { formatCompact, getLogger } from '@zhin.js/logger';
6
+ import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
7
+ import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
8
+ import { buildSendAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
9
+ import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, startNapCatHeartbeat, } from './ws-transport.js';
10
+ import { verifyNapCatAccessToken } from './wss-auth.js';
11
+ const logger = getLogger('napcat');
12
+ export class NapCatWssEndpoint {
13
+ #options;
14
+ #inboundDeduper = new InboundMessageDeduper();
15
+ #ws;
16
+ #wsRelease;
17
+ #heartbeatTimer;
18
+ #requestId = { value: 0 };
19
+ #pending = new Map();
20
+ #open = false;
21
+ #started = false;
22
+ #unregisterAgent;
23
+ constructor(options) {
24
+ this.#options = options;
25
+ }
26
+ async start() {
27
+ if (this.#started)
28
+ return;
29
+ this.#started = true;
30
+ this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
31
+ const handle = this.#options.http.ws(this.#options.config.path);
32
+ this.#wsRelease = handle.onConnection((connection) => {
33
+ this.#acceptConnection(connection);
34
+ });
35
+ logger.info(formatCompact({
36
+ op: 'listen',
37
+ endpoint: this.#options.config.name,
38
+ mode: 'wss',
39
+ path: this.#options.config.path,
40
+ }));
41
+ }
42
+ open() {
43
+ this.#open = true;
44
+ }
45
+ close() {
46
+ this.#open = false;
47
+ }
48
+ async stop() {
49
+ this.#open = false;
50
+ this.#unregisterAgent?.();
51
+ this.#unregisterAgent = undefined;
52
+ this.#wsRelease?.();
53
+ this.#wsRelease = undefined;
54
+ if (this.#heartbeatTimer) {
55
+ clearInterval(this.#heartbeatTimer);
56
+ this.#heartbeatTimer = undefined;
57
+ }
58
+ rejectAllPending(this.#pending);
59
+ this.#inboundDeduper.clear();
60
+ if (this.#ws) {
61
+ try {
62
+ this.#ws.close();
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ this.#ws = undefined;
68
+ }
69
+ this.#started = false;
70
+ }
71
+ async send({ target, payload }) {
72
+ const message = formatOutboundSegments(payload);
73
+ const { action, params } = buildSendAction(target, message);
74
+ const data = await this.callApi(action, params);
75
+ return data?.message_id != null ? String(data.message_id) : '';
76
+ }
77
+ callApi(action, params = {}) {
78
+ return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
79
+ }
80
+ admit(ev) {
81
+ if (!this.#open || !isMessageEvent(ev))
82
+ return;
83
+ if (isSelfMessage(ev))
84
+ return;
85
+ const msgId = String(ev.message_id);
86
+ if (!this.#inboundDeduper.shouldProcess(msgId))
87
+ return;
88
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
89
+ ev = { ...ev, message: normalizeMessage(ev.message) };
90
+ }
91
+ const target = formatInboundTarget(ev);
92
+ const nickname = senderNickname(ev);
93
+ const mentioned = isNapCatBotMentioned(ev);
94
+ void this.#options.gateway.receive({
95
+ adapter: this.#options.id,
96
+ target,
97
+ content: formatInboundContent(ev),
98
+ sender: senderUserId(ev),
99
+ id: msgId,
100
+ metadata: Object.freeze({
101
+ message_type: ev.message_type,
102
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
103
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
104
+ endpoint: this.#options.config.name,
105
+ time: ev.time,
106
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
107
+ role: ev.sender?.role,
108
+ ...(nickname ? { nickname } : {}),
109
+ ...(mentioned ? { mentioned: true } : {}),
110
+ }),
111
+ }).catch((err) => {
112
+ logger.warn(formatCompact({
113
+ op: 'napcat_gateway_receive_failed',
114
+ target,
115
+ error: err instanceof Error ? err.message : String(err),
116
+ }));
117
+ });
118
+ }
119
+ #acceptConnection(connection) {
120
+ if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
121
+ connection.socket.close(4003, 'Unauthorized');
122
+ return;
123
+ }
124
+ const socket = connection.socket;
125
+ if (this.#ws) {
126
+ try {
127
+ this.#ws.close();
128
+ }
129
+ catch {
130
+ /* ignore */
131
+ }
132
+ }
133
+ this.#ws = socket;
134
+ this.#heartbeatTimer = startNapCatHeartbeat(this.#ws, this.#options.config.heartbeat_interval, this.#heartbeatTimer);
135
+ socket.on('message', (data) => {
136
+ handleNapCatWsMessage(data, {
137
+ endpointName: this.#options.config.name,
138
+ pending: this.#pending,
139
+ admit: (event) => this.admit(event),
140
+ });
141
+ });
142
+ socket.on('close', () => {
143
+ if (this.#ws === socket) {
144
+ this.#ws = undefined;
145
+ if (this.#heartbeatTimer) {
146
+ clearInterval(this.#heartbeatTimer);
147
+ this.#heartbeatTimer = undefined;
148
+ }
149
+ }
150
+ });
151
+ logger.debug(formatCompact({
152
+ endpoint: this.#options.config.name,
153
+ mode: 'wss',
154
+ peer: connection.request.socket.remoteAddress,
155
+ }));
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-napcat",
3
- "version": "3.0.2",
4
- "description": "Zhin.js adapter for NapCatQQ (OneBot11 superset with go-cqhttp & NapCat extensions)",
3
+ "version": "4.0.1",
4
+ "description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -13,12 +13,12 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "adapters",
17
+ "plugin.ts",
18
+ "schema.json",
16
19
  "src",
17
20
  "lib",
18
21
  "agent",
19
- "client",
20
- "dist",
21
- "plugin.yml",
22
22
  "README.md",
23
23
  "CHANGELOG.md"
24
24
  ],
@@ -34,57 +34,53 @@
34
34
  "qq",
35
35
  "chatbot"
36
36
  ],
37
- "author": {
38
- "name": "lc-cn",
39
- "email": "admin@liucl.cn",
40
- "url": "https://github.com/lc-cn"
41
- },
42
37
  "license": "MIT",
43
38
  "repository": {
44
- "url": "git+https://github.com/zhinjs/zhin.git",
45
39
  "type": "git",
40
+ "url": "https://github.com/zhinjs/zhin",
46
41
  "directory": "plugins/adapters/napcat"
47
42
  },
48
43
  "dependencies": {
49
- "ws": "^8.21.0"
44
+ "ws": "^8.21.0",
45
+ "@zhin.js/adapter": "1.0.1",
46
+ "@zhin.js/core": "1.3.5",
47
+ "@zhin.js/host-http": "1.0.1",
48
+ "@zhin.js/logger": "1.0.75",
49
+ "@zhin.js/plugin-runtime": "1.0.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^26.1.0",
53
- "@types/react": "^19.2.17",
54
- "@types/react-dom": "^19.2.3",
55
53
  "@types/ws": "^8.18.1",
56
- "lucide-react": "^1.22.0",
57
54
  "typescript": "^6.0.3",
55
+ "vitest": "^4.1.10",
58
56
  "zod": "^4.4.3",
59
- "@zhin.js/agent": "1.0.3",
60
- "@zhin.js/cli": "1.0.93",
61
- "@zhin.js/contract": "1.0.3",
62
- "@zhin.js/host-api": "2.0.5",
63
- "@zhin.js/host-router": "2.0.3",
64
- "zhin.js": "4.1.2"
57
+ "@zhin.js/agent": "1.0.4",
58
+ "@zhin.js/host-http": "1.0.1"
65
59
  },
66
60
  "peerDependencies": {
67
61
  "zod": "^4.0.0",
68
- "@zhin.js/agent": "1.0.3",
69
- "@zhin.js/host-api": "2.0.5",
70
- "@zhin.js/contract": "1.0.3",
71
- "@zhin.js/host-router": "2.0.3",
72
- "zhin.js": "4.1.2"
62
+ "@zhin.js/adapter": "1.0.1",
63
+ "@zhin.js/core": "1.3.5",
64
+ "@zhin.js/plugin-runtime": "1.0.1",
65
+ "zhin.js": "4.1.3",
66
+ "@zhin.js/agent": "1.0.4"
73
67
  },
74
68
  "peerDependenciesMeta": {
75
- "@zhin.js/agent": {
76
- "optional": true
77
- },
78
- "@zhin.js/host-api": {
69
+ "zhin.js": {
79
70
  "optional": true
80
71
  },
81
- "@zhin.js/host-router": {
72
+ "@zhin.js/agent": {
82
73
  "optional": true
83
74
  },
84
75
  "zod": {
85
76
  "optional": true
86
77
  }
87
78
  },
79
+ "author": {
80
+ "name": "lc-cn",
81
+ "email": "admin@liucl.cn",
82
+ "url": "https://github.com/lc-cn"
83
+ },
88
84
  "publishConfig": {
89
85
  "access": "public",
90
86
  "registry": "https://registry.npmjs.org"
@@ -92,8 +88,23 @@
92
88
  "engines": {
93
89
  "node": "^20.19.0 || >=22.12.0"
94
90
  },
91
+ "zhin": {
92
+ "protocol": 1,
93
+ "type": "plugin",
94
+ "entry": "./plugin.ts",
95
+ "engine": "^1.0.0",
96
+ "runtime": "trusted",
97
+ "features": [
98
+ {
99
+ "package": "@zhin.js/adapter",
100
+ "api": "^1.0.0"
101
+ }
102
+ ],
103
+ "plugins": []
104
+ },
95
105
  "scripts": {
96
- "build": "zhin build",
97
- "clean": "rimraf lib"
106
+ "build": "tsc",
107
+ "clean": "rimraf lib",
108
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/napcat/tests"
98
109
  }
99
110
  }
package/plugin.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { definePlugin } from '@zhin.js/plugin-runtime';
2
+ import { registerDefaultScenePlatformPermitChecker } from '@zhin.js/core';
3
+
4
+ export default definePlugin({
5
+ name: 'napcat',
6
+ metadata: {
7
+ displayName: 'NapCat Adapter',
8
+ },
9
+ setup() {
10
+ // 平台权限门禁:scene_admin / scene_owner 由 sender role 判定(见各 endpoint admit metadata)
11
+ return registerDefaultScenePlatformPermitChecker('napcat');
12
+ },
13
+ });
package/schema.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "connection": {
7
+ "type": "string",
8
+ "enum": ["ws", "wss", "http"],
9
+ "default": "ws",
10
+ "description": "ws (default), wss (reverse WS), or http (POST webhook + HTTP API outbound)"
11
+ },
12
+ "name": {
13
+ "type": "string",
14
+ "default": "napcat-bot"
15
+ },
16
+ "url": {
17
+ "type": "string",
18
+ "description": "NapCat WebSocket URL (required for connection: ws)"
19
+ },
20
+ "path": {
21
+ "type": "string",
22
+ "description": "WS path for reverse-wss"
23
+ },
24
+ "http_url": {
25
+ "type": "string",
26
+ "description": "HTTP API base URL (connection: http outbound)"
27
+ },
28
+ "post_path": {
29
+ "type": "string",
30
+ "description": "HTTP POST event path (connection: http inbound)"
31
+ },
32
+ "access_token": {
33
+ "type": "string"
34
+ },
35
+ "reconnect_interval": {
36
+ "type": "number",
37
+ "default": 5000
38
+ },
39
+ "heartbeat_interval": {
40
+ "type": "number",
41
+ "default": 30000
42
+ },
43
+ "poll_interval": {
44
+ "type": "number",
45
+ "default": 30000
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { EndpointInstance } from '@zhin.js/adapter';
6
+ import type { MessageGateway } from '@zhin.js/core/runtime';
7
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
+ import { formatCompact, getLogger } from '@zhin.js/logger';
9
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
+ import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
11
+ import {
12
+ InboundMessageDeduper,
13
+ isNapCatBotMentioned,
14
+ isSelfMessage,
15
+ normalizeMessage,
16
+ } from './napcat-inbound.js';
17
+ import {
18
+ buildSendAction,
19
+ callNapCatHttpAction,
20
+ formatInboundContent,
21
+ formatInboundTarget,
22
+ formatOutboundSegments,
23
+ isMessageEvent,
24
+ senderNickname,
25
+ senderUserId,
26
+ type NapCatEvent,
27
+ type NapCatHttpConfig,
28
+ } from './protocol.js';
29
+ import { readRequestBody } from './webhook.js';
30
+ import { NapCatWsEndpoint } from './ws-endpoint.js';
31
+ import { verifyNapCatAccessToken } from './wss-auth.js';
32
+
33
+ const logger = getLogger('napcat');
34
+
35
+ export interface NapCatHttpEndpointOptions {
36
+ readonly id: CapabilityId;
37
+ readonly gateway: MessageGateway;
38
+ readonly http: HttpHost;
39
+ readonly config: NapCatHttpConfig;
40
+ readonly callHttpAction?: typeof callNapCatHttpAction;
41
+ }
42
+
43
+ export class NapCatHttpEndpoint implements EndpointInstance {
44
+ readonly #options: NapCatHttpEndpointOptions;
45
+ readonly #inboundDeduper = new InboundMessageDeduper();
46
+ readonly #callHttpAction: typeof callNapCatHttpAction;
47
+ #routeReleases: HttpRouteRegistration[] = [];
48
+ #open = false;
49
+ #started = false;
50
+ #unregisterAgent?: () => void;
51
+
52
+ constructor(options: NapCatHttpEndpointOptions) {
53
+ this.#options = options;
54
+ this.#callHttpAction = options.callHttpAction ?? callNapCatHttpAction;
55
+ }
56
+
57
+ async start(): Promise<void> {
58
+ if (this.#started) return;
59
+ this.#started = true;
60
+ this.#unregisterAgent = registerNapcatAgentEndpoint(
61
+ this.#options.config.name,
62
+ this as unknown as NapCatWsEndpoint,
63
+ );
64
+ this.#setupRoutes();
65
+ logger.info(formatCompact({
66
+ op: 'listen',
67
+ endpoint: this.#options.config.name,
68
+ mode: 'http',
69
+ path: this.#options.config.post_path,
70
+ }));
71
+ }
72
+
73
+ open(): void {
74
+ this.#open = true;
75
+ }
76
+
77
+ close(): void {
78
+ this.#open = false;
79
+ }
80
+
81
+ async stop(): Promise<void> {
82
+ this.#open = false;
83
+ for (const release of this.#routeReleases.splice(0)) release();
84
+ this.#unregisterAgent?.();
85
+ this.#unregisterAgent = undefined;
86
+ this.#inboundDeduper.clear();
87
+ this.#started = false;
88
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
89
+ }
90
+
91
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
92
+ const message = formatOutboundSegments(payload);
93
+ const { action, params } = buildSendAction(target, message);
94
+ const resp = await this.#callHttpAction(
95
+ {
96
+ http_url: this.#options.config.http_url,
97
+ access_token: this.#options.config.access_token,
98
+ },
99
+ action,
100
+ params,
101
+ );
102
+ const data = resp.data as { message_id?: number | string } | undefined;
103
+ const messageId = data?.message_id != null ? String(data.message_id) : '';
104
+ logger.debug(formatCompact({
105
+ op: 'napcat_send',
106
+ endpoint: this.#options.config.name,
107
+ target,
108
+ messageId,
109
+ }));
110
+ return messageId;
111
+ }
112
+
113
+ callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
114
+ return this.#callHttpAction(
115
+ {
116
+ http_url: this.#options.config.http_url,
117
+ access_token: this.#options.config.access_token,
118
+ },
119
+ action,
120
+ params,
121
+ ).then((resp) => resp.data);
122
+ }
123
+
124
+ admit(ev: NapCatEvent): void {
125
+ if (!this.#open || !isMessageEvent(ev)) return;
126
+ if (isSelfMessage(ev)) return;
127
+ const msgId = String(ev.message_id);
128
+ if (!this.#inboundDeduper.shouldProcess(msgId)) return;
129
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
130
+ ev = { ...ev, message: normalizeMessage(ev.message) };
131
+ }
132
+ const target = formatInboundTarget(ev);
133
+ const nickname = senderNickname(ev);
134
+ const mentioned = isNapCatBotMentioned(ev);
135
+ void this.#options.gateway.receive({
136
+ adapter: this.#options.id,
137
+ target,
138
+ content: formatInboundContent(ev),
139
+ sender: senderUserId(ev),
140
+ id: msgId,
141
+ metadata: Object.freeze({
142
+ message_type: ev.message_type,
143
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
144
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
145
+ endpoint: this.#options.config.name,
146
+ time: ev.time,
147
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
148
+ role: ev.sender?.role,
149
+ ...(nickname ? { nickname } : {}),
150
+ ...(mentioned ? { mentioned: true } : {}),
151
+ }),
152
+ }).catch((err) => {
153
+ logger.warn(formatCompact({
154
+ op: 'napcat_gateway_receive_failed',
155
+ target,
156
+ error: err instanceof Error ? err.message : String(err),
157
+ }));
158
+ });
159
+ }
160
+
161
+ #setupRoutes(): void {
162
+ const path = this.#options.config.post_path;
163
+ this.#routeReleases.push(
164
+ this.#options.http.route('POST', path, async (request, response) => {
165
+ await this.#handlePost(request, response);
166
+ }, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
167
+ );
168
+ }
169
+
170
+ async #handlePost(request: IncomingMessage, response: ServerResponse): Promise<void> {
171
+ try {
172
+ if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
173
+ response.writeHead(403, { 'Content-Type': 'application/json' });
174
+ response.end(JSON.stringify({ message: 'Unauthorized' }));
175
+ return;
176
+ }
177
+ const raw = await readRequestBody(request);
178
+ let ev: NapCatEvent;
179
+ try {
180
+ ev = JSON.parse(raw) as NapCatEvent;
181
+ } catch {
182
+ response.writeHead(400, { 'Content-Type': 'application/json' });
183
+ response.end(JSON.stringify({ message: 'Invalid JSON' }));
184
+ return;
185
+ }
186
+ if (this.#open) this.admit(ev);
187
+ response.writeHead(200, { 'Content-Type': 'application/json' });
188
+ response.end(JSON.stringify({ status: 'ok' }));
189
+ } catch (error) {
190
+ logger.error('NapCat HTTP webhook error:', error);
191
+ if (!response.headersSent) {
192
+ response.writeHead(500, { 'Content-Type': 'application/json' });
193
+ response.end(JSON.stringify({ message: 'Internal Server Error' }));
194
+ }
195
+ }
196
+ }
197
+ }