@zhin.js/adapter-kook 5.0.1 → 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.
- package/CHANGELOG.md +60 -0
- package/README.md +58 -451
- package/adapters/kook.ts +46 -0
- package/agent/tools/blacklist.ts +33 -0
- package/agent/tools/create_role.ts +25 -0
- package/agent/tools/delete_role.ts +21 -0
- package/agent/tools/grant_role.ts +22 -0
- package/agent/tools/list_roles.ts +27 -0
- package/agent/tools/revoke_role.ts +22 -0
- package/lib/endpoint.d.ts +80 -151
- package/lib/endpoint.js +260 -736
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -350
- package/lib/kook-agent-deps.d.ts +29 -0
- package/lib/kook-agent-deps.js +30 -0
- package/lib/platform-permit.d.ts +12 -3
- package/lib/platform-permit.js +5 -3
- package/lib/protocol.d.ts +147 -0
- package/lib/protocol.js +279 -0
- package/lib/webhook.d.ts +15 -0
- package/lib/webhook.js +76 -0
- package/lib/ws.d.ts +42 -0
- package/lib/ws.js +63 -0
- package/package.json +47 -33
- package/plugin.ts +13 -0
- package/schema.json +55 -0
- package/src/endpoint.ts +287 -866
- package/src/index.ts +55 -331
- package/src/kook-agent-deps.ts +55 -0
- package/src/platform-permit.ts +13 -4
- package/src/protocol.ts +429 -0
- package/src/webhook.ts +116 -0
- package/src/ws.ts +120 -0
- package/client/Dashboard.tsx +0 -255
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -31
- package/lib/adapter.d.ts +0 -30
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -67
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/kook-asset-upload.d.ts +0 -12
- package/lib/kook-asset-upload.d.ts.map +0 -1
- package/lib/kook-asset-upload.js +0 -46
- package/lib/kook-asset-upload.js.map +0 -1
- package/lib/kook-inbound.d.ts +0 -6
- package/lib/kook-inbound.d.ts.map +0 -1
- package/lib/kook-inbound.js +0 -22
- package/lib/kook-inbound.js.map +0 -1
- package/lib/kook-msg-route.d.ts +0 -34
- package/lib/kook-msg-route.d.ts.map +0 -1
- package/lib/kook-msg-route.js +0 -106
- package/lib/kook-msg-route.js.map +0 -1
- package/lib/kook-side-events.d.ts +0 -46
- package/lib/kook-side-events.d.ts.map +0 -1
- package/lib/kook-side-events.js +0 -189
- package/lib/kook-side-events.js.map +0 -1
- package/lib/outbound-keyboard.d.ts +0 -13
- package/lib/outbound-keyboard.d.ts.map +0 -1
- package/lib/outbound-keyboard.js +0 -63
- package/lib/outbound-keyboard.js.map +0 -1
- package/lib/outbound-media.d.ts +0 -10
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js +0 -42
- package/lib/outbound-media.js.map +0 -1
- package/lib/outbound-sendable.d.ts +0 -12
- package/lib/outbound-sendable.d.ts.map +0 -1
- package/lib/outbound-sendable.js +0 -64
- package/lib/outbound-sendable.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -54
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -8
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -76
- package/src/kook-asset-upload.ts +0 -55
- package/src/kook-inbound.ts +0 -22
- package/src/kook-msg-route.ts +0 -122
- package/src/kook-side-events.ts +0 -250
- package/src/outbound-keyboard.ts +0 -78
- package/src/outbound-media.ts +0 -67
- package/src/outbound-sendable.ts +0 -73
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -59
- /package/{skills/kook → agent}/PERMITS.md +0 -0
- /package/{skills/kook/SKILL.md → agent/skills/kook.md} +0 -0
package/lib/endpoint.js
CHANGED
|
@@ -1,775 +1,299 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
adapter;
|
|
23
|
-
inboundDeduper = new InboundMessageDeduper();
|
|
24
|
-
onGatewayEvent = (raw) => {
|
|
25
|
-
this.handleGatewayEvent(raw);
|
|
26
|
-
};
|
|
27
|
-
/** kook-client 构造函数注入的 process 监听器,disconnect 时需移除 */
|
|
28
|
-
_kookProcessListeners = [];
|
|
29
|
-
get $id() {
|
|
30
|
-
return this.$config.name;
|
|
31
|
-
}
|
|
32
|
-
get pluginLogger() {
|
|
33
|
-
return this.adapter.plugin.logger;
|
|
34
|
-
}
|
|
35
|
-
constructor(adapter, $config) {
|
|
36
|
-
// 记录构造前 process 上的 uncaughtException 监听器数量
|
|
37
|
-
const beforeCount = process.listenerCount('uncaughtException');
|
|
38
|
-
super({
|
|
39
|
-
token: $config.token,
|
|
40
|
-
mode: "websocket", // KOOK 默认使用 WebSocket 模式
|
|
41
|
-
data_dir: $config.data_dir || path.join(process.cwd(), "data", "kook"),
|
|
42
|
-
timeout: $config.timeout || 10000,
|
|
43
|
-
max_retry: $config.max_retry || 3,
|
|
44
|
-
ignore: $config.ignore || "bot",
|
|
45
|
-
logLevel: $config.logLevel || "info",
|
|
46
|
-
});
|
|
47
|
-
this.$config = $config;
|
|
48
|
-
// 捕获 kook-client 构造函数注入的 process 监听器
|
|
49
|
-
const afterListeners = process.listeners('uncaughtException');
|
|
50
|
-
for (let i = beforeCount; i < afterListeners.length; i++) {
|
|
51
|
-
this._kookProcessListeners.push({ event: 'uncaughtException', fn: afterListeners[i] });
|
|
52
|
-
}
|
|
53
|
-
this.adapter = adapter;
|
|
54
|
-
this.setupEventListeners();
|
|
55
|
-
this.hookGatewayReceiver();
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 在 kook-client Receiver transform 之前拦截原始 gateway 事件(SDK 未实现 notice transform)
|
|
59
|
-
*/
|
|
60
|
-
hookGatewayReceiver() {
|
|
61
|
-
const receiver = this.receiver;
|
|
62
|
-
receiver.prependListener("event", this.onGatewayEvent);
|
|
63
|
-
}
|
|
64
|
-
handleGatewayEvent(raw) {
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { registerKookAgentEndpoint } from './kook-agent-deps.js';
|
|
3
|
+
import { formatInboundContent, formatInboundTarget, formatOutboundKmarkdown, isKookBotMentioned, parseSendTarget, senderDisplayName, } from './protocol.js';
|
|
4
|
+
import { registerKookWebhookRoutes } from './webhook.js';
|
|
5
|
+
import { defaultCreateClient, defaultCreateWebhookClient, normalizeKookMessage, } from './ws.js';
|
|
6
|
+
const logger = getLogger('kook');
|
|
7
|
+
export class KookWebsocketEndpoint {
|
|
8
|
+
#options;
|
|
9
|
+
#createClient;
|
|
10
|
+
#client = null;
|
|
11
|
+
#open = false;
|
|
12
|
+
#started = false;
|
|
13
|
+
#unregisterAgent;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.#options = options;
|
|
16
|
+
this.#createClient = options.createClient ?? defaultCreateClient;
|
|
17
|
+
}
|
|
18
|
+
async start() {
|
|
19
|
+
if (this.#started)
|
|
20
|
+
return;
|
|
21
|
+
this.#started = true;
|
|
65
22
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
if (!isKookNoticeGatewayEvent(raw))
|
|
78
|
-
return;
|
|
79
|
-
const dedupeKey = resolveKookSideEventDedupeKey(raw, "notice");
|
|
80
|
-
if (!this.inboundDeduper.shouldProcess(dedupeKey))
|
|
81
|
-
return;
|
|
82
|
-
const notice = formatKookNotice(raw, this.$config.name);
|
|
83
|
-
this.emitSideEvent("notice.receive", notice);
|
|
84
|
-
this.pluginLogger.info(formatKookNoticeLog(notice));
|
|
23
|
+
this.#unregisterAgent = registerKookAgentEndpoint(this.#options.config.name, this);
|
|
24
|
+
this.#client = this.#createClient(this.#options.config);
|
|
25
|
+
this.#bindClient(this.#client);
|
|
26
|
+
await this.#client.connect();
|
|
27
|
+
logger.info(formatCompact({
|
|
28
|
+
op: 'connect',
|
|
29
|
+
endpoint: this.#options.config.name,
|
|
30
|
+
mode: 'websocket',
|
|
31
|
+
self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
|
|
32
|
+
}));
|
|
85
33
|
}
|
|
86
34
|
catch (error) {
|
|
87
|
-
this.
|
|
35
|
+
await this.stop();
|
|
36
|
+
logger.error('Failed to connect KOOK websocket:', error);
|
|
37
|
+
throw error;
|
|
88
38
|
}
|
|
89
39
|
}
|
|
90
|
-
|
|
91
|
-
this
|
|
40
|
+
open() {
|
|
41
|
+
this.#open = true;
|
|
92
42
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*/
|
|
96
|
-
setupEventListeners() {
|
|
97
|
-
// 监听消息事件
|
|
98
|
-
this.on("message", (msg) => {
|
|
99
|
-
try {
|
|
100
|
-
const message = this.$formatMessage(msg);
|
|
101
|
-
const atSegs = message.$content.filter((s) => s.type === 'at');
|
|
102
|
-
if (atSegs.length > 0) {
|
|
103
|
-
this.pluginLogger.debug(`KOOK @解析: self_id=${this.$platformUserId ?? this.self_id ?? '?'}`
|
|
104
|
-
+ ` at=${JSON.stringify(atSegs.map((s) => s.data))}`
|
|
105
|
-
+ ` preview=${segment.raw(message.$content)}`);
|
|
106
|
-
}
|
|
107
|
-
this.pluginLogger.debug(`KOOK 格式化消息: $content=${JSON.stringify(message.$content)}, $raw=${message.$raw}`);
|
|
108
|
-
this.adapter.emit("message.receive", message);
|
|
109
|
-
// 根据消息类型触发特定事件
|
|
110
|
-
const eventMap = {
|
|
111
|
-
private: "message.private.receive",
|
|
112
|
-
group: "message.group.receive",
|
|
113
|
-
channel: "message.channel.receive",
|
|
114
|
-
};
|
|
115
|
-
const specificEvent = eventMap[msg.message_type];
|
|
116
|
-
if (specificEvent) {
|
|
117
|
-
this.adapter.emit(specificEvent, message);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (error) {
|
|
121
|
-
this.pluginLogger.error(`处理 KOOK 消息失败:`, error);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
// 监听连接事件
|
|
125
|
-
this.on("connect", () => {
|
|
126
|
-
this.$connected = true;
|
|
127
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 已连接`);
|
|
128
|
-
});
|
|
129
|
-
// 监听断开事件
|
|
130
|
-
this.on("disconnect", () => {
|
|
131
|
-
this.$connected = false;
|
|
132
|
-
this.pluginLogger.warn(`KOOK Endpoint ${this.$id} 已断开`);
|
|
133
|
-
});
|
|
134
|
-
// 监听错误事件
|
|
135
|
-
this.on("error", (error) => {
|
|
136
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 错误:`, error);
|
|
137
|
-
});
|
|
43
|
+
close() {
|
|
44
|
+
this.#open = false;
|
|
138
45
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
? msg.channel_id
|
|
145
|
-
: msg.author_id;
|
|
146
|
-
// 获取发送者的权限信息
|
|
147
|
-
const senderInfo = this.getSenderInfo(msg);
|
|
148
|
-
// 频道消息需要获取 guild_id
|
|
149
|
-
let guildId;
|
|
150
|
-
if (msg.message_type === "channel") {
|
|
151
|
-
const channelMsg = msg;
|
|
152
|
-
// 尝试从 channel 获取 guild_id
|
|
46
|
+
async stop() {
|
|
47
|
+
this.#open = false;
|
|
48
|
+
this.#unregisterAgent?.();
|
|
49
|
+
this.#unregisterAgent = undefined;
|
|
50
|
+
if (this.#client) {
|
|
153
51
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
52
|
+
this.#client.removeAllListeners();
|
|
53
|
+
await this.#client.disconnect();
|
|
156
54
|
}
|
|
157
55
|
catch {
|
|
158
|
-
|
|
56
|
+
/* ignore */
|
|
159
57
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
58
|
+
this.#client = null;
|
|
59
|
+
}
|
|
60
|
+
this.#started = false;
|
|
61
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
62
|
+
}
|
|
63
|
+
async send({ target, payload }) {
|
|
64
|
+
const body = formatOutboundKmarkdown(payload);
|
|
65
|
+
const parsed = parseSendTarget(target);
|
|
66
|
+
const client = this.#requireClient();
|
|
67
|
+
const result = parsed.kind === 'private'
|
|
68
|
+
? await client.sendPrivateMsg(parsed.id, body)
|
|
69
|
+
: await client.sendChannelMsg(parsed.id, body);
|
|
70
|
+
const messageId = result?.msg_id != null ? String(result.msg_id) : '';
|
|
71
|
+
logger.debug(formatCompact({
|
|
72
|
+
op: 'kook_send',
|
|
73
|
+
endpoint: this.#options.config.name,
|
|
74
|
+
target,
|
|
75
|
+
messageId,
|
|
76
|
+
}));
|
|
77
|
+
return messageId;
|
|
78
|
+
}
|
|
79
|
+
/** Test / internal: admit a message when open. */
|
|
80
|
+
admit(msg) {
|
|
81
|
+
if (!this.#open)
|
|
82
|
+
return;
|
|
83
|
+
if (msg.authorBot)
|
|
84
|
+
return;
|
|
85
|
+
const target = formatInboundTarget(msg);
|
|
86
|
+
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
87
|
+
void this.#options.gateway.receive({
|
|
88
|
+
adapter: this.#options.id,
|
|
89
|
+
target,
|
|
90
|
+
content: formatInboundContent(msg),
|
|
91
|
+
sender: senderDisplayName(msg),
|
|
92
|
+
id: msg.id,
|
|
93
|
+
metadata: Object.freeze({
|
|
94
|
+
endpoint: this.#options.config.name,
|
|
95
|
+
channelKind: msg.channelKind,
|
|
96
|
+
userId: msg.authorId,
|
|
97
|
+
guildId: msg.guildId,
|
|
98
|
+
roles: msg.authorRoles,
|
|
99
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
100
|
+
}),
|
|
101
|
+
}).catch((err) => {
|
|
102
|
+
logger.warn(formatCompact({
|
|
103
|
+
op: 'kook_gateway_receive_failed',
|
|
104
|
+
target,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
}));
|
|
199
107
|
});
|
|
200
|
-
return message;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* 获取发送者的详细权限信息
|
|
204
|
-
*/
|
|
205
|
-
getSenderInfo(msg) {
|
|
206
|
-
const authorInfo = msg.author?.info;
|
|
207
|
-
const senderInfo = {
|
|
208
|
-
id: msg.author_id.toString(),
|
|
209
|
-
name: authorInfo?.nickname || authorInfo?.username || "未知用户",
|
|
210
|
-
};
|
|
211
|
-
// 频道消息才有权限信息
|
|
212
|
-
if (msg.message_type === "channel") {
|
|
213
|
-
const channelMsg = msg;
|
|
214
|
-
// 从 author.info 中获取权限信息(如果 kook-client 提供)
|
|
215
|
-
if (authorInfo) {
|
|
216
|
-
// 尝试获取角色列表
|
|
217
|
-
senderInfo.roles = authorInfo.roles || [];
|
|
218
|
-
// 尝试获取 guild_id 并检查是否为服务器主人
|
|
219
|
-
try {
|
|
220
|
-
const channel = channelMsg.channel;
|
|
221
|
-
const guildId = channel?.info?.guild_id;
|
|
222
|
-
if (guildId) {
|
|
223
|
-
const guildInfo = this.guilds?.get(guildId);
|
|
224
|
-
if (guildInfo) {
|
|
225
|
-
senderInfo.isGuildOwner = guildInfo.user_id === msg.author_id;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
// 忽略获取失败
|
|
231
|
-
}
|
|
232
|
-
// 根据 permission 字段判断(如果有)
|
|
233
|
-
const permission = authorInfo.permission;
|
|
234
|
-
if (permission !== undefined) {
|
|
235
|
-
senderInfo.permission = permission;
|
|
236
|
-
senderInfo.isAdmin = permission === KookPermission.Admin ||
|
|
237
|
-
permission === KookPermission.Owner ||
|
|
238
|
-
permission === KookPermission.ChannelAdmin;
|
|
239
|
-
senderInfo.isGuildOwner = senderInfo.isGuildOwner || permission === KookPermission.Owner;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
const normalized = normalizeKookSenderForPermit(senderInfo, msg.message_type === "channel");
|
|
244
|
-
senderInfo.role = normalized.role;
|
|
245
|
-
senderInfo.permissions = normalized.permissions;
|
|
246
|
-
return senderInfo;
|
|
247
|
-
}
|
|
248
|
-
// ==================== 频道管理 API ====================
|
|
249
|
-
/**
|
|
250
|
-
* 踢出用户
|
|
251
|
-
* @param guildId 服务器ID
|
|
252
|
-
* @param userId 用户ID
|
|
253
|
-
*/
|
|
254
|
-
async kickUser(guildId, userId) {
|
|
255
|
-
try {
|
|
256
|
-
const guild = this.pickGuild(guildId);
|
|
257
|
-
const result = await guild.kick(userId);
|
|
258
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 踢出用户 ${userId} 从服务器 ${guildId}`);
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 踢出用户失败:`, error);
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
108
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
* @param userId 用户ID
|
|
270
|
-
* @param remark 备注
|
|
271
|
-
* @param delMsgDays 删除消息天数(0-7)
|
|
272
|
-
*/
|
|
273
|
-
async addToBlacklist(guildId, userId, remark, delMsgDays) {
|
|
274
|
-
try {
|
|
275
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
276
|
-
const result = await member.addToBlackList(remark, delMsgDays);
|
|
277
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 将用户 ${userId} 加入黑名单(服务器 ${guildId})`);
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 加入黑名单失败:`, error);
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
109
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
110
|
+
async getRoleList(guildId) {
|
|
111
|
+
return this.#requireClient().pickGuild(guildId).getRoleList();
|
|
284
112
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
async removeFromBlacklist(guildId, userId) {
|
|
291
|
-
try {
|
|
292
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
293
|
-
const result = await member.removeFromBlackList();
|
|
294
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 将用户 ${userId} 从黑名单移除(服务器 ${guildId})`);
|
|
295
|
-
return result;
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 移除黑名单失败:`, error);
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
113
|
+
async createRole(guildId, name) {
|
|
114
|
+
return this.#requireClient().pickGuild(guildId).createRole(name);
|
|
115
|
+
}
|
|
116
|
+
async deleteRole(guildId, roleId) {
|
|
117
|
+
return this.#requireClient().pickGuild(guildId).deleteRole(roleId);
|
|
301
118
|
}
|
|
302
|
-
/**
|
|
303
|
-
* 给用户授予角色
|
|
304
|
-
* @param guildId 服务器ID
|
|
305
|
-
* @param userId 用户ID
|
|
306
|
-
* @param roleId 角色ID
|
|
307
|
-
*/
|
|
308
119
|
async grantRole(guildId, userId, roleId) {
|
|
309
|
-
|
|
310
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
311
|
-
const result = await member.grant(roleId);
|
|
312
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 授予用户 ${userId} 角色 ${roleId}(服务器 ${guildId})`);
|
|
313
|
-
return result;
|
|
314
|
-
}
|
|
315
|
-
catch (error) {
|
|
316
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 授予角色失败:`, error);
|
|
317
|
-
throw error;
|
|
318
|
-
}
|
|
120
|
+
return this.#requireClient().pickGuildMember(guildId, userId).grant(roleId);
|
|
319
121
|
}
|
|
320
|
-
/**
|
|
321
|
-
* 撤销用户角色
|
|
322
|
-
* @param guildId 服务器ID
|
|
323
|
-
* @param userId 用户ID
|
|
324
|
-
* @param roleId 角色ID
|
|
325
|
-
*/
|
|
326
122
|
async revokeRole(guildId, userId, roleId) {
|
|
327
|
-
|
|
328
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
329
|
-
const result = await member.revoke(roleId);
|
|
330
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 撤销用户 ${userId} 角色 ${roleId}(服务器 ${guildId})`);
|
|
331
|
-
return result;
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 撤销角色失败:`, error);
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
123
|
+
return this.#requireClient().pickGuildMember(guildId, userId).revoke(roleId);
|
|
337
124
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
* @param guildId 服务器ID
|
|
341
|
-
* @param userId 用户ID
|
|
342
|
-
* @param nickname 新昵称
|
|
343
|
-
*/
|
|
344
|
-
async setNickname(guildId, userId, nickname) {
|
|
345
|
-
try {
|
|
346
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
347
|
-
const result = await member.setNickname(nickname);
|
|
348
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 设置用户 ${userId} 昵称为 "${nickname}"(服务器 ${guildId})`);
|
|
349
|
-
return result;
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 设置昵称失败:`, error);
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
125
|
+
async addToBlacklist(guildId, userId, remark) {
|
|
126
|
+
return this.#requireClient().pickGuildMember(guildId, userId).addToBlackList(remark);
|
|
355
127
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
* @param guildId 服务器ID
|
|
359
|
-
*/
|
|
360
|
-
async getRoleList(guildId) {
|
|
361
|
-
try {
|
|
362
|
-
const guild = this.pickGuild(guildId);
|
|
363
|
-
return await guild.getRoleList();
|
|
364
|
-
}
|
|
365
|
-
catch (error) {
|
|
366
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 获取角色列表失败:`, error);
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
128
|
+
async removeFromBlacklist(guildId, userId) {
|
|
129
|
+
return this.#requireClient().pickGuildMember(guildId, userId).removeFromBlackList();
|
|
369
130
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
const guild = this.pickGuild(guildId);
|
|
378
|
-
const role = await guild.createRole(name);
|
|
379
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 创建角色 "${name}"(服务器 ${guildId})`);
|
|
380
|
-
return role;
|
|
381
|
-
}
|
|
382
|
-
catch (error) {
|
|
383
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 创建角色失败:`, error);
|
|
384
|
-
throw error;
|
|
385
|
-
}
|
|
131
|
+
#bindClient(client) {
|
|
132
|
+
client.on('message', (raw) => {
|
|
133
|
+
const msg = normalizeKookMessage(raw);
|
|
134
|
+
if (msg)
|
|
135
|
+
this.admit(msg);
|
|
136
|
+
});
|
|
386
137
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
*/
|
|
392
|
-
async deleteRole(guildId, roleId) {
|
|
393
|
-
try {
|
|
394
|
-
const guild = this.pickGuild(guildId);
|
|
395
|
-
const result = await guild.deleteRole(roleId);
|
|
396
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 删除角色 ${roleId}(服务器 ${guildId})`);
|
|
397
|
-
return result;
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 删除角色失败:`, error);
|
|
401
|
-
throw error;
|
|
402
|
-
}
|
|
138
|
+
#requireClient() {
|
|
139
|
+
if (!this.#client)
|
|
140
|
+
throw new Error('KOOK client not connected');
|
|
141
|
+
return this.#client;
|
|
403
142
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
143
|
+
}
|
|
144
|
+
export class KookWebhookEndpoint {
|
|
145
|
+
#options;
|
|
146
|
+
#createClient;
|
|
147
|
+
#client = null;
|
|
148
|
+
#routeReleases = [];
|
|
149
|
+
#processedSn = new Set();
|
|
150
|
+
#open = false;
|
|
151
|
+
#started = false;
|
|
152
|
+
#unregisterAgent;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
this.#options = options;
|
|
155
|
+
this.#createClient = options.createClient ?? defaultCreateWebhookClient;
|
|
156
|
+
}
|
|
157
|
+
/** Used by webhook handler. */
|
|
158
|
+
get isOpen() {
|
|
159
|
+
return this.#open;
|
|
160
|
+
}
|
|
161
|
+
get config() {
|
|
162
|
+
return this.#options.config;
|
|
163
|
+
}
|
|
164
|
+
get selfId() {
|
|
165
|
+
return this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
166
|
+
}
|
|
167
|
+
async start() {
|
|
168
|
+
if (this.#started)
|
|
169
|
+
return;
|
|
170
|
+
this.#started = true;
|
|
410
171
|
try {
|
|
411
|
-
|
|
172
|
+
this.#unregisterAgent = registerKookAgentEndpoint(this.#options.config.name, this);
|
|
173
|
+
this.#client = this.#createClient(this.#options.config);
|
|
174
|
+
await this.#client.init();
|
|
175
|
+
this.#routeReleases.push(...registerKookWebhookRoutes(this.#options.http, this));
|
|
176
|
+
logger.info(formatCompact({
|
|
177
|
+
op: 'connect',
|
|
178
|
+
endpoint: this.#options.config.name,
|
|
179
|
+
mode: 'webhook',
|
|
180
|
+
path: this.#options.config.webhookPath,
|
|
181
|
+
self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
|
|
182
|
+
}));
|
|
412
183
|
}
|
|
413
184
|
catch (error) {
|
|
414
|
-
this.
|
|
185
|
+
await this.stop();
|
|
186
|
+
logger.error('Failed to connect KOOK webhook:', error);
|
|
415
187
|
throw error;
|
|
416
188
|
}
|
|
417
189
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const channelRegex = /\(chn\)(\d+)\(chn\)/g;
|
|
436
|
-
let lastIndex = 0;
|
|
437
|
-
const matches = [];
|
|
438
|
-
// 解析图片
|
|
439
|
-
let match;
|
|
440
|
-
while ((match = imageRegex.exec(content)) !== null) {
|
|
441
|
-
matches.push({
|
|
442
|
-
index: match.index,
|
|
443
|
-
length: match[0].length,
|
|
444
|
-
element: { type: "image", data: { url: match[2], alt: match[1] } }
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
// 解析 @提及
|
|
448
|
-
while ((match = mentionRegex.exec(content)) !== null) {
|
|
449
|
-
const userId = match[1] || match[2];
|
|
450
|
-
matches.push({
|
|
451
|
-
index: match.index,
|
|
452
|
-
length: match[0].length,
|
|
453
|
-
element: this.buildAtElement(userId),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
// 解析表情
|
|
457
|
-
while ((match = emojiRegex.exec(content)) !== null) {
|
|
458
|
-
matches.push({
|
|
459
|
-
index: match.index,
|
|
460
|
-
length: match[0].length,
|
|
461
|
-
element: { type: "face", data: { id: match[2], name: match[1] } }
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
// 解析频道引用
|
|
465
|
-
while ((match = channelRegex.exec(content)) !== null) {
|
|
466
|
-
matches.push({
|
|
467
|
-
index: match.index,
|
|
468
|
-
length: match[0].length,
|
|
469
|
-
element: { type: "text", data: { text: `#频道:${match[1]}` } }
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
// 按位置排序
|
|
473
|
-
matches.sort((a, b) => a.index - b.index);
|
|
474
|
-
// 组装消息段
|
|
475
|
-
for (const match of matches) {
|
|
476
|
-
// 添加之前的文本
|
|
477
|
-
if (match.index > lastIndex) {
|
|
478
|
-
const text = content.slice(lastIndex, match.index);
|
|
479
|
-
if (text) {
|
|
480
|
-
elements.push({ type: "text", data: { text } });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
// 添加特殊元素
|
|
484
|
-
elements.push(match.element);
|
|
485
|
-
lastIndex = match.index + match.length;
|
|
486
|
-
}
|
|
487
|
-
// 添加剩余文本
|
|
488
|
-
if (lastIndex < content.length) {
|
|
489
|
-
const text = content.slice(lastIndex);
|
|
490
|
-
if (text) {
|
|
491
|
-
elements.push({ type: "text", data: { text } });
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
// 如果没有解析到任何特殊元素,返回纯文本
|
|
495
|
-
if (elements.length === 0) {
|
|
496
|
-
elements.push({ type: "text", data: { text: content } });
|
|
497
|
-
}
|
|
498
|
-
return elements;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* 将 kook-client 的 MessageSegment[] 转换为 Zhin 的 MessageElement[]
|
|
502
|
-
*/
|
|
503
|
-
parseMessageContent(segments) {
|
|
504
|
-
const elements = [];
|
|
505
|
-
for (const segment of segments) {
|
|
506
|
-
switch (segment.type) {
|
|
507
|
-
case "markdown":
|
|
508
|
-
// 检查是否包含特殊语法,如果是纯文本则直接转换
|
|
509
|
-
if (this.hasKMarkdownSyntax(segment.text)) {
|
|
510
|
-
elements.push(...this.parseMarkdown(segment.text));
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
elements.push({ type: "text", data: { text: segment.text } });
|
|
514
|
-
}
|
|
515
|
-
break;
|
|
516
|
-
case "text":
|
|
517
|
-
elements.push({ type: "text", data: { text: segment.text } });
|
|
518
|
-
break;
|
|
519
|
-
case "at":
|
|
520
|
-
elements.push(this.buildAtElement(segment.user_id));
|
|
521
|
-
break;
|
|
522
|
-
case "image":
|
|
523
|
-
elements.push({
|
|
524
|
-
type: "image",
|
|
525
|
-
data: { url: segment.url, alt: segment.title || "图片" }
|
|
526
|
-
});
|
|
527
|
-
break;
|
|
528
|
-
case "video":
|
|
529
|
-
elements.push({ type: "video", data: { url: segment.url } });
|
|
530
|
-
break;
|
|
531
|
-
case "audio":
|
|
532
|
-
elements.push({ type: "audio", data: { url: segment.url } });
|
|
533
|
-
break;
|
|
534
|
-
case "file":
|
|
535
|
-
elements.push({
|
|
536
|
-
type: "file",
|
|
537
|
-
data: { url: segment.url, name: segment.name }
|
|
538
|
-
});
|
|
539
|
-
break;
|
|
540
|
-
case "reply":
|
|
541
|
-
elements.push({ type: "reply", data: { id: segment.id } });
|
|
542
|
-
break;
|
|
543
|
-
case "card":
|
|
544
|
-
// Card 消息暂不支持,转为提示文本
|
|
545
|
-
elements.push({ type: "text", data: { text: "[卡片消息]" } });
|
|
546
|
-
break;
|
|
547
|
-
default:
|
|
548
|
-
this.pluginLogger.warn(`未知的 KOOK 消息段类型: ${segment.type}`);
|
|
549
|
-
break;
|
|
190
|
+
open() {
|
|
191
|
+
this.#open = true;
|
|
192
|
+
}
|
|
193
|
+
close() {
|
|
194
|
+
this.#open = false;
|
|
195
|
+
}
|
|
196
|
+
async stop() {
|
|
197
|
+
this.#open = false;
|
|
198
|
+
for (const release of this.#routeReleases.splice(0))
|
|
199
|
+
release();
|
|
200
|
+
this.#unregisterAgent?.();
|
|
201
|
+
this.#unregisterAgent = undefined;
|
|
202
|
+
this.#processedSn.clear();
|
|
203
|
+
if (this.#client) {
|
|
204
|
+
try {
|
|
205
|
+
this.#client.removeAllListeners();
|
|
206
|
+
await this.#client.disconnect();
|
|
550
207
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* 检查文本是否包含 KMarkdown 特殊语法
|
|
556
|
-
*/
|
|
557
|
-
hasKMarkdownSyntax(text) {
|
|
558
|
-
return /!\[.*?\]\(.*?\)|\(met\)|\(emj\)|\(chn\)|@\S+/.test(text);
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* 连接到 KOOK
|
|
562
|
-
*/
|
|
563
|
-
async $connect() {
|
|
564
|
-
try {
|
|
565
|
-
await this.connect();
|
|
566
|
-
this.$connected = true;
|
|
567
|
-
const selfId = this.self_id;
|
|
568
|
-
if (selfId != null) {
|
|
569
|
-
this.$platformUserId = String(selfId);
|
|
208
|
+
catch {
|
|
209
|
+
/* ignore */
|
|
570
210
|
}
|
|
571
|
-
this
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
211
|
+
this.#client = null;
|
|
212
|
+
}
|
|
213
|
+
this.#started = false;
|
|
214
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
215
|
+
}
|
|
216
|
+
async send({ target, payload }) {
|
|
217
|
+
const body = formatOutboundKmarkdown(payload);
|
|
218
|
+
const parsed = parseSendTarget(target);
|
|
219
|
+
const client = this.#requireClient();
|
|
220
|
+
const result = parsed.kind === 'private'
|
|
221
|
+
? await client.sendPrivateMsg(parsed.id, body)
|
|
222
|
+
: await client.sendChannelMsg(parsed.id, body);
|
|
223
|
+
const messageId = result?.msg_id != null ? String(result.msg_id) : '';
|
|
224
|
+
logger.debug(formatCompact({
|
|
225
|
+
op: 'kook_send',
|
|
226
|
+
endpoint: this.#options.config.name,
|
|
227
|
+
target,
|
|
228
|
+
messageId,
|
|
229
|
+
}));
|
|
230
|
+
return messageId;
|
|
231
|
+
}
|
|
232
|
+
/** Test / internal: admit a message when open. */
|
|
233
|
+
admit(msg) {
|
|
234
|
+
if (!this.#open)
|
|
235
|
+
return;
|
|
236
|
+
if (msg.authorBot)
|
|
237
|
+
return;
|
|
238
|
+
const target = formatInboundTarget(msg);
|
|
239
|
+
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
240
|
+
void this.#options.gateway.receive({
|
|
241
|
+
adapter: this.#options.id,
|
|
242
|
+
target,
|
|
243
|
+
content: formatInboundContent(msg),
|
|
244
|
+
sender: senderDisplayName(msg),
|
|
245
|
+
id: msg.id,
|
|
246
|
+
metadata: Object.freeze({
|
|
247
|
+
endpoint: this.#options.config.name,
|
|
248
|
+
channelKind: msg.channelKind,
|
|
249
|
+
userId: msg.authorId,
|
|
250
|
+
guildId: msg.guildId,
|
|
251
|
+
roles: msg.authorRoles,
|
|
252
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
253
|
+
}),
|
|
254
|
+
}).catch((err) => {
|
|
255
|
+
logger.warn(formatCompact({
|
|
256
|
+
op: 'kook_gateway_receive_failed',
|
|
257
|
+
target,
|
|
258
|
+
error: err instanceof Error ? err.message : String(err),
|
|
259
|
+
}));
|
|
260
|
+
});
|
|
578
261
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this.removeAllListeners();
|
|
588
|
-
// 移除 kook-client 构造函数注入的 process 监听器
|
|
589
|
-
for (const { event, fn } of this._kookProcessListeners) {
|
|
590
|
-
process.removeListener(event, fn);
|
|
591
|
-
}
|
|
592
|
-
this._kookProcessListeners.length = 0;
|
|
593
|
-
await this.disconnect();
|
|
594
|
-
this.$connected = false;
|
|
595
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 已断开连接`);
|
|
596
|
-
}
|
|
597
|
-
catch (error) {
|
|
598
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 断开连接失败:`, error);
|
|
599
|
-
throw error;
|
|
262
|
+
checkAndRememberSn(sn) {
|
|
263
|
+
if (this.#processedSn.has(sn))
|
|
264
|
+
return false;
|
|
265
|
+
this.#processedSn.add(sn);
|
|
266
|
+
if (this.#processedSn.size > 1024) {
|
|
267
|
+
const first = this.#processedSn.values().next().value;
|
|
268
|
+
if (first != null)
|
|
269
|
+
this.#processedSn.delete(first);
|
|
600
270
|
}
|
|
271
|
+
return true;
|
|
601
272
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
*/
|
|
605
|
-
async uploadMedia(data) {
|
|
606
|
-
return uploadKookAsset(this.request, data);
|
|
273
|
+
async getRoleList(guildId) {
|
|
274
|
+
return this.#requireClient().pickGuild(guildId).getRoleList();
|
|
607
275
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
*/
|
|
611
|
-
async $sendMessage(options) {
|
|
612
|
-
try {
|
|
613
|
-
const { id, type, content } = options;
|
|
614
|
-
const expanded = expandInteractiveSegmentsInContent(content);
|
|
615
|
-
const canonical = toCanonicalSegments(Array.isArray(expanded) ? expanded : [expanded]);
|
|
616
|
-
const wire = fromCanonicalSegments(canonical);
|
|
617
|
-
const elements = await materializeOutboundMedia(this, wire);
|
|
618
|
-
const kookContent = convertToKookSendable(elements, (els) => this.convertToKookFormat(els));
|
|
619
|
-
// 根据消息类型发送
|
|
620
|
-
let result;
|
|
621
|
-
if (type === "private") {
|
|
622
|
-
result = await this.sendPrivateMsg(id, kookContent);
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
result = await this.sendChannelMsg(id, kookContent);
|
|
626
|
-
}
|
|
627
|
-
const route = routeFromSendType(type);
|
|
628
|
-
return encodeKookMsgRef(route, String(result?.msg_id ?? ''));
|
|
629
|
-
}
|
|
630
|
-
catch (error) {
|
|
631
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 发送消息失败:`, error);
|
|
632
|
-
throw error;
|
|
633
|
-
}
|
|
276
|
+
async createRole(guildId, name) {
|
|
277
|
+
return this.#requireClient().pickGuild(guildId).createRole(name);
|
|
634
278
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
*/
|
|
638
|
-
async $recallMessage(messageIdOrRef, hint) {
|
|
639
|
-
const { route: encodedRoute, msgId } = parseKookMsgRef(messageIdOrRef);
|
|
640
|
-
const routeHint = encodedRoute ?? hint?.route ?? routeFromSceneType(hint?.sceneType);
|
|
641
|
-
try {
|
|
642
|
-
await this.deleteKookMsg(msgId, routeHint);
|
|
643
|
-
this.pluginLogger.debug(`KOOK Endpoint ${this.$id} 撤回消息 (${routeHint ?? 'auto'}): ${msgId}`);
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 撤回消息失败:`, error);
|
|
647
|
-
throw error;
|
|
648
|
-
}
|
|
279
|
+
async deleteRole(guildId, roleId) {
|
|
280
|
+
return this.#requireClient().pickGuild(guildId).deleteRole(roleId);
|
|
649
281
|
}
|
|
650
|
-
async
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* 为消息添加表情回应(TypingIndicator reaction 模式)
|
|
669
|
-
* @returns reactionId(含 channel/direct 路由),供 $removeReaction 使用
|
|
670
|
-
*/
|
|
671
|
-
async $addReaction(messageId, emoji, hint) {
|
|
672
|
-
const msgId = plainKookMsgId(messageId);
|
|
673
|
-
const route = await this.mutateMsgReaction(msgId, emoji, 'add', routeFromSceneType(hint?.sceneType));
|
|
674
|
-
return encodeKookReactionId(route, msgId, emoji);
|
|
675
|
-
}
|
|
676
|
-
/** 移除本 Endpoint 在消息上的表情回应 */
|
|
677
|
-
async $removeReaction(messageId, reactionId) {
|
|
678
|
-
const { route, emoji } = parseKookReactionId(reactionId);
|
|
679
|
-
await this.mutateMsgReaction(plainKookMsgId(messageId), emoji, 'delete', route);
|
|
680
|
-
}
|
|
681
|
-
async mutateMsgReaction(msgId, emoji, action, routeHint) {
|
|
682
|
-
const routes = resolveKookRoutes(action, routeHint);
|
|
683
|
-
const body = { msg_id: msgId, emoji };
|
|
684
|
-
let lastError;
|
|
685
|
-
for (const route of routes) {
|
|
686
|
-
try {
|
|
687
|
-
const result = await this.request.post(kookReactionApiPath(route, action), body);
|
|
688
|
-
if (isKookApiSuccess(result)) {
|
|
689
|
-
this.pluginLogger.debug(`KOOK Endpoint ${this.$id} ${action} reaction (${route}) on ${msgId}`);
|
|
690
|
-
return route;
|
|
691
|
-
}
|
|
692
|
-
if (action === 'delete' && shouldStopDeleteAfterResponse(result, routes.length)) {
|
|
693
|
-
const label = isKookApiGoneResult(result) ? 'already gone' : 'done';
|
|
694
|
-
this.pluginLogger.debug(`KOOK Endpoint ${this.$id} delete reaction ${label} (${route}) on ${msgId}`);
|
|
695
|
-
return route;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
catch (err) {
|
|
699
|
-
if (action === 'delete' && isKookMsgGoneError(err)) {
|
|
700
|
-
this.pluginLogger.debug(`KOOK Endpoint ${this.$id} delete reaction already gone (${route}) on ${msgId}`);
|
|
701
|
-
return route;
|
|
702
|
-
}
|
|
703
|
-
lastError = err;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
if (action === 'delete') {
|
|
707
|
-
this.pluginLogger.debug(`KOOK Endpoint ${this.$id} delete reaction noop on ${msgId}`);
|
|
708
|
-
return routeHint ?? 'channel';
|
|
709
|
-
}
|
|
710
|
-
throw lastError instanceof Error
|
|
711
|
-
? lastError
|
|
712
|
-
: new Error(`KOOK ${action} reaction failed (msg_id=${msgId})`);
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* 将消息段转换为 KOOK KMarkdown 格式
|
|
716
|
-
* 支持:文本、图片、@提及、表情、引用等
|
|
717
|
-
*/
|
|
718
|
-
convertToKookFormat(content) {
|
|
719
|
-
return content
|
|
720
|
-
.map((el) => {
|
|
721
|
-
switch (el.type) {
|
|
722
|
-
case "text":
|
|
723
|
-
// 纯文本,转义特殊字符
|
|
724
|
-
return el.data.text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
|
|
725
|
-
case "image":
|
|
726
|
-
// 图片:
|
|
727
|
-
return ``;
|
|
728
|
-
case "at": {
|
|
729
|
-
const atId = this.resolveAtUserId(el.data);
|
|
730
|
-
if (atId === 'all') {
|
|
731
|
-
return '(met)all(met)';
|
|
732
|
-
}
|
|
733
|
-
return `(met)${atId}(met)`;
|
|
734
|
-
}
|
|
735
|
-
case "face":
|
|
736
|
-
// 表情:(emj)表情名(emj)[表情ID]
|
|
737
|
-
return `(emj)${el.data.name || 'emoji'}(emj)[${el.data.id}]`;
|
|
738
|
-
case "reply":
|
|
739
|
-
// 引用消息(KOOK 使用 quote 参数,不在消息内容中)
|
|
740
|
-
return "";
|
|
741
|
-
case "video":
|
|
742
|
-
// 视频:使用链接形式
|
|
743
|
-
return `[视频](${el.data.url || el.data.file})`;
|
|
744
|
-
case "audio":
|
|
745
|
-
// 音频:使用链接形式
|
|
746
|
-
return `[音频](${el.data.url || el.data.file})`;
|
|
747
|
-
case "file":
|
|
748
|
-
// 文件:使用链接形式
|
|
749
|
-
return `[文件: ${el.data.name || '未命名'}](${el.data.url || el.data.file})`;
|
|
750
|
-
case "link":
|
|
751
|
-
// 链接:[文本](url)
|
|
752
|
-
return `[${el.data.text || el.data.url}](${el.data.url})`;
|
|
753
|
-
case "bold":
|
|
754
|
-
// 粗体:**文本**
|
|
755
|
-
return `**${el.data.text}**`;
|
|
756
|
-
case "italic":
|
|
757
|
-
// 斜体:*文本*
|
|
758
|
-
return `*${el.data.text}*`;
|
|
759
|
-
case "code":
|
|
760
|
-
// 行内代码:`代码`
|
|
761
|
-
return `\`${el.data.text}\``;
|
|
762
|
-
case "code_block":
|
|
763
|
-
// 代码块:```语言\n代码\n```
|
|
764
|
-
return `\`\`\`${el.data.language || ''}\n${el.data.text}\n\`\`\``;
|
|
765
|
-
default:
|
|
766
|
-
// 未知类型,尝试转换为文本
|
|
767
|
-
this.pluginLogger.warn(`未知的消息段类型: ${el.type}`);
|
|
768
|
-
return el.data.text || JSON.stringify(el.data);
|
|
769
|
-
}
|
|
770
|
-
})
|
|
771
|
-
.filter(Boolean)
|
|
772
|
-
.join("");
|
|
282
|
+
async grantRole(guildId, userId, roleId) {
|
|
283
|
+
return this.#requireClient().pickGuildMember(guildId, userId).grant(roleId);
|
|
284
|
+
}
|
|
285
|
+
async revokeRole(guildId, userId, roleId) {
|
|
286
|
+
return this.#requireClient().pickGuildMember(guildId, userId).revoke(roleId);
|
|
287
|
+
}
|
|
288
|
+
async addToBlacklist(guildId, userId, remark) {
|
|
289
|
+
return this.#requireClient().pickGuildMember(guildId, userId).addToBlackList(remark);
|
|
290
|
+
}
|
|
291
|
+
async removeFromBlacklist(guildId, userId) {
|
|
292
|
+
return this.#requireClient().pickGuildMember(guildId, userId).removeFromBlackList();
|
|
293
|
+
}
|
|
294
|
+
#requireClient() {
|
|
295
|
+
if (!this.#client)
|
|
296
|
+
throw new Error('KOOK client not initialized');
|
|
297
|
+
return this.#client;
|
|
773
298
|
}
|
|
774
299
|
}
|
|
775
|
-
//# sourceMappingURL=endpoint.js.map
|