@zhin.js/adapter-kook 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 +173 -0
- package/LICENSE +21 -0
- package/PERMITS.md +20 -14
- package/README.md +6 -2
- package/adapters/kook/index.js +3 -0
- package/adapters/kook/index.ts +3 -0
- package/agents/kook/conversation.js +88 -0
- package/agents/kook/conversation.ts +99 -0
- package/agents/kook/messages.js +16 -0
- package/agents/kook/messages.ts +16 -0
- package/agents/kook/skills/kook/tools/blacklist/index.js +9 -6
- package/agents/kook/skills/kook/tools/blacklist/index.ts +9 -7
- package/agents/kook/skills/kook/tools/create_role/index.js +5 -6
- package/agents/kook/skills/kook/tools/create_role/index.ts +6 -7
- package/agents/kook/skills/kook/tools/delete_role/index.js +5 -6
- package/agents/kook/skills/kook/tools/delete_role/index.ts +6 -7
- package/agents/kook/skills/kook/tools/grant_role/index.js +9 -7
- package/agents/kook/skills/kook/tools/grant_role/index.ts +9 -8
- package/agents/kook/skills/kook/tools/list_roles/index.js +5 -7
- package/agents/kook/skills/kook/tools/list_roles/index.ts +6 -8
- package/agents/kook/skills/kook/tools/revoke_role/index.js +9 -7
- package/agents/kook/skills/kook/tools/revoke_role/index.ts +9 -8
- package/lib/console-spec.js +148 -13
- package/lib/endpoint.d.ts +2 -1
- package/lib/endpoint.js +27 -12
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/platform-permit.d.ts +4 -19
- package/lib/platform-permit.js +7 -46
- package/lib/protocol.d.ts +1 -5
- package/lib/protocol.js +3 -28
- package/lib/sender-roles.d.ts +6 -6
- package/lib/sender-roles.js +55 -21
- package/lib/webhook.d.ts +1 -2
- package/lib/webhook.js +13 -33
- package/package.json +23 -26
- package/plugin.js +0 -6
- package/schema.json +0 -56
- package/src/console-spec.ts +148 -13
- package/src/endpoint.ts +27 -13
- package/src/index.ts +1 -7
- package/src/platform-permit.ts +5 -70
- package/src/protocol.ts +4 -33
- package/src/sender-roles.ts +59 -27
- package/src/webhook.ts +14 -36
package/src/console-spec.ts
CHANGED
|
@@ -7,19 +7,154 @@ export const kookConsoleSpec = {
|
|
|
7
7
|
adapterKey: 'kook',
|
|
8
8
|
adapterDisplayName: 'KOOK',
|
|
9
9
|
fields: [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
10
|
+
{
|
|
11
|
+
key: 'token',
|
|
12
|
+
required: true,
|
|
13
|
+
env: true,
|
|
14
|
+
label: 'Bot Token (机器人令牌)',
|
|
15
|
+
labelI18n: { en: 'Bot Token' },
|
|
16
|
+
group: '基础认证',
|
|
17
|
+
groupI18n: { en: 'Basic Auth' },
|
|
18
|
+
placeholder: '1/MTIzNDU=/...',
|
|
19
|
+
description: 'KOOK 开发者中心获取的 Bot Token',
|
|
20
|
+
descriptionI18n: { en: 'KOOK bot token' },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'connection',
|
|
24
|
+
label: '连接模式',
|
|
25
|
+
labelI18n: { en: 'Connection Mode' },
|
|
26
|
+
group: '连接与回调',
|
|
27
|
+
groupI18n: { en: 'Connection & Webhook' },
|
|
28
|
+
placeholder: 'websocket',
|
|
29
|
+
description: 'websocket(默认长连接)或 webhook',
|
|
30
|
+
descriptionI18n: { en: 'websocket or webhook' },
|
|
31
|
+
options: [
|
|
32
|
+
{ value: 'websocket', label: 'WebSocket 长连接(推荐)', labelI18n: { en: 'WebSocket (Recommended)' } },
|
|
33
|
+
{ value: 'webhook', label: 'Webhook 回调', labelI18n: { en: 'Webhook' } },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'encrypt_key',
|
|
38
|
+
env: true,
|
|
39
|
+
label: 'Webhook Encrypt Key',
|
|
40
|
+
labelI18n: { en: 'Webhook Encrypt Key' },
|
|
41
|
+
group: '连接与回调',
|
|
42
|
+
groupI18n: { en: 'Connection & Webhook' },
|
|
43
|
+
placeholder: 'Webhook 模式下的 Encrypt Key',
|
|
44
|
+
placeholderI18n: { en: 'Encrypt key for webhook mode' },
|
|
45
|
+
description: 'webhook 加密密钥(connection: webhook)',
|
|
46
|
+
descriptionI18n: { en: 'Webhook encryption secret' },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: 'verify_token',
|
|
50
|
+
env: true,
|
|
51
|
+
label: 'Webhook Verify Token',
|
|
52
|
+
labelI18n: { en: 'Webhook Verify Token' },
|
|
53
|
+
group: '连接与回调',
|
|
54
|
+
groupI18n: { en: 'Connection & Webhook' },
|
|
55
|
+
placeholder: 'Webhook 模式下的 Verify Token',
|
|
56
|
+
placeholderI18n: { en: 'Verify token for webhook mode' },
|
|
57
|
+
description: 'webhook 验证 token(connection: webhook)',
|
|
58
|
+
descriptionI18n: { en: 'Webhook verification token' },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'webhookPath',
|
|
62
|
+
label: 'Webhook 本地路由',
|
|
63
|
+
labelI18n: { en: 'Webhook Local Path' },
|
|
64
|
+
group: '连接与回调',
|
|
65
|
+
groupI18n: { en: 'Connection & Webhook' },
|
|
66
|
+
placeholder: '/kook/webhook',
|
|
67
|
+
description: '本地监听的 Webhook 路径',
|
|
68
|
+
descriptionI18n: { en: 'Local webhook route' },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'ignore',
|
|
72
|
+
label: '消息忽略策略',
|
|
73
|
+
labelI18n: { en: 'Ignore Policy' },
|
|
74
|
+
group: '流式体验与代理',
|
|
75
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
76
|
+
placeholder: 'bot',
|
|
77
|
+
description: '忽略消息来源:bot(所有机器人)或 self(仅自身)',
|
|
78
|
+
descriptionI18n: { en: 'SDK inbound ignore setting: bot or self' },
|
|
79
|
+
options: [
|
|
80
|
+
{ value: 'bot', label: '忽略所有机器人 (bot)', labelI18n: { en: 'Ignore all bots (bot)' } },
|
|
81
|
+
{ value: 'self', label: '仅忽略自身 (self)', labelI18n: { en: 'Ignore self only (self)' } },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: 'timeout',
|
|
86
|
+
type: 'number',
|
|
87
|
+
label: '请求超时 (ms)',
|
|
88
|
+
labelI18n: { en: 'Request Timeout (ms)' },
|
|
89
|
+
group: '流式体验与代理',
|
|
90
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
91
|
+
placeholder: '10000',
|
|
92
|
+
description: 'API 请求超时时间(毫秒)',
|
|
93
|
+
descriptionI18n: { en: 'SDK request timeout' },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'max_retry',
|
|
97
|
+
type: 'number',
|
|
98
|
+
label: '最大重试次数',
|
|
99
|
+
labelI18n: { en: 'Max Retries' },
|
|
100
|
+
group: '流式体验与代理',
|
|
101
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
102
|
+
placeholder: '3',
|
|
103
|
+
description: 'SDK 请求失败最大重试次数',
|
|
104
|
+
descriptionI18n: { en: 'SDK configured retry count' },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: 'data_dir',
|
|
108
|
+
label: 'SDK 数据目录',
|
|
109
|
+
labelI18n: { en: 'SDK Data Directory' },
|
|
110
|
+
group: '流式体验与代理',
|
|
111
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
112
|
+
placeholder: './data/kook',
|
|
113
|
+
description: 'KOOK SDK 本地缓存目录(可选)',
|
|
114
|
+
descriptionI18n: { en: 'SDK data directory' },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: 'logLevel',
|
|
118
|
+
label: 'SDK 日志级别',
|
|
119
|
+
labelI18n: { en: 'SDK Log Level' },
|
|
120
|
+
group: '流式体验与代理',
|
|
121
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
122
|
+
placeholder: 'info',
|
|
123
|
+
description: 'KOOK SDK 日志级别(info / warn / error / debug)',
|
|
124
|
+
descriptionI18n: { en: 'SDK log level' },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'apiProxy.port',
|
|
128
|
+
type: 'number',
|
|
129
|
+
label: 'API 代理端口',
|
|
130
|
+
labelI18n: { en: 'API Proxy Port' },
|
|
131
|
+
group: '流式体验与代理',
|
|
132
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
133
|
+
placeholder: '例如 7890',
|
|
134
|
+
description: '本地回环 API 代理端口(可选)',
|
|
135
|
+
descriptionI18n: { en: 'Configured API loopback proxy port' },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'streamProxy.port',
|
|
139
|
+
type: 'number',
|
|
140
|
+
label: '网关代理端口',
|
|
141
|
+
labelI18n: { en: 'Gateway Proxy Port' },
|
|
142
|
+
group: '流式体验与代理',
|
|
143
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
144
|
+
placeholder: '例如 7890',
|
|
145
|
+
description: '本地回环 Gateway 代理端口(可选)',
|
|
146
|
+
descriptionI18n: { en: 'Configured Gateway loopback proxy port' },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: 'streamProxy.serverName',
|
|
150
|
+
label: '网关 TLS 主机名',
|
|
151
|
+
labelI18n: { en: 'Gateway TLS Server Name' },
|
|
152
|
+
group: '流式体验与代理',
|
|
153
|
+
groupI18n: { en: 'Streaming & Proxy' },
|
|
154
|
+
placeholder: 'gateway.kookapp.cn',
|
|
155
|
+
description: '代理校验的 Gateway TLS 主机名(可选)',
|
|
156
|
+
descriptionI18n: { en: 'Expected Gateway TLS hostname' },
|
|
157
|
+
},
|
|
23
158
|
] as readonly AdapterConsoleField[],
|
|
24
159
|
docsUrl: 'https://zhin.js.org/adapters/kook',
|
|
25
160
|
readPermission: 'kook:read',
|
package/src/endpoint.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EndpointDeliveryError, type ConversationRef } from '@zhin.js/im-contract';
|
|
1
|
+
import { EndpointDeliveryError, type ConversationRef, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
|
|
2
2
|
/**
|
|
3
3
|
* KookEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
|
|
4
4
|
*/
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
type KookClientTransport,
|
|
41
41
|
} from './ws.js';
|
|
42
42
|
import { recallKookMessage, sendKookOutbound } from './outbound.js';
|
|
43
|
-
import { KookSenderRoleResolver
|
|
43
|
+
import { KookSenderRoleResolver } from './sender-roles.js';
|
|
44
44
|
import { receiveKookSideEvent } from './side-event-dispatch.js';
|
|
45
45
|
import { createKookSelfProfile } from './self-profile.js';
|
|
46
46
|
|
|
@@ -49,12 +49,12 @@ export interface KookEndpointOptions {
|
|
|
49
49
|
readonly config: ResolvedKookWebsocketConfig;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** 入站 message.receive
|
|
52
|
+
/** 入站 message.receive 载荷:发送者平台身份进 `sender.platform`(ADR 0056 §1)。 */
|
|
53
53
|
function kookInboundPayload(
|
|
54
54
|
endpointId: string,
|
|
55
55
|
conversation: ConversationRef,
|
|
56
56
|
msg: KookInboundMessage,
|
|
57
|
-
|
|
57
|
+
platform: PlatformStanding | undefined,
|
|
58
58
|
selfId: string | undefined,
|
|
59
59
|
) {
|
|
60
60
|
return {
|
|
@@ -66,8 +66,7 @@ function kookInboundPayload(
|
|
|
66
66
|
sender: {
|
|
67
67
|
id: msg.authorId,
|
|
68
68
|
name: senderDisplayName(msg) || undefined,
|
|
69
|
-
...(
|
|
70
|
-
...(projection.permissions.length ? { permissions: [...projection.permissions] } : {}),
|
|
69
|
+
...(platform ? { platform } : {}),
|
|
71
70
|
},
|
|
72
71
|
endpointId,
|
|
73
72
|
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
@@ -76,7 +75,6 @@ function kookInboundPayload(
|
|
|
76
75
|
userId: msg.authorId,
|
|
77
76
|
guildId: msg.guildId,
|
|
78
77
|
authorRoleIds: msg.authorRoles,
|
|
79
|
-
...(projection.role ? { senderRole: projection.role } : {}),
|
|
80
78
|
}),
|
|
81
79
|
};
|
|
82
80
|
}
|
|
@@ -91,7 +89,7 @@ export class KookWebsocketEndpoint extends RuntimeKookClient implements Endpoint
|
|
|
91
89
|
#open = false;
|
|
92
90
|
#executionSignal?: AbortSignal;
|
|
93
91
|
#started = false;
|
|
94
|
-
readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
|
|
92
|
+
readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient(), this.#roles);
|
|
95
93
|
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
96
94
|
recall: request => recallKookMessage(this.#requireClient(), request),
|
|
97
95
|
});
|
|
@@ -172,10 +170,10 @@ export class KookWebsocketEndpoint extends RuntimeKookClient implements Endpoint
|
|
|
172
170
|
if (msg.authorBot) return;
|
|
173
171
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
174
172
|
const selfId = this.self_id != null ? String(this.self_id) : undefined;
|
|
175
|
-
return this.#roles.resolve(msg).then((
|
|
173
|
+
return this.#roles.resolve(msg).then((platform) => {
|
|
176
174
|
this.#executionSignal?.throwIfAborted();
|
|
177
175
|
return this[endpointSpec].events.emit('message.receive',
|
|
178
|
-
kookInboundPayload(this.#options.config.id, conversation, msg,
|
|
176
|
+
kookInboundPayload(this.#options.config.id, conversation, msg, platform, selfId));
|
|
179
177
|
}).catch((err) => {
|
|
180
178
|
this.#logger.warn(formatCompact({
|
|
181
179
|
op: 'kook_gateway_receive_failed',
|
|
@@ -255,7 +253,7 @@ export class KookWebhookEndpoint extends RuntimeKookClient implements Endpoint {
|
|
|
255
253
|
#open = false;
|
|
256
254
|
#executionSignal?: AbortSignal;
|
|
257
255
|
#started = false;
|
|
258
|
-
readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
|
|
256
|
+
readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient(), this.#roles);
|
|
259
257
|
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
260
258
|
recall: request => recallKookMessage(this.#requireClient(), request),
|
|
261
259
|
});
|
|
@@ -374,10 +372,10 @@ export class KookWebhookEndpoint extends RuntimeKookClient implements Endpoint {
|
|
|
374
372
|
if (msg.authorBot) return;
|
|
375
373
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
376
374
|
const selfId = this.self_id != null ? String(this.self_id) : undefined;
|
|
377
|
-
return this.#roles.resolve(msg).then((
|
|
375
|
+
return this.#roles.resolve(msg).then((platform) => {
|
|
378
376
|
this.#executionSignal?.throwIfAborted();
|
|
379
377
|
return this[endpointSpec].events.emit('message.receive',
|
|
380
|
-
kookInboundPayload(this.#options.config.id, conversation, msg,
|
|
378
|
+
kookInboundPayload(this.#options.config.id, conversation, msg, platform, selfId));
|
|
381
379
|
}).catch((err) => {
|
|
382
380
|
this.#logger.warn(formatCompact({
|
|
383
381
|
op: 'kook_gateway_receive_failed',
|
|
@@ -438,8 +436,24 @@ function isKookTextChannelType(type: string | number | undefined): boolean {
|
|
|
438
436
|
*/
|
|
439
437
|
export function createKookEndpointManagement(
|
|
440
438
|
requireClient: () => KookClientTransport,
|
|
439
|
+
roles: KookSenderRoleResolver = new KookSenderRoleResolver(requireClient),
|
|
441
440
|
): EndpointManagement {
|
|
442
441
|
return Object.freeze<EndpointManagement>({
|
|
442
|
+
/**
|
|
443
|
+
* ADR 0056 §1: the Bot's own standing in the channel's guild (`/v3/user/view` self → its role
|
|
444
|
+
* ids, then the same guild owner / role-permission resolution as senders).
|
|
445
|
+
*/
|
|
446
|
+
async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
|
|
447
|
+
const guildId = conversation.kind === 'channel' ? conversation.parent?.id : undefined;
|
|
448
|
+
if (!guildId) return undefined;
|
|
449
|
+
const operation = createEndpointOperation({ signal });
|
|
450
|
+
const client = requireClient();
|
|
451
|
+
const selfId = client.self_id != null ? String(client.self_id) : undefined;
|
|
452
|
+
if (!selfId) return undefined;
|
|
453
|
+
const { data } = await operation.run(() => client.request.get('/v3/user/view', { params: { user_id: selfId, guild_id: guildId } }));
|
|
454
|
+
const held = Array.isArray((data as { roles?: unknown })?.roles) ? (data as { roles: unknown[] }).roles.map(Number).filter(Number.isFinite) : [];
|
|
455
|
+
return roles.resolve({ guildId, authorId: selfId, authorRoles: held });
|
|
456
|
+
},
|
|
443
457
|
async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
|
|
444
458
|
const operation = createEndpointOperation(request);
|
|
445
459
|
const guilds = await operation.run(() => requireClient().getGuildList());
|
package/src/index.ts
CHANGED
|
@@ -36,10 +36,4 @@ export {
|
|
|
36
36
|
export { type KookClientEventMap } from './client.js';
|
|
37
37
|
export { KookAdapter } from './adapter.js';
|
|
38
38
|
|
|
39
|
-
export {
|
|
40
|
-
checkKookPlatformPermit,
|
|
41
|
-
kookGroupPermitResolver,
|
|
42
|
-
normalizeKookSenderForPermit,
|
|
43
|
-
platformPermit,
|
|
44
|
-
type KookSenderInfo,
|
|
45
|
-
} from './platform-permit.js';
|
|
39
|
+
export { platformPermit } from './platform-permit.js';
|
package/src/platform-permit.ts
CHANGED
|
@@ -1,75 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* KOOK platform permit
|
|
2
|
+
* KOOK platform permit helper(ADR 0056 §5)。
|
|
3
|
+
*
|
|
4
|
+
* 平台身份由适配器上报到 `sender.platform`(见 `sender-roles.ts`),permit 由内置评估器
|
|
5
|
+
* (`@zhin.js/permission`)判断;适配器不再导出自有的 permit 解析器或检查函数。
|
|
3
6
|
*/
|
|
4
|
-
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
|
-
import { KookPermission } from './protocol.js';
|
|
6
|
-
|
|
7
|
-
const ADAPTER = 'kook';
|
|
8
7
|
|
|
9
8
|
export function platformPermit(perm: string): string {
|
|
10
|
-
return `platform
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const FACTORY_PERM_MAP: Record<string, string> = {
|
|
14
|
-
scene_admin: 'guild_admin',
|
|
15
|
-
scene_owner: 'guild_owner',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function kookGroupPermitResolver(logicalPerm: string): string {
|
|
19
|
-
return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
|
|
9
|
+
return `platform-permission:${perm}`;
|
|
20
10
|
}
|
|
21
|
-
|
|
22
|
-
export interface KookSenderInfo {
|
|
23
|
-
id: string;
|
|
24
|
-
name: string;
|
|
25
|
-
permission?: KookPermission;
|
|
26
|
-
roles?: number[];
|
|
27
|
-
isGuildOwner?: boolean;
|
|
28
|
-
isAdmin?: boolean;
|
|
29
|
-
role?: string;
|
|
30
|
-
permissions?: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function normalizeKookSenderForPermit(
|
|
34
|
-
info: KookSenderInfo,
|
|
35
|
-
isChannel: boolean,
|
|
36
|
-
): { role?: string; permissions?: string[] } {
|
|
37
|
-
const permissions: string[] = [];
|
|
38
|
-
if (info.roles?.length) {
|
|
39
|
-
permissions.push(...info.roles.map((r) => `role:${r}`));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (info.isGuildOwner || info.permission === KookPermission.Owner) {
|
|
43
|
-
permissions.push('guild_owner', 'guild_admin');
|
|
44
|
-
return { role: 'owner', permissions };
|
|
45
|
-
}
|
|
46
|
-
if (info.permission === KookPermission.ChannelAdmin && isChannel) {
|
|
47
|
-
permissions.push('channel_admin', 'guild_admin');
|
|
48
|
-
return { role: 'channel_admin', permissions };
|
|
49
|
-
}
|
|
50
|
-
if (info.isAdmin || info.permission === KookPermission.Admin) {
|
|
51
|
-
permissions.push('guild_admin');
|
|
52
|
-
return { role: 'admin', permissions };
|
|
53
|
-
}
|
|
54
|
-
return { role: 'member', permissions };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function checkKookPlatformPermit(perm: string, subject: PermissionSubject): boolean {
|
|
58
|
-
const role = subject.sender?.role?.[0];
|
|
59
|
-
const permissions = subject.sender?.permissions ?? [];
|
|
60
|
-
const has = (t: string) => permissions.includes(t);
|
|
61
|
-
|
|
62
|
-
switch (perm) {
|
|
63
|
-
case 'guild_owner':
|
|
64
|
-
return role === 'owner' || has('guild_owner');
|
|
65
|
-
case 'guild_admin':
|
|
66
|
-
return role === 'owner' || role === 'admin' || role === 'channel_admin' || has('guild_admin') || has('channel_admin');
|
|
67
|
-
case 'channel_admin':
|
|
68
|
-
return role === 'owner' || role === 'admin' || role === 'channel_admin' || has('channel_admin') || has('guild_admin');
|
|
69
|
-
case 'manage_roles':
|
|
70
|
-
return role === 'owner' || role === 'admin' || has('guild_owner') || has('guild_admin');
|
|
71
|
-
default:
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
package/src/protocol.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
3
3
|
|
|
4
4
|
import { createDecipheriv, timingSafeEqual } from 'node:crypto';
|
|
5
5
|
import { inflateSync } from 'node:zlib';
|
|
6
|
-
import
|
|
6
|
+
import { readBodyBytes } from '@zhin.js/host-http';
|
|
7
7
|
import { EndpointDeliveryError, isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { Locale } from 'zhin.js/i18n';
|
|
@@ -329,42 +329,13 @@ export function normalizeWebhookPath(path: string): string {
|
|
|
329
329
|
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
export async function readTextBody(
|
|
333
|
-
request: IncomingMessage,
|
|
334
|
-
options: { readonly limit?: number } = {},
|
|
335
|
-
): Promise<string> {
|
|
336
|
-
const limit = options.limit ?? 1_048_576;
|
|
337
|
-
const chunks: Buffer[] = [];
|
|
338
|
-
let size = 0;
|
|
339
|
-
for await (const chunk of request) {
|
|
340
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
341
|
-
size += buffer.length;
|
|
342
|
-
if (size > limit) {
|
|
343
|
-
request.destroy();
|
|
344
|
-
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
345
|
-
}
|
|
346
|
-
chunks.push(buffer);
|
|
347
|
-
}
|
|
348
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
349
|
-
}
|
|
350
332
|
|
|
351
333
|
export async function readRequestBody(
|
|
352
|
-
request:
|
|
334
|
+
request: Request,
|
|
353
335
|
options: { readonly limit?: number } = {},
|
|
354
336
|
): Promise<Buffer> {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
let size = 0;
|
|
358
|
-
for await (const chunk of request) {
|
|
359
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
360
|
-
size += buffer.length;
|
|
361
|
-
if (size > limit) {
|
|
362
|
-
request.destroy();
|
|
363
|
-
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
364
|
-
}
|
|
365
|
-
chunks.push(buffer);
|
|
366
|
-
}
|
|
367
|
-
return Buffer.concat(chunks);
|
|
337
|
+
// Oversize bodies throw (the webhook answers its generic OK), as before.
|
|
338
|
+
return Buffer.from(await readBodyBytes(request, { limit: options.limit ?? 1_048_576 }));
|
|
368
339
|
}
|
|
369
340
|
|
|
370
341
|
export function padKookEncryptKey(key: string): Buffer {
|
package/src/sender-roles.ts
CHANGED
|
@@ -1,27 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* KOOK
|
|
2
|
+
* KOOK 入站发送者平台身份(ADR 0056 §1)。
|
|
3
3
|
*
|
|
4
4
|
* 事件只携带数字角色 id,无法直接判断群主 / 管理员;这里按服务器缓存
|
|
5
|
-
* guild.view(群主 id)与 guild-role.list
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* guild.view(群主 id)与 guild-role.list(角色名与权限位),把发送者归一为
|
|
6
|
+
* `sender.platform`:
|
|
7
|
+
* - `standing`:服务器主 → `owner`;持有带「管理员」权限位的角色 → `admin`;否则 `member`。
|
|
8
|
+
* 查询失败、无法判断时不上报(不猜)。
|
|
9
|
+
* - `roles`:发送者持有的服务器角色(id + 名称)。
|
|
10
|
+
* - `permissions`:「全体成员」角色(role_id 0,每个成员都持有、事件里不列出)与这些角色权限位的并集,
|
|
11
|
+
* 按 KOOK 权限名(snake_case)上报;服务器主与持有「管理员」位的成员拥有全部权限,展开为全部权限名。
|
|
12
|
+
* 机器人自身身份(`inspectSelfStanding`)走同一套规则。
|
|
8
13
|
*/
|
|
9
14
|
import { TtlLookupCache } from 'zhin.js/adapter';
|
|
15
|
+
import type { PlatformStanding } from '@zhin.js/im-contract';
|
|
10
16
|
import type { KookClientTransport } from './ws.js';
|
|
11
17
|
|
|
12
|
-
/** KOOK 角色权限位:0x1 = 管理员。 */
|
|
13
|
-
const KOOK_ROLE_ADMIN_BIT = 1;
|
|
14
18
|
const CACHE_TTL_MS = 60_000;
|
|
19
|
+
/** 「全体成员」:每个成员都持有的基础角色。 */
|
|
20
|
+
const EVERYONE_ROLE_ID = '0';
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/** KOOK 角色权限位(按位序,见 KOOK 开发者文档「权限说明」)。 */
|
|
23
|
+
export const KOOK_ROLE_PERMISSIONS: readonly string[] = Object.freeze([
|
|
24
|
+
'administrator', 'manage_guild', 'view_audit_log', 'create_invite', 'manage_invite', 'manage_channels',
|
|
25
|
+
'kick_members', 'ban_members', 'manage_emojis', 'change_nickname', 'manage_roles', 'view_channels',
|
|
26
|
+
'send_messages', 'manage_messages', 'attach_files', 'connect_voice', 'manage_voice', 'mention_everyone',
|
|
27
|
+
'add_reaction', 'follow_reaction', 'passive_connect_voice', 'push_to_talk_only', 'free_voice_speak', 'speak',
|
|
28
|
+
'deafen_members', 'mute_members', 'manage_nicknames', 'play_music',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** KOOK 权限位 → 权限名;未知位丢弃。 */
|
|
32
|
+
export function kookPermissionNames(bits: number): string[] {
|
|
33
|
+
if (!Number.isFinite(bits) || bits <= 0) return [];
|
|
34
|
+
return KOOK_ROLE_PERMISSIONS.filter((_, index) => Math.floor(bits / 2 ** index) % 2 === 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface GuildRole {
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
readonly permissions: number;
|
|
20
40
|
}
|
|
21
41
|
|
|
22
42
|
interface GuildSnapshot {
|
|
23
43
|
readonly ownerId?: string;
|
|
24
|
-
|
|
44
|
+
/** `undefined` when the role list lookup failed. */
|
|
45
|
+
readonly roles?: ReadonlyMap<string, GuildRole>;
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
export class KookSenderRoleResolver {
|
|
@@ -33,19 +54,28 @@ export class KookSenderRoleResolver {
|
|
|
33
54
|
readonly guildId?: string;
|
|
34
55
|
readonly authorId: string;
|
|
35
56
|
readonly authorRoles?: readonly number[];
|
|
36
|
-
}): Promise<
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
if (!input.guildId) return { permissions };
|
|
40
|
-
|
|
57
|
+
}): Promise<PlatformStanding | undefined> {
|
|
58
|
+
if (!input.guildId) return undefined;
|
|
59
|
+
const roleIds = [...new Set((input.authorRoles ?? []).map(String))];
|
|
41
60
|
const snapshot = await this.#snapshot(input.guildId);
|
|
42
|
-
if (snapshot
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
if (!snapshot) return undefined;
|
|
62
|
+
const held = roleIds.filter((id) => id !== EVERYONE_ROLE_ID).map((id) => ({ id, role: snapshot.roles?.get(id) }));
|
|
63
|
+
const everyone = snapshot.roles?.get(EVERYONE_ROLE_ID)?.permissions ?? 0;
|
|
64
|
+
const bits = held.reduce((sum, { role }) => sum | (role?.permissions ?? 0), everyone);
|
|
65
|
+
const roles = held.map(({ id, role }) => (role?.name ? { id, name: role.name } : { id }));
|
|
66
|
+
const isOwner = snapshot.ownerId !== undefined && snapshot.ownerId === input.authorId;
|
|
67
|
+
const isAdmin = kookPermissionNames(bits).includes('administrator');
|
|
68
|
+
// The server owner and administrators hold every permission (KOOK ignores per-bit grants for them).
|
|
69
|
+
const permissions = isOwner || isAdmin ? [...KOOK_ROLE_PERMISSIONS] : kookPermissionNames(bits);
|
|
70
|
+
// Without the owner id or the role list the standing cannot be decided; an admin role still proves admin.
|
|
71
|
+
const standing = isOwner ? 'owner' : isAdmin ? 'admin'
|
|
72
|
+
: snapshot.ownerId !== undefined && snapshot.roles ? 'member' : undefined;
|
|
73
|
+
if (!standing) return undefined;
|
|
74
|
+
return {
|
|
75
|
+
standing,
|
|
76
|
+
...(roles.length ? { roles } : {}),
|
|
77
|
+
...(permissions.length ? { permissions } : {}),
|
|
78
|
+
};
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
async #snapshot(guildId: string): Promise<GuildSnapshot | undefined> {
|
|
@@ -58,16 +88,18 @@ export class KookSenderRoleResolver {
|
|
|
58
88
|
const client = this.getClient();
|
|
59
89
|
if (!client) return undefined;
|
|
60
90
|
let ownerId: string | undefined;
|
|
61
|
-
|
|
91
|
+
let roles: Map<string, GuildRole> | undefined;
|
|
62
92
|
try {
|
|
63
93
|
const info = await client.getGuildInfo?.(guildId);
|
|
64
94
|
if (info?.user_id != null && String(info.user_id)) ownerId = String(info.user_id);
|
|
65
95
|
} catch { /* 保守:视为无群主信息 */ }
|
|
66
96
|
try {
|
|
97
|
+
roles = new Map();
|
|
67
98
|
for (const role of await client.pickGuild(guildId).getRoleList()) {
|
|
68
|
-
|
|
99
|
+
const name = typeof (role as { name?: unknown }).name === 'string' ? (role as { name: string }).name : undefined;
|
|
100
|
+
roles.set(String(role.role_id), { ...(name ? { name } : {}), permissions: Number(role.permissions) || 0 });
|
|
69
101
|
}
|
|
70
|
-
} catch { /*
|
|
71
|
-
return { ...(ownerId ? { ownerId } : {}),
|
|
102
|
+
} catch { roles = undefined; /* 保守:视为无角色信息 */ }
|
|
103
|
+
return { ...(ownerId ? { ownerId } : {}), ...(roles ? { roles } : {}) };
|
|
72
104
|
}
|
|
73
105
|
}
|