@zhin.js/adapter-napcat 1.1.25 → 1.1.27
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 +201 -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/console-spec.js +52 -5
- 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/console-spec.ts +52 -5
- 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
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { messages as t } from "../../../../messages.js";
|
|
3
|
+
import { currentTarget } from "../../../../conversation.js";
|
|
3
4
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
export default defineAgentTool({
|
|
6
|
-
description: '
|
|
7
|
+
description: '在当前会话戳一戳:群聊中戳 user_id 指定的成员,私聊中戳对方。',
|
|
7
8
|
inputSchema: z.object({
|
|
8
|
-
user_id: z.number().describe('
|
|
9
|
-
group_id: z.number().optional().describe('群号(不传则为私聊戳一戳)'),
|
|
9
|
+
user_id: z.number().optional().describe('群聊中要戳的成员 QQ 号(私聊中忽略,固定为对方)'),
|
|
10
10
|
}),
|
|
11
11
|
adapter: 'napcat',
|
|
12
12
|
tags: ['napcat', 'qq'],
|
|
13
13
|
keywords: ['戳一戳', 'poke', '戳', '拍一拍'],
|
|
14
|
-
async execute({ user_id
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
async execute({ user_id }, context) {
|
|
15
|
+
const target = currentTarget(context);
|
|
16
|
+
const userId = target.type === 'group' ? user_id : target.id;
|
|
17
|
+
if (userId === undefined)
|
|
18
|
+
return { success: false, message: t(context.locale, 'pokeTargetRequired') };
|
|
19
|
+
await context.$client.sendPoke(userId, target.type === 'group' ? target.id : undefined);
|
|
20
|
+
return { success: true, message: t(context.locale, 'userPoked', { p0: userId }) };
|
|
18
21
|
},
|
|
19
22
|
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { messages as t } from "../../../../messages.js";
|
|
2
|
+
import { currentTarget } from "../../../../conversation.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
|
|
5
|
-
export default defineAgentTool<{ user_id
|
|
6
|
-
description: '
|
|
6
|
+
export default defineAgentTool<{ user_id?: number }>({
|
|
7
|
+
description: '在当前会话戳一戳:群聊中戳 user_id 指定的成员,私聊中戳对方。',
|
|
7
8
|
inputSchema: z.object({
|
|
8
|
-
user_id: z.number().describe('
|
|
9
|
-
group_id: z.number().optional().describe('群号(不传则为私聊戳一戳)'),
|
|
9
|
+
user_id: z.number().optional().describe('群聊中要戳的成员 QQ 号(私聊中忽略,固定为对方)'),
|
|
10
10
|
}),
|
|
11
11
|
adapter: 'napcat',
|
|
12
12
|
tags: ['napcat', 'qq'],
|
|
13
13
|
keywords: ['戳一戳', 'poke', '戳', '拍一拍'],
|
|
14
|
-
async execute({ user_id
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
async execute({ user_id }, context) {
|
|
15
|
+
const target = currentTarget(context);
|
|
16
|
+
const userId = target.type === 'group' ? user_id : target.id;
|
|
17
|
+
if (userId === undefined) return { success: false, message: t(context.locale, 'pokeTargetRequired') };
|
|
18
|
+
await context.$client.sendPoke(userId, target.type === 'group' ? target.id : undefined);
|
|
19
|
+
return { success: true, message: t(context.locale, 'userPoked', { p0: userId }) };
|
|
18
20
|
},
|
|
19
21
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { currentPrivatePeer } from "../../../../conversation.js";
|
|
2
3
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
export default defineAgentTool({
|
|
5
|
-
description: '
|
|
6
|
+
description: '获取当前私聊的消息历史记录。',
|
|
6
7
|
inputSchema: z.object({
|
|
7
|
-
user_id: z.number().describe('QQ 号'),
|
|
8
8
|
message_seq: z.number().optional().describe('起始消息序号(可选)'),
|
|
9
9
|
count: z.number().optional().describe('获取条数(可选)'),
|
|
10
10
|
}),
|
|
@@ -12,8 +12,7 @@ export default defineAgentTool({
|
|
|
12
12
|
scopes: ["private"],
|
|
13
13
|
tags: ['napcat', 'qq'],
|
|
14
14
|
keywords: ['私聊记录', '好友聊天记录', 'friend history'],
|
|
15
|
-
async execute({
|
|
16
|
-
|
|
17
|
-
return endpoint.getFriendMsgHistory(user_id, message_seq, count);
|
|
15
|
+
async execute({ message_seq, count }, context) {
|
|
16
|
+
return context.$client.getFriendMsgHistory(currentPrivatePeer(context), message_seq, count);
|
|
18
17
|
},
|
|
19
18
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { currentPrivatePeer } from "../../../../conversation.js";
|
|
1
2
|
import { defineAgentTool } from '@zhin.js/tool';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
|
-
export default defineAgentTool<{
|
|
5
|
-
description: '
|
|
5
|
+
export default defineAgentTool<{ message_seq?: number; count?: number }>({
|
|
6
|
+
description: '获取当前私聊的消息历史记录。',
|
|
6
7
|
inputSchema: z.object({
|
|
7
|
-
user_id: z.number().describe('QQ 号'),
|
|
8
8
|
message_seq: z.number().optional().describe('起始消息序号(可选)'),
|
|
9
9
|
count: z.number().optional().describe('获取条数(可选)'),
|
|
10
10
|
}),
|
|
@@ -12,8 +12,7 @@ export default defineAgentTool<{ user_id: number; message_seq?: number; count?:
|
|
|
12
12
|
scopes: ["private"],
|
|
13
13
|
tags: ['napcat', 'qq'],
|
|
14
14
|
keywords: ['私聊记录', '好友聊天记录', 'friend history'],
|
|
15
|
-
async execute({
|
|
16
|
-
|
|
17
|
-
return endpoint.getFriendMsgHistory(user_id, message_seq, count);
|
|
15
|
+
async execute({ message_seq, count }, context) {
|
|
16
|
+
return context.$client.getFriendMsgHistory(currentPrivatePeer(context), message_seq, count);
|
|
18
17
|
},
|
|
19
18
|
});
|
package/lib/console-spec.js
CHANGED
|
@@ -2,11 +2,58 @@ export const napcatConsoleSpec = {
|
|
|
2
2
|
adapterKey: 'napcat',
|
|
3
3
|
adapterDisplayName: 'NapCat',
|
|
4
4
|
fields: [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
{
|
|
6
|
+
key: 'url',
|
|
7
|
+
label: '正向 WebSocket 地址',
|
|
8
|
+
labelI18n: { en: 'Forward WebSocket URL' },
|
|
9
|
+
group: '连接模式',
|
|
10
|
+
groupI18n: { en: 'Connection Mode' },
|
|
11
|
+
placeholder: 'ws://127.0.0.1:3001',
|
|
12
|
+
description: 'NapCat WebSocket URL(connection: ws 必填)',
|
|
13
|
+
descriptionI18n: { en: 'NapCat WebSocket URL (required for connection: ws)' },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'path',
|
|
17
|
+
label: '反向 WebSocket 路径',
|
|
18
|
+
labelI18n: { en: 'Reverse WebSocket Path' },
|
|
19
|
+
group: '连接模式',
|
|
20
|
+
groupI18n: { en: 'Connection Mode' },
|
|
21
|
+
placeholder: '/napcat/ws',
|
|
22
|
+
description: 'reverse-wss 路径(connection: wss)',
|
|
23
|
+
descriptionI18n: { en: 'Reverse WebSocket path (connection: wss)' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'http_url',
|
|
27
|
+
label: 'HTTP API 地址',
|
|
28
|
+
labelI18n: { en: 'HTTP API Base URL' },
|
|
29
|
+
group: '连接模式',
|
|
30
|
+
groupI18n: { en: 'Connection Mode' },
|
|
31
|
+
placeholder: 'http://127.0.0.1:3000',
|
|
32
|
+
description: 'HTTP API base URL(connection: http 出站)',
|
|
33
|
+
descriptionI18n: { en: 'HTTP API base URL (outbound connection: http)' },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'post_path',
|
|
37
|
+
label: 'HTTP 事件上报路径',
|
|
38
|
+
labelI18n: { en: 'HTTP Event Post Path' },
|
|
39
|
+
group: '连接模式',
|
|
40
|
+
groupI18n: { en: 'Connection Mode' },
|
|
41
|
+
placeholder: '/napcat/event',
|
|
42
|
+
description: 'HTTP POST 事件路径(connection: http 入站)',
|
|
43
|
+
descriptionI18n: { en: 'HTTP POST event path (inbound connection: http)' },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'access_token',
|
|
47
|
+
env: true,
|
|
48
|
+
label: 'Access Token (访问令牌)',
|
|
49
|
+
labelI18n: { en: 'Access Token' },
|
|
50
|
+
group: '安全认证',
|
|
51
|
+
groupI18n: { en: 'Authentication' },
|
|
52
|
+
placeholder: '与 NapCat 服务端一致的 Token',
|
|
53
|
+
placeholderI18n: { en: 'Token matching NapCat server config' },
|
|
54
|
+
description: 'NapCat 接口鉴权令牌(Access Token)',
|
|
55
|
+
descriptionI18n: { en: 'NapCat access token' },
|
|
56
|
+
},
|
|
10
57
|
],
|
|
11
58
|
docsUrl: 'https://zhin.js.org/adapters/napcat',
|
|
12
59
|
readPermission: 'napcat:read',
|
|
@@ -1,5 +1,42 @@
|
|
|
1
|
+
import { standingFromMemberRole } from '@zhin.js/im-contract';
|
|
2
|
+
/** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
|
|
3
|
+
const SELF_ID_TTL_MS = 5_000;
|
|
1
4
|
export function createNapCatEndpointManagement(endpoint) {
|
|
5
|
+
// The login account can change across reconnects (or a different account on the same
|
|
6
|
+
// connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
|
|
7
|
+
// to coalesce bursts, and never across a failed lookup.
|
|
8
|
+
let login;
|
|
9
|
+
const loginId = async (signal) => {
|
|
10
|
+
if (login && Date.now() - login.at < SELF_ID_TTL_MS)
|
|
11
|
+
return login.id;
|
|
12
|
+
login = undefined;
|
|
13
|
+
const info = asRecord(await endpoint.requestApi({ action: 'get_login_info', signal }));
|
|
14
|
+
const id = info.user_id;
|
|
15
|
+
const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
|
|
16
|
+
if (value)
|
|
17
|
+
login = { id: value, at: Date.now() };
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
2
20
|
return Object.freeze({
|
|
21
|
+
/** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
|
|
22
|
+
async inspectSelfStanding({ conversation, signal }) {
|
|
23
|
+
if (conversation.kind !== 'group')
|
|
24
|
+
return undefined;
|
|
25
|
+
const selfId = await loginId(signal);
|
|
26
|
+
if (!selfId)
|
|
27
|
+
return undefined;
|
|
28
|
+
let data;
|
|
29
|
+
try {
|
|
30
|
+
data = asRecord(await endpoint.requestApi({ action: 'get_group_member_info', signal, params: {
|
|
31
|
+
group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
|
|
32
|
+
} }));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
login = undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return standingFromMemberRole(data.role);
|
|
39
|
+
},
|
|
3
40
|
async listFriends(request = {}) {
|
|
4
41
|
const data = await endpoint.requestApi({ action: 'get_friend_list', signal: request.signal });
|
|
5
42
|
return toArray(data).map((value) => {
|
package/lib/http-endpoint.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
|
|
3
|
+
*/
|
|
1
4
|
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
2
5
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
3
6
|
import type { CapabilityId } from 'zhin.js';
|
package/lib/http-endpoint.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
2
|
import { toCanonicalSegments } from '@zhin.js/core';
|
|
3
|
+
/**
|
|
4
|
+
* NapCat HTTP endpoint — POST inbound events + HTTP API outbound.
|
|
5
|
+
*/
|
|
3
6
|
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
4
7
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
5
8
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
6
9
|
import { createNapcatSelfProfile } from './self-profile.js';
|
|
7
10
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
8
|
-
import { buildSendAction, callNapCatHttpAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
|
|
11
|
+
import { buildSendAction, callNapCatHttpAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
|
|
9
12
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
10
13
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
11
14
|
import { NapcatClient } from './client.js';
|
|
12
|
-
import {
|
|
15
|
+
import { readTextBody } from '@zhin.js/host-http-contract';
|
|
13
16
|
import { verifyNapCatAccessToken } from './wss-auth.js';
|
|
14
17
|
export class NapCatHttpEndpoint extends NapcatClient {
|
|
15
18
|
[endpointSpec];
|
|
@@ -123,6 +126,7 @@ export class NapCatHttpEndpoint extends NapcatClient {
|
|
|
123
126
|
const mentioned = isNapCatBotMentioned(ev);
|
|
124
127
|
const quoteId = extractQuoteId(ev);
|
|
125
128
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
129
|
+
const platform = napcatSenderStanding(ev);
|
|
126
130
|
void this[endpointSpec].events.message({
|
|
127
131
|
event: nativeEvent,
|
|
128
132
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -133,7 +137,7 @@ export class NapCatHttpEndpoint extends NapcatClient {
|
|
|
133
137
|
sender: {
|
|
134
138
|
id: senderUserId(ev),
|
|
135
139
|
name: nickname,
|
|
136
|
-
...(
|
|
140
|
+
...(platform ? { platform } : {}),
|
|
137
141
|
},
|
|
138
142
|
endpointId: this.#options.config.id,
|
|
139
143
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -157,41 +161,31 @@ export class NapCatHttpEndpoint extends NapcatClient {
|
|
|
157
161
|
}
|
|
158
162
|
#setupRoutes() {
|
|
159
163
|
const path = this.#options.config.post_path;
|
|
160
|
-
this.#routeReleases.push(this.#options.http.route('POST', path,
|
|
161
|
-
await this.#handlePost(request, response);
|
|
162
|
-
}, { summary: 'NapCat HTTP event callback', tags: ['napcat'] }));
|
|
164
|
+
this.#routeReleases.push(this.#options.http.route('POST', path, (request) => this.#handlePost(request), { summary: 'NapCat HTTP event callback', tags: ['napcat'] }));
|
|
163
165
|
}
|
|
164
|
-
async #handlePost(request
|
|
166
|
+
async #handlePost(request) {
|
|
165
167
|
try {
|
|
166
168
|
if (!verifyNapCatAccessToken(this.#options.config.access_token, request)) {
|
|
167
|
-
|
|
168
|
-
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
169
|
-
return;
|
|
169
|
+
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
170
170
|
}
|
|
171
|
-
const raw = await
|
|
171
|
+
const raw = await readTextBody(request);
|
|
172
172
|
let ev;
|
|
173
173
|
try {
|
|
174
174
|
ev = JSON.parse(raw);
|
|
175
175
|
}
|
|
176
176
|
catch {
|
|
177
|
-
|
|
178
|
-
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
179
|
-
return;
|
|
177
|
+
return new Response(JSON.stringify({ message: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
180
178
|
}
|
|
181
179
|
if (this.#open) {
|
|
182
180
|
void this[endpointSpec].events.run(() => this.ingest(ev)).catch((error) => {
|
|
183
181
|
this.#logger.debug(formatCompact({ op: 'napcat_receive_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
184
182
|
});
|
|
185
183
|
}
|
|
186
|
-
|
|
187
|
-
response.end(JSON.stringify({ status: 'ok' }));
|
|
184
|
+
return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
188
185
|
}
|
|
189
186
|
catch (error) {
|
|
190
187
|
this.#logger.error('NapCat HTTP webhook error:', error);
|
|
191
|
-
|
|
192
|
-
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
193
|
-
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
194
|
-
}
|
|
188
|
+
return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
195
189
|
}
|
|
196
190
|
}
|
|
197
191
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -9,4 +9,3 @@ export { NapCatHttpEndpoint, type NapCatHttpEndpointOptions, } from './http-endp
|
|
|
9
9
|
export type { NapCatWsSocket, NapCatWsCreateOptions } from './ws-types.js';
|
|
10
10
|
export { createNapCatRpc, decodeWsPayload, type NapCatRpc, type NapCatRpcOptions, } from './ws-transport.js';
|
|
11
11
|
export { verifyNapCatAccessToken } from './wss-auth.js';
|
|
12
|
-
export { readRequestBody } from './webhook.js';
|
package/lib/index.js
CHANGED
|
@@ -7,4 +7,3 @@ export { NapCatWssEndpoint, } from './wss-endpoint.js';
|
|
|
7
7
|
export { NapCatHttpEndpoint, } from './http-endpoint.js';
|
|
8
8
|
export { createNapCatRpc, decodeWsPayload, } from './ws-transport.js';
|
|
9
9
|
export { verifyNapCatAccessToken } from './wss-auth.js';
|
|
10
|
-
export { readRequestBody } from './webhook.js';
|
package/lib/protocol.d.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 { type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
|
|
8
8
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
9
9
|
export interface NapCatEndpointConfig extends EndpointProfileConfiguration {
|
|
10
10
|
readonly connection?: 'ws' | 'wss' | 'http';
|
|
@@ -116,6 +116,11 @@ export declare function formatInboundContent(ev: NapCatEvent): string;
|
|
|
116
116
|
* 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
|
|
117
117
|
* 显示名走 metadata.nickname,见 senderNickname。
|
|
118
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
121
|
+
* 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
|
|
122
|
+
*/
|
|
123
|
+
export declare function napcatSenderStanding(ev: NapCatEvent): PlatformStanding | undefined;
|
|
119
124
|
export declare function senderUserId(ev: NapCatEvent): string;
|
|
120
125
|
/** 显示名(群名片 card 优先于 nickname),没有则 undefined。 */
|
|
121
126
|
export declare function senderNickname(ev: NapCatEvent): string | undefined;
|
package/lib/protocol.js
CHANGED
|
@@ -149,6 +149,16 @@ function stripCqCodes(input) {
|
|
|
149
149
|
* 入站 sender 语义:必须是用户 ID(Runtime Message contract)。
|
|
150
150
|
* 显示名走 metadata.nickname,见 senderNickname。
|
|
151
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
|
|
154
|
+
* 私聊与未知角色词不上报;OneBot / NapCat 没有平台原生角色与权限位。
|
|
155
|
+
*/
|
|
156
|
+
export function napcatSenderStanding(ev) {
|
|
157
|
+
if (ev.message_type !== 'group')
|
|
158
|
+
return undefined;
|
|
159
|
+
const role = ev.sender?.role;
|
|
160
|
+
return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
|
|
161
|
+
}
|
|
152
162
|
export function senderUserId(ev) {
|
|
153
163
|
return ev.user_id != null ? String(ev.user_id) : '';
|
|
154
164
|
}
|
|
@@ -252,6 +262,9 @@ function canonicalToOneBotSegment(segment) {
|
|
|
252
262
|
return oneBotMediaSegment(segment.type, data);
|
|
253
263
|
case 'audio':
|
|
254
264
|
return oneBotMediaSegment('record', data);
|
|
265
|
+
case 'napcat:node':
|
|
266
|
+
// Agent-authored merged-forward node (napcat_send_forward_msg); NapCat sends `node` arrays as a forward.
|
|
267
|
+
return { type: 'node', data };
|
|
255
268
|
default:
|
|
256
269
|
return { type: segment.type, data };
|
|
257
270
|
}
|
package/lib/ws-endpoint.js
CHANGED
|
@@ -9,7 +9,7 @@ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
|
9
9
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
10
10
|
import { createNapcatSelfProfile } from './self-profile.js';
|
|
11
11
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
12
|
-
import { buildSendAction, buildWsConnectOptions, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
|
|
12
|
+
import { buildSendAction, buildWsConnectOptions, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
|
|
13
13
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
14
14
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
15
15
|
import { NapcatClient } from './client.js';
|
|
@@ -137,6 +137,7 @@ export class NapCatWsEndpoint extends NapcatClient {
|
|
|
137
137
|
const mentioned = isNapCatBotMentioned(ev);
|
|
138
138
|
const quoteId = extractQuoteId(ev);
|
|
139
139
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
140
|
+
const platform = napcatSenderStanding(ev);
|
|
140
141
|
void this[endpointSpec].events.message({
|
|
141
142
|
event: nativeEvent,
|
|
142
143
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -147,7 +148,7 @@ export class NapCatWsEndpoint extends NapcatClient {
|
|
|
147
148
|
sender: {
|
|
148
149
|
id: senderUserId(ev),
|
|
149
150
|
name: nickname,
|
|
150
|
-
...(
|
|
151
|
+
...(platform ? { platform } : {}),
|
|
151
152
|
},
|
|
152
153
|
endpointId: this.#options.config.id,
|
|
153
154
|
...(mentioned ? { mentioned: true } : {}),
|
package/lib/ws-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WsConnection } from '@zhin.js/host-http';
|
|
1
2
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
3
|
export interface NapCatWsSocket {
|
|
3
4
|
readonly readyState: number;
|
|
@@ -11,3 +12,8 @@ export interface NapCatWsCreateOptions {
|
|
|
11
12
|
readonly headers?: Record<string, string>;
|
|
12
13
|
}
|
|
13
14
|
export declare const WS_OPEN = 1;
|
|
15
|
+
/**
|
|
16
|
+
* A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
|
|
17
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function socketOfConnection(connection: WsConnection): NapCatWsSocket;
|
package/lib/ws-types.js
CHANGED
|
@@ -1 +1,32 @@
|
|
|
1
1
|
export const WS_OPEN = 1;
|
|
2
|
+
/**
|
|
3
|
+
* A Host reverse-WebSocket connection seen through the NapCatWsSocket surface. Protocol
|
|
4
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
5
|
+
*/
|
|
6
|
+
export function socketOfConnection(connection) {
|
|
7
|
+
let readyState = 1;
|
|
8
|
+
connection.onClose(() => { readyState = 3; });
|
|
9
|
+
const subscriptions = new Map();
|
|
10
|
+
const off = (_event, listener) => {
|
|
11
|
+
subscriptions.get(listener)?.();
|
|
12
|
+
subscriptions.delete(listener);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
get readyState() { return readyState; },
|
|
16
|
+
send: (data) => connection.send(data),
|
|
17
|
+
close: (code, reason) => connection.close(code, reason),
|
|
18
|
+
on(event, listener) {
|
|
19
|
+
if (event === 'message') {
|
|
20
|
+
subscriptions.set(listener, connection.onMessage((data) => {
|
|
21
|
+
listener(typeof data === 'string' ? data : Buffer.from(data));
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
else if (event === 'close') {
|
|
25
|
+
subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
|
|
26
|
+
}
|
|
27
|
+
// 'error' surfaces as a close on a WsConnection; 'open' already happened.
|
|
28
|
+
},
|
|
29
|
+
off,
|
|
30
|
+
removeListener: off,
|
|
31
|
+
};
|
|
32
|
+
}
|
package/lib/wss-auth.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function verifyNapCatAccessToken(accessToken: string | undefined, request:
|
|
1
|
+
import { type ProtocolAuthRequest } from '@zhin.js/host-http';
|
|
2
|
+
export declare function verifyNapCatAccessToken(accessToken: string | undefined, request: ProtocolAuthRequest): boolean;
|
package/lib/wss-auth.js
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
+
import { verifyProtocolAccessToken } from '@zhin.js/host-http';
|
|
1
2
|
export function verifyNapCatAccessToken(accessToken, request) {
|
|
3
|
+
// Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
|
|
2
4
|
if (!accessToken)
|
|
3
5
|
return true;
|
|
4
|
-
|
|
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;
|
|
6
|
+
return verifyProtocolAccessToken(accessToken, request);
|
|
16
7
|
}
|
package/lib/wss-endpoint.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
|
|
3
3
|
*/
|
|
4
4
|
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
5
|
-
import type
|
|
5
|
+
import { type HttpHost } from '@zhin.js/host-http';
|
|
6
6
|
import type { CapabilityId } from 'zhin.js';
|
|
7
7
|
import { type NapCatWssConfig } from './protocol.js';
|
|
8
8
|
import { NapcatClient } from './client.js';
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -4,15 +4,16 @@ import { toCanonicalSegments } from '@zhin.js/core';
|
|
|
4
4
|
* NapCat reverse WSS endpoint — accepts inbound WebSocket from NapCat.
|
|
5
5
|
*/
|
|
6
6
|
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
7
|
+
import { verifyProtocolAccessToken } from '@zhin.js/host-http';
|
|
7
8
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
9
|
import { createNapCatEndpointManagement } from './endpoint-management.js';
|
|
9
10
|
import { createNapcatSelfProfile } from './self-profile.js';
|
|
10
11
|
import { InboundMessageDeduper, isNapCatBotMentioned, isSelfMessage, normalizeMessage, } from './napcat-inbound.js';
|
|
11
|
-
import { buildSendAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, senderUserId, } from './protocol.js';
|
|
12
|
+
import { buildSendAction, extractQuoteId, formatInboundContent, formatOutboundSegments, isMessageEvent, napcatInboundConversation, napcatOutboundTarget, senderNickname, napcatSenderStanding, senderUserId, } from './protocol.js';
|
|
12
13
|
import { receiveNapCatSideEvent } from './side-event-dispatch.js';
|
|
13
14
|
import { createNapCatContentPort } from './onebot-get-msg.js';
|
|
14
15
|
import { NapcatClient } from './client.js';
|
|
15
|
-
import {
|
|
16
|
+
import { socketOfConnection } from './ws-types.js';
|
|
16
17
|
export class NapCatWssEndpoint extends NapcatClient {
|
|
17
18
|
[endpointSpec];
|
|
18
19
|
#logger;
|
|
@@ -71,7 +72,17 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
71
72
|
return;
|
|
72
73
|
this.#stopped = false;
|
|
73
74
|
this.#started = true;
|
|
74
|
-
|
|
75
|
+
if (!this.#options.config.access_token) {
|
|
76
|
+
// 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
|
|
77
|
+
this.#logger.warn(formatCompact({
|
|
78
|
+
endpoint: this.#options.config.id,
|
|
79
|
+
mode: 'wss',
|
|
80
|
+
ok: false,
|
|
81
|
+
error: 'missing access_token: only loopback peers are accepted',
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
// The peer authenticates with this endpoint's own access_token, not a Console credential.
|
|
85
|
+
const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
|
|
75
86
|
this.#wsRelease = handle.onConnection((connection) => {
|
|
76
87
|
this.#connectionTask = this.#connectionTask
|
|
77
88
|
.then(() => this.#acceptConnection(connection))
|
|
@@ -150,6 +161,7 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
150
161
|
const mentioned = isNapCatBotMentioned(ev);
|
|
151
162
|
const quoteId = extractQuoteId(ev);
|
|
152
163
|
const segments = Array.isArray(ev.message) ? toCanonicalSegments(ev.message) : undefined;
|
|
164
|
+
const platform = napcatSenderStanding(ev);
|
|
153
165
|
void this[endpointSpec].events.message({
|
|
154
166
|
event: nativeEvent,
|
|
155
167
|
...(typeof nativeEvent.time === 'number' ? { platformSentAt: nativeEvent.time * 1000 } : {}),
|
|
@@ -160,7 +172,7 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
160
172
|
sender: {
|
|
161
173
|
id: senderUserId(ev),
|
|
162
174
|
name: nickname,
|
|
163
|
-
...(
|
|
175
|
+
...(platform ? { platform } : {}),
|
|
164
176
|
},
|
|
165
177
|
endpointId: this.#options.config.id,
|
|
166
178
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -184,14 +196,14 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
184
196
|
}
|
|
185
197
|
async #acceptConnection(connection) {
|
|
186
198
|
if (!this.#started || this.#stopped) {
|
|
187
|
-
connection.
|
|
199
|
+
connection.close();
|
|
188
200
|
return;
|
|
189
201
|
}
|
|
190
|
-
if (!
|
|
191
|
-
connection.
|
|
202
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
203
|
+
connection.close(4003, 'Unauthorized');
|
|
192
204
|
return;
|
|
193
205
|
}
|
|
194
|
-
const socket = connection
|
|
206
|
+
const socket = socketOfConnection(connection);
|
|
195
207
|
await this.#connectionLifecycle.stop();
|
|
196
208
|
this.rpc.disconnect('Connection replaced');
|
|
197
209
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
@@ -208,18 +220,7 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
208
220
|
/* ignore */
|
|
209
221
|
}
|
|
210
222
|
});
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
socket.ping?.();
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
/* ignore */
|
|
217
|
-
}
|
|
218
|
-
}, this.#options.config.heartbeat_interval);
|
|
219
|
-
socket.on('pong', () => {
|
|
220
|
-
if (this.transportSocket === socket)
|
|
221
|
-
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
222
|
-
});
|
|
223
|
+
// Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
|
|
223
224
|
socket.on('message', (data) => {
|
|
224
225
|
if (this.transportSocket !== socket || this.#connectionLifecycle.state === 'closed')
|
|
225
226
|
return;
|
|
@@ -242,7 +243,7 @@ export class NapCatWssEndpoint extends NapcatClient {
|
|
|
242
243
|
this.#logger.debug(formatCompact({
|
|
243
244
|
endpoint: this.#options.config.id,
|
|
244
245
|
mode: 'wss',
|
|
245
|
-
peer: connection.
|
|
246
|
+
peer: connection.remoteAddress,
|
|
246
247
|
}));
|
|
247
248
|
}
|
|
248
249
|
}
|