@zhin.js/adapter-kook 1.1.11 → 1.1.13
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 +32 -0
- package/lib/endpoint.js +28 -36
- package/lib/outbound.js +1 -1
- package/lib/sender-roles.d.ts +24 -0
- package/lib/sender-roles.js +61 -0
- package/lib/ws.d.ts +4 -0
- package/package.json +15 -15
- package/src/endpoint.ts +39 -36
- package/src/outbound.ts +1 -1
- package/src/sender-roles.ts +83 -0
- package/src/ws.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @zhin.js/adapter-kook
|
|
2
2
|
|
|
3
|
+
## 1.1.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [db06de8]
|
|
8
|
+
- @zhin.js/core@1.1.48
|
|
9
|
+
|
|
10
|
+
## 1.1.12
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- e40899f: Adapters now project group owners and admins onto the canonical `sender.roles` names `owner` / `admin` (Telegram creator/administrator, KOOK guild owner and admin-permission roles, Lark chat owner/managers, QQ official guild owner/admin identities). KOOK numeric role ids moved from `roles` to `permissions` (`role:<id>`). `@zhin.js/command` exports `canManageConversation` ("private chat or owner/admin/master"), now shared by the Bilibili and earthquake subscription commands; Roulette lets owners and admins toggle the mute penalty and cancel a game.
|
|
15
|
+
- 9e1d1a6: 飞书 / KOOK 查询群主与管理员时,缓存失效那一刻同一个群(服务器)的并发消息共用一次查询,不再各自调用一次接口。
|
|
16
|
+
- fbfefb7: Share segments accept `data.platform` (the song's music service: qq, netease, kugou, kuwo, migu). icqq maps it to the matching music app id when `config.appid` is absent and offers it to the Agent; adapters that cannot render share app metadata reject it like `audio` and `config`.
|
|
17
|
+
- Updated dependencies [1a11207]
|
|
18
|
+
- Updated dependencies [9943df6]
|
|
19
|
+
- Updated dependencies [5be09ae]
|
|
20
|
+
- Updated dependencies [a3ab153]
|
|
21
|
+
- Updated dependencies [e8f7309]
|
|
22
|
+
- Updated dependencies [5afc18f]
|
|
23
|
+
- Updated dependencies [d3563d9]
|
|
24
|
+
- Updated dependencies [c618eb7]
|
|
25
|
+
- Updated dependencies [fbfefb7]
|
|
26
|
+
- @zhin.js/tool@1.1.8
|
|
27
|
+
- @zhin.js/core@1.1.47
|
|
28
|
+
- @zhin.js/adapter@1.1.21
|
|
29
|
+
- @zhin.js/feature-kit@1.1.7
|
|
30
|
+
- @zhin.js/im-contract@1.1.5
|
|
31
|
+
- @zhin.js/agent-feature@1.1.8
|
|
32
|
+
- @zhin.js/skill@1.1.8
|
|
33
|
+
- @zhin.js/host-http@1.1.8
|
|
34
|
+
|
|
3
35
|
## 1.1.11
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/lib/endpoint.js
CHANGED
|
@@ -5,13 +5,38 @@ import { formatInboundContent, isKookBotMentioned, kookInboundConversation, norm
|
|
|
5
5
|
import { registerKookWebhookRoutes } from './webhook.js';
|
|
6
6
|
import { defaultCreateClient, defaultCreateWebhookClient, normalizeKookMessage, } from './ws.js';
|
|
7
7
|
import { recallKookMessage, sendKookOutbound } from './outbound.js';
|
|
8
|
+
import { KookSenderRoleResolver } from './sender-roles.js';
|
|
8
9
|
import { receiveKookSideEvent } from './side-event-dispatch.js';
|
|
10
|
+
/** 入站 message.receive 载荷:规范角色 owner/admin 进 roles,原始角色 id 进 permissions。 */
|
|
11
|
+
function kookInboundPayload(endpointId, conversation, msg, projection, selfId) {
|
|
12
|
+
return {
|
|
13
|
+
conversation,
|
|
14
|
+
message: { conversation, id: msg.id },
|
|
15
|
+
content: formatInboundContent(msg),
|
|
16
|
+
sender: {
|
|
17
|
+
id: msg.authorId,
|
|
18
|
+
name: senderDisplayName(msg) || undefined,
|
|
19
|
+
...(projection.role ? { roles: [projection.role] } : {}),
|
|
20
|
+
...(projection.permissions.length ? { permissions: [...projection.permissions] } : {}),
|
|
21
|
+
},
|
|
22
|
+
endpointId,
|
|
23
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
24
|
+
metadata: Object.freeze({
|
|
25
|
+
channelKind: msg.channelKind,
|
|
26
|
+
userId: msg.authorId,
|
|
27
|
+
guildId: msg.guildId,
|
|
28
|
+
authorRoleIds: msg.authorRoles,
|
|
29
|
+
...(projection.role ? { senderRole: projection.role } : {}),
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
9
33
|
export class KookWebsocketEndpoint extends Endpoint {
|
|
10
34
|
#sentConversations = new Map();
|
|
11
35
|
#logger;
|
|
12
36
|
#options;
|
|
13
37
|
#createClient;
|
|
14
38
|
#client = null;
|
|
39
|
+
#roles = new KookSenderRoleResolver(() => this.#client);
|
|
15
40
|
#open = false;
|
|
16
41
|
#started = false;
|
|
17
42
|
management = createKookEndpointManagement(() => this.#requireClient());
|
|
@@ -94,24 +119,7 @@ export class KookWebsocketEndpoint extends Endpoint {
|
|
|
94
119
|
return;
|
|
95
120
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
96
121
|
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
97
|
-
void this.emit('message.receive', {
|
|
98
|
-
conversation,
|
|
99
|
-
message: { conversation, id: msg.id },
|
|
100
|
-
content: formatInboundContent(msg),
|
|
101
|
-
sender: {
|
|
102
|
-
id: msg.authorId,
|
|
103
|
-
name: senderDisplayName(msg) || undefined,
|
|
104
|
-
...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
|
|
105
|
-
},
|
|
106
|
-
endpointId: this.#options.config.id,
|
|
107
|
-
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
108
|
-
metadata: Object.freeze({
|
|
109
|
-
channelKind: msg.channelKind,
|
|
110
|
-
userId: msg.authorId,
|
|
111
|
-
guildId: msg.guildId,
|
|
112
|
-
roles: msg.authorRoles,
|
|
113
|
-
}),
|
|
114
|
-
}).catch((err) => {
|
|
122
|
+
void this.#roles.resolve(msg).then((projection) => this.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId))).catch((err) => {
|
|
115
123
|
this.#logger.warn(formatCompact({
|
|
116
124
|
op: 'kook_gateway_receive_failed',
|
|
117
125
|
target: `${conversation.kind}:${conversation.id}`,
|
|
@@ -172,6 +180,7 @@ export class KookWebhookEndpoint extends Endpoint {
|
|
|
172
180
|
#options;
|
|
173
181
|
#createClient;
|
|
174
182
|
#client = null;
|
|
183
|
+
#roles = new KookSenderRoleResolver(() => this.#client);
|
|
175
184
|
#routeReleases = [];
|
|
176
185
|
#processedSn = new Set();
|
|
177
186
|
#open = false;
|
|
@@ -289,24 +298,7 @@ export class KookWebhookEndpoint extends Endpoint {
|
|
|
289
298
|
return;
|
|
290
299
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
291
300
|
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
292
|
-
void this.emit('message.receive', {
|
|
293
|
-
conversation,
|
|
294
|
-
message: { conversation, id: msg.id },
|
|
295
|
-
content: formatInboundContent(msg),
|
|
296
|
-
sender: {
|
|
297
|
-
id: msg.authorId,
|
|
298
|
-
name: senderDisplayName(msg) || undefined,
|
|
299
|
-
...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
|
|
300
|
-
},
|
|
301
|
-
endpointId: this.#options.config.id,
|
|
302
|
-
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
303
|
-
metadata: Object.freeze({
|
|
304
|
-
channelKind: msg.channelKind,
|
|
305
|
-
userId: msg.authorId,
|
|
306
|
-
guildId: msg.guildId,
|
|
307
|
-
roles: msg.authorRoles,
|
|
308
|
-
}),
|
|
309
|
-
}).catch((err) => {
|
|
301
|
+
void this.#roles.resolve(msg).then((projection) => this.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId))).catch((err) => {
|
|
310
302
|
this.#logger.warn(formatCompact({
|
|
311
303
|
op: 'kook_gateway_receive_failed',
|
|
312
304
|
target: `${conversation.kind}:${conversation.id}`,
|
package/lib/outbound.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function sendKookOutbound(client, { conversation, payload }) {
|
|
|
46
46
|
}
|
|
47
47
|
if (!['http:', 'https:'].includes(url.protocol) || typeof data.title !== 'string' || !data.title.trim())
|
|
48
48
|
throw new EndpointDeliveryError('invalid_payload', 'KOOK share requires URL and title', 'not_sent');
|
|
49
|
-
if (['image', 'audio', 'artist', 'duration', 'config'].some(key => data[key] !== undefined))
|
|
49
|
+
if (['image', 'audio', 'artist', 'duration', 'platform', 'config'].some(key => data[key] !== undefined))
|
|
50
50
|
throw new EndpointDeliveryError('unsupported_operation', 'KOOK share media/app metadata is not implemented', 'not_sent');
|
|
51
51
|
modules.push({ type: 'section', mode: 'left', text: { type: 'plain-text', content: data.title } });
|
|
52
52
|
for (const value of [data.description, data.content])
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KOOK 入站发送者角色投影。
|
|
3
|
+
*
|
|
4
|
+
* 事件只携带数字角色 id,无法直接判断群主 / 管理员;这里按服务器缓存
|
|
5
|
+
* guild.view(群主 id)与 guild-role.list(角色权限位),把发送者投影为
|
|
6
|
+
* 跨平台规范角色 owner / admin。原始角色 id 以 `role:<id>` 形式放入 permissions,
|
|
7
|
+
* 不进入 `sender.roles`。
|
|
8
|
+
*/
|
|
9
|
+
import type { KookClientTransport } from './ws.js';
|
|
10
|
+
export interface KookSenderRoleProjection {
|
|
11
|
+
/** 规范角色:owner / admin;普通成员为 undefined。 */
|
|
12
|
+
readonly role?: 'owner' | 'admin';
|
|
13
|
+
readonly permissions: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export declare class KookSenderRoleResolver {
|
|
16
|
+
#private;
|
|
17
|
+
private readonly getClient;
|
|
18
|
+
constructor(getClient: () => KookClientTransport | null);
|
|
19
|
+
resolve(input: {
|
|
20
|
+
readonly guildId?: string;
|
|
21
|
+
readonly authorId: string;
|
|
22
|
+
readonly authorRoles?: readonly number[];
|
|
23
|
+
}): Promise<KookSenderRoleProjection>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** KOOK 角色权限位:0x1 = 管理员。 */
|
|
2
|
+
const KOOK_ROLE_ADMIN_BIT = 1;
|
|
3
|
+
const CACHE_TTL_MS = 60_000;
|
|
4
|
+
export class KookSenderRoleResolver {
|
|
5
|
+
getClient;
|
|
6
|
+
#guilds = new Map();
|
|
7
|
+
/** One lookup per guild at a time: concurrent messages on a cache miss share it. */
|
|
8
|
+
#lookups = new Map();
|
|
9
|
+
constructor(getClient) {
|
|
10
|
+
this.getClient = getClient;
|
|
11
|
+
}
|
|
12
|
+
async resolve(input) {
|
|
13
|
+
const roleIds = (input.authorRoles ?? []).map(String);
|
|
14
|
+
const permissions = roleIds.map((id) => `role:${id}`);
|
|
15
|
+
if (!input.guildId)
|
|
16
|
+
return { permissions };
|
|
17
|
+
const snapshot = await this.#snapshot(input.guildId);
|
|
18
|
+
if (snapshot?.ownerId !== undefined && snapshot.ownerId === input.authorId) {
|
|
19
|
+
return { role: 'owner', permissions: [...permissions, 'guild_owner', 'guild_admin'] };
|
|
20
|
+
}
|
|
21
|
+
if (snapshot && roleIds.some((id) => snapshot.adminRoleIds.has(id))) {
|
|
22
|
+
return { role: 'admin', permissions: [...permissions, 'guild_admin'] };
|
|
23
|
+
}
|
|
24
|
+
return { permissions };
|
|
25
|
+
}
|
|
26
|
+
async #snapshot(guildId) {
|
|
27
|
+
const cached = this.#guilds.get(guildId);
|
|
28
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS)
|
|
29
|
+
return cached;
|
|
30
|
+
let lookup = this.#lookups.get(guildId);
|
|
31
|
+
if (!lookup) {
|
|
32
|
+
lookup = this.#fetch(guildId).finally(() => this.#lookups.delete(guildId));
|
|
33
|
+
this.#lookups.set(guildId, lookup);
|
|
34
|
+
}
|
|
35
|
+
return lookup;
|
|
36
|
+
}
|
|
37
|
+
async #fetch(guildId) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const client = this.getClient();
|
|
40
|
+
if (!client)
|
|
41
|
+
return undefined;
|
|
42
|
+
let ownerId;
|
|
43
|
+
const adminRoleIds = new Set();
|
|
44
|
+
try {
|
|
45
|
+
const info = await client.getGuildInfo?.(guildId);
|
|
46
|
+
if (info?.user_id != null && String(info.user_id))
|
|
47
|
+
ownerId = String(info.user_id);
|
|
48
|
+
}
|
|
49
|
+
catch { /* 保守:视为无群主信息 */ }
|
|
50
|
+
try {
|
|
51
|
+
for (const role of await client.pickGuild(guildId).getRoleList()) {
|
|
52
|
+
if ((Number(role.permissions) & KOOK_ROLE_ADMIN_BIT) !== 0)
|
|
53
|
+
adminRoleIds.add(String(role.role_id));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { /* 保守:视为无管理员角色 */ }
|
|
57
|
+
const snapshot = { at: now, ...(ownerId ? { ownerId } : {}), adminRoleIds };
|
|
58
|
+
this.#guilds.set(guildId, snapshot);
|
|
59
|
+
return snapshot;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/lib/ws.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface KookClientTransport {
|
|
|
22
22
|
uploadMedia?(data: Buffer): Promise<string>;
|
|
23
23
|
recallChannelMsg?(channelId: string, messageId: string): Promise<boolean>;
|
|
24
24
|
recallPrivateMsg?(userId: string, messageId: string): Promise<boolean>;
|
|
25
|
+
/** GET /api/v3/guild/view(群主 id)。 */
|
|
26
|
+
getGuildInfo?(guildId: string): Promise<{
|
|
27
|
+
user_id?: string | number;
|
|
28
|
+
}>;
|
|
25
29
|
pickGuild(guildId: string): {
|
|
26
30
|
kick(userId: string): Promise<boolean>;
|
|
27
31
|
getRoleList(): Promise<Array<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-kook",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "Zhin.js KOOK adapter for Plugin Runtime (WebSocket gateway or Webhook)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"kook-client": "^1.0.5",
|
|
36
|
-
"@zhin.js/adapter": "1.1.
|
|
37
|
-
"@zhin.js/agent-feature": "1.1.
|
|
38
|
-
"@zhin.js/core": "1.1.
|
|
39
|
-
"@zhin.js/feature-kit": "1.1.
|
|
40
|
-
"@zhin.js/host-http": "1.1.
|
|
41
|
-
"@zhin.js/im-contract": "1.1.
|
|
36
|
+
"@zhin.js/adapter": "1.1.21",
|
|
37
|
+
"@zhin.js/agent-feature": "1.1.8",
|
|
38
|
+
"@zhin.js/core": "1.1.48",
|
|
39
|
+
"@zhin.js/feature-kit": "1.1.7",
|
|
40
|
+
"@zhin.js/host-http": "1.1.8",
|
|
41
|
+
"@zhin.js/im-contract": "1.1.5",
|
|
42
42
|
"@zhin.js/logger": "1.1.2",
|
|
43
43
|
"@zhin.js/permission": "1.1.7",
|
|
44
|
-
"@zhin.js/skill": "1.1.
|
|
45
|
-
"@zhin.js/tool": "1.1.
|
|
44
|
+
"@zhin.js/skill": "1.1.8",
|
|
45
|
+
"@zhin.js/tool": "1.1.8"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
50
|
-
"@zhin.js/command": "^1.1.
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
52
|
-
"zhin.js": "^1.1.
|
|
49
|
+
"@zhin.js/adapter": "^1.1.21",
|
|
50
|
+
"@zhin.js/command": "^1.1.9",
|
|
51
|
+
"@zhin.js/core": "^1.1.48",
|
|
52
|
+
"zhin.js": "^1.1.13"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@zhin.js/command": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "^6.0.3",
|
|
68
68
|
"vitest": "^4.1.10",
|
|
69
69
|
"zod": "^4.4.3",
|
|
70
|
-
"@zhin.js/host-http": "1.1.
|
|
71
|
-
"zhin.js": "1.1.
|
|
70
|
+
"@zhin.js/host-http": "1.1.8",
|
|
71
|
+
"zhin.js": "1.1.13"
|
|
72
72
|
},
|
|
73
73
|
"files": [
|
|
74
74
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type KookClientTransport,
|
|
38
38
|
} from './ws.js';
|
|
39
39
|
import { recallKookMessage, sendKookOutbound } from './outbound.js';
|
|
40
|
+
import { KookSenderRoleResolver, type KookSenderRoleProjection } from './sender-roles.js';
|
|
40
41
|
import { receiveKookSideEvent } from './side-event-dispatch.js';
|
|
41
42
|
|
|
42
43
|
export interface KookEndpointOptions {
|
|
@@ -45,6 +46,36 @@ export interface KookEndpointOptions {
|
|
|
45
46
|
readonly createClient?: CreateKookClient;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** 入站 message.receive 载荷:规范角色 owner/admin 进 roles,原始角色 id 进 permissions。 */
|
|
50
|
+
function kookInboundPayload(
|
|
51
|
+
endpointId: string,
|
|
52
|
+
conversation: ConversationRef,
|
|
53
|
+
msg: KookInboundMessage,
|
|
54
|
+
projection: KookSenderRoleProjection,
|
|
55
|
+
selfId: string | undefined,
|
|
56
|
+
) {
|
|
57
|
+
return {
|
|
58
|
+
conversation,
|
|
59
|
+
message: { conversation, id: msg.id },
|
|
60
|
+
content: formatInboundContent(msg),
|
|
61
|
+
sender: {
|
|
62
|
+
id: msg.authorId,
|
|
63
|
+
name: senderDisplayName(msg) || undefined,
|
|
64
|
+
...(projection.role ? { roles: [projection.role] } : {}),
|
|
65
|
+
...(projection.permissions.length ? { permissions: [...projection.permissions] } : {}),
|
|
66
|
+
},
|
|
67
|
+
endpointId,
|
|
68
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
69
|
+
metadata: Object.freeze({
|
|
70
|
+
channelKind: msg.channelKind,
|
|
71
|
+
userId: msg.authorId,
|
|
72
|
+
guildId: msg.guildId,
|
|
73
|
+
authorRoleIds: msg.authorRoles,
|
|
74
|
+
...(projection.role ? { senderRole: projection.role } : {}),
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
export class KookWebsocketEndpoint extends Endpoint<KookClientTransport> {
|
|
49
80
|
#sentConversations = new Map<string, ConversationRef>();
|
|
50
81
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
@@ -52,6 +83,7 @@ export class KookWebsocketEndpoint extends Endpoint<KookClientTransport> {
|
|
|
52
83
|
readonly #options: KookEndpointOptions;
|
|
53
84
|
readonly #createClient: CreateKookClient;
|
|
54
85
|
#client: KookClientTransport | null = null;
|
|
86
|
+
readonly #roles = new KookSenderRoleResolver(() => this.#client);
|
|
55
87
|
#open = false;
|
|
56
88
|
#started = false;
|
|
57
89
|
readonly management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
|
|
@@ -137,24 +169,9 @@ export class KookWebsocketEndpoint extends Endpoint<KookClientTransport> {
|
|
|
137
169
|
if (msg.authorBot) return;
|
|
138
170
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
139
171
|
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
140
|
-
void this.emit('message.receive',
|
|
141
|
-
conversation,
|
|
142
|
-
|
|
143
|
-
content: formatInboundContent(msg),
|
|
144
|
-
sender: {
|
|
145
|
-
id: msg.authorId,
|
|
146
|
-
name: senderDisplayName(msg) || undefined,
|
|
147
|
-
...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
|
|
148
|
-
},
|
|
149
|
-
endpointId: this.#options.config.id,
|
|
150
|
-
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
151
|
-
metadata: Object.freeze({
|
|
152
|
-
channelKind: msg.channelKind,
|
|
153
|
-
userId: msg.authorId,
|
|
154
|
-
guildId: msg.guildId,
|
|
155
|
-
roles: msg.authorRoles,
|
|
156
|
-
}),
|
|
157
|
-
}).catch((err) => {
|
|
172
|
+
void this.#roles.resolve(msg).then((projection) => this.emit('message.receive',
|
|
173
|
+
kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId),
|
|
174
|
+
)).catch((err) => {
|
|
158
175
|
this.#logger.warn(formatCompact({
|
|
159
176
|
op: 'kook_gateway_receive_failed',
|
|
160
177
|
target: `${conversation.kind}:${conversation.id}`,
|
|
@@ -223,6 +240,7 @@ export class KookWebhookEndpoint extends Endpoint<KookClientTransport> {
|
|
|
223
240
|
readonly #options: KookWebhookEndpointOptions;
|
|
224
241
|
readonly #createClient: CreateKookClient;
|
|
225
242
|
#client: KookClientTransport | null = null;
|
|
243
|
+
readonly #roles = new KookSenderRoleResolver(() => this.#client);
|
|
226
244
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
227
245
|
#processedSn = new Set<number>();
|
|
228
246
|
#open = false;
|
|
@@ -348,24 +366,9 @@ export class KookWebhookEndpoint extends Endpoint<KookClientTransport> {
|
|
|
348
366
|
if (msg.authorBot) return;
|
|
349
367
|
const conversation = kookInboundConversation(String(this.#options.id), msg);
|
|
350
368
|
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
351
|
-
void this.emit('message.receive',
|
|
352
|
-
conversation,
|
|
353
|
-
|
|
354
|
-
content: formatInboundContent(msg),
|
|
355
|
-
sender: {
|
|
356
|
-
id: msg.authorId,
|
|
357
|
-
name: senderDisplayName(msg) || undefined,
|
|
358
|
-
...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
|
|
359
|
-
},
|
|
360
|
-
endpointId: this.#options.config.id,
|
|
361
|
-
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
362
|
-
metadata: Object.freeze({
|
|
363
|
-
channelKind: msg.channelKind,
|
|
364
|
-
userId: msg.authorId,
|
|
365
|
-
guildId: msg.guildId,
|
|
366
|
-
roles: msg.authorRoles,
|
|
367
|
-
}),
|
|
368
|
-
}).catch((err) => {
|
|
369
|
+
void this.#roles.resolve(msg).then((projection) => this.emit('message.receive',
|
|
370
|
+
kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId),
|
|
371
|
+
)).catch((err) => {
|
|
369
372
|
this.#logger.warn(formatCompact({
|
|
370
373
|
op: 'kook_gateway_receive_failed',
|
|
371
374
|
target: `${conversation.kind}:${conversation.id}`,
|
package/src/outbound.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function sendKookOutbound(client: KookClientTransport, { conversati
|
|
|
37
37
|
let url: URL;
|
|
38
38
|
try { url = new URL(data.url); } catch { throw new EndpointDeliveryError('invalid_payload', 'KOOK share requires an HTTP(S) URL', 'not_sent'); }
|
|
39
39
|
if (!['http:', 'https:'].includes(url.protocol) || typeof data.title !== 'string' || !data.title.trim()) throw new EndpointDeliveryError('invalid_payload', 'KOOK share requires URL and title', 'not_sent');
|
|
40
|
-
if (['image', 'audio', 'artist', 'duration', 'config'].some(key => data[key] !== undefined)) throw new EndpointDeliveryError('unsupported_operation', 'KOOK share media/app metadata is not implemented', 'not_sent');
|
|
40
|
+
if (['image', 'audio', 'artist', 'duration', 'platform', 'config'].some(key => data[key] !== undefined)) throw new EndpointDeliveryError('unsupported_operation', 'KOOK share media/app metadata is not implemented', 'not_sent');
|
|
41
41
|
modules.push({ type: 'section', mode: 'left', text: { type: 'plain-text', content: data.title } });
|
|
42
42
|
for (const value of [data.description, data.content]) if (typeof value === 'string' && value) modules.push({ type: 'section', mode: 'left', text: { type: 'plain-text', content: value } });
|
|
43
43
|
modules.push({ type: 'action-group', elements: [{ type: 'button', theme: 'secondary', click: 'link', value: url.href, text: { type: 'plain-text', content: '打开链接' } }] });
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KOOK 入站发送者角色投影。
|
|
3
|
+
*
|
|
4
|
+
* 事件只携带数字角色 id,无法直接判断群主 / 管理员;这里按服务器缓存
|
|
5
|
+
* guild.view(群主 id)与 guild-role.list(角色权限位),把发送者投影为
|
|
6
|
+
* 跨平台规范角色 owner / admin。原始角色 id 以 `role:<id>` 形式放入 permissions,
|
|
7
|
+
* 不进入 `sender.roles`。
|
|
8
|
+
*/
|
|
9
|
+
import type { KookClientTransport } from './ws.js';
|
|
10
|
+
|
|
11
|
+
/** KOOK 角色权限位:0x1 = 管理员。 */
|
|
12
|
+
const KOOK_ROLE_ADMIN_BIT = 1;
|
|
13
|
+
const CACHE_TTL_MS = 60_000;
|
|
14
|
+
|
|
15
|
+
export interface KookSenderRoleProjection {
|
|
16
|
+
/** 规范角色:owner / admin;普通成员为 undefined。 */
|
|
17
|
+
readonly role?: 'owner' | 'admin';
|
|
18
|
+
readonly permissions: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface GuildSnapshot {
|
|
22
|
+
readonly at: number;
|
|
23
|
+
readonly ownerId?: string;
|
|
24
|
+
readonly adminRoleIds: ReadonlySet<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class KookSenderRoleResolver {
|
|
28
|
+
readonly #guilds = new Map<string, GuildSnapshot>();
|
|
29
|
+
/** One lookup per guild at a time: concurrent messages on a cache miss share it. */
|
|
30
|
+
readonly #lookups = new Map<string, Promise<GuildSnapshot | undefined>>();
|
|
31
|
+
|
|
32
|
+
constructor(private readonly getClient: () => KookClientTransport | null) {}
|
|
33
|
+
|
|
34
|
+
async resolve(input: {
|
|
35
|
+
readonly guildId?: string;
|
|
36
|
+
readonly authorId: string;
|
|
37
|
+
readonly authorRoles?: readonly number[];
|
|
38
|
+
}): Promise<KookSenderRoleProjection> {
|
|
39
|
+
const roleIds = (input.authorRoles ?? []).map(String);
|
|
40
|
+
const permissions = roleIds.map((id) => `role:${id}`);
|
|
41
|
+
if (!input.guildId) return { permissions };
|
|
42
|
+
|
|
43
|
+
const snapshot = await this.#snapshot(input.guildId);
|
|
44
|
+
if (snapshot?.ownerId !== undefined && snapshot.ownerId === input.authorId) {
|
|
45
|
+
return { role: 'owner', permissions: [...permissions, 'guild_owner', 'guild_admin'] };
|
|
46
|
+
}
|
|
47
|
+
if (snapshot && roleIds.some((id) => snapshot.adminRoleIds.has(id))) {
|
|
48
|
+
return { role: 'admin', permissions: [...permissions, 'guild_admin'] };
|
|
49
|
+
}
|
|
50
|
+
return { permissions };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async #snapshot(guildId: string): Promise<GuildSnapshot | undefined> {
|
|
54
|
+
const cached = this.#guilds.get(guildId);
|
|
55
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached;
|
|
56
|
+
let lookup = this.#lookups.get(guildId);
|
|
57
|
+
if (!lookup) {
|
|
58
|
+
lookup = this.#fetch(guildId).finally(() => this.#lookups.delete(guildId));
|
|
59
|
+
this.#lookups.set(guildId, lookup);
|
|
60
|
+
}
|
|
61
|
+
return lookup;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async #fetch(guildId: string): Promise<GuildSnapshot | undefined> {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const client = this.getClient();
|
|
67
|
+
if (!client) return undefined;
|
|
68
|
+
let ownerId: string | undefined;
|
|
69
|
+
const adminRoleIds = new Set<string>();
|
|
70
|
+
try {
|
|
71
|
+
const info = await client.getGuildInfo?.(guildId);
|
|
72
|
+
if (info?.user_id != null && String(info.user_id)) ownerId = String(info.user_id);
|
|
73
|
+
} catch { /* 保守:视为无群主信息 */ }
|
|
74
|
+
try {
|
|
75
|
+
for (const role of await client.pickGuild(guildId).getRoleList()) {
|
|
76
|
+
if ((Number(role.permissions) & KOOK_ROLE_ADMIN_BIT) !== 0) adminRoleIds.add(String(role.role_id));
|
|
77
|
+
}
|
|
78
|
+
} catch { /* 保守:视为无管理员角色 */ }
|
|
79
|
+
const snapshot: GuildSnapshot = { at: now, ...(ownerId ? { ownerId } : {}), adminRoleIds };
|
|
80
|
+
this.#guilds.set(guildId, snapshot);
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/ws.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface KookClientTransport {
|
|
|
35
35
|
uploadMedia?(data: Buffer): Promise<string>;
|
|
36
36
|
recallChannelMsg?(channelId: string, messageId: string): Promise<boolean>;
|
|
37
37
|
recallPrivateMsg?(userId: string, messageId: string): Promise<boolean>;
|
|
38
|
+
/** GET /api/v3/guild/view(群主 id)。 */
|
|
39
|
+
getGuildInfo?(guildId: string): Promise<{ user_id?: string | number }>;
|
|
38
40
|
pickGuild(guildId: string): {
|
|
39
41
|
kick(userId: string): Promise<boolean>;
|
|
40
42
|
getRoleList(): Promise<Array<{
|