@zhin.js/adapter-kook 1.1.26 → 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 +164 -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/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/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/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
|
}
|
package/src/webhook.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { endpointSpec, type Endpoint } from 'zhin.js/adapter';
|
|
|
2
2
|
/**
|
|
3
3
|
* KOOK webhook HTTP: verify → parse → admit.
|
|
4
4
|
*/
|
|
5
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
5
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
7
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
7
|
import {
|
|
@@ -34,17 +33,14 @@ export function registerKookWebhookRoutes(
|
|
|
34
33
|
): HttpRouteRegistration[] {
|
|
35
34
|
const path = handler.endpointConfig.webhookPath;
|
|
36
35
|
return [
|
|
37
|
-
http.route('POST', path,
|
|
38
|
-
await handleKookWebhookRequest(request, response, handler);
|
|
39
|
-
}, { summary: 'KOOK webhook callback', tags: ['kook'] }),
|
|
36
|
+
http.route('POST', path, (request) => handleKookWebhookRequest(request, handler), { summary: 'KOOK webhook callback', tags: ['kook'] }),
|
|
40
37
|
];
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
export async function handleKookWebhookRequest(
|
|
44
|
-
request:
|
|
45
|
-
response: ServerResponse,
|
|
41
|
+
request: Request,
|
|
46
42
|
handler: KookWebhookHandler,
|
|
47
|
-
): Promise<
|
|
43
|
+
): Promise<Response> {
|
|
48
44
|
try {
|
|
49
45
|
const rawBody = await readRequestBody(request);
|
|
50
46
|
let frame: KookWebhookFrame;
|
|
@@ -56,46 +52,32 @@ export async function handleKookWebhookRequest(
|
|
|
56
52
|
ok: false,
|
|
57
53
|
error: error instanceof Error ? error.message : String(error),
|
|
58
54
|
}));
|
|
59
|
-
|
|
60
|
-
response.end(JSON.stringify({ message: 'Invalid webhook payload' }));
|
|
61
|
-
return;
|
|
55
|
+
return new Response(JSON.stringify({ message: 'Invalid webhook payload' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
62
56
|
}
|
|
63
57
|
|
|
64
58
|
const event = frame.d;
|
|
65
59
|
if (!event || typeof event !== 'object') {
|
|
66
|
-
|
|
67
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
68
|
-
return;
|
|
60
|
+
return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
if (isKookWebhookChallenge(event)) {
|
|
72
64
|
if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
|
|
73
|
-
|
|
74
|
-
response.end(JSON.stringify({ message: 'Invalid verify_token' }));
|
|
75
|
-
return;
|
|
65
|
+
return new Response(JSON.stringify({ message: 'Invalid verify_token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
76
66
|
}
|
|
77
|
-
|
|
78
|
-
response.end(JSON.stringify({ challenge: event.challenge ?? '' }));
|
|
79
|
-
return;
|
|
67
|
+
return new Response(JSON.stringify({ challenge: event.challenge ?? '' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
|
|
83
|
-
|
|
84
|
-
response.end(JSON.stringify({ message: 'Invalid verify_token' }));
|
|
85
|
-
return;
|
|
71
|
+
return new Response(JSON.stringify({ message: 'Invalid verify_token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
86
72
|
}
|
|
87
73
|
|
|
88
74
|
if (!handler.isOpen) {
|
|
89
|
-
|
|
90
|
-
response.end(JSON.stringify({ message: 'Endpoint unavailable' }));
|
|
91
|
-
return;
|
|
75
|
+
return new Response(JSON.stringify({ message: 'Endpoint unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
92
76
|
}
|
|
93
|
-
await handler[endpointSpec].events.run(async () => {
|
|
77
|
+
return await handler[endpointSpec].events.run(async () => {
|
|
94
78
|
if (frame.sn != null) {
|
|
95
79
|
if (!handler.checkAndRememberSn(frame.sn)) {
|
|
96
|
-
|
|
97
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
98
|
-
return;
|
|
80
|
+
return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
99
81
|
}
|
|
100
82
|
}
|
|
101
83
|
|
|
@@ -103,17 +85,13 @@ export async function handleKookWebhookRequest(
|
|
|
103
85
|
await handler.admitEvent(event);
|
|
104
86
|
} catch {
|
|
105
87
|
if (frame.sn != null) handler.forgetSn?.(frame.sn);
|
|
106
|
-
|
|
107
|
-
response.end(JSON.stringify({ message: 'Admission unavailable' }));
|
|
108
|
-
return;
|
|
88
|
+
return new Response(JSON.stringify({ message: 'Admission unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
109
89
|
}
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
91
|
+
return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
113
92
|
});
|
|
114
93
|
} catch (error) {
|
|
115
94
|
logger.error('KOOK webhook error:', error);
|
|
116
|
-
|
|
117
|
-
response.end(JSON.stringify({ message: 'OK' }));
|
|
95
|
+
return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
118
96
|
}
|
|
119
97
|
}
|