@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 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.11",
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.20",
37
- "@zhin.js/agent-feature": "1.1.7",
38
- "@zhin.js/core": "1.1.46",
39
- "@zhin.js/feature-kit": "1.1.6",
40
- "@zhin.js/host-http": "1.1.7",
41
- "@zhin.js/im-contract": "1.1.4",
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.7",
45
- "@zhin.js/tool": "1.1.7"
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.20",
50
- "@zhin.js/command": "^1.1.8",
51
- "@zhin.js/core": "^1.1.46",
52
- "zhin.js": "^1.1.11"
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.7",
71
- "zhin.js": "1.1.11"
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
- message: { conversation, id: msg.id },
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
- message: { conversation, id: msg.id },
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<{