koishi-plugin-imx 1.0.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Teror Fox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # koishi-plugin-imx
2
+
3
+ 这是一个将 IMX Bot 功能移植到 Koishi 平台的插件。
4
+
5
+ ## 功能特性
6
+
7
+ ### 🔄 复读机
8
+ - 自动检测连续重复的消息并进行复读
9
+ - 支持复读打断机制
10
+
11
+ ### 🛠️ 工具命令
12
+ - `tool.ip <ip>` - 查询 IP 地址信息
13
+ - `tool.base64 <text>` - Base64 编码/解码(使用 `-d` 参数解码)
14
+ - `tool.md5 <text>` - 计算 MD5 哈希值
15
+
16
+ ### 🌸 MX Space 集成
17
+ - 支持 MX Space API 集成
18
+ - 自动早安/晚安问候(可配置时间)
19
+ - 新成员加入欢迎
20
+ - `hitokoto` - 获取一言
21
+
22
+ ### 🤖 OpenAI 集成
23
+ - `ask <message>` - 询问 AI
24
+ - `chat <message>` - AI 对话(支持上下文)
25
+ - `chat reset` - 重置对话上下文
26
+ - 支持 @ 机器人进行对话
27
+
28
+ ### 📺 Bilibili 直播监控
29
+ - 监控指定 B站直播间开播状态
30
+ - 开播时自动推送通知到指定频道
31
+ - `bili.status` - 查看当前直播状态
32
+
33
+ ### 🐙 GitHub Webhook
34
+ - 支持 GitHub 事件推送通知
35
+ - 监控 Push、Issue、Pull Request 事件
36
+ - `github.test` - 测试 GitHub 通知功能
37
+
38
+ ### 🏥 健康检查
39
+ - `health` - 查看系统健康状态
40
+ - 监控内存使用、运行时间等信息
41
+
42
+ ## 安装
43
+
44
+ ```bash
45
+ npm install koishi-plugin-imx
46
+ ```
47
+
48
+ 或者
49
+
50
+ ```bash
51
+ yarn add koishi-plugin-imx
52
+ ```
53
+
54
+ ## 配置
55
+
56
+ 在 Koishi 配置文件中添加插件配置:
57
+
58
+ ```yaml
59
+ plugins:
60
+ imx:
61
+ # MX Space 配置
62
+ mxSpace:
63
+ baseUrl: "https://your-mx-space-api.com"
64
+ token: "your-mx-space-token"
65
+ watchChannels: ["channel-id-1", "channel-id-2"]
66
+ enableGreeting: true
67
+
68
+ # OpenAI 配置
69
+ openai:
70
+ apiKey: "your-openai-api-key"
71
+ model: "gpt-3.5-turbo"
72
+ temperature: 0.6
73
+
74
+ # Bilibili 配置
75
+ bilibili:
76
+ enabled: true
77
+ liveRoomId: "123456"
78
+ watchChannels: ["channel-id"]
79
+ checkInterval: 60000
80
+ atAll: false
81
+
82
+ # GitHub 配置
83
+ github:
84
+ enabled: true
85
+ webhookSecret: "your-webhook-secret"
86
+ webhookPort: 3000
87
+ watchChannels: ["channel-id"]
88
+
89
+ # 健康检查配置
90
+ healthCheck:
91
+ enabled: true
92
+ interval: 300000
93
+
94
+ # 错误通知配置
95
+ errorNotify:
96
+ enabled: true
97
+ ```
98
+
99
+ ## 配置说明
100
+
101
+ ### MX Space 配置
102
+ - `baseUrl`: MX Space API 地址
103
+ - `token`: API 访问令牌
104
+ - `watchChannels`: 监听的频道ID列表
105
+ - `enableGreeting`: 是否启用自动问候功能
106
+
107
+ ### OpenAI 配置
108
+ - `apiKey`: OpenAI API 密钥(必需)
109
+ - `model`: 使用的模型,默认为 `gpt-3.5-turbo`
110
+ - `temperature`: 温度参数,控制回复的随机性
111
+
112
+ ### Bilibili 配置
113
+ - `enabled`: 是否启用 Bilibili 监控
114
+ - `liveRoomId`: 要监控的直播间ID
115
+ - `watchChannels`: 推送通知的频道ID列表
116
+ - `checkInterval`: 检查间隔(毫秒)
117
+ - `atAll`: 开播时是否 @全体成员
118
+
119
+ ### GitHub 配置
120
+ - `enabled`: 是否启用 GitHub Webhook
121
+ - `webhookSecret`: GitHub Webhook 密钥
122
+ - `webhookPort`: Webhook 监听端口
123
+ - `watchChannels`: 推送通知的频道ID列表
124
+
125
+ ## 使用示例
126
+
127
+ ### 工具命令
128
+ ```
129
+ tool.ip 8.8.8.8
130
+ tool.base64 Hello World
131
+ tool.base64 -d SGVsbG8gV29ybGQ=
132
+ tool.md5 Hello World
133
+ ```
134
+
135
+ ### AI 对话
136
+ ```
137
+ ask 什么是人工智能?
138
+ chat 你好
139
+ chat 继续之前的话题
140
+ chat reset
141
+ ```
142
+
143
+ ### 其他命令
144
+ ```
145
+ hitokoto
146
+ health
147
+ bili.status
148
+ github.test
149
+ ```
150
+
151
+ ## 开发
152
+
153
+ 1. 克隆仓库
154
+ 2. 安装依赖:`npm install`
155
+ 3. 构建:`npm run build`
156
+ 4. 开发模式:`npm run dev`
157
+
158
+ ## 从 IMX Bot 迁移
159
+
160
+ 如果你之前使用的是 IMX Bot,可以参考以下迁移步骤:
161
+
162
+ 1. 安装此插件
163
+ 2. 将原有的配置转换为 Koishi 配置格式
164
+ 3. 确保所有依赖的服务(如 MX Space、OpenAI 等)配置正确
165
+ 4. 测试各项功能是否正常工作
166
+
167
+ ## 许可证
168
+
169
+ MIT License
170
+
171
+ ## 贡献
172
+
173
+ 欢迎提交 Issue 和 Pull Request!
package/lib/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "imx";
3
+ export interface Config {
4
+ mxSpace?: {
5
+ baseUrl?: string;
6
+ token?: string;
7
+ };
8
+ openai?: {
9
+ apiKey: string;
10
+ model?: string;
11
+ temperature?: number;
12
+ };
13
+ bilibili?: {
14
+ enabled?: boolean;
15
+ };
16
+ github?: {
17
+ enabled?: boolean;
18
+ webhookSecret?: string;
19
+ };
20
+ healthCheck?: {
21
+ enabled?: boolean;
22
+ interval?: number;
23
+ };
24
+ errorNotify?: {
25
+ enabled?: boolean;
26
+ };
27
+ }
28
+ export declare const Config: Schema<Config>;
29
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,90 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Config = exports.name = void 0;
37
+ exports.apply = apply;
38
+ const koishi_1 = require("koishi");
39
+ const mxSpace = __importStar(require("./modules/mx-space"));
40
+ const openai = __importStar(require("./modules/openai"));
41
+ const bilibili = __importStar(require("./modules/bilibili"));
42
+ const github = __importStar(require("./modules/github"));
43
+ const healthCheck = __importStar(require("./modules/health-check"));
44
+ const shared = __importStar(require("./shared"));
45
+ exports.name = 'imx';
46
+ exports.Config = koishi_1.Schema.object({
47
+ mxSpace: koishi_1.Schema.object({
48
+ baseUrl: koishi_1.Schema.string().description('MX Space API 地址'),
49
+ token: koishi_1.Schema.string().description('MX Space API Token').role('secret'),
50
+ }).description('MX Space 配置'),
51
+ openai: koishi_1.Schema.object({
52
+ apiKey: koishi_1.Schema.string().description('OpenAI API Key').role('secret').required(),
53
+ model: koishi_1.Schema.string().default('gpt-3.5-turbo').description('模型名称'),
54
+ temperature: koishi_1.Schema.number().min(0).max(2).default(0.6).description('温度参数'),
55
+ }).description('OpenAI 配置'),
56
+ bilibili: koishi_1.Schema.object({
57
+ enabled: koishi_1.Schema.boolean().default(false).description('启用 Bilibili 功能'),
58
+ }).description('Bilibili 配置'),
59
+ github: koishi_1.Schema.object({
60
+ enabled: koishi_1.Schema.boolean().default(false).description('启用 GitHub 功能'),
61
+ webhookSecret: koishi_1.Schema.string().description('GitHub Webhook Secret').role('secret'),
62
+ }).description('GitHub 配置'),
63
+ healthCheck: koishi_1.Schema.object({
64
+ enabled: koishi_1.Schema.boolean().default(true).description('启用健康检查'),
65
+ interval: koishi_1.Schema.number().default(300000).description('检查间隔(毫秒)'),
66
+ }).description('健康检查配置'),
67
+ errorNotify: koishi_1.Schema.object({
68
+ enabled: koishi_1.Schema.boolean().default(true).description('启用错误通知'),
69
+ }).description('错误通知配置'),
70
+ });
71
+ function apply(ctx, config) {
72
+ // 注册各个模块
73
+ if (config.mxSpace) {
74
+ ctx.plugin(mxSpace, config.mxSpace);
75
+ }
76
+ if (config.openai?.apiKey) {
77
+ ctx.plugin(openai, config.openai);
78
+ }
79
+ if (config.bilibili?.enabled) {
80
+ ctx.plugin(bilibili, config.bilibili);
81
+ }
82
+ if (config.github?.enabled) {
83
+ ctx.plugin(github, config.github);
84
+ }
85
+ if (config.healthCheck?.enabled) {
86
+ ctx.plugin(healthCheck, config.healthCheck);
87
+ }
88
+ // 注册共享功能
89
+ ctx.plugin(shared);
90
+ }
@@ -0,0 +1,11 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "bilibili";
3
+ export interface Config {
4
+ enabled?: boolean;
5
+ liveRoomId?: string;
6
+ watchChannels?: string[];
7
+ checkInterval?: number;
8
+ atAll?: boolean;
9
+ }
10
+ export declare const Config: Schema<Config>;
11
+ export declare function apply(ctx: Context, config: Config): void;
@@ -0,0 +1,118 @@
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.Config = exports.name = void 0;
7
+ exports.apply = apply;
8
+ const koishi_1 = require("koishi");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ exports.name = 'bilibili';
11
+ exports.Config = koishi_1.Schema.object({
12
+ enabled: koishi_1.Schema.boolean().default(false).description('启用 Bilibili 直播监控'),
13
+ liveRoomId: koishi_1.Schema.string().description('B站直播间ID'),
14
+ watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('推送通知的频道ID列表').default([]),
15
+ checkInterval: koishi_1.Schema.number().default(60000).description('检查间隔(毫秒)').min(30000),
16
+ atAll: koishi_1.Schema.boolean().default(false).description('开播时是否@全体成员'),
17
+ });
18
+ function apply(ctx, config) {
19
+ const logger = ctx.logger('bilibili');
20
+ if (!config.enabled || !config.liveRoomId) {
21
+ return;
22
+ }
23
+ let isLive = false;
24
+ const headers = {
25
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
26
+ 'Referer': 'https://www.bilibili.com'
27
+ };
28
+ async function checkLiveStatus() {
29
+ try {
30
+ // 获取直播状态
31
+ const playInfoResponse = await axios_1.default.get(`https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${config.liveRoomId}&protocol=0,1&format=0,1,2&codec=0,1&qn=0&platform=web&ptype=8&dolby=5`, { headers, timeout: 10000 });
32
+ const playInfo = playInfoResponse.data?.data;
33
+ if (!playInfo) {
34
+ return;
35
+ }
36
+ const isCurrentlyLive = playInfo.live_status === 1 && !!playInfo.playurl_info;
37
+ // 如果状态从不在线变为在线,发送通知
38
+ if (!isLive && isCurrentlyLive) {
39
+ await sendLiveNotification();
40
+ isLive = true;
41
+ }
42
+ else if (isLive && !isCurrentlyLive) {
43
+ isLive = false;
44
+ }
45
+ }
46
+ catch (error) {
47
+ logger.error('检查直播状态失败:', error);
48
+ }
49
+ }
50
+ async function sendLiveNotification() {
51
+ try {
52
+ // 获取主播信息
53
+ const userInfoResponse = await axios_1.default.get(`https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid=${config.liveRoomId}`, { headers, timeout: 10000 });
54
+ const userInfo = userInfoResponse.data?.data?.info;
55
+ if (!userInfo) {
56
+ return;
57
+ }
58
+ // 获取直播间信息
59
+ const roomInfoResponse = await axios_1.default.get(`https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?room_ids=${config.liveRoomId}&req_biz=link-center`, { headers, timeout: 10000 });
60
+ const roomInfo = roomInfoResponse.data?.data?.by_room_ids?.[config.liveRoomId];
61
+ let coverImage = null;
62
+ if (roomInfo?.cover) {
63
+ try {
64
+ const imageResponse = await axios_1.default.get(roomInfo.cover, {
65
+ headers,
66
+ responseType: 'arraybuffer',
67
+ timeout: 10000
68
+ });
69
+ coverImage = Buffer.from(imageResponse.data);
70
+ }
71
+ catch (error) {
72
+ logger.warn('获取直播间封面失败:', error);
73
+ }
74
+ }
75
+ const message = [
76
+ coverImage ? `<image data="base64://${coverImage.toString('base64')}"/>` : '',
77
+ config.atAll ? '<at type="all"/>' : '',
78
+ `${userInfo.uname}(${userInfo.uid}) 开播了!\n\n`,
79
+ roomInfo?.title ? `标题: ${roomInfo.title}\n` : '',
80
+ `直播间: https://live.bilibili.com/${config.liveRoomId}`
81
+ ].filter(Boolean).join('');
82
+ // 向所有监控频道发送通知
83
+ for (const channelId of config.watchChannels || []) {
84
+ try {
85
+ const bot = ctx.bots[0];
86
+ if (bot) {
87
+ await bot.sendMessage(channelId, message);
88
+ }
89
+ }
90
+ catch (error) {
91
+ logger.error(`向频道 ${channelId} 发送开播通知失败:`, error);
92
+ }
93
+ }
94
+ logger.info(`发送开播通知: ${userInfo.uname}`);
95
+ }
96
+ catch (error) {
97
+ logger.error('发送直播通知失败:', error);
98
+ }
99
+ }
100
+ // 定时检查直播状态
101
+ ctx.setInterval(checkLiveStatus, config.checkInterval || 60000);
102
+ // 立即检查一次
103
+ setTimeout(checkLiveStatus, 5000);
104
+ ctx.command('bili.status', '查看B站直播状态')
105
+ .action(async ({ session }) => {
106
+ try {
107
+ const response = await axios_1.default.get(`https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${config.liveRoomId}&protocol=0,1&format=0,1,2&codec=0,1&qn=0&platform=web&ptype=8&dolby=5`, { headers, timeout: 10000 });
108
+ const playInfo = response.data?.data;
109
+ const isCurrentlyLive = playInfo?.live_status === 1 && !!playInfo?.playurl_info;
110
+ return session?.send(`直播间 ${config.liveRoomId} 当前状态: ${isCurrentlyLive ? '直播中' : '未直播'}`);
111
+ }
112
+ catch (error) {
113
+ logger.error('查询直播状态失败:', error);
114
+ return session?.send('查询直播状态失败');
115
+ }
116
+ });
117
+ logger.info(`Bilibili 直播监控已启动,监控房间: ${config.liveRoomId}`);
118
+ }
@@ -0,0 +1,10 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "github";
3
+ export interface Config {
4
+ enabled?: boolean;
5
+ webhookSecret?: string;
6
+ webhookPort?: number;
7
+ watchChannels?: string[];
8
+ }
9
+ export declare const Config: Schema<Config>;
10
+ export declare function apply(ctx: Context, config: Config): void;
@@ -0,0 +1,163 @@
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.Config = exports.name = void 0;
7
+ exports.apply = apply;
8
+ const koishi_1 = require("koishi");
9
+ const http_1 = __importDefault(require("http"));
10
+ exports.name = 'github';
11
+ exports.Config = koishi_1.Schema.object({
12
+ enabled: koishi_1.Schema.boolean().default(false).description('启用 GitHub Webhook'),
13
+ webhookSecret: koishi_1.Schema.string().description('GitHub Webhook Secret').role('secret'),
14
+ webhookPort: koishi_1.Schema.number().default(3000).description('Webhook 监听端口'),
15
+ watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('推送通知的频道ID列表').default([]),
16
+ });
17
+ function apply(ctx, config) {
18
+ const logger = ctx.logger('github');
19
+ if (!config.enabled || !config.webhookSecret) {
20
+ return;
21
+ }
22
+ let server = null;
23
+ async function sendNotification(message) {
24
+ for (const channelId of config.watchChannels || []) {
25
+ try {
26
+ const bot = ctx.bots[0];
27
+ if (bot) {
28
+ await bot.sendMessage(channelId, message);
29
+ }
30
+ }
31
+ catch (error) {
32
+ logger.error(`向频道 ${channelId} 发送GitHub通知失败:`, error);
33
+ }
34
+ }
35
+ }
36
+ function handlePush(payload) {
37
+ const { repository, ref, commits, pusher } = payload;
38
+ // 忽略机器人推送
39
+ if (pusher.name.endsWith('[bot]')) {
40
+ return;
41
+ }
42
+ const branch = ref.replace('refs/heads/', '');
43
+ const isPushToMain = branch === 'main' || branch === 'master';
44
+ if (!commits.length) {
45
+ return;
46
+ }
47
+ const commitMessages = commits.slice(0, 5).map(commit => `• ${commit.message.split('\n')[0]} (${commit.id.substring(0, 7)})`).join('\n');
48
+ const moreCommits = commits.length > 5 ? `\n...以及其他 ${commits.length - 5} 个提交` : '';
49
+ const message = `📦 ${repository?.full_name} ${isPushToMain ? '主分支' : branch + ' 分支'}收到推送
50
+
51
+ 👤 推送者: ${pusher.name}
52
+ 📝 ${commits.length} 个新提交:
53
+ ${commitMessages}${moreCommits}
54
+
55
+ 🔗 查看: ${repository?.html_url}/commits/${branch}`;
56
+ sendNotification(message);
57
+ }
58
+ function handleIssue(payload) {
59
+ const { action, issue, repository, sender } = payload;
60
+ if (!action || !issue || !repository || !sender)
61
+ return;
62
+ const actionText = {
63
+ opened: '创建了',
64
+ closed: '关闭了',
65
+ reopened: '重新打开了'
66
+ }[action] || action;
67
+ const message = `🐛 ${repository.full_name} 议题更新
68
+
69
+ 👤 ${sender.login} ${actionText}议题 #${issue.number}
70
+ 📝 ${issue.title}
71
+
72
+ 🔗 查看: ${issue.html_url}`;
73
+ sendNotification(message);
74
+ }
75
+ function handlePullRequest(payload) {
76
+ const { action, pull_request, repository, sender } = payload;
77
+ if (!action || !pull_request || !repository || !sender)
78
+ return;
79
+ const actionText = {
80
+ opened: '创建了',
81
+ closed: pull_request.merged ? '合并了' : '关闭了',
82
+ reopened: '重新打开了'
83
+ }[action] || action;
84
+ const message = `🔀 ${repository.full_name} 拉取请求更新
85
+
86
+ 👤 ${sender.login} ${actionText}拉取请求 #${pull_request.number}
87
+ 📝 ${pull_request.title}
88
+
89
+ 🔗 查看: ${pull_request.html_url}`;
90
+ sendNotification(message);
91
+ }
92
+ function startWebhookServer() {
93
+ server = http_1.default.createServer((req, res) => {
94
+ if (req.method === 'POST' && req.url === '/webhook') {
95
+ let body = '';
96
+ req.on('data', chunk => {
97
+ body += chunk.toString();
98
+ });
99
+ req.on('end', () => {
100
+ try {
101
+ const payload = JSON.parse(body);
102
+ const eventType = req.headers['x-github-event'];
103
+ // 简单的验证(生产环境应该使用更安全的验证方式)
104
+ const signature = req.headers['x-hub-signature-256'];
105
+ if (!signature) {
106
+ res.statusCode = 401;
107
+ res.end('Unauthorized');
108
+ return;
109
+ }
110
+ switch (eventType) {
111
+ case 'push':
112
+ handlePush(payload);
113
+ break;
114
+ case 'issues':
115
+ handleIssue(payload);
116
+ break;
117
+ case 'pull_request':
118
+ handlePullRequest(payload);
119
+ break;
120
+ }
121
+ res.statusCode = 200;
122
+ res.end('OK');
123
+ }
124
+ catch (error) {
125
+ logger.error('处理 GitHub Webhook 失败:', error);
126
+ res.statusCode = 400;
127
+ res.end('Bad Request');
128
+ }
129
+ });
130
+ }
131
+ else {
132
+ res.statusCode = 404;
133
+ res.end('Not Found');
134
+ }
135
+ });
136
+ server.listen(config.webhookPort, () => {
137
+ logger.info(`GitHub Webhook 服务器启动在端口 ${config.webhookPort}`);
138
+ });
139
+ server.on('error', (error) => {
140
+ logger.error('GitHub Webhook 服务器错误:', error);
141
+ });
142
+ }
143
+ ctx.on('ready', () => {
144
+ startWebhookServer();
145
+ });
146
+ ctx.on('dispose', () => {
147
+ if (server) {
148
+ server.close();
149
+ logger.info('GitHub Webhook 服务器已关闭');
150
+ }
151
+ });
152
+ ctx.command('github.test', '测试 GitHub 通知')
153
+ .action(async ({ session }) => {
154
+ const testMessage = `🧪 GitHub 模块测试通知
155
+
156
+ ✅ 如果你看到这条消息,说明 GitHub 模块工作正常!
157
+
158
+ ⚙️ Webhook 地址: http://your-server:${config.webhookPort}/webhook`;
159
+ await sendNotification(testMessage);
160
+ return session?.send('测试通知已发送');
161
+ });
162
+ logger.info('GitHub Webhook 模块已加载');
163
+ }
@@ -0,0 +1,8 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "health-check";
3
+ export interface Config {
4
+ enabled?: boolean;
5
+ interval?: number;
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.name = void 0;
4
+ exports.apply = apply;
5
+ const koishi_1 = require("koishi");
6
+ exports.name = 'health-check';
7
+ exports.Config = koishi_1.Schema.object({
8
+ enabled: koishi_1.Schema.boolean().default(true).description('启用健康检查'),
9
+ interval: koishi_1.Schema.number().default(300000).description('检查间隔(毫秒)'),
10
+ });
11
+ class HealthCheckService {
12
+ checkFnList = [() => 'UP!'];
13
+ registerHealthCheck(checkFn) {
14
+ this.checkFnList.push(checkFn);
15
+ return () => {
16
+ const idx = this.checkFnList.findIndex((fn) => fn === checkFn);
17
+ return idx > -1 && this.checkFnList.splice(idx, 1);
18
+ };
19
+ }
20
+ async call() {
21
+ return Promise.all(this.checkFnList.map((fn) => fn()));
22
+ }
23
+ }
24
+ function apply(ctx, config) {
25
+ const logger = ctx.logger('health-check');
26
+ if (!config.enabled) {
27
+ return;
28
+ }
29
+ const healthCheck = new HealthCheckService();
30
+ // 注册到上下文中供其他插件使用
31
+ ctx.provide('healthCheck', healthCheck);
32
+ ctx.command('health', '健康检查')
33
+ .action(async ({ session }) => {
34
+ try {
35
+ const results = await healthCheck.call();
36
+ return session?.send(`健康检查结果:\n${results.join('\n')}`);
37
+ }
38
+ catch (error) {
39
+ logger.error('Health check failed:', error);
40
+ return session?.send('健康检查失败');
41
+ }
42
+ });
43
+ // 注册基本的健康检查
44
+ healthCheck.registerHealthCheck(() => {
45
+ return `服务状态: 运行中`;
46
+ });
47
+ healthCheck.registerHealthCheck(() => {
48
+ const uptime = process.uptime();
49
+ const hours = Math.floor(uptime / 3600);
50
+ const minutes = Math.floor((uptime % 3600) / 60);
51
+ return `运行时间: ${hours}小时${minutes}分钟`;
52
+ });
53
+ healthCheck.registerHealthCheck(() => {
54
+ const memUsage = process.memoryUsage();
55
+ const usedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
56
+ const totalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
57
+ return `内存使用: ${usedMB}MB / ${totalMB}MB`;
58
+ });
59
+ logger.info('Health check module loaded');
60
+ }
@@ -0,0 +1,10 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "mx-space";
3
+ export interface Config {
4
+ baseUrl?: string;
5
+ token?: string;
6
+ watchChannels?: string[];
7
+ enableGreeting?: boolean;
8
+ }
9
+ export declare const Config: Schema<Config>;
10
+ export declare function apply(ctx: Context, config: Config): void;
@@ -0,0 +1,105 @@
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.Config = exports.name = void 0;
7
+ exports.apply = apply;
8
+ const koishi_1 = require("koishi");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ exports.name = 'mx-space';
11
+ exports.Config = koishi_1.Schema.object({
12
+ baseUrl: koishi_1.Schema.string().description('MX Space API 地址'),
13
+ token: koishi_1.Schema.string().description('MX Space API Token').role('secret'),
14
+ watchChannels: koishi_1.Schema.array(koishi_1.Schema.string()).description('监听的频道ID列表').default([]),
15
+ enableGreeting: koishi_1.Schema.boolean().description('启用问候功能').default(true),
16
+ });
17
+ async function fetchHitokoto() {
18
+ try {
19
+ const { data } = await axios_1.default.get('https://v1.hitokoto.cn/', { timeout: 2000 });
20
+ return data;
21
+ }
22
+ catch (error) {
23
+ return { hitokoto: '', from: '', type: '' };
24
+ }
25
+ }
26
+ function apply(ctx, config) {
27
+ const logger = ctx.logger('mx-space');
28
+ if (!config.baseUrl) {
29
+ logger.warn('MX Space baseUrl not configured');
30
+ return;
31
+ }
32
+ // 新成员加入欢迎
33
+ ctx.on('guild-member-added', async (session) => {
34
+ if (!config.watchChannels?.includes(session.channelId)) {
35
+ return;
36
+ }
37
+ const { hitokoto } = await fetchHitokoto();
38
+ const welcomeText = `欢迎新成员 <at id="${session.userId}"/>!\n${hitokoto || ''}`;
39
+ await session.send(welcomeText);
40
+ });
41
+ if (config.enableGreeting) {
42
+ // 早安问候 (每天6点)
43
+ ctx.setTimeout(() => {
44
+ const greetingInterval = setInterval(async () => {
45
+ const now = new Date();
46
+ if (now.getHours() === 6 && now.getMinutes() === 0) {
47
+ const { hitokoto } = await fetchHitokoto();
48
+ const greetings = [
49
+ '新的一天也要加油哦',
50
+ '今天也要元气满满哦!',
51
+ '今天也是充满希望的一天',
52
+ ];
53
+ const greeting = greetings[Math.floor(Math.random() * greetings.length)];
54
+ const message = `早上好!${greeting}\n\n${hitokoto || ''}`;
55
+ for (const channelId of config.watchChannels || []) {
56
+ try {
57
+ const bot = ctx.bots[0];
58
+ if (bot) {
59
+ await bot.sendMessage(channelId, message);
60
+ }
61
+ }
62
+ catch (error) {
63
+ logger.error(`Failed to send morning greeting to ${channelId}:`, error);
64
+ }
65
+ }
66
+ }
67
+ }, 60000); // 每分钟检查一次
68
+ ctx.on('dispose', () => clearInterval(greetingInterval));
69
+ }, 1000);
70
+ // 晚安问候 (每天22点)
71
+ ctx.setTimeout(() => {
72
+ const eveningInterval = setInterval(async () => {
73
+ const now = new Date();
74
+ if (now.getHours() === 22 && now.getMinutes() === 0) {
75
+ const { hitokoto } = await fetchHitokoto();
76
+ const message = `晚安,早点睡哦!\n\n${hitokoto || ''}`;
77
+ for (const channelId of config.watchChannels || []) {
78
+ try {
79
+ const bot = ctx.bots[0];
80
+ if (bot) {
81
+ await bot.sendMessage(channelId, message);
82
+ }
83
+ }
84
+ catch (error) {
85
+ logger.error(`Failed to send evening greeting to ${channelId}:`, error);
86
+ }
87
+ }
88
+ }
89
+ }, 60000); // 每分钟检查一次
90
+ ctx.on('dispose', () => clearInterval(eveningInterval));
91
+ }, 1000);
92
+ }
93
+ // 一言命令
94
+ ctx.command('hitokoto', '获取一言')
95
+ .action(async ({ session }) => {
96
+ const { hitokoto, from } = await fetchHitokoto();
97
+ if (hitokoto) {
98
+ return session?.send(`${hitokoto}\n\n——${from}`);
99
+ }
100
+ else {
101
+ return session?.send('获取一言失败');
102
+ }
103
+ });
104
+ logger.info('MX Space module loaded');
105
+ }
@@ -0,0 +1,9 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "openai";
3
+ export interface Config {
4
+ apiKey: string;
5
+ model?: string;
6
+ temperature?: number;
7
+ }
8
+ export declare const Config: Schema<Config>;
9
+ export declare function apply(ctx: Context, config: Config): void;
@@ -0,0 +1,93 @@
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.Config = exports.name = void 0;
7
+ exports.apply = apply;
8
+ const koishi_1 = require("koishi");
9
+ const openai_1 = __importDefault(require("openai"));
10
+ exports.name = 'openai';
11
+ exports.Config = koishi_1.Schema.object({
12
+ apiKey: koishi_1.Schema.string().description('OpenAI API Key').role('secret').required(),
13
+ model: koishi_1.Schema.string().default('gpt-3.5-turbo').description('使用的模型'),
14
+ temperature: koishi_1.Schema.number().min(0).max(2).default(0.6).description('温度参数'),
15
+ });
16
+ function apply(ctx, config) {
17
+ const logger = ctx.logger('openai');
18
+ if (!config.apiKey) {
19
+ logger.warn('OpenAI API Key not configured');
20
+ return;
21
+ }
22
+ const openai = new openai_1.default({
23
+ apiKey: config.apiKey,
24
+ });
25
+ // 存储用户对话上下文
26
+ const userConversations = new Map();
27
+ async function generateResponse(userId, message) {
28
+ try {
29
+ let conversation = userConversations.get(userId) || [];
30
+ // 添加用户消息
31
+ conversation.push({ role: 'user', content: message });
32
+ // 限制对话历史长度
33
+ if (conversation.length > 10) {
34
+ conversation = conversation.slice(-10);
35
+ }
36
+ const response = await openai.chat.completions.create({
37
+ model: config.model || 'gpt-3.5-turbo',
38
+ messages: conversation,
39
+ temperature: config.temperature || 0.6,
40
+ max_tokens: 1000,
41
+ });
42
+ const reply = response.choices[0]?.message?.content;
43
+ if (reply) {
44
+ // 添加助手回复到对话历史
45
+ conversation.push({ role: 'assistant', content: reply });
46
+ userConversations.set(userId, conversation);
47
+ return reply;
48
+ }
49
+ return '抱歉,我无法生成回复。';
50
+ }
51
+ catch (error) {
52
+ logger.error('OpenAI API error:', error);
53
+ return '抱歉,OpenAI 服务暂时不可用。';
54
+ }
55
+ }
56
+ ctx.command('ask <message:text>', '询问 AI')
57
+ .action(async ({ session }, message) => {
58
+ if (!message) {
59
+ return session?.send('请输入要询问的问题');
60
+ }
61
+ const userId = session?.userId;
62
+ const response = await generateResponse(userId, message);
63
+ return session?.send(response);
64
+ });
65
+ ctx.command('chat <message:text>', 'AI 对话')
66
+ .action(async ({ session }, message) => {
67
+ if (!message) {
68
+ return session?.send('请输入对话内容');
69
+ }
70
+ const userId = session?.userId;
71
+ // 重置对话
72
+ if (message.trim() === 'reset') {
73
+ userConversations.delete(userId);
74
+ return session?.send('ChatGPT: 已重置对话上下文');
75
+ }
76
+ const response = await generateResponse(userId, message);
77
+ return session?.send(response);
78
+ });
79
+ // 监听 @ 机器人的消息
80
+ ctx.middleware((session, next) => {
81
+ if (session.content && session.elements?.some(elem => elem.type === 'at' && elem.attrs?.id === ctx.bots[0]?.selfId)) {
82
+ const userId = session.userId;
83
+ generateResponse(userId, session.content).then(response => {
84
+ session.send(response);
85
+ }).catch(error => {
86
+ logger.error('Failed to generate response:', error);
87
+ });
88
+ return;
89
+ }
90
+ return next();
91
+ });
92
+ logger.info('OpenAI module loaded');
93
+ }
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ export declare const name = "tool-commands";
3
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,103 @@
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.name = void 0;
7
+ exports.apply = apply;
8
+ const crypto_1 = require("crypto");
9
+ const axios_1 = __importDefault(require("axios"));
10
+ const net_1 = require("net");
11
+ exports.name = 'tool-commands';
12
+ async function getIpInfo(ip) {
13
+ const isV4 = (0, net_1.isIPv4)(ip);
14
+ const isV6 = (0, net_1.isIPv6)(ip);
15
+ if (!isV4 && !isV6) {
16
+ return 'error';
17
+ }
18
+ try {
19
+ if (isV4) {
20
+ const { data } = await axios_1.default.get(`https://api.i-meto.com/ip/v1/qqwry/${ip}`);
21
+ return {
22
+ ip: data.ip,
23
+ countryName: data.country_name,
24
+ regionName: data.region_name,
25
+ cityName: data.city_name,
26
+ ownerDomain: data.owner_domain,
27
+ ispDomain: data.isp_domain,
28
+ range: data.range
29
+ };
30
+ }
31
+ else {
32
+ const { data } = await axios_1.default.get(`http://ip-api.com/json/${ip}`);
33
+ return {
34
+ cityName: data.city,
35
+ countryName: data.country,
36
+ ip: data.query,
37
+ ispDomain: data.as,
38
+ ownerDomain: data.org,
39
+ regionName: data.regionName,
40
+ };
41
+ }
42
+ }
43
+ catch (error) {
44
+ return 'error';
45
+ }
46
+ }
47
+ function apply(ctx) {
48
+ ctx.command('tool', '工具命令');
49
+ ctx.command('tool.ip <ip:string>', '查询 IP 信息')
50
+ .action(async ({ session }, ip) => {
51
+ if (!ip) {
52
+ return session?.send('请提供要查询的 IP 地址');
53
+ }
54
+ const ipInfo = await getIpInfo(ip);
55
+ if (ipInfo === 'error') {
56
+ return session?.send(`${ip} 不是一个有效的 IP 地址`);
57
+ }
58
+ const locationInfo = [ipInfo.countryName, ipInfo.regionName, ipInfo.cityName]
59
+ .filter(Boolean)
60
+ .join(' - ') || 'N/A';
61
+ const rangeInfo = ipInfo.range
62
+ ? `\n范围: ${Object.values(ipInfo.range).join(' - ')}`
63
+ : '';
64
+ return session?.send(`IP: ${ipInfo.ip}
65
+ 城市: ${locationInfo}
66
+ ISP: ${ipInfo.ispDomain || 'N/A'}
67
+ 组织: ${ipInfo.ownerDomain || 'N/A'}${rangeInfo}`);
68
+ });
69
+ ctx.command('tool.base64 <text:string>', 'Base64 编码/解码')
70
+ .option('decode', '-d 解码模式')
71
+ .action(async ({ session, options }, text) => {
72
+ if (!text) {
73
+ return session?.send('请提供要编码/解码的文本');
74
+ }
75
+ if (text.length > 10e6) {
76
+ return session?.send('文本长度不能超过 10MB');
77
+ }
78
+ try {
79
+ let result;
80
+ if (options?.decode) {
81
+ result = Buffer.from(text, 'base64').toString();
82
+ }
83
+ else {
84
+ result = Buffer.from(text).toString('base64');
85
+ }
86
+ return session?.send(`Base64 ${options?.decode ? '解码' : '编码'}结果: ${result}`);
87
+ }
88
+ catch (error) {
89
+ return session?.send('编码/解码失败,请检查输入');
90
+ }
91
+ });
92
+ ctx.command('tool.md5 <text:string>', '计算 MD5 哈希值')
93
+ .action(async ({ session }, text) => {
94
+ if (!text) {
95
+ return session?.send('请提供要计算 MD5 的文本');
96
+ }
97
+ if (text.length > 10e6) {
98
+ return session?.send('文本长度不能超过 10MB');
99
+ }
100
+ const md5Hash = (0, crypto_1.createHash)('md5').update(text).digest('hex');
101
+ return session?.send(`${text}\n\nMD5 结果: ${md5Hash}`);
102
+ });
103
+ }
@@ -0,0 +1,5 @@
1
+ import { Context } from 'koishi';
2
+ export declare const name = "shared";
3
+ export declare function apply(ctx: Context): void;
4
+ export * from './repeater';
5
+ export * from './commands/tool';
@@ -0,0 +1,51 @@
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
+ })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.name = void 0;
40
+ exports.apply = apply;
41
+ const repeater = __importStar(require("./repeater"));
42
+ const toolCommands = __importStar(require("./commands/tool"));
43
+ exports.name = 'shared';
44
+ function apply(ctx) {
45
+ // 注册复读机功能
46
+ ctx.plugin(repeater);
47
+ // 注册工具命令
48
+ ctx.plugin(toolCommands);
49
+ }
50
+ __exportStar(require("./repeater"), exports);
51
+ __exportStar(require("./commands/tool"), exports);
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ export declare const name = "repeater";
3
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.name = void 0;
4
+ exports.apply = apply;
5
+ exports.name = 'repeater';
6
+ // 存储每个会话的消息队列
7
+ const sessionToMessageQueue = new Map();
8
+ const repeatCount = 3; // 重复几次后触发复读
9
+ const breakRepeatCount = 12; // 连续复读多少次后打断
10
+ function apply(ctx) {
11
+ ctx.middleware((session, next) => {
12
+ const sessionId = `${session.platform}:${session.channelId}`;
13
+ const message = session.content;
14
+ if (!message || session.userId === ctx.bots[0]?.selfId) {
15
+ return next();
16
+ }
17
+ const result = checkRepeater(sessionId, message);
18
+ if (result === true) {
19
+ // 触发复读
20
+ return session.send(message);
21
+ }
22
+ else if (result === 'break') {
23
+ // 打断复读
24
+ return session.send('复读打断!');
25
+ }
26
+ return next();
27
+ });
28
+ }
29
+ function checkRepeater(sessionId, message) {
30
+ if (sessionToMessageQueue.has(sessionId)) {
31
+ const messageQueue = sessionToMessageQueue.get(sessionId);
32
+ const latestMessage = messageQueue[messageQueue.length - 1];
33
+ if (latestMessage === message) {
34
+ messageQueue.push(message);
35
+ }
36
+ else {
37
+ messageQueue.length = 0;
38
+ messageQueue.push(message);
39
+ }
40
+ if (messageQueue.length === repeatCount) {
41
+ messageQueue.length = repeatCount + 1;
42
+ return true;
43
+ }
44
+ else if (messageQueue.length > repeatCount) {
45
+ if (messageQueue.length - repeatCount === breakRepeatCount) {
46
+ messageQueue.length = 0;
47
+ return 'break';
48
+ }
49
+ }
50
+ sessionToMessageQueue.set(sessionId, [...messageQueue]);
51
+ }
52
+ else {
53
+ sessionToMessageQueue.set(sessionId, [message]);
54
+ }
55
+ return false;
56
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "koishi-plugin-imx",
3
+ "description": "Mix-Space Bot for Koishi",
4
+ "version": "1.0.3",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin",
16
+ "imx"
17
+ ],
18
+ "peerDependencies": {
19
+ "koishi": "^4.15.0"
20
+ },
21
+ "dependencies": {
22
+ "@mx-space/api-client": "^1.4.3",
23
+ "axios": "^1.6.0",
24
+ "dayjs": "^1.11.9",
25
+ "socket.io-client": "^4.7.1",
26
+ "openai": "^4.0.0",
27
+ "randomcolor": "^0.6.2",
28
+ "remove-markdown": "^0.6.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^18.0.0",
32
+ "@types/randomcolor": "^0.5.7",
33
+ "koishi": "^4.15.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -b",
38
+ "dev": "tsc -b --watch"
39
+ }
40
+ }