kook-client 1.0.0
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/LICENSE +21 -0
- package/README.md +51 -0
- package/lib/client.js +222 -0
- package/lib/constans.js +27 -0
- package/lib/core/baseClient.js +166 -0
- package/lib/core/receiver.js +88 -0
- package/lib/core/receivers/index.js +7 -0
- package/lib/core/receivers/webhook.js +18 -0
- package/lib/core/receivers/websocket.js +267 -0
- package/lib/elements.js +342 -0
- package/lib/entries/channel.js +137 -0
- package/lib/entries/channelMember.js +31 -0
- package/lib/entries/contact.js +9 -0
- package/lib/entries/guild.js +139 -0
- package/lib/entries/guildMember.js +73 -0
- package/lib/entries/user.js +115 -0
- package/lib/event/index.js +17 -0
- package/lib/event/message.js +86 -0
- package/lib/index.d.ts +958 -0
- package/lib/index.js +23 -0
- package/lib/message.js +173 -0
- package/lib/tsconfig.build.tsbuildinfo +1 -0
- package/lib/types.js +2 -0
- package/lib/utils.js +124 -0
- package/package.json +57 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebsocketReceiver = void 0;
|
|
4
|
+
const receiver_1 = require("../../core/receiver");
|
|
5
|
+
const ws_1 = require("ws");
|
|
6
|
+
const constans_1 = require("../../constans");
|
|
7
|
+
class WebsocketReceiver extends receiver_1.Receiver {
|
|
8
|
+
constructor(client, config) {
|
|
9
|
+
super(client);
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.ws = null;
|
|
12
|
+
this.url = null;
|
|
13
|
+
this.session_id = null;
|
|
14
|
+
this.timers = new Map();
|
|
15
|
+
this.pingJitterRange = 10000;
|
|
16
|
+
this.pingBaseInterval = 25000;
|
|
17
|
+
this.helloTimeout = 5000;
|
|
18
|
+
// 重连相关属性
|
|
19
|
+
this.reconnectAttempts = 0;
|
|
20
|
+
this.maxReconnectAttempts = 10;
|
|
21
|
+
this.reconnectBaseDelay = 1000;
|
|
22
|
+
this.reconnectTimer = null;
|
|
23
|
+
// 心跳检测相关属性
|
|
24
|
+
this.lastPongTime = 0;
|
|
25
|
+
this.heartbeatTimeout = null;
|
|
26
|
+
this._state = WebsocketReceiver.State.Initial;
|
|
27
|
+
this.compress = config.compress ?? false;
|
|
28
|
+
this.setupNetworkMonitoring();
|
|
29
|
+
}
|
|
30
|
+
get state() {
|
|
31
|
+
return this._state;
|
|
32
|
+
}
|
|
33
|
+
set state(value) {
|
|
34
|
+
this._state = value;
|
|
35
|
+
this.logger.info(`WebSocket state changed to: ${WebsocketReceiver.State[value]}`);
|
|
36
|
+
}
|
|
37
|
+
get logger() {
|
|
38
|
+
return this.c.logger;
|
|
39
|
+
}
|
|
40
|
+
async getGatewayUrl(compress) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await this.c.request.get('/v3/gateway/index', { params: { compress } });
|
|
43
|
+
return res.data.url;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.logger.error('Failed to get gateway URL', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async waitForHello() {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
this.off('hello', helloHandler);
|
|
54
|
+
reject(new Error('WebSocket receive hello code timeout'));
|
|
55
|
+
}, this.helloTimeout);
|
|
56
|
+
const helloHandler = (data) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
this.session_id = data.session_id;
|
|
59
|
+
resolve(data.code);
|
|
60
|
+
};
|
|
61
|
+
this.once('hello', helloHandler);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
sendPing() {
|
|
65
|
+
if (this.ws?.readyState === ws_1.WebSocket.OPEN) {
|
|
66
|
+
// 发送包含当前 sn 的 Ping(必须为数字)
|
|
67
|
+
this.ws.send(JSON.stringify({
|
|
68
|
+
s: constans_1.OpCode.Ping,
|
|
69
|
+
sn: this.sn || 0
|
|
70
|
+
}));
|
|
71
|
+
this.lastPongTime = Date.now();
|
|
72
|
+
// 设置心跳超时检测
|
|
73
|
+
if (this.heartbeatTimeout) {
|
|
74
|
+
clearTimeout(this.heartbeatTimeout);
|
|
75
|
+
}
|
|
76
|
+
this.heartbeatTimeout = setTimeout(() => {
|
|
77
|
+
if (Date.now() - this.lastPongTime > 10000) { // 10秒超时
|
|
78
|
+
this.logger.debug('Heartbeat timeout, reconnecting...');
|
|
79
|
+
this.scheduleReconnect();
|
|
80
|
+
}
|
|
81
|
+
}, 10000);
|
|
82
|
+
this.logger.debug(`Sent ping to server with sn: ${this.sn}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
setupEventListeners() {
|
|
86
|
+
if (!this.ws)
|
|
87
|
+
return;
|
|
88
|
+
this.ws.on('message', (event) => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(this.decryptData(event.toString(), this.config.encrypt_key ?? ''));
|
|
91
|
+
if (data.sn)
|
|
92
|
+
this.sn = Number(data.sn); // 确保为数字
|
|
93
|
+
switch (data.s) {
|
|
94
|
+
case constans_1.OpCode.Hello:
|
|
95
|
+
this.emit('hello', data.d);
|
|
96
|
+
break;
|
|
97
|
+
case constans_1.OpCode.Event:
|
|
98
|
+
this.emit('event', data.d);
|
|
99
|
+
break;
|
|
100
|
+
case constans_1.OpCode.Reconnect:
|
|
101
|
+
this.logger.debug('Received reconnect command from server', data.d);
|
|
102
|
+
this.scheduleReconnect();
|
|
103
|
+
break;
|
|
104
|
+
case constans_1.OpCode.ResumeAck:
|
|
105
|
+
this.emit('resume', data.d);
|
|
106
|
+
break;
|
|
107
|
+
case constans_1.OpCode.Pong:
|
|
108
|
+
this.lastPongTime = Date.now();
|
|
109
|
+
this.logger.debug('Received pong from server');
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
this.logger.debug('Received unknown opcode', data.s);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.logger.error('Error processing WebSocket message', error);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
this.ws.on('close', (code, reason) => {
|
|
120
|
+
this.logger.info(`WebSocket connection closed, code: ${code}, reason: ${reason.toString()}`);
|
|
121
|
+
this.cleanup();
|
|
122
|
+
this.state = WebsocketReceiver.State.Closed;
|
|
123
|
+
// 自动重连(非正常关闭时)
|
|
124
|
+
if (code !== 1000) { // 1000是正常关闭
|
|
125
|
+
this.scheduleReconnect();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.ws.on('error', (error) => {
|
|
129
|
+
this.logger.error('WebSocket error', error);
|
|
130
|
+
this.cleanup();
|
|
131
|
+
this.state = WebsocketReceiver.State.Closed;
|
|
132
|
+
this.scheduleReconnect();
|
|
133
|
+
});
|
|
134
|
+
this.ws.on('open', () => {
|
|
135
|
+
this.logger.debug('WebSocket connection opened');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
cleanup() {
|
|
139
|
+
// Clear all timers
|
|
140
|
+
this.timers.forEach((timer, key) => {
|
|
141
|
+
clearInterval(timer);
|
|
142
|
+
this.timers.delete(key);
|
|
143
|
+
});
|
|
144
|
+
// 清理重连定时器
|
|
145
|
+
if (this.reconnectTimer) {
|
|
146
|
+
clearTimeout(this.reconnectTimer);
|
|
147
|
+
this.reconnectTimer = null;
|
|
148
|
+
}
|
|
149
|
+
// 清理心跳超时检测
|
|
150
|
+
if (this.heartbeatTimeout) {
|
|
151
|
+
clearTimeout(this.heartbeatTimeout);
|
|
152
|
+
this.heartbeatTimeout = null;
|
|
153
|
+
}
|
|
154
|
+
// Clean up WebSocket
|
|
155
|
+
if (this.ws) {
|
|
156
|
+
this.ws.removeAllListeners();
|
|
157
|
+
if (this.ws.readyState === ws_1.WebSocket.OPEN) {
|
|
158
|
+
this.ws.close();
|
|
159
|
+
}
|
|
160
|
+
this.ws = null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
getResumeQueryParams() {
|
|
164
|
+
const params = new URLSearchParams();
|
|
165
|
+
if (this.sn)
|
|
166
|
+
params.append('sn', this.sn.toString());
|
|
167
|
+
if (this.session_id)
|
|
168
|
+
params.append('session_id', this.session_id);
|
|
169
|
+
params.append('resume', '1');
|
|
170
|
+
return params;
|
|
171
|
+
}
|
|
172
|
+
scheduleReconnect() {
|
|
173
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
174
|
+
this.logger.error('Max reconnection attempts reached');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.reconnectAttempts++;
|
|
178
|
+
const delay = this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
179
|
+
this.logger.info(`Scheduling reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
180
|
+
this.reconnectTimer = setTimeout(() => {
|
|
181
|
+
this.reconnect().catch(error => {
|
|
182
|
+
this.logger.error('Reconnect failed', error);
|
|
183
|
+
this.scheduleReconnect();
|
|
184
|
+
});
|
|
185
|
+
}, delay);
|
|
186
|
+
}
|
|
187
|
+
async reconnect() {
|
|
188
|
+
this.logger.info('Attempting to reconnect...');
|
|
189
|
+
this.cleanup();
|
|
190
|
+
this.state = WebsocketReceiver.State.Reconnection;
|
|
191
|
+
await this.connect(true);
|
|
192
|
+
}
|
|
193
|
+
setupNetworkMonitoring() {
|
|
194
|
+
// 在Node.js环境中监听网络状态变化
|
|
195
|
+
if (typeof process !== 'undefined') {
|
|
196
|
+
// 可以添加定期的网络连通性检查
|
|
197
|
+
const networkCheckInterval = setInterval(() => {
|
|
198
|
+
// 简单的网络连通性检查
|
|
199
|
+
require('dns').resolve('www.kookapp.cn', (err) => {
|
|
200
|
+
if (err) {
|
|
201
|
+
this.logger.debug('Network connectivity issue detected');
|
|
202
|
+
if (this.state === WebsocketReceiver.State.Closed) {
|
|
203
|
+
this.scheduleReconnect();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}, 30000); // 每30秒检查一次
|
|
208
|
+
// 在清理时移除监听
|
|
209
|
+
this.timers.set('networkCheck', networkCheckInterval);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async connect(isReconnect = false) {
|
|
213
|
+
try {
|
|
214
|
+
this.state = WebsocketReceiver.State.PullingGateway;
|
|
215
|
+
const gatewayUrl = await this.getGatewayUrl(this.config.compress ? 1 : 0);
|
|
216
|
+
const url = new URL(gatewayUrl);
|
|
217
|
+
if (isReconnect) {
|
|
218
|
+
this.getResumeQueryParams().forEach((value, key) => {
|
|
219
|
+
url.searchParams.append(key, value);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
this.url = url;
|
|
223
|
+
this.state = WebsocketReceiver.State.Connecting;
|
|
224
|
+
this.ws = new ws_1.WebSocket(this.url);
|
|
225
|
+
this.setupEventListeners();
|
|
226
|
+
const receiveCode = await this.waitForHello();
|
|
227
|
+
if (receiveCode !== 0) {
|
|
228
|
+
this.logger.error(`WebSocket connect failed, receive code: ${receiveCode}`);
|
|
229
|
+
return this.connect(isReconnect);
|
|
230
|
+
}
|
|
231
|
+
this.state = WebsocketReceiver.State.Open;
|
|
232
|
+
this.reconnectAttempts = 0; // 重置重连计数器
|
|
233
|
+
this.sendPing();
|
|
234
|
+
// Schedule periodic pings with jitter
|
|
235
|
+
const pingInterval = this.pingBaseInterval + Math.random() * this.pingJitterRange;
|
|
236
|
+
this.timers.set('ping', setInterval(() => this.sendPing(), pingInterval));
|
|
237
|
+
this.logger.info(`WebSocket connected successfully to ${this.url.host}`);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.logger.error('WebSocket connection error', error);
|
|
241
|
+
this.cleanup();
|
|
242
|
+
// 连接失败时也触发重连
|
|
243
|
+
if (!isReconnect) {
|
|
244
|
+
this.scheduleReconnect();
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async disconnect() {
|
|
250
|
+
this.logger.info('Disconnecting WebSocket...');
|
|
251
|
+
this.cleanup();
|
|
252
|
+
this.state = WebsocketReceiver.State.Closed;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.WebsocketReceiver = WebsocketReceiver;
|
|
256
|
+
(function (WebsocketReceiver) {
|
|
257
|
+
let State;
|
|
258
|
+
(function (State) {
|
|
259
|
+
State[State["Initial"] = 0] = "Initial";
|
|
260
|
+
State[State["PullingGateway"] = 1] = "PullingGateway";
|
|
261
|
+
State[State["Connecting"] = 2] = "Connecting";
|
|
262
|
+
State[State["Open"] = 3] = "Open";
|
|
263
|
+
State[State["Closed"] = 4] = "Closed";
|
|
264
|
+
State[State["Reconnection"] = 5] = "Reconnection";
|
|
265
|
+
})(State = WebsocketReceiver.State || (WebsocketReceiver.State = {}));
|
|
266
|
+
})(WebsocketReceiver || (exports.WebsocketReceiver = WebsocketReceiver = {}));
|
|
267
|
+
receiver_1.Receiver.register('websocket', WebsocketReceiver);
|
package/lib/elements.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.segment = exports.msgMod = exports.element = void 0;
|
|
4
|
+
exports.validateCard = validateCard;
|
|
5
|
+
// 验证卡片消息
|
|
6
|
+
function validateCard(card) {
|
|
7
|
+
// 1. 验证卡片结构
|
|
8
|
+
if (!card.modules || !Array.isArray(card.modules)) {
|
|
9
|
+
throw new Error("卡片必须包含 'modules' 数组");
|
|
10
|
+
}
|
|
11
|
+
// 2. 验证模块数量限制
|
|
12
|
+
const totalModules = card.modules.length;
|
|
13
|
+
if (totalModules > 50) {
|
|
14
|
+
throw new Error(`卡片模块数量不能超过 50,当前有 ${totalModules} 个`);
|
|
15
|
+
}
|
|
16
|
+
// 3. 验证主题限制
|
|
17
|
+
if (card.theme === 'invisible') {
|
|
18
|
+
const allowedTypes = new Set(['context', 'action-group', 'divider', 'header', 'container', 'file', 'audio', 'video']);
|
|
19
|
+
for (const mod of card.modules) {
|
|
20
|
+
if (!allowedTypes.has(mod.type)) {
|
|
21
|
+
// 使用类型守卫检查 section 模块
|
|
22
|
+
if (isSectionModule(mod) && mod.accessory) {
|
|
23
|
+
throw new Error("在 'invisible' 主题下,section 模块不能包含 accessory");
|
|
24
|
+
}
|
|
25
|
+
if (!allowedTypes.has(mod.type)) {
|
|
26
|
+
throw new Error(`在 'invisible' 主题下,不允许使用 ${mod.type} 模块`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 4. 验证每个模块
|
|
32
|
+
for (const mod of card.modules) {
|
|
33
|
+
validateModule(mod);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// 类型守卫函数
|
|
37
|
+
function isSectionModule(module) {
|
|
38
|
+
return module.type === 'section';
|
|
39
|
+
}
|
|
40
|
+
function validateModule(module) {
|
|
41
|
+
switch (module.type) {
|
|
42
|
+
case 'header':
|
|
43
|
+
const headerMod = module;
|
|
44
|
+
if (!headerMod.text || headerMod.text.type !== 'plain-text') {
|
|
45
|
+
throw new Error("header 模块必须包含 'text' 元素,且类型必须是 'plain-text'");
|
|
46
|
+
}
|
|
47
|
+
if (headerMod.text.content.length > 100) {
|
|
48
|
+
throw new Error("header 文本内容不能超过 100 个字符");
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
case 'section':
|
|
52
|
+
if (!isSectionModule(module))
|
|
53
|
+
break;
|
|
54
|
+
const validTextTypes = ['plain-text', 'kmarkdown', 'paragraph'];
|
|
55
|
+
if (module.text && !validTextTypes.includes(module.text.type)) {
|
|
56
|
+
throw new Error(`section 的 text 必须是以下类型之一: ${validTextTypes.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
const validAccessoryTypes = ['image', 'button'];
|
|
59
|
+
if (module.accessory && !validAccessoryTypes.includes(module.accessory.type)) {
|
|
60
|
+
throw new Error(`section 的 accessory 必须是以下类型之一: ${validAccessoryTypes.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
if (module.mode !== 'left' && module.mode !== 'right') {
|
|
63
|
+
throw new Error("section 的 mode 必须是 'left' 或 'right'");
|
|
64
|
+
}
|
|
65
|
+
if (module.accessory && module.mode === 'left' && module.accessory.type === 'button') {
|
|
66
|
+
throw new Error("button 不能放置在左侧");
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case 'image-group':
|
|
70
|
+
const imageGroupMod = module;
|
|
71
|
+
if (!imageGroupMod.elements || !Array.isArray(imageGroupMod.elements)) {
|
|
72
|
+
throw new Error("image-group 必须包含 'elements' 数组");
|
|
73
|
+
}
|
|
74
|
+
if (imageGroupMod.elements.length < 1 || imageGroupMod.elements.length > 9) {
|
|
75
|
+
throw new Error("image-group 只能包含 1-9 张图片");
|
|
76
|
+
}
|
|
77
|
+
for (const el of imageGroupMod.elements) {
|
|
78
|
+
if (el.type !== 'image') {
|
|
79
|
+
throw new Error("image-group 只能包含 image 元素");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'container':
|
|
84
|
+
const containerMod = module;
|
|
85
|
+
if (!containerMod.elements || !Array.isArray(containerMod.elements)) {
|
|
86
|
+
throw new Error("container 必须包含 'elements' 数组");
|
|
87
|
+
}
|
|
88
|
+
if (containerMod.elements.length < 1 || containerMod.elements.length > 9) {
|
|
89
|
+
throw new Error("container 只能包含 1-9 张图片");
|
|
90
|
+
}
|
|
91
|
+
for (const el of containerMod.elements) {
|
|
92
|
+
if (el.type !== 'image') {
|
|
93
|
+
throw new Error("container 只能包含 image 元素");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 'action-group':
|
|
98
|
+
const actionMod = module;
|
|
99
|
+
if (!actionMod.elements || !Array.isArray(actionMod.elements)) {
|
|
100
|
+
throw new Error("action-group 必须包含 'elements' 数组");
|
|
101
|
+
}
|
|
102
|
+
if (actionMod.elements.length > 4) {
|
|
103
|
+
throw new Error("action-group 最多只能包含 4 个按钮");
|
|
104
|
+
}
|
|
105
|
+
for (const el of actionMod.elements) {
|
|
106
|
+
if (el.type !== 'button') {
|
|
107
|
+
throw new Error("action-group 只能包含 button 元素");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case 'context':
|
|
112
|
+
const contextMod = module;
|
|
113
|
+
if (!contextMod.elements || !Array.isArray(contextMod.elements)) {
|
|
114
|
+
throw new Error("context 必须包含 'elements' 数组");
|
|
115
|
+
}
|
|
116
|
+
if (contextMod.elements.length > 10) {
|
|
117
|
+
throw new Error("context 最多只能包含 10 个元素");
|
|
118
|
+
}
|
|
119
|
+
const validContextTypes = ['plain-text', 'kmarkdown', 'image'];
|
|
120
|
+
for (const el of contextMod.elements) {
|
|
121
|
+
if (!validContextTypes.includes(el.type)) {
|
|
122
|
+
throw new Error(`context 只能包含以下类型元素: ${validContextTypes.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 'file':
|
|
127
|
+
case 'audio':
|
|
128
|
+
case 'video':
|
|
129
|
+
const fileMod = module;
|
|
130
|
+
if (!fileMod.src) {
|
|
131
|
+
throw new Error(`${fileMod.type} 模块必须包含 'src' 属性`);
|
|
132
|
+
}
|
|
133
|
+
if (!fileMod.title) {
|
|
134
|
+
throw new Error(`${fileMod.type} 模块必须包含 'title' 属性`);
|
|
135
|
+
}
|
|
136
|
+
if (fileMod.type === 'audio' && !('cover' in fileMod)) {
|
|
137
|
+
console.warn("audio 模块建议提供 'cover' 属性");
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case 'countdown':
|
|
141
|
+
const countdownMod = module;
|
|
142
|
+
if (!countdownMod.endTime) {
|
|
143
|
+
throw new Error("countdown 模块必须包含 'endTime' 属性");
|
|
144
|
+
}
|
|
145
|
+
if (!countdownMod.mode || !['day', 'hour', 'second'].includes(countdownMod.mode)) {
|
|
146
|
+
throw new Error("countdown 的 mode 必须是 'day', 'hour' 或 'second'");
|
|
147
|
+
}
|
|
148
|
+
if (countdownMod.mode === 'second' && !countdownMod.startTime) {
|
|
149
|
+
throw new Error("当 mode 为 'second' 时,必须提供 'startTime'");
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case 'invite':
|
|
153
|
+
const inviteMod = module;
|
|
154
|
+
if (!inviteMod.code) {
|
|
155
|
+
throw new Error("invite 模块必须包含 'code' 属性");
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 验证元素
|
|
161
|
+
function validateElement(element) {
|
|
162
|
+
switch (element.type) {
|
|
163
|
+
case 'plain-text':
|
|
164
|
+
if (!element.content) {
|
|
165
|
+
throw new Error("plain-text 元素必须包含 'content' 属性");
|
|
166
|
+
}
|
|
167
|
+
if (element.content.length > 2000) {
|
|
168
|
+
throw new Error("plain-text 内容不能超过 2000 个字符");
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
case 'kmarkdown':
|
|
172
|
+
if (!element.content) {
|
|
173
|
+
throw new Error("kmarkdown 元素必须包含 'content' 属性");
|
|
174
|
+
}
|
|
175
|
+
if (element.content.length > 5000) {
|
|
176
|
+
throw new Error("kmarkdown 内容不能超过 5000 个字符");
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'image':
|
|
180
|
+
if (!element.src) {
|
|
181
|
+
throw new Error("image 元素必须包含 'src' 属性");
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'button':
|
|
185
|
+
if (!element.text) {
|
|
186
|
+
throw new Error("button 元素必须包含 'text' 属性");
|
|
187
|
+
}
|
|
188
|
+
if (!element.value) {
|
|
189
|
+
throw new Error("button 元素必须包含 'value' 属性");
|
|
190
|
+
}
|
|
191
|
+
if (element.click && !['link', 'return-val'].includes(element.click)) {
|
|
192
|
+
throw new Error("button 的 click 属性必须是 'link' 或 'return-val'");
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case 'paragraph':
|
|
196
|
+
if (!element.fields || !Array.isArray(element.fields)) {
|
|
197
|
+
throw new Error("paragraph 必须包含 'fields' 数组");
|
|
198
|
+
}
|
|
199
|
+
if (element.fields.length > 50) {
|
|
200
|
+
throw new Error("paragraph 最多只能包含 50 个字段");
|
|
201
|
+
}
|
|
202
|
+
if (element.cols < 1 || element.cols > 3) {
|
|
203
|
+
throw new Error("paragraph 的 cols 必须是 1, 2 或 3");
|
|
204
|
+
}
|
|
205
|
+
const validFieldTypes = ['plain-text', 'kmarkdown'];
|
|
206
|
+
for (const field of element.fields) {
|
|
207
|
+
if (!validFieldTypes.includes(field.type)) {
|
|
208
|
+
throw new Error(`paragraph 只能包含以下类型字段: ${validFieldTypes.join(', ')}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.element = function (type, data) {
|
|
215
|
+
return {
|
|
216
|
+
type,
|
|
217
|
+
...data
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
exports.element.text = (content, emoji = true) => ({
|
|
221
|
+
type: 'plain-text',
|
|
222
|
+
content,
|
|
223
|
+
emoji
|
|
224
|
+
});
|
|
225
|
+
exports.element.markdown = (content) => ({
|
|
226
|
+
type: 'kmarkdown',
|
|
227
|
+
content
|
|
228
|
+
});
|
|
229
|
+
exports.element.image = (src, alt, size, circle, fallbackUrl) => ({
|
|
230
|
+
type: 'image',
|
|
231
|
+
src,
|
|
232
|
+
...(alt && { alt }),
|
|
233
|
+
...(size && { size }),
|
|
234
|
+
...(circle && { circle }),
|
|
235
|
+
...(fallbackUrl && { fallbackUrl })
|
|
236
|
+
});
|
|
237
|
+
exports.element.button = (text, value, theme = 'primary', click) => ({
|
|
238
|
+
type: 'button',
|
|
239
|
+
text,
|
|
240
|
+
value,
|
|
241
|
+
theme,
|
|
242
|
+
...(click && { click })
|
|
243
|
+
});
|
|
244
|
+
exports.element.paragraph = (cols, fields) => ({
|
|
245
|
+
type: 'paragraph',
|
|
246
|
+
cols,
|
|
247
|
+
fields
|
|
248
|
+
});
|
|
249
|
+
exports.msgMod = function (type, data) {
|
|
250
|
+
return {
|
|
251
|
+
type,
|
|
252
|
+
...data
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
exports.msgMod.header = (text) => ({
|
|
256
|
+
type: 'header',
|
|
257
|
+
text
|
|
258
|
+
});
|
|
259
|
+
exports.msgMod.section = (text, mode = 'left', accessory) => ({
|
|
260
|
+
type: 'section',
|
|
261
|
+
...(text && { text }),
|
|
262
|
+
mode,
|
|
263
|
+
...(accessory && { accessory })
|
|
264
|
+
});
|
|
265
|
+
exports.msgMod.image = (elements) => ({
|
|
266
|
+
type: 'image-group',
|
|
267
|
+
elements
|
|
268
|
+
});
|
|
269
|
+
exports.msgMod.container = (elements) => ({
|
|
270
|
+
type: 'container',
|
|
271
|
+
elements
|
|
272
|
+
});
|
|
273
|
+
exports.msgMod.action = (elements) => ({
|
|
274
|
+
type: 'action-group',
|
|
275
|
+
elements
|
|
276
|
+
});
|
|
277
|
+
exports.msgMod.context = (elements) => ({
|
|
278
|
+
type: 'context',
|
|
279
|
+
elements
|
|
280
|
+
});
|
|
281
|
+
exports.msgMod.divider = () => ({
|
|
282
|
+
type: 'divider'
|
|
283
|
+
});
|
|
284
|
+
exports.msgMod.file = (src, title) => ({
|
|
285
|
+
type: 'file',
|
|
286
|
+
src,
|
|
287
|
+
title
|
|
288
|
+
});
|
|
289
|
+
exports.msgMod.audio = (src, title, cover) => ({
|
|
290
|
+
type: 'audio',
|
|
291
|
+
src,
|
|
292
|
+
title,
|
|
293
|
+
...(cover && { cover })
|
|
294
|
+
});
|
|
295
|
+
exports.msgMod.video = (src, title) => ({
|
|
296
|
+
type: 'video',
|
|
297
|
+
src,
|
|
298
|
+
title
|
|
299
|
+
});
|
|
300
|
+
exports.msgMod.countdown = (endTime, mode = 'hour', startTime) => ({
|
|
301
|
+
type: 'countdown',
|
|
302
|
+
endTime,
|
|
303
|
+
mode,
|
|
304
|
+
...(startTime && { startTime })
|
|
305
|
+
});
|
|
306
|
+
exports.msgMod.invite = (code) => ({
|
|
307
|
+
type: 'invite',
|
|
308
|
+
code
|
|
309
|
+
});
|
|
310
|
+
exports.segment = function (type, attrs) {
|
|
311
|
+
return {
|
|
312
|
+
type,
|
|
313
|
+
...attrs
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
exports.segment.text = (text) => (0, exports.segment)('text', { text });
|
|
317
|
+
exports.segment.at = (user_id) => (0, exports.segment)('at', { user_id });
|
|
318
|
+
// 修改后的卡片消息段生成器
|
|
319
|
+
exports.segment.card = (modules, options) => {
|
|
320
|
+
// 创建卡片对象
|
|
321
|
+
const cardObj = {
|
|
322
|
+
type: 'card',
|
|
323
|
+
modules,
|
|
324
|
+
...(options || {})
|
|
325
|
+
};
|
|
326
|
+
// 返回一个特殊对象,标记为卡片类型
|
|
327
|
+
return {
|
|
328
|
+
__isCard: true,
|
|
329
|
+
...cardObj
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
exports.segment.image = (url, title) => (0, exports.segment)('image', { url, ...(title && { title }) });
|
|
333
|
+
exports.segment.video = (url, title) => (0, exports.segment)('video', { url, ...(title && { title }) });
|
|
334
|
+
exports.segment.audio = (url, title) => (0, exports.segment)('audio', { url, ...(title && { title }) });
|
|
335
|
+
exports.segment.markdown = (text) => (0, exports.segment)('markdown', { text });
|
|
336
|
+
exports.segment.reply = (message_id) => (0, exports.segment)('reply', { id: message_id });
|
|
337
|
+
exports.segment.file = (url, name, file_type, size) => (0, exports.segment)('file', {
|
|
338
|
+
url,
|
|
339
|
+
...(name && { name }),
|
|
340
|
+
...(file_type && { file_type }),
|
|
341
|
+
...(size && { size })
|
|
342
|
+
});
|