@zhin.js/adapter-napcat 1.1.26 → 1.1.28
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 +206 -0
- package/LICENSE +21 -0
- package/PERMITS.md +17 -3
- package/README.md +10 -6
- package/adapters/napcat/index.js +3 -0
- package/adapters/napcat/index.ts +3 -0
- package/agents/napcat/conversation.js +71 -0
- package/agents/napcat/conversation.ts +76 -0
- package/agents/napcat/media.js +49 -0
- package/agents/napcat/media.ts +44 -0
- package/agents/napcat/messages.js +32 -0
- package/agents/napcat/messages.ts +32 -0
- package/agents/napcat/skills/napcat-account/tools/delete_friend/index.js +1 -1
- package/agents/napcat/skills/napcat-account/tools/delete_friend/index.ts +1 -1
- package/agents/napcat/skills/napcat-account/tools/set_avatar/index.js +3 -1
- package/agents/napcat/skills/napcat-account/tools/set_avatar/index.ts +3 -1
- package/agents/napcat/skills/napcat-files-history/tools/download_file/index.js +3 -1
- package/agents/napcat/skills/napcat-files-history/tools/download_file/index.ts +3 -1
- package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.js +5 -5
- package/agents/napcat/skills/napcat-group-content/tools/del_group_notice/index.ts +6 -6
- package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.js +5 -3
- package/agents/napcat/skills/napcat-group-content/tools/delete_essence_msg/index.ts +6 -4
- package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.js +5 -7
- package/agents/napcat/skills/napcat-group-content/tools/get_essence_list/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.js +5 -7
- package/agents/napcat/skills/napcat-group-content/tools/get_group_notice/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.js +9 -7
- package/agents/napcat/skills/napcat-group-content/tools/send_group_notice/index.ts +10 -9
- package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.js +5 -3
- package/agents/napcat/skills/napcat-group-content/tools/set_essence_msg/index.ts +6 -4
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_file_url/index.js +4 -5
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_file_url/index.ts +5 -6
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_msg_history/index.js +4 -5
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_msg_history/index.ts +5 -6
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_root_files/index.js +5 -7
- package/agents/napcat/skills/napcat-group-files-history/tools/get_group_root_files/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.js +6 -5
- package/agents/napcat/skills/napcat-group-files-history/tools/upload_group_file/index.ts +8 -7
- package/agents/napcat/skills/napcat-group-media/tools/ai_tts/index.js +4 -5
- package/agents/napcat/skills/napcat-group-media/tools/ai_tts/index.ts +5 -6
- package/agents/napcat/skills/napcat-group-media/tools/get_ai_characters/index.js +5 -7
- package/agents/napcat/skills/napcat-group-media/tools/get_ai_characters/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.js +5 -7
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_info_ex/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.js +5 -7
- package/agents/napcat/skills/napcat-group-settings/tools/get_group_shut_list/index.ts +6 -8
- package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.js +5 -7
- package/agents/napcat/skills/napcat-group-settings/tools/group_sign/index.ts +7 -9
- package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.js +6 -5
- package/agents/napcat/skills/napcat-group-settings/tools/set_group_portrait/index.ts +7 -6
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.js +5 -5
- package/agents/napcat/skills/napcat-group-settings/tools/set_title/index.ts +6 -6
- package/agents/napcat/skills/napcat-media/tools/ocr_image/index.js +4 -3
- package/agents/napcat/skills/napcat-media/tools/ocr_image/index.ts +5 -4
- package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.js +3 -1
- package/agents/napcat/skills/napcat-messaging/tools/forward_single_msg/index.ts +3 -1
- package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.js +12 -6
- package/agents/napcat/skills/napcat-messaging/tools/send_forward_msg/index.ts +13 -16
- package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.js +10 -7
- package/agents/napcat/skills/napcat-messaging/tools/send_poke/index.ts +10 -8
- package/agents/napcat/skills/napcat-private-history/tools/get_friend_msg_history/index.js +4 -5
- package/agents/napcat/skills/napcat-private-history/tools/get_friend_msg_history/index.ts +5 -6
- package/lib/endpoint-management.js +37 -0
- package/lib/http-endpoint.d.ts +3 -0
- package/lib/http-endpoint.js +14 -20
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +13 -0
- package/lib/ws-endpoint.js +3 -2
- package/lib/ws-types.d.ts +6 -0
- package/lib/ws-types.js +31 -0
- package/lib/wss-auth.d.ts +2 -2
- package/lib/wss-auth.js +3 -12
- package/lib/wss-endpoint.d.ts +1 -1
- package/lib/wss-endpoint.js +22 -21
- package/package.json +24 -28
- package/plugin.js +0 -5
- package/schema.json +0 -56
- package/src/endpoint-management.ts +33 -0
- package/src/http-endpoint.ts +11 -20
- package/src/index.ts +0 -2
- package/src/protocol.ts +14 -1
- package/src/ws-endpoint.ts +3 -1
- package/src/ws-types.ts +33 -0
- package/src/wss-auth.ts +4 -11
- package/src/wss-endpoint.ts +22 -22
- package/lib/webhook.d.ts +0 -2
- package/lib/webhook.js +0 -14
- package/src/webhook.ts +0 -16
package/plugin.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { definePlugin } from 'zhin.js';
|
|
3
|
-
import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
|
|
4
3
|
import { createNapcatConsoleRpc } from "./lib/console.js";
|
|
5
4
|
export default definePlugin({
|
|
6
5
|
name: 'napcat',
|
|
@@ -9,12 +8,8 @@ export default definePlugin({
|
|
|
9
8
|
displayName: 'NapCat Adapter',
|
|
10
9
|
},
|
|
11
10
|
setup(context) {
|
|
12
|
-
const dispose = context.resources.has(permissionHostToken)
|
|
13
|
-
? context.resources.use(permissionHostToken).registerPlatform('napcat', createSceneRolePlatformChecker())
|
|
14
|
-
: undefined;
|
|
15
11
|
return {
|
|
16
12
|
consoleRpc: createNapcatConsoleRpc(context.resources, () => context.config.get()),
|
|
17
|
-
...(dispose ? { dispose } : {}),
|
|
18
13
|
};
|
|
19
14
|
},
|
|
20
15
|
});
|
package/schema.json
CHANGED
|
@@ -25,34 +25,6 @@
|
|
|
25
25
|
"type": "number",
|
|
26
26
|
"default": 30000
|
|
27
27
|
},
|
|
28
|
-
"master": {
|
|
29
|
-
"type": [
|
|
30
|
-
"string",
|
|
31
|
-
"number"
|
|
32
|
-
],
|
|
33
|
-
"description": "框架 master(QQ uin;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
|
|
34
|
-
"x-i18n": {
|
|
35
|
-
"en": {
|
|
36
|
-
"description": "Framework masters (QQ uin; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"trusted": {
|
|
41
|
-
"type": "array",
|
|
42
|
-
"items": {
|
|
43
|
-
"type": [
|
|
44
|
-
"string",
|
|
45
|
-
"number"
|
|
46
|
-
],
|
|
47
|
-
"description": "Trusted QQ uin"
|
|
48
|
-
},
|
|
49
|
-
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
|
|
50
|
-
"x-i18n": {
|
|
51
|
-
"en": {
|
|
52
|
-
"description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
28
|
"endpoints": {
|
|
57
29
|
"type": "array",
|
|
58
30
|
"minItems": 1,
|
|
@@ -61,34 +33,6 @@
|
|
|
61
33
|
"type": "object",
|
|
62
34
|
"additionalProperties": false,
|
|
63
35
|
"properties": {
|
|
64
|
-
"master": {
|
|
65
|
-
"type": [
|
|
66
|
-
"string",
|
|
67
|
-
"number"
|
|
68
|
-
],
|
|
69
|
-
"description": "本 endpoint 的框架 master(QQ uin);覆盖顶层 master",
|
|
70
|
-
"x-i18n": {
|
|
71
|
-
"en": {
|
|
72
|
-
"description": "Framework masters for this endpoint (QQ uin), overriding the top-level master list."
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
"trusted": {
|
|
77
|
-
"type": "array",
|
|
78
|
-
"items": {
|
|
79
|
-
"type": [
|
|
80
|
-
"string",
|
|
81
|
-
"number"
|
|
82
|
-
],
|
|
83
|
-
"description": "Trusted QQ uin"
|
|
84
|
-
},
|
|
85
|
-
"description": "本 endpoint 的 trusted 列表",
|
|
86
|
-
"x-i18n": {
|
|
87
|
-
"en": {
|
|
88
|
-
"description": "Trusted users for this endpoint."
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
36
|
"connection": {
|
|
93
37
|
"type": "string",
|
|
94
38
|
"enum": [
|
|
@@ -16,16 +16,49 @@ import {
|
|
|
16
16
|
type EndpointGroupRequest,
|
|
17
17
|
type EndpointGroupNoticeRequest,
|
|
18
18
|
} from 'zhin.js/adapter';
|
|
19
|
+
import { standingFromMemberRole, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
|
|
19
20
|
|
|
20
21
|
/** 管理面只依赖 endpoint 的 callApi(ws/wss/http 三种传输各自实现)。 */
|
|
21
22
|
export interface NapCatManagementCaller {
|
|
22
23
|
requestApi(request: PendingRpcRequest): Promise<unknown>;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
|
|
27
|
+
const SELF_ID_TTL_MS = 5_000;
|
|
28
|
+
|
|
25
29
|
export function createNapCatEndpointManagement(
|
|
26
30
|
endpoint: NapCatManagementCaller,
|
|
27
31
|
): EndpointManagement {
|
|
32
|
+
// The login account can change across reconnects (or a different account on the same
|
|
33
|
+
// connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
|
|
34
|
+
// to coalesce bursts, and never across a failed lookup.
|
|
35
|
+
let login: { readonly id: string; readonly at: number } | undefined;
|
|
36
|
+
const loginId = async (signal: AbortSignal | undefined): Promise<string | undefined> => {
|
|
37
|
+
if (login && Date.now() - login.at < SELF_ID_TTL_MS) return login.id;
|
|
38
|
+
login = undefined;
|
|
39
|
+
const info = asRecord(await endpoint.requestApi({ action: 'get_login_info', signal }));
|
|
40
|
+
const id = info.user_id;
|
|
41
|
+
const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
|
|
42
|
+
if (value) login = { id: value, at: Date.now() };
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
28
45
|
return Object.freeze<EndpointManagement>({
|
|
46
|
+
/** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
|
|
47
|
+
async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
|
|
48
|
+
if (conversation.kind !== 'group') return undefined;
|
|
49
|
+
const selfId = await loginId(signal);
|
|
50
|
+
if (!selfId) return undefined;
|
|
51
|
+
let data: Record<string, unknown>;
|
|
52
|
+
try {
|
|
53
|
+
data = asRecord(await endpoint.requestApi({ action: 'get_group_member_info', signal, params: {
|
|
54
|
+
group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
|
|
55
|
+
} }));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
login = undefined;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
return standingFromMemberRole(data.role);
|
|
61
|
+
},
|
|
29
62
|
async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
|
|
30
63
|
const data = await endpoint.requestApi({ action: 'get_friend_list', signal: request.signal });
|
|
31
64
|
return toArray(data).map((value) => {
|
package/src/http-endpoint.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { toCanonicalSegments } from '@zhin.js/core';
|
|
|
3
3
|
/**
|
|
4
4
|
* NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
|
|
5
5
|
*/
|
|
6
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
6
|
import {
|
|
8
7
|
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
9
8
|
createRecallEndpointControl,
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
napcatInboundConversation,
|
|
34
33
|
napcatOutboundTarget,
|
|
35
34
|
senderNickname,
|
|
35
|
+
napcatSenderStanding,
|
|
36
36
|
senderUserId,
|
|
37
37
|
type NapCatEvent,
|
|
38
38
|
type NapCatHttpConfig,
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
41
41
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
42
42
|
import { NapcatClient } from './client.js';
|
|
43
|
-
import {
|
|
43
|
+
import { readTextBody } from '@zhin.js/host-http-contract';
|
|
44
44
|
import { verifyNapCatAccessToken } from './wss-auth.js';
|
|
45
45
|
|
|
46
46
|
export interface NapCatHttpEndpointOptions {
|
|
@@ -175,6 +175,7 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
|
|
|
175
175
|
const mentioned = isNapCatBotMentioned(ev);
|
|
176
176
|
const quoteId = extractQuoteId(ev);
|
|
177
177
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
178
|
+
const platform = napcatSenderStanding(ev);
|
|
178
179
|
void this[endpointSpec].events.message({
|
|
179
180
|
event: nativeEvent,
|
|
180
181
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -185,7 +186,7 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
|
|
|
185
186
|
sender: {
|
|
186
187
|
id: senderUserId(ev),
|
|
187
188
|
name: nickname,
|
|
188
|
-
...(
|
|
189
|
+
...(platform ? { platform } : {}),
|
|
189
190
|
},
|
|
190
191
|
endpointId: this.#options.config.id,
|
|
191
192
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -211,41 +212,31 @@ export class NapCatHttpEndpoint extends NapcatClient implements Endpoint {
|
|
|
211
212
|
#setupRoutes(): void {
|
|
212
213
|
const path = this.#options.config.post_path;
|
|
213
214
|
this.#routeReleases.push(
|
|
214
|
-
this.#options.http.route('POST', path,
|
|
215
|
-
await this.#handlePost(request, response);
|
|
216
|
-
}, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
|
|
215
|
+
this.#options.http.route('POST', path, (request) => this.#handlePost(request), { summary: 'NapCat HTTP event callback', tags: ['napcat'] }),
|
|
217
216
|
);
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
async #handlePost(request:
|
|
219
|
+
async #handlePost(request: Request): Promise<Response> {
|
|
221
220
|
try {
|
|
222
221
|
if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
|
|
223
|
-
|
|
224
|
-
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
225
|
-
return;
|
|
222
|
+
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
226
223
|
}
|
|
227
|
-
const raw = await
|
|
224
|
+
const raw = await readTextBody(request);
|
|
228
225
|
let ev: NapCatEvent;
|
|
229
226
|
try {
|
|
230
227
|
ev = JSON.parse(raw) as NapCatEvent;
|
|
231
228
|
} catch {
|
|
232
|
-
|
|
233
|
-
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
234
|
-
return;
|
|
229
|
+
return new Response(JSON.stringify({ message: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
235
230
|
}
|
|
236
231
|
if (this.#open) {
|
|
237
232
|
void this[endpointSpec].events.run(() => this.ingest(ev)).catch((error: unknown) => {
|
|
238
233
|
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
239
234
|
});
|
|
240
235
|
}
|
|
241
|
-
|
|
242
|
-
response.end(JSON.stringify({ status: 'ok' }));
|
|
236
|
+
return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
243
237
|
} catch (error) {
|
|
244
238
|
this.#logger.error('NapCat HTTP webhook error:', error);
|
|
245
|
-
|
|
246
|
-
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
247
|
-
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
248
|
-
}
|
|
239
|
+
return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
249
240
|
}
|
|
250
241
|
}
|
|
251
242
|
}
|
package/src/index.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
4
4
|
* No legacy Adapter/Endpoint / segment-mapper.
|
|
5
5
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
6
6
|
*/
|
|
7
|
-
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
|
|
10
10
|
const logger = getLogger('napcat');
|
|
@@ -273,6 +273,16 @@ function stripCqCodes(input: string): string {
|
|
|
273
273
|
* 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
|
|
274
274
|
* 显示名走 metadata.nickname,见 senderNickname。
|
|
275
275
|
*/
|
|
276
|
+
/**
|
|
277
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
278
|
+
* 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
|
|
279
|
+
*/
|
|
280
|
+
export function napcatSenderStanding(ev: NapCatEvent): PlatformStanding | undefined {
|
|
281
|
+
if (ev.message_type !== 'group') return undefined;
|
|
282
|
+
const role = ev.sender?.role;
|
|
283
|
+
return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
276
286
|
export function senderUserId(ev: NapCatEvent): string {
|
|
277
287
|
return ev.user_id != null ? String(ev.user_id) : '';
|
|
278
288
|
}
|
|
@@ -379,6 +389,9 @@ function canonicalToOneBotSegment(segment: NapCatWireSegment): MessageSegment |
|
|
|
379
389
|
return oneBotMediaSegment(segment.type, data);
|
|
380
390
|
case 'audio':
|
|
381
391
|
return oneBotMediaSegment('record', data);
|
|
392
|
+
case 'napcat:node':
|
|
393
|
+
// Agent-authored merged-forward node (napcat_send_forward_msg); NapCat sends `node` arrays as a forward.
|
|
394
|
+
return { type: 'node', data };
|
|
382
395
|
default:
|
|
383
396
|
return { type: segment.type, data };
|
|
384
397
|
}
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
napcatInboundConversation,
|
|
36
36
|
napcatOutboundTarget,
|
|
37
37
|
senderNickname,
|
|
38
|
+
napcatSenderStanding,
|
|
38
39
|
senderUserId,
|
|
39
40
|
type NapCatEvent,
|
|
40
41
|
type NapCatWsConfig,
|
|
@@ -194,6 +195,7 @@ export class NapCatWsEndpoint extends NapcatClient implements Endpoint {
|
|
|
194
195
|
const mentioned = isNapCatBotMentioned(ev);
|
|
195
196
|
const quoteId = extractQuoteId(ev);
|
|
196
197
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
198
|
+
const platform = napcatSenderStanding(ev);
|
|
197
199
|
void this[endpointSpec].events.message({
|
|
198
200
|
event: nativeEvent,
|
|
199
201
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -204,7 +206,7 @@ export class NapCatWsEndpoint extends NapcatClient implements Endpoint {
|
|
|
204
206
|
sender: {
|
|
205
207
|
id: senderUserId(ev),
|
|
206
208
|
name: nickname,
|
|
207
|
-
...(
|
|
209
|
+
...(platform ? { platform } : {}),
|
|
208
210
|
},
|
|
209
211
|
endpointId: this.#options.config.id,
|
|
210
212
|
...(mentioned ? { mentioned: true } : {}),
|
package/src/ws-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WsConnection } from '@zhin.js/host-http';
|
|
2
|
+
|
|
1
3
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
4
|
export interface NapCatWsSocket {
|
|
3
5
|
readonly readyState: number;
|
|
@@ -13,3 +15,34 @@ export interface NapCatWsCreateOptions {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const WS_OPEN = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
|
|
21
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
22
|
+
*/
|
|
23
|
+
export function socketOfConnection(connection: WsConnection): NapCatWsSocket {
|
|
24
|
+
let readyState = 1;
|
|
25
|
+
connection.onClose(() => { readyState = 3; });
|
|
26
|
+
const subscriptions = new Map<(...args: unknown[]) => void, () => void>();
|
|
27
|
+
const off = (_event: string, listener: (...args: unknown[]) => void): void => {
|
|
28
|
+
subscriptions.get(listener)?.();
|
|
29
|
+
subscriptions.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
get readyState() { return readyState; },
|
|
33
|
+
send: (data: string) => connection.send(data),
|
|
34
|
+
close: (code?: number, reason?: string) => connection.close(code, reason),
|
|
35
|
+
on(event: string, listener: (...args: unknown[]) => void) {
|
|
36
|
+
if (event === 'message') {
|
|
37
|
+
subscriptions.set(listener, connection.onMessage((data) => {
|
|
38
|
+
listener(typeof data === 'string' ? data : Buffer.from(data));
|
|
39
|
+
}));
|
|
40
|
+
} else if (event === 'close') {
|
|
41
|
+
subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
|
|
42
|
+
}
|
|
43
|
+
// 'error' surfaces as a close on a WsConnection; 'open' already happened.
|
|
44
|
+
},
|
|
45
|
+
off,
|
|
46
|
+
removeListener: off,
|
|
47
|
+
} as NapCatWsSocket;
|
|
48
|
+
}
|
package/src/wss-auth.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { verifyProtocolAccessToken, type ProtocolAuthRequest } from '@zhin.js/host-http';
|
|
2
2
|
|
|
3
3
|
export function verifyNapCatAccessToken(
|
|
4
4
|
accessToken: string | undefined,
|
|
5
|
-
request:
|
|
5
|
+
request: ProtocolAuthRequest,
|
|
6
6
|
): boolean {
|
|
7
|
+
// Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
|
|
7
8
|
if (!accessToken) return true;
|
|
8
|
-
|
|
9
|
-
if (auth === `Bearer ${accessToken}`) return true;
|
|
10
|
-
try {
|
|
11
|
-
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
12
|
-
if (url.searchParams.get('access_token') === accessToken) return true;
|
|
13
|
-
} catch {
|
|
14
|
-
/* ignore */
|
|
15
|
-
}
|
|
16
|
-
return false;
|
|
9
|
+
return verifyProtocolAccessToken(accessToken, request);
|
|
17
10
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type EndpointSendRequest,
|
|
14
14
|
type EndpointTransportState,
|
|
15
15
|
} from 'zhin.js/adapter';
|
|
16
|
-
import type
|
|
16
|
+
import { verifyProtocolAccessToken, type HttpHost, type WsConnection } from '@zhin.js/host-http';
|
|
17
17
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
18
18
|
import type { CapabilityId } from 'zhin.js';
|
|
19
19
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
napcatInboundConversation,
|
|
34
34
|
napcatOutboundTarget,
|
|
35
35
|
senderNickname,
|
|
36
|
+
napcatSenderStanding,
|
|
36
37
|
senderUserId,
|
|
37
38
|
type NapCatEvent,
|
|
38
39
|
type NapCatWssConfig,
|
|
@@ -40,10 +41,7 @@ import {
|
|
|
40
41
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
41
42
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
42
43
|
import { NapcatClient } from './client.js';
|
|
43
|
-
import {
|
|
44
|
-
type NapCatWsSocket,
|
|
45
|
-
} from './ws-types.js';
|
|
46
|
-
import { verifyNapCatAccessToken } from './wss-auth.js';
|
|
44
|
+
import { socketOfConnection, type NapCatWsSocket } from './ws-types.js';
|
|
47
45
|
|
|
48
46
|
export interface NapCatWssEndpointOptions {
|
|
49
47
|
readonly id: CapabilityId;
|
|
@@ -110,7 +108,17 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
110
108
|
if (this.#started) return;
|
|
111
109
|
this.#stopped = false;
|
|
112
110
|
this.#started = true;
|
|
113
|
-
|
|
111
|
+
if (!this.#options.config.access_token) {
|
|
112
|
+
// 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
|
|
113
|
+
this.#logger.warn(formatCompact({
|
|
114
|
+
endpoint: this.#options.config.id,
|
|
115
|
+
mode: 'wss',
|
|
116
|
+
ok: false,
|
|
117
|
+
error: 'missing access_token: only loopback peers are accepted',
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
// The peer authenticates with this endpoint's own access_token, not a Console credential.
|
|
121
|
+
const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
|
|
114
122
|
this.#wsRelease = handle.onConnection((connection) => {
|
|
115
123
|
this.#connectionTask = this.#connectionTask
|
|
116
124
|
.then(() => this.#acceptConnection(connection))
|
|
@@ -199,6 +207,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
199
207
|
const mentioned = isNapCatBotMentioned(ev);
|
|
200
208
|
const quoteId = extractQuoteId(ev);
|
|
201
209
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
210
|
+
const platform = napcatSenderStanding(ev);
|
|
202
211
|
void this[endpointSpec].events.message({
|
|
203
212
|
event: nativeEvent,
|
|
204
213
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -209,7 +218,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
209
218
|
sender: {
|
|
210
219
|
id: senderUserId(ev),
|
|
211
220
|
name: nickname,
|
|
212
|
-
...(
|
|
221
|
+
...(platform ? { platform } : {}),
|
|
213
222
|
},
|
|
214
223
|
endpointId: this.#options.config.id,
|
|
215
224
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -234,14 +243,14 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
234
243
|
|
|
235
244
|
async #acceptConnection(connection: WsConnection): Promise<void> {
|
|
236
245
|
if (!this.#started || this.#stopped) {
|
|
237
|
-
connection.
|
|
246
|
+
connection.close();
|
|
238
247
|
return;
|
|
239
248
|
}
|
|
240
|
-
if (!
|
|
241
|
-
connection.
|
|
249
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
250
|
+
connection.close(4003, 'Unauthorized');
|
|
242
251
|
return;
|
|
243
252
|
}
|
|
244
|
-
const socket = connection
|
|
253
|
+
const socket = socketOfConnection(connection);
|
|
245
254
|
await this.#connectionLifecycle.stop();
|
|
246
255
|
this.rpc.disconnect('Connection replaced');
|
|
247
256
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
@@ -257,16 +266,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
257
266
|
/* ignore */
|
|
258
267
|
}
|
|
259
268
|
});
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
socket.ping?.();
|
|
263
|
-
} catch {
|
|
264
|
-
/* ignore */
|
|
265
|
-
}
|
|
266
|
-
}, this.#options.config.heartbeat_interval);
|
|
267
|
-
socket.on('pong', () => {
|
|
268
|
-
if (this.transportSocket === socket) this.#connectionLifecycle.notifyHeartbeatAck();
|
|
269
|
-
});
|
|
269
|
+
// Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
|
|
270
270
|
socket.on('message', (data) => {
|
|
271
271
|
if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed') return;
|
|
272
272
|
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
@@ -286,7 +286,7 @@ export class NapCatWssEndpoint extends NapcatClient implements Endpoint {
|
|
|
286
286
|
this.#logger.debug(formatCompact({
|
|
287
287
|
endpoint: this.#options.config.id,
|
|
288
288
|
mode: 'wss',
|
|
289
|
-
peer: connection.
|
|
289
|
+
peer: connection.remoteAddress,
|
|
290
290
|
}));
|
|
291
291
|
}
|
|
292
292
|
}
|
package/lib/webhook.d.ts
DELETED
package/lib/webhook.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export async function readRequestBody(request) {
|
|
2
|
-
const chunks = [];
|
|
3
|
-
let size = 0;
|
|
4
|
-
for await (const chunk of request) {
|
|
5
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6
|
-
size += buffer.length;
|
|
7
|
-
if (size > 1_048_576) {
|
|
8
|
-
request.destroy();
|
|
9
|
-
throw new Error('Request body exceeds 1MB');
|
|
10
|
-
}
|
|
11
|
-
chunks.push(buffer);
|
|
12
|
-
}
|
|
13
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
14
|
-
}
|
package/src/webhook.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|