koishi-plugin-imx 2.2.0 → 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
  }
@@ -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'];
@@ -126,7 +127,17 @@ function setupWebhook(ctx, config, logger) {
126
127
  body: koaCtx.request.body
127
128
  });
128
129
  const body = koaCtx.request.body;
129
- const signature = koaCtx.request.headers['x-hub-signature-256'];
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'];
130
141
  // 检查请求体是否存在
131
142
  if (!body) {
132
143
  logger.warn('Webhook 请求体为空');
@@ -138,14 +149,33 @@ function setupWebhook(ctx, config, logger) {
138
149
  if (config.webhook?.secret && signature) {
139
150
  const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
140
151
  const payload = JSON.stringify(body);
141
- const hmac = crypto.createHmac('sha256', config.webhook.secret);
142
- hmac.update(payload);
143
- 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
+ }
144
170
  logger.debug('签名验证:', {
145
171
  received: signature,
146
- expected: expectedSignature
172
+ algorithm: signature.startsWith('sha256=') ? 'SHA256' : (headers['x-webhook-signature256'] ? 'SHA256' : 'SHA1'),
173
+ isValid: isValidSignature,
174
+ eventType,
175
+ webhookId,
176
+ timestamp
147
177
  });
148
- if (signature !== expectedSignature) {
178
+ if (!isValidSignature) {
149
179
  logger.warn('Webhook 签名验证失败');
150
180
  koaCtx.status = 401;
151
181
  koaCtx.body = { error: 'Invalid signature' };
@@ -159,26 +189,47 @@ function setupWebhook(ctx, config, logger) {
159
189
  return;
160
190
  }
161
191
  // 检查请求体格式
162
- if (!body.type || !body.data) {
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;
201
+ }
202
+ else if (body.type && body.data) {
203
+ // GitHub 格式:事件类型和数据都在请求体
204
+ eventTypeToProcess = body.type;
205
+ eventData = body.data;
206
+ }
207
+ else {
163
208
  logger.warn('Webhook 请求体格式错误:', body);
164
209
  koaCtx.status = 400;
165
210
  koaCtx.body = {
166
211
  error: 'Invalid webhook payload',
167
- details: 'Missing required fields: type or data',
168
- received: body
212
+ details: 'Missing required fields: event type or data',
213
+ received: body,
214
+ headers: { eventType, webhookId, timestamp }
169
215
  };
170
216
  return;
171
217
  }
172
- logger.info(`处理 MX Space 事件: ${body.type}`);
218
+ logger.info(`处理 MX Space 事件: ${eventTypeToProcess}`, {
219
+ webhookId,
220
+ timestamp,
221
+ format: eventType ? 'mx-space' : 'github'
222
+ });
173
223
  // 处理事件
174
- 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);
175
225
  koaCtx.status = 200;
176
226
  koaCtx.body = { message: 'Webhook processed successfully' };
177
227
  }
178
228
  catch (error) {
179
- logger.error('处理 MX Space webhook 失败:', error);
229
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '处理 MX Space webhook');
230
+ logger.error(simplified.message);
180
231
  koaCtx.status = 500;
181
- koaCtx.body = { error: 'Internal server error', details: error.message };
232
+ koaCtx.body = { error: 'Internal server error', details: simplified.message };
182
233
  }
183
234
  });
184
235
  logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath}`);
@@ -197,7 +248,8 @@ function setupWelcomeNewMember(ctx, config, logger) {
197
248
  await session.send(welcomeMessage);
198
249
  }
199
250
  catch (error) {
200
- logger.error('发送欢迎消息失败:', error);
251
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送欢迎消息');
252
+ logger.warn(simplified.message);
201
253
  }
202
254
  });
203
255
  logger.info('新成员欢迎功能已启用');
@@ -227,7 +279,8 @@ function setupCommentReply(ctx, config, logger, globalState) {
227
279
  globalState.memoChatId = null;
228
280
  }
229
281
  catch (error) {
230
- await session.send(`回复失败!${error.message || error}`);
282
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '回复评论');
283
+ await session.send(`回复失败!${simplified.message}`);
231
284
  globalState.toCommentId = null;
232
285
  globalState.memoChatId = null;
233
286
  }
@@ -252,7 +305,8 @@ function setupGreeting(ctx, config, logger) {
252
305
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
253
306
  }
254
307
  catch (error) {
255
- logger.error('发送早安消息失败:', error);
308
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送早安消息');
309
+ logger.warn(simplified.message);
256
310
  }
257
311
  }, null, false, 'Asia/Shanghai');
258
312
  // 晚安定时任务
@@ -271,7 +325,8 @@ function setupGreeting(ctx, config, logger) {
271
325
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
272
326
  }
273
327
  catch (error) {
274
- logger.error('发送晚安消息失败:', error);
328
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '发送晚安消息');
329
+ logger.warn(simplified.message);
275
330
  }
276
331
  }, null, false, 'Asia/Shanghai');
277
332
  morningJob.start();
@@ -296,7 +351,8 @@ function setupCommands(ctx, config, logger) {
296
351
  return `💭 ${hitokoto}\n\n—— ${from || '未知'}`;
297
352
  }
298
353
  catch (error) {
299
- logger.error('获取一言失败:', error);
354
+ const simplified = (0, axios_error_1.simplifyAxiosError)(error, '获取一言');
355
+ logger.warn(simplified.message);
300
356
  return '获取一言失败';
301
357
  }
302
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.0",
4
+ "version": "2.2.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [