@zhin.js/adapter-napcat 6.0.2 → 6.0.5

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.
package/src/protocol.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
6
  import { isMediaRef, type MediaRef } from '@zhin.js/core';
7
+ import type { ConversationRef } from '@zhin.js/im-contract';
7
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
8
9
 
9
10
  const logger = getLogger('napcat');
@@ -12,7 +13,7 @@ const logger = getLogger('napcat');
12
13
  export interface NapCatLegacyEndpointRow {
13
14
  readonly context?: string;
14
15
  readonly connection?: 'ws' | 'wss' | 'http';
15
- readonly name?: string;
16
+ readonly id?: string;
16
17
  readonly access_token?: string;
17
18
  readonly url?: string;
18
19
  readonly path?: string;
@@ -26,7 +27,7 @@ export interface NapCatLegacyEndpointRow {
26
27
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
27
28
  export interface NapCatAdapterConfig {
28
29
  readonly connection?: 'ws' | 'wss' | 'http';
29
- readonly name?: string;
30
+ readonly id?: string;
30
31
  readonly access_token?: string;
31
32
  readonly url?: string;
32
33
  readonly path?: string;
@@ -41,7 +42,7 @@ export interface NapCatAdapterConfig {
41
42
 
42
43
  export interface NapCatConfigBase {
43
44
  readonly context: 'napcat';
44
- readonly name: string;
45
+ readonly id: string;
45
46
  readonly access_token?: string;
46
47
  }
47
48
 
@@ -142,8 +143,8 @@ function normalizeConnection(
142
143
  export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedNapCatConfig {
143
144
  const entry = config.endpoints?.find((item) => item.context === 'napcat');
144
145
  const connection = normalizeConnection(config.connection ?? entry?.connection);
145
- const name = (typeof config.name === 'string' && config.name)
146
- || (typeof entry?.name === 'string' && entry.name)
146
+ const id = (typeof config.id === 'string' && config.id)
147
+ || (typeof entry?.id === 'string' && entry.id)
147
148
  || process.env.NAPCAT_BOT_NAME
148
149
  || 'napcat-bot';
149
150
  const access_token = config.access_token ?? entry?.access_token;
@@ -158,7 +159,7 @@ export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedN
158
159
  return {
159
160
  context: 'napcat',
160
161
  connection: 'ws',
161
- name,
162
+ id,
162
163
  access_token,
163
164
  url,
164
165
  reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
@@ -172,7 +173,7 @@ export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedN
172
173
  return {
173
174
  context: 'napcat',
174
175
  connection: 'wss',
175
- name,
176
+ id,
176
177
  access_token,
177
178
  path,
178
179
  heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
@@ -188,7 +189,7 @@ export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedN
188
189
  return {
189
190
  context: 'napcat',
190
191
  connection: 'http',
191
- name,
192
+ id,
192
193
  access_token,
193
194
  http_url,
194
195
  post_path,
@@ -213,23 +214,33 @@ export function getChannelId(ev: NapCatEvent): string {
213
214
  return '';
214
215
  }
215
216
 
216
- export function formatInboundTarget(ev: NapCatEvent): string {
217
- const messageType = ev.message_type === 'group' || (ev.group_id != null && ev.message_type !== 'private')
218
- ? 'group'
219
- : 'private';
220
- return `${messageType}:${getChannelId(ev)}`;
217
+ /**
218
+ * 入站归一化 → ConversationRef:群消息 → kind 'group';私聊临时会话
219
+ * (sub_type 'group')→ kind 'private' + 群容器进 `parent`;其余私聊 → 'private'。
220
+ */
221
+ export function napcatInboundConversation(endpointKey: string, ev: NapCatEvent): ConversationRef {
222
+ const endpoint = { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey };
223
+ const isGroup = ev.message_type === 'group' || (ev.group_id != null && ev.message_type !== 'private');
224
+ if (isGroup && ev.group_id != null) {
225
+ return { endpoint, kind: 'group', id: String(ev.group_id) };
226
+ }
227
+ if (ev.sub_type === 'group' && ev.group_id != null) {
228
+ return {
229
+ endpoint,
230
+ kind: 'private',
231
+ id: ev.user_id != null ? String(ev.user_id) : '',
232
+ parent: { kind: 'group', id: String(ev.group_id) },
233
+ };
234
+ }
235
+ return { endpoint, kind: 'private', id: ev.user_id != null ? String(ev.user_id) : '' };
221
236
  }
222
237
 
223
- export function parseSendTarget(target: string): ParsedSendTarget {
224
- const sep = target.indexOf(':');
225
- if (sep <= 0) {
226
- return { message_type: 'private', id: target };
227
- }
228
- const head = target.slice(0, sep);
229
- const rest = target.slice(sep + 1);
230
- if (head === 'group') return { message_type: 'group', id: rest };
231
- if (head === 'private') return { message_type: 'private', id: rest };
232
- return { message_type: 'private', id: target };
238
+ /** 出站:ConversationRef → OneBot 私聊/群聊目标。 */
239
+ export function napcatOutboundTarget(conversation: ConversationRef): ParsedSendTarget {
240
+ return {
241
+ message_type: conversation.kind === 'group' ? 'group' : 'private',
242
+ id: conversation.id,
243
+ };
233
244
  }
234
245
 
235
246
  export function formatInboundContent(ev: NapCatEvent): string {
@@ -373,15 +384,14 @@ export function formatOutboundSegments(payload: unknown): MessageSegment[] {
373
384
  }
374
385
 
375
386
  export function buildSendAction(
376
- target: string,
387
+ target: ParsedSendTarget,
377
388
  message: MessageSegment[],
378
389
  ): { action: string; params: Record<string, unknown> } {
379
- const parsed = parseSendTarget(target);
380
- if (parsed.message_type === 'group') {
390
+ if (target.message_type === 'group') {
381
391
  return {
382
392
  action: 'send_group_msg',
383
393
  params: {
384
- group_id: Number(parsed.id) || parsed.id,
394
+ group_id: Number(target.id) || target.id,
385
395
  message,
386
396
  },
387
397
  };
@@ -389,7 +399,7 @@ export function buildSendAction(
389
399
  return {
390
400
  action: 'send_private_msg',
391
401
  params: {
392
- user_id: Number(parsed.id) || parsed.id,
402
+ user_id: Number(target.id) || target.id,
393
403
  message,
394
404
  },
395
405
  };
@@ -8,9 +8,10 @@ import {
8
8
  type EndpointInstance,
9
9
  type EndpointLifecycle,
10
10
  type EndpointManagement,
11
+ type EndpointSendRequest,
11
12
  } from '@zhin.js/adapter';
12
13
  import type { MessageGateway } from '@zhin.js/core/runtime';
13
- import { formatCompact, getLogger } from '@zhin.js/logger';
14
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
14
15
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
15
16
  import { createNapCatEndpointManagement } from './endpoint-management.js';
16
17
  import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
@@ -24,9 +25,10 @@ import {
24
25
  buildSendAction,
25
26
  buildWsConnectOptions,
26
27
  formatInboundContent,
27
- formatInboundTarget,
28
28
  formatOutboundSegments,
29
29
  isMessageEvent,
30
+ napcatInboundConversation,
31
+ napcatOutboundTarget,
30
32
  senderNickname,
31
33
  senderUserId,
32
34
  type NapCatEvent,
@@ -43,8 +45,6 @@ import {
43
45
  type NapCatWsSocket,
44
46
  } from './ws-types.js';
45
47
 
46
- const logger = getLogger('napcat');
47
-
48
48
  export interface NapCatWsEndpointOptions {
49
49
  readonly id: CapabilityId;
50
50
  readonly gateway: MessageGateway;
@@ -56,6 +56,8 @@ export interface NapCatWsEndpointOptions {
56
56
  }
57
57
 
58
58
  export class NapCatWsEndpoint implements EndpointInstance {
59
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
60
+
59
61
  readonly #options: NapCatWsEndpointOptions;
60
62
  readonly #inboundDeduper = new InboundMessageDeduper();
61
63
  readonly management: EndpointManagement = createNapCatEndpointManagement(this);
@@ -67,9 +69,10 @@ export class NapCatWsEndpoint implements EndpointInstance {
67
69
  #unregisterAgent?: () => void;
68
70
 
69
71
  constructor(options: NapCatWsEndpointOptions) {
72
+ this.#logger = getAdapterLogger('napcat', options.config.id);
70
73
  this.#options = options;
71
74
  this.#lifecycle = createEndpointLifecycle({
72
- name: options.config.name,
75
+ name: options.config.id,
73
76
  // reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
74
77
  reconnect: {
75
78
  initialIntervalMs: options.config.reconnect_interval,
@@ -82,7 +85,7 @@ export class NapCatWsEndpoint implements EndpointInstance {
82
85
 
83
86
  async start(): Promise<void> {
84
87
  if (this.#lifecycle.started) return;
85
- this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
88
+ this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.id, this);
86
89
  try {
87
90
  await this.#lifecycle.start((handle) => this.#connect(handle));
88
91
  } catch (err) {
@@ -118,15 +121,15 @@ export class NapCatWsEndpoint implements EndpointInstance {
118
121
  }
119
122
  }
120
123
 
121
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
124
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
122
125
  const message = formatOutboundSegments(payload);
123
- const { action, params } = buildSendAction(target, message);
126
+ const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
124
127
  const data = await this.callApi(action, params) as { message_id?: number | string } | undefined;
125
128
  const messageId = data?.message_id != null ? String(data.message_id) : '';
126
- logger.debug(formatCompact({
129
+ this.#logger.debug(formatCompact({
127
130
  op: 'napcat_send',
128
- endpoint: this.#options.config.name,
129
- target,
131
+ endpoint: this.#options.config.id,
132
+ target: `${conversation.kind}:${conversation.id}`,
130
133
  messageId,
131
134
  }));
132
135
  return messageId;
@@ -326,31 +329,34 @@ export class NapCatWsEndpoint implements EndpointInstance {
326
329
  if (Array.isArray(ev.message) || typeof ev.message === 'string') {
327
330
  ev = { ...ev, message: normalizeMessage(ev.message) };
328
331
  }
329
- const target = formatInboundTarget(ev);
332
+ const conversation = napcatInboundConversation(String(this.#options.id), ev);
330
333
  const content = formatInboundContent(ev);
331
334
  const nickname = senderNickname(ev);
332
335
  const mentioned = isNapCatBotMentioned(ev);
333
336
  void this.#options.gateway.receive({
334
- adapter: this.#options.id,
335
- target,
337
+ conversation,
338
+ message: { conversation, id: msgId },
336
339
  content,
337
- sender: senderUserId(ev),
338
- id: msgId,
340
+ sender: {
341
+ id: senderUserId(ev),
342
+ name: nickname,
343
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
344
+ },
345
+ endpointId: this.#options.config.id,
346
+ ...(mentioned ? { mentioned: true } : {}),
339
347
  metadata: Object.freeze({
340
348
  message_type: ev.message_type,
341
349
  user_id: ev.user_id != null ? String(ev.user_id) : undefined,
342
350
  group_id: ev.group_id != null ? String(ev.group_id) : undefined,
343
- endpoint: this.#options.config.name,
344
351
  time: ev.time,
345
352
  self_id: ev.self_id != null ? String(ev.self_id) : undefined,
346
353
  role: ev.sender?.role,
347
354
  ...(nickname ? { nickname } : {}),
348
- ...(mentioned ? { mentioned: true } : {}),
349
355
  }),
350
356
  }).catch((err) => {
351
- logger.warn(formatCompact({
357
+ this.#logger.warn(formatCompact({
352
358
  op: 'napcat_gateway_receive_failed',
353
- target,
359
+ target: `${conversation.kind}:${conversation.id}`,
354
360
  error: err instanceof Error ? err.message : String(err),
355
361
  }));
356
362
  });
@@ -378,14 +384,14 @@ export class NapCatWsEndpoint implements EndpointInstance {
378
384
  if (settled) return;
379
385
  settled = true;
380
386
  if (!this.#options.config.access_token) {
381
- logger.warn(formatCompact({
382
- endpoint: this.#options.config.name,
387
+ this.#logger.warn(formatCompact({
388
+ endpoint: this.#options.config.id,
383
389
  ok: false,
384
390
  error: 'missing access_token',
385
391
  }));
386
392
  }
387
- logger.debug(formatCompact({
388
- endpoint: this.#options.config.name,
393
+ this.#logger.debug(formatCompact({
394
+ endpoint: this.#options.config.id,
389
395
  mode: 'ws',
390
396
  url: safeUrl,
391
397
  }));
@@ -404,7 +410,7 @@ export class NapCatWsEndpoint implements EndpointInstance {
404
410
 
405
411
  ws.on('message', (data) => {
406
412
  handleNapCatWsMessage(data, {
407
- endpointName: this.#options.config.name,
413
+ endpointId: this.#options.config.id,
408
414
  pending: this.#pending,
409
415
  admit: (event) => this.admit(event),
410
416
  });
@@ -422,9 +428,8 @@ export class NapCatWsEndpoint implements EndpointInstance {
422
428
  : codeNum === 1006
423
429
  ? ' [abnormal]'
424
430
  : '';
425
- logger.warn(formatCompact({
431
+ this.#logger.warn(formatCompact({
426
432
  op: 'disconnect',
427
- endpoint: this.#options.config.name,
428
433
  code: codeNum,
429
434
  error: `${reasonStr || 'closed'}${codeHint}`,
430
435
  reconnect_ms: this.#options.config.reconnect_interval,
@@ -439,9 +444,9 @@ export class NapCatWsEndpoint implements EndpointInstance {
439
444
 
440
445
  ws.on('error', (err) => {
441
446
  const error = err instanceof Error ? err : new Error(String(err));
442
- logger.warn(formatCompact({
447
+ this.#logger.warn(formatCompact({
443
448
  op: 'ws_error',
444
- endpoint: this.#options.config.name,
449
+ endpoint: this.#options.config.id,
445
450
  ok: false,
446
451
  error: error.message,
447
452
  }));
@@ -23,7 +23,7 @@ export function decodeWsPayload(data: unknown): string {
23
23
  export function handleNapCatWsMessage(
24
24
  data: unknown,
25
25
  options: {
26
- readonly endpointName: string;
26
+ readonly endpointId: string;
27
27
  readonly pending: Map<string, NapCatPendingAction>;
28
28
  readonly admit: (ev: NapCatEvent) => void;
29
29
  },
@@ -50,7 +50,7 @@ export function handleNapCatWsMessage(
50
50
  } catch (error) {
51
51
  logger.warn(formatCompact({
52
52
  op: 'napcat_parse_failed',
53
- endpoint: options.endpointName,
53
+ endpoint: options.endpointId,
54
54
  error: error instanceof Error ? error.message : String(error),
55
55
  }));
56
56
  }
@@ -2,10 +2,10 @@
2
2
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
3
3
  */
4
4
  import { clearInterval } from 'node:timers';
5
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
5
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
6
6
  import type { MessageGateway } from '@zhin.js/core/runtime';
7
7
  import type { HttpHost, WsConnection } from '@zhin.js/host-http';
8
- import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
9
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
10
  import { createNapCatEndpointManagement } from './endpoint-management.js';
11
11
  import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
@@ -18,9 +18,10 @@ import {
18
18
  import {
19
19
  buildSendAction,
20
20
  formatInboundContent,
21
- formatInboundTarget,
22
21
  formatOutboundSegments,
23
22
  isMessageEvent,
23
+ napcatInboundConversation,
24
+ napcatOutboundTarget,
24
25
  senderNickname,
25
26
  senderUserId,
26
27
  type NapCatEvent,
@@ -39,8 +40,6 @@ import {
39
40
  } from './ws-types.js';
40
41
  import { verifyNapCatAccessToken } from './wss-auth.js';
41
42
 
42
- const logger = getLogger('napcat');
43
-
44
43
  export interface NapCatWssEndpointOptions {
45
44
  readonly id: CapabilityId;
46
45
  readonly gateway: MessageGateway;
@@ -49,6 +48,8 @@ export interface NapCatWssEndpointOptions {
49
48
  }
50
49
 
51
50
  export class NapCatWssEndpoint implements EndpointInstance {
51
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
52
+
52
53
  readonly #options: NapCatWssEndpointOptions;
53
54
  readonly #inboundDeduper = new InboundMessageDeduper();
54
55
  readonly management: EndpointManagement = createNapCatEndpointManagement(this);
@@ -62,6 +63,7 @@ export class NapCatWssEndpoint implements EndpointInstance {
62
63
  #unregisterAgent?: () => void;
63
64
 
64
65
  constructor(options: NapCatWssEndpointOptions) {
66
+ this.#logger = getAdapterLogger('napcat', options.config.id);
65
67
  this.#options = options;
66
68
  }
67
69
 
@@ -69,16 +71,16 @@ export class NapCatWssEndpoint implements EndpointInstance {
69
71
  if (this.#started) return;
70
72
  this.#started = true;
71
73
  this.#unregisterAgent = registerNapcatAgentEndpoint(
72
- this.#options.config.name,
74
+ this.#options.config.id,
73
75
  this as unknown as NapCatWsEndpoint,
74
76
  );
75
77
  const handle = this.#options.http.ws(this.#options.config.path);
76
78
  this.#wsRelease = handle.onConnection((connection) => {
77
79
  this.#acceptConnection(connection);
78
80
  });
79
- logger.info(formatCompact({
81
+ this.#logger.info(formatCompact({
80
82
  op: 'listen',
81
- endpoint: this.#options.config.name,
83
+ endpoint: this.#options.config.id,
82
84
  mode: 'wss',
83
85
  path: this.#options.config.path,
84
86
  }));
@@ -115,9 +117,9 @@ export class NapCatWssEndpoint implements EndpointInstance {
115
117
  this.#started = false;
116
118
  }
117
119
 
118
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
120
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
119
121
  const message = formatOutboundSegments(payload);
120
- const { action, params } = buildSendAction(target, message);
122
+ const { action, params } = buildSendAction(napcatOutboundTarget(conversation), message);
121
123
  const data = await this.callApi(action, params) as { message_id?: number | string } | undefined;
122
124
  return data?.message_id != null ? String(data.message_id) : '';
123
125
  }
@@ -139,30 +141,33 @@ export class NapCatWssEndpoint implements EndpointInstance {
139
141
  if (Array.isArray(ev.message) || typeof ev.message === 'string') {
140
142
  ev = { ...ev, message: normalizeMessage(ev.message) };
141
143
  }
142
- const target = formatInboundTarget(ev);
144
+ const conversation = napcatInboundConversation(String(this.#options.id), ev);
143
145
  const nickname = senderNickname(ev);
144
146
  const mentioned = isNapCatBotMentioned(ev);
145
147
  void this.#options.gateway.receive({
146
- adapter: this.#options.id,
147
- target,
148
+ conversation,
149
+ message: { conversation, id: msgId },
148
150
  content: formatInboundContent(ev),
149
- sender: senderUserId(ev),
150
- id: msgId,
151
+ sender: {
152
+ id: senderUserId(ev),
153
+ name: nickname,
154
+ ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
155
+ },
156
+ endpointId: this.#options.config.id,
157
+ ...(mentioned ? { mentioned: true } : {}),
151
158
  metadata: Object.freeze({
152
159
  message_type: ev.message_type,
153
160
  user_id: ev.user_id != null ? String(ev.user_id) : undefined,
154
161
  group_id: ev.group_id != null ? String(ev.group_id) : undefined,
155
- endpoint: this.#options.config.name,
156
162
  time: ev.time,
157
163
  self_id: ev.self_id != null ? String(ev.self_id) : undefined,
158
164
  role: ev.sender?.role,
159
165
  ...(nickname ? { nickname } : {}),
160
- ...(mentioned ? { mentioned: true } : {}),
161
166
  }),
162
167
  }).catch((err) => {
163
- logger.warn(formatCompact({
168
+ this.#logger.warn(formatCompact({
164
169
  op: 'napcat_gateway_receive_failed',
165
- target,
170
+ target: `${conversation.kind}:${conversation.id}`,
166
171
  error: err instanceof Error ? err.message : String(err),
167
172
  }));
168
173
  });
@@ -189,7 +194,7 @@ export class NapCatWssEndpoint implements EndpointInstance {
189
194
  );
190
195
  socket.on('message', (data) => {
191
196
  handleNapCatWsMessage(data, {
192
- endpointName: this.#options.config.name,
197
+ endpointId: this.#options.config.id,
193
198
  pending: this.#pending,
194
199
  admit: (event) => this.admit(event),
195
200
  });
@@ -203,8 +208,8 @@ export class NapCatWssEndpoint implements EndpointInstance {
203
208
  }
204
209
  }
205
210
  });
206
- logger.debug(formatCompact({
207
- endpoint: this.#options.config.name,
211
+ this.#logger.debug(formatCompact({
212
+ endpoint: this.#options.config.id,
208
213
  mode: 'wss',
209
214
  peer: connection.request.socket.remoteAddress,
210
215
  }));