@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/LICENSE +21 -0
  3. package/PERMITS.md +20 -14
  4. package/README.md +6 -2
  5. package/adapters/kook/index.js +3 -0
  6. package/adapters/kook/index.ts +3 -0
  7. package/agents/kook/conversation.js +88 -0
  8. package/agents/kook/conversation.ts +99 -0
  9. package/agents/kook/messages.js +16 -0
  10. package/agents/kook/messages.ts +16 -0
  11. package/agents/kook/skills/kook/tools/blacklist/index.js +9 -6
  12. package/agents/kook/skills/kook/tools/blacklist/index.ts +9 -7
  13. package/agents/kook/skills/kook/tools/create_role/index.js +5 -6
  14. package/agents/kook/skills/kook/tools/create_role/index.ts +6 -7
  15. package/agents/kook/skills/kook/tools/delete_role/index.js +5 -6
  16. package/agents/kook/skills/kook/tools/delete_role/index.ts +6 -7
  17. package/agents/kook/skills/kook/tools/grant_role/index.js +9 -7
  18. package/agents/kook/skills/kook/tools/grant_role/index.ts +9 -8
  19. package/agents/kook/skills/kook/tools/list_roles/index.js +5 -7
  20. package/agents/kook/skills/kook/tools/list_roles/index.ts +6 -8
  21. package/agents/kook/skills/kook/tools/revoke_role/index.js +9 -7
  22. package/agents/kook/skills/kook/tools/revoke_role/index.ts +9 -8
  23. package/lib/endpoint.d.ts +2 -1
  24. package/lib/endpoint.js +27 -12
  25. package/lib/index.d.ts +1 -1
  26. package/lib/index.js +1 -1
  27. package/lib/platform-permit.d.ts +4 -19
  28. package/lib/platform-permit.js +7 -46
  29. package/lib/protocol.d.ts +1 -5
  30. package/lib/protocol.js +3 -28
  31. package/lib/sender-roles.d.ts +6 -6
  32. package/lib/sender-roles.js +55 -21
  33. package/lib/webhook.d.ts +1 -2
  34. package/lib/webhook.js +13 -33
  35. package/package.json +23 -26
  36. package/plugin.js +0 -6
  37. package/schema.json +0 -56
  38. package/src/endpoint.ts +27 -13
  39. package/src/index.ts +1 -7
  40. package/src/platform-permit.ts +5 -70
  41. package/src/protocol.ts +4 -33
  42. package/src/sender-roles.ts +59 -27
  43. package/src/webhook.ts +14 -36
@@ -1,15 +1,34 @@
1
1
  /**
2
- * KOOK 入站发送者角色投影。
2
+ * KOOK 入站发送者平台身份(ADR 0056 §1)。
3
3
  *
4
4
  * 事件只携带数字角色 id,无法直接判断群主 / 管理员;这里按服务器缓存
5
- * guild.view(群主 id)与 guild-role.list(角色权限位),把发送者投影为
6
- * 跨平台规范角色 owner / admin。原始角色 id 以 `role:<id>` 形式放入 permissions,
7
- * 不进入 `sender.roles`。
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';
10
- /** KOOK 角色权限位:0x1 = 管理员。 */
11
- const KOOK_ROLE_ADMIN_BIT = 1;
12
15
  const CACHE_TTL_MS = 60_000;
16
+ /** 「全体成员」:每个成员都持有的基础角色。 */
17
+ const EVERYONE_ROLE_ID = '0';
18
+ /** KOOK 角色权限位(按位序,见 KOOK 开发者文档「权限说明」)。 */
19
+ export const KOOK_ROLE_PERMISSIONS = Object.freeze([
20
+ 'administrator', 'manage_guild', 'view_audit_log', 'create_invite', 'manage_invite', 'manage_channels',
21
+ 'kick_members', 'ban_members', 'manage_emojis', 'change_nickname', 'manage_roles', 'view_channels',
22
+ 'send_messages', 'manage_messages', 'attach_files', 'connect_voice', 'manage_voice', 'mention_everyone',
23
+ 'add_reaction', 'follow_reaction', 'passive_connect_voice', 'push_to_talk_only', 'free_voice_speak', 'speak',
24
+ 'deafen_members', 'mute_members', 'manage_nicknames', 'play_music',
25
+ ]);
26
+ /** KOOK 权限位 → 权限名;未知位丢弃。 */
27
+ export function kookPermissionNames(bits) {
28
+ if (!Number.isFinite(bits) || bits <= 0)
29
+ return [];
30
+ return KOOK_ROLE_PERMISSIONS.filter((_, index) => Math.floor(bits / 2 ** index) % 2 === 1);
31
+ }
13
32
  export class KookSenderRoleResolver {
14
33
  getClient;
15
34
  #guilds = new TtlLookupCache({ ttlMs: CACHE_TTL_MS });
@@ -17,18 +36,30 @@ export class KookSenderRoleResolver {
17
36
  this.getClient = getClient;
18
37
  }
19
38
  async resolve(input) {
20
- const roleIds = (input.authorRoles ?? []).map(String);
21
- const permissions = roleIds.map((id) => `role:${id}`);
22
39
  if (!input.guildId)
23
- return { permissions };
40
+ return undefined;
41
+ const roleIds = [...new Set((input.authorRoles ?? []).map(String))];
24
42
  const snapshot = await this.#snapshot(input.guildId);
25
- if (snapshot?.ownerId !== undefined && snapshot.ownerId === input.authorId) {
26
- return { role: 'owner', permissions: [...permissions, 'guild_owner', 'guild_admin'] };
27
- }
28
- if (snapshot && roleIds.some((id) => snapshot.adminRoleIds.has(id))) {
29
- return { role: 'admin', permissions: [...permissions, 'guild_admin'] };
30
- }
31
- return { permissions };
43
+ if (!snapshot)
44
+ return undefined;
45
+ const held = roleIds.filter((id) => id !== EVERYONE_ROLE_ID).map((id) => ({ id, role: snapshot.roles?.get(id) }));
46
+ const everyone = snapshot.roles?.get(EVERYONE_ROLE_ID)?.permissions ?? 0;
47
+ const bits = held.reduce((sum, { role }) => sum | (role?.permissions ?? 0), everyone);
48
+ const roles = held.map(({ id, role }) => (role?.name ? { id, name: role.name } : { id }));
49
+ const isOwner = snapshot.ownerId !== undefined && snapshot.ownerId === input.authorId;
50
+ const isAdmin = kookPermissionNames(bits).includes('administrator');
51
+ // The server owner and administrators hold every permission (KOOK ignores per-bit grants for them).
52
+ const permissions = isOwner || isAdmin ? [...KOOK_ROLE_PERMISSIONS] : kookPermissionNames(bits);
53
+ // Without the owner id or the role list the standing cannot be decided; an admin role still proves admin.
54
+ const standing = isOwner ? 'owner' : isAdmin ? 'admin'
55
+ : snapshot.ownerId !== undefined && snapshot.roles ? 'member' : undefined;
56
+ if (!standing)
57
+ return undefined;
58
+ return {
59
+ standing,
60
+ ...(roles.length ? { roles } : {}),
61
+ ...(permissions.length ? { permissions } : {}),
62
+ };
32
63
  }
33
64
  async #snapshot(guildId) {
34
65
  return this.#guilds.getOrLoad(guildId, () => this.#fetch(guildId), {
@@ -40,7 +71,7 @@ export class KookSenderRoleResolver {
40
71
  if (!client)
41
72
  return undefined;
42
73
  let ownerId;
43
- const adminRoleIds = new Set();
74
+ let roles;
44
75
  try {
45
76
  const info = await client.getGuildInfo?.(guildId);
46
77
  if (info?.user_id != null && String(info.user_id))
@@ -48,12 +79,15 @@ export class KookSenderRoleResolver {
48
79
  }
49
80
  catch { /* 保守:视为无群主信息 */ }
50
81
  try {
82
+ roles = new Map();
51
83
  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));
84
+ const name = typeof role.name === 'string' ? role.name : undefined;
85
+ roles.set(String(role.role_id), { ...(name ? { name } : {}), permissions: Number(role.permissions) || 0 });
54
86
  }
55
87
  }
56
- catch { /* 保守:视为无管理员角色 */ }
57
- return { ...(ownerId ? { ownerId } : {}), adminRoleIds };
88
+ catch {
89
+ roles = undefined; /* 保守:视为无角色信息 */
90
+ }
91
+ return { ...(ownerId ? { ownerId } : {}), ...(roles ? { roles } : {}) };
58
92
  }
59
93
  }
package/lib/webhook.d.ts CHANGED
@@ -2,7 +2,6 @@ import { 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 { type KookInboundMessage, type KookWebhookEventData, type ResolvedKookWebhookConfig } from './protocol.js';
8
7
  export interface KookWebhookHandler extends Endpoint {
@@ -15,4 +14,4 @@ export interface KookWebhookHandler extends Endpoint {
15
14
  forgetSn?(sn: number): void;
16
15
  }
17
16
  export declare function registerKookWebhookRoutes(http: HttpHost, handler: KookWebhookHandler): HttpRouteRegistration[];
18
- export declare function handleKookWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: KookWebhookHandler): Promise<void>;
17
+ export declare function handleKookWebhookRequest(request: Request, handler: KookWebhookHandler): Promise<Response>;
package/lib/webhook.js CHANGED
@@ -5,12 +5,10 @@ const logger = getLogger('kook');
5
5
  export function registerKookWebhookRoutes(http, handler) {
6
6
  const path = handler.endpointConfig.webhookPath;
7
7
  return [
8
- http.route('POST', path, async (request, response) => {
9
- await handleKookWebhookRequest(request, response, handler);
10
- }, { summary: 'KOOK webhook callback', tags: ['kook'] }),
8
+ http.route('POST', path, (request) => handleKookWebhookRequest(request, handler), { summary: 'KOOK webhook callback', tags: ['kook'] }),
11
9
  ];
12
10
  }
13
- export async function handleKookWebhookRequest(request, response, handler) {
11
+ export async function handleKookWebhookRequest(request, handler) {
14
12
  try {
15
13
  const rawBody = await readRequestBody(request);
16
14
  let frame;
@@ -23,42 +21,28 @@ export async function handleKookWebhookRequest(request, response, handler) {
23
21
  ok: false,
24
22
  error: error instanceof Error ? error.message : String(error),
25
23
  }));
26
- response.writeHead(400, { 'Content-Type': 'application/json' });
27
- response.end(JSON.stringify({ message: 'Invalid webhook payload' }));
28
- return;
24
+ return new Response(JSON.stringify({ message: 'Invalid webhook payload' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
29
25
  }
30
26
  const event = frame.d;
31
27
  if (!event || typeof event !== 'object') {
32
- response.writeHead(200, { 'Content-Type': 'application/json' });
33
- response.end(JSON.stringify({ message: 'OK' }));
34
- return;
28
+ return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
35
29
  }
36
30
  if (isKookWebhookChallenge(event)) {
37
31
  if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
38
- response.writeHead(403, { 'Content-Type': 'application/json' });
39
- response.end(JSON.stringify({ message: 'Invalid verify_token' }));
40
- return;
32
+ return new Response(JSON.stringify({ message: 'Invalid verify_token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
41
33
  }
42
- response.writeHead(200, { 'Content-Type': 'application/json' });
43
- response.end(JSON.stringify({ challenge: event.challenge ?? '' }));
44
- return;
34
+ return new Response(JSON.stringify({ challenge: event.challenge ?? '' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
45
35
  }
46
36
  if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
47
- response.writeHead(403, { 'Content-Type': 'application/json' });
48
- response.end(JSON.stringify({ message: 'Invalid verify_token' }));
49
- return;
37
+ return new Response(JSON.stringify({ message: 'Invalid verify_token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
50
38
  }
51
39
  if (!handler.isOpen) {
52
- response.writeHead(503, { 'Content-Type': 'application/json' });
53
- response.end(JSON.stringify({ message: 'Endpoint unavailable' }));
54
- return;
40
+ return new Response(JSON.stringify({ message: 'Endpoint unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
55
41
  }
56
- await handler[endpointSpec].events.run(async () => {
42
+ return await handler[endpointSpec].events.run(async () => {
57
43
  if (frame.sn != null) {
58
44
  if (!handler.checkAndRememberSn(frame.sn)) {
59
- response.writeHead(200, { 'Content-Type': 'application/json' });
60
- response.end(JSON.stringify({ message: 'OK' }));
61
- return;
45
+ return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
62
46
  }
63
47
  }
64
48
  try {
@@ -67,17 +51,13 @@ export async function handleKookWebhookRequest(request, response, handler) {
67
51
  catch {
68
52
  if (frame.sn != null)
69
53
  handler.forgetSn?.(frame.sn);
70
- response.writeHead(503, { 'Content-Type': 'application/json' });
71
- response.end(JSON.stringify({ message: 'Admission unavailable' }));
72
- return;
54
+ return new Response(JSON.stringify({ message: 'Admission unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
73
55
  }
74
- response.writeHead(200, { 'Content-Type': 'application/json' });
75
- response.end(JSON.stringify({ message: 'OK' }));
56
+ return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
76
57
  });
77
58
  }
78
59
  catch (error) {
79
60
  logger.error('KOOK webhook error:', error);
80
- response.writeHead(200, { 'Content-Type': 'application/json' });
81
- response.end(JSON.stringify({ message: 'OK' }));
61
+ return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
82
62
  }
83
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-kook",
3
- "version": "1.1.26",
3
+ "version": "1.1.27",
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,25 +33,26 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "kook-client": "^1.0.5",
36
- "@zhin.js/adapter": "1.1.32",
37
- "@zhin.js/agent-feature": "1.1.16",
38
- "@zhin.js/console-contract": "1.1.5",
39
- "@zhin.js/core": "1.1.60",
40
- "@zhin.js/feature-kit": "1.1.15",
41
- "@zhin.js/host-http": "1.1.17",
42
- "@zhin.js/im-contract": "1.1.13",
43
- "@zhin.js/logger": "1.1.2",
44
- "@zhin.js/page": "1.1.17",
45
- "@zhin.js/permission": "1.1.15",
46
- "@zhin.js/skill": "1.1.16",
47
- "@zhin.js/tool": "1.1.18"
36
+ "ws": "^8.21.1",
37
+ "zod": "^4.6.7",
38
+ "@zhin.js/adapter": "1.1.33",
39
+ "@zhin.js/agent-feature": "1.1.17",
40
+ "@zhin.js/console-contract": "1.1.6",
41
+ "@zhin.js/core": "1.2.0",
42
+ "@zhin.js/feature-kit": "1.1.16",
43
+ "@zhin.js/host-http": "1.2.0",
44
+ "@zhin.js/im-contract": "1.1.14",
45
+ "@zhin.js/logger": "1.1.3",
46
+ "@zhin.js/page": "1.1.18",
47
+ "@zhin.js/permission": "1.2.0",
48
+ "@zhin.js/skill": "1.1.17",
49
+ "@zhin.js/tool": "1.1.19"
48
50
  },
49
51
  "peerDependencies": {
50
- "zod": "^4.0.0",
51
- "@zhin.js/adapter": "^1.1.32",
52
- "@zhin.js/command": "^1.1.17",
53
- "@zhin.js/core": "^1.1.60",
54
- "zhin.js": "^1.1.25"
52
+ "@zhin.js/adapter": "^1.1.33",
53
+ "@zhin.js/command": "^1.2.0",
54
+ "@zhin.js/core": "^1.2.0",
55
+ "zhin.js": "^1.1.26"
55
56
  },
56
57
  "peerDependenciesMeta": {
57
58
  "@zhin.js/command": {
@@ -59,18 +60,15 @@
59
60
  },
60
61
  "zhin.js": {
61
62
  "optional": true
62
- },
63
- "zod": {
64
- "optional": true
65
63
  }
66
64
  },
67
65
  "devDependencies": {
68
66
  "@types/node": "^26.1.2",
67
+ "@types/ws": "^8.18.1",
69
68
  "typescript": "^6.0.3",
70
69
  "vitest": "^4.1.10",
71
- "zod": "^4.4.3",
72
- "@zhin.js/host-http": "1.1.17",
73
- "zhin.js": "1.1.25"
70
+ "@zhin.js/host-http": "1.2.0",
71
+ "zhin.js": "1.1.26"
74
72
  },
75
73
  "files": [
76
74
  "adapters",
@@ -80,7 +78,6 @@
80
78
  "schema.json",
81
79
  "src",
82
80
  "lib",
83
- "tools",
84
81
  "agents",
85
82
  "README.md",
86
83
  "CHANGELOG.md",
@@ -130,6 +127,6 @@
130
127
  "scripts": {
131
128
  "build": "tsc",
132
129
  "clean": "rimraf lib",
133
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/kook/tests"
130
+ "test": "vitest run --root ../../.. plugins/adapters/kook/tests"
134
131
  }
135
132
  }
package/plugin.js CHANGED
@@ -1,7 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin } from 'zhin.js';
3
- import { permissionHostToken } from '@zhin.js/permission';
4
- import { checkKookPlatformPermit } from "./lib/platform-permit.js";
5
3
  import { createKookConsoleRpc } from "./lib/console.js";
6
4
  export default definePlugin({
7
5
  name: 'kook',
@@ -10,12 +8,8 @@ export default definePlugin({
10
8
  displayName: 'KOOK WebSocket Adapter',
11
9
  },
12
10
  setup(context) {
13
- const dispose = context.resources.has(permissionHostToken)
14
- ? context.resources.use(permissionHostToken).registerPlatform('kook', checkKookPlatformPermit)
15
- : undefined;
16
11
  return {
17
12
  consoleRpc: createKookConsoleRpc(() => context.config.get()),
18
- ...(dispose ? { dispose } : {}),
19
13
  };
20
14
  },
21
15
  });
package/schema.json CHANGED
@@ -50,34 +50,6 @@
50
50
  ],
51
51
  "default": "info"
52
52
  },
53
- "master": {
54
- "type": [
55
- "string",
56
- "number"
57
- ],
58
- "description": "框架 master(KOOK user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
59
- "x-i18n": {
60
- "en": {
61
- "description": "Framework masters (KOOK user id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
62
- }
63
- }
64
- },
65
- "trusted": {
66
- "type": "array",
67
- "items": {
68
- "type": [
69
- "string",
70
- "number"
71
- ],
72
- "description": "Trusted KOOK user id"
73
- },
74
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
75
- "x-i18n": {
76
- "en": {
77
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
78
- }
79
- }
80
- },
81
53
  "endpoints": {
82
54
  "type": "array",
83
55
  "minItems": 1,
@@ -86,34 +58,6 @@
86
58
  "type": "object",
87
59
  "additionalProperties": false,
88
60
  "properties": {
89
- "master": {
90
- "type": [
91
- "string",
92
- "number"
93
- ],
94
- "description": "本 endpoint 的框架 master(KOOK user id);覆盖顶层 master",
95
- "x-i18n": {
96
- "en": {
97
- "description": "Framework masters for this endpoint (KOOK user id), overriding the top-level master list."
98
- }
99
- }
100
- },
101
- "trusted": {
102
- "type": "array",
103
- "items": {
104
- "type": [
105
- "string",
106
- "number"
107
- ],
108
- "description": "Trusted KOOK user id"
109
- },
110
- "description": "本 endpoint 的 trusted 列表",
111
- "x-i18n": {
112
- "en": {
113
- "description": "Trusted users for this endpoint."
114
- }
115
- }
116
- },
117
61
  "token": {
118
62
  "type": "string",
119
63
  "description": "KOOK bot token"
package/src/endpoint.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EndpointDeliveryError, type ConversationRef } from '@zhin.js/im-contract';
1
+ import { EndpointDeliveryError, type ConversationRef, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
2
2
  /**
3
3
  * KookEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
4
4
  */
@@ -40,7 +40,7 @@ import {
40
40
  type KookClientTransport,
41
41
  } from './ws.js';
42
42
  import { recallKookMessage, sendKookOutbound } from './outbound.js';
43
- import { KookSenderRoleResolver, type KookSenderRoleProjection } from './sender-roles.js';
43
+ import { KookSenderRoleResolver } from './sender-roles.js';
44
44
  import { receiveKookSideEvent } from './side-event-dispatch.js';
45
45
  import { createKookSelfProfile } from './self-profile.js';
46
46
 
@@ -49,12 +49,12 @@ export interface KookEndpointOptions {
49
49
  readonly config: ResolvedKookWebsocketConfig;
50
50
  }
51
51
 
52
- /** 入站 message.receive 载荷:规范角色 owner/admin 进 roles,原始角色 id 进 permissions。 */
52
+ /** 入站 message.receive 载荷:发送者平台身份进 `sender.platform`(ADR 0056 §1)。 */
53
53
  function kookInboundPayload(
54
54
  endpointId: string,
55
55
  conversation: ConversationRef,
56
56
  msg: KookInboundMessage,
57
- projection: KookSenderRoleProjection,
57
+ platform: PlatformStanding | undefined,
58
58
  selfId: string | undefined,
59
59
  ) {
60
60
  return {
@@ -66,8 +66,7 @@ function kookInboundPayload(
66
66
  sender: {
67
67
  id: msg.authorId,
68
68
  name: senderDisplayName(msg) || undefined,
69
- ...(projection.role ? { roles: [projection.role] } : {}),
70
- ...(projection.permissions.length ? { permissions: [...projection.permissions] } : {}),
69
+ ...(platform ? { platform } : {}),
71
70
  },
72
71
  endpointId,
73
72
  ...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
@@ -76,7 +75,6 @@ function kookInboundPayload(
76
75
  userId: msg.authorId,
77
76
  guildId: msg.guildId,
78
77
  authorRoleIds: msg.authorRoles,
79
- ...(projection.role ? { senderRole: projection.role } : {}),
80
78
  }),
81
79
  };
82
80
  }
@@ -91,7 +89,7 @@ export class KookWebsocketEndpoint extends RuntimeKookClient implements Endpoint
91
89
  #open = false;
92
90
  #executionSignal?: AbortSignal;
93
91
  #started = false;
94
- readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
92
+ readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient(), this.#roles);
95
93
  readonly #control: EndpointControl = Object.freeze<EndpointControl>({
96
94
  recall: request => recallKookMessage(this.#requireClient(), request),
97
95
  });
@@ -172,10 +170,10 @@ export class KookWebsocketEndpoint extends RuntimeKookClient implements Endpoint
172
170
  if (msg.authorBot) return;
173
171
  const conversation = kookInboundConversation(String(this.#options.id), msg);
174
172
  const selfId = this.self_id != null ? String(this.self_id) : undefined;
175
- return this.#roles.resolve(msg).then((projection) => {
173
+ return this.#roles.resolve(msg).then((platform) => {
176
174
  this.#executionSignal?.throwIfAborted();
177
175
  return this[endpointSpec].events.emit('message.receive',
178
- kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId));
176
+ kookInboundPayload(this.#options.config.id, conversation, msg, platform, selfId));
179
177
  }).catch((err) => {
180
178
  this.#logger.warn(formatCompact({
181
179
  op: 'kook_gateway_receive_failed',
@@ -255,7 +253,7 @@ export class KookWebhookEndpoint extends RuntimeKookClient implements Endpoint {
255
253
  #open = false;
256
254
  #executionSignal?: AbortSignal;
257
255
  #started = false;
258
- readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
256
+ readonly #management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient(), this.#roles);
259
257
  readonly #control: EndpointControl = Object.freeze<EndpointControl>({
260
258
  recall: request => recallKookMessage(this.#requireClient(), request),
261
259
  });
@@ -374,10 +372,10 @@ export class KookWebhookEndpoint extends RuntimeKookClient implements Endpoint {
374
372
  if (msg.authorBot) return;
375
373
  const conversation = kookInboundConversation(String(this.#options.id), msg);
376
374
  const selfId = this.self_id != null ? String(this.self_id) : undefined;
377
- return this.#roles.resolve(msg).then((projection) => {
375
+ return this.#roles.resolve(msg).then((platform) => {
378
376
  this.#executionSignal?.throwIfAborted();
379
377
  return this[endpointSpec].events.emit('message.receive',
380
- kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId));
378
+ kookInboundPayload(this.#options.config.id, conversation, msg, platform, selfId));
381
379
  }).catch((err) => {
382
380
  this.#logger.warn(formatCompact({
383
381
  op: 'kook_gateway_receive_failed',
@@ -438,8 +436,24 @@ function isKookTextChannelType(type: string | number | undefined): boolean {
438
436
  */
439
437
  export function createKookEndpointManagement(
440
438
  requireClient: () => KookClientTransport,
439
+ roles: KookSenderRoleResolver = new KookSenderRoleResolver(requireClient),
441
440
  ): EndpointManagement {
442
441
  return Object.freeze<EndpointManagement>({
442
+ /**
443
+ * ADR 0056 §1: the Bot's own standing in the channel's guild (`/v3/user/view` self → its role
444
+ * ids, then the same guild owner / role-permission resolution as senders).
445
+ */
446
+ async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
447
+ const guildId = conversation.kind === 'channel' ? conversation.parent?.id : undefined;
448
+ if (!guildId) return undefined;
449
+ const operation = createEndpointOperation({ signal });
450
+ const client = requireClient();
451
+ const selfId = client.self_id != null ? String(client.self_id) : undefined;
452
+ if (!selfId) return undefined;
453
+ const { data } = await operation.run(() => client.request.get('/v3/user/view', { params: { user_id: selfId, guild_id: guildId } }));
454
+ const held = Array.isArray((data as { roles?: unknown })?.roles) ? (data as { roles: unknown[] }).roles.map(Number).filter(Number.isFinite) : [];
455
+ return roles.resolve({ guildId, authorId: selfId, authorRoles: held });
456
+ },
443
457
  async listGroups(request: EndpointManagementRequest = {}): Promise<readonly EndpointGroup[]> {
444
458
  const operation = createEndpointOperation(request);
445
459
  const guilds = await operation.run(() => requireClient().getGuildList());
package/src/index.ts CHANGED
@@ -36,10 +36,4 @@ export {
36
36
  export { type KookClientEventMap } from './client.js';
37
37
  export { KookAdapter } from './adapter.js';
38
38
 
39
- export {
40
- checkKookPlatformPermit,
41
- kookGroupPermitResolver,
42
- normalizeKookSenderForPermit,
43
- platformPermit,
44
- type KookSenderInfo,
45
- } from './platform-permit.js';
39
+ export { platformPermit } from './platform-permit.js';
@@ -1,75 +1,10 @@
1
1
  /**
2
- * KOOK platform permit — guild / channel 差异化门禁
2
+ * KOOK platform permit helper(ADR 0056 §5)。
3
+ *
4
+ * 平台身份由适配器上报到 `sender.platform`(见 `sender-roles.ts`),permit 由内置评估器
5
+ * (`@zhin.js/permission`)判断;适配器不再导出自有的 permit 解析器或检查函数。
3
6
  */
4
- import type { PermissionSubject } from '@zhin.js/permission';
5
- import { KookPermission } from './protocol.js';
6
-
7
- const ADAPTER = 'kook';
8
7
 
9
8
  export function platformPermit(perm: string): string {
10
- return `platform(${ADAPTER},${perm})`;
11
- }
12
-
13
- const FACTORY_PERM_MAP: Record<string, string> = {
14
- scene_admin: 'guild_admin',
15
- scene_owner: 'guild_owner',
16
- };
17
-
18
- export function kookGroupPermitResolver(logicalPerm: string): string {
19
- return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
9
+ return `platform-permission:${perm}`;
20
10
  }
21
-
22
- export interface KookSenderInfo {
23
- id: string;
24
- name: string;
25
- permission?: KookPermission;
26
- roles?: number[];
27
- isGuildOwner?: boolean;
28
- isAdmin?: boolean;
29
- role?: string;
30
- permissions?: string[];
31
- }
32
-
33
- export function normalizeKookSenderForPermit(
34
- info: KookSenderInfo,
35
- isChannel: boolean,
36
- ): { role?: string; permissions?: string[] } {
37
- const permissions: string[] = [];
38
- if (info.roles?.length) {
39
- permissions.push(...info.roles.map((r) => `role:${r}`));
40
- }
41
-
42
- if (info.isGuildOwner || info.permission === KookPermission.Owner) {
43
- permissions.push('guild_owner', 'guild_admin');
44
- return { role: 'owner', permissions };
45
- }
46
- if (info.permission === KookPermission.ChannelAdmin && isChannel) {
47
- permissions.push('channel_admin', 'guild_admin');
48
- return { role: 'channel_admin', permissions };
49
- }
50
- if (info.isAdmin || info.permission === KookPermission.Admin) {
51
- permissions.push('guild_admin');
52
- return { role: 'admin', permissions };
53
- }
54
- return { role: 'member', permissions };
55
- }
56
-
57
- export function checkKookPlatformPermit(perm: string, subject: PermissionSubject): boolean {
58
- const role = subject.sender?.role?.[0];
59
- const permissions = subject.sender?.permissions ?? [];
60
- const has = (t: string) => permissions.includes(t);
61
-
62
- switch (perm) {
63
- case 'guild_owner':
64
- return role === 'owner' || has('guild_owner');
65
- case 'guild_admin':
66
- return role === 'owner' || role === 'admin' || role === 'channel_admin' || has('guild_admin') || has('channel_admin');
67
- case 'channel_admin':
68
- return role === 'owner' || role === 'admin' || role === 'channel_admin' || has('channel_admin') || has('guild_admin');
69
- case 'manage_roles':
70
- return role === 'owner' || role === 'admin' || has('guild_owner') || has('guild_admin');
71
- default:
72
- return false;
73
- }
74
- }
75
-
package/src/protocol.ts CHANGED
@@ -3,7 +3,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
3
3
 
4
4
  import { createDecipheriv, timingSafeEqual } from 'node:crypto';
5
5
  import { inflateSync } from 'node:zlib';
6
- import type { IncomingMessage } from 'node:http';
6
+ import { readBodyBytes } from '@zhin.js/host-http';
7
7
  import { EndpointDeliveryError, isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
8
8
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
9
  import type { Locale } from 'zhin.js/i18n';
@@ -329,42 +329,13 @@ export function normalizeWebhookPath(path: string): string {
329
329
  return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
330
330
  }
331
331
 
332
- export async function readTextBody(
333
- request: IncomingMessage,
334
- options: { readonly limit?: number } = {},
335
- ): Promise<string> {
336
- const limit = options.limit ?? 1_048_576;
337
- const chunks: Buffer[] = [];
338
- let size = 0;
339
- for await (const chunk of request) {
340
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
341
- size += buffer.length;
342
- if (size > limit) {
343
- request.destroy();
344
- throw new Error(`Request body exceeds ${limit} bytes`);
345
- }
346
- chunks.push(buffer);
347
- }
348
- return Buffer.concat(chunks).toString('utf8');
349
- }
350
332
 
351
333
  export async function readRequestBody(
352
- request: IncomingMessage,
334
+ request: Request,
353
335
  options: { readonly limit?: number } = {},
354
336
  ): Promise<Buffer> {
355
- const limit = options.limit ?? 1_048_576;
356
- const chunks: Buffer[] = [];
357
- let size = 0;
358
- for await (const chunk of request) {
359
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
360
- size += buffer.length;
361
- if (size > limit) {
362
- request.destroy();
363
- throw new Error(`Request body exceeds ${limit} bytes`);
364
- }
365
- chunks.push(buffer);
366
- }
367
- return Buffer.concat(chunks);
337
+ // Oversize bodies throw (the webhook answers its generic OK), as before.
338
+ return Buffer.from(await readBodyBytes(request, { limit: options.limit ?? 1_048_576 }));
368
339
  }
369
340
 
370
341
  export function padKookEncryptKey(key: string): Buffer {