@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.
- package/CHANGELOG.md +58 -0
- package/README.md +43 -96
- package/adapters/napcat.ts +39 -0
- package/agent/tools/ai_tts.ts +21 -0
- package/agent/tools/del_group_notice.ts +22 -0
- package/agent/tools/delete_essence_msg.ts +21 -0
- package/agent/tools/delete_friend.ts +20 -0
- package/agent/tools/download_file.ts +19 -0
- package/agent/tools/forward_single_msg.ts +32 -0
- package/agent/tools/get_ai_characters.ts +19 -0
- package/agent/tools/get_essence_list.ts +19 -0
- package/agent/tools/get_friend_msg_history.ts +20 -0
- package/agent/tools/get_group_file_url.ts +21 -0
- package/agent/tools/get_group_info_ex.ts +19 -0
- package/agent/tools/get_group_msg_history.ts +21 -0
- package/agent/tools/get_group_notice.ts +19 -0
- package/agent/tools/get_group_root_files.ts +19 -0
- package/agent/tools/get_group_shut_list.ts +19 -0
- package/agent/tools/get_mini_app_ark.ts +22 -0
- package/agent/tools/get_user_status.ts +18 -0
- package/agent/tools/group_sign.ts +20 -0
- package/agent/tools/mark_msg_as_read.ts +19 -0
- package/agent/tools/ocr_image.ts +18 -0
- package/agent/tools/send_forward_msg.ts +31 -0
- package/agent/tools/send_group_notice.ts +23 -0
- package/agent/tools/send_like.ts +20 -0
- package/agent/tools/send_poke.ts +20 -0
- package/agent/tools/set_avatar.ts +19 -0
- package/agent/tools/set_emoji_reaction.ts +20 -0
- package/agent/tools/set_essence_msg.ts +21 -0
- package/agent/tools/set_group_portrait.ts +22 -0
- package/agent/tools/set_online_status.ts +20 -0
- package/agent/tools/set_profile.ts +23 -0
- package/agent/tools/set_signature.ts +19 -0
- package/agent/tools/set_title.ts +23 -0
- package/agent/tools/translate.ts +18 -0
- package/agent/tools/upload_group_file.ts +23 -0
- package/lib/http-endpoint.d.ts +26 -0
- package/lib/http-endpoint.js +147 -0
- package/lib/index.d.ts +11 -21
- package/lib/index.js +10 -59
- package/lib/napcat-agent-deps.d.ts +51 -0
- package/lib/napcat-agent-deps.js +33 -0
- package/lib/napcat-inbound.d.ts +12 -5
- package/lib/napcat-inbound.js +28 -13
- package/lib/onebot-get-msg.d.ts +13 -2
- package/lib/onebot-get-msg.js +0 -1
- package/lib/protocol.d.ts +151 -0
- package/lib/protocol.js +217 -0
- package/lib/webhook.d.ts +2 -0
- package/lib/webhook.js +14 -0
- package/lib/ws-endpoint.d.ts +61 -0
- package/lib/ws-endpoint.js +357 -0
- package/lib/ws-transport.d.ts +13 -0
- package/lib/ws-transport.js +75 -0
- package/lib/ws-types.d.ts +17 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-auth.d.ts +2 -0
- package/lib/wss-auth.js +16 -0
- package/lib/wss-endpoint.d.ts +25 -0
- package/lib/wss-endpoint.js +157 -0
- package/package.json +48 -32
- package/plugin.ts +13 -0
- package/schema.json +48 -0
- package/src/http-endpoint.ts +197 -0
- package/src/index.ts +66 -82
- package/src/napcat-agent-deps.ts +95 -0
- package/src/napcat-inbound.ts +34 -10
- package/src/onebot-get-msg.ts +8 -2
- package/src/protocol.ts +373 -0
- package/src/webhook.ts +16 -0
- package/src/ws-endpoint.ts +452 -0
- package/src/ws-transport.ts +105 -0
- package/src/ws-types.ts +20 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +205 -0
- package/client/NapCatManagement.tsx +0 -113
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -27
- package/lib/adapter.d.ts +0 -36
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -102
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts +0 -3
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js +0 -106
- package/lib/agent-prompt.js.map +0 -1
- package/lib/endpoint-base.d.ts +0 -144
- package/lib/endpoint-base.d.ts.map +0 -1
- package/lib/endpoint-base.js +0 -341
- package/lib/endpoint-base.js.map +0 -1
- package/lib/endpoint-http.d.ts +0 -17
- package/lib/endpoint-http.d.ts.map +0 -1
- package/lib/endpoint-http.js +0 -106
- package/lib/endpoint-http.js.map +0 -1
- package/lib/endpoint-ws-client.d.ts +0 -20
- package/lib/endpoint-ws-client.d.ts.map +0 -1
- package/lib/endpoint-ws-client.js +0 -139
- package/lib/endpoint-ws-client.js.map +0 -1
- package/lib/endpoint-ws-server.d.ts +0 -20
- package/lib/endpoint-ws-server.d.ts.map +0 -1
- package/lib/endpoint-ws-server.js +0 -137
- package/lib/endpoint-ws-server.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/napcat-inbound.d.ts.map +0 -1
- package/lib/napcat-inbound.js.map +0 -1
- package/lib/onebot-get-msg.d.ts.map +0 -1
- package/lib/onebot-get-msg.js.map +0 -1
- package/lib/routes.d.ts +0 -4
- package/lib/routes.d.ts.map +0 -1
- package/lib/routes.js +0 -61
- package/lib/routes.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/tools.d.ts +0 -8
- package/lib/tools.d.ts.map +0 -1
- package/lib/tools.js +0 -605
- package/lib/tools.js.map +0 -1
- package/lib/types.d.ts +0 -275
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -6
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -109
- package/src/agent-prompt.ts +0 -116
- package/src/endpoint-base.ts +0 -386
- package/src/endpoint-http.ts +0 -95
- package/src/endpoint-ws-client.ts +0 -137
- package/src/endpoint-ws-server.ts +0 -136
- package/src/routes.ts +0 -61
- package/src/segment-mapper.ts +0 -1
- package/src/tools.ts +0 -632
- package/src/types.ts +0 -266
- /package/{skills/napcat → agent}/PERMITS.md +0 -0
- /package/{skills/napcat/SKILL.md → agent/skills/napcat.md} +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { type NapCatEvent, type NapCatWsConfig } from './protocol.js';
|
|
5
|
+
import { type NapCatWsCreateOptions, type NapCatWsSocket } from './ws-types.js';
|
|
6
|
+
export interface NapCatWsEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly config: NapCatWsConfig;
|
|
10
|
+
readonly createWebSocket?: (url: string, options: NapCatWsCreateOptions) => NapCatWsSocket;
|
|
11
|
+
}
|
|
12
|
+
export declare class NapCatWsEndpoint implements EndpointInstance {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: NapCatWsEndpointOptions);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
open(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
send({ target, payload }: {
|
|
20
|
+
readonly target: string;
|
|
21
|
+
readonly payload: unknown;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
24
|
+
setTitle(groupId: number, userId: number, title: string, duration?: number): Promise<boolean>;
|
|
25
|
+
sendLike(userId: number, times?: number): Promise<unknown>;
|
|
26
|
+
deleteFriend(userId: number): Promise<unknown>;
|
|
27
|
+
markMsgAsRead(messageId: number): Promise<unknown>;
|
|
28
|
+
ocrImage(image: string): Promise<unknown>;
|
|
29
|
+
setQQProfile(nickname: string, company?: string, email?: string, college?: string, personalNote?: string): Promise<unknown>;
|
|
30
|
+
setGroupPortrait(groupId: number, file: string): Promise<unknown>;
|
|
31
|
+
setEssenceMsg(messageId: number): Promise<unknown>;
|
|
32
|
+
deleteEssenceMsg(messageId: number): Promise<unknown>;
|
|
33
|
+
getEssenceMsgList(groupId: number): Promise<unknown>;
|
|
34
|
+
sendGroupSign(groupId: number): Promise<unknown>;
|
|
35
|
+
sendGroupNotice(groupId: number, content: string, image?: string): Promise<unknown>;
|
|
36
|
+
getGroupNotice(groupId: number): Promise<unknown>;
|
|
37
|
+
deleteGroupNotice(groupId: number, noticeId: string): Promise<unknown>;
|
|
38
|
+
uploadGroupFile(groupId: number, file: string, name: string, folder?: string): Promise<unknown>;
|
|
39
|
+
getGroupRootFiles(groupId: number): Promise<unknown>;
|
|
40
|
+
getGroupFileUrl(groupId: number, fileId: string, busid: number): Promise<unknown>;
|
|
41
|
+
downloadFile(url: string, threadCount?: number, headers?: string[]): Promise<unknown>;
|
|
42
|
+
setOnlineStatus(status: number, extStatus: number): Promise<unknown>;
|
|
43
|
+
setQQAvatar(file: string): Promise<unknown>;
|
|
44
|
+
forwardFriendSingleMsg(userId: number, messageId: number): Promise<unknown>;
|
|
45
|
+
forwardGroupSingleMsg(groupId: number, messageId: number): Promise<unknown>;
|
|
46
|
+
translateEn2Zh(sourceText: string): Promise<unknown>;
|
|
47
|
+
setMsgEmojiLike(messageId: number, emojiId: string): Promise<unknown>;
|
|
48
|
+
sendForwardMsg(messageType: 'private' | 'group', id: number, messages: unknown[]): Promise<unknown>;
|
|
49
|
+
getFriendMsgHistory(userId: number, messageSeq?: number, count?: number): Promise<unknown>;
|
|
50
|
+
getGroupMsgHistory(groupId: number, messageSeq?: number, count?: number): Promise<unknown>;
|
|
51
|
+
setSelfLongnick(longnick: string): Promise<unknown>;
|
|
52
|
+
getGroupInfoEx(groupId: number): Promise<unknown>;
|
|
53
|
+
sendPoke(userId: number, groupId?: number): Promise<unknown>;
|
|
54
|
+
ncGetUserStatus(userId: number): Promise<unknown>;
|
|
55
|
+
getGroupShutList(groupId: number): Promise<unknown>;
|
|
56
|
+
getMiniAppArk(type: string, title: string, desc: string, picUrl: string, jumpUrl: string): Promise<unknown>;
|
|
57
|
+
getAiCharacters(groupId: number): Promise<unknown>;
|
|
58
|
+
sendGroupAiRecord(groupId: number, characterId: string, text: string): Promise<unknown>;
|
|
59
|
+
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
60
|
+
admit(ev: NapCatEvent): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat WS client endpoint — outbound connect to NapCat.
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { clearInterval, clearTimeout, setTimeout } from 'node:timers';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import { registerNapcatAgentEndpoint } from './napcat-agent-deps.js';
|
|
8
|
+
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
9
|
+
import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
10
|
+
import { callNapCatWsAction, handleNapCatWsMessage, rejectAllPending, startNapCatHeartbeat, } from './ws-transport.js';
|
|
11
|
+
const logger = getLogger('napcat');
|
|
12
|
+
export class NapCatWsEndpoint {
|
|
13
|
+
#options;
|
|
14
|
+
#inboundDeduper = new InboundMessageDeduper();
|
|
15
|
+
#ws;
|
|
16
|
+
#reconnectTimer;
|
|
17
|
+
#heartbeatTimer;
|
|
18
|
+
#requestId = { value: 0 };
|
|
19
|
+
#pending = new Map();
|
|
20
|
+
#open = false;
|
|
21
|
+
#started = false;
|
|
22
|
+
#stopping = false;
|
|
23
|
+
#unregisterAgent;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.#options = options;
|
|
26
|
+
}
|
|
27
|
+
async start() {
|
|
28
|
+
if (this.#started)
|
|
29
|
+
return;
|
|
30
|
+
this.#started = true;
|
|
31
|
+
this.#stopping = false;
|
|
32
|
+
this.#unregisterAgent = registerNapcatAgentEndpoint(this.#options.config.name, this);
|
|
33
|
+
try {
|
|
34
|
+
await this.#connect();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
this.#started = false;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
open() {
|
|
42
|
+
this.#open = true;
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.#open = false;
|
|
46
|
+
}
|
|
47
|
+
async stop() {
|
|
48
|
+
this.#open = false;
|
|
49
|
+
this.#stopping = true;
|
|
50
|
+
this.#started = false;
|
|
51
|
+
this.#unregisterAgent?.();
|
|
52
|
+
this.#unregisterAgent = undefined;
|
|
53
|
+
if (this.#reconnectTimer) {
|
|
54
|
+
clearTimeout(this.#reconnectTimer);
|
|
55
|
+
this.#reconnectTimer = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (this.#heartbeatTimer) {
|
|
58
|
+
clearInterval(this.#heartbeatTimer);
|
|
59
|
+
this.#heartbeatTimer = undefined;
|
|
60
|
+
}
|
|
61
|
+
rejectAllPending(this.#pending);
|
|
62
|
+
this.#inboundDeduper.clear();
|
|
63
|
+
if (this.#ws) {
|
|
64
|
+
try {
|
|
65
|
+
this.#ws.close();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
this.#ws = undefined;
|
|
71
|
+
}
|
|
72
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
73
|
+
}
|
|
74
|
+
async send({ target, payload }) {
|
|
75
|
+
const message = formatOutboundSegments(payload);
|
|
76
|
+
const { action, params } = buildSendAction(target, message);
|
|
77
|
+
const data = await this.callApi(action, params);
|
|
78
|
+
const messageId = data?.message_id != null ? String(data.message_id) : '';
|
|
79
|
+
logger.debug(formatCompact({
|
|
80
|
+
op: 'napcat_send',
|
|
81
|
+
endpoint: this.#options.config.name,
|
|
82
|
+
target,
|
|
83
|
+
messageId,
|
|
84
|
+
}));
|
|
85
|
+
return messageId;
|
|
86
|
+
}
|
|
87
|
+
callApi(action, params = {}) {
|
|
88
|
+
return callNapCatWsAction(this.#ws, this.#pending, this.#requestId, action, params);
|
|
89
|
+
}
|
|
90
|
+
// ── Agent-facing API wrappers ─────────────────────────────────────
|
|
91
|
+
async setTitle(groupId, userId, title, duration = -1) {
|
|
92
|
+
await this.callApi('set_group_special_title', {
|
|
93
|
+
group_id: groupId,
|
|
94
|
+
user_id: userId,
|
|
95
|
+
special_title: title,
|
|
96
|
+
duration,
|
|
97
|
+
});
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
sendLike(userId, times = 1) {
|
|
101
|
+
return this.callApi('send_like', { user_id: userId, times });
|
|
102
|
+
}
|
|
103
|
+
deleteFriend(userId) {
|
|
104
|
+
return this.callApi('delete_friend', { user_id: userId });
|
|
105
|
+
}
|
|
106
|
+
markMsgAsRead(messageId) {
|
|
107
|
+
return this.callApi('mark_msg_as_read', { message_id: messageId });
|
|
108
|
+
}
|
|
109
|
+
ocrImage(image) {
|
|
110
|
+
return this.callApi('ocr_image', { image });
|
|
111
|
+
}
|
|
112
|
+
setQQProfile(nickname, company, email, college, personalNote) {
|
|
113
|
+
return this.callApi('set_qq_profile', {
|
|
114
|
+
nickname,
|
|
115
|
+
company,
|
|
116
|
+
email,
|
|
117
|
+
college,
|
|
118
|
+
personal_note: personalNote,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
setGroupPortrait(groupId, file) {
|
|
122
|
+
return this.callApi('set_group_portrait', { group_id: groupId, file });
|
|
123
|
+
}
|
|
124
|
+
setEssenceMsg(messageId) {
|
|
125
|
+
return this.callApi('set_essence_msg', { message_id: messageId });
|
|
126
|
+
}
|
|
127
|
+
deleteEssenceMsg(messageId) {
|
|
128
|
+
return this.callApi('delete_essence_msg', { message_id: messageId });
|
|
129
|
+
}
|
|
130
|
+
getEssenceMsgList(groupId) {
|
|
131
|
+
return this.callApi('get_essence_msg_list', { group_id: groupId });
|
|
132
|
+
}
|
|
133
|
+
sendGroupSign(groupId) {
|
|
134
|
+
return this.callApi('send_group_sign', { group_id: groupId });
|
|
135
|
+
}
|
|
136
|
+
sendGroupNotice(groupId, content, image) {
|
|
137
|
+
return this.callApi('_send_group_notice', { group_id: groupId, content, image });
|
|
138
|
+
}
|
|
139
|
+
getGroupNotice(groupId) {
|
|
140
|
+
return this.callApi('_get_group_notice', { group_id: groupId });
|
|
141
|
+
}
|
|
142
|
+
deleteGroupNotice(groupId, noticeId) {
|
|
143
|
+
return this.callApi('_del_group_notice', { group_id: groupId, notice_id: noticeId });
|
|
144
|
+
}
|
|
145
|
+
uploadGroupFile(groupId, file, name, folder) {
|
|
146
|
+
return this.callApi('upload_group_file', { group_id: groupId, file, name, folder });
|
|
147
|
+
}
|
|
148
|
+
getGroupRootFiles(groupId) {
|
|
149
|
+
return this.callApi('get_group_root_files', { group_id: groupId });
|
|
150
|
+
}
|
|
151
|
+
getGroupFileUrl(groupId, fileId, busid) {
|
|
152
|
+
return this.callApi('get_group_file_url', { group_id: groupId, file_id: fileId, busid });
|
|
153
|
+
}
|
|
154
|
+
downloadFile(url, threadCount = 1, headers) {
|
|
155
|
+
return this.callApi('download_file', { url, thread_count: threadCount, headers });
|
|
156
|
+
}
|
|
157
|
+
setOnlineStatus(status, extStatus) {
|
|
158
|
+
return this.callApi('set_online_status', { status, ext_status: extStatus });
|
|
159
|
+
}
|
|
160
|
+
setQQAvatar(file) {
|
|
161
|
+
return this.callApi('set_qq_avatar', { file });
|
|
162
|
+
}
|
|
163
|
+
forwardFriendSingleMsg(userId, messageId) {
|
|
164
|
+
return this.callApi('forward_friend_single_msg', { user_id: userId, message_id: messageId });
|
|
165
|
+
}
|
|
166
|
+
forwardGroupSingleMsg(groupId, messageId) {
|
|
167
|
+
return this.callApi('forward_group_single_msg', { group_id: groupId, message_id: messageId });
|
|
168
|
+
}
|
|
169
|
+
translateEn2Zh(sourceText) {
|
|
170
|
+
return this.callApi('translate_en2zh', { source_text: sourceText });
|
|
171
|
+
}
|
|
172
|
+
setMsgEmojiLike(messageId, emojiId) {
|
|
173
|
+
return this.callApi('set_msg_emoji_like', { message_id: messageId, emoji_id: emojiId });
|
|
174
|
+
}
|
|
175
|
+
sendForwardMsg(messageType, id, messages) {
|
|
176
|
+
return this.callApi('send_forward_msg', {
|
|
177
|
+
message_type: messageType,
|
|
178
|
+
[messageType === 'group' ? 'group_id' : 'user_id']: id,
|
|
179
|
+
messages,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
getFriendMsgHistory(userId, messageSeq, count) {
|
|
183
|
+
return this.callApi('get_friend_msg_history', {
|
|
184
|
+
user_id: userId,
|
|
185
|
+
message_seq: messageSeq,
|
|
186
|
+
count,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
getGroupMsgHistory(groupId, messageSeq, count) {
|
|
190
|
+
return this.callApi('get_group_msg_history', {
|
|
191
|
+
group_id: groupId,
|
|
192
|
+
message_seq: messageSeq,
|
|
193
|
+
count,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
setSelfLongnick(longnick) {
|
|
197
|
+
return this.callApi('set_self_longnick', { longNick: longnick });
|
|
198
|
+
}
|
|
199
|
+
getGroupInfoEx(groupId) {
|
|
200
|
+
return this.callApi('get_group_info_ex', { group_id: groupId });
|
|
201
|
+
}
|
|
202
|
+
sendPoke(userId, groupId) {
|
|
203
|
+
return this.callApi('send_poke', { user_id: userId, group_id: groupId });
|
|
204
|
+
}
|
|
205
|
+
ncGetUserStatus(userId) {
|
|
206
|
+
return this.callApi('nc_get_user_status', { user_id: userId });
|
|
207
|
+
}
|
|
208
|
+
getGroupShutList(groupId) {
|
|
209
|
+
return this.callApi('get_group_shut_list', { group_id: groupId });
|
|
210
|
+
}
|
|
211
|
+
getMiniAppArk(type, title, desc, picUrl, jumpUrl) {
|
|
212
|
+
return this.callApi('get_mini_app_ark', { type, title, desc, picUrl, jumpUrl });
|
|
213
|
+
}
|
|
214
|
+
getAiCharacters(groupId) {
|
|
215
|
+
return this.callApi('get_ai_characters', { group_id: groupId });
|
|
216
|
+
}
|
|
217
|
+
sendGroupAiRecord(groupId, characterId, text) {
|
|
218
|
+
return this.callApi('send_group_ai_record', {
|
|
219
|
+
group_id: groupId,
|
|
220
|
+
character: characterId,
|
|
221
|
+
text,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
225
|
+
admit(ev) {
|
|
226
|
+
if (!this.#open || !isMessageEvent(ev))
|
|
227
|
+
return;
|
|
228
|
+
if (isSelfMessage(ev))
|
|
229
|
+
return;
|
|
230
|
+
const msgId = String(ev.message_id);
|
|
231
|
+
if (!this.#inboundDeduper.shouldProcess(msgId))
|
|
232
|
+
return;
|
|
233
|
+
if (Array.isArray(ev.message) || typeof ev.message === 'string') {
|
|
234
|
+
ev = { ...ev, message: normalizeMessage(ev.message) };
|
|
235
|
+
}
|
|
236
|
+
const target = formatInboundTarget(ev);
|
|
237
|
+
const content = formatInboundContent(ev);
|
|
238
|
+
const nickname = senderNickname(ev);
|
|
239
|
+
const mentioned = isNapCatBotMentioned(ev);
|
|
240
|
+
void this.#options.gateway.receive({
|
|
241
|
+
adapter: this.#options.id,
|
|
242
|
+
target,
|
|
243
|
+
content,
|
|
244
|
+
sender: senderUserId(ev),
|
|
245
|
+
id: msgId,
|
|
246
|
+
metadata: Object.freeze({
|
|
247
|
+
message_type: ev.message_type,
|
|
248
|
+
user_id: ev.user_id != null ? String(ev.user_id) : undefined,
|
|
249
|
+
group_id: ev.group_id != null ? String(ev.group_id) : undefined,
|
|
250
|
+
endpoint: this.#options.config.name,
|
|
251
|
+
time: ev.time,
|
|
252
|
+
self_id: ev.self_id != null ? String(ev.self_id) : undefined,
|
|
253
|
+
role: ev.sender?.role,
|
|
254
|
+
...(nickname ? { nickname } : {}),
|
|
255
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
256
|
+
}),
|
|
257
|
+
}).catch((err) => {
|
|
258
|
+
logger.warn(formatCompact({
|
|
259
|
+
op: 'napcat_gateway_receive_failed',
|
|
260
|
+
target,
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
}));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async #connect() {
|
|
266
|
+
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
267
|
+
const create = this.#options.createWebSocket
|
|
268
|
+
?? ((connectUrl, options) => new WebSocket(connectUrl, { headers: options.headers }));
|
|
269
|
+
await new Promise((resolve, reject) => {
|
|
270
|
+
let settled = false;
|
|
271
|
+
const ws = create(url, { headers });
|
|
272
|
+
this.#ws = ws;
|
|
273
|
+
ws.on('open', () => {
|
|
274
|
+
if (settled)
|
|
275
|
+
return;
|
|
276
|
+
settled = true;
|
|
277
|
+
if (!this.#options.config.access_token) {
|
|
278
|
+
logger.warn(formatCompact({
|
|
279
|
+
endpoint: this.#options.config.name,
|
|
280
|
+
ok: false,
|
|
281
|
+
error: 'missing access_token',
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
logger.debug(formatCompact({
|
|
285
|
+
endpoint: this.#options.config.name,
|
|
286
|
+
mode: 'ws',
|
|
287
|
+
url: safeUrl,
|
|
288
|
+
}));
|
|
289
|
+
this.#heartbeatTimer = this.#stopping
|
|
290
|
+
? this.#heartbeatTimer
|
|
291
|
+
: startNapCatHeartbeat(this.#ws, this.#options.config.heartbeat_interval, this.#heartbeatTimer);
|
|
292
|
+
resolve();
|
|
293
|
+
});
|
|
294
|
+
ws.on('message', (data) => {
|
|
295
|
+
handleNapCatWsMessage(data, {
|
|
296
|
+
endpointName: this.#options.config.name,
|
|
297
|
+
pending: this.#pending,
|
|
298
|
+
admit: (event) => this.admit(event),
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
ws.on('close', (code, reason) => {
|
|
302
|
+
const reasonStr = typeof reason === 'string'
|
|
303
|
+
? reason
|
|
304
|
+
: Buffer.isBuffer(reason)
|
|
305
|
+
? reason.toString()
|
|
306
|
+
: String(reason ?? '');
|
|
307
|
+
const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
|
|
308
|
+
const codeHint = codeNum === 1005
|
|
309
|
+
? ' [no status]'
|
|
310
|
+
: codeNum === 1006
|
|
311
|
+
? ' [abnormal]'
|
|
312
|
+
: '';
|
|
313
|
+
logger.warn(formatCompact({
|
|
314
|
+
op: 'disconnect',
|
|
315
|
+
endpoint: this.#options.config.name,
|
|
316
|
+
code: codeNum,
|
|
317
|
+
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
318
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
319
|
+
}));
|
|
320
|
+
if (!settled) {
|
|
321
|
+
settled = true;
|
|
322
|
+
reject(new Error(`NapCat WS closed: ${codeNum} ${reasonStr}`));
|
|
323
|
+
}
|
|
324
|
+
this.#scheduleReconnect();
|
|
325
|
+
});
|
|
326
|
+
ws.on('error', (err) => {
|
|
327
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
328
|
+
logger.warn(formatCompact({
|
|
329
|
+
op: 'ws_error',
|
|
330
|
+
endpoint: this.#options.config.name,
|
|
331
|
+
ok: false,
|
|
332
|
+
error: error.message,
|
|
333
|
+
}));
|
|
334
|
+
if (!settled) {
|
|
335
|
+
settled = true;
|
|
336
|
+
reject(error);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
#scheduleReconnect() {
|
|
342
|
+
if (this.#stopping || !this.#started || this.#reconnectTimer)
|
|
343
|
+
return;
|
|
344
|
+
const delay = this.#options.config.reconnect_interval;
|
|
345
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
346
|
+
this.#reconnectTimer = undefined;
|
|
347
|
+
void this.#connect().catch((err) => {
|
|
348
|
+
logger.warn(formatCompact({
|
|
349
|
+
op: 'reconnect',
|
|
350
|
+
endpoint: this.#options.config.name,
|
|
351
|
+
ok: false,
|
|
352
|
+
error: err instanceof Error ? err.message : String(err),
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
355
|
+
}, delay);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type NapCatEvent } from './protocol.js';
|
|
2
|
+
import { type NapCatPendingAction, type NapCatWsSocket } from './ws-types.js';
|
|
3
|
+
export declare function decodeWsPayload(data: unknown): string;
|
|
4
|
+
export declare function handleNapCatWsMessage(data: unknown, options: {
|
|
5
|
+
readonly endpointName: string;
|
|
6
|
+
readonly pending: Map<string, NapCatPendingAction>;
|
|
7
|
+
readonly admit: (ev: NapCatEvent) => void;
|
|
8
|
+
}): void;
|
|
9
|
+
export declare function callNapCatWsAction(ws: NapCatWsSocket | undefined, pending: Map<string, NapCatPendingAction>, requestId: {
|
|
10
|
+
value: number;
|
|
11
|
+
}, action: string, params: Record<string, unknown>): Promise<unknown>;
|
|
12
|
+
export declare function startNapCatHeartbeat(ws: NapCatWsSocket | undefined, intervalMs: number, existingTimer?: NodeJS.Timeout): NodeJS.Timeout;
|
|
13
|
+
export declare function rejectAllPending(pending: Map<string, NapCatPendingAction>, message?: string): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'node:timers';
|
|
2
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
|
+
import { WS_OPEN, } from './ws-types.js';
|
|
4
|
+
const logger = getLogger('napcat');
|
|
5
|
+
export function decodeWsPayload(data) {
|
|
6
|
+
if (typeof data === 'string')
|
|
7
|
+
return data;
|
|
8
|
+
if (Buffer.isBuffer(data))
|
|
9
|
+
return data.toString();
|
|
10
|
+
if (data instanceof ArrayBuffer)
|
|
11
|
+
return new TextDecoder().decode(data);
|
|
12
|
+
return String(data ?? '');
|
|
13
|
+
}
|
|
14
|
+
export function handleNapCatWsMessage(data, options) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = decodeWsPayload(data);
|
|
17
|
+
const msg = JSON.parse(raw);
|
|
18
|
+
if ('echo' in msg && typeof msg.echo === 'string') {
|
|
19
|
+
const resp = msg;
|
|
20
|
+
const pending = options.pending.get(resp.echo);
|
|
21
|
+
if (pending) {
|
|
22
|
+
options.pending.delete(resp.echo);
|
|
23
|
+
clearTimeout(pending.timeout);
|
|
24
|
+
if (resp.status === 'ok')
|
|
25
|
+
pending.resolve(resp.data);
|
|
26
|
+
else {
|
|
27
|
+
pending.reject(new Error(`API error [${resp.retcode}]: ${resp.message || resp.wording || 'unknown'}`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
options.admit(msg);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
logger.warn(formatCompact({
|
|
36
|
+
op: 'napcat_parse_failed',
|
|
37
|
+
endpoint: options.endpointName,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function callNapCatWsAction(ws, pending, requestId, action, params) {
|
|
43
|
+
if (!ws || ws.readyState !== WS_OPEN) {
|
|
44
|
+
return Promise.reject(new Error('WebSocket is not connected'));
|
|
45
|
+
}
|
|
46
|
+
const echo = `req_${++requestId.value}`;
|
|
47
|
+
const req = { action, params, echo };
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const timeout = setTimeout(() => {
|
|
50
|
+
pending.delete(echo);
|
|
51
|
+
reject(new Error(`API call timeout: ${action}`));
|
|
52
|
+
}, 30_000);
|
|
53
|
+
pending.set(echo, { resolve, reject, timeout });
|
|
54
|
+
ws.send(JSON.stringify(req));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function startNapCatHeartbeat(ws, intervalMs, existingTimer) {
|
|
58
|
+
if (existingTimer)
|
|
59
|
+
clearInterval(existingTimer);
|
|
60
|
+
return setInterval(() => {
|
|
61
|
+
try {
|
|
62
|
+
ws?.ping?.();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore */
|
|
66
|
+
}
|
|
67
|
+
}, intervalMs);
|
|
68
|
+
}
|
|
69
|
+
export function rejectAllPending(pending, message = 'Connection closed') {
|
|
70
|
+
for (const [, entry] of pending) {
|
|
71
|
+
clearTimeout(entry.timeout);
|
|
72
|
+
entry.reject(new Error(message));
|
|
73
|
+
}
|
|
74
|
+
pending.clear();
|
|
75
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
|
+
export interface NapCatWsSocket {
|
|
3
|
+
readonly readyState: number;
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
ping?(): void;
|
|
7
|
+
on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
8
|
+
}
|
|
9
|
+
export interface NapCatWsCreateOptions {
|
|
10
|
+
readonly headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare const WS_OPEN = 1;
|
|
13
|
+
export interface NapCatPendingAction {
|
|
14
|
+
resolve: (value: unknown) => void;
|
|
15
|
+
reject: (err: Error) => void;
|
|
16
|
+
timeout: NodeJS.Timeout;
|
|
17
|
+
}
|
package/lib/ws-types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const WS_OPEN = 1;
|
package/lib/wss-auth.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function verifyNapCatAccessToken(accessToken, request) {
|
|
2
|
+
if (!accessToken)
|
|
3
|
+
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;
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
5
|
+
import { type NapCatEvent, type NapCatWssConfig } from './protocol.js';
|
|
6
|
+
export interface NapCatWssEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly http: HttpHost;
|
|
10
|
+
readonly config: NapCatWssConfig;
|
|
11
|
+
}
|
|
12
|
+
export declare class NapCatWssEndpoint implements EndpointInstance {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: NapCatWssEndpointOptions);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
open(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
send({ target, payload }: {
|
|
20
|
+
readonly target: string;
|
|
21
|
+
readonly payload: unknown;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
24
|
+
admit(ev: NapCatEvent): void;
|
|
25
|
+
}
|