@zhin.js/adapter-kook 1.1.10 → 1.1.12
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 +52 -0
- package/README.md +19 -15
- 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 +16 -16
- 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,57 @@
|
|
|
1
1
|
# @zhin.js/adapter-kook
|
|
2
2
|
|
|
3
|
+
## 1.1.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 9e1d1a6: 飞书 / KOOK 查询群主与管理员时,缓存失效那一刻同一个群(服务器)的并发消息共用一次查询,不再各自调用一次接口。
|
|
9
|
+
- 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`.
|
|
10
|
+
- Updated dependencies [1a11207]
|
|
11
|
+
- Updated dependencies [9943df6]
|
|
12
|
+
- Updated dependencies [5be09ae]
|
|
13
|
+
- Updated dependencies [a3ab153]
|
|
14
|
+
- Updated dependencies [e8f7309]
|
|
15
|
+
- Updated dependencies [5afc18f]
|
|
16
|
+
- Updated dependencies [d3563d9]
|
|
17
|
+
- Updated dependencies [c618eb7]
|
|
18
|
+
- Updated dependencies [fbfefb7]
|
|
19
|
+
- @zhin.js/tool@1.1.8
|
|
20
|
+
- @zhin.js/core@1.1.47
|
|
21
|
+
- @zhin.js/adapter@1.1.21
|
|
22
|
+
- @zhin.js/feature-kit@1.1.7
|
|
23
|
+
- @zhin.js/im-contract@1.1.5
|
|
24
|
+
- @zhin.js/agent-feature@1.1.8
|
|
25
|
+
- @zhin.js/skill@1.1.8
|
|
26
|
+
- @zhin.js/host-http@1.1.8
|
|
27
|
+
|
|
28
|
+
## 1.1.11
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- 9816c18: 将应用插件装配改为 configVersion: 2:主配置 plugins 映射实例名到包,disable_plugins 控制启停,业务配置使用顶层同名块。package.json 继续保留 features、entry、engine 等包契约,并支持静态 displayName/icon。
|
|
33
|
+
|
|
34
|
+
这是配置格式的破坏性更新:旧配置不能继续运行,请先停止 Bot,运行 `zhin migrate instances` 预览,再使用 `--apply` 转换;提供备份与中断继续/回滚,无法无损转换的嵌套、optional 和别名碰撞需人工迁移。Runtime、Console、CLI 与创建向导统一使用新声明。
|
|
35
|
+
|
|
36
|
+
保留框架顶层 `menuKeyword` / `menuStyle`,通过当前 generation 的 Root 配置视图供 Command Feature 消费;严格校验和配置修改不会丢失菜单设置,子实例配置保持隔离。
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [c407bd2]
|
|
39
|
+
- Updated dependencies [064b06d]
|
|
40
|
+
- Updated dependencies [2b4d6dc]
|
|
41
|
+
- Updated dependencies [a50fc8a]
|
|
42
|
+
- Updated dependencies [b58548a]
|
|
43
|
+
- Updated dependencies [e914a93]
|
|
44
|
+
- Updated dependencies [e914a93]
|
|
45
|
+
- Updated dependencies [064b06d]
|
|
46
|
+
- @zhin.js/core@1.1.46
|
|
47
|
+
- @zhin.js/feature-kit@1.1.6
|
|
48
|
+
- @zhin.js/host-http@1.1.7
|
|
49
|
+
- @zhin.js/permission@1.1.7
|
|
50
|
+
- @zhin.js/adapter@1.1.20
|
|
51
|
+
- @zhin.js/agent-feature@1.1.7
|
|
52
|
+
- @zhin.js/skill@1.1.7
|
|
53
|
+
- @zhin.js/tool@1.1.7
|
|
54
|
+
|
|
3
55
|
## 1.1.10
|
|
4
56
|
|
|
5
57
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ pnpm add @zhin.js/adapter-kook
|
|
|
23
23
|
- `@zhin.js/core` — `Endpoint.emit(...)` 入站、`outboundMessageToken` 出站
|
|
24
24
|
- `@zhin.js/host-http` — Webhook 模式 POST 路由(WebSocket 不需要)
|
|
25
25
|
- `zhin.js` — `plugin.ts`(`definePlugin`)
|
|
26
|
-
- 配置经插件 `schema.json`
|
|
26
|
+
- 配置经插件 `schema.json` 落到顶层 `<instanceKey>`
|
|
27
27
|
- **WebSocket 路径无需** `@zhin.js/host-http` / `@zhin.js/host-router`
|
|
28
28
|
|
|
29
29
|
入站:`gateway.receive({ conversation, message, content, sender, metadata })`(ConversationRef:`channel` 频道消息带 guild `parent`,`private` 私聊)
|
|
@@ -44,16 +44,18 @@ pnpm add @zhin.js/adapter-kook
|
|
|
44
44
|
## 最小配置
|
|
45
45
|
|
|
46
46
|
```yaml
|
|
47
|
+
configVersion: 2
|
|
47
48
|
# zhin.config.yml(Plugin Runtime)
|
|
48
49
|
plugins:
|
|
49
|
-
kook:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
kook: "@zhin.js/adapter-kook"
|
|
51
|
+
kook:
|
|
52
|
+
# connection: websocket # 默认
|
|
53
|
+
endpoints:
|
|
54
|
+
- id: my-kook-bot
|
|
55
|
+
token: ${KOOK_TOKEN}
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
主配置 `plugins` 映射需引用 `@zhin.js/adapter-kook`(`instanceKey: kook`)。
|
|
57
59
|
|
|
58
60
|
## 环境变量
|
|
59
61
|
|
|
@@ -68,15 +70,17 @@ plugins:
|
|
|
68
70
|
在 KOOK 开发者后台选择 **WebHook** 连接模式,Callback URL 指向 Host 暴露的公网地址(建议在 URL 加 `?compress=0` 便于调试)。
|
|
69
71
|
|
|
70
72
|
```yaml
|
|
73
|
+
configVersion: 2
|
|
71
74
|
plugins:
|
|
72
|
-
kook:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
kook: "@zhin.js/adapter-kook"
|
|
76
|
+
kook:
|
|
77
|
+
connection: webhook
|
|
78
|
+
webhookPath: /kook/webhook
|
|
79
|
+
endpoints:
|
|
80
|
+
- id: my-kook-bot
|
|
81
|
+
token: ${KOOK_TOKEN}
|
|
82
|
+
verify_token: ${KOOK_VERIFY_TOKEN}
|
|
83
|
+
# encrypt_key: ${KOOK_ENCRYPT_KEY} # 启用消息加密时必填
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
Host 需注入 `httpHostToken`。Challenge(`type: 255`)会校验 `verify_token` 并回显 `challenge`;普通事件经 `gateway.receive` 入站,出站仍走 KOOK HTTP API。
|
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.12",
|
|
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.47",
|
|
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
|
-
"@zhin.js/permission": "1.1.
|
|
44
|
-
"@zhin.js/
|
|
45
|
-
"@zhin.js/
|
|
43
|
+
"@zhin.js/permission": "1.1.7",
|
|
44
|
+
"@zhin.js/tool": "1.1.8",
|
|
45
|
+
"@zhin.js/skill": "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.47",
|
|
52
|
+
"zhin.js": "^1.1.12"
|
|
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.12"
|
|
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<{
|