@zhin.js/adapter-napcat 3.0.1 → 4.0.1

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 (140) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +43 -96
  3. package/adapters/napcat.ts +39 -0
  4. package/agent/tools/ai_tts.ts +21 -0
  5. package/agent/tools/del_group_notice.ts +22 -0
  6. package/agent/tools/delete_essence_msg.ts +21 -0
  7. package/agent/tools/delete_friend.ts +20 -0
  8. package/agent/tools/download_file.ts +19 -0
  9. package/agent/tools/forward_single_msg.ts +32 -0
  10. package/agent/tools/get_ai_characters.ts +19 -0
  11. package/agent/tools/get_essence_list.ts +19 -0
  12. package/agent/tools/get_friend_msg_history.ts +20 -0
  13. package/agent/tools/get_group_file_url.ts +21 -0
  14. package/agent/tools/get_group_info_ex.ts +19 -0
  15. package/agent/tools/get_group_msg_history.ts +21 -0
  16. package/agent/tools/get_group_notice.ts +19 -0
  17. package/agent/tools/get_group_root_files.ts +19 -0
  18. package/agent/tools/get_group_shut_list.ts +19 -0
  19. package/agent/tools/get_mini_app_ark.ts +22 -0
  20. package/agent/tools/get_user_status.ts +18 -0
  21. package/agent/tools/group_sign.ts +20 -0
  22. package/agent/tools/mark_msg_as_read.ts +19 -0
  23. package/agent/tools/ocr_image.ts +18 -0
  24. package/agent/tools/send_forward_msg.ts +31 -0
  25. package/agent/tools/send_group_notice.ts +23 -0
  26. package/agent/tools/send_like.ts +20 -0
  27. package/agent/tools/send_poke.ts +20 -0
  28. package/agent/tools/set_avatar.ts +19 -0
  29. package/agent/tools/set_emoji_reaction.ts +20 -0
  30. package/agent/tools/set_essence_msg.ts +21 -0
  31. package/agent/tools/set_group_portrait.ts +22 -0
  32. package/agent/tools/set_online_status.ts +20 -0
  33. package/agent/tools/set_profile.ts +23 -0
  34. package/agent/tools/set_signature.ts +19 -0
  35. package/agent/tools/set_title.ts +23 -0
  36. package/agent/tools/translate.ts +18 -0
  37. package/agent/tools/upload_group_file.ts +23 -0
  38. package/lib/http-endpoint.d.ts +26 -0
  39. package/lib/http-endpoint.js +147 -0
  40. package/lib/index.d.ts +11 -21
  41. package/lib/index.js +10 -59
  42. package/lib/napcat-agent-deps.d.ts +51 -0
  43. package/lib/napcat-agent-deps.js +33 -0
  44. package/lib/napcat-inbound.d.ts +12 -5
  45. package/lib/napcat-inbound.js +28 -13
  46. package/lib/onebot-get-msg.d.ts +13 -2
  47. package/lib/onebot-get-msg.js +0 -1
  48. package/lib/protocol.d.ts +151 -0
  49. package/lib/protocol.js +217 -0
  50. package/lib/webhook.d.ts +2 -0
  51. package/lib/webhook.js +14 -0
  52. package/lib/ws-endpoint.d.ts +61 -0
  53. package/lib/ws-endpoint.js +357 -0
  54. package/lib/ws-transport.d.ts +13 -0
  55. package/lib/ws-transport.js +75 -0
  56. package/lib/ws-types.d.ts +17 -0
  57. package/lib/ws-types.js +1 -0
  58. package/lib/wss-auth.d.ts +2 -0
  59. package/lib/wss-auth.js +16 -0
  60. package/lib/wss-endpoint.d.ts +25 -0
  61. package/lib/wss-endpoint.js +157 -0
  62. package/package.json +48 -32
  63. package/plugin.ts +13 -0
  64. package/schema.json +48 -0
  65. package/src/http-endpoint.ts +197 -0
  66. package/src/index.ts +66 -82
  67. package/src/napcat-agent-deps.ts +95 -0
  68. package/src/napcat-inbound.ts +34 -10
  69. package/src/onebot-get-msg.ts +8 -2
  70. package/src/protocol.ts +373 -0
  71. package/src/webhook.ts +16 -0
  72. package/src/ws-endpoint.ts +452 -0
  73. package/src/ws-transport.ts +105 -0
  74. package/src/ws-types.ts +20 -0
  75. package/src/wss-auth.ts +17 -0
  76. package/src/wss-endpoint.ts +205 -0
  77. package/client/NapCatManagement.tsx +0 -113
  78. package/client/index.tsx +0 -11
  79. package/client/tsconfig.json +0 -7
  80. package/client/utils/api.ts +0 -30
  81. package/dist/index.js +0 -27
  82. package/lib/adapter.d.ts +0 -36
  83. package/lib/adapter.d.ts.map +0 -1
  84. package/lib/adapter.js +0 -102
  85. package/lib/adapter.js.map +0 -1
  86. package/lib/agent-prompt.d.ts +0 -3
  87. package/lib/agent-prompt.d.ts.map +0 -1
  88. package/lib/agent-prompt.js +0 -106
  89. package/lib/agent-prompt.js.map +0 -1
  90. package/lib/endpoint-base.d.ts +0 -144
  91. package/lib/endpoint-base.d.ts.map +0 -1
  92. package/lib/endpoint-base.js +0 -341
  93. package/lib/endpoint-base.js.map +0 -1
  94. package/lib/endpoint-http.d.ts +0 -17
  95. package/lib/endpoint-http.d.ts.map +0 -1
  96. package/lib/endpoint-http.js +0 -106
  97. package/lib/endpoint-http.js.map +0 -1
  98. package/lib/endpoint-ws-client.d.ts +0 -20
  99. package/lib/endpoint-ws-client.d.ts.map +0 -1
  100. package/lib/endpoint-ws-client.js +0 -139
  101. package/lib/endpoint-ws-client.js.map +0 -1
  102. package/lib/endpoint-ws-server.d.ts +0 -20
  103. package/lib/endpoint-ws-server.d.ts.map +0 -1
  104. package/lib/endpoint-ws-server.js +0 -137
  105. package/lib/endpoint-ws-server.js.map +0 -1
  106. package/lib/index.d.ts.map +0 -1
  107. package/lib/index.js.map +0 -1
  108. package/lib/napcat-inbound.d.ts.map +0 -1
  109. package/lib/napcat-inbound.js.map +0 -1
  110. package/lib/onebot-get-msg.d.ts.map +0 -1
  111. package/lib/onebot-get-msg.js.map +0 -1
  112. package/lib/routes.d.ts +0 -4
  113. package/lib/routes.d.ts.map +0 -1
  114. package/lib/routes.js +0 -61
  115. package/lib/routes.js.map +0 -1
  116. package/lib/segment-mapper.d.ts +0 -2
  117. package/lib/segment-mapper.d.ts.map +0 -1
  118. package/lib/segment-mapper.js +0 -2
  119. package/lib/segment-mapper.js.map +0 -1
  120. package/lib/tools.d.ts +0 -8
  121. package/lib/tools.d.ts.map +0 -1
  122. package/lib/tools.js +0 -605
  123. package/lib/tools.js.map +0 -1
  124. package/lib/types.d.ts +0 -275
  125. package/lib/types.d.ts.map +0 -1
  126. package/lib/types.js +0 -6
  127. package/lib/types.js.map +0 -1
  128. package/plugin.yml +0 -3
  129. package/src/adapter.ts +0 -109
  130. package/src/agent-prompt.ts +0 -116
  131. package/src/endpoint-base.ts +0 -386
  132. package/src/endpoint-http.ts +0 -95
  133. package/src/endpoint-ws-client.ts +0 -137
  134. package/src/endpoint-ws-server.ts +0 -136
  135. package/src/routes.ts +0 -61
  136. package/src/segment-mapper.ts +0 -1
  137. package/src/tools.ts +0 -632
  138. package/src/types.ts +0 -266
  139. /package/{skills/napcat → agent}/PERMITS.md +0 -0
  140. /package/{skills/napcat/SKILL.md → agent/skills/napcat.md} +0 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
3
+ */
4
+ import { clearInterval } from 'node:timers';
5
+ import { formatCompact, getLogger } from '@zhin.js/logger';
6
+ import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
7
+ import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
8
+ import { buildSendAction, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
9
+ import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, startNapCatHeartbeat, } from './ws-transport.js';
10
+ import { verifyNapCatAccessToken } from './wss-auth.js';
11
+ const logger = getLogger('napcat');
12
+ export class NapCatWssEndpoint {
13
+ #options;
14
+ #inboundDeduper = new InboundMessageDeduper();
15
+ #ws;
16
+ #wsRelease;
17
+ #heartbeatTimer;
18
+ #requestId = { value: 0 };
19
+ #pending = new Map();
20
+ #open = false;
21
+ #started = false;
22
+ #unregisterAgent;
23
+ constructor(options) {
24
+ this.#options = options;
25
+ }
26
+ async start() {
27
+ if (this.#started)
28
+ return;
29
+ this.#started = true;
30
+ this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
31
+ const handle = this.#options.http.ws(this.#options.config.path);
32
+ this.#wsRelease = handle.onConnection((connection) => {
33
+ this.#acceptConnection(connection);
34
+ });
35
+ logger.info(formatCompact({
36
+ op: 'listen',
37
+ endpoint: this.#options.config.name,
38
+ mode: 'wss',
39
+ path: this.#options.config.path,
40
+ }));
41
+ }
42
+ open() {
43
+ this.#open = true;
44
+ }
45
+ close() {
46
+ this.#open = false;
47
+ }
48
+ async stop() {
49
+ this.#open = false;
50
+ this.#unregisterAgent?.();
51
+ this.#unregisterAgent = undefined;
52
+ this.#wsRelease?.();
53
+ this.#wsRelease = undefined;
54
+ if (this.#heartbeatTimer) {
55
+ clearInterval(this.#heartbeatTimer);
56
+ this.#heartbeatTimer = undefined;
57
+ }
58
+ rejectAllPending(this.#pending);
59
+ this.#inboundDeduper.clear();
60
+ if (this.#ws) {
61
+ try {
62
+ this.#ws.close();
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ this.#ws = undefined;
68
+ }
69
+ this.#started = false;
70
+ }
71
+ async send({ target, payload }) {
72
+ const message = formatOutboundSegments(payload);
73
+ const { action, params } = buildSendAction(target, message);
74
+ const data = await this.callApi(action, params);
75
+ return data?.message_id != null ? String(data.message_id) : '';
76
+ }
77
+ callApi(action, params = {}) {
78
+ return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
79
+ }
80
+ admit(ev) {
81
+ if (!this.#open || !isMessageEvent(ev))
82
+ return;
83
+ if (isSelfMessage(ev))
84
+ return;
85
+ const msgId = String(ev.message_id);
86
+ if (!this.#inboundDeduper.shouldProcess(msgId))
87
+ return;
88
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
89
+ ev = { ...ev, message: normalizeMessage(ev.message) };
90
+ }
91
+ const target = formatInboundTarget(ev);
92
+ const nickname = senderNickname(ev);
93
+ const mentioned = isNapCatBotMentioned(ev);
94
+ void this.#options.gateway.receive({
95
+ adapter: this.#options.id,
96
+ target,
97
+ content: formatInboundContent(ev),
98
+ sender: senderUserId(ev),
99
+ id: msgId,
100
+ metadata: Object.freeze({
101
+ message_type: ev.message_type,
102
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
103
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
104
+ endpoint: this.#options.config.name,
105
+ time: ev.time,
106
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
107
+ role: ev.sender?.role,
108
+ ...(nickname ? { nickname } : {}),
109
+ ...(mentioned ? { mentioned: true } : {}),
110
+ }),
111
+ }).catch((err) => {
112
+ logger.warn(formatCompact({
113
+ op: 'napcat_gateway_receive_failed',
114
+ target,
115
+ error: err instanceof Error ? err.message : String(err),
116
+ }));
117
+ });
118
+ }
119
+ #acceptConnection(connection) {
120
+ if (!verifyNapCatAccessToken(this.#options.config.access_token, connection.request)) {
121
+ connection.socket.close(4003, 'Unauthorized');
122
+ return;
123
+ }
124
+ const socket = connection.socket;
125
+ if (this.#ws) {
126
+ try {
127
+ this.#ws.close();
128
+ }
129
+ catch {
130
+ /* ignore */
131
+ }
132
+ }
133
+ this.#ws = socket;
134
+ this.#heartbeatTimer = startNapCatHeartbeat(this.#ws, this.#options.config.heartbeat_interval, this.#heartbeatTimer);
135
+ socket.on('message', (data) => {
136
+ handleNapCatWsMessage(data, {
137
+ endpointName: this.#options.config.name,
138
+ pending: this.#pending,
139
+ admit: (event) => this.admit(event),
140
+ });
141
+ });
142
+ socket.on('close', () => {
143
+ if (this.#ws === socket) {
144
+ this.#ws = undefined;
145
+ if (this.#heartbeatTimer) {
146
+ clearInterval(this.#heartbeatTimer);
147
+ this.#heartbeatTimer = undefined;
148
+ }
149
+ }
150
+ });
151
+ logger.debug(formatCompact({
152
+ endpoint: this.#options.config.name,
153
+ mode: 'wss',
154
+ peer: connection.request.socket.remoteAddress,
155
+ }));
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-napcat",
3
- "version": "3.0.1",
4
- "description": "Zhin.js adapter for NapCatQQ (OneBot11 superset with go-cqhttp & NapCat extensions)",
3
+ "version": "4.0.1",
4
+ "description": "Zhin.js NapCat adapter for Plugin Runtime (WebSocket client, OneBot11 + NapCat extensions)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -13,12 +13,12 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "adapters",
17
+ "plugin.ts",
18
+ "schema.json",
16
19
  "src",
17
20
  "lib",
18
- "client",
19
- "dist",
20
- "skills",
21
- "plugin.yml",
21
+ "agent",
22
22
  "README.md",
23
23
  "CHANGELOG.md"
24
24
  ],
@@ -34,52 +34,53 @@
34
34
  "qq",
35
35
  "chatbot"
36
36
  ],
37
- "author": {
38
- "name": "lc-cn",
39
- "email": "admin@liucl.cn",
40
- "url": "https://github.com/lc-cn"
41
- },
42
37
  "license": "MIT",
43
38
  "repository": {
44
- "url": "git+https://github.com/zhinjs/zhin.git",
45
39
  "type": "git",
40
+ "url": "https://github.com/zhinjs/zhin",
46
41
  "directory": "plugins/adapters/napcat"
47
42
  },
48
43
  "dependencies": {
49
- "ws": "^8.21.0"
44
+ "ws": "^8.21.0",
45
+ "@zhin.js/adapter": "1.0.1",
46
+ "@zhin.js/core": "1.3.5",
47
+ "@zhin.js/host-http": "1.0.1",
48
+ "@zhin.js/logger": "1.0.75",
49
+ "@zhin.js/plugin-runtime": "1.0.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^26.1.0",
53
- "@types/react": "^19.2.17",
54
- "@types/react-dom": "^19.2.3",
55
53
  "@types/ws": "^8.18.1",
56
- "lucide-react": "^1.22.0",
57
54
  "typescript": "^6.0.3",
58
- "@zhin.js/cli": "1.0.92",
59
- "@zhin.js/contract": "1.0.2",
60
- "@zhin.js/agent": "1.0.2",
61
- "@zhin.js/host-api": "2.0.4",
62
- "@zhin.js/host-router": "2.0.2",
63
- "zhin.js": "4.1.1"
55
+ "vitest": "^4.1.10",
56
+ "zod": "^4.4.3",
57
+ "@zhin.js/agent": "1.0.4",
58
+ "@zhin.js/host-http": "1.0.1"
64
59
  },
65
60
  "peerDependencies": {
66
- "@zhin.js/contract": "1.0.2",
67
- "@zhin.js/host-router": "2.0.2",
68
- "@zhin.js/agent": "1.0.2",
69
- "zhin.js": "4.1.1",
70
- "@zhin.js/host-api": "2.0.4"
61
+ "zod": "^4.0.0",
62
+ "@zhin.js/adapter": "1.0.1",
63
+ "@zhin.js/core": "1.3.5",
64
+ "@zhin.js/plugin-runtime": "1.0.1",
65
+ "zhin.js": "4.1.3",
66
+ "@zhin.js/agent": "1.0.4"
71
67
  },
72
68
  "peerDependenciesMeta": {
73
- "@zhin.js/agent": {
69
+ "zhin.js": {
74
70
  "optional": true
75
71
  },
76
- "@zhin.js/host-api": {
72
+ "@zhin.js/agent": {
77
73
  "optional": true
78
74
  },
79
- "@zhin.js/host-router": {
75
+ "zod": {
80
76
  "optional": true
81
77
  }
82
78
  },
79
+ "author": {
80
+ "name": "lc-cn",
81
+ "email": "admin@liucl.cn",
82
+ "url": "https://github.com/lc-cn"
83
+ },
83
84
  "publishConfig": {
84
85
  "access": "public",
85
86
  "registry": "https://registry.npmjs.org"
@@ -87,8 +88,23 @@
87
88
  "engines": {
88
89
  "node": "^20.19.0 || >=22.12.0"
89
90
  },
91
+ "zhin": {
92
+ "protocol": 1,
93
+ "type": "plugin",
94
+ "entry": "./plugin.ts",
95
+ "engine": "^1.0.0",
96
+ "runtime": "trusted",
97
+ "features": [
98
+ {
99
+ "package": "@zhin.js/adapter",
100
+ "api": "^1.0.0"
101
+ }
102
+ ],
103
+ "plugins": []
104
+ },
90
105
  "scripts": {
91
- "build": "zhin build",
92
- "clean": "rimraf lib"
106
+ "build": "tsc",
107
+ "clean": "rimraf lib",
108
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/napcat/tests"
93
109
  }
94
110
  }
package/plugin.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { definePlugin } from '@zhin.js/plugin-runtime';
2
+ import { registerDefaultScenePlatformPermitChecker } from '@zhin.js/core';
3
+
4
+ export default definePlugin({
5
+ name: 'napcat',
6
+ metadata: {
7
+ displayName: 'NapCat Adapter',
8
+ },
9
+ setup() {
10
+ // 平台权限门禁:scene_admin / scene_owner 由 sender role 判定(见各 endpoint admit metadata)
11
+ return registerDefaultScenePlatformPermitChecker('napcat');
12
+ },
13
+ });
package/schema.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "connection": {
7
+ "type": "string",
8
+ "enum": ["ws", "wss", "http"],
9
+ "default": "ws",
10
+ "description": "ws (default), wss (reverse WS), or http (POST webhook + HTTP API outbound)"
11
+ },
12
+ "name": {
13
+ "type": "string",
14
+ "default": "napcat-bot"
15
+ },
16
+ "url": {
17
+ "type": "string",
18
+ "description": "NapCat WebSocket URL (required for connection: ws)"
19
+ },
20
+ "path": {
21
+ "type": "string",
22
+ "description": "WS path for reverse-wss"
23
+ },
24
+ "http_url": {
25
+ "type": "string",
26
+ "description": "HTTP API base URL (connection: http outbound)"
27
+ },
28
+ "post_path": {
29
+ "type": "string",
30
+ "description": "HTTP POST event path (connection: http inbound)"
31
+ },
32
+ "access_token": {
33
+ "type": "string"
34
+ },
35
+ "reconnect_interval": {
36
+ "type": "number",
37
+ "default": 5000
38
+ },
39
+ "heartbeat_interval": {
40
+ "type": "number",
41
+ "default": 30000
42
+ },
43
+ "poll_interval": {
44
+ "type": "number",
45
+ "default": 30000
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { EndpointInstance } from '@zhin.js/adapter';
6
+ import type { MessageGateway } from '@zhin.js/core/runtime';
7
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
+ import { formatCompact, getLogger } from '@zhin.js/logger';
9
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
10
+ import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
11
+ import {
12
+ InboundMessageDeduper,
13
+ isNapCatBotMentioned,
14
+ isSelfMessage,
15
+ normalizeMessage,
16
+ } from './napcat-inbound.js';
17
+ import {
18
+ buildSendAction,
19
+ callNapCatHttpAction,
20
+ formatInboundContent,
21
+ formatInboundTarget,
22
+ formatOutboundSegments,
23
+ isMessageEvent,
24
+ senderNickname,
25
+ senderUserId,
26
+ type NapCatEvent,
27
+ type NapCatHttpConfig,
28
+ } from './protocol.js';
29
+ import { readRequestBody } from './webhook.js';
30
+ import { NapCatWsEndpoint } from './ws-endpoint.js';
31
+ import { verifyNapCatAccessToken } from './wss-auth.js';
32
+
33
+ const logger = getLogger('napcat');
34
+
35
+ export interface NapCatHttpEndpointOptions {
36
+ readonly id: CapabilityId;
37
+ readonly gateway: MessageGateway;
38
+ readonly http: HttpHost;
39
+ readonly config: NapCatHttpConfig;
40
+ readonly callHttpAction?: typeof callNapCatHttpAction;
41
+ }
42
+
43
+ export class NapCatHttpEndpoint implements EndpointInstance {
44
+ readonly #options: NapCatHttpEndpointOptions;
45
+ readonly #inboundDeduper = new InboundMessageDeduper();
46
+ readonly #callHttpAction: typeof callNapCatHttpAction;
47
+ #routeReleases: HttpRouteRegistration[] = [];
48
+ #open = false;
49
+ #started = false;
50
+ #unregisterAgent?: () => void;
51
+
52
+ constructor(options: NapCatHttpEndpointOptions) {
53
+ this.#options = options;
54
+ this.#callHttpAction = options.callHttpAction ?? callNapCatHttpAction;
55
+ }
56
+
57
+ async start(): Promise<void> {
58
+ if (this.#started) return;
59
+ this.#started = true;
60
+ this.#unregisterAgent = registerNapcatAgentEndpoint(
61
+ this.#options.config.name,
62
+ this as unknown as NapCatWsEndpoint,
63
+ );
64
+ this.#setupRoutes();
65
+ logger.info(formatCompact({
66
+ op: 'listen',
67
+ endpoint: this.#options.config.name,
68
+ mode: 'http',
69
+ path: this.#options.config.post_path,
70
+ }));
71
+ }
72
+
73
+ open(): void {
74
+ this.#open = true;
75
+ }
76
+
77
+ close(): void {
78
+ this.#open = false;
79
+ }
80
+
81
+ async stop(): Promise<void> {
82
+ this.#open = false;
83
+ for (const release of this.#routeReleases.splice(0)) release();
84
+ this.#unregisterAgent?.();
85
+ this.#unregisterAgent = undefined;
86
+ this.#inboundDeduper.clear();
87
+ this.#started = false;
88
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
89
+ }
90
+
91
+ async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
92
+ const message = formatOutboundSegments(payload);
93
+ const { action, params } = buildSendAction(target, message);
94
+ const resp = await this.#callHttpAction(
95
+ {
96
+ http_url: this.#options.config.http_url,
97
+ access_token: this.#options.config.access_token,
98
+ },
99
+ action,
100
+ params,
101
+ );
102
+ const data = resp.data as { message_id?: number | string } | undefined;
103
+ const messageId = data?.message_id != null ? String(data.message_id) : '';
104
+ logger.debug(formatCompact({
105
+ op: 'napcat_send',
106
+ endpoint: this.#options.config.name,
107
+ target,
108
+ messageId,
109
+ }));
110
+ return messageId;
111
+ }
112
+
113
+ callApi(action: string, params: Record<string, unknown> = {}): Promise<unknown> {
114
+ return this.#callHttpAction(
115
+ {
116
+ http_url: this.#options.config.http_url,
117
+ access_token: this.#options.config.access_token,
118
+ },
119
+ action,
120
+ params,
121
+ ).then((resp) => resp.data);
122
+ }
123
+
124
+ admit(ev: NapCatEvent): void {
125
+ if (!this.#open || !isMessageEvent(ev)) return;
126
+ if (isSelfMessage(ev)) return;
127
+ const msgId = String(ev.message_id);
128
+ if (!this.#inboundDeduper.shouldProcess(msgId)) return;
129
+ if (Array.isArray(ev.message) || typeof ev.message === 'string') {
130
+ ev = { ...ev, message: normalizeMessage(ev.message) };
131
+ }
132
+ const target = formatInboundTarget(ev);
133
+ const nickname = senderNickname(ev);
134
+ const mentioned = isNapCatBotMentioned(ev);
135
+ void this.#options.gateway.receive({
136
+ adapter: this.#options.id,
137
+ target,
138
+ content: formatInboundContent(ev),
139
+ sender: senderUserId(ev),
140
+ id: msgId,
141
+ metadata: Object.freeze({
142
+ message_type: ev.message_type,
143
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
144
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
145
+ endpoint: this.#options.config.name,
146
+ time: ev.time,
147
+ self_id: ev.self_id != null ? String(ev.self_id) : undefined,
148
+ role: ev.sender?.role,
149
+ ...(nickname ? { nickname } : {}),
150
+ ...(mentioned ? { mentioned: true } : {}),
151
+ }),
152
+ }).catch((err) => {
153
+ logger.warn(formatCompact({
154
+ op: 'napcat_gateway_receive_failed',
155
+ target,
156
+ error: err instanceof Error ? err.message : String(err),
157
+ }));
158
+ });
159
+ }
160
+
161
+ #setupRoutes(): void {
162
+ const path = this.#options.config.post_path;
163
+ this.#routeReleases.push(
164
+ this.#options.http.route('POST', path, async (request, response) => {
165
+ await this.#handlePost(request, response);
166
+ }, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
167
+ );
168
+ }
169
+
170
+ async #handlePost(request: IncomingMessage, response: ServerResponse): Promise<void> {
171
+ try {
172
+ if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
173
+ response.writeHead(403, { 'Content-Type': 'application/json' });
174
+ response.end(JSON.stringify({ message: 'Unauthorized' }));
175
+ return;
176
+ }
177
+ const raw = await readRequestBody(request);
178
+ let ev: NapCatEvent;
179
+ try {
180
+ ev = JSON.parse(raw) as NapCatEvent;
181
+ } catch {
182
+ response.writeHead(400, { 'Content-Type': 'application/json' });
183
+ response.end(JSON.stringify({ message: 'Invalid JSON' }));
184
+ return;
185
+ }
186
+ if (this.#open) this.admit(ev);
187
+ response.writeHead(200, { 'Content-Type': 'application/json' });
188
+ response.end(JSON.stringify({ status: 'ok' }));
189
+ } catch (error) {
190
+ 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
+ }
195
+ }
196
+ }
197
+ }