koishi-plugin-imx 2.0.0 → 2.1.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.
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "imx";
3
+ export declare const inject: string[];
3
4
  export interface Config {
4
5
  mxSpace?: {
5
6
  baseUrl?: string;
@@ -19,6 +20,14 @@ export interface Config {
19
20
  enabled?: boolean;
20
21
  replyPrefix?: string;
21
22
  };
23
+ welcomeNewMember?: {
24
+ enabled?: boolean;
25
+ channels?: string[];
26
+ };
27
+ commentReply?: {
28
+ enabled?: boolean;
29
+ channels?: string[];
30
+ };
22
31
  };
23
32
  bilibili?: {
24
33
  enabled?: boolean;
package/lib/index.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Config = exports.name = void 0;
36
+ exports.Config = exports.inject = exports.name = void 0;
37
37
  exports.apply = apply;
38
38
  const koishi_1 = require("koishi");
39
39
  const mxSpace = __importStar(require("./modules/mx-space"));
@@ -41,6 +41,7 @@ const bilibili = __importStar(require("./modules/bilibili"));
41
41
  const github = __importStar(require("./modules/github"));
42
42
  const shared = __importStar(require("./shared"));
43
43
  exports.name = 'imx';
44
+ exports.inject = ['server'];
44
45
  exports.Config = koishi_1.Schema.object({
45
46
  mxSpace: koishi_1.Schema.object({
46
47
  baseUrl: koishi_1.Schema.string().description('MX Space API 地址').required(),
@@ -60,6 +61,14 @@ exports.Config = koishi_1.Schema.object({
60
61
  enabled: koishi_1.Schema.boolean().description('启用命令功能').default(true),
61
62
  replyPrefix: koishi_1.Schema.string().description('回复前缀').default('来自 Mix Space 的'),
62
63
  }).description('命令功能配置'),
64
+ welcomeNewMember: koishi_1.Schema.object({
65
+ enabled: koishi_1.Schema.boolean().description('启用新成员欢迎功能').default(false),
66
+ channels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的群组ID列表').default([]),
67
+ }).description('新成员欢迎配置'),
68
+ commentReply: koishi_1.Schema.object({
69
+ enabled: koishi_1.Schema.boolean().description('启用评论回复功能').default(false),
70
+ channels: koishi_1.Schema.array(koishi_1.Schema.string()).description('允许回复评论的频道ID列表').default([]),
71
+ }).description('评论回复配置'),
63
72
  }).description('MX Space 配置'),
64
73
  bilibili: koishi_1.Schema.object({
65
74
  enabled: koishi_1.Schema.boolean().description('启用 Bilibili 功能').default(false),
@@ -124,5 +133,8 @@ function apply(ctx, config) {
124
133
  ctx.plugin(shared, config.shared);
125
134
  logger.info('共享功能模块已加载');
126
135
  }
127
- logger.info('IMX 插件加载完成');
136
+ else {
137
+ logger.debug('共享功能未配置');
138
+ }
139
+ logger.info('IMX 插件启动完成');
128
140
  }
@@ -1,8 +1,14 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "mx-space";
3
+ export declare const inject: string[];
3
4
  export interface Config {
4
5
  baseUrl?: string;
5
6
  token?: string;
7
+ webhook?: {
8
+ secret?: string;
9
+ path?: string;
10
+ watchChannels?: string[];
11
+ };
6
12
  greeting?: {
7
13
  enabled?: boolean;
8
14
  channels?: string[];
@@ -13,6 +19,14 @@ export interface Config {
13
19
  enabled?: boolean;
14
20
  replyPrefix?: string;
15
21
  };
22
+ welcomeNewMember?: {
23
+ enabled?: boolean;
24
+ channels?: string[];
25
+ };
26
+ commentReply?: {
27
+ enabled?: boolean;
28
+ channels?: string[];
29
+ };
16
30
  }
17
31
  export declare const Config: Schema<Config>;
18
32
  export declare function apply(ctx: Context, config: Config): void;
@@ -1,14 +1,31 @@
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
- exports.Config = exports.name = void 0;
6
+ exports.Config = exports.inject = exports.name = void 0;
4
7
  exports.apply = apply;
5
8
  const koishi_1 = require("koishi");
6
9
  const cron_1 = require("cron");
10
+ const lodash_1 = require("lodash");
11
+ const dayjs_1 = __importDefault(require("dayjs"));
12
+ const relativeTime_1 = __importDefault(require("dayjs/plugin/relativeTime"));
13
+ const remove_markdown_1 = __importDefault(require("remove-markdown"));
7
14
  const hitokoto_1 = require("../utils/hitokoto");
15
+ const mx_api_1 = require("../utils/mx-api");
16
+ const mx_url_builder_1 = require("../utils/mx-url-builder");
17
+ const mx_event_handler_1 = require("../utils/mx-event-handler");
18
+ dayjs_1.default.extend(relativeTime_1.default);
8
19
  exports.name = 'mx-space';
20
+ exports.inject = ['server'];
9
21
  exports.Config = koishi_1.Schema.object({
10
22
  baseUrl: koishi_1.Schema.string().description('MX Space API 地址').required(),
11
23
  token: koishi_1.Schema.string().description('MX Space API Token').role('secret'),
24
+ webhook: koishi_1.Schema.object({
25
+ secret: koishi_1.Schema.string().description('MX Space Webhook Secret').role('secret'),
26
+ path: koishi_1.Schema.string().description('Webhook 路径').default('/mx-space/webhook'),
27
+ watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的频道ID列表').default([]),
28
+ }).description('Webhook 配置'),
12
29
  greeting: koishi_1.Schema.object({
13
30
  enabled: koishi_1.Schema.boolean().description('启用问候功能').default(true),
14
31
  channels: koishi_1.Schema.array(koishi_1.Schema.string()).description('问候消息发送的频道').default([]),
@@ -19,6 +36,14 @@ exports.Config = koishi_1.Schema.object({
19
36
  enabled: koishi_1.Schema.boolean().description('启用命令功能').default(true),
20
37
  replyPrefix: koishi_1.Schema.string().description('回复前缀').default('来自 Mix Space 的'),
21
38
  }).description('命令功能配置'),
39
+ welcomeNewMember: koishi_1.Schema.object({
40
+ enabled: koishi_1.Schema.boolean().description('启用新成员欢迎功能').default(false),
41
+ channels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的群组ID列表').default([]),
42
+ }).description('新成员欢迎配置'),
43
+ commentReply: koishi_1.Schema.object({
44
+ enabled: koishi_1.Schema.boolean().description('启用评论回复功能').default(false),
45
+ channels: koishi_1.Schema.array(koishi_1.Schema.string()).description('允许回复评论的频道ID列表').default([]),
46
+ }).description('评论回复配置'),
22
47
  });
23
48
  function apply(ctx, config) {
24
49
  const logger = ctx.logger('mx-space');
@@ -26,6 +51,15 @@ function apply(ctx, config) {
26
51
  logger.warn('MX Space baseUrl not configured');
27
52
  return;
28
53
  }
54
+ // 全局状态存储
55
+ const globalState = {
56
+ toCommentId: null,
57
+ memoChatId: null,
58
+ };
59
+ // 设置 Webhook 处理器
60
+ if (config.webhook?.secret && ctx.server) {
61
+ setupWebhook(ctx, config, logger);
62
+ }
29
63
  // 设置问候功能
30
64
  if (config.greeting?.enabled) {
31
65
  setupGreeting(ctx, config, logger);
@@ -34,8 +68,92 @@ function apply(ctx, config) {
34
68
  if (config.commands?.enabled) {
35
69
  setupCommands(ctx, config, logger);
36
70
  }
71
+ // 设置新成员欢迎
72
+ if (config.welcomeNewMember?.enabled) {
73
+ setupWelcomeNewMember(ctx, config, logger);
74
+ }
75
+ // 设置评论回复功能
76
+ if (config.commentReply?.enabled) {
77
+ setupCommentReply(ctx, config, logger, globalState);
78
+ }
37
79
  logger.info('MX Space 模块已启动');
38
80
  }
81
+ function setupWebhook(ctx, config, logger) {
82
+ if (!config.webhook?.secret || !ctx.server)
83
+ return;
84
+ const webhookPath = config.webhook.path || '/mx-space/webhook';
85
+ ctx.server.post(webhookPath, async (koaCtx) => {
86
+ try {
87
+ const body = koaCtx.request.body;
88
+ const signature = koaCtx.request.headers['x-hub-signature-256'];
89
+ // 简单的签名验证(生产环境应该使用更安全的验证方式)
90
+ if (!body.type || !body.data) {
91
+ koaCtx.status = 400;
92
+ return;
93
+ }
94
+ // 处理事件
95
+ await (0, mx_event_handler_1.handleMxSpaceEvent)(ctx, config, body.type, body.data, logger);
96
+ koaCtx.status = 200;
97
+ }
98
+ catch (error) {
99
+ logger.error('处理 MX Space webhook 失败:', error);
100
+ koaCtx.status = 500;
101
+ }
102
+ });
103
+ logger.info(`MX Space Webhook 已启动,监听路径: ${webhookPath}`);
104
+ }
105
+ function setupWelcomeNewMember(ctx, config, logger) {
106
+ if (!config.welcomeNewMember?.channels?.length)
107
+ return;
108
+ ctx.on('guild-member-added', async (session) => {
109
+ const channelId = session.channelId;
110
+ if (!channelId || !config.welcomeNewMember?.channels?.includes(channelId))
111
+ return;
112
+ try {
113
+ const { hitokoto } = await (0, hitokoto_1.fetchHitokoto)();
114
+ const username = session.username || session.userId;
115
+ const welcomeMessage = `欢迎新成员 ${username}!\n\n${hitokoto || ''}`;
116
+ await session.send(welcomeMessage);
117
+ }
118
+ catch (error) {
119
+ logger.error('发送欢迎消息失败:', error);
120
+ }
121
+ });
122
+ logger.info('新成员欢迎功能已启用');
123
+ }
124
+ function setupCommentReply(ctx, config, logger, globalState) {
125
+ if (!config.commentReply?.channels?.length)
126
+ return;
127
+ // 处理回复消息的中间件
128
+ ctx.middleware(async (session, next) => {
129
+ if (session.type !== 'message' || !session.content)
130
+ return next();
131
+ const channelId = session.channelId;
132
+ if (!channelId || !config.commentReply?.channels?.includes(channelId))
133
+ return next();
134
+ if (channelId !== globalState.memoChatId || !globalState.toCommentId)
135
+ return next();
136
+ try {
137
+ const apiClient = (0, mx_api_1.getApiClient)(ctx, config);
138
+ await apiClient.comment.proxy.master
139
+ .reply(globalState.toCommentId)
140
+ .post({
141
+ data: { text: session.content }
142
+ });
143
+ await session.send('回复成功!');
144
+ // 清除状态
145
+ globalState.toCommentId = null;
146
+ globalState.memoChatId = null;
147
+ }
148
+ catch (error) {
149
+ await session.send(`回复失败!${error.message || error}`);
150
+ globalState.toCommentId = null;
151
+ globalState.memoChatId = null;
152
+ }
153
+ return;
154
+ });
155
+ logger.info('评论回复功能已启用');
156
+ }
39
157
  function setupGreeting(ctx, config, logger) {
40
158
  // 早安定时任务
41
159
  const morningJob = new cron_1.CronJob(config.greeting.morningTime || '0 0 6 * * *', async () => {
@@ -48,7 +166,7 @@ function setupGreeting(ctx, config, logger) {
48
166
  '早上好!愿你今天心情美丽',
49
167
  '新的一天开始了,加油!',
50
168
  ];
51
- const greeting = greetings[Math.floor(Math.random() * greetings.length)];
169
+ const greeting = (0, lodash_1.sample)(greetings) || greetings[0];
52
170
  const message = `🌅 早上好!${greeting}\n\n${hitokoto || ''}`;
53
171
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
54
172
  }
@@ -67,7 +185,7 @@ function setupGreeting(ctx, config, logger) {
67
185
  '睡个好觉,明天会更好',
68
186
  '夜深了,注意休息哦',
69
187
  ];
70
- const greeting = greetings[Math.floor(Math.random() * greetings.length)];
188
+ const greeting = (0, lodash_1.sample)(greetings) || greetings[0];
71
189
  const message = `🌙 ${greeting}\n\n${hitokoto || ''}`;
72
190
  await sendToChannels(ctx, config.greeting.channels || [], message, logger);
73
191
  }
@@ -86,27 +204,155 @@ function setupGreeting(ctx, config, logger) {
86
204
  logger.info('问候功能已启动');
87
205
  }
88
206
  function setupCommands(ctx, config, logger) {
207
+ const apiClient = (0, mx_api_1.getApiClient)(ctx, config);
208
+ const cmd = ctx.command('mx-space', 'MX Space 相关功能');
89
209
  // 一言命令
90
- ctx.command('hitokoto', '获取一言')
210
+ cmd
211
+ .subcommand('.hitokoto', '获取一言')
91
212
  .action(async ({ session }) => {
92
213
  try {
93
214
  const { hitokoto, from } = await (0, hitokoto_1.fetchHitokoto)();
94
- return `💭 ${hitokoto}\n\n—— ${from}`;
215
+ return `💭 ${hitokoto}\n\n—— ${from || '未知'}`;
95
216
  }
96
217
  catch (error) {
97
218
  logger.error('获取一言失败:', error);
98
219
  return '获取一言失败';
99
220
  }
100
221
  });
222
+ // 统计信息命令
223
+ cmd
224
+ .subcommand('.stat', '获取 MX Space 统计信息')
225
+ .action(async ({ session }) => {
226
+ try {
227
+ const data = await apiClient.aggregate.getStat();
228
+ const { posts, notes, comments, links, says, recently, todayIpAccessCount, todayMaxOnline, todayOnlineTotal, unreadComments, linkApply, callTime, online } = data;
229
+ const replyPrefix = config.commands?.replyPrefix || '来自 Mix Space 的';
230
+ return `📊 ${replyPrefix}统计信息:\n\n` +
231
+ `📝 文章 ${posts} 篇,📔 记录 ${notes} 篇\n` +
232
+ `💬 评论 ${comments} 条,🔗 友链 ${links} 条\n` +
233
+ `💭 说说 ${says} 条,⚡ 速记 ${recently} 条\n\n` +
234
+ `🔔 未读评论 ${unreadComments} 条,📮 友链申请 ${linkApply} 条\n` +
235
+ `📈 今日访问 ${todayIpAccessCount} 次,👥 最高在线 ${todayMaxOnline} 人\n` +
236
+ `📊 总计在线 ${todayOnlineTotal} 人,🔄 调用 ${callTime} 次\n` +
237
+ `🟢 当前在线 ${online} 人`;
238
+ }
239
+ catch (error) {
240
+ logger.error('获取统计信息失败:', error);
241
+ return '获取统计信息失败';
242
+ }
243
+ });
244
+ // 获取最新文章
245
+ cmd
246
+ .subcommand('.posts [page:number]', '获取最新的文章列表')
247
+ .action(async ({ session }, page = 1) => {
248
+ try {
249
+ const data = await apiClient.post.getList(page, 10);
250
+ if (!data.data.length) {
251
+ return '没有找到文章';
252
+ }
253
+ const aggregateData = await (0, mx_api_1.getMxSpaceAggregateData)(ctx, config);
254
+ const webUrl = aggregateData.url.webUrl;
255
+ const articles = data.data.map((post) => {
256
+ const timeAgo = (0, dayjs_1.default)(post.created).fromNow();
257
+ const url = `${webUrl}/posts/${post.category.slug}/${post.slug}`;
258
+ return `${timeAgo} · [${post.title}](${url})`;
259
+ }).join('\n');
260
+ const replyPrefix = config.commands?.replyPrefix || '来自 Mix Space 的';
261
+ return `📚 ${replyPrefix}最新文章:\n\n${articles}`;
262
+ }
263
+ catch (error) {
264
+ logger.error('获取文章列表失败:', error);
265
+ return '获取文章列表失败';
266
+ }
267
+ });
268
+ // 获取最新日记
269
+ cmd
270
+ .subcommand('.notes [page:number]', '获取最新的日记列表')
271
+ .action(async ({ session }, page = 1) => {
272
+ try {
273
+ const data = await apiClient.note.getList(page, 10);
274
+ if (!data.data.length) {
275
+ return '没有找到日记';
276
+ }
277
+ const aggregateData = await (0, mx_api_1.getMxSpaceAggregateData)(ctx, config);
278
+ const webUrl = aggregateData.url.webUrl;
279
+ const notes = data.data.map((note) => {
280
+ const timeAgo = (0, dayjs_1.default)(note.created).fromNow();
281
+ const url = `${webUrl}/notes/${note.nid}`;
282
+ return `${timeAgo} · [${note.title}](${url})`;
283
+ }).join('\n');
284
+ const replyPrefix = config.commands?.replyPrefix || '来自 Mix Space 的';
285
+ return `📔 ${replyPrefix}最新日记:\n\n${notes}`;
286
+ }
287
+ catch (error) {
288
+ logger.error('获取日记列表失败:', error);
289
+ return '获取日记列表失败';
290
+ }
291
+ });
292
+ // 获取详情命令
293
+ cmd
294
+ .subcommand('.detail <type> [offset:number]', '获取文章或日记详情')
295
+ .action(async ({ session }, type, offset = 1) => {
296
+ if (!['post', 'note'].includes(type)) {
297
+ return '类型必须是 post 或 note';
298
+ }
299
+ try {
300
+ const replyPrefix = config.commands?.replyPrefix || '来自 Mix Space 的';
301
+ if (type === 'post') {
302
+ const data = await apiClient.post.getList(offset, 1);
303
+ if (!data.data.length) {
304
+ return '没有找到文章';
305
+ }
306
+ const post = data.data[0];
307
+ const url = await mx_url_builder_1.urlBuilder.build(ctx, config, post);
308
+ const preview = (0, remove_markdown_1.default)(post.text)
309
+ .split('\n\n')
310
+ .slice(0, 3)
311
+ .join('\n\n')
312
+ .substring(0, 200);
313
+ return `📚 ${replyPrefix}文章详情:\n\n` +
314
+ `📝 ${post.title}\n\n` +
315
+ `${preview}${preview.length >= 200 ? '...' : ''}\n\n` +
316
+ `🔗 [阅读全文](${url})`;
317
+ }
318
+ else {
319
+ const data = await apiClient.note.getList(offset, 1);
320
+ if (!data.data.length) {
321
+ return '没有找到日记';
322
+ }
323
+ const note = data.data[0];
324
+ const url = await mx_url_builder_1.urlBuilder.build(ctx, config, note);
325
+ const preview = (0, remove_markdown_1.default)(note.text)
326
+ .split('\n\n')
327
+ .slice(0, 3)
328
+ .join('\n\n')
329
+ .substring(0, 200);
330
+ return `📔 ${replyPrefix}日记详情:\n\n` +
331
+ `📝 ${note.title}\n\n` +
332
+ `${preview}${preview.length >= 200 ? '...' : ''}\n\n` +
333
+ `🔗 [阅读全文](${url})`;
334
+ }
335
+ }
336
+ catch (error) {
337
+ logger.error('获取详情失败:', error);
338
+ return '获取详情失败';
339
+ }
340
+ });
101
341
  logger.info('MX Space 命令已注册');
102
342
  }
103
343
  async function sendToChannels(ctx, channels, message, logger) {
104
- for (const channelId of channels) {
344
+ if (!channels.length)
345
+ return;
346
+ const tasks = channels.map(async (channelId) => {
105
347
  try {
106
- await ctx.broadcast([channelId], message);
348
+ const bot = ctx.bots.find(bot => bot.selfId);
349
+ if (bot) {
350
+ await bot.sendMessage(channelId, message);
351
+ }
107
352
  }
108
353
  catch (error) {
109
354
  logger.error(`发送消息到频道 ${channelId} 失败:`, error);
110
355
  }
111
- }
356
+ });
357
+ await Promise.allSettled(tasks);
112
358
  }
@@ -0,0 +1,2 @@
1
+ export type { CategoryModel, NoteModel, PageModel, PostModel, CommentModel, LinkModel, RecentlyModel, SayModel, } from '@mx-space/api-client';
2
+ export { CollectionRefTypes, LinkState } from '@mx-space/api-client';
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LinkState = exports.CollectionRefTypes = void 0;
4
+ var api_client_1 = require("@mx-space/api-client");
5
+ Object.defineProperty(exports, "CollectionRefTypes", { enumerable: true, get: function () { return api_client_1.CollectionRefTypes; } });
6
+ Object.defineProperty(exports, "LinkState", { enumerable: true, get: function () { return api_client_1.LinkState; } });
@@ -9,8 +9,8 @@ exports.getUserIdentifier = getUserIdentifier;
9
9
  * 转义 Markdown 特殊字符
10
10
  */
11
11
  function escapeMarkdown(text) {
12
- // Escape markdown special characters for MarkdownV2
13
- return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
12
+ // Escape backslashes first, then markdown special characters for MarkdownV2
13
+ return text.replace(/\\/g, '\\\\').replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
14
14
  }
15
15
  /**
16
16
  * 生成随机颜色
@@ -0,0 +1,9 @@
1
+ import { Context } from 'koishi';
2
+ export interface Config {
3
+ baseUrl?: string;
4
+ token?: string;
5
+ webhookSecret?: string;
6
+ watchGroupIds?: string[];
7
+ }
8
+ export declare function getApiClient(ctx: Context, config: Config): any;
9
+ export declare function getMxSpaceAggregateData(ctx: Context, config: Config): Promise<any>;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getApiClient = getApiClient;
4
+ exports.getMxSpaceAggregateData = getMxSpaceAggregateData;
5
+ const api_client_1 = require("@mx-space/api-client");
6
+ const axios_1 = require("@mx-space/api-client/dist/adaptors/axios");
7
+ let apiClientInstance;
8
+ function getApiClient(ctx, config) {
9
+ if (apiClientInstance) {
10
+ return apiClientInstance;
11
+ }
12
+ if (!config.baseUrl) {
13
+ throw new Error('MX Space baseUrl is required');
14
+ }
15
+ const logger = ctx.logger('mx-space-api');
16
+ axios_1.axiosAdaptor.default.interceptors.request.use((req) => {
17
+ req.headers = {
18
+ ...req.headers,
19
+ authorization: config.token,
20
+ 'x-request-id': Math.random().toString(36).slice(2),
21
+ };
22
+ return req;
23
+ });
24
+ axios_1.axiosAdaptor.default.interceptors.response.use((res) => {
25
+ return res;
26
+ }, (err) => {
27
+ const res = err.response;
28
+ const error = Promise.reject(err);
29
+ if (!res) {
30
+ return error;
31
+ }
32
+ logger.error(`HTTP Response Failed ${`${res.config.baseURL || ''}${res.config.url}`}`);
33
+ return error;
34
+ });
35
+ const apiClient = (0, api_client_1.createClient)(axios_1.axiosAdaptor)(config.baseUrl, {
36
+ controllers: api_client_1.allControllers,
37
+ });
38
+ apiClientInstance = apiClient;
39
+ return apiClient;
40
+ }
41
+ let aggregateDataCache;
42
+ let cacheTime;
43
+ async function getMxSpaceAggregateData(ctx, config) {
44
+ const now = Date.now();
45
+ if (aggregateDataCache && cacheTime && now - cacheTime < 1000 * 60 * 5) {
46
+ return aggregateDataCache;
47
+ }
48
+ const apiClient = getApiClient(ctx, config);
49
+ const data = await apiClient.aggregate.getAggregateData();
50
+ aggregateDataCache = data;
51
+ cacheTime = now;
52
+ return data;
53
+ }
@@ -0,0 +1,11 @@
1
+ import { Context } from 'koishi';
2
+ export declare enum BusinessEvents {
3
+ POST_CREATE = "post_create",
4
+ POST_UPDATE = "post_update",
5
+ NOTE_CREATE = "note_create",
6
+ COMMENT_CREATE = "comment_create",
7
+ LINK_APPLY = "link_apply",
8
+ SAY_CREATE = "say_create",
9
+ RECENTLY_CREATE = "recently_create"
10
+ }
11
+ export declare function handleMxSpaceEvent(ctx: Context, config: any, type: string, payload: any, logger: any): Promise<void>;
@@ -0,0 +1,200 @@
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.BusinessEvents = void 0;
7
+ exports.handleMxSpaceEvent = handleMxSpaceEvent;
8
+ const koishi_1 = require("koishi");
9
+ const dayjs_1 = __importDefault(require("dayjs"));
10
+ const remove_markdown_1 = __importDefault(require("remove-markdown"));
11
+ const api_client_1 = require("@mx-space/api-client");
12
+ const mx_api_1 = require("./mx-api");
13
+ const mx_url_builder_1 = require("./mx-url-builder");
14
+ // MX Space 事件类型
15
+ var BusinessEvents;
16
+ (function (BusinessEvents) {
17
+ BusinessEvents["POST_CREATE"] = "post_create";
18
+ BusinessEvents["POST_UPDATE"] = "post_update";
19
+ BusinessEvents["NOTE_CREATE"] = "note_create";
20
+ BusinessEvents["COMMENT_CREATE"] = "comment_create";
21
+ BusinessEvents["LINK_APPLY"] = "link_apply";
22
+ BusinessEvents["SAY_CREATE"] = "say_create";
23
+ BusinessEvents["RECENTLY_CREATE"] = "recently_create";
24
+ })(BusinessEvents || (exports.BusinessEvents = BusinessEvents = {}));
25
+ async function handleMxSpaceEvent(ctx, config, type, payload, logger) {
26
+ logger.info(`处理 MX Space 事件: ${type}`);
27
+ try {
28
+ const aggregateData = await (0, mx_api_1.getMxSpaceAggregateData)(ctx, config);
29
+ const owner = aggregateData.user;
30
+ const watchChannels = config.webhook?.watchChannels || [];
31
+ if (!watchChannels.length) {
32
+ logger.warn('没有配置监听频道,跳过事件处理');
33
+ return;
34
+ }
35
+ const sendToChannels = async (message) => {
36
+ const tasks = watchChannels.map(async (channelId) => {
37
+ try {
38
+ const bot = ctx.bots.find(bot => bot.selfId);
39
+ if (bot) {
40
+ await bot.sendMessage(channelId, message);
41
+ }
42
+ }
43
+ catch (error) {
44
+ logger.error(`发送消息到频道 ${channelId} 失败:`, error);
45
+ }
46
+ });
47
+ await Promise.allSettled(tasks);
48
+ };
49
+ switch (type) {
50
+ case BusinessEvents.POST_CREATE:
51
+ case BusinessEvents.POST_UPDATE: {
52
+ const isNew = type === BusinessEvents.POST_CREATE;
53
+ const publishDescription = isNew ? '发布了新文章' : '更新了文章';
54
+ const { title, category, id, summary, created } = payload;
55
+ if (type === BusinessEvents.POST_UPDATE) {
56
+ // 只有创建90天内的文章更新才发送通知
57
+ const createdDate = (0, dayjs_1.default)(created);
58
+ const now = (0, dayjs_1.default)();
59
+ const diff = now.diff(createdDate, 'day');
60
+ if (diff >= 90) {
61
+ return;
62
+ }
63
+ }
64
+ if (!category) {
65
+ logger.error(`category not found, post id: ${id}`);
66
+ return;
67
+ }
68
+ const url = await mx_url_builder_1.urlBuilder.build(ctx, config, payload);
69
+ const message = `📚 ${owner.name} ${publishDescription}: ${title}\n\n${summary ? `${summary}\n\n` : ''}🔗 前往阅读:${url}`;
70
+ await sendToChannels(message);
71
+ return;
72
+ }
73
+ case BusinessEvents.NOTE_CREATE: {
74
+ const publishDescription = '发布了新的日记';
75
+ const { title, text, mood, weather, images, hide, password } = payload;
76
+ // 检查是否为隐私内容
77
+ const isSecret = checkNoteIsSecret(payload);
78
+ if (hide || password || isSecret) {
79
+ return;
80
+ }
81
+ const simplePreview = getSimplePreview(text);
82
+ const status = [mood ? `心情: ${mood}` : '', weather ? `天气: ${weather}` : '']
83
+ .filter(Boolean)
84
+ .join('\t');
85
+ const url = await mx_url_builder_1.urlBuilder.build(ctx, config, payload);
86
+ let message = `📔 ${owner.name} ${publishDescription}: ${title}\n${status ? `\n${status}\n\n` : '\n'}${simplePreview}\n\n🔗 前往阅读:${url}`;
87
+ // 如果有图片,发送图片消息
88
+ if (Array.isArray(images) && images.length > 0) {
89
+ const imageMessages = images.map(img => koishi_1.h.image(img.src));
90
+ await sendToChannels([koishi_1.h.text(message), ...imageMessages]);
91
+ }
92
+ else {
93
+ await sendToChannels(message);
94
+ }
95
+ return;
96
+ }
97
+ case BusinessEvents.LINK_APPLY: {
98
+ const { avatar, name, url, description, state } = payload;
99
+ if (state !== api_client_1.LinkState.Audit) {
100
+ return;
101
+ }
102
+ let message = `🔗 有新的友链申请!\n\n` +
103
+ `📝 名称: ${name}\n` +
104
+ `🌐 链接: ${url}\n` +
105
+ `📄 描述: ${description}`;
106
+ if (avatar) {
107
+ await sendToChannels([koishi_1.h.image(avatar), koishi_1.h.text(message)]);
108
+ }
109
+ else {
110
+ await sendToChannels(message);
111
+ }
112
+ return;
113
+ }
114
+ case BusinessEvents.COMMENT_CREATE: {
115
+ const { author, text, refType, parent, id, isWhispers } = payload;
116
+ const siteTitle = aggregateData.seo.title;
117
+ if (isWhispers) {
118
+ await sendToChannels(`🤫 「${siteTitle}」嘘,有人说了一句悄悄话...`);
119
+ return;
120
+ }
121
+ // 检查父评论是否为悄悄话
122
+ const parentIsWhispers = (() => {
123
+ const walk = (parent) => {
124
+ if (!parent || typeof parent === 'string') {
125
+ return false;
126
+ }
127
+ return parent.isWhispers || walk(parent?.parent);
128
+ };
129
+ return walk(parent);
130
+ })();
131
+ if (parentIsWhispers) {
132
+ logger.warn('[comment]: parent comment is whispers, ignore');
133
+ return;
134
+ }
135
+ const refId = payload.ref?.id || payload.ref?._id || payload.ref;
136
+ let refModel = null;
137
+ try {
138
+ const apiClient = (0, mx_api_1.getApiClient)(ctx, config);
139
+ switch (refType) {
140
+ case api_client_1.CollectionRefTypes.Post: {
141
+ refModel = await apiClient.post.getPost(refId);
142
+ break;
143
+ }
144
+ case api_client_1.CollectionRefTypes.Note: {
145
+ refModel = await apiClient.note.getNoteById(refId);
146
+ break;
147
+ }
148
+ case api_client_1.CollectionRefTypes.Page: {
149
+ refModel = await apiClient.page.getById(refId);
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ catch (error) {
155
+ logger.error(`[comment]: 获取引用内容失败, refId: ${refId}`, error);
156
+ return;
157
+ }
158
+ if (!refModel) {
159
+ logger.error(`[comment]: ref model not found, refId: ${refId}`);
160
+ return;
161
+ }
162
+ const isMaster = author === owner.name || author === owner.username;
163
+ let message;
164
+ if (isMaster && !parent) {
165
+ const timeAgo = (0, dayjs_1.default)(refModel.created).fromNow();
166
+ message = `💬 ${author} 在「${refModel.title}」发表之后的 ${timeAgo}又说:\n\n${text}`;
167
+ }
168
+ else {
169
+ message = `💬 ${author} 在「${refModel.title}」发表了评论:\n\n${text}`;
170
+ }
171
+ await sendToChannels(message);
172
+ return;
173
+ }
174
+ default: {
175
+ logger.info(`未处理的事件类型: ${type}`);
176
+ }
177
+ }
178
+ }
179
+ catch (error) {
180
+ logger.error('处理 MX Space 事件失败:', error);
181
+ }
182
+ }
183
+ function checkNoteIsSecret(note) {
184
+ // 检查是否包含敏感关键词
185
+ const sensitiveKeywords = ['密码', '私密', '秘密', '不公开'];
186
+ const text = note.text?.toLowerCase() || '';
187
+ const title = note.title?.toLowerCase() || '';
188
+ return sensitiveKeywords.some(keyword => text.includes(keyword) || title.includes(keyword));
189
+ }
190
+ function getSimplePreview(text) {
191
+ if (!text)
192
+ return '';
193
+ const cleaned = (0, remove_markdown_1.default)(text);
194
+ const preview = cleaned
195
+ .split('\n\n')
196
+ .slice(0, 3)
197
+ .join('\n\n')
198
+ .substring(0, 200);
199
+ return preview + (preview.length >= 200 ? '...' : '');
200
+ }
@@ -0,0 +1,8 @@
1
+ import { Context } from 'koishi';
2
+ import { NoteModel, PageModel, PostModel } from '../types/mx-space/api';
3
+ import { Config } from './mx-api';
4
+ declare function buildPath(model: PostModel | NoteModel | PageModel): string;
5
+ export declare const urlBuilder: {
6
+ build: (ctx: Context, config: Config, model: Parameters<typeof buildPath>[0]) => Promise<string>;
7
+ };
8
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.urlBuilder = void 0;
4
+ const mx_api_1 = require("./mx-api");
5
+ async function buildUrlBase(ctx, config, path = '') {
6
+ const aggregate = await (0, mx_api_1.getMxSpaceAggregateData)(ctx, config);
7
+ return new URL(path, aggregate?.url.webUrl).toString();
8
+ }
9
+ function isPostModel(model) {
10
+ return (isDefined(model.title) && isDefined(model.slug) && !isDefined(model.order));
11
+ }
12
+ function isPageModel(model) {
13
+ return (isDefined(model.title) && isDefined(model.slug) && isDefined(model.order));
14
+ }
15
+ function isNoteModel(model) {
16
+ return isDefined(model.title) && isDefined(model.nid);
17
+ }
18
+ function buildPath(model) {
19
+ if (isPostModel(model)) {
20
+ if (!model.category) {
21
+ console.error('PostModel.category is missing!!!!!');
22
+ return '#';
23
+ }
24
+ return `/posts/${model.category.slug}/${encodeURIComponent(model.slug)}`;
25
+ }
26
+ else if (isPageModel(model)) {
27
+ return `/${model.slug}`;
28
+ }
29
+ else if (isNoteModel(model)) {
30
+ return `/notes/${model.nid}`;
31
+ }
32
+ return '/';
33
+ }
34
+ function isDefined(data) {
35
+ return data !== undefined && data !== null;
36
+ }
37
+ exports.urlBuilder = {
38
+ build: async (ctx, config, model) => {
39
+ return buildUrlBase(ctx, config, buildPath(model));
40
+ },
41
+ };
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.0.0",
4
+ "version": "2.1.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -36,31 +36,34 @@
36
36
  },
37
37
  "service": {
38
38
  "required": [
39
- "http"
39
+ "server"
40
40
  ]
41
41
  }
42
42
  },
43
43
  "dependencies": {
44
- "@mx-space/api-client": "^1.16.1",
44
+ "@koishijs/plugin-server": "^3.2.7",
45
+ "@mx-space/api-client": "^1.17.0",
45
46
  "@mx-space/webhook": "^0.5.0",
46
- "@tanstack/query-core": "^4.36.1",
47
- "axios": "^1.8.2",
47
+ "@tanstack/query-core": "^4.40.0",
48
+ "axios": "^1.10.0",
49
+ "chalk": "^4.1.2",
50
+ "consola": "^2.15.3",
51
+ "cron": "^2.4.4",
48
52
  "dayjs": "^1.11.13",
49
- "socket.io-client": "^4.8.1",
50
- "randomcolor": "^0.6.2",
51
- "remove-markdown": "^0.5.5",
52
53
  "lodash": "^4.17.21",
53
- "cron": "^2.4.4",
54
- "chalk": "^4.1.2",
55
54
  "marked": "^5.1.2",
56
- "consola": "^2.15.3",
57
- "zod": "^3.22.4"
55
+ "randomcolor": "^0.6.2",
56
+ "remove-markdown": "^0.5.5",
57
+ "socket.io-client": "^4.8.1",
58
+ "zod": "^3.25.76"
58
59
  },
59
60
  "devDependencies": {
60
- "@types/node": "^22.0.0",
61
- "@types/randomcolor": "^0.5.7",
62
- "koishi": "^4.15.0",
63
- "typescript": "^5.0.0"
61
+ "@types/lodash": "^4.17.20",
62
+ "@types/node": "^22.16.5",
63
+ "@types/randomcolor": "^0.5.9",
64
+ "@types/remove-markdown": "^0.3.4",
65
+ "koishi": "^4.18.8",
66
+ "typescript": "^5.8.3"
64
67
  },
65
68
  "scripts": {
66
69
  "build": "tsc -b",