@zhin.js/adapter-kook 5.0.2 → 5.0.3

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 (88) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +58 -452
  3. package/adapters/kook.ts +46 -0
  4. package/agent/tools/blacklist.ts +2 -2
  5. package/agent/tools/create_role.ts +2 -2
  6. package/agent/tools/delete_role.ts +2 -2
  7. package/agent/tools/grant_role.ts +2 -2
  8. package/agent/tools/list_roles.ts +2 -2
  9. package/agent/tools/revoke_role.ts +2 -2
  10. package/lib/endpoint.d.ts +84 -0
  11. package/lib/endpoint.js +299 -0
  12. package/lib/index.d.ts +6 -0
  13. package/lib/index.js +6 -0
  14. package/lib/kook-agent-deps.d.ts +29 -0
  15. package/lib/kook-agent-deps.js +30 -0
  16. package/lib/platform-permit.d.ts +23 -0
  17. package/lib/{src/platform-permit.js → platform-permit.js} +2 -3
  18. package/lib/protocol.d.ts +147 -0
  19. package/lib/protocol.js +279 -0
  20. package/lib/webhook.d.ts +15 -0
  21. package/lib/webhook.js +76 -0
  22. package/lib/ws.d.ts +42 -0
  23. package/lib/ws.js +63 -0
  24. package/package.json +44 -40
  25. package/plugin.ts +13 -0
  26. package/schema.json +55 -0
  27. package/src/endpoint.ts +287 -879
  28. package/src/index.ts +55 -177
  29. package/src/kook-agent-deps.ts +44 -11
  30. package/src/platform-permit.ts +13 -3
  31. package/src/protocol.ts +429 -0
  32. package/src/webhook.ts +116 -0
  33. package/src/ws.ts +120 -0
  34. package/client/Dashboard.tsx +0 -255
  35. package/client/index.tsx +0 -11
  36. package/client/tsconfig.json +0 -7
  37. package/client/utils/api.ts +0 -30
  38. package/dist/index.js +0 -31
  39. package/lib/agent/tools/blacklist.js +0 -33
  40. package/lib/agent/tools/blacklist.js.map +0 -1
  41. package/lib/agent/tools/create_role.js +0 -25
  42. package/lib/agent/tools/create_role.js.map +0 -1
  43. package/lib/agent/tools/delete_role.js +0 -21
  44. package/lib/agent/tools/delete_role.js.map +0 -1
  45. package/lib/agent/tools/grant_role.js +0 -22
  46. package/lib/agent/tools/grant_role.js.map +0 -1
  47. package/lib/agent/tools/list_roles.js +0 -27
  48. package/lib/agent/tools/list_roles.js.map +0 -1
  49. package/lib/agent/tools/revoke_role.js +0 -22
  50. package/lib/agent/tools/revoke_role.js.map +0 -1
  51. package/lib/src/adapter.js +0 -67
  52. package/lib/src/adapter.js.map +0 -1
  53. package/lib/src/endpoint.js +0 -796
  54. package/lib/src/endpoint.js.map +0 -1
  55. package/lib/src/index.js +0 -197
  56. package/lib/src/index.js.map +0 -1
  57. package/lib/src/kook-agent-deps.js +0 -10
  58. package/lib/src/kook-agent-deps.js.map +0 -1
  59. package/lib/src/kook-asset-upload.js +0 -46
  60. package/lib/src/kook-asset-upload.js.map +0 -1
  61. package/lib/src/kook-inbound.js +0 -22
  62. package/lib/src/kook-inbound.js.map +0 -1
  63. package/lib/src/kook-msg-route.js +0 -106
  64. package/lib/src/kook-msg-route.js.map +0 -1
  65. package/lib/src/kook-side-events.js +0 -166
  66. package/lib/src/kook-side-events.js.map +0 -1
  67. package/lib/src/outbound-keyboard.js +0 -66
  68. package/lib/src/outbound-keyboard.js.map +0 -1
  69. package/lib/src/outbound-media.js +0 -45
  70. package/lib/src/outbound-media.js.map +0 -1
  71. package/lib/src/outbound-sendable.js +0 -64
  72. package/lib/src/outbound-sendable.js.map +0 -1
  73. package/lib/src/platform-permit.js.map +0 -1
  74. package/lib/src/segment-mapper.js +0 -2
  75. package/lib/src/segment-mapper.js.map +0 -1
  76. package/lib/src/types.js +0 -8
  77. package/lib/src/types.js.map +0 -1
  78. package/plugin.yml +0 -3
  79. package/src/adapter.ts +0 -75
  80. package/src/kook-asset-upload.ts +0 -55
  81. package/src/kook-inbound.ts +0 -22
  82. package/src/kook-msg-route.ts +0 -122
  83. package/src/kook-side-events.ts +0 -230
  84. package/src/outbound-keyboard.ts +0 -77
  85. package/src/outbound-media.ts +0 -60
  86. package/src/outbound-sendable.ts +0 -73
  87. package/src/segment-mapper.ts +0 -1
  88. package/src/types.ts +0 -58
@@ -1,230 +0,0 @@
1
- /**
2
- * KOOK Gateway 非聊天入站:系统消息 (type=255) / BROADCAST → Notice
3
- */
4
- import {
5
- Notice,
6
- Message,
7
- mapNoticeParts,
8
- senderFromId,
9
- buildNotice,
10
- formatCompact,
11
- } from "zhin.js";
12
-
13
- export interface KookGatewayEvent {
14
- channel_type?: string;
15
- type?: number;
16
- target_id?: string;
17
- author_id?: string;
18
- content?: string;
19
- msg_id?: string;
20
- msg_timestamp?: number;
21
- nonce?: string;
22
- extra?: KookGatewayExtra;
23
- [key: string]: unknown;
24
- }
25
-
26
- export interface KookGatewayExtra {
27
- type?: string;
28
- body?: Record<string, unknown>;
29
- guild_id?: string;
30
- channel_id?: string;
31
- channel_name?: string;
32
- author?: { id?: string; username?: string; nickname?: string };
33
- [key: string]: unknown;
34
- }
35
-
36
- const SYSTEM_MESSAGE_TYPE = 255;
37
-
38
- export function isKookNoticeGatewayEvent(data: unknown): data is KookGatewayEvent {
39
- if (!data || typeof data !== "object") return false;
40
- const ev = data as KookGatewayEvent;
41
- if (ev.type === SYSTEM_MESSAGE_TYPE) return true;
42
- return ev.channel_type === "BROADCAST";
43
- }
44
-
45
- export function resolveKookSideEventDedupeKey(
46
- event: KookGatewayEvent,
47
- kind: "notice" | "gateway",
48
- ): string {
49
- if (event.msg_id) return `${kind}:${event.msg_id}`;
50
- const extra = event.extra ?? {};
51
- const noticeType = extra.type ?? String(event.type ?? "");
52
- const scope = event.target_id ?? "";
53
- const ts = event.msg_timestamp ?? 0;
54
- return `${kind}:${ts}_${noticeType}_${scope}`;
55
- }
56
-
57
- function readString(obj: Record<string, unknown> | undefined, key: string): string | undefined {
58
- const v = obj?.[key];
59
- if (v == null || v === "") return undefined;
60
- return String(v);
61
- }
62
-
63
- export function resolveKookNoticeChannel(
64
- event: KookGatewayEvent,
65
- ): { id: string; type: "group" | "private" | "channel" } {
66
- const extra = event.extra ?? {};
67
- const body = extra.body ?? {};
68
- const rawType = String(extra.type ?? "");
69
-
70
- if (event.channel_type === "BROADCAST") {
71
- const guildId =
72
- readString(body, "guild_id")
73
- ?? extra.guild_id
74
- ?? event.target_id
75
- ?? "";
76
- return { id: guildId, type: "group" };
77
- }
78
-
79
- if (event.type === SYSTEM_MESSAGE_TYPE && event.channel_type === "GROUP") {
80
- const guildId =
81
- readString(body, "guild_id")
82
- ?? extra.guild_id
83
- ?? event.target_id
84
- ?? "";
85
- if (
86
- rawType === "joined_guild"
87
- || rawType === "exited_guild"
88
- || rawType === "updated_guild"
89
- || rawType === "deleted_guild"
90
- || rawType.startsWith("added_role")
91
- || rawType.startsWith("deleted_role")
92
- || rawType.startsWith("updated_role")
93
- || rawType.startsWith("added_block")
94
- || rawType.startsWith("deleted_block")
95
- || rawType === "self_joined_guild"
96
- || rawType === "self_exited_guild"
97
- ) {
98
- return { id: guildId, type: "group" };
99
- }
100
- const channelId =
101
- readString(body, "channel_id")
102
- ?? extra.channel_id
103
- ?? event.target_id
104
- ?? "";
105
- return { id: channelId, type: "channel" };
106
- }
107
-
108
- if (
109
- rawType.startsWith("private_")
110
- || rawType.includes("private_message")
111
- || event.channel_type === "PERSON"
112
- ) {
113
- const userId =
114
- readString(body, "user_id")
115
- ?? readString(body, "target_id")
116
- ?? event.target_id
117
- ?? event.author_id
118
- ?? "";
119
- return { id: userId, type: "private" };
120
- }
121
-
122
- const channelId =
123
- readString(body, "channel_id")
124
- ?? extra.channel_id
125
- ?? event.target_id
126
- ?? "";
127
- return { id: channelId, type: "channel" };
128
- }
129
-
130
- export function enrichKookGatewayForPlugins(event: KookGatewayEvent): KookGatewayEvent {
131
- const extra = event.extra ?? {};
132
- const noticeType = extra.type;
133
- if (!isKookNoticeGatewayEvent(event)) {
134
- return { ...(event as object), post_type: "message" } as KookGatewayEvent;
135
- }
136
- return {
137
- ...(event as object),
138
- post_type: "notice",
139
- notice_type: noticeType ?? "system",
140
- } as KookGatewayEvent;
141
- }
142
-
143
- export function formatKookNotice(
144
- event: KookGatewayEvent,
145
- endpointName: string,
146
- ) {
147
- const extra = event.extra ?? {};
148
- const body = extra.body ?? {};
149
- const rawType = String(extra.type ?? "unknown");
150
- const { scene_type, sub_type } = mapNoticeParts('kook', rawType);
151
- const channel = resolveKookNoticeChannel(event);
152
- const ts = event.msg_timestamp ?? Date.now();
153
-
154
- const targetId =
155
- readString(body, "user_id")
156
- ?? readString(body, "msg_id")
157
- ?? readString(body, "target_id");
158
- const operatorId =
159
- readString(body, "operator_id")
160
- ?? (event.author_id !== "1" ? event.author_id : undefined);
161
-
162
- return buildNotice(event, {
163
- $id: resolveKookSideEventDedupeKey(event, "notice"),
164
- $adapter: "kook",
165
- $endpoint: endpointName,
166
- $type: "notice",
167
- $scene_id: channel.id,
168
- $scene_type: scene_type,
169
- $sub_type: sub_type,
170
- $actor: senderFromId(operatorId),
171
- $target: senderFromId(targetId),
172
- $timestamp: ts,
173
- });
174
- }
175
-
176
- export function formatKookNoticeLog(notice: ReturnType<typeof formatKookNotice>): string {
177
- return formatCompact({
178
- notice: `${notice.$type}.${notice.$scene_type}.${notice.$sub_type}`,
179
- kook_type: notice.$sub_type,
180
- scene: `${notice.$scene_type}(${notice.$scene_id})`,
181
- endpoint: notice.$endpoint,
182
- });
183
- }
184
-
185
- export function isKookButtonClickEvent(event: KookGatewayEvent): boolean {
186
- return event.type === SYSTEM_MESSAGE_TYPE && event.extra?.type === 'message_btn_click';
187
- }
188
-
189
- export function formatKookButtonClickMessage(
190
- event: KookGatewayEvent,
191
- endpointName: string,
192
- ): ReturnType<typeof Message.from<KookGatewayEvent>> {
193
- const extra = event.extra ?? {};
194
- const body = extra.body ?? {};
195
- const payload = String(body.value ?? '');
196
- const userInfo = body.user_info as { id?: string; username?: string; nickname?: string } | undefined;
197
- const userId = String(body.user_id ?? userInfo?.id ?? event.author_id ?? '');
198
- const userName = userInfo?.nickname ?? userInfo?.username ?? userId;
199
- const sourceMessageId = String(body.msg_id ?? '');
200
-
201
- let channelType: 'private' | 'channel' = 'private';
202
- let channelId = String(body.target_id ?? event.target_id ?? userId);
203
- if (body.channel_type === 'GROUP' || event.channel_type === 'GROUP') {
204
- channelType = 'channel';
205
- channelId = String(body.target_id ?? event.target_id ?? channelId);
206
- } else if (body.channel_type === 'PERSON' || event.channel_type === 'PERSON') {
207
- channelType = 'private';
208
- channelId = String(body.target_id ?? event.target_id ?? userId);
209
- }
210
-
211
- return Message.from(event, {
212
- $id: event.msg_id ?? `btn:${Date.now()}`,
213
- $adapter: 'kook',
214
- $endpoint: endpointName,
215
- $sender: { id: userId, name: userName },
216
- $channel: { id: channelId, type: channelType },
217
- $content: [{
218
- type: 'action',
219
- data: {
220
- id: payload,
221
- payload,
222
- sourceMessageId,
223
- },
224
- }],
225
- $raw: payload,
226
- $timestamp: event.msg_timestamp ?? Date.now(),
227
- $recall: async () => {},
228
- $reply: async () => '',
229
- });
230
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * core keyboard 段 → KOOK 卡片消息(action-group + button)
3
- */
4
- import { KeyboardSegment, isKeyboardSegment, type KeyboardSegmentData, type MessageElement } from 'zhin.js';
5
-
6
- function asKeyboardElement(
7
- el: MessageElement,
8
- ): (MessageElement & { data: KeyboardSegmentData }) | null {
9
- if (el instanceof KeyboardSegment) {
10
- return { type: "keyboard", data: el.data };
11
- }
12
- if (isKeyboardSegment(el)) return el;
13
- return null;
14
- }
15
-
16
- function textFromElements(elements: MessageElement[]): string {
17
- return elements
18
- .filter((el) => el.type === "text" && typeof el.data.text === "string")
19
- .map((el) => el.data.text as string)
20
- .join("\n")
21
- .trim();
22
- }
23
-
24
- /** core keyboard 布局 → KOOK card[](可直接传给 sendChannelMsg / sendPrivateMsg) */
25
- export function coreKeyboardToKookCard(
26
- data: KeyboardSegmentData,
27
- prefixElements: MessageElement[] = [],
28
- ): Array<Record<string, unknown>> {
29
- const modules: Array<Record<string, unknown>> = [];
30
- const prefixText = textFromElements(prefixElements);
31
- if (prefixText) {
32
- modules.push({
33
- type: "section",
34
- text: { type: "plain-text", content: prefixText },
35
- });
36
- }
37
-
38
- for (const row of data.rows ?? []) {
39
- modules.push({
40
- type: "action-group",
41
- elements: row.map((btn) => ({
42
- type: "button",
43
- theme: btn.disabled ? "secondary" : "primary",
44
- value: btn.payload,
45
- click: "return-val",
46
- text: { type: "plain-text", content: btn.label },
47
- })),
48
- });
49
- }
50
-
51
- return [{
52
- type: "card",
53
- theme: "secondary",
54
- size: "sm",
55
- modules,
56
- }];
57
- }
58
-
59
- export function findKeyboardSegment(
60
- elements: MessageElement[],
61
- ): { keyboard: MessageElement & { data: KeyboardSegmentData }; rest: MessageElement[] } | null {
62
- let keyboard: (MessageElement & { data: KeyboardSegmentData }) | undefined;
63
- const rest: MessageElement[] = [];
64
-
65
- for (const el of elements) {
66
- const kb = asKeyboardElement(el);
67
- if (kb) {
68
- if (keyboard) return null;
69
- keyboard = kb;
70
- continue;
71
- }
72
- rest.push(el);
73
- }
74
-
75
- if (!keyboard) return null;
76
- return { keyboard, rest };
77
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * KOOK 出站 media:base64 / 本地文件须先走 /v3/asset/create 上传,再嵌入 KMarkdown。
3
- */
4
- import { asMessageElements, extractBase64Buffer, isMediaSegmentType, isRemoteUrl, resolveLocalMediaPath, type MessageElement, type MessageSegment, type SendContent } from 'zhin.js';
5
-
6
- export interface KookMediaUploader {
7
- uploadMedia(data: string | Buffer): Promise<string>;
8
- }
9
-
10
- function isMessageSegment(seg: MessageElement): seg is MessageSegment {
11
- return typeof seg.type === "string";
12
- }
13
-
14
- function withUploadedUrl(seg: MessageSegment, uploadedUrl: string): MessageSegment {
15
- const data = { ...seg.data } as Record<string, unknown>;
16
- data.url = uploadedUrl;
17
- data.file = uploadedUrl;
18
- delete data.base64;
19
- return { type: seg.type, data } as MessageSegment;
20
- }
21
-
22
- async function resolveMediaSegment(
23
- uploader: KookMediaUploader,
24
- seg: MessageSegment,
25
- ): Promise<MessageSegment> {
26
- if (!isMediaSegmentType(seg.type)) return seg;
27
-
28
- const data = seg.data as Record<string, unknown>;
29
- const url = typeof data.url === "string" ? data.url : undefined;
30
- const file = typeof data.file === "string" ? data.file : undefined;
31
- if ((url && isRemoteUrl(url)) || (file && isRemoteUrl(file))) {
32
- return seg;
33
- }
34
-
35
- const buffer = extractBase64Buffer(data);
36
- if (buffer) {
37
- const uploaded = await uploader.uploadMedia(buffer);
38
- return withUploadedUrl(seg, uploaded);
39
- }
40
-
41
- const local = resolveLocalMediaPath(data);
42
- if (local) {
43
- const uploaded = await uploader.uploadMedia(local);
44
- return withUploadedUrl(seg, uploaded);
45
- }
46
-
47
- return seg;
48
- }
49
-
50
- /** 将 base64 / 本地媒体上传为 KOOK 可访问 URL */
51
- export async function materializeOutboundMedia(
52
- uploader: KookMediaUploader,
53
- content: SendContent,
54
- ): Promise<MessageElement[]> {
55
- const segments = asMessageElements(content);
56
- return Promise.all(segments.map(async (item) => {
57
- if (!isMessageSegment(item)) return item;
58
- return resolveMediaSegment(uploader, item);
59
- }));
60
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Zhin 消息段 → kook-client Sendable。
3
- * 图片/音视频须用 segment 发送对应 msg_type,不能写成 Markdown ![alt](url)。
4
- */
5
- import { segment, type MessageSegment as KookMessageSegment } from "kook-client";
6
- import type { MessageElement } from "zhin.js";
7
- import { coreKeyboardToKookCard, findKeyboardSegment } from "./outbound-keyboard.js";
8
-
9
- export type KookSendable = string | KookMessageSegment[] | Array<Record<string, unknown>>;
10
-
11
- function mediaUrl(data: Record<string, unknown>): string {
12
- return String(data.url ?? data.file ?? "");
13
- }
14
-
15
- /**
16
- * 单张图片(可带 reply)→ kook image 消息段(type=2)
17
- */
18
- export function convertToKookSendable(
19
- elements: MessageElement[],
20
- formatText: (content: MessageElement[]) => string,
21
- ): KookSendable {
22
- const keyboardBundle = findKeyboardSegment(elements);
23
- if (keyboardBundle) {
24
- const { keyboard, rest } = keyboardBundle;
25
- const nonReplyRest = rest.filter((el) => el.type !== "reply");
26
- if (nonReplyRest.every((el) => el.type === "text" || el.type === "reply")) {
27
- return coreKeyboardToKookCard(keyboard.data, nonReplyRest.filter((el) => el.type === "text"));
28
- }
29
- }
30
-
31
- const replies: string[] = [];
32
- const images: string[] = [];
33
- const videos: string[] = [];
34
- const audios: string[] = [];
35
- const rest: MessageElement[] = [];
36
-
37
- for (const el of elements) {
38
- if (el.type === "reply" && el.data.id) {
39
- replies.push(String(el.data.id));
40
- continue;
41
- }
42
- if (el.type === "image") {
43
- const url = mediaUrl(el.data as Record<string, unknown>);
44
- if (url) images.push(url);
45
- continue;
46
- }
47
- if (el.type === "video") {
48
- const url = mediaUrl(el.data as Record<string, unknown>);
49
- if (url) videos.push(url);
50
- continue;
51
- }
52
- if (el.type === "audio") {
53
- const url = mediaUrl(el.data as Record<string, unknown>);
54
- if (url) audios.push(url);
55
- continue;
56
- }
57
- rest.push(el);
58
- }
59
-
60
- const replySegs = replies.map((id) => segment.reply(id));
61
-
62
- if (images.length === 1 && videos.length === 0 && audios.length === 0 && rest.length === 0) {
63
- return [...replySegs, segment.image(images[0])];
64
- }
65
- if (videos.length === 1 && images.length === 0 && audios.length === 0 && rest.length === 0) {
66
- return [...replySegs, segment.video(videos[0])];
67
- }
68
- if (audios.length === 1 && images.length === 0 && videos.length === 0 && rest.length === 0) {
69
- return [...replySegs, segment.audio(audios[0])];
70
- }
71
-
72
- return formatText(elements);
73
- }
@@ -1 +0,0 @@
1
- export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';
package/src/types.ts DELETED
@@ -1,58 +0,0 @@
1
- /**
2
- * KOOK 适配器类型与配置
3
- */
4
- import { type PrivateMessageEvent, type ChannelMessageEvent, type LogLevel } from 'kook-client';
5
-
6
- export type { LogLevel };
7
-
8
- export enum KookPermission {
9
- Normal = 1,
10
- Admin = 2,
11
- Owner = 4,
12
- ChannelAdmin = 5,
13
- }
14
-
15
- export interface KookSenderInfo {
16
- id: string;
17
- name: string;
18
- permission?: KookPermission;
19
- roles?: number[];
20
- isGuildOwner?: boolean;
21
- isAdmin?: boolean;
22
- /** 归一化平台身份(供 platform checker) */
23
- role?: string;
24
- permissions?: string[];
25
- }
26
-
27
- export interface KookTypingIndicatorConfig {
28
- enabled?: boolean;
29
- defaultEmoji?: string;
30
- autoRemove?: boolean;
31
- removeDelay?: number;
32
- privateConfig?: {
33
- type?: 'reaction' | 'message' | 'typing' | 'none';
34
- emoji?: string;
35
- message?: string;
36
- };
37
- /** 频道/群聊(KOOK channel 走 groupConfig 合并逻辑) */
38
- groupConfig?: {
39
- type?: 'reaction' | 'message' | 'typing' | 'none';
40
- emoji?: string;
41
- message?: string;
42
- };
43
- }
44
-
45
- export interface KookEndpointConfig {
46
- context: "kook";
47
- name: string;
48
- token: string;
49
- data_dir?: string;
50
- timeout?: number;
51
- max_retry?: number;
52
- ignore?: "bot" | "self";
53
- logLevel?: LogLevel;
54
- /** AI 处理中提示(reaction 推荐:不打断会话) */
55
- typingIndicator?: KookTypingIndicatorConfig;
56
- }
57
-
58
- export type KookRawMessage = PrivateMessageEvent | ChannelMessageEvent;