@zhin.js/adapter-napcat 1.0.1 → 1.1.2

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 (202) hide show
  1. package/CHANGELOG.md +757 -0
  2. package/{skills/napcat/PERMITS.md → PERMITS.md} +3 -3
  3. package/README.md +63 -95
  4. package/adapters/napcat/index.js +47 -0
  5. package/adapters/napcat/index.ts +64 -0
  6. package/agents/napcat/agent.json +20 -0
  7. package/agents/napcat/boundaries.md +3 -0
  8. package/agents/napcat/conventions.md +3 -0
  9. package/agents/napcat/skills/napcat-account/SKILL.md +25 -0
  10. package/agents/napcat/skills/napcat-account/tools/delete_friend/index.js +18 -0
  11. package/agents/napcat/skills/napcat-account/tools/delete_friend/index.ts +18 -0
  12. package/agents/napcat/skills/napcat-account/tools/get_user_status/index.js +16 -0
  13. package/agents/napcat/skills/napcat-account/tools/get_user_status/index.ts +16 -0
  14. package/agents/napcat/skills/napcat-account/tools/send_like/index.js +18 -0
  15. package/agents/napcat/skills/napcat-account/tools/send_like/index.ts +18 -0
  16. package/agents/napcat/skills/napcat-account/tools/set_avatar/index.js +17 -0
  17. package/agents/napcat/skills/napcat-account/tools/set_avatar/index.ts +17 -0
  18. package/agents/napcat/skills/napcat-account/tools/set_online_status/index.js +18 -0
  19. package/agents/napcat/skills/napcat-account/tools/set_online_status/index.ts +18 -0
  20. package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +21 -0
  21. package/agents/napcat/skills/napcat-account/tools/set_profile/index.ts +21 -0
  22. package/agents/napcat/skills/napcat-account/tools/set_signature/index.js +17 -0
  23. package/agents/napcat/skills/napcat-account/tools/set_signature/index.ts +17 -0
  24. package/agents/napcat/skills/napcat-files-history/SKILL.md +22 -0
  25. package/agents/napcat/skills/napcat-files-history/tools/download_file/index.js +17 -0
  26. package/agents/napcat/skills/napcat-files-history/tools/download_file/index.ts +17 -0
  27. package/agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.js +18 -0
  28. package/agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.ts +18 -0
  29. package/agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.js +19 -0
  30. package/agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.ts +19 -0
  31. package/agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.js +19 -0
  32. package/agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.ts +19 -0
  33. package/agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.js +17 -0
  34. package/agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.ts +17 -0
  35. package/agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.js +21 -0
  36. package/agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.ts +21 -0
  37. package/agents/napcat/skills/napcat-group-content/SKILL.md +21 -0
  38. package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.js +20 -0
  39. package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.ts +20 -0
  40. package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.js +19 -0
  41. package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.ts +19 -0
  42. package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.js +17 -0
  43. package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.ts +17 -0
  44. package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.js +17 -0
  45. package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.ts +17 -0
  46. package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.js +21 -0
  47. package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.ts +21 -0
  48. package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.js +19 -0
  49. package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.ts +19 -0
  50. package/agents/napcat/skills/napcat-group-settings/SKILL.md +23 -0
  51. package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.js +17 -0
  52. package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.ts +17 -0
  53. package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.js +17 -0
  54. package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.ts +17 -0
  55. package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.js +18 -0
  56. package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.ts +18 -0
  57. package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.js +20 -0
  58. package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.ts +20 -0
  59. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +21 -0
  60. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +21 -0
  61. package/agents/napcat/skills/napcat-media/SKILL.md +23 -0
  62. package/agents/napcat/skills/napcat-media/tools/ai_tts/index.js +19 -0
  63. package/agents/napcat/skills/napcat-media/tools/ai_tts/index.ts +19 -0
  64. package/agents/napcat/skills/napcat-media/tools/get_ai_characters/index.js +17 -0
  65. package/agents/napcat/skills/napcat-media/tools/get_ai_characters/index.ts +17 -0
  66. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +20 -0
  67. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts +20 -0
  68. package/agents/napcat/skills/napcat-media/tools/ocr_image/index.js +16 -0
  69. package/agents/napcat/skills/napcat-media/tools/ocr_image/index.ts +16 -0
  70. package/agents/napcat/skills/napcat-media/tools/translate/index.js +16 -0
  71. package/agents/napcat/skills/napcat-media/tools/translate/index.ts +16 -0
  72. package/agents/napcat/skills/napcat-messaging/SKILL.md +22 -0
  73. package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.js +22 -0
  74. package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.ts +28 -0
  75. package/agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.js +17 -0
  76. package/agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.ts +17 -0
  77. package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.js +19 -0
  78. package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.ts +27 -0
  79. package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.js +18 -0
  80. package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.ts +18 -0
  81. package/agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.js +18 -0
  82. package/agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.ts +18 -0
  83. package/agents/napcat/system.md +3 -0
  84. package/commands/napcat/endpoint/add/[id]/index.js +3 -0
  85. package/commands/napcat/endpoint/add/[id]/index.ts +3 -0
  86. package/commands/napcat/endpoint/definition.js +29 -0
  87. package/commands/napcat/endpoint/definition.ts +26 -0
  88. package/commands/napcat/endpoint/list/index.js +3 -0
  89. package/commands/napcat/endpoint/list/index.ts +3 -0
  90. package/commands/napcat/endpoint/remove/[id]/index.js +3 -0
  91. package/commands/napcat/endpoint/remove/[id]/index.ts +3 -0
  92. package/lib/client.d.ts +52 -0
  93. package/lib/client.js +62 -0
  94. package/lib/endpoint-management.d.ts +15 -0
  95. package/lib/endpoint-management.js +47 -0
  96. package/lib/http-endpoint.d.ts +27 -0
  97. package/lib/http-endpoint.js +177 -0
  98. package/lib/index.d.ts +11 -22
  99. package/lib/index.js +10 -59
  100. package/lib/napcat-inbound.d.ts +12 -5
  101. package/lib/napcat-inbound.js +28 -13
  102. package/lib/napcat-runtime-state.d.ts +1 -0
  103. package/lib/napcat-runtime-state.js +6 -0
  104. package/lib/onebot-get-msg.d.ts +16 -3
  105. package/lib/onebot-get-msg.js +53 -1
  106. package/lib/protocol.d.ts +147 -0
  107. package/lib/protocol.js +318 -0
  108. package/lib/side-event-dispatch.d.ts +7 -0
  109. package/lib/side-event-dispatch.js +36 -0
  110. package/lib/webhook.d.ts +2 -0
  111. package/lib/webhook.js +14 -0
  112. package/lib/ws-endpoint.d.ts +27 -0
  113. package/lib/ws-endpoint.js +241 -0
  114. package/lib/ws-transport.d.ts +13 -0
  115. package/lib/ws-transport.js +75 -0
  116. package/lib/ws-types.d.ts +17 -0
  117. package/lib/ws-types.js +1 -0
  118. package/lib/wss-auth.d.ts +2 -0
  119. package/lib/wss-auth.js +16 -0
  120. package/lib/wss-endpoint.d.ts +29 -0
  121. package/lib/wss-endpoint.js +196 -0
  122. package/package.json +76 -34
  123. package/plugin.js +18 -0
  124. package/schema.json +131 -0
  125. package/src/client.ts +79 -0
  126. package/src/endpoint-management.ts +72 -0
  127. package/src/http-endpoint.ts +234 -0
  128. package/src/index.ts +62 -87
  129. package/src/napcat-inbound.ts +34 -10
  130. package/src/napcat-runtime-state.ts +7 -0
  131. package/src/onebot-get-msg.ts +69 -5
  132. package/src/protocol.ts +465 -0
  133. package/src/side-event-dispatch.ts +48 -0
  134. package/src/webhook.ts +16 -0
  135. package/src/ws-endpoint.ts +301 -0
  136. package/src/ws-transport.ts +105 -0
  137. package/src/ws-types.ts +20 -0
  138. package/src/wss-auth.ts +17 -0
  139. package/src/wss-endpoint.ts +247 -0
  140. package/client/NapCatManagement.tsx +0 -113
  141. package/client/index.tsx +0 -11
  142. package/client/tsconfig.json +0 -7
  143. package/client/utils/api.ts +0 -30
  144. package/dist/index.js +0 -27
  145. package/lib/adapter.d.ts +0 -28
  146. package/lib/adapter.d.ts.map +0 -1
  147. package/lib/adapter.js +0 -94
  148. package/lib/adapter.js.map +0 -1
  149. package/lib/agent-prompt.d.ts +0 -3
  150. package/lib/agent-prompt.d.ts.map +0 -1
  151. package/lib/agent-prompt.js +0 -105
  152. package/lib/agent-prompt.js.map +0 -1
  153. package/lib/endpoint-base.d.ts +0 -143
  154. package/lib/endpoint-base.d.ts.map +0 -1
  155. package/lib/endpoint-base.js +0 -334
  156. package/lib/endpoint-base.js.map +0 -1
  157. package/lib/endpoint-http.d.ts +0 -18
  158. package/lib/endpoint-http.d.ts.map +0 -1
  159. package/lib/endpoint-http.js +0 -119
  160. package/lib/endpoint-http.js.map +0 -1
  161. package/lib/endpoint-ws-client.d.ts +0 -21
  162. package/lib/endpoint-ws-client.d.ts.map +0 -1
  163. package/lib/endpoint-ws-client.js +0 -165
  164. package/lib/endpoint-ws-client.js.map +0 -1
  165. package/lib/endpoint-ws-server.d.ts +0 -21
  166. package/lib/endpoint-ws-server.d.ts.map +0 -1
  167. package/lib/endpoint-ws-server.js +0 -150
  168. package/lib/endpoint-ws-server.js.map +0 -1
  169. package/lib/index.d.ts.map +0 -1
  170. package/lib/index.js.map +0 -1
  171. package/lib/napcat-inbound.d.ts.map +0 -1
  172. package/lib/napcat-inbound.js.map +0 -1
  173. package/lib/onebot-get-msg.d.ts.map +0 -1
  174. package/lib/onebot-get-msg.js.map +0 -1
  175. package/lib/routes.d.ts +0 -4
  176. package/lib/routes.d.ts.map +0 -1
  177. package/lib/routes.js +0 -61
  178. package/lib/routes.js.map +0 -1
  179. package/lib/tools.d.ts +0 -8
  180. package/lib/tools.d.ts.map +0 -1
  181. package/lib/tools.js +0 -605
  182. package/lib/tools.js.map +0 -1
  183. package/lib/types.d.ts +0 -293
  184. package/lib/types.d.ts.map +0 -1
  185. package/lib/types.js +0 -6
  186. package/lib/types.js.map +0 -1
  187. package/lib/typing-indicator.d.ts +0 -45
  188. package/lib/typing-indicator.d.ts.map +0 -1
  189. package/lib/typing-indicator.js +0 -132
  190. package/lib/typing-indicator.js.map +0 -1
  191. package/plugin.yml +0 -3
  192. package/skills/napcat/SKILL.md +0 -311
  193. package/src/adapter.ts +0 -101
  194. package/src/agent-prompt.ts +0 -114
  195. package/src/endpoint-base.ts +0 -376
  196. package/src/endpoint-http.ts +0 -109
  197. package/src/endpoint-ws-client.ts +0 -164
  198. package/src/endpoint-ws-server.ts +0 -150
  199. package/src/routes.ts +0 -61
  200. package/src/tools.ts +0 -632
  201. package/src/types.ts +0 -279
  202. package/src/typing-indicator.ts +0 -184
@@ -0,0 +1,301 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ /**
3
+ * NapCat WS client endpoint — outbound connect to NapCat.
4
+ */
5
+ import WebSocket from 'ws';
6
+ import {
7
+ createRecallEndpointControl,
8
+ createEndpointLifecycle,
9
+ type EndpointConnectHandle,
10
+ type EndpointControl,
11
+ type EndpointLifecycle,
12
+ type EndpointManagement,
13
+ type EndpointSendRequest,
14
+ } from 'zhin.js/adapter';
15
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
+ import type { CapabilityId } from 'zhin.js';
17
+ import { createNapCatEndpointManagement } from './endpoint-management.js';
18
+ import {
19
+ InboundMessageDeduper,
20
+ isNapCatBotMentioned,
21
+ isSelfMessage,
22
+ normalizeMessage,
23
+ } from './napcat-inbound.js';
24
+ import {
25
+ buildSendAction,
26
+ buildWsConnectOptions,
27
+ formatInboundContent,
28
+ formatOutboundSegments,
29
+ isMessageEvent,
30
+ napcatInboundConversation,
31
+ napcatOutboundTarget,
32
+ senderNickname,
33
+ senderUserId,
34
+ type NapCatEvent,
35
+ type NapCatWsConfig,
36
+ } from './protocol.js';
37
+ import { receiveNapCatSideEvent } from './side-event-dispatch.js';
38
+ import {
39
+ callNapCatWsAction,
40
+ handleNapCatWsMessage,
41
+ rejectAllPending,
42
+ } from './ws-transport.js';
43
+ import {
44
+ type NapCatPendingAction,
45
+ type NapCatWsCreateOptions,
46
+ type NapCatWsSocket,
47
+ } from './ws-types.js';
48
+ import { createNapCatContentPort } from './onebot-get-msg.js';
49
+ import { NapcatClient } from './client.js';
50
+
51
+ export interface NapCatWsEndpointOptions {
52
+ readonly id: CapabilityId;
53
+ readonly config: NapCatWsConfig;
54
+ readonly createWebSocket?: (
55
+ url: string,
56
+ options: NapCatWsCreateOptions,
57
+ ) => NapCatWsSocket;
58
+ }
59
+
60
+ export class NapCatWsEndpoint extends Endpoint<NapcatClient> {
61
+ readonly client = new NapcatClient((action, params) => this.#callApi(action, params));
62
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
63
+
64
+ readonly #options: NapCatWsEndpointOptions;
65
+ readonly #inboundDeduper = new InboundMessageDeduper();
66
+ readonly management: EndpointManagement = createNapCatEndpointManagement(this.client);
67
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
68
+ readonly content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
69
+ readonly #lifecycle: EndpointLifecycle;
70
+ #ws?: NapCatWsSocket;
71
+ #requestId = { value: 0 };
72
+ #pending = new Map<string, NapCatPendingAction>();
73
+ #open = false;
74
+
75
+ constructor(options: NapCatWsEndpointOptions) {
76
+ super();
77
+ this.#logger = getAdapterLogger('napcat', options.config.id);
78
+ this.#options = options;
79
+ this.#lifecycle = createEndpointLifecycle({
80
+ name: options.config.id,
81
+ // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
82
+ reconnect: {
83
+ initialIntervalMs: options.config.reconnect_interval,
84
+ multiplier: 1,
85
+ maxIntervalMs: Number.MAX_SAFE_INTEGER,
86
+ jitterMs: 0,
87
+ },
88
+ });
89
+ }
90
+
91
+ async start(): Promise<void> {
92
+ if (this.#lifecycle.started) return;
93
+ await this.#lifecycle.start((handle) => this.#connect(handle));
94
+ }
95
+
96
+ open(): void {
97
+ this.#open = true;
98
+ }
99
+
100
+ close(): void {
101
+ this.#open = false;
102
+ }
103
+
104
+ async stop(): Promise<void> {
105
+ this.#open = false;
106
+ await this.#lifecycle.stop();
107
+ rejectAllPending(this.#pending);
108
+ this.#inboundDeduper.clear();
109
+ if (this.#ws) {
110
+ try {
111
+ this.#ws.close();
112
+ } catch {
113
+ /* ignore */
114
+ }
115
+ this.#ws = undefined;
116
+ }
117
+ }
118
+
119
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
120
+ const message = formatOutboundSegments(payload);
121
+ const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
122
+ const data = await this.client.callApi(action, params) as { message_id?: number | string } | undefined;
123
+ const messageId = data?.message_id != null ? String(data.message_id) : '';
124
+ this.#logger.debug(formatCompact({
125
+ op: 'napcat_send',
126
+ endpoint: this.#options.config.id,
127
+ target: `${conversation.kind}:${conversation.id}`,
128
+ messageId,
129
+ }));
130
+ return messageId;
131
+ }
132
+
133
+ async recallMessage(messageId: string): Promise<void> {
134
+ if (!messageId) return;
135
+ await this.client.callApi('delete_msg', { message_id: Number(messageId) });
136
+ }
137
+
138
+ #callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
139
+ return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
140
+ }
141
+
142
+ /** Test / internal: admit a parsed event when the endpoint is open. */
143
+ admit(ev: NapCatEvent): void {
144
+ if (!this.#open) return;
145
+ void this.emitPlatform(napCatPlatformEventName(ev), ev).catch((error) => {
146
+ this.#logger.warn(formatCompact({
147
+ op: 'napcat_platform_event_failed',
148
+ error: error instanceof Error ? error.message : String(error),
149
+ }));
150
+ });
151
+ if (!isMessageEvent(ev)) {
152
+ receiveNapCatSideEvent(
153
+ (name, payload) => this.emit(name, payload),
154
+ this.#options.config.id,
155
+ this.client,
156
+ ev,
157
+ this.#logger,
158
+ );
159
+ return;
160
+ }
161
+ if (isSelfMessage(ev)) return;
162
+ const msgId = String(ev.message_id);
163
+ if (!this.#inboundDeduper.shouldProcess(msgId)) return;
164
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
165
+ ev = { ...ev, message: normalizeMessage(ev.message) };
166
+ }
167
+ const conversation = napcatInboundConversation(String(this.#options.id), ev);
168
+ const content = formatInboundContent(ev);
169
+ const nickname = senderNickname(ev);
170
+ const mentioned = isNapCatBotMentioned(ev);
171
+ void this.emit('message.receive', {
172
+ conversation,
173
+ message: { conversation, id: msgId },
174
+ content,
175
+ sender: {
176
+ id: senderUserId(ev),
177
+ name: nickname,
178
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
179
+ },
180
+ endpointId: this.#options.config.id,
181
+ ...(mentioned ? { mentioned: true } : {}),
182
+ metadata: Object.freeze({
183
+ message_type: ev.message_type,
184
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
185
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
186
+ time: ev.time,
187
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
188
+ role: ev.sender?.role,
189
+ ...(nickname ? { nickname } : {}),
190
+ }),
191
+ }).catch((err) => {
192
+ this.#logger.warn(formatCompact({
193
+ op: 'napcat_gateway_receive_failed',
194
+ target: `${conversation.kind}:${conversation.id}`,
195
+ error: err instanceof Error ? err.message : String(err),
196
+ }));
197
+ });
198
+ }
199
+
200
+ async #connect(handle: EndpointConnectHandle): Promise<void> {
201
+ const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
202
+ const create = this.#options.createWebSocket
203
+ ?? ((connectUrl: string, options: NapCatWsCreateOptions) =>
204
+ new WebSocket(connectUrl, { headers: options.headers }) as unknown as NapCatWsSocket);
205
+
206
+ await new Promise<void>((resolve, reject) => {
207
+ let settled = false;
208
+ const ws = create(url, { headers });
209
+ this.#ws = ws;
210
+ handle.onForceClose(() => {
211
+ try {
212
+ ws.close();
213
+ } catch {
214
+ /* ignore */
215
+ }
216
+ });
217
+
218
+ ws.on('open', () => {
219
+ if (settled) return;
220
+ settled = true;
221
+ if (!this.#options.config.access_token) {
222
+ this.#logger.warn(formatCompact({
223
+ endpoint: this.#options.config.id,
224
+ ok: false,
225
+ error: 'missing access_token',
226
+ }));
227
+ }
228
+ this.#logger.debug(formatCompact({
229
+ endpoint: this.#options.config.id,
230
+ mode: 'ws',
231
+ url: safeUrl,
232
+ }));
233
+ // stop-during-connect 竞态:已停止则不再武装心跳(基座 stop 已清理定时器)
234
+ if (this.#lifecycle.started) {
235
+ this.#lifecycle.startHeartbeat(() => {
236
+ try {
237
+ ws.ping?.();
238
+ } catch {
239
+ /* ignore */
240
+ }
241
+ }, this.#options.config.heartbeat_interval);
242
+ }
243
+ resolve();
244
+ });
245
+
246
+ ws.on('message', (data) => {
247
+ handleNapCatWsMessage(data, {
248
+ endpointId: this.#options.config.id,
249
+ pending: this.#pending,
250
+ admit: (event) => this.admit(event),
251
+ });
252
+ });
253
+
254
+ ws.on('close', (code, reason) => {
255
+ const reasonStr = typeof reason === 'string'
256
+ ? reason
257
+ : Buffer.isBuffer(reason)
258
+ ? reason.toString()
259
+ : String(reason ?? '');
260
+ const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
261
+ const codeHint = codeNum === 1005
262
+ ? ' [no status]'
263
+ : codeNum === 1006
264
+ ? ' [abnormal]'
265
+ : '';
266
+ this.#logger.warn(formatCompact({
267
+ op: 'disconnect',
268
+ code: codeNum,
269
+ error: `${reasonStr || 'closed'}${codeHint}`,
270
+ reconnect_ms: this.#options.config.reconnect_interval,
271
+ }));
272
+ // 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
273
+ handle.notifyClosed(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
274
+ if (!settled) {
275
+ settled = true;
276
+ reject(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
277
+ }
278
+ });
279
+
280
+ ws.on('error', (err) => {
281
+ const error = err instanceof Error ? err : new Error(String(err));
282
+ this.#logger.warn(formatCompact({
283
+ op: 'ws_error',
284
+ endpoint: this.#options.config.id,
285
+ ok: false,
286
+ error: error.message,
287
+ }));
288
+ if (!settled) {
289
+ settled = true;
290
+ reject(error);
291
+ }
292
+ });
293
+ });
294
+ }
295
+ }
296
+
297
+ function napCatPlatformEventName(ev: NapCatEvent): string {
298
+ return [ev.post_type, ev.message_type ?? ev.notice_type ?? ev.request_type, ev.sub_type]
299
+ .filter(Boolean)
300
+ .join('.');
301
+ }
@@ -0,0 +1,105 @@
1
+ import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
2
+ import { formatCompact, getLogger } from '@zhin.js/logger';
3
+ import {
4
+ type NapCatActionRequest,
5
+ type NapCatActionResponse,
6
+ type NapCatEvent,
7
+ } from './protocol.js';
8
+ import {
9
+ WS_OPEN,
10
+ type NapCatPendingAction,
11
+ type NapCatWsSocket,
12
+ } from './ws-types.js';
13
+
14
+ const logger = getLogger('napcat');
15
+
16
+ export function decodeWsPayload(data: unknown): string {
17
+ if (typeof data === 'string') return data;
18
+ if (Buffer.isBuffer(data)) return data.toString();
19
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
20
+ return String(data ?? '');
21
+ }
22
+
23
+ export function handleNapCatWsMessage(
24
+ data: unknown,
25
+ options: {
26
+ readonly endpointId: string;
27
+ readonly pending: Map<string, NapCatPendingAction>;
28
+ readonly admit: (ev: NapCatEvent) => void;
29
+ },
30
+ ): void {
31
+ try {
32
+ const raw = decodeWsPayload(data);
33
+ const msg = JSON.parse(raw) as NapCatEvent | NapCatActionResponse;
34
+ if ('echo' in msg && typeof (msg as NapCatActionResponse).echo === 'string') {
35
+ const resp = msg as NapCatActionResponse;
36
+ const pending = options.pending.get(resp.echo!);
37
+ if (pending) {
38
+ options.pending.delete(resp.echo!);
39
+ clearTimeout(pending.timeout);
40
+ if (resp.status === 'ok') pending.resolve(resp.data);
41
+ else {
42
+ pending.reject(new Error(
43
+ `API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`,
44
+ ));
45
+ }
46
+ }
47
+ return;
48
+ }
49
+ options.admit(msg as NapCatEvent);
50
+ } catch (error) {
51
+ logger.warn(formatCompact({
52
+ op: 'napcat_parse_failed',
53
+ endpoint: options.endpointId,
54
+ error: error instanceof Error ? error.message : String(error),
55
+ }));
56
+ }
57
+ }
58
+
59
+ export function callNapCatWsAction(
60
+ ws: NapCatWsSocket | undefined,
61
+ pending: Map<string, NapCatPendingAction>,
62
+ requestId: { value: number },
63
+ action: string,
64
+ params: Record<string, unknown>,
65
+ ): Promise<unknown> {
66
+ if (!ws || ws.readyState !== WS_OPEN) {
67
+ return Promise.reject(new Error('WebSocket is not connected'));
68
+ }
69
+ const echo = `req_${++requestId.value}`;
70
+ const req: NapCatActionRequest = { action, params, echo };
71
+ return new Promise((resolve, reject) => {
72
+ const timeout = setTimeout(() => {
73
+ pending.delete(echo);
74
+ reject(new Error(`API call timeout: ${action}`));
75
+ }, 30_000);
76
+ pending.set(echo, { resolve, reject, timeout });
77
+ ws.send(JSON.stringify(req));
78
+ });
79
+ }
80
+
81
+ export function startNapCatHeartbeat(
82
+ ws: NapCatWsSocket | undefined,
83
+ intervalMs: number,
84
+ existingTimer?: NodeJS.Timeout,
85
+ ): NodeJS.Timeout {
86
+ if (existingTimer) clearInterval(existingTimer);
87
+ return setInterval(() => {
88
+ try {
89
+ ws?.ping?.();
90
+ } catch {
91
+ /* ignore */
92
+ }
93
+ }, intervalMs);
94
+ }
95
+
96
+ export function rejectAllPending(
97
+ pending: Map<string, NapCatPendingAction>,
98
+ message = 'Connection closed',
99
+ ): void {
100
+ for (const [, entry] of pending) {
101
+ clearTimeout(entry.timeout);
102
+ entry.reject(new Error(message));
103
+ }
104
+ pending.clear();
105
+ }
@@ -0,0 +1,20 @@
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
+
10
+ export interface NapCatWsCreateOptions {
11
+ readonly headers?: Record<string, string>;
12
+ }
13
+
14
+ export const WS_OPEN = 1;
15
+
16
+ export interface NapCatPendingAction {
17
+ resolve: (value: unknown) => void;
18
+ reject: (err: Error) => void;
19
+ timeout: NodeJS.Timeout;
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+
3
+ export function verifyNapCatAccessToken(
4
+ accessToken: string | undefined,
5
+ request: IncomingMessage,
6
+ ): boolean {
7
+ if (!accessToken) return true;
8
+ const auth = request.headers.authorization ?? '';
9
+ if (auth === `Bearer ${accessToken}`) return true;
10
+ try {
11
+ const url = new URL(request.url ?? '/', 'http://localhost');
12
+ if (url.searchParams.get('access_token') === accessToken) return true;
13
+ } catch {
14
+ /* ignore */
15
+ }
16
+ return false;
17
+ }
@@ -0,0 +1,247 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ /**
3
+ * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
+ */
5
+ import {
6
+ createEndpointLifecycle,
7
+ createRecallEndpointControl,
8
+ type EndpointLifecycle,
9
+ type EndpointControl,
10
+ type EndpointManagement,
11
+ type EndpointSendRequest,
12
+ } from 'zhin.js/adapter';
13
+ import type { HttpHost, WsConnection } from '@zhin.js/host-http';
14
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
15
+ import type { CapabilityId } from 'zhin.js';
16
+ import { createNapCatEndpointManagement } from './endpoint-management.js';
17
+ import {
18
+ InboundMessageDeduper,
19
+ isNapCatBotMentioned,
20
+ isSelfMessage,
21
+ normalizeMessage,
22
+ } from './napcat-inbound.js';
23
+ import {
24
+ buildSendAction,
25
+ formatInboundContent,
26
+ formatOutboundSegments,
27
+ isMessageEvent,
28
+ napcatInboundConversation,
29
+ napcatOutboundTarget,
30
+ senderNickname,
31
+ senderUserId,
32
+ type NapCatEvent,
33
+ type NapCatWssConfig,
34
+ } from './protocol.js';
35
+ import { receiveNapCatSideEvent } from './side-event-dispatch.js';
36
+ import { createNapCatContentPort } from './onebot-get-msg.js';
37
+ import { NapcatClient } from './client.js';
38
+ import {
39
+ callNapCatWsAction,
40
+ handleNapCatWsMessage,
41
+ rejectAllPending,
42
+ } from './ws-transport.js';
43
+ import { NapCatWsEndpoint } from './ws-endpoint.js';
44
+ import {
45
+ type NapCatPendingAction,
46
+ type NapCatWsSocket,
47
+ } from './ws-types.js';
48
+ import { verifyNapCatAccessToken } from './wss-auth.js';
49
+
50
+ export interface NapCatWssEndpointOptions {
51
+ readonly id: CapabilityId;
52
+ readonly http: HttpHost;
53
+ readonly config: NapCatWssConfig;
54
+ }
55
+
56
+ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
57
+ readonly client = new NapcatClient((action, params) => this.#callApi(action, params));
58
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
59
+
60
+ readonly #options: NapCatWssEndpointOptions;
61
+ readonly #inboundDeduper = new InboundMessageDeduper();
62
+ readonly management: EndpointManagement = createNapCatEndpointManagement(this.client);
63
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
64
+ readonly content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
65
+ readonly #connectionLifecycle: EndpointLifecycle;
66
+ #ws?: NapCatWsSocket;
67
+ #wsRelease?: () => void;
68
+ #connectionTask = Promise.resolve();
69
+ #requestId = { value: 0 };
70
+ #pending = new Map<string, NapCatPendingAction>();
71
+ #open = false;
72
+ #started = false;
73
+
74
+ constructor(options: NapCatWssEndpointOptions) {
75
+ super();
76
+ this.#logger = getAdapterLogger('napcat', options.config.id);
77
+ this.#options = options;
78
+ this.#connectionLifecycle = createEndpointLifecycle({
79
+ name: `${options.config.id}:inbound`,
80
+ reconnect: false,
81
+ });
82
+ }
83
+
84
+ async start(): Promise<void> {
85
+ if (this.#started) return;
86
+ this.#started = true;
87
+ const handle = this.#options.http.ws(this.#options.config.path);
88
+ this.#wsRelease = handle.onConnection((connection) => {
89
+ this.#connectionTask = this.#connectionTask
90
+ .then(() => this.#acceptConnection(connection))
91
+ .catch((error) => {
92
+ this.#logger.warn(formatCompact({
93
+ op: 'wss_connection_failed',
94
+ endpoint: this.#options.config.id,
95
+ error: error instanceof Error ? error.message : String(error),
96
+ }));
97
+ });
98
+ });
99
+ this.#logger.info(formatCompact({
100
+ op: 'listen',
101
+ endpoint: this.#options.config.id,
102
+ mode: 'wss',
103
+ path: this.#options.config.path,
104
+ }));
105
+ }
106
+
107
+ open(): void {
108
+ this.#open = true;
109
+ }
110
+
111
+ close(): void {
112
+ this.#open = false;
113
+ }
114
+
115
+ async stop(): Promise<void> {
116
+ this.#open = false;
117
+ this.#wsRelease?.();
118
+ this.#wsRelease = undefined;
119
+ await this.#connectionTask;
120
+ await this.#connectionLifecycle.stop();
121
+ rejectAllPending(this.#pending);
122
+ this.#inboundDeduper.clear();
123
+ this.#ws = undefined;
124
+ this.#started = false;
125
+ }
126
+
127
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
128
+ const message = formatOutboundSegments(payload);
129
+ const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
130
+ const data = await this.client.callApi(action, params) as { message_id?: number | string } | undefined;
131
+ return data?.message_id != null ? String(data.message_id) : '';
132
+ }
133
+
134
+ async recallMessage(messageId: string): Promise<void> {
135
+ if (!messageId) return;
136
+ await this.client.callApi('delete_msg', { message_id: Number(messageId) });
137
+ }
138
+
139
+ #callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
140
+ return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
141
+ }
142
+
143
+ admit(ev: NapCatEvent): void {
144
+ if (!this.#open) return;
145
+ void this.emitPlatform(napCatPlatformEventName(ev), ev).catch((error) => {
146
+ this.#logger.warn(formatCompact({
147
+ op: 'napcat_platform_event_failed',
148
+ error: error instanceof Error ? error.message : String(error),
149
+ }));
150
+ });
151
+ if (!isMessageEvent(ev)) {
152
+ receiveNapCatSideEvent(
153
+ (name, payload) => this.emit(name, payload),
154
+ this.#options.config.id,
155
+ this.client,
156
+ ev,
157
+ this.#logger,
158
+ );
159
+ return;
160
+ }
161
+ if (isSelfMessage(ev)) return;
162
+ const msgId = String(ev.message_id);
163
+ if (!this.#inboundDeduper.shouldProcess(msgId)) return;
164
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
165
+ ev = { ...ev, message: normalizeMessage(ev.message) };
166
+ }
167
+ const conversation = napcatInboundConversation(String(this.#options.id), ev);
168
+ const nickname = senderNickname(ev);
169
+ const mentioned = isNapCatBotMentioned(ev);
170
+ void this.emit('message.receive', {
171
+ conversation,
172
+ message: { conversation, id: msgId },
173
+ content: formatInboundContent(ev),
174
+ sender: {
175
+ id: senderUserId(ev),
176
+ name: nickname,
177
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
178
+ },
179
+ endpointId: this.#options.config.id,
180
+ ...(mentioned ? { mentioned: true } : {}),
181
+ metadata: Object.freeze({
182
+ message_type: ev.message_type,
183
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
184
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
185
+ time: ev.time,
186
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
187
+ role: ev.sender?.role,
188
+ ...(nickname ? { nickname } : {}),
189
+ }),
190
+ }).catch((err) => {
191
+ this.#logger.warn(formatCompact({
192
+ op: 'napcat_gateway_receive_failed',
193
+ target: `${conversation.kind}:${conversation.id}`,
194
+ error: err instanceof Error ? err.message : String(err),
195
+ }));
196
+ });
197
+ }
198
+
199
+ async #acceptConnection(connection: WsConnection): Promise<void> {
200
+ if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
201
+ connection.socket.close(4003, 'Unauthorized');
202
+ return;
203
+ }
204
+ const socket = connection.socket as unknown as NapCatWsSocket;
205
+ await this.#connectionLifecycle.stop();
206
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
207
+ this.#ws = socket;
208
+ lifecycleHandle.onForceClose(() => {
209
+ try {
210
+ socket.close();
211
+ } catch {
212
+ /* ignore */
213
+ }
214
+ });
215
+ this.#connectionLifecycle.startHeartbeat(() => {
216
+ try {
217
+ socket.ping?.();
218
+ } catch {
219
+ /* ignore */
220
+ }
221
+ }, this.#options.config.heartbeat_interval);
222
+ socket.on('message', (data) => {
223
+ this.#connectionLifecycle.notifyHeartbeatAck();
224
+ handleNapCatWsMessage(data, {
225
+ endpointId: this.#options.config.id,
226
+ pending: this.#pending,
227
+ admit: (event) => this.admit(event),
228
+ });
229
+ });
230
+ socket.on('close', () => {
231
+ if (this.#ws === socket) this.#ws = undefined;
232
+ lifecycleHandle.notifyClosed(new Error('NapCat reverse WebSocket closed'));
233
+ });
234
+ });
235
+ this.#logger.debug(formatCompact({
236
+ endpoint: this.#options.config.id,
237
+ mode: 'wss',
238
+ peer: connection.request.socket.remoteAddress,
239
+ }));
240
+ }
241
+ }
242
+
243
+ function napCatPlatformEventName(ev: NapCatEvent): string {
244
+ return [ev.post_type, ev.message_type ?? ev.notice_type ?? ev.request_type, ev.sub_type]
245
+ .filter(Boolean)
246
+ .join('.');
247
+ }