koishi-plugin-imx 2.2.1 → 2.2.2

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.
@@ -7,6 +7,7 @@ exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
10
+ const axios_error_1 = require("../utils/axios-error");
10
11
  exports.name = 'bilibili';
11
12
  exports.Config = koishi_1.Schema.object({
12
13
  enabled: koishi_1.Schema.boolean().description('启用 Bilibili 直播监控').default(false),
@@ -38,11 +39,11 @@ function apply(ctx, config) {
38
39
  }
39
40
  const statusList = [];
40
41
  for (const roomId of config.roomIds) {
41
- try {
42
- const isLive = await getRoomLiveStatus(roomId);
42
+ const isLive = await (0, axios_error_1.axiosRequestWithLog)(logger, () => getRoomLiveStatus(roomId), `获取房间 ${roomId} 状态`);
43
+ if (isLive !== null) {
43
44
  statusList.push(`房间 ${roomId}: ${isLive ? '🔴 直播中' : '⚫ 未直播'}`);
44
45
  }
45
- catch (error) {
46
+ else {
46
47
  statusList.push(`房间 ${roomId}: ❌ 获取失败`);
47
48
  }
48
49
  }
@@ -52,20 +53,21 @@ function apply(ctx, config) {
52
53
  }
53
54
  async function checkLiveStatus(ctx, config, logger) {
54
55
  for (const roomId of config.roomIds) {
55
- try {
56
- const isLive = await getRoomLiveStatus(roomId);
57
- const wasLive = liveStatusCache.get(roomId) || false;
58
- if (isLive && !wasLive) {
59
- // 开播通知
60
- const roomInfo = await getRoomInfo(roomId);
56
+ const isLive = await (0, axios_error_1.axiosRequestWithLog)(logger, () => getRoomLiveStatus(roomId), `检查房间 ${roomId} 直播状态`);
57
+ if (isLive === null) {
58
+ // 请求失败,跳过此次检查
59
+ continue;
60
+ }
61
+ const wasLive = liveStatusCache.get(roomId) || false;
62
+ if (isLive && !wasLive) {
63
+ // 开播通知
64
+ const roomInfo = await (0, axios_error_1.axiosRequestWithLog)(logger, () => getRoomInfo(roomId), `获取房间 ${roomId} 信息`);
65
+ if (roomInfo) {
61
66
  const message = formatLiveMessage(roomInfo);
62
67
  await sendToChannels(ctx, config.watchChannels, message, logger);
63
68
  }
64
- liveStatusCache.set(roomId, isLive);
65
- }
66
- catch (error) {
67
- logger.error(`检查房间 ${roomId} 状态失败:`, error);
68
69
  }
70
+ liveStatusCache.set(roomId, isLive);
69
71
  }
70
72
  }
71
73
  async function getRoomLiveStatus(roomId) {
@@ -90,7 +92,8 @@ async function sendToChannels(ctx, channels, message, logger) {
90
92
  await ctx.broadcast([channelId], message);
91
93
  }
92
94
  catch (error) {
93
- logger.error(`发送消息到频道 ${channelId} 失败:`, error);
95
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, `发送消息到频道 ${channelId}`);
96
+ logger.warn(simplified.message);
94
97
  }
95
98
  }
96
99
  }
@@ -7,6 +7,7 @@ exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
10
+ const axios_error_1 = require("../utils/axios-error");
10
11
  exports.name = 'github';
11
12
  exports.Config = koishi_1.Schema.object({
12
13
  enabled: koishi_1.Schema.boolean().description('启用 GitHub 功能').default(false),
@@ -32,11 +33,11 @@ function apply(ctx, config) {
32
33
  }
33
34
  const statusList = [];
34
35
  for (const repo of config.repositories) {
35
- try {
36
- const repoInfo = await getRepoInfo(repo);
36
+ const repoInfo = await (0, axios_error_1.axiosRequestWithLog)(logger, () => getRepoInfo(repo), `获取仓库 ${repo} 信息`);
37
+ if (repoInfo) {
37
38
  statusList.push(`${repo}: ⭐ ${repoInfo.stargazers_count} | 🍴 ${repoInfo.forks_count}`);
38
39
  }
39
- catch (error) {
40
+ else {
40
41
  statusList.push(`${repo}: ❌ 获取失败`);
41
42
  }
42
43
  }
@@ -8,7 +8,6 @@ export interface Config {
8
8
  secret?: string;
9
9
  path?: string;
10
10
  watchChannels?: string[];
11
- requireSignature?: boolean;
12
11
  };
13
12
  greeting?: {
14
13
  enabled?: boolean;
@@ -48,6 +48,7 @@ const hitokoto_1 = require("../utils/hitokoto");
48
48
  const mx_api_1 = require("../utils/mx-api");
49
49
  const mx_url_builder_1 = require("../utils/mx-url-builder");
50
50
  const mx_event_handler_1 = require("../utils/mx-event-handler");
51
+ const axios_error_1 = require("../utils/axios-error");
51
52
  dayjs_1.default.extend(relativeTime_1.default);
52
53
  exports.name = 'mx-space';
53
54
  exports.inject = ['server'];
@@ -58,7 +59,6 @@ exports.Config = koishi_1.Schema.object({
58
59
  secret: koishi_1.Schema.string().description('MX Space Webhook Secret').role('secret'),
59
60
  path: koishi_1.Schema.string().description('Webhook 路径').default('/mx-space/webhook'),
60
61
  watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的频道ID列表').default([]),
61
- requireSignature: koishi_1.Schema.boolean().description('是否要求签名验证(禁用可允许健康检查等请求)').default(true),
62
62
  }).description('Webhook 配置'),
63
63
  greeting: koishi_1.Schema.object({
64
64
  enabled: koishi_1.Schema.boolean().description('启用问候功能').default(true),
@@ -91,7 +91,7 @@ function apply(ctx, config) {
91
91
  memoChatId: null,
92
92
  };
93
93
  // 设置 Webhook 处理器
94
- if (config.webhook && ctx.server) {
94
+ if (config.webhook?.secret && ctx.server) {
95
95
  setupWebhook(ctx, config, logger);
96
96
  }
97
97
  // 设置问候功能
@@ -113,11 +113,11 @@ function apply(ctx, config) {
113
113
  logger.info('MX Space 模块已启动');
114
114
  }
115
115
  function setupWebhook(ctx, config, logger) {
116
- if (!ctx.server) {
117
- logger.warn('Server 插件未启用,无法设置 webhook');
116
+ if (!config.webhook?.secret || !ctx.server) {
117
+ logger.warn('Webhook 配置不完整或 server 插件未启用');
118
118
  return;
119
119
  }
120
- const webhookPath = config.webhook?.path || '/mx-space/webhook';
120
+ const webhookPath = config.webhook.path || '/mx-space/webhook';
121
121
  ctx.server.post(webhookPath, async (koaCtx) => {
122
122
  try {
123
123
  logger.debug('收到 webhook 请求:', {
@@ -127,8 +127,17 @@ function setupWebhook(ctx, config, logger) {
127
127
  body: koaCtx.request.body
128
128
  });
129
129
  const body = koaCtx.request.body;
130
- const signature = koaCtx.request.headers['x-hub-signature-256'] ||
131
- koaCtx.request.headers['x-webhook-signature256'];
130
+ const headers = koaCtx.request.headers;
131
+ // 兼容多种签名头格式
132
+ // GitHub: x-hub-signature-256
133
+ // MX Space: X-Webhook-Signature (SHA1), X-Webhook-Signature256 (SHA256)
134
+ const signature = headers['x-hub-signature-256'] ||
135
+ headers['x-webhook-signature256'] ||
136
+ headers['x-webhook-signature'];
137
+ // 获取事件类型和其他 MX Space 专用头
138
+ const eventType = headers['x-webhook-event'];
139
+ const webhookId = headers['x-webhook-id'];
140
+ const timestamp = headers['x-webhook-timestamp'];
132
141
  // 检查请求体是否存在
133
142
  if (!body) {
134
143
  logger.warn('Webhook 请求体为空');
@@ -136,75 +145,94 @@ function setupWebhook(ctx, config, logger) {
136
145
  koaCtx.body = { error: 'Request body is empty' };
137
146
  return;
138
147
  }
139
- // 检查webhook事件类型
140
- const eventType = koaCtx.request.headers['x-webhook-event'];
141
- // 健康检查等系统事件允许无签名通过
142
- const isSystemEvent = ['health_check', 'ping'].includes(eventType);
143
- // 检查是否需要签名验证
144
- const requireSignature = config.webhook?.requireSignature !== false; // 默认为true
145
- // 验证签名(系统事件可以跳过)
146
- if (requireSignature && config.webhook?.secret && !isSystemEvent) {
147
- if (!signature) {
148
- logger.warn(`事件 ${eventType} 缺少签名头,但配置要求签名验证`);
149
- koaCtx.status = 401;
150
- koaCtx.body = { error: 'Missing signature' };
151
- return;
152
- }
148
+ // 验证签名
149
+ if (config.webhook?.secret && signature) {
153
150
  const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
154
151
  const payload = JSON.stringify(body);
155
- const hmac = crypto.createHmac('sha256', config.webhook.secret);
156
- hmac.update(payload);
157
- const expectedSignature = 'sha256=' + hmac.digest('hex');
152
+ let isValidSignature = false;
153
+ // 判断签名算法并验证
154
+ if (signature.startsWith('sha256=') || headers['x-webhook-signature256']) {
155
+ // SHA256 签名验证
156
+ const hmac = crypto.createHmac('sha256', config.webhook.secret);
157
+ hmac.update(payload);
158
+ const expectedSignature = signature.startsWith('sha256=')
159
+ ? 'sha256=' + hmac.digest('hex')
160
+ : hmac.digest('hex');
161
+ isValidSignature = signature === expectedSignature;
162
+ }
163
+ else if (headers['x-webhook-signature']) {
164
+ // SHA1 签名验证(MX Space 默认)
165
+ const hmac = crypto.createHmac('sha1', config.webhook.secret);
166
+ hmac.update(payload);
167
+ const expectedSignature = hmac.digest('hex');
168
+ isValidSignature = signature === expectedSignature;
169
+ }
158
170
  logger.debug('签名验证:', {
159
- event: eventType,
160
171
  received: signature,
161
- expected: expectedSignature
172
+ algorithm: signature.startsWith('sha256=') ? 'SHA256' : (headers['x-webhook-signature256'] ? 'SHA256' : 'SHA1'),
173
+ isValid: isValidSignature,
174
+ eventType,
175
+ webhookId,
176
+ timestamp
162
177
  });
163
- if (signature !== expectedSignature) {
164
- logger.warn(`事件 ${eventType} 签名验证失败`);
178
+ if (!isValidSignature) {
179
+ logger.warn('Webhook 签名验证失败');
165
180
  koaCtx.status = 401;
166
181
  koaCtx.body = { error: 'Invalid signature' };
167
182
  return;
168
183
  }
169
184
  }
170
- else if (!requireSignature) {
171
- logger.debug('签名验证已禁用,跳过签名检查');
185
+ else if (config.webhook?.secret && !signature) {
186
+ logger.warn('配置了签名但请求中没有签名头');
187
+ koaCtx.status = 401;
188
+ koaCtx.body = { error: 'Missing signature' };
189
+ return;
172
190
  }
173
- else if (isSystemEvent) {
174
- logger.debug(`系统事件 ${eventType} 跳过签名验证`);
191
+ // 检查请求体格式
192
+ // 兼容多种格式:
193
+ // 1. GitHub 格式: { type, data }
194
+ // 2. MX Space 格式: 直接的事件数据,事件类型在 X-Webhook-Event 头中
195
+ let eventTypeToProcess;
196
+ let eventData;
197
+ if (eventType) {
198
+ // MX Space 格式:事件类型在头部,数据在请求体
199
+ eventTypeToProcess = eventType;
200
+ eventData = body;
175
201
  }
176
- // 检查标准请求体格式
177
- if (!body.type || !body.data) {
178
- // 如果是健康检查但没有标准格式,也允许通过
179
- if (eventType === 'health_check' || Object.keys(body).length === 0) {
180
- logger.info('处理健康检查或空载荷请求');
181
- koaCtx.status = 200;
182
- koaCtx.body = { message: 'Health check successful' };
183
- return;
184
- }
202
+ else if (body.type && body.data) {
203
+ // GitHub 格式:事件类型和数据都在请求体
204
+ eventTypeToProcess = body.type;
205
+ eventData = body.data;
206
+ }
207
+ else {
185
208
  logger.warn('Webhook 请求体格式错误:', body);
186
209
  koaCtx.status = 400;
187
210
  koaCtx.body = {
188
211
  error: 'Invalid webhook payload',
189
- details: 'Missing required fields: type or data',
190
- received: body
212
+ details: 'Missing required fields: event type or data',
213
+ received: body,
214
+ headers: { eventType, webhookId, timestamp }
191
215
  };
192
216
  return;
193
217
  }
194
- logger.info(`处理 MX Space 事件: ${body.type}`);
218
+ logger.info(`处理 MX Space 事件: ${eventTypeToProcess}`, {
219
+ webhookId,
220
+ timestamp,
221
+ format: eventType ? 'mx-space' : 'github'
222
+ });
195
223
  // 处理事件
196
- await (0, mx_event_handler_1.handleMxSpaceEvent)(ctx, config, body.type, body.data, logger);
224
+ await (0, mx_event_handler_1.handleMxSpaceEvent)(ctx, config, eventTypeToProcess, eventData, logger);
197
225
  koaCtx.status = 200;
198
226
  koaCtx.body = { message: 'Webhook processed successfully' };
199
227
  }
200
228
  catch (error) {
201
- logger.error('处理 MX Space webhook 失败:', error);
229
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '处理 MX Space webhook');
230
+ logger.error(simplified.message);
202
231
  koaCtx.status = 500;
203
- koaCtx.body = { error: 'Internal server error', details: error.message };
232
+ koaCtx.body = { error: 'Internal server error', details: simplified.message };
204
233
  }
205
234
  });
206
- const signatureStatus = config.webhook?.requireSignature !== false ? '已启用' : '已禁用';
207
- logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath},签名验证: ${signatureStatus}`);
235
+ logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath}`);
208
236
  }
209
237
  function setupWelcomeNewMember(ctx, config, logger) {
210
238
  if (!config.welcomeNewMember?.channels?.length)
@@ -220,7 +248,8 @@ function setupWelcomeNewMember(ctx, config, logger) {
220
248
  await session.send(welcomeMessage);
221
249
  }
222
250
  catch (error) {
223
- logger.error('发送欢迎消息失败:', error);
251
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送欢迎消息');
252
+ logger.warn(simplified.message);
224
253
  }
225
254
  });
226
255
  logger.info('新成员欢迎功能已启用');
@@ -250,7 +279,8 @@ function setupCommentReply(ctx, config, logger, globalState) {
250
279
  globalState.memoChatId = null;
251
280
  }
252
281
  catch (error) {
253
- await session.send(`回复失败!${error.message || error}`);
282
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '回复评论');
283
+ await session.send(`回复失败!${simplified.message}`);
254
284
  globalState.toCommentId = null;
255
285
  globalState.memoChatId = null;
256
286
  }
@@ -275,7 +305,8 @@ function setupGreeting(ctx, config, logger) {
275
305
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
276
306
  }
277
307
  catch (error) {
278
- logger.error('发送早安消息失败:', error);
308
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送早安消息');
309
+ logger.warn(simplified.message);
279
310
  }
280
311
  }, null, false, 'Asia/Shanghai');
281
312
  // 晚安定时任务
@@ -294,7 +325,8 @@ function setupGreeting(ctx, config, logger) {
294
325
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
295
326
  }
296
327
  catch (error) {
297
- logger.error('发送晚安消息失败:', error);
328
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送晚安消息');
329
+ logger.warn(simplified.message);
298
330
  }
299
331
  }, null, false, 'Asia/Shanghai');
300
332
  morningJob.start();
@@ -319,7 +351,8 @@ function setupCommands(ctx, config, logger) {
319
351
  return `💭 ${hitokoto}\n\n—— ${from || '未知'}`;
320
352
  }
321
353
  catch (error) {
322
- logger.error('获取一言失败:', error);
354
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '获取一言');
355
+ logger.warn(simplified.message);
323
356
  return '获取一言失败';
324
357
  }
325
358
  });
@@ -0,0 +1,35 @@
1
+ import { Logger } from 'koishi';
2
+ export interface SimplifiedError {
3
+ message: string;
4
+ status?: number;
5
+ code?: string;
6
+ }
7
+ /**
8
+ * 简化 axios 错误信息
9
+ * @param error axios 错误对象
10
+ * @param context 错误上下文描述
11
+ * @returns 简化的错误信息
12
+ */
13
+ export declare function simplifyAxiosError(error: any, context?: string): SimplifiedError;
14
+ /**
15
+ * 记录简化的错误日志
16
+ * @param logger 日志记录器
17
+ * @param error 错误对象
18
+ * @param context 错误上下文
19
+ */
20
+ export declare function logSimplifiedError(logger: Logger, error: any, context?: string): void;
21
+ /**
22
+ * 安全的 axios 请求包装器
23
+ * @param requestFn axios 请求函数
24
+ * @param context 请求上下文描述
25
+ * @returns Promise<T | null>
26
+ */
27
+ export declare function safeAxiosRequest<T>(requestFn: () => Promise<T>, context?: string): Promise<T | null>;
28
+ /**
29
+ * 带日志的 axios 请求包装器
30
+ * @param logger 日志记录器
31
+ * @param requestFn axios 请求函数
32
+ * @param context 请求上下文描述
33
+ * @returns Promise<T | null>
34
+ */
35
+ export declare function axiosRequestWithLog<T>(logger: Logger, requestFn: () => Promise<T>, context?: string): Promise<T | null>;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.simplifyAxiosError = simplifyAxiosError;
7
+ exports.logSimplifiedError = logSimplifiedError;
8
+ exports.safeAxiosRequest = safeAxiosRequest;
9
+ exports.axiosRequestWithLog = axiosRequestWithLog;
10
+ const axios_1 = __importDefault(require("axios"));
11
+ /**
12
+ * 简化 axios 错误信息
13
+ * @param error axios 错误对象
14
+ * @param context 错误上下文描述
15
+ * @returns 简化的错误信息
16
+ */
17
+ function simplifyAxiosError(error, context = '请求') {
18
+ if (axios_1.default.isAxiosError(error)) {
19
+ const axiosError = error;
20
+ // 网络错误
21
+ if (!axiosError.response) {
22
+ return {
23
+ message: `${context}失败: 网络连接错误`,
24
+ code: axiosError.code || 'NETWORK_ERROR'
25
+ };
26
+ }
27
+ // HTTP 错误状态码
28
+ const status = axiosError.response.status;
29
+ const statusText = axiosError.response.statusText;
30
+ switch (status) {
31
+ case 400:
32
+ return { message: `${context}失败: 请求参数错误`, status };
33
+ case 401:
34
+ return { message: `${context}失败: 未授权访问`, status };
35
+ case 403:
36
+ return { message: `${context}失败: 访问被拒绝`, status };
37
+ case 404:
38
+ return { message: `${context}失败: 资源不存在`, status };
39
+ case 429:
40
+ return { message: `${context}失败: 请求过于频繁`, status };
41
+ case 500:
42
+ return { message: `${context}失败: 服务器内部错误`, status };
43
+ case 502:
44
+ return { message: `${context}失败: 网关错误`, status };
45
+ case 503:
46
+ return { message: `${context}失败: 服务不可用`, status };
47
+ default:
48
+ return {
49
+ message: `${context}失败: HTTP ${status} ${statusText}`,
50
+ status
51
+ };
52
+ }
53
+ }
54
+ // 其他类型的错误
55
+ return {
56
+ message: `${context}失败: ${error?.message || '未知错误'}`,
57
+ code: 'UNKNOWN_ERROR'
58
+ };
59
+ }
60
+ /**
61
+ * 记录简化的错误日志
62
+ * @param logger 日志记录器
63
+ * @param error 错误对象
64
+ * @param context 错误上下文
65
+ */
66
+ function logSimplifiedError(logger, error, context = '操作') {
67
+ const simplified = simplifyAxiosError(error, context);
68
+ if (simplified.status && simplified.status >= 500) {
69
+ // 服务器错误使用 error 级别
70
+ logger.error(simplified.message);
71
+ }
72
+ else if (simplified.status && simplified.status >= 400) {
73
+ // 客户端错误使用 warn 级别
74
+ logger.warn(simplified.message);
75
+ }
76
+ else {
77
+ // 网络错误等使用 error 级别
78
+ logger.error(simplified.message);
79
+ }
80
+ }
81
+ /**
82
+ * 安全的 axios 请求包装器
83
+ * @param requestFn axios 请求函数
84
+ * @param context 请求上下文描述
85
+ * @returns Promise<T | null>
86
+ */
87
+ async function safeAxiosRequest(requestFn, context = '请求') {
88
+ try {
89
+ return await requestFn();
90
+ }
91
+ catch (error) {
92
+ // 静默处理错误,返回 null
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * 带日志的 axios 请求包装器
98
+ * @param logger 日志记录器
99
+ * @param requestFn axios 请求函数
100
+ * @param context 请求上下文描述
101
+ * @returns Promise<T | null>
102
+ */
103
+ async function axiosRequestWithLog(logger, requestFn, context = '请求') {
104
+ try {
105
+ return await requestFn();
106
+ }
107
+ catch (error) {
108
+ logSimplifiedError(logger, error, context);
109
+ return null;
110
+ }
111
+ }
@@ -5,6 +5,7 @@ exports.getMxSpaceAggregateData = getMxSpaceAggregateData;
5
5
  const api_client_1 = require("@mx-space/api-client");
6
6
  const axios_1 = require("@mx-space/api-client/dist/adaptors/axios");
7
7
  const constants_1 = require("../constants");
8
+ const axios_error_1 = require("./axios-error");
8
9
  let apiClientInstance;
9
10
  function getApiClient(ctx, config) {
10
11
  if (apiClientInstance) {
@@ -27,12 +28,15 @@ function getApiClient(ctx, config) {
27
28
  return res;
28
29
  }, (err) => {
29
30
  const res = err.response;
30
- const error = Promise.reject(err);
31
31
  if (!res) {
32
- return error;
32
+ // 网络错误等,记录简化日志
33
+ (0, axios_error_1.logSimplifiedError)(logger, err, 'MX Space API 请求');
33
34
  }
34
- logger.error(`HTTP Response Failed ${`${res.config.baseURL || ''}${res.config.url}`}`);
35
- return error;
35
+ else {
36
+ // HTTP 错误,记录简化日志
37
+ (0, axios_error_1.logSimplifiedError)(logger, err, `MX Space API 请求 ${res.config.url}`);
38
+ }
39
+ return Promise.reject(err);
36
40
  });
37
41
  const apiClient = (0, api_client_1.createClient)(axios_1.axiosAdaptor)(config.baseUrl, {
38
42
  controllers: api_client_1.allControllers,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-imx",
3
3
  "description": "Mix-Space Bot for Koishi - 集成多种功能的聊天机器人插件",
4
- "version": "2.2.1",
4
+ "version": "2.2.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [