@zhin.js/adapter-napcat 1.1.26 → 1.1.28

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 +206 -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
@@ -1,19 +1,22 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { messages as t } from "../../../../messages.js";
3
+ import { currentTarget } from "../../../../conversation.js";
3
4
  import { defineAgentTool } from '@zhin.js/tool';
4
5
  import { z } from 'zod';
5
6
  export default defineAgentTool({
6
- description: '戳一戳(群聊或私聊)。group_id 不传时为私聊戳一戳。',
7
+ description: '在当前会话戳一戳:群聊中戳 user_id 指定的成员,私聊中戳对方。',
7
8
  inputSchema: z.object({
8
- user_id: z.number().describe('目标用户 QQ 号'),
9
- group_id: z.number().optional().describe('群号(不传则为私聊戳一戳)'),
9
+ user_id: z.number().optional().describe('群聊中要戳的成员 QQ 号(私聊中忽略,固定为对方)'),
10
10
  }),
11
11
  adapter: 'napcat',
12
12
  tags: ['napcat', 'qq'],
13
13
  keywords: ['戳一戳', 'poke', '戳', '拍一拍'],
14
- async execute({ user_id, group_id }, context) {
15
- const endpoint = context.$client;
16
- await endpoint.sendPoke(user_id, group_id);
17
- return { success: true, message: t(context.locale, 'userPoked', { p0: user_id }) };
14
+ async execute({ user_id }, context) {
15
+ const target = currentTarget(context);
16
+ const userId = target.type === 'group' ? user_id : target.id;
17
+ if (userId === undefined)
18
+ return { success: false, message: t(context.locale, 'pokeTargetRequired') };
19
+ await context.$client.sendPoke(userId, target.type === 'group' ? target.id : undefined);
20
+ return { success: true, message: t(context.locale, 'userPoked', { p0: userId }) };
18
21
  },
19
22
  });
@@ -1,19 +1,21 @@
1
1
  import { messages as t } from "../../../../messages.js";
2
+ import { currentTarget } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
 
5
- export default defineAgentTool<{ user_id: number; group_id?: number }>({
6
- description: '戳一戳(群聊或私聊)。group_id 不传时为私聊戳一戳。',
6
+ export default defineAgentTool<{ user_id?: number }>({
7
+ description: '在当前会话戳一戳:群聊中戳 user_id 指定的成员,私聊中戳对方。',
7
8
  inputSchema: z.object({
8
- user_id: z.number().describe('目标用户 QQ 号'),
9
- group_id: z.number().optional().describe('群号(不传则为私聊戳一戳)'),
9
+ user_id: z.number().optional().describe('群聊中要戳的成员 QQ 号(私聊中忽略,固定为对方)'),
10
10
  }),
11
11
  adapter: 'napcat',
12
12
  tags: ['napcat', 'qq'],
13
13
  keywords: ['戳一戳', 'poke', '戳', '拍一拍'],
14
- async execute({ user_id, group_id }: { user_id: number; group_id?: number }, context) {
15
- const endpoint = context.$client;
16
- await endpoint.sendPoke(user_id, group_id);
17
- return { success: true, message: t(context.locale, 'userPoked', { p0: user_id }) };
14
+ async execute({ user_id }, context) {
15
+ const target = currentTarget(context);
16
+ const userId = target.type === 'group' ? user_id : target.id;
17
+ if (userId === undefined) return { success: false, message: t(context.locale, 'pokeTargetRequired') };
18
+ await context.$client.sendPoke(userId, target.type === 'group' ? target.id : undefined);
19
+ return { success: true, message: t(context.locale, 'userPoked', { p0: userId }) };
18
20
  },
19
21
  });
@@ -1,10 +1,10 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { currentPrivatePeer } from "../../../../conversation.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { z } from 'zod';
4
5
  export default defineAgentTool({
5
- description: '获取私聊消息历史记录。',
6
+ description: '获取当前私聊的消息历史记录。',
6
7
  inputSchema: z.object({
7
- user_id: z.number().describe('QQ 号'),
8
8
  message_seq: z.number().optional().describe('起始消息序号(可选)'),
9
9
  count: z.number().optional().describe('获取条数(可选)'),
10
10
  }),
@@ -12,8 +12,7 @@ export default defineAgentTool({
12
12
  scopes: ["private"],
13
13
  tags: ['napcat', 'qq'],
14
14
  keywords: ['私聊记录', '好友聊天记录', 'friend history'],
15
- async execute({ user_id, message_seq, count }, context) {
16
- const endpoint = context.$client;
17
- return endpoint.getFriendMsgHistory(user_id, message_seq, count);
15
+ async execute({ message_seq, count }, context) {
16
+ return context.$client.getFriendMsgHistory(currentPrivatePeer(context), message_seq, count);
18
17
  },
19
18
  });
@@ -1,10 +1,10 @@
1
+ import { currentPrivatePeer } from "../../../../conversation.js";
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { z } from 'zod';
3
4
 
4
- export default defineAgentTool<{ user_id: number; message_seq?: number; count?: number }>({
5
- description: '获取私聊消息历史记录。',
5
+ export default defineAgentTool<{ message_seq?: number; count?: number }>({
6
+ description: '获取当前私聊的消息历史记录。',
6
7
  inputSchema: z.object({
7
- user_id: z.number().describe('QQ 号'),
8
8
  message_seq: z.number().optional().describe('起始消息序号(可选)'),
9
9
  count: z.number().optional().describe('获取条数(可选)'),
10
10
  }),
@@ -12,8 +12,7 @@ export default defineAgentTool<{ user_id: number; message_seq?: number; count?:
12
12
  scopes: ["private"],
13
13
  tags: ['napcat', 'qq'],
14
14
  keywords: ['私聊记录', '好友聊天记录', 'friend history'],
15
- async execute({ user_id, message_seq, count }: { user_id: number; message_seq?: number; count?: number }, context) {
16
- const endpoint = context.$client;
17
- return endpoint.getFriendMsgHistory(user_id, message_seq, count);
15
+ async execute({ message_seq, count }, context) {
16
+ return context.$client.getFriendMsgHistory(currentPrivatePeer(context), message_seq, count);
18
17
  },
19
18
  });
@@ -1,5 +1,42 @@
1
+ import { standingFromMemberRole } from '@zhin.js/im-contract';
2
+ /** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
3
+ const SELF_ID_TTL_MS = 5_000;
1
4
  export function createNapCatEndpointManagement(endpoint) {
5
+ // The login account can change across reconnects (or a different account on the same
6
+ // connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
7
+ // to coalesce bursts, and never across a failed lookup.
8
+ let login;
9
+ const loginId = async (signal) => {
10
+ if (login && Date.now() - login.at < SELF_ID_TTL_MS)
11
+ return login.id;
12
+ login = undefined;
13
+ const info = asRecord(await endpoint.requestApi({ action: 'get_login_info', signal }));
14
+ const id = info.user_id;
15
+ const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
16
+ if (value)
17
+ login = { id: value, at: Date.now() };
18
+ return value;
19
+ };
2
20
  return Object.freeze({
21
+ /** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
22
+ async inspectSelfStanding({ conversation, signal }) {
23
+ if (conversation.kind !== 'group')
24
+ return undefined;
25
+ const selfId = await loginId(signal);
26
+ if (!selfId)
27
+ return undefined;
28
+ let data;
29
+ try {
30
+ data = asRecord(await endpoint.requestApi({ action: 'get_group_member_info', signal, params: {
31
+ group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
32
+ } }));
33
+ }
34
+ catch (error) {
35
+ login = undefined;
36
+ throw error;
37
+ }
38
+ return standingFromMemberRole(data.role);
39
+ },
3
40
  async listFriends(request = {}) {
4
41
  const data = await endpoint.requestApi({ action: 'get_friend_list', signal: request.signal });
5
42
  return toArray(data).map((value) => {
@@ -1,3 +1,6 @@
1
+ /**
2
+ * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
3
+ */
1
4
  import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
2
5
  import type { HttpHost } from '@zhin.js/host-http';
3
6
  import type { CapabilityId } from 'zhin.js';
@@ -1,15 +1,18 @@
1
1
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
2
  import { toCanonicalSegments } from '@zhin.js/core';
3
+ /**
4
+ * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
5
+ */
3
6
  import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, } from 'zhin.js/adapter';
4
7
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
5
8
  import { createNapCatEndpointManagement } from './endpoint-management.js';
6
9
  import { createNapcatSelfProfile } from './self-profile.js';
7
10
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
8
- import { buildSendAction, callNapCatHttpAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
11
+ import { buildSendAction, callNapCatHttpAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
9
12
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
10
13
  import { createNapCatContentPort } from './onebot-get-msg.js';
11
14
  import { NapcatClient } from './client.js';
12
- import { readRequestBody } from './webhook.js';
15
+ import { readTextBody } from '@zhin.js/host-http-contract';
13
16
  import { verifyNapCatAccessToken } from './wss-auth.js';
14
17
  export class NapCatHttpEndpoint extends NapcatClient {
15
18
  [endpointSpec];
@@ -123,6 +126,7 @@ export class NapCatHttpEndpoint extends NapcatClient {
123
126
  const mentioned = isNapCatBotMentioned(ev);
124
127
  const quoteId = extractQuoteId(ev);
125
128
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
129
+ const platform = napcatSenderStanding(ev);
126
130
  void this[endpointSpec].events.message({
127
131
  event: nativeEvent,
128
132
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -133,7 +137,7 @@ export class NapCatHttpEndpoint extends NapcatClient {
133
137
  sender: {
134
138
  id: senderUserId(ev),
135
139
  name: nickname,
136
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
140
+ ...(platform ? { platform } : {}),
137
141
  },
138
142
  endpointId: this.#options.config.id,
139
143
  ...(mentioned ? { mentioned: true } : {}),
@@ -157,41 +161,31 @@ export class NapCatHttpEndpoint extends NapcatClient {
157
161
  }
158
162
  #setupRoutes() {
159
163
  const path = this.#options.config.post_path;
160
- this.#routeReleases.push(this.#options.http.route('POST', path, async (request, response) => {
161
- await this.#handlePost(request, response);
162
- }, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }));
164
+ this.#routeReleases.push(this.#options.http.route('POST', path, (request) => this.#handlePost(request), { summary: 'NapCat HTTP event callback', tags: ['napcat'] }));
163
165
  }
164
- async #handlePost(request, response) {
166
+ async #handlePost(request) {
165
167
  try {
166
168
  if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
167
- response.writeHead(403, { 'Content-Type': 'application/json' });
168
- response.end(JSON.stringify({ message: 'Unauthorized' }));
169
- return;
169
+ return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
170
170
  }
171
- const raw = await readRequestBody(request);
171
+ const raw = await readTextBody(request);
172
172
  let ev;
173
173
  try {
174
174
  ev = JSON.parse(raw);
175
175
  }
176
176
  catch {
177
- response.writeHead(400, { 'Content-Type': 'application/json' });
178
- response.end(JSON.stringify({ message: 'Invalid JSON' }));
179
- return;
177
+ return new Response(JSON.stringify({ message: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
180
178
  }
181
179
  if (this.#open) {
182
180
  void this[endpointSpec].events.run(() => this.ingest(ev)).catch((error) => {
183
181
  this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
184
182
  });
185
183
  }
186
- response.writeHead(200, { 'Content-Type': 'application/json' });
187
- response.end(JSON.stringify({ status: 'ok' }));
184
+ return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
188
185
  }
189
186
  catch (error) {
190
187
  this.#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
- }
188
+ return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
195
189
  }
196
190
  }
197
191
  }
package/lib/index.d.ts CHANGED
@@ -9,4 +9,3 @@ export { NapCatHttpEndpoint, type NapCatHttpEndpointOptions, } from './http-endp
9
9
  export type { NapCatWsSocket, NapCatWsCreateOptions } from './ws-types.js';
10
10
  export { createNapCatRpc, decodeWsPayload, type NapCatRpc, type NapCatRpcOptions, } from './ws-transport.js';
11
11
  export { verifyNapCatAccessToken } from './wss-auth.js';
12
- export { readRequestBody } from './webhook.js';
package/lib/index.js CHANGED
@@ -7,4 +7,3 @@ export { NapCatWssEndpoint, } from './wss-endpoint.js';
7
7
  export { NapCatHttpEndpoint, } from './http-endpoint.js';
8
8
  export { createNapCatRpc, decodeWsPayload, } from './ws-transport.js';
9
9
  export { verifyNapCatAccessToken } from './wss-auth.js';
10
- export { readRequestBody } from './webhook.js';
package/lib/protocol.d.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 { type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
7
+ import { type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
8
8
  /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
9
9
  export interface NapCatEndpointConfig extends EndpointProfileConfiguration {
10
10
  readonly connection?: 'ws' | 'wss' | 'http';
@@ -116,6 +116,11 @@ export declare function formatInboundContent(ev: NapCatEvent): string;
116
116
  * 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
117
117
  * 显示名走 metadata.nickname,见 senderNickname。
118
118
  */
119
+ /**
120
+ * ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
121
+ * 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
122
+ */
123
+ export declare function napcatSenderStanding(ev: NapCatEvent): PlatformStanding | undefined;
119
124
  export declare function senderUserId(ev: NapCatEvent): string;
120
125
  /** 显示名(群名片 card 优先于 nickname),没有则 undefined。 */
121
126
  export declare function senderNickname(ev: NapCatEvent): string | undefined;
package/lib/protocol.js CHANGED
@@ -149,6 +149,16 @@ function stripCqCodes(input) {
149
149
  * 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
150
150
  * 显示名走 metadata.nickname,见 senderNickname。
151
151
  */
152
+ /**
153
+ * ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
154
+ * 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
155
+ */
156
+ export function napcatSenderStanding(ev) {
157
+ if (ev.message_type !== 'group')
158
+ return undefined;
159
+ const role = ev.sender?.role;
160
+ return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
161
+ }
152
162
  export function senderUserId(ev) {
153
163
  return ev.user_id != null ? String(ev.user_id) : '';
154
164
  }
@@ -252,6 +262,9 @@ function canonicalToOneBotSegment(segment) {
252
262
  return oneBotMediaSegment(segment.type, data);
253
263
  case 'audio':
254
264
  return oneBotMediaSegment('record', data);
265
+ case 'napcat:node':
266
+ // Agent-authored merged-forward node (napcat_send_forward_msg); NapCat sends `node` arrays as a forward.
267
+ return { type: 'node', data };
255
268
  default:
256
269
  return { type: segment.type, data };
257
270
  }
@@ -9,7 +9,7 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
9
  import { createNapCatEndpointManagement } from './endpoint-management.js';
10
10
  import { createNapcatSelfProfile } from './self-profile.js';
11
11
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
12
- import { buildSendAction, buildWsConnectOptions, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
12
+ import { buildSendAction, buildWsConnectOptions, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
13
13
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
14
14
  import { createNapCatContentPort } from './onebot-get-msg.js';
15
15
  import { NapcatClient } from './client.js';
@@ -137,6 +137,7 @@ export class NapCatWsEndpoint extends NapcatClient {
137
137
  const mentioned = isNapCatBotMentioned(ev);
138
138
  const quoteId = extractQuoteId(ev);
139
139
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
140
+ const platform = napcatSenderStanding(ev);
140
141
  void this[endpointSpec].events.message({
141
142
  event: nativeEvent,
142
143
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -147,7 +148,7 @@ export class NapCatWsEndpoint extends NapcatClient {
147
148
  sender: {
148
149
  id: senderUserId(ev),
149
150
  name: nickname,
150
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
151
+ ...(platform ? { platform } : {}),
151
152
  },
152
153
  endpointId: this.#options.config.id,
153
154
  ...(mentioned ? { mentioned: true } : {}),
package/lib/ws-types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { WsConnection } from '@zhin.js/host-http';
1
2
  /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
3
  export interface NapCatWsSocket {
3
4
  readonly readyState: number;
@@ -11,3 +12,8 @@ export interface NapCatWsCreateOptions {
11
12
  readonly headers?: Record<string, string>;
12
13
  }
13
14
  export declare const WS_OPEN = 1;
15
+ /**
16
+ * A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
17
+ * keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
18
+ */
19
+ export declare function socketOfConnection(connection: WsConnection): NapCatWsSocket;
package/lib/ws-types.js CHANGED
@@ -1 +1,32 @@
1
1
  export const WS_OPEN = 1;
2
+ /**
3
+ * A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
4
+ * keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
5
+ */
6
+ export function socketOfConnection(connection) {
7
+ let readyState = 1;
8
+ connection.onClose(() => { readyState = 3; });
9
+ const subscriptions = new Map();
10
+ const off = (_event, listener) => {
11
+ subscriptions.get(listener)?.();
12
+ subscriptions.delete(listener);
13
+ };
14
+ return {
15
+ get readyState() { return readyState; },
16
+ send: (data) => connection.send(data),
17
+ close: (code, reason) => connection.close(code, reason),
18
+ on(event, listener) {
19
+ if (event === 'message') {
20
+ subscriptions.set(listener, connection.onMessage((data) => {
21
+ listener(typeof data === 'string' ? data : Buffer.from(data));
22
+ }));
23
+ }
24
+ else if (event === 'close') {
25
+ subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
26
+ }
27
+ // 'error' surfaces as a close on a WsConnection; 'open' already happened.
28
+ },
29
+ off,
30
+ removeListener: off,
31
+ };
32
+ }
package/lib/wss-auth.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { IncomingMessage } from 'node:http';
2
- export declare function verifyNapCatAccessToken(accessToken: string | undefined, request: IncomingMessage): boolean;
1
+ import { type ProtocolAuthRequest } from '@zhin.js/host-http';
2
+ export declare function verifyNapCatAccessToken(accessToken: string | undefined, request: ProtocolAuthRequest): boolean;
package/lib/wss-auth.js CHANGED
@@ -1,16 +1,7 @@
1
+ import { verifyProtocolAccessToken } from '@zhin.js/host-http';
1
2
  export function verifyNapCatAccessToken(accessToken, request) {
3
+ // Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
2
4
  if (!accessToken)
3
5
  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;
6
+ return verifyProtocolAccessToken(accessToken, request);
16
7
  }
@@ -2,7 +2,7 @@
2
2
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
3
3
  */
4
4
  import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
5
- import type { HttpHost } from '@zhin.js/host-http';
5
+ import { type HttpHost } from '@zhin.js/host-http';
6
6
  import type { CapabilityId } from 'zhin.js';
7
7
  import { type NapCatWssConfig } from './protocol.js';
8
8
  import { NapcatClient } from './client.js';
@@ -4,15 +4,16 @@ import { toCanonicalSegments } from '@zhin.js/core';
4
4
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
5
5
  */
6
6
  import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
7
+ import { verifyProtocolAccessToken } from '@zhin.js/host-http';
7
8
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
9
  import { createNapCatEndpointManagement } from './endpoint-management.js';
9
10
  import { createNapcatSelfProfile } from './self-profile.js';
10
11
  import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
11
- import { buildSendAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
12
+ import { buildSendAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
12
13
  import { receiveNapCatSideEvent } from './side-event-dispatch.js';
13
14
  import { createNapCatContentPort } from './onebot-get-msg.js';
14
15
  import { NapcatClient } from './client.js';
15
- import { verifyNapCatAccessToken } from './wss-auth.js';
16
+ import { socketOfConnection } from './ws-types.js';
16
17
  export class NapCatWssEndpoint extends NapcatClient {
17
18
  [endpointSpec];
18
19
  #logger;
@@ -71,7 +72,17 @@ export class NapCatWssEndpoint extends NapcatClient {
71
72
  return;
72
73
  this.#stopped = false;
73
74
  this.#started = true;
74
- const handle = this.#options.http.ws(this.#options.config.path);
75
+ if (!this.#options.config.access_token) {
76
+ // 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
77
+ this.#logger.warn(formatCompact({
78
+ endpoint: this.#options.config.id,
79
+ mode: 'wss',
80
+ ok: false,
81
+ error: 'missing access_token: only loopback peers are accepted',
82
+ }));
83
+ }
84
+ // The peer authenticates with this endpoint's own access_token, not a Console credential.
85
+ const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
75
86
  this.#wsRelease = handle.onConnection((connection) => {
76
87
  this.#connectionTask = this.#connectionTask
77
88
  .then(() => this.#acceptConnection(connection))
@@ -150,6 +161,7 @@ export class NapCatWssEndpoint extends NapcatClient {
150
161
  const mentioned = isNapCatBotMentioned(ev);
151
162
  const quoteId = extractQuoteId(ev);
152
163
  const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
164
+ const platform = napcatSenderStanding(ev);
153
165
  void this[endpointSpec].events.message({
154
166
  event: nativeEvent,
155
167
  ...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
@@ -160,7 +172,7 @@ export class NapCatWssEndpoint extends NapcatClient {
160
172
  sender: {
161
173
  id: senderUserId(ev),
162
174
  name: nickname,
163
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
175
+ ...(platform ? { platform } : {}),
164
176
  },
165
177
  endpointId: this.#options.config.id,
166
178
  ...(mentioned ? { mentioned: true } : {}),
@@ -184,14 +196,14 @@ export class NapCatWssEndpoint extends NapcatClient {
184
196
  }
185
197
  async #acceptConnection(connection) {
186
198
  if (!this.#started || this.#stopped) {
187
- connection.socket.close();
199
+ connection.close();
188
200
  return;
189
201
  }
190
- if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
191
- connection.socket.close(4003, 'Unauthorized');
202
+ if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
203
+ connection.close(4003, 'Unauthorized');
192
204
  return;
193
205
  }
194
- const socket = connection.socket;
206
+ const socket = socketOfConnection(connection);
195
207
  await this.#connectionLifecycle.stop();
196
208
  this.rpc.disconnect('Connection replaced');
197
209
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
@@ -208,18 +220,7 @@ export class NapCatWssEndpoint extends NapcatClient {
208
220
  /* ignore */
209
221
  }
210
222
  });
211
- this.#connectionLifecycle.startHeartbeat(() => {
212
- try {
213
- socket.ping?.();
214
- }
215
- catch {
216
- /* ignore */
217
- }
218
- }, this.#options.config.heartbeat_interval);
219
- socket.on('pong', () => {
220
- if (this.transportSocket === socket)
221
- this.#connectionLifecycle.notifyHeartbeatAck();
222
- });
223
+ // Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
223
224
  socket.on('message', (data) => {
224
225
  if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed')
225
226
  return;
@@ -242,7 +243,7 @@ export class NapCatWssEndpoint extends NapcatClient {
242
243
  this.#logger.debug(formatCompact({
243
244
  endpoint: this.#options.config.id,
244
245
  mode: 'wss',
245
- peer: connection.request.socket.remoteAddress,
246
+ peer: connection.remoteAddress,
246
247
  }));
247
248
  }
248
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-napcat",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,7 +19,6 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "tools",
23
22
  "agents",
24
23
  "README.md",
25
24
  "CHANGELOG.md",
@@ -40,40 +39,40 @@
40
39
  "license": "MIT",
41
40
  "repository": {
42
41
  "type": "git",
43
- "url": "https://github.com/zhinjs/zhin",
42
+ "url": "git+https://github.com/zhinjs/zhin.git",
44
43
  "directory": "plugins/adapters/napcat"
45
44
  },
46
45
  "dependencies": {
47
46
  "ws": "^8.21.1",
48
- "@zhin.js/adapter-onebot-events": "0.0.5",
49
- "@zhin.js/adapter": "1.1.32",
50
- "@zhin.js/agent-feature": "1.1.16",
51
- "@zhin.js/console-contract": "1.1.5",
52
- "@zhin.js/core": "1.1.60",
53
- "@zhin.js/feature-kit": "1.1.15",
54
- "@zhin.js/host-http": "1.1.17",
55
- "@zhin.js/im-contract": "1.1.13",
56
- "@zhin.js/logger": "1.1.2",
57
- "@zhin.js/page": "1.1.17",
58
- "@zhin.js/permission": "1.1.15",
59
- "@zhin.js/skill": "1.1.16",
60
- "@zhin.js/tool": "1.1.18"
47
+ "zod": "^4.6.7",
48
+ "@zhin.js/adapter": "1.1.33",
49
+ "@zhin.js/adapter-onebot-events": "0.0.7",
50
+ "@zhin.js/agent-feature": "1.1.17",
51
+ "@zhin.js/console-contract": "1.1.6",
52
+ "@zhin.js/core": "1.6.0",
53
+ "@zhin.js/feature-kit": "1.1.16",
54
+ "@zhin.js/host-http": "1.2.1",
55
+ "@zhin.js/host-http-contract": "1.2.0",
56
+ "@zhin.js/im-contract": "1.1.14",
57
+ "@zhin.js/logger": "1.1.3",
58
+ "@zhin.js/page": "1.1.18",
59
+ "@zhin.js/permission": "1.2.0",
60
+ "@zhin.js/skill": "1.1.17",
61
+ "@zhin.js/tool": "1.1.19"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@types/node": "^26.1.2",
64
65
  "@types/ws": "^8.18.1",
65
66
  "typescript": "^6.0.3",
66
67
  "vitest": "^4.1.10",
67
- "zod": "^4.4.3",
68
- "@zhin.js/host-http": "1.1.17",
69
- "zhin.js": "1.1.25"
68
+ "@zhin.js/host-http": "1.2.1",
69
+ "zhin.js": "1.1.27"
70
70
  },
71
71
  "peerDependencies": {
72
- "zod": "^4.0.0",
73
- "@zhin.js/adapter": "^1.1.32",
74
- "@zhin.js/command": "^1.1.17",
75
- "@zhin.js/core": "^1.1.60",
76
- "zhin.js": "^1.1.25"
72
+ "@zhin.js/adapter": "^1.1.33",
73
+ "@zhin.js/command": "^1.2.1",
74
+ "@zhin.js/core": "^1.6.0",
75
+ "zhin.js": "^1.1.27"
77
76
  },
78
77
  "peerDependenciesMeta": {
79
78
  "@zhin.js/command": {
@@ -81,9 +80,6 @@
81
80
  },
82
81
  "zhin.js": {
83
82
  "optional": true
84
- },
85
- "zod": {
86
- "optional": true
87
83
  }
88
84
  },
89
85
  "author": {
@@ -135,6 +131,6 @@
135
131
  "scripts": {
136
132
  "build": "tsc",
137
133
  "clean": "rimraf lib",
138
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/napcat/tests"
134
+ "test": "vitest run --root ../../.. plugins/adapters/napcat/tests"
139
135
  }
140
136
  }