@zhin.js/adapter-napcat 1.1.0 → 1.1.3

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 (108) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +11 -8
  3. package/adapters/{napcat.js → napcat/index.js} +10 -10
  4. package/adapters/{napcat.ts → napcat/index.ts} +13 -13
  5. package/agents/napcat/agent.json +20 -0
  6. package/agents/napcat/boundaries.md +3 -0
  7. package/agents/napcat/conventions.md +3 -0
  8. package/agents/napcat/skills/napcat-account/SKILL.md +25 -0
  9. package/agents/napcat/skills/napcat-account/tools/delete_friend/index.js +18 -0
  10. package/{agent/tools/delete_friend.ts → agents/napcat/skills/napcat-account/tools/delete_friend/index.ts} +1 -1
  11. package/agents/napcat/skills/napcat-account/tools/get_user_status/index.js +16 -0
  12. package/{agent/tools/get_user_status.ts → agents/napcat/skills/napcat-account/tools/get_user_status/index.ts} +1 -1
  13. package/agents/napcat/skills/napcat-account/tools/send_like/index.js +18 -0
  14. package/{agent/tools/send_like.ts → agents/napcat/skills/napcat-account/tools/send_like/index.ts} +1 -1
  15. package/agents/napcat/skills/napcat-account/tools/set_avatar/index.js +17 -0
  16. package/{agent/tools/set_avatar.ts → agents/napcat/skills/napcat-account/tools/set_avatar/index.ts} +1 -1
  17. package/agents/napcat/skills/napcat-account/tools/set_online_status/index.js +18 -0
  18. package/{agent/tools/set_online_status.ts → agents/napcat/skills/napcat-account/tools/set_online_status/index.ts} +1 -1
  19. package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +21 -0
  20. package/{agent/tools/set_profile.ts → agents/napcat/skills/napcat-account/tools/set_profile/index.ts} +1 -1
  21. package/agents/napcat/skills/napcat-account/tools/set_signature/index.js +17 -0
  22. package/{agent/tools/set_signature.ts → agents/napcat/skills/napcat-account/tools/set_signature/index.ts} +1 -1
  23. package/agents/napcat/skills/napcat-files-history/SKILL.md +22 -0
  24. package/agents/napcat/skills/napcat-files-history/tools/download_file/index.js +17 -0
  25. package/{agent/tools/download_file.ts → agents/napcat/skills/napcat-files-history/tools/download_file/index.ts} +1 -1
  26. package/agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.js +18 -0
  27. package/{agent/tools/get_friend_msg_history.ts → agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.ts} +1 -1
  28. package/agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.js +19 -0
  29. package/{agent/tools/get_group_file_url.ts → agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.ts} +1 -1
  30. package/agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.js +19 -0
  31. package/{agent/tools/get_group_msg_history.ts → agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.ts} +1 -1
  32. package/agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.js +17 -0
  33. package/{agent/tools/get_group_root_files.ts → agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.ts} +1 -1
  34. package/agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.js +21 -0
  35. package/{agent/tools/upload_group_file.ts → agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.ts} +1 -1
  36. package/agents/napcat/skills/napcat-group-content/SKILL.md +21 -0
  37. package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.js +20 -0
  38. package/{agent/tools/del_group_notice.ts → agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.ts} +1 -1
  39. package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.js +19 -0
  40. package/{agent/tools/delete_essence_msg.ts → agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.ts} +1 -1
  41. package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.js +17 -0
  42. package/{agent/tools/get_essence_list.ts → agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.ts} +1 -1
  43. package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.js +17 -0
  44. package/{agent/tools/get_group_notice.ts → agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.ts} +1 -1
  45. package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.js +21 -0
  46. package/{agent/tools/send_group_notice.ts → agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.ts} +1 -1
  47. package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.js +19 -0
  48. package/{agent/tools/set_essence_msg.ts → agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.ts} +1 -1
  49. package/agents/napcat/skills/napcat-group-settings/SKILL.md +23 -0
  50. package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.js +17 -0
  51. package/{agent/tools/get_group_info_ex.ts → agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.ts} +1 -1
  52. package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.js +17 -0
  53. package/{agent/tools/get_group_shut_list.ts → agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.ts} +1 -1
  54. package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.js +18 -0
  55. package/{agent/tools/group_sign.ts → agents/napcat/skills/napcat-group-settings/tools/group_sign/index.ts} +1 -1
  56. package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.js +20 -0
  57. package/{agent/tools/set_group_portrait.ts → agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.ts} +1 -1
  58. package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +21 -0
  59. package/{agent/tools/set_title.ts → agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts} +1 -1
  60. package/agents/napcat/skills/napcat-media/SKILL.md +23 -0
  61. package/agents/napcat/skills/napcat-media/tools/ai_tts/index.js +19 -0
  62. package/{agent/tools/ai_tts.ts → agents/napcat/skills/napcat-media/tools/ai_tts/index.ts} +1 -1
  63. package/agents/napcat/skills/napcat-media/tools/get_ai_characters/index.js +17 -0
  64. package/{agent/tools/get_ai_characters.ts → agents/napcat/skills/napcat-media/tools/get_ai_characters/index.ts} +1 -1
  65. package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +20 -0
  66. package/{agent/tools/get_mini_app_ark.ts → agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts} +1 -1
  67. package/agents/napcat/skills/napcat-media/tools/ocr_image/index.js +16 -0
  68. package/{agent/tools/ocr_image.ts → agents/napcat/skills/napcat-media/tools/ocr_image/index.ts} +1 -1
  69. package/agents/napcat/skills/napcat-media/tools/translate/index.js +16 -0
  70. package/{agent/tools/translate.ts → agents/napcat/skills/napcat-media/tools/translate/index.ts} +1 -1
  71. package/agents/napcat/skills/napcat-messaging/SKILL.md +22 -0
  72. package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.js +22 -0
  73. package/{agent/tools/forward_single_msg.ts → agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.ts} +1 -1
  74. package/agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.js +17 -0
  75. package/{agent/tools/mark_msg_as_read.ts → agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.ts} +1 -1
  76. package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.js +19 -0
  77. package/{agent/tools/send_forward_msg.ts → agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.ts} +1 -1
  78. package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.js +18 -0
  79. package/{agent/tools/send_poke.ts → agents/napcat/skills/napcat-messaging/tools/send_poke/index.ts} +1 -1
  80. package/agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.js +18 -0
  81. package/{agent/tools/set_emoji_reaction.ts → agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.ts} +1 -1
  82. package/agents/napcat/system.md +3 -0
  83. package/commands/{endpoint/add/[id].js → napcat/endpoint/add/[id]/index.js} +1 -1
  84. package/commands/napcat/endpoint/add/[id]/index.ts +3 -0
  85. package/{lib/napcat-endpoint-commands.js → commands/napcat/endpoint/definition.js} +4 -3
  86. package/{src/napcat-endpoint-commands.ts → commands/napcat/endpoint/definition.ts} +3 -3
  87. package/commands/{endpoint/list.js → napcat/endpoint/list/index.js} +1 -1
  88. package/commands/napcat/endpoint/list/index.ts +3 -0
  89. package/commands/{endpoint/remove/[id].js → napcat/endpoint/remove/[id]/index.js} +1 -1
  90. package/commands/napcat/endpoint/remove/[id]/index.ts +3 -0
  91. package/lib/index.d.ts +1 -1
  92. package/lib/napcat-runtime-state.js +1 -1
  93. package/lib/protocol.d.ts +5 -23
  94. package/lib/protocol.js +40 -27
  95. package/lib/wss-endpoint.d.ts +3 -0
  96. package/lib/wss-endpoint.js +51 -42
  97. package/package.json +33 -21
  98. package/schema.json +22 -1
  99. package/src/index.ts +0 -1
  100. package/src/napcat-runtime-state.ts +1 -1
  101. package/src/protocol.ts +47 -50
  102. package/src/wss-endpoint.ts +48 -43
  103. package/agent/skills/napcat.md +0 -311
  104. package/commands/endpoint/add/[id].ts +0 -3
  105. package/commands/endpoint/list.ts +0 -3
  106. package/commands/endpoint/remove/[id].ts +0 -3
  107. package/lib/napcat-endpoint-commands.d.ts +0 -1
  108. /package/{agent/PERMITS.md → PERMITS.md} +0 -0
package/src/index.ts CHANGED
@@ -15,7 +15,6 @@ export {
15
15
  type MessageSegment,
16
16
  type NapCatActionRequest,
17
17
  type NapCatActionResponse,
18
- type NapCatAdapterConfig,
19
18
  type NapCatConfigBase,
20
19
  type NapCatEndpointConfig,
21
20
  type NapCatEvent,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NapCat 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `napcat.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `napcat endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -3,31 +3,15 @@
3
3
  * No legacy Adapter/Endpoint / segment-mapper.
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  */
6
- import { isMediaRef, type MediaRef } from '@zhin.js/core';
7
- import type { ConversationRef } from '@zhin.js/im-contract';
6
+ import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
8
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
8
 
10
9
  const logger = getLogger('napcat');
11
10
 
12
- /** Transitional legacy endpoint row (`endpoints[]` with `context: napcat`). */
13
- export interface NapCatLegacyEndpointRow {
14
- readonly context?: string;
11
+ /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
12
+ export interface NapCatEndpointConfig {
15
13
  readonly connection?: 'ws' | 'wss' | 'http';
16
- readonly id?: string;
17
- readonly access_token?: string;
18
- readonly url?: string;
19
- readonly path?: string;
20
- readonly http_url?: string;
21
- readonly post_path?: string;
22
- readonly reconnect_interval?: number;
23
- readonly heartbeat_interval?: number;
24
- readonly poll_interval?: number;
25
- }
26
-
27
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
28
- export interface NapCatAdapterConfig {
29
- readonly connection?: 'ws' | 'wss' | 'http';
30
- readonly id?: string;
14
+ readonly id: string;
31
15
  readonly access_token?: string;
32
16
  readonly url?: string;
33
17
  readonly path?: string;
@@ -36,8 +20,6 @@ export interface NapCatAdapterConfig {
36
20
  readonly reconnect_interval?: number;
37
21
  readonly heartbeat_interval?: number;
38
22
  readonly poll_interval?: number;
39
- /** Transitional: legacy root `endpoints[]` with `context: napcat`. */
40
- readonly endpoints?: ReadonlyArray<NapCatLegacyEndpointRow>;
41
23
  }
42
24
 
43
25
  export interface NapCatConfigBase {
@@ -70,7 +52,6 @@ export interface NapCatHttpConfig extends NapCatConfigBase {
70
52
  }
71
53
 
72
54
  export type ResolvedNapCatConfig = NapCatWsConfig | NapCatWssConfig | NapCatHttpConfig;
73
- export type NapCatEndpointConfig = ResolvedNapCatConfig;
74
55
 
75
56
  export interface NapCatSender {
76
57
  readonly role?: string;
@@ -140,52 +121,39 @@ function normalizeConnection(
140
121
  return 'ws';
141
122
  }
142
123
 
143
- export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedNapCatConfig {
144
- const entry = config.endpoints?.find((item) => item.context === 'napcat');
145
- const connection = normalizeConnection(config.connection ?? entry?.connection);
146
- const id = (typeof config.id === 'string' && config.id)
147
- || (typeof entry?.id === 'string' && entry.id)
148
- || process.env.NAPCAT_BOT_NAME
149
- || 'napcat-bot';
150
- const access_token = config.access_token ?? entry?.access_token;
124
+ export function resolveNapCatConfig(config: NapCatEndpointConfig): ResolvedNapCatConfig {
125
+ const connection = normalizeConnection(config.connection);
126
+ const id = requiredEndpointField(config.id, 'id');
127
+ const access_token = optionalEndpointField(config.access_token);
151
128
 
152
129
  if (connection === 'ws') {
153
- const url = config.url ?? entry?.url;
154
- if (!url) {
155
- throw new TypeError(
156
- 'NapCat connection:ws requires url (plugins.<key>.url or endpoints with context: napcat)',
157
- );
158
- }
130
+ const url = requiredEndpointField(config.url, 'url');
159
131
  return {
160
132
  context: 'napcat',
161
133
  connection: 'ws',
162
134
  id,
163
135
  access_token,
164
136
  url,
165
- reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
166
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
137
+ reconnect_interval: config.reconnect_interval ?? 5000,
138
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
167
139
  };
168
140
  }
169
141
 
170
142
  if (connection === 'wss') {
171
- const path = config.path ?? entry?.path;
172
- if (!path) throw new TypeError('NapCat connection:wss requires path');
143
+ const path = requiredEndpointField(config.path, 'path');
173
144
  return {
174
145
  context: 'napcat',
175
146
  connection: 'wss',
176
147
  id,
177
148
  access_token,
178
149
  path,
179
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
150
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
180
151
  };
181
152
  }
182
153
 
183
154
  if (connection === 'http') {
184
- const http_url = config.http_url ?? entry?.http_url;
185
- const post_path = config.post_path ?? entry?.post_path;
186
- if (!http_url || !post_path) {
187
- throw new TypeError('NapCat connection:http requires http_url and post_path');
188
- }
155
+ const http_url = requiredEndpointField(config.http_url, 'http_url');
156
+ const post_path = requiredEndpointField(config.post_path, 'post_path');
189
157
  return {
190
158
  context: 'napcat',
191
159
  connection: 'http',
@@ -193,13 +161,28 @@ export function resolveNapCatConfig(config: NapCatAdapterConfig = {}): ResolvedN
193
161
  access_token,
194
162
  http_url,
195
163
  post_path,
196
- poll_interval: config.poll_interval ?? entry?.poll_interval ?? 30_000,
164
+ poll_interval: config.poll_interval ?? 30_000,
197
165
  };
198
166
  }
199
167
 
200
168
  throw new TypeError(`Unknown NapCat connection: ${String(connection)}`);
201
169
  }
202
170
 
171
+ function requiredEndpointField(
172
+ value: unknown,
173
+ field: 'id' | 'url' | 'path' | 'http_url' | 'post_path',
174
+ ): string {
175
+ const resolved = optionalEndpointField(value);
176
+ if (!resolved) {
177
+ throw new TypeError(`NapCat endpoint requires a non-empty ${field}`);
178
+ }
179
+ return resolved;
180
+ }
181
+
182
+ function optionalEndpointField(value: unknown): string | undefined {
183
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
184
+ }
185
+
203
186
  export function isMessageEvent(
204
187
  ev: NapCatEvent,
205
188
  ): ev is NapCatMessageEvent {
@@ -249,12 +232,26 @@ export function formatInboundContent(ev: NapCatEvent): string {
249
232
  .map((seg) => (seg.type === 'text' ? String(seg.data?.text ?? '') : ''))
250
233
  .join('');
251
234
  }
252
- if (typeof ev.message === 'string') return ev.message.replace(/\[CQ:[^\]]+\]/g, '').trim();
235
+ if (typeof ev.message === 'string') return stripCqCodes(ev.message).trim();
253
236
  return typeof ev.raw_message === 'string'
254
- ? ev.raw_message.replace(/\[CQ:[^\]]+\]/g, '').trim()
237
+ ? stripCqCodes(ev.raw_message).trim()
255
238
  : '';
256
239
  }
257
240
 
241
+ function stripCqCodes(input: string): string {
242
+ let result = '';
243
+ let cursor = 0;
244
+ while (cursor < input.length) {
245
+ const start = input.indexOf('[CQ:', cursor);
246
+ if (start < 0) return result + input.slice(cursor);
247
+ result += input.slice(cursor, start);
248
+ const end = input.indexOf(']', start + 4);
249
+ if (end < 0) return result + input.slice(start);
250
+ cursor = end + 1;
251
+ }
252
+ return result;
253
+ }
254
+
258
255
  /**
259
256
  * 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
260
257
  * 显示名走 metadata.nickname,见 senderNickname。
@@ -2,9 +2,10 @@ import { Endpoint } from 'zhin.js/adapter';
2
2
  /**
3
3
  * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
4
4
  */
5
- import { clearInterval } from 'node:timers';
6
5
  import {
6
+ createEndpointLifecycle,
7
7
  createRecallEndpointControl,
8
+ type EndpointLifecycle,
8
9
  type EndpointControl,
9
10
  type EndpointManagement,
10
11
  type EndpointSendRequest,
@@ -38,7 +39,6 @@ import {
38
39
  callNapCatWsAction,
39
40
  handleNapCatWsMessage,
40
41
  rejectAllPending,
41
- startNapCatHeartbeat,
42
42
  } from './ws-transport.js';
43
43
  import { NapCatWsEndpoint } from './ws-endpoint.js';
44
44
  import {
@@ -62,9 +62,10 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
62
62
  readonly management: EndpointManagement = createNapCatEndpointManagement(this.client);
63
63
  readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
64
64
  readonly content = createNapCatContentPort((action, params) => this.client.callApi(action, params));
65
+ readonly #connectionLifecycle: EndpointLifecycle;
65
66
  #ws?: NapCatWsSocket;
66
67
  #wsRelease?: () => void;
67
- #heartbeatTimer?: NodeJS.Timeout;
68
+ #connectionTask = Promise.resolve();
68
69
  #requestId = { value: 0 };
69
70
  #pending = new Map<string, NapCatPendingAction>();
70
71
  #open = false;
@@ -74,6 +75,10 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
74
75
  super();
75
76
  this.#logger = getAdapterLogger('napcat', options.config.id);
76
77
  this.#options = options;
78
+ this.#connectionLifecycle = createEndpointLifecycle({
79
+ name: `${options.config.id}:inbound`,
80
+ reconnect: false,
81
+ });
77
82
  }
78
83
 
79
84
  async start(): Promise<void> {
@@ -81,7 +86,15 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
81
86
  this.#started = true;
82
87
  const handle = this.#options.http.ws(this.#options.config.path);
83
88
  this.#wsRelease = handle.onConnection((connection) => {
84
- this.#acceptConnection(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
+ });
85
98
  });
86
99
  this.#logger.info(formatCompact({
87
100
  op: 'listen',
@@ -103,20 +116,11 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
103
116
  this.#open = false;
104
117
  this.#wsRelease?.();
105
118
  this.#wsRelease = undefined;
106
- if (this.#heartbeatTimer) {
107
- clearInterval(this.#heartbeatTimer);
108
- this.#heartbeatTimer = undefined;
109
- }
119
+ await this.#connectionTask;
120
+ await this.#connectionLifecycle.stop();
110
121
  rejectAllPending(this.#pending);
111
122
  this.#inboundDeduper.clear();
112
- if (this.#ws) {
113
- try {
114
- this.#ws.close();
115
- } catch {
116
- /* ignore */
117
- }
118
- this.#ws = undefined;
119
- }
123
+ this.#ws = undefined;
120
124
  this.#started = false;
121
125
  }
122
126
 
@@ -192,40 +196,41 @@ export class NapCatWssEndpoint extends Endpoint<NapcatClient> {
192
196
  });
193
197
  }
194
198
 
195
- #acceptConnection(connection: WsConnection): void {
199
+ async #acceptConnection(connection: WsConnection): Promise<void> {
196
200
  if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
197
201
  connection.socket.close(4003, 'Unauthorized');
198
202
  return;
199
203
  }
200
204
  const socket = connection.socket as unknown as NapCatWsSocket;
201
- if (this.#ws) {
202
- try {
203
- this.#ws.close();
204
- } catch {
205
- /* ignore */
206
- }
207
- }
208
- this.#ws = socket;
209
- this.#heartbeatTimer = startNapCatHeartbeat(
210
- this.#ws,
211
- this.#options.config.heartbeat_interval,
212
- this.#heartbeatTimer,
213
- );
214
- socket.on('message', (data) => {
215
- handleNapCatWsMessage(data, {
216
- endpointId: this.#options.config.id,
217
- pending: this.#pending,
218
- admit: (event) => this.admit(event),
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
+ }
219
214
  });
220
- });
221
- socket.on('close', () => {
222
- if (this.#ws === socket) {
223
- this.#ws = undefined;
224
- if (this.#heartbeatTimer) {
225
- clearInterval(this.#heartbeatTimer);
226
- this.#heartbeatTimer = undefined;
215
+ this.#connectionLifecycle.startHeartbeat(() => {
216
+ try {
217
+ socket.ping?.();
218
+ } catch {
219
+ /* ignore */
227
220
  }
228
- }
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
+ });
229
234
  });
230
235
  this.#logger.debug(formatCompact({
231
236
  endpoint: this.#options.config.id,
@@ -1,311 +0,0 @@
1
- ---
2
- name: napcat
3
- platforms:
4
- - napcat
5
- description: >-
6
- NapCatQQ 适配器完整能力集。当用户在 QQ 群或私聊中请求群管理(踢人、禁言、设管理、改名片、
7
- 设头衔)、社交互动(戳一戳、点赞、表情回应)、消息操作(合并转发、单条转发、
8
- 消息历史回溯)、群运营(精华消息、群公告、群签到、群文件管理)、多媒体能力(AI 语音
9
- TTS、图片 OCR 文字识别、小程序卡片)、个人设置(修改资料/头像/签名/在线状态)、
10
- 或任何 QQ 特有功能时,都应使用此技能。即使用户没有明确提到 NapCat 或 OneBot,
11
- 只要上下文是 QQ 群聊/私聊场景且涉及上述能力,就应触发。覆盖 OneBot11 标准 +
12
- go-cqhttp 扩展 + NapCat 独有 API 共 92 个接口。
13
- keywords:
14
- - napcat
15
- - napneko
16
- - onebot11
17
- - adapter:napcat
18
- - qq
19
- - 群管理
20
- - 头衔
21
- - 戳一戳
22
- - 表情回应
23
- - 合并转发
24
- - 群文件
25
- - 精华消息
26
- - 群公告
27
- - AI语音
28
- - OCR
29
- - 小程序
30
- - list_members
31
- - 禁言
32
- - 踢人
33
- - 点赞
34
- - 签到
35
- - 转发
36
- - 语音
37
- - 翻译
38
- - 头像
39
- - 在线状态
40
- tags:
41
- - group
42
- - management
43
- - im
44
- - qq
45
- - napcat
46
- tools:
47
- - napcat_kick_member
48
- - napcat_mute_member
49
- - napcat_mute_all
50
- - napcat_set_admin
51
- - napcat_set_nickname
52
- - napcat_set_group_name
53
- - napcat_list_members
54
- - napcat_get_group_info
55
- - napcat_send_poke
56
- - napcat_set_emoji_reaction
57
- - napcat_send_forward_msg
58
- - napcat_forward_single_msg
59
- - napcat_send_like
60
- - napcat_set_essence_msg
61
- - napcat_delete_essence_msg
62
- - napcat_get_essence_list
63
- - napcat_send_group_notice
64
- - napcat_get_group_notice
65
- - napcat_del_group_notice
66
- - napcat_upload_group_file
67
- - napcat_get_group_file_url
68
- - napcat_get_group_root_files
69
- - napcat_get_group_shut_list
70
- - napcat_set_group_portrait
71
- - napcat_set_title
72
- - napcat_group_sign
73
- - napcat_ai_tts
74
- - napcat_get_ai_characters
75
- - napcat_ocr_image
76
- - napcat_get_mini_app_ark
77
- - napcat_get_group_msg_history
78
- - napcat_get_friend_msg_history
79
- - napcat_get_user_status
80
- - napcat_get_group_info_ex
81
- - napcat_set_profile
82
- - napcat_set_avatar
83
- - napcat_set_online_status
84
- - napcat_set_signature
85
- - napcat_translate
86
- - napcat_mark_msg_as_read
87
- - napcat_download_file
88
- - napcat_delete_friend
89
- ---
90
-
91
- # NapCat QQ 适配器技能
92
-
93
- NapCat 是 OneBot11 协议的超集实现,提供标准群管理之外大量 QQ 特有功能。这份技能帮助你正确选择工具、组合调用、处理边界情况。
94
-
95
- ## 核心原则
96
-
97
- ### 先查后操作
98
-
99
- 用户说「把小明踢了」,但工具需要的是 QQ 号(数字),不是昵称。所有针对具体成员的操作,如果手头只有昵称/名片,第一步永远是 `napcat_list_members` 获取成员列表,从中匹配到 `user_id`,然后再执行后续操作。不要猜测 QQ 号。
100
-
101
- **Example:**
102
- ```
103
- 用户: 把群里那个叫"小飞"的禁言10分钟
104
- 步骤1: napcat_list_members → 找到 小飞 的 user_id = 123456
105
- 步骤2: napcat_mute_member(user_id=123456, duration=600)
106
- ```
107
-
108
- ### 权限意识
109
-
110
- 工具分三个权限层级,调用前先判断 Endpoint 在群里的身份:
111
-
112
- - **user** — 普通成员即可,如查询、点赞、戳一戳
113
- - **scene_admin** — 需要管理员权限,如踢人、禁言、设精华、发公告
114
- - **scene_owner** — 需要群主权限,如设管理员、设专属头衔
115
-
116
- 如果 Endpoint 权限不足,API 会返回错误。遇到权限不足时,向用户说明需要什么权限,而不是重试。
117
-
118
- ### 数值单位
119
-
120
- - **禁言 duration**:单位是秒。600 = 10 分钟,3600 = 1 小时,86400 = 1 天。设为 **0** 是解除禁言,不是永久禁言。
121
- - **message_id**:消息收到时自带的数字 ID,用于回应、精华、转发等操作。
122
- - **group_id / user_id**:QQ 号,纯数字。
123
-
124
- ---
125
-
126
- ## 工具分类与使用指南
127
-
128
- ### 群管理(基础)
129
-
130
- 日常群管理操作。Bot 需要管理员权限(`set_admin` 需群主权限)。
131
-
132
- | 工具 | 用途 | 关键参数 |
133
- |------|------|----------|
134
- | `napcat_kick_member` | 踢出成员 | `user_id` |
135
- | `napcat_mute_member` | 禁言/解禁 | `user_id`, `duration`(秒,0=解禁) |
136
- | `napcat_mute_all` | 全员禁言/解除 | `enable`(true/false) |
137
- | `napcat_set_admin` | 设/取消管理员 | `user_id`, `enable` |
138
- | `napcat_set_nickname` | 改群名片 | `user_id`, `nickname` |
139
- | `napcat_set_group_name` | 改群名称 | `name` |
140
- | `napcat_set_title` | 设专属头衔 | `user_id`, `title`(需群主) |
141
- | `napcat_list_members` | 获取成员列表 | — |
142
- | `napcat_get_group_info` | 获取群基本信息 | — |
143
- | `napcat_get_group_info_ex` | 获取群详细信息 | — |
144
- | `napcat_get_group_shut_list` | 获取当前禁言列表 | — |
145
-
146
- **批量禁言场景:** 用户说「把最近刷屏的人都禁言」时,先用 `napcat_get_group_msg_history` 分析最近消息,识别刷屏用户,然后逐个 `napcat_mute_member`。
147
-
148
- ### 社交互动
149
-
150
- 不需要管理员权限的轻量交互。
151
-
152
- | 工具 | 用途 | 说明 |
153
- |------|------|------|
154
- | `napcat_send_poke` | 戳一戳 | `group_id` 不传 = 私聊戳 |
155
- | `napcat_set_emoji_reaction` | 表情回应 | 对某条消息贴表情,需 `message_id` + `emoji_id` |
156
- | `napcat_send_like` | 给好友点赞 | 每人每天上限 10 次 |
157
- | `napcat_group_sign` | 群签到/打卡 | 每天一次 |
158
-
159
- **Example:**
160
- ```
161
- 用户: 戳一下群里的张三
162
- 步骤1: napcat_list_members → 找到张三 user_id = 789
163
- 步骤2: napcat_send_poke(user_id=789, group_id=当前群号)
164
- ```
165
-
166
- ### 消息操作
167
-
168
- 转发、历史查询是 NapCat 超越标准 OneBot11 的核心能力。
169
-
170
- | 工具 | 用途 | 说明 |
171
- |------|------|------|
172
- | `napcat_send_forward_msg` | 合并转发 | 多条消息打包为一条转发消息 |
173
- | `napcat_forward_single_msg` | 单条转发 | 把某条消息转到另一个群/好友 |
174
- | `napcat_get_group_msg_history` | 群消息历史 | 支持指定起始序号和条数 |
175
- | `napcat_get_friend_msg_history` | 私聊历史 | 同上 |
176
- | `napcat_mark_msg_as_read` | 标记已读 | — |
177
-
178
- **合并转发的 messages 格式:**
179
-
180
- `messages` 参数是 JSON 字符串,每个元素是一个 node 节点:
181
-
182
- ```json
183
- [
184
- {
185
- "type": "node",
186
- "data": {
187
- "name": "发送者昵称",
188
- "uin": "10001",
189
- "content": [{"type": "text", "data": {"text": "消息内容"}}]
190
- }
191
- }
192
- ]
193
- ```
194
-
195
- 也可以用 `id` 引用已有消息:`{"type": "node", "data": {"id": "消息ID"}}`
196
-
197
- ### 群运营
198
-
199
- 精华消息、群公告、群文件、群头像 — 让群看起来正规且有序。
200
-
201
- | 工具 | 用途 | 权限 |
202
- |------|------|------|
203
- | `napcat_set_essence_msg` | 设为精华消息 | admin |
204
- | `napcat_delete_essence_msg` | 取消精华 | admin |
205
- | `napcat_get_essence_list` | 精华消息列表 | user |
206
- | `napcat_send_group_notice` | 发群公告 | admin |
207
- | `napcat_get_group_notice` | 查看公告 | user |
208
- | `napcat_del_group_notice` | 删除公告 | admin |
209
- | `napcat_upload_group_file` | 上传群文件 | user |
210
- | `napcat_get_group_file_url` | 获取文件下载链接 | user |
211
- | `napcat_get_group_root_files` | 群文件列表 | user |
212
- | `napcat_set_group_portrait` | 修改群头像 | admin |
213
-
214
- **群公告带图:** `napcat_send_group_notice` 的 `image` 参数支持 URL 或 base64,方便把生成的图片直接发到公告里。
215
-
216
- **群文件工作流:** 用户说「帮我把这个文件发到群里」→ 用 `napcat_upload_group_file`,`file` 可以是本地路径或 URL。如果用户问「群里那个文件在哪下载」→ 先 `napcat_get_group_root_files` 列出文件,找到 `file_id` 和 `busid`,再 `napcat_get_group_file_url` 获取下载链接。
217
-
218
- ### AI 与多媒体
219
-
220
- NapCat 独有的多媒体处理能力,标准 OneBot11 不支持这些。
221
-
222
- | 工具 | 用途 | 说明 |
223
- |------|------|------|
224
- | `napcat_ai_tts` | AI 文字转语音 | 在群里发语音,需指定角色 |
225
- | `napcat_get_ai_characters` | 语音角色列表 | 配合 ai_tts 使用 |
226
- | `napcat_ocr_image` | 图片文字识别 | 传入图片的 file 字段 |
227
- | `napcat_get_mini_app_ark` | 小程序卡片签名 | 生成可分享的小程序卡片 |
228
- | `napcat_translate` | 英译中翻译 | NapCat 内置翻译 |
229
- | `napcat_download_file` | 下载文件到缓存 | 返回本地路径 |
230
-
231
- **AI 语音工作流:** 用户说「用甜美女声读一段话」→ 先 `napcat_get_ai_characters` 列出可用角色,从中选择匹配的 `character_id`,然后 `napcat_ai_tts(character=所选ID, text=要读的内容)`。角色列表因群不同可能不一样,所以每次都应重新查询。
232
-
233
- **OCR 使用:** 当用户发了一张带文字的图片并问「这上面写了什么」,从消息中提取图片的 `file` 字段(通常是 `file://...` 或一个 hash),传给 `napcat_ocr_image`。
234
-
235
- ### 个人设置
236
-
237
- 修改 Endpoint 自身的 QQ 资料,通常是用户明确要求时才使用。
238
-
239
- | 工具 | 用途 |
240
- |------|------|
241
- | `napcat_set_profile` | 修改昵称、公司、邮箱等 |
242
- | `napcat_set_avatar` | 换头像(URL 或 base64) |
243
- | `napcat_set_online_status` | 设在线状态 |
244
- | `napcat_set_signature` | 设个性签名 |
245
- | `napcat_delete_friend` | 删除好友 |
246
- | `napcat_get_user_status` | 查看某人在线状态 |
247
-
248
- **在线状态码:** 11=在线, 21=离开, 31=隐身, 41=忙碌, 50=请勿打扰, 60=Q我吧。
249
-
250
- ---
251
-
252
- ## 常见场景决策树
253
-
254
- ### 用户想管理群成员
255
-
256
- ```
257
- 用户提供了 QQ 号?
258
- ├─ 是 → 直接操作
259
- └─ 否(只有昵称/名片)
260
- └─ napcat_list_members → 匹配 → 操作
261
- ```
262
-
263
- ### 用户想查看/操作消息历史
264
-
265
- ```
266
- 群聊还是私聊?
267
- ├─ 群聊 → napcat_get_group_msg_history(group_id)
268
- └─ 私聊 → napcat_get_friend_msg_history(user_id)
269
-
270
- 想转发其中某条?
271
- ├─ 转单条 → napcat_forward_single_msg(message_id)
272
- └─ 打包多条 → napcat_send_forward_msg(messages=[node...])
273
- ```
274
-
275
- ### 用户想发语音
276
-
277
- ```
278
- napcat_get_ai_characters(group_id) → 列出角色
279
- 选择最匹配的 character_id
280
- napcat_ai_tts(group_id, character, text)
281
- ```
282
-
283
- ### 用户想发公告/管理精华
284
-
285
- ```
286
- 发新公告 → napcat_send_group_notice(content, image?)
287
- 查看公告 → napcat_get_group_notice
288
- 删除公告 → 先 get 拿到 notice_id → napcat_del_group_notice
289
-
290
- 设精华 → napcat_set_essence_msg(message_id)
291
- 取消精华 → napcat_delete_essence_msg(message_id)
292
- 看精华列表 → napcat_get_essence_list
293
- ```
294
-
295
- ---
296
-
297
- ## 易错点
298
-
299
- 1. **禁言 duration=0 是解禁**,不是永久禁言。没有「永久禁言」的概念,最长可以设很大的数(如 2592000 = 30 天)。
300
-
301
- 2. **合并转发的 messages 必须是 JSON 字符串**,不是对象。工具内部会解析,但传参时要传字符串化的 JSON。
302
-
303
- 3. **头衔(set_title)只有群主能设**,管理员不行。用户说「给他加个头衔」但 Endpoint 不是群主时,需告知无法操作。
304
-
305
- 4. **点赞有每日上限**(每人 10 次),用户说「给他点 100 个赞」时要说明限制。
306
-
307
- 5. **AI 语音只能在群聊用**,私聊不支持。如果用户在私聊请求 AI 语音,建议改用群聊。
308
-
309
- 6. **群文件操作需要 file_id 和 busid 两个参数**,它们来自 `get_group_root_files` 的返回,不要编造。
310
-
311
- 7. **OCR 的 image 参数**不是图片 URL,而是消息中图片的 `file` 字段值(形如 `xxxxx.image` 或本地路径)。
@@ -1,3 +0,0 @@
1
- import { napcatEndpointCommands } from '../../../src/napcat-endpoint-commands.js';
2
-
3
- export default napcatEndpointCommands.add;
@@ -1,3 +0,0 @@
1
- import { napcatEndpointCommands } from '../../src/napcat-endpoint-commands.js';
2
-
3
- export default napcatEndpointCommands.list;
@@ -1,3 +0,0 @@
1
- import { napcatEndpointCommands } from '../../../src/napcat-endpoint-commands.js';
2
-
3
- export default napcatEndpointCommands.remove;
@@ -1 +0,0 @@
1
- export declare const napcatEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
File without changes