@zhin.js/adapter-napcat 1.0.1 → 1.1.2
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 +757 -0
- package/{skills/napcat/PERMITS.md → PERMITS.md} +3 -3
- package/README.md +63 -95
- package/adapters/napcat/index.js +47 -0
- package/adapters/napcat/index.ts +64 -0
- package/agents/napcat/agent.json +20 -0
- package/agents/napcat/boundaries.md +3 -0
- package/agents/napcat/conventions.md +3 -0
- package/agents/napcat/skills/napcat-account/SKILL.md +25 -0
- package/agents/napcat/skills/napcat-account/tools/delete_friend/index.js +18 -0
- package/agents/napcat/skills/napcat-account/tools/delete_friend/index.ts +18 -0
- package/agents/napcat/skills/napcat-account/tools/get_user_status/index.js +16 -0
- package/agents/napcat/skills/napcat-account/tools/get_user_status/index.ts +16 -0
- package/agents/napcat/skills/napcat-account/tools/send_like/index.js +18 -0
- package/agents/napcat/skills/napcat-account/tools/send_like/index.ts +18 -0
- package/agents/napcat/skills/napcat-account/tools/set_avatar/index.js +17 -0
- package/agents/napcat/skills/napcat-account/tools/set_avatar/index.ts +17 -0
- package/agents/napcat/skills/napcat-account/tools/set_online_status/index.js +18 -0
- package/agents/napcat/skills/napcat-account/tools/set_online_status/index.ts +18 -0
- package/agents/napcat/skills/napcat-account/tools/set_profile/index.js +21 -0
- package/agents/napcat/skills/napcat-account/tools/set_profile/index.ts +21 -0
- package/agents/napcat/skills/napcat-account/tools/set_signature/index.js +17 -0
- package/agents/napcat/skills/napcat-account/tools/set_signature/index.ts +17 -0
- package/agents/napcat/skills/napcat-files-history/SKILL.md +22 -0
- package/agents/napcat/skills/napcat-files-history/tools/download_file/index.js +17 -0
- package/agents/napcat/skills/napcat-files-history/tools/download_file/index.ts +17 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.js +18 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_friend_msg_history/index.ts +18 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.js +19 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_file_url/index.ts +19 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.js +19 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_msg_history/index.ts +19 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.js +17 -0
- package/agents/napcat/skills/napcat-files-history/tools/get_group_root_files/index.ts +17 -0
- package/agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.js +21 -0
- package/agents/napcat/skills/napcat-files-history/tools/upload_group_file/index.ts +21 -0
- package/agents/napcat/skills/napcat-group-content/SKILL.md +21 -0
- package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.js +20 -0
- package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.ts +20 -0
- package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.js +19 -0
- package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.ts +19 -0
- package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.js +17 -0
- package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.ts +17 -0
- package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.js +17 -0
- package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.ts +17 -0
- package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.js +21 -0
- package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.ts +21 -0
- package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.js +19 -0
- package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.ts +19 -0
- package/agents/napcat/skills/napcat-group-settings/SKILL.md +23 -0
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.js +17 -0
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.ts +17 -0
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.js +17 -0
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.ts +17 -0
- package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.js +18 -0
- package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.ts +18 -0
- package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.js +20 -0
- package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.ts +20 -0
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +21 -0
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +21 -0
- package/agents/napcat/skills/napcat-media/SKILL.md +23 -0
- package/agents/napcat/skills/napcat-media/tools/ai_tts/index.js +19 -0
- package/agents/napcat/skills/napcat-media/tools/ai_tts/index.ts +19 -0
- package/agents/napcat/skills/napcat-media/tools/get_ai_characters/index.js +17 -0
- package/agents/napcat/skills/napcat-media/tools/get_ai_characters/index.ts +17 -0
- package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.js +20 -0
- package/agents/napcat/skills/napcat-media/tools/get_mini_app_ark/index.ts +20 -0
- package/agents/napcat/skills/napcat-media/tools/ocr_image/index.js +16 -0
- package/agents/napcat/skills/napcat-media/tools/ocr_image/index.ts +16 -0
- package/agents/napcat/skills/napcat-media/tools/translate/index.js +16 -0
- package/agents/napcat/skills/napcat-media/tools/translate/index.ts +16 -0
- package/agents/napcat/skills/napcat-messaging/SKILL.md +22 -0
- package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.js +22 -0
- package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.ts +28 -0
- package/agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.js +17 -0
- package/agents/napcat/skills/napcat-messaging/tools/mark_msg_as_read/index.ts +17 -0
- package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.js +19 -0
- package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.ts +27 -0
- package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.js +18 -0
- package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.ts +18 -0
- package/agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.js +18 -0
- package/agents/napcat/skills/napcat-messaging/tools/set_emoji_reaction/index.ts +18 -0
- package/agents/napcat/system.md +3 -0
- package/commands/napcat/endpoint/add/[id]/index.js +3 -0
- package/commands/napcat/endpoint/add/[id]/index.ts +3 -0
- package/commands/napcat/endpoint/definition.js +29 -0
- package/commands/napcat/endpoint/definition.ts +26 -0
- package/commands/napcat/endpoint/list/index.js +3 -0
- package/commands/napcat/endpoint/list/index.ts +3 -0
- package/commands/napcat/endpoint/remove/[id]/index.js +3 -0
- package/commands/napcat/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +52 -0
- package/lib/client.js +62 -0
- package/lib/endpoint-management.d.ts +15 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/http-endpoint.d.ts +27 -0
- package/lib/http-endpoint.js +177 -0
- package/lib/index.d.ts +11 -22
- package/lib/index.js +10 -59
- package/lib/napcat-inbound.d.ts +12 -5
- package/lib/napcat-inbound.js +28 -13
- package/lib/napcat-runtime-state.d.ts +1 -0
- package/lib/napcat-runtime-state.js +6 -0
- package/lib/onebot-get-msg.d.ts +16 -3
- package/lib/onebot-get-msg.js +53 -1
- package/lib/protocol.d.ts +147 -0
- package/lib/protocol.js +318 -0
- package/lib/side-event-dispatch.d.ts +7 -0
- package/lib/side-event-dispatch.js +36 -0
- package/lib/webhook.d.ts +2 -0
- package/lib/webhook.js +14 -0
- package/lib/ws-endpoint.d.ts +27 -0
- package/lib/ws-endpoint.js +241 -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 +29 -0
- package/lib/wss-endpoint.js +196 -0
- package/package.json +76 -34
- package/plugin.js +18 -0
- package/schema.json +131 -0
- package/src/client.ts +79 -0
- package/src/endpoint-management.ts +72 -0
- package/src/http-endpoint.ts +234 -0
- package/src/index.ts +62 -87
- package/src/napcat-inbound.ts +34 -10
- package/src/napcat-runtime-state.ts +7 -0
- package/src/onebot-get-msg.ts +69 -5
- package/src/protocol.ts +465 -0
- package/src/side-event-dispatch.ts +48 -0
- package/src/webhook.ts +16 -0
- package/src/ws-endpoint.ts +301 -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 +247 -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 -28
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -94
- 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 -105
- package/lib/agent-prompt.js.map +0 -1
- package/lib/endpoint-base.d.ts +0 -143
- package/lib/endpoint-base.d.ts.map +0 -1
- package/lib/endpoint-base.js +0 -334
- package/lib/endpoint-base.js.map +0 -1
- package/lib/endpoint-http.d.ts +0 -18
- package/lib/endpoint-http.d.ts.map +0 -1
- package/lib/endpoint-http.js +0 -119
- package/lib/endpoint-http.js.map +0 -1
- package/lib/endpoint-ws-client.d.ts +0 -21
- package/lib/endpoint-ws-client.d.ts.map +0 -1
- package/lib/endpoint-ws-client.js +0 -165
- package/lib/endpoint-ws-client.js.map +0 -1
- package/lib/endpoint-ws-server.d.ts +0 -21
- package/lib/endpoint-ws-server.d.ts.map +0 -1
- package/lib/endpoint-ws-server.js +0 -150
- 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/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 -293
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -6
- package/lib/types.js.map +0 -1
- package/lib/typing-indicator.d.ts +0 -45
- package/lib/typing-indicator.d.ts.map +0 -1
- package/lib/typing-indicator.js +0 -132
- package/lib/typing-indicator.js.map +0 -1
- package/plugin.yml +0 -3
- package/skills/napcat/SKILL.md +0 -311
- package/src/adapter.ts +0 -101
- package/src/agent-prompt.ts +0 -114
- package/src/endpoint-base.ts +0 -376
- package/src/endpoint-http.ts +0 -109
- package/src/endpoint-ws-client.ts +0 -164
- package/src/endpoint-ws-server.ts +0 -150
- package/src/routes.ts +0 -61
- package/src/tools.ts +0 -632
- package/src/types.ts +0 -279
- package/src/typing-indicator.ts +0 -184
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat protocol helpers (OneBot 11 + NapCat extensions).
|
|
3
|
+
* No legacy Adapter/Endpoint / segment-mapper.
|
|
4
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
|
+
*/
|
|
6
|
+
import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
|
+
|
|
9
|
+
const logger = getLogger('napcat');
|
|
10
|
+
|
|
11
|
+
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
12
|
+
export interface NapCatEndpointConfig {
|
|
13
|
+
readonly connection?: 'ws' | 'wss' | 'http';
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly access_token?: string;
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
readonly path?: string;
|
|
18
|
+
readonly http_url?: string;
|
|
19
|
+
readonly post_path?: string;
|
|
20
|
+
readonly reconnect_interval?: number;
|
|
21
|
+
readonly heartbeat_interval?: number;
|
|
22
|
+
readonly poll_interval?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NapCatConfigBase {
|
|
26
|
+
readonly context: 'napcat';
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly access_token?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 正向 WebSocket:应用连 NapCat WS */
|
|
32
|
+
export interface NapCatWsConfig extends NapCatConfigBase {
|
|
33
|
+
readonly connection: 'ws';
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly reconnect_interval: number;
|
|
36
|
+
readonly heartbeat_interval: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 反向 WebSocket:httpHostToken WS upgrade 入站/出站 */
|
|
40
|
+
export interface NapCatWssConfig extends NapCatConfigBase {
|
|
41
|
+
readonly connection: 'wss';
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly heartbeat_interval: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** HTTP API + POST 上报:httpHostToken POST 入站 + http_url/{action} 出站 */
|
|
47
|
+
export interface NapCatHttpConfig extends NapCatConfigBase {
|
|
48
|
+
readonly connection: 'http';
|
|
49
|
+
readonly http_url: string;
|
|
50
|
+
readonly post_path: string;
|
|
51
|
+
readonly poll_interval: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ResolvedNapCatConfig = NapCatWsConfig | NapCatWssConfig | NapCatHttpConfig;
|
|
55
|
+
|
|
56
|
+
export interface NapCatSender {
|
|
57
|
+
readonly role?: string;
|
|
58
|
+
readonly nickname?: string;
|
|
59
|
+
readonly card?: string;
|
|
60
|
+
readonly title?: string;
|
|
61
|
+
readonly user_id?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MessageSegment {
|
|
65
|
+
type: string;
|
|
66
|
+
data?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface NapCatEvent {
|
|
70
|
+
post_type: string;
|
|
71
|
+
self_id?: number | string;
|
|
72
|
+
message_type?: string;
|
|
73
|
+
sub_type?: string;
|
|
74
|
+
message_id?: number | string;
|
|
75
|
+
user_id?: number | string;
|
|
76
|
+
group_id?: number | string;
|
|
77
|
+
sender?: NapCatSender;
|
|
78
|
+
message?: MessageSegment[] | string;
|
|
79
|
+
raw_message?: string;
|
|
80
|
+
time?: number;
|
|
81
|
+
notice_type?: string;
|
|
82
|
+
request_type?: string;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type NapCatMessageEvent = NapCatEvent & {
|
|
87
|
+
post_type: 'message' | 'message_sent';
|
|
88
|
+
message_id: number | string;
|
|
89
|
+
user_id: number | string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export interface NapCatActionRequest {
|
|
93
|
+
action: string;
|
|
94
|
+
params: Record<string, unknown>;
|
|
95
|
+
echo?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface NapCatActionResponse {
|
|
99
|
+
status: string;
|
|
100
|
+
retcode: number;
|
|
101
|
+
data?: unknown;
|
|
102
|
+
echo?: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
wording?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface NapCatWireSegment {
|
|
108
|
+
readonly type: string;
|
|
109
|
+
readonly data?: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ParsedSendTarget {
|
|
113
|
+
readonly message_type: 'private' | 'group';
|
|
114
|
+
readonly id: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeConnection(
|
|
118
|
+
connection: string | undefined,
|
|
119
|
+
): 'ws' | 'wss' | 'http' {
|
|
120
|
+
if (connection === 'wss' || connection === 'http') return connection;
|
|
121
|
+
return 'ws';
|
|
122
|
+
}
|
|
123
|
+
|
|
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);
|
|
128
|
+
|
|
129
|
+
if (connection === 'ws') {
|
|
130
|
+
const url = requiredEndpointField(config.url, 'url');
|
|
131
|
+
return {
|
|
132
|
+
context: 'napcat',
|
|
133
|
+
connection: 'ws',
|
|
134
|
+
id,
|
|
135
|
+
access_token,
|
|
136
|
+
url,
|
|
137
|
+
reconnect_interval: config.reconnect_interval ?? 5000,
|
|
138
|
+
heartbeat_interval: config.heartbeat_interval ?? 30_000,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (connection === 'wss') {
|
|
143
|
+
const path = requiredEndpointField(config.path, 'path');
|
|
144
|
+
return {
|
|
145
|
+
context: 'napcat',
|
|
146
|
+
connection: 'wss',
|
|
147
|
+
id,
|
|
148
|
+
access_token,
|
|
149
|
+
path,
|
|
150
|
+
heartbeat_interval: config.heartbeat_interval ?? 30_000,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (connection === 'http') {
|
|
155
|
+
const http_url = requiredEndpointField(config.http_url, 'http_url');
|
|
156
|
+
const post_path = requiredEndpointField(config.post_path, 'post_path');
|
|
157
|
+
return {
|
|
158
|
+
context: 'napcat',
|
|
159
|
+
connection: 'http',
|
|
160
|
+
id,
|
|
161
|
+
access_token,
|
|
162
|
+
http_url,
|
|
163
|
+
post_path,
|
|
164
|
+
poll_interval: config.poll_interval ?? 30_000,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new TypeError(`Unknown NapCat connection: ${String(connection)}`);
|
|
169
|
+
}
|
|
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
|
+
|
|
186
|
+
export function isMessageEvent(
|
|
187
|
+
ev: NapCatEvent,
|
|
188
|
+
): ev is NapCatMessageEvent {
|
|
189
|
+
return (ev.post_type === 'message' || ev.post_type === 'message_sent')
|
|
190
|
+
&& ev.message_id != null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getChannelId(ev: NapCatEvent): string {
|
|
194
|
+
if (ev.message_type === 'group' && ev.group_id != null) return String(ev.group_id);
|
|
195
|
+
if (ev.group_id != null && ev.message_type !== 'private') return String(ev.group_id);
|
|
196
|
+
if (ev.user_id != null) return String(ev.user_id);
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 入站归一化 → ConversationRef:群消息 → kind 'group';私聊临时会话
|
|
202
|
+
* (sub_type 'group')→ kind 'private' + 群容器进 `parent`;其余私聊 → 'private'。
|
|
203
|
+
*/
|
|
204
|
+
export function napcatInboundConversation(endpointKey: string, ev: NapCatEvent): ConversationRef {
|
|
205
|
+
const endpoint = { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey };
|
|
206
|
+
const isGroup = ev.message_type === 'group' || (ev.group_id != null && ev.message_type !== 'private');
|
|
207
|
+
if (isGroup && ev.group_id != null) {
|
|
208
|
+
return { endpoint, kind: 'group', id: String(ev.group_id) };
|
|
209
|
+
}
|
|
210
|
+
if (ev.sub_type === 'group' && ev.group_id != null) {
|
|
211
|
+
return {
|
|
212
|
+
endpoint,
|
|
213
|
+
kind: 'private',
|
|
214
|
+
id: ev.user_id != null ? String(ev.user_id) : '',
|
|
215
|
+
parent: { kind: 'group', id: String(ev.group_id) },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { endpoint, kind: 'private', id: ev.user_id != null ? String(ev.user_id) : '' };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 出站:ConversationRef → OneBot 私聊/群聊目标。 */
|
|
222
|
+
export function napcatOutboundTarget(conversation: ConversationRef): ParsedSendTarget {
|
|
223
|
+
return {
|
|
224
|
+
message_type: conversation.kind === 'group' ? 'group' : 'private',
|
|
225
|
+
id: conversation.id,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function formatInboundContent(ev: NapCatEvent): string {
|
|
230
|
+
if (Array.isArray(ev.message)) {
|
|
231
|
+
return ev.message
|
|
232
|
+
.map((seg) => (seg.type === 'text' ? String(seg.data?.text ?? '') : ''))
|
|
233
|
+
.join('');
|
|
234
|
+
}
|
|
235
|
+
if (typeof ev.message === 'string') return stripCqCodes(ev.message).trim();
|
|
236
|
+
return typeof ev.raw_message === 'string'
|
|
237
|
+
? stripCqCodes(ev.raw_message).trim()
|
|
238
|
+
: '';
|
|
239
|
+
}
|
|
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
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
|
|
257
|
+
* 显示名走 metadata.nickname,见 senderNickname。
|
|
258
|
+
*/
|
|
259
|
+
export function senderUserId(ev: NapCatEvent): string {
|
|
260
|
+
return ev.user_id != null ? String(ev.user_id) : '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 显示名(群名片 card 优先于 nickname),没有则 undefined。 */
|
|
264
|
+
export function senderNickname(ev: NapCatEvent): string | undefined {
|
|
265
|
+
const name = ev.sender?.card || ev.sender?.nickname;
|
|
266
|
+
return typeof name === 'string' && name ? name : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* canonical MediaRef → OneBot `file` 参数:url 直传、base64 → `base64://`、
|
|
271
|
+
* 本地路径 → `file://`(路径在 OneBot 实现侧解析)、kind=file 不透明引用原样透传。
|
|
272
|
+
*/
|
|
273
|
+
export function mediaRefToOneBotFile(media: MediaRef): string {
|
|
274
|
+
if (media.kind === 'base64') {
|
|
275
|
+
return media.value.startsWith('base64://') ? media.value : `base64://${media.value}`;
|
|
276
|
+
}
|
|
277
|
+
if (media.kind === 'path') {
|
|
278
|
+
return media.value.startsWith('file://') ? media.value : `file://${media.value}`;
|
|
279
|
+
}
|
|
280
|
+
return media.value;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 媒体段 data 里的 canonical-only 字段,不进 OneBot wire。 */
|
|
284
|
+
const MEDIA_DATA_SKIP_KEYS = new Set(['media', 'alt', 'url', 'base64', 'file', 'mime_type']);
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 媒体段 → OneBot wire:媒体来源唯一认 canonical `data.media`(MediaRef-only,
|
|
288
|
+
* 无 legacy 字段回读)。缺失或形状非法时 warn + 丢弃(返回 null)。
|
|
289
|
+
*/
|
|
290
|
+
function oneBotMediaSegment(
|
|
291
|
+
type: 'image' | 'record' | 'video',
|
|
292
|
+
data: Record<string, unknown>,
|
|
293
|
+
): MessageSegment | null {
|
|
294
|
+
if (!isMediaRef(data.media)) {
|
|
295
|
+
logger.warn(formatCompact({
|
|
296
|
+
op: 'napcat_outbound_media_dropped',
|
|
297
|
+
type,
|
|
298
|
+
reason: 'missing_media_ref',
|
|
299
|
+
}));
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const extra: Record<string, unknown> = {};
|
|
303
|
+
for (const [key, value] of Object.entries(data)) {
|
|
304
|
+
if (!MEDIA_DATA_SKIP_KEYS.has(key)) extra[key] = value;
|
|
305
|
+
}
|
|
306
|
+
return { type, data: { ...extra, file: mediaRefToOneBotFile(data.media) } };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* canonical Segment → OneBot 11 数组段:
|
|
311
|
+
* - mention → at(`qq: target`);
|
|
312
|
+
* - reply(`message_id`)→ reply(`id`);
|
|
313
|
+
* - image / audio→record / video 的 MediaRef → `file`(url / base64:// / file://),
|
|
314
|
+
* 无 canonical `data.media` 的媒体段丢弃(返回 null);
|
|
315
|
+
* - face 取 `id`;
|
|
316
|
+
* - 其余(NapCat 扩展段、已是 wire 形状的段)原样透传。
|
|
317
|
+
*/
|
|
318
|
+
function canonicalToOneBotSegment(segment: NapCatWireSegment): MessageSegment | null {
|
|
319
|
+
const data = segment.data ?? {};
|
|
320
|
+
switch (segment.type) {
|
|
321
|
+
case 'mention': {
|
|
322
|
+
const target = data.target ?? data.qq ?? data.id;
|
|
323
|
+
if (target == null) return { type: segment.type, data };
|
|
324
|
+
return { type: 'at', data: { qq: String(target) } };
|
|
325
|
+
}
|
|
326
|
+
case 'reply': {
|
|
327
|
+
const messageId = data.message_id ?? data.id;
|
|
328
|
+
if (messageId == null) return { type: segment.type, data };
|
|
329
|
+
return { type: 'reply', data: { id: String(messageId) } };
|
|
330
|
+
}
|
|
331
|
+
case 'face': {
|
|
332
|
+
if (data.id == null) return { type: segment.type, data };
|
|
333
|
+
return { type: 'face', data: { id: data.id } };
|
|
334
|
+
}
|
|
335
|
+
case 'image':
|
|
336
|
+
case 'record':
|
|
337
|
+
case 'video':
|
|
338
|
+
return oneBotMediaSegment(segment.type, data);
|
|
339
|
+
case 'audio':
|
|
340
|
+
return oneBotMediaSegment('record', data);
|
|
341
|
+
default:
|
|
342
|
+
return { type: segment.type, data };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Wire-encode an already-rendered outbound payload into OneBot message segments.
|
|
348
|
+
* 入参假定已经 core `normalizeOutboundPayload` 归一为 canonical Segment[];
|
|
349
|
+
* 媒体段(image/audio/video)仅认 canonical `data.media`,缺失则 warn + 丢弃;
|
|
350
|
+
* 非媒体的 wire 段(at 等)原样透传。
|
|
351
|
+
*/
|
|
352
|
+
export function formatOutboundSegments(payload: unknown): MessageSegment[] {
|
|
353
|
+
if (typeof payload === 'string') {
|
|
354
|
+
return [{ type: 'text', data: { text: payload } }];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const items: Array<string | NapCatWireSegment> = Array.isArray(payload)
|
|
358
|
+
? payload as Array<string | NapCatWireSegment>
|
|
359
|
+
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
360
|
+
? [payload as NapCatWireSegment]
|
|
361
|
+
: [];
|
|
362
|
+
|
|
363
|
+
if (items.length === 0) {
|
|
364
|
+
const text = payload == null
|
|
365
|
+
? ''
|
|
366
|
+
: typeof payload === 'object'
|
|
367
|
+
? JSON.stringify(payload)
|
|
368
|
+
: String(payload);
|
|
369
|
+
return [{ type: 'text', data: { text } }];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const segs: MessageSegment[] = [];
|
|
373
|
+
for (const item of items) {
|
|
374
|
+
if (typeof item === 'string') {
|
|
375
|
+
segs.push({ type: 'text', data: { text: item } });
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const seg = canonicalToOneBotSegment(item);
|
|
379
|
+
if (seg) segs.push(seg);
|
|
380
|
+
}
|
|
381
|
+
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function buildSendAction(
|
|
385
|
+
target: ParsedSendTarget,
|
|
386
|
+
message: MessageSegment[],
|
|
387
|
+
): { action: string; params: Record<string, unknown> } {
|
|
388
|
+
if (target.message_type === 'group') {
|
|
389
|
+
return {
|
|
390
|
+
action: 'send_group_msg',
|
|
391
|
+
params: {
|
|
392
|
+
group_id: Number(target.id) || target.id,
|
|
393
|
+
message,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
action: 'send_private_msg',
|
|
399
|
+
params: {
|
|
400
|
+
user_id: Number(target.id) || target.id,
|
|
401
|
+
message,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
407
|
+
export function buildWsConnectOptions(config: NapCatWsConfig): {
|
|
408
|
+
readonly url: string;
|
|
409
|
+
readonly headers: Record<string, string>;
|
|
410
|
+
readonly safeUrl: string;
|
|
411
|
+
} {
|
|
412
|
+
const headers: Record<string, string> = {};
|
|
413
|
+
let connectUrl = config.url;
|
|
414
|
+
if (config.access_token) {
|
|
415
|
+
headers.Authorization = `Bearer ${config.access_token}`;
|
|
416
|
+
const url = new URL(config.url);
|
|
417
|
+
url.searchParams.set('access_token', config.access_token);
|
|
418
|
+
connectUrl = url.toString();
|
|
419
|
+
}
|
|
420
|
+
const safeUrl = new URL(connectUrl);
|
|
421
|
+
safeUrl.searchParams.delete('access_token');
|
|
422
|
+
return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export interface NapCatHttpOptions {
|
|
426
|
+
readonly http_url: string;
|
|
427
|
+
readonly access_token?: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 向 NapCat HTTP API 发送动作请求:POST {http_url}/{action}。
|
|
432
|
+
* 供 connection:http 出站与纯协议测试使用。
|
|
433
|
+
*/
|
|
434
|
+
export async function callNapCatHttpAction(
|
|
435
|
+
options: NapCatHttpOptions,
|
|
436
|
+
action: string,
|
|
437
|
+
params: Record<string, unknown> = {},
|
|
438
|
+
): Promise<NapCatActionResponse> {
|
|
439
|
+
const base = options.http_url.replace(/\/$/, '');
|
|
440
|
+
const url = `${base}/${action}`;
|
|
441
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
442
|
+
if (options.access_token) {
|
|
443
|
+
headers.Authorization = `Bearer ${options.access_token}`;
|
|
444
|
+
}
|
|
445
|
+
const res = await fetch(url, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
headers,
|
|
448
|
+
body: JSON.stringify(params),
|
|
449
|
+
});
|
|
450
|
+
const text = await res.text();
|
|
451
|
+
if (res.status === 401) throw new Error(`NapCat HTTP auth failed: ${text}`);
|
|
452
|
+
if (res.status !== 200) throw new Error(`NapCat HTTP ${res.status}: ${text}`);
|
|
453
|
+
let data: NapCatActionResponse;
|
|
454
|
+
try {
|
|
455
|
+
data = JSON.parse(text) as NapCatActionResponse;
|
|
456
|
+
} catch {
|
|
457
|
+
throw new Error(`NapCat HTTP invalid response: ${text.slice(0, 200)}`);
|
|
458
|
+
}
|
|
459
|
+
if (data.status !== 'ok' && data.retcode !== 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`NapCat HTTP action failed [${data.retcode}]: ${data.message || data.wording || 'unknown'}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return data;
|
|
465
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { receiveOneBotLikeSideEvent } from '@zhin.js/core';
|
|
2
|
+
import type { EndpointEventEmitter } from 'zhin.js/adapter';
|
|
3
|
+
import { formatCompact, type getAdapterLogger } from '@zhin.js/logger';
|
|
4
|
+
import type { NapCatEvent } from './protocol.js';
|
|
5
|
+
|
|
6
|
+
export interface NapCatSideEventCaller {
|
|
7
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function receiveNapCatSideEvent(
|
|
11
|
+
emit: EndpointEventEmitter,
|
|
12
|
+
endpointKey: string,
|
|
13
|
+
caller: NapCatSideEventCaller,
|
|
14
|
+
raw: NapCatEvent,
|
|
15
|
+
logger: ReturnType<typeof getAdapterLogger>,
|
|
16
|
+
): void {
|
|
17
|
+
if (!emit) return;
|
|
18
|
+
const record = raw as Record<string, unknown>;
|
|
19
|
+
const postType = String(record.post_type ?? '');
|
|
20
|
+
const requestType = String(record.request_type ?? '');
|
|
21
|
+
const isRequest = postType === 'request' || postType.startsWith('request.');
|
|
22
|
+
const isFriend = requestType === 'friend' || postType.includes('friend');
|
|
23
|
+
void receiveOneBotLikeSideEvent(emit, {
|
|
24
|
+
adapter: 'napcat',
|
|
25
|
+
endpointKey,
|
|
26
|
+
platform: 'napcat',
|
|
27
|
+
raw: record,
|
|
28
|
+
...(isRequest ? {
|
|
29
|
+
approve: async (flag, remark) => {
|
|
30
|
+
await (isFriend
|
|
31
|
+
? caller.callApi('set_friend_add_request', { flag, approve: true, remark })
|
|
32
|
+
: caller.callApi('set_group_add_request', { flag, approve: true, reason: remark }));
|
|
33
|
+
},
|
|
34
|
+
reject: async (flag, reason) => {
|
|
35
|
+
await (isFriend
|
|
36
|
+
? caller.callApi('set_friend_add_request', { flag, approve: false })
|
|
37
|
+
: caller.callApi('set_group_add_request', { flag, approve: false, reason }));
|
|
38
|
+
},
|
|
39
|
+
} : {}),
|
|
40
|
+
}).catch((err) => {
|
|
41
|
+
logger.warn(formatCompact({
|
|
42
|
+
op: 'napcat_side_event_failed',
|
|
43
|
+
endpoint: endpointKey,
|
|
44
|
+
post_type: postType || undefined,
|
|
45
|
+
error: err instanceof Error ? err.message : String(err),
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
|
|
3
|
+
export async function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
4
|
+
const chunks: Buffer[] = [];
|
|
5
|
+
let size = 0;
|
|
6
|
+
for await (const chunk of request) {
|
|
7
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
8
|
+
size += buffer.length;
|
|
9
|
+
if (size > 1_048_576) {
|
|
10
|
+
request.destroy();
|
|
11
|
+
throw new Error('Request body exceeds 1MB');
|
|
12
|
+
}
|
|
13
|
+
chunks.push(buffer);
|
|
14
|
+
}
|
|
15
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
16
|
+
}
|