@zhin.js/adapter-napcat 1.1.26 → 1.1.27

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 (90) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/LICENSE +21 -0
  3. package/PERMITS.md +17 -3
  4. package/README.md +10 -6
  5. package/adapters/napcat/index.js +3 -0
  6. package/adapters/napcat/index.ts +3 -0
  7. package/agents/napcat/conversation.js +71 -0
  8. package/agents/napcat/conversation.ts +76 -0
  9. package/agents/napcat/media.js +49 -0
  10. package/agents/napcat/media.ts +44 -0
  11. package/agents/napcat/messages.js +32 -0
  12. package/agents/napcat/messages.ts +32 -0
  13. package/agents/napcat/skills/napcat-account/tools/delete_friend/index.js +1 -1
  14. package/agents/napcat/skills/napcat-account/tools/delete_friend/index.ts +1 -1
  15. package/agents/napcat/skills/napcat-account/tools/set_avatar/index.js +3 -1
  16. package/agents/napcat/skills/napcat-account/tools/set_avatar/index.ts +3 -1
  17. package/agents/napcat/skills/napcat-files-history/tools/download_file/index.js +3 -1
  18. package/agents/napcat/skills/napcat-files-history/tools/download_file/index.ts +3 -1
  19. package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.js +5 -5
  20. package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.ts +6 -6
  21. package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.js +5 -3
  22. package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.ts +6 -4
  23. package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.js +5 -7
  24. package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.ts +6 -8
  25. package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.js +5 -7
  26. package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.ts +6 -8
  27. package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.js +9 -7
  28. package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.ts +10 -9
  29. package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.js +5 -3
  30. package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.ts +6 -4
  31. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_file_url/index.js +4 -5
  32. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_file_url/index.ts +5 -6
  33. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_msg_history/index.js +4 -5
  34. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_msg_history/index.ts +5 -6
  35. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_root_files/index.js +5 -7
  36. package/agents/napcat/skills/napcat-group-files-history/tools/get_group_root_files/index.ts +6 -8
  37. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.js +6 -5
  38. package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.ts +8 -7
  39. package/agents/napcat/skills/napcat-group-media/tools/ai_tts/index.js +4 -5
  40. package/agents/napcat/skills/napcat-group-media/tools/ai_tts/index.ts +5 -6
  41. package/agents/napcat/skills/napcat-group-media/tools/get_ai_characters/index.js +5 -7
  42. package/agents/napcat/skills/napcat-group-media/tools/get_ai_characters/index.ts +6 -8
  43. package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.js +5 -7
  44. package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.ts +6 -8
  45. package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.js +5 -7
  46. package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.ts +6 -8
  47. package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.js +5 -7
  48. package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.ts +7 -9
  49. package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.js +6 -5
  50. package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.ts +7 -6
  51. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +5 -5
  52. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +6 -6
  53. package/agents/napcat/skills/napcat-media/tools/ocr_image/index.js +4 -3
  54. package/agents/napcat/skills/napcat-media/tools/ocr_image/index.ts +5 -4
  55. package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.js +3 -1
  56. package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.ts +3 -1
  57. package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.js +12 -6
  58. package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.ts +13 -16
  59. package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.js +10 -7
  60. package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.ts +10 -8
  61. package/agents/napcat/skills/napcat-private-history/tools/get_friend_msg_history/index.js +4 -5
  62. package/agents/napcat/skills/napcat-private-history/tools/get_friend_msg_history/index.ts +5 -6
  63. package/lib/endpoint-management.js +37 -0
  64. package/lib/http-endpoint.d.ts +3 -0
  65. package/lib/http-endpoint.js +14 -20
  66. package/lib/index.d.ts +0 -1
  67. package/lib/index.js +0 -1
  68. package/lib/protocol.d.ts +6 -1
  69. package/lib/protocol.js +13 -0
  70. package/lib/ws-endpoint.js +3 -2
  71. package/lib/ws-types.d.ts +6 -0
  72. package/lib/ws-types.js +31 -0
  73. package/lib/wss-auth.d.ts +2 -2
  74. package/lib/wss-auth.js +3 -12
  75. package/lib/wss-endpoint.d.ts +1 -1
  76. package/lib/wss-endpoint.js +22 -21
  77. package/package.json +24 -28
  78. package/plugin.js +0 -5
  79. package/schema.json +0 -56
  80. package/src/endpoint-management.ts +33 -0
  81. package/src/http-endpoint.ts +11 -20
  82. package/src/index.ts +0 -2
  83. package/src/protocol.ts +14 -1
  84. package/src/ws-endpoint.ts +3 -1
  85. package/src/ws-types.ts +33 -0
  86. package/src/wss-auth.ts +4 -11
  87. package/src/wss-endpoint.ts +22 -22
  88. package/lib/webhook.d.ts +0 -2
  89. package/lib/webhook.js +0 -14
  90. package/src/webhook.ts +0 -16
package/plugin.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin } from 'zhin.js';
3
- import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
4
3
  import { createNapcatConsoleRpc } from "./lib/console.js";
5
4
  export default definePlugin({
6
5
  name: 'napcat',
@@ -9,12 +8,8 @@ export default definePlugin({
9
8
  displayName: 'NapCat Adapter',
10
9
  },
11
10
  setup(context) {
12
- const dispose = context.resources.has(permissionHostToken)
13
- ? context.resources.use(permissionHostToken).registerPlatform('napcat', createSceneRolePlatformChecker())
14
- : undefined;
15
11
  return {
16
12
  consoleRpc: createNapcatConsoleRpc(context.resources, () => context.config.get()),
17
- ...(dispose ? { dispose } : {}),
18
13
  };
19
14
  },
20
15
  });
package/schema.json CHANGED
@@ -25,34 +25,6 @@
25
25
  "type": "number",
26
26
  "default": 30000
27
27
  },
28
- "master": {
29
- "type": [
30
- "string",
31
- "number"
32
- ],
33
- "description": "框架 master(QQ uin;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
34
- "x-i18n": {
35
- "en": {
36
- "description": "Framework masters (QQ uin; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
37
- }
38
- }
39
- },
40
- "trusted": {
41
- "type": "array",
42
- "items": {
43
- "type": [
44
- "string",
45
- "number"
46
- ],
47
- "description": "Trusted QQ uin"
48
- },
49
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
50
- "x-i18n": {
51
- "en": {
52
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
53
- }
54
- }
55
- },
56
28
  "endpoints": {
57
29
  "type": "array",
58
30
  "minItems": 1,
@@ -61,34 +33,6 @@
61
33
  "type": "object",
62
34
  "additionalProperties": false,
63
35
  "properties": {
64
- "master": {
65
- "type": [
66
- "string",
67
- "number"
68
- ],
69
- "description": "本 endpoint 的框架 master(QQ uin);覆盖顶层 master",
70
- "x-i18n": {
71
- "en": {
72
- "description": "Framework masters for this endpoint (QQ uin), overriding the top-level master list."
73
- }
74
- }
75
- },
76
- "trusted": {
77
- "type": "array",
78
- "items": {
79
- "type": [
80
- "string",
81
- "number"
82
- ],
83
- "description": "Trusted QQ uin"
84
- },
85
- "description": "本 endpoint 的 trusted 列表",
86
- "x-i18n": {
87
- "en": {
88
- "description": "Trusted users for this endpoint."
89
- }
90
- }
91
- },
92
36
  "connection": {
93
37
  "type": "string",
94
38
  "enum": [
@@ -16,16 +16,49 @@ import {
16
16
  type EndpointGroupRequest,
17
17
  type EndpointGroupNoticeRequest,
18
18
  } from 'zhin.js/adapter';
19
+ import { standingFromMemberRole, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
19
20
 
20
21
  /** 管理面只依赖 endpoint 的 callApi(ws/wss/http 三种传输各自实现)。 */
21
22
  export interface NapCatManagementCaller {
22
23
  requestApi(request: PendingRpcRequest): Promise<unknown>;
23
24
  }
24
25
 
26
+ /** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
27
+ const SELF_ID_TTL_MS = 5_000;
28
+
25
29
  export function createNapCatEndpointManagement(
26
30
  endpoint: NapCatManagementCaller,
27
31
  ): EndpointManagement {
32
+ // The login account can change across reconnects (or a different account on the same
33
+ // connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
34
+ // to coalesce bursts, and never across a failed lookup.
35
+ let login: { readonly id: string; readonly at: number } | undefined;
36
+ const loginId = async (signal: AbortSignal | undefined): Promise<string | undefined> => {
37
+ if (login && Date.now() - login.at < SELF_ID_TTL_MS) return login.id;
38
+ login = undefined;
39
+ const info = asRecord(await endpoint.requestApi({ action: 'get_login_info', signal }));
40
+ const id = info.user_id;
41
+ const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
42
+ if (value) login = { id: value, at: Date.now() };
43
+ return value;
44
+ };
28
45
  return Object.freeze<EndpointManagement>({
46
+ /** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
47
+ async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
48
+ if (conversation.kind !== 'group') return undefined;
49
+ const selfId = await loginId(signal);
50
+ if (!selfId) return undefined;
51
+ let data: Record<string, unknown>;
52
+ try {
53
+ data = asRecord(await endpoint.requestApi({ action: 'get_group_member_info', signal, params: {
54
+ group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
55
+ } }));
56
+ } catch (error) {
57
+ login = undefined;
58
+ throw error;
59
+ }
60
+ return standingFromMemberRole(data.role);
61
+ },
29
62
  async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
30
63
  const data = await endpoint.requestApi({ action: 'get_friend_list', signal: request.signal });
31
64
  return toArray(data).map((value) => {
@@ -3,7 +3,6 @@ import { toCanonicalSegments } from '@zhin.js/core';
3
3
  /**
4
4
  * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
5
5
  */
6
- import type { IncomingMessage, ServerResponse } from 'node:http';
7
6
  import {
8
7
  createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
9
8
  createRecallEndpointControl,
@@ -33,6 +32,7 @@ import {
33
32
  napcatInboundConversation,
34
33
  napcatOutboundTarget,
35
34
  senderNickname,
35
+ napcatSenderStanding,
36
36
  senderUserId,
37
37
  type NapCatEvent,
38
38
  type NapCatHttpConfig,
@@ -40,7 +40,7 @@ import {
40
40
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
41
41
  import { createNapCatContentPort } from './onebot-get-msg.js';
42
42
  import { NapcatClient } from './client.js';
43
- import { readRequestBody } from './webhook.js';
43
+ import { readTextBody } from '@zhin.js/host-http-contract';
44
44
  import { verifyNapCatAccessToken } from './wss-auth.js';
45
45
 
46
46
  export interface NapCatHttpEndpointOptions {
@@ -175,6 +175,7 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
175
175
  const mentioned = isNapCatBotMentioned(ev);
176
176
  const quoteId = extractQuoteId(ev);
177
177
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
178
+ const platform = napcatSenderStanding(ev);
178
179
  void this[endpointSpec].events.message({
179
180
  event: nativeEvent,
180
181
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -185,7 +186,7 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
185
186
  sender: {
186
187
  id: senderUserId(ev),
187
188
  name: nickname,
188
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
189
+ ...(platform ? { platform } : {}),
189
190
  },
190
191
  endpointId: this.#options.config.id,
191
192
  ...(mentioned ? { mentioned: true } : {}),
@@ -211,41 +212,31 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
211
212
  #setupRoutes(): void {
212
213
  const path = this.#options.config.post_path;
213
214
  this.#routeReleases.push(
214
- this.#options.http.route('POST', path, async (request, response) => {
215
- await this.#handlePost(request, response);
216
- }, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
215
+ this.#options.http.route('POST', path, (request) => this.#handlePost(request), { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
217
216
  );
218
217
  }
219
218
 
220
- async #handlePost(request: IncomingMessage, response: ServerResponse): Promise<void> {
219
+ async #handlePost(request: Request): Promise<Response> {
221
220
  try {
222
221
  if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
223
- response.writeHead(403, { 'Content-Type': 'application/json' });
224
- response.end(JSON.stringify({ message: 'Unauthorized' }));
225
- return;
222
+ return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
226
223
  }
227
- const raw = await readRequestBody(request);
224
+ const raw = await readTextBody(request);
228
225
  let ev: NapCatEvent;
229
226
  try {
230
227
  ev = JSON.parse(raw) as NapCatEvent;
231
228
  } catch {
232
- response.writeHead(400, { 'Content-Type': 'application/json' });
233
- response.end(JSON.stringify({ message: 'Invalid JSON' }));
234
- return;
229
+ return new Response(JSON.stringify({ message: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
235
230
  }
236
231
  if (this.#open) {
237
232
  void this[endpointSpec].events.run(() => this.ingest(ev)).catch((error: unknown) => {
238
233
  this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
239
234
  });
240
235
  }
241
- response.writeHead(200, { 'Content-Type': 'application/json' });
242
- response.end(JSON.stringify({ status: 'ok' }));
236
+ return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
243
237
  } catch (error) {
244
238
  this.#logger.error('NapCat HTTP webhook error:', error);
245
- if (!response.headersSent) {
246
- response.writeHead(500, { 'Content-Type': 'application/json' });
247
- response.end(JSON.stringify({ message: 'Internal Server Error' }));
248
- }
239
+ return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
249
240
  }
250
241
  }
251
242
  }
package/src/index.ts CHANGED
@@ -73,5 +73,3 @@ export {
73
73
  } from './ws-transport.js';
74
74
 
75
75
  export { verifyNapCatAccessToken } from './wss-auth.js';
76
-
77
- export { readRequestBody } from './webhook.js';
package/src/protocol.ts CHANGED
@@ -4,7 +4,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
4
4
  * No legacy Adapter/Endpoint / segment-mapper.
5
5
  * Canonicalization is owned by gateway/core before endpoint.send.
6
6
  */
7
- import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
7
+ import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
8
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
9
 
10
10
  const logger = getLogger('napcat');
@@ -273,6 +273,16 @@ function stripCqCodes(input: string): string {
273
273
  * 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
274
274
  * 显示名走 metadata.nickname,见 senderNickname。
275
275
  */
276
+ /**
277
+ * ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
278
+ * 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
279
+ */
280
+ export function napcatSenderStanding(ev: NapCatEvent): PlatformStanding | undefined {
281
+ if (ev.message_type !== 'group') return undefined;
282
+ const role = ev.sender?.role;
283
+ return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
284
+ }
285
+
276
286
  export function senderUserId(ev: NapCatEvent): string {
277
287
  return ev.user_id != null ? String(ev.user_id) : '';
278
288
  }
@@ -379,6 +389,9 @@ function canonicalToOneBotSegment(segment: NapCatWireSegment): MessageSegment |
379
389
  return oneBotMediaSegment(segment.type, data);
380
390
  case 'audio':
381
391
  return oneBotMediaSegment('record', data);
392
+ case 'napcat:node':
393
+ // Agent-authored merged-forward node (napcat_send_forward_msg); NapCat sends `node` arrays as a forward.
394
+ return { type: 'node', data };
382
395
  default:
383
396
  return { type: segment.type, data };
384
397
  }
@@ -35,6 +35,7 @@ import {
35
35
  napcatInboundConversation,
36
36
  napcatOutboundTarget,
37
37
  senderNickname,
38
+ napcatSenderStanding,
38
39
  senderUserId,
39
40
  type NapCatEvent,
40
41
  type NapCatWsConfig,
@@ -194,6 +195,7 @@ export class NapCatWsEndpoint extends NapcatClient implements Endpoint {
194
195
  const mentioned = isNapCatBotMentioned(ev);
195
196
  const quoteId = extractQuoteId(ev);
196
197
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
198
+ const platform = napcatSenderStanding(ev);
197
199
  void this[endpointSpec].events.message({
198
200
  event: nativeEvent,
199
201
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -204,7 +206,7 @@ export class NapCatWsEndpoint extends NapcatClient implements Endpoint {
204
206
  sender: {
205
207
  id: senderUserId(ev),
206
208
  name: nickname,
207
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
209
+ ...(platform ? { platform } : {}),
208
210
  },
209
211
  endpointId: this.#options.config.id,
210
212
  ...(mentioned ? { mentioned: true } : {}),
package/src/ws-types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { WsConnection } from '@zhin.js/host-http';
2
+
1
3
  /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
4
  export interface NapCatWsSocket {
3
5
  readonly readyState: number;
@@ -13,3 +15,34 @@ export interface NapCatWsCreateOptions {
13
15
  }
14
16
 
15
17
  export const WS_OPEN = 1;
18
+
19
+ /**
20
+ * A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
21
+ * keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
22
+ */
23
+ export function socketOfConnection(connection: WsConnection): NapCatWsSocket {
24
+ let readyState = 1;
25
+ connection.onClose(() => { readyState = 3; });
26
+ const subscriptions = new Map<(...args: unknown[]) => void, () => void>();
27
+ const off = (_event: string, listener: (...args: unknown[]) => void): void => {
28
+ subscriptions.get(listener)?.();
29
+ subscriptions.delete(listener);
30
+ };
31
+ return {
32
+ get readyState() { return readyState; },
33
+ send: (data: string) => connection.send(data),
34
+ close: (code?: number, reason?: string) => connection.close(code, reason),
35
+ on(event: string, listener: (...args: unknown[]) => void) {
36
+ if (event === 'message') {
37
+ subscriptions.set(listener, connection.onMessage((data) => {
38
+ listener(typeof data === 'string' ? data : Buffer.from(data));
39
+ }));
40
+ } else if (event === 'close') {
41
+ subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
42
+ }
43
+ // 'error' surfaces as a close on a WsConnection; 'open' already happened.
44
+ },
45
+ off,
46
+ removeListener: off,
47
+ } as NapCatWsSocket;
48
+ }
package/src/wss-auth.ts CHANGED
@@ -1,17 +1,10 @@
1
- import type { IncomingMessage } from 'node:http';
1
+ import { verifyProtocolAccessToken, type ProtocolAuthRequest } from '@zhin.js/host-http';
2
2
 
3
3
  export function verifyNapCatAccessToken(
4
4
  accessToken: string | undefined,
5
- request: IncomingMessage,
5
+ request: ProtocolAuthRequest,
6
6
  ): boolean {
7
+ // Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
7
8
  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;
9
+ return verifyProtocolAccessToken(accessToken, request);
17
10
  }
@@ -13,7 +13,7 @@ import {
13
13
  type EndpointSendRequest,
14
14
  type EndpointTransportState,
15
15
  } from 'zhin.js/adapter';
16
- import type { HttpHost, WsConnection } from '@zhin.js/host-http';
16
+ import { verifyProtocolAccessToken, type HttpHost, type WsConnection } from '@zhin.js/host-http';
17
17
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
18
18
  import type { CapabilityId } from 'zhin.js';
19
19
  import { createNapCatEndpointManagement } from './endpoint-management.js';
@@ -33,6 +33,7 @@ import {
33
33
  napcatInboundConversation,
34
34
  napcatOutboundTarget,
35
35
  senderNickname,
36
+ napcatSenderStanding,
36
37
  senderUserId,
37
38
  type NapCatEvent,
38
39
  type NapCatWssConfig,
@@ -40,10 +41,7 @@ import {
40
41
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
41
42
  import { createNapCatContentPort } from './onebot-get-msg.js';
42
43
  import { NapcatClient } from './client.js';
43
- import {
44
- type NapCatWsSocket,
45
- } from './ws-types.js';
46
- import { verifyNapCatAccessToken } from './wss-auth.js';
44
+ import { socketOfConnection, type NapCatWsSocket } from './ws-types.js';
47
45
 
48
46
  export interface NapCatWssEndpointOptions {
49
47
  readonly id: CapabilityId;
@@ -110,7 +108,17 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
110
108
  if (this.#started) return;
111
109
  this.#stopped = false;
112
110
  this.#started = true;
113
- const handle = this.#options.http.ws(this.#options.config.path);
111
+ if (!this.#options.config.access_token) {
112
+ // 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
113
+ this.#logger.warn(formatCompact({
114
+ endpoint: this.#options.config.id,
115
+ mode: 'wss',
116
+ ok: false,
117
+ error: 'missing access_token: only loopback peers are accepted',
118
+ }));
119
+ }
120
+ // The peer authenticates with this endpoint's own access_token, not a Console credential.
121
+ const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
114
122
  this.#wsRelease = handle.onConnection((connection) => {
115
123
  this.#connectionTask = this.#connectionTask
116
124
  .then(() => this.#acceptConnection(connection))
@@ -199,6 +207,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
199
207
  const mentioned = isNapCatBotMentioned(ev);
200
208
  const quoteId = extractQuoteId(ev);
201
209
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
210
+ const platform = napcatSenderStanding(ev);
202
211
  void this[endpointSpec].events.message({
203
212
  event: nativeEvent,
204
213
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -209,7 +218,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
209
218
  sender: {
210
219
  id: senderUserId(ev),
211
220
  name: nickname,
212
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
221
+ ...(platform ? { platform } : {}),
213
222
  },
214
223
  endpointId: this.#options.config.id,
215
224
  ...(mentioned ? { mentioned: true } : {}),
@@ -234,14 +243,14 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
234
243
 
235
244
  async #acceptConnection(connection: WsConnection): Promise<void> {
236
245
  if (!this.#started || this.#stopped) {
237
- connection.socket.close();
246
+ connection.close();
238
247
  return;
239
248
  }
240
- if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
241
- connection.socket.close(4003, 'Unauthorized');
249
+ if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
250
+ connection.close(4003, 'Unauthorized');
242
251
  return;
243
252
  }
244
- const socket = connection.socket as unknown as NapCatWsSocket;
253
+ const socket = socketOfConnection(connection);
245
254
  await this.#connectionLifecycle.stop();
246
255
  this.rpc.disconnect('Connection replaced');
247
256
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
@@ -257,16 +266,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
257
266
  /* ignore */
258
267
  }
259
268
  });
260
- this.#connectionLifecycle.startHeartbeat(() => {
261
- try {
262
- socket.ping?.();
263
- } catch {
264
- /* ignore */
265
- }
266
- }, this.#options.config.heartbeat_interval);
267
- socket.on('pong', () => {
268
- if (this.transportSocket === socket) this.#connectionLifecycle.notifyHeartbeatAck();
269
- });
269
+ // Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
270
270
  socket.on('message', (data) => {
271
271
  if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed') return;
272
272
  this.#connectionLifecycle.notifyHeartbeatAck();
@@ -286,7 +286,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
286
286
  this.#logger.debug(formatCompact({
287
287
  endpoint: this.#options.config.id,
288
288
  mode: 'wss',
289
- peer: connection.request.socket.remoteAddress,
289
+ peer: connection.remoteAddress,
290
290
  }));
291
291
  }
292
292
  }
package/lib/webhook.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { IncomingMessage } from 'node:http';
2
- export declare function readRequestBody(request: IncomingMessage): Promise<string>;
package/lib/webhook.js DELETED
@@ -1,14 +0,0 @@
1
- export async function readRequestBody(request) {
2
- const chunks = [];
3
- let size = 0;
4
- for await (const chunk of request) {
5
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
6
- size += buffer.length;
7
- if (size > 1_048_576) {
8
- request.destroy();
9
- throw new Error('Request body exceeds 1MB');
10
- }
11
- chunks.push(buffer);
12
- }
13
- return Buffer.concat(chunks).toString('utf8');
14
- }
package/src/webhook.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { IncomingMessage } from 'node:http';
2
-
3
- export async function readRequestBody(request: IncomingMessage): Promise<string> {
4
- const chunks: Buffer[] = [];
5
- let size = 0;
6
- for await (const chunk of request) {
7
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8
- size += buffer.length;
9
- if (size > 1_048_576) {
10
- request.destroy();
11
- throw new Error('Request body exceeds 1MB');
12
- }
13
- chunks.push(buffer);
14
- }
15
- return Buffer.concat(chunks).toString('utf8');
16
- }