koishi-plugin-imx 2.1.1 → 2.2.1

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.
@@ -11,6 +11,10 @@ export declare function isBot(username: string): boolean;
11
11
  * 用户代理字符串
12
12
  */
13
13
  export declare const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
14
+ /**
15
+ * MX Space API 专用用户代理(常见浏览器 UA)
16
+ */
17
+ export declare const mxSpaceUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
14
18
  /**
15
19
  * 开发环境检测
16
20
  */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isDev = exports.userAgent = exports.botList = void 0;
3
+ exports.isDev = exports.mxSpaceUserAgent = exports.userAgent = exports.botList = void 0;
4
4
  exports.isBot = isBot;
5
5
  /**
6
6
  * 常见的机器人账号列表
@@ -42,6 +42,10 @@ function isBot(username) {
42
42
  * 用户代理字符串
43
43
  */
44
44
  exports.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36';
45
+ /**
46
+ * MX Space API 专用用户代理(常见浏览器 UA)
47
+ */
48
+ exports.mxSpaceUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36';
45
49
  /**
46
50
  * 开发环境检测
47
51
  */
@@ -0,0 +1,8 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "webhook-debug";
3
+ export declare const inject: string[];
4
+ export interface Config {
5
+ path?: string;
6
+ }
7
+ export declare const Config: Schema<Config>;
8
+ export declare function apply(ctx: Context, config: Config): void;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Config = exports.inject = exports.name = void 0;
4
+ exports.apply = apply;
5
+ const koishi_1 = require("koishi");
6
+ exports.name = 'webhook-debug';
7
+ exports.inject = ['server'];
8
+ exports.Config = koishi_1.Schema.object({
9
+ path: koishi_1.Schema.string().description('调试 Webhook 路径').default('/debug/webhook'),
10
+ });
11
+ function apply(ctx, config) {
12
+ const logger = ctx.logger('webhook-debug');
13
+ if (!ctx.server) {
14
+ logger.error('server 插件未启用');
15
+ return;
16
+ }
17
+ const webhookPath = config.path || '/debug/webhook';
18
+ // 创建一个简单的调试 webhook 处理器
19
+ ctx.server.post(webhookPath, async (koaCtx) => {
20
+ logger.info('=== Webhook Debug Info ===');
21
+ logger.info('Method:', koaCtx.method);
22
+ logger.info('URL:', koaCtx.url);
23
+ logger.info('Headers:', JSON.stringify(koaCtx.headers, null, 2));
24
+ logger.info('Raw Body:', koaCtx.request.rawBody);
25
+ logger.info('Parsed Body:', JSON.stringify(koaCtx.request.body, null, 2));
26
+ logger.info('Body Type:', typeof koaCtx.request.body);
27
+ logger.info('Content-Type:', koaCtx.headers['content-type']);
28
+ logger.info('Content-Length:', koaCtx.headers['content-length']);
29
+ logger.info('========================');
30
+ // 检查各种可能的问题
31
+ const diagnostics = {
32
+ hasBody: !!koaCtx.request.body,
33
+ bodyType: typeof koaCtx.request.body,
34
+ isObject: typeof koaCtx.request.body === 'object',
35
+ hasType: !!(koaCtx.request.body && koaCtx.request.body.type),
36
+ hasData: !!(koaCtx.request.body && koaCtx.request.body.data),
37
+ contentType: koaCtx.headers['content-type'],
38
+ bodyKeys: koaCtx.request.body ? Object.keys(koaCtx.request.body) : [],
39
+ };
40
+ logger.info('Diagnostics:', JSON.stringify(diagnostics, null, 2));
41
+ koaCtx.status = 200;
42
+ koaCtx.body = {
43
+ message: 'Debug webhook received',
44
+ diagnostics,
45
+ receivedData: koaCtx.request.body
46
+ };
47
+ });
48
+ // 也处理 GET 请求用于简单测试
49
+ ctx.server.get(webhookPath, async (koaCtx) => {
50
+ koaCtx.status = 200;
51
+ koaCtx.body = {
52
+ message: 'Webhook debug endpoint is working',
53
+ method: 'GET',
54
+ timestamp: new Date().toISOString()
55
+ };
56
+ });
57
+ logger.info(`调试 Webhook 已启动,路径: ${webhookPath}`);
58
+ logger.info(`测试 GET: curl ${ctx.server.config.selfUrl || 'http://localhost:5140'}${webhookPath}`);
59
+ logger.info(`测试 POST: curl -X POST -H "Content-Type: application/json" -d '{"test": "data"}' ${ctx.server.config.selfUrl || 'http://localhost:5140'}${webhookPath}`);
60
+ }
@@ -8,6 +8,7 @@ export interface Config {
8
8
  secret?: string;
9
9
  path?: string;
10
10
  watchChannels?: string[];
11
+ requireSignature?: boolean;
11
12
  };
12
13
  greeting?: {
13
14
  enabled?: boolean;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -25,6 +58,7 @@ exports.Config = koishi_1.Schema.object({
25
58
  secret: koishi_1.Schema.string().description('MX Space Webhook Secret').role('secret'),
26
59
  path: koishi_1.Schema.string().description('Webhook 路径').default('/mx-space/webhook'),
27
60
  watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的频道ID列表').default([]),
61
+ requireSignature: koishi_1.Schema.boolean().description('是否要求签名验证(禁用可允许健康检查等请求)').default(true),
28
62
  }).description('Webhook 配置'),
29
63
  greeting: koishi_1.Schema.object({
30
64
  enabled: koishi_1.Schema.boolean().description('启用问候功能').default(true),
@@ -57,7 +91,7 @@ function apply(ctx, config) {
57
91
  memoChatId: null,
58
92
  };
59
93
  // 设置 Webhook 处理器
60
- if (config.webhook?.secret && ctx.server) {
94
+ if (config.webhook && ctx.server) {
61
95
  setupWebhook(ctx, config, logger);
62
96
  }
63
97
  // 设置问候功能
@@ -79,28 +113,98 @@ function apply(ctx, config) {
79
113
  logger.info('MX Space 模块已启动');
80
114
  }
81
115
  function setupWebhook(ctx, config, logger) {
82
- if (!config.webhook?.secret || !ctx.server)
116
+ if (!ctx.server) {
117
+ logger.warn('Server 插件未启用,无法设置 webhook');
83
118
  return;
84
- const webhookPath = config.webhook.path || '/mx-space/webhook';
119
+ }
120
+ const webhookPath = config.webhook?.path || '/mx-space/webhook';
85
121
  ctx.server.post(webhookPath, async (koaCtx) => {
86
122
  try {
123
+ logger.debug('收到 webhook 请求:', {
124
+ method: koaCtx.method,
125
+ url: koaCtx.url,
126
+ headers: koaCtx.headers,
127
+ body: koaCtx.request.body
128
+ });
87
129
  const body = koaCtx.request.body;
88
- const signature = koaCtx.request.headers['x-hub-signature-256'];
89
- // 简单的签名验证(生产环境应该使用更安全的验证方式)
130
+ const signature = koaCtx.request.headers['x-hub-signature-256'] ||
131
+ koaCtx.request.headers['x-webhook-signature256'];
132
+ // 检查请求体是否存在
133
+ if (!body) {
134
+ logger.warn('Webhook 请求体为空');
135
+ koaCtx.status = 400;
136
+ koaCtx.body = { error: 'Request body is empty' };
137
+ return;
138
+ }
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
+ }
153
+ const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
154
+ 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');
158
+ logger.debug('签名验证:', {
159
+ event: eventType,
160
+ received: signature,
161
+ expected: expectedSignature
162
+ });
163
+ if (signature !== expectedSignature) {
164
+ logger.warn(`事件 ${eventType} 签名验证失败`);
165
+ koaCtx.status = 401;
166
+ koaCtx.body = { error: 'Invalid signature' };
167
+ return;
168
+ }
169
+ }
170
+ else if (!requireSignature) {
171
+ logger.debug('签名验证已禁用,跳过签名检查');
172
+ }
173
+ else if (isSystemEvent) {
174
+ logger.debug(`系统事件 ${eventType} 跳过签名验证`);
175
+ }
176
+ // 检查标准请求体格式
90
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
+ }
185
+ logger.warn('Webhook 请求体格式错误:', body);
91
186
  koaCtx.status = 400;
187
+ koaCtx.body = {
188
+ error: 'Invalid webhook payload',
189
+ details: 'Missing required fields: type or data',
190
+ received: body
191
+ };
92
192
  return;
93
193
  }
194
+ logger.info(`处理 MX Space 事件: ${body.type}`);
94
195
  // 处理事件
95
196
  await (0, mx_event_handler_1.handleMxSpaceEvent)(ctx, config, body.type, body.data, logger);
96
197
  koaCtx.status = 200;
198
+ koaCtx.body = { message: 'Webhook processed successfully' };
97
199
  }
98
200
  catch (error) {
99
201
  logger.error('处理 MX Space webhook 失败:', error);
100
202
  koaCtx.status = 500;
203
+ koaCtx.body = { error: 'Internal server error', details: error.message };
101
204
  }
102
205
  });
103
- logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath}`);
206
+ const signatureStatus = config.webhook?.requireSignature !== false ? '已启用' : '已禁用';
207
+ logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath},签名验证: ${signatureStatus}`);
104
208
  }
105
209
  function setupWelcomeNewMember(ctx, config, logger) {
106
210
  if (!config.welcomeNewMember?.channels?.length)
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.escapeMarkdown = escapeMarkdown;
4
7
  exports.randomColor = randomColor;
5
8
  exports.truncateText = truncateText;
6
9
  exports.stripHtml = stripHtml;
7
10
  exports.getUserIdentifier = getUserIdentifier;
11
+ const sanitize_html_1 = __importDefault(require("sanitize-html"));
8
12
  /**
9
13
  * 转义 Markdown 特殊字符
10
14
  */
@@ -31,7 +35,8 @@ function truncateText(text, maxLength = 200) {
31
35
  * 清理 HTML 标签
32
36
  */
33
37
  function stripHtml(html) {
34
- return html.replace(/<[^>]*>/g, '');
38
+ // Remove all HTML tags and attributes using sanitize-html
39
+ return (0, sanitize_html_1.default)(html, { allowedTags: [], allowedAttributes: {} });
35
40
  }
36
41
  /**
37
42
  * 获取用户标识符
@@ -4,6 +4,7 @@ exports.getApiClient = getApiClient;
4
4
  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
+ const constants_1 = require("../constants");
7
8
  let apiClientInstance;
8
9
  function getApiClient(ctx, config) {
9
10
  if (apiClientInstance) {
@@ -17,6 +18,7 @@ function getApiClient(ctx, config) {
17
18
  req.headers = {
18
19
  ...req.headers,
19
20
  authorization: config.token,
21
+ 'user-agent': constants_1.mxSpaceUserAgent,
20
22
  'x-request-id': Math.random().toString(36).slice(2),
21
23
  };
22
24
  return req;
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.1.1",
4
+ "version": "2.2.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -32,7 +32,7 @@
32
32
  "koishi": {
33
33
  "description": {
34
34
  "en": "A versatile chatbot plugin for Koishi, integrating multiple functionalities including Bilibili and GitHub.",
35
- "zh": "一个多功能的聊天机器人插件,集成了 Bilibili GitHub 等多种功能。"
35
+ "zh": "一个多功能的聊天机器人插件,集成了 Mix-Space信息推送、Bilibili GitHub 等多种功能。"
36
36
  },
37
37
  "service": {
38
38
  "required": [
@@ -54,6 +54,7 @@
54
54
  "marked": "^5.1.2",
55
55
  "randomcolor": "^0.6.2",
56
56
  "remove-markdown": "^0.5.5",
57
+ "sanitize-html": "^2.17.0",
57
58
  "socket.io-client": "^4.8.1",
58
59
  "zod": "^3.25.76"
59
60
  },
@@ -62,6 +63,7 @@
62
63
  "@types/node": "^22.16.5",
63
64
  "@types/randomcolor": "^0.5.9",
64
65
  "@types/remove-markdown": "^0.3.4",
66
+ "@types/sanitize-html": "^2.16.0",
65
67
  "koishi": "^4.18.8",
66
68
  "typescript": "^5.8.3"
67
69
  },