dm-bot 1.0.0

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/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ DISCORD_TOKEN=你的机器人令牌
2
+ # 在这里粘贴你的 Discord 机器人令牌
3
+ # 不要将 .env 文件提交到版本控制
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,139 @@
1
+ # Discord 交互式机器人
2
+
3
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Discord.js](https://img.shields.io/badge/discord.js-v14.14.1-blue.svg)](https://discord.js.org/)
6
+
7
+ 一个基于 Discord.js 的交互式机器人,集成 OpenCode 功能,支持私聊对话和消息队列管理。
8
+
9
+ ## 功能特性
10
+
11
+ - 私聊消息处理
12
+ - 消息队列管理(防止并发处理)
13
+ - 用户 Session 持久化
14
+ - OpenCode 集成
15
+ - SQLite 数据存储
16
+ - 支持以下命令:
17
+ - `!help` - 显示帮助信息
18
+ - `!reset` - 清空消息队列
19
+ - `!status` - 查看机器人状态
20
+
21
+ ## 环境要求
22
+
23
+ - Node.js >= 18.0.0
24
+ - npm 或 yarn
25
+ - OpenCode CLI(用于 AI 功能)
26
+
27
+ ## 快速开始
28
+
29
+ ### 1. 克隆仓库
30
+
31
+ ```bash
32
+ git clone https://github.com/958877748/skills.git
33
+ cd discord-bot
34
+ ```
35
+
36
+ ### 2. 安装依赖
37
+
38
+ ```bash
39
+ npm install
40
+ ```
41
+
42
+ ### 3. 配置环境变量
43
+
44
+ ```bash
45
+ cp .env.example .env
46
+ # 编辑 .env 文件,填入你的 Discord 机器人令牌
47
+ ```
48
+
49
+ ### 4. 获取 Discord 机器人令牌
50
+
51
+ 1. 访问 [Discord Developer Portal](https://discord.com/developers/applications)
52
+ 2. 点击 "New Application" 创建新应用
53
+ 3. 进入 "Bot" 标签页,点击 "Add Bot"
54
+ 4. 在 "Privileged Gateway Intents" 中启用:
55
+ - MESSAGE CONTENT INTENT
56
+ 5. 复制 Bot Token 到 `.env` 文件
57
+
58
+ ### 5. 邀请机器人到服务器
59
+
60
+ 1. 在 Developer Portal 中,进入 "OAuth2" -> "URL Generator"
61
+ 2. 在 SCOPES 中选择 `bot`
62
+ 3. 在 BOT PERMISSIONS 中选择:
63
+ - Send Messages
64
+ - Read Message History
65
+ - Read Messages/View Channels
66
+ 4. 复制生成的 URL 并在浏览器中打开,选择要添加的服务器
67
+
68
+ ### 6. 启动机器人
69
+
70
+ ```bash
71
+ # 生产环境
72
+ npm start
73
+
74
+ # 开发环境(带自动重启)
75
+ npm run dev
76
+ ```
77
+
78
+ 或者使用脚本:
79
+
80
+ ```bash
81
+ # Linux/Mac
82
+ chmod +x start.sh
83
+ ./start.sh
84
+
85
+ # Windows
86
+ start.bat
87
+ ```
88
+
89
+ ## 使用方法
90
+
91
+ 1. 向机器人发送私信开始对话
92
+ 2. 使用以下命令:
93
+ - `!help` - 显示帮助信息
94
+ - `!reset` - 清空当前队列
95
+ - `!status` - 查看机器人运行状态
96
+
97
+ ## 项目结构
98
+
99
+ ```
100
+ discord-bot/
101
+ ├── index.js # 主入口文件
102
+ ├── db.js # 数据库操作模块
103
+ ├── .env.example # 环境变量示例
104
+ ├── .gitignore # Git 忽略配置
105
+ ├── package.json # 项目配置
106
+ ├── start.sh # Linux/Mac 启动脚本
107
+ ├── start.bat # Windows 启动脚本
108
+ └── README.md # 项目说明
109
+ ```
110
+
111
+ ## 数据库
112
+
113
+ 使用 SQLite 存储数据:
114
+ - `message_queue` - 消息队列表
115
+ - `user_sessions` - 用户 Session 表
116
+
117
+ 数据库文件会自动创建,无需手动初始化。
118
+
119
+ ## 注意事项
120
+
121
+ - 机器人只处理私聊消息,服务器中的消息将被忽略
122
+ - 确保 OpenCode CLI 已安装并可用
123
+ - 不要将 `.env` 文件提交到版本控制
124
+ - 数据库文件 (`*.db`) 不会被 Git 追踪
125
+
126
+ ## 故障排除
127
+
128
+ ### 机器人不响应消息
129
+ - 检查 Bot Token 是否正确
130
+ - 确认 MESSAGE CONTENT INTENT 已启用
131
+ - 查看控制台是否有错误信息
132
+
133
+ ### OpenCode 执行失败
134
+ - 确保 OpenCode CLI 已安装: `opencode --version`
135
+ - 检查 opencode 是否在系统 PATH 中
136
+
137
+ ## 许可证
138
+
139
+ [MIT](LICENSE)
package/bin/cli.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const { startBot } = require('../index');
8
+ const { resetDatabase } = require('../db');
9
+
10
+ // 标准配置目录: ~/.config/discord-bot/
11
+ const configDir = path.join(os.homedir(), '.config', 'discord-bot');
12
+ const configPath = path.join(configDir, 'config.json');
13
+
14
+ // 确保配置目录存在
15
+ function ensureConfigDir() {
16
+ if (!fs.existsSync(configDir)) {
17
+ fs.mkdirSync(configDir, { recursive: true });
18
+ }
19
+ }
20
+
21
+ // 读取配置
22
+ function readConfig() {
23
+ if (fs.existsSync(configPath)) {
24
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ }
26
+ return {};
27
+ }
28
+
29
+ // 保存配置
30
+ function saveConfig(config) {
31
+ ensureConfigDir();
32
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
33
+ }
34
+
35
+ const program = new Command();
36
+
37
+ program
38
+ .name('dm-bot')
39
+ .description('Discord DM Bot CLI with OpenCode integration')
40
+ .version('1.0.0');
41
+
42
+ program
43
+ .command('start')
44
+ .description('启动 Discord 机器人')
45
+ .option('-t, --token <token>', 'Discord Bot Token')
46
+ .action(async (options) => {
47
+ let token = options.token;
48
+
49
+ // 优先级: 1. 命令行参数 2. 环境变量 3. 配置文件
50
+ if (!token) {
51
+ token = process.env.DISCORD_TOKEN;
52
+ }
53
+ if (!token) {
54
+ const config = readConfig();
55
+ token = config.token;
56
+ }
57
+
58
+ if (!token) {
59
+ ensureConfigDir();
60
+
61
+ // 生成配置文件模板
62
+ const configTemplate = {
63
+ token: "YOUR_DISCORD_BOT_TOKEN_HERE",
64
+ description: "请将上面的 token 替换为你的 Discord Bot Token"
65
+ };
66
+ saveConfig(configTemplate);
67
+
68
+ console.error('错误: 未设置 Discord Token');
69
+ console.log('');
70
+ console.log('已为你生成配置文件模板:');
71
+ console.log(` ${configPath}`);
72
+ console.log('');
73
+ console.log('请按以下步骤操作:');
74
+ console.log(' 1. 打开上述文件');
75
+ console.log(' 2. 将 YOUR_DISCORD_BOT_TOKEN_HERE 替换为你的 Discord Bot Token');
76
+ console.log(' 3. 再次运行 dm-bot start');
77
+ console.log('');
78
+ console.log('或者你可以使用以下方式直接设置:');
79
+ console.log(' dm-bot start --token <your-token>');
80
+ process.exit(1);
81
+ }
82
+
83
+ process.env.DISCORD_TOKEN = token;
84
+ console.log('正在启动 Discord Bot...');
85
+ await startBot();
86
+ });
87
+
88
+ program
89
+ .command('config')
90
+ .description('配置 Discord Bot')
91
+ .option('-t, --token <token>', '设置 Discord Bot Token', '')
92
+ .action((options) => {
93
+ if (options.token) {
94
+ const config = readConfig();
95
+ config.token = options.token;
96
+ saveConfig(config);
97
+ console.log(`Token 已保存到 ${configPath}`);
98
+ } else {
99
+ const config = readConfig();
100
+ if (config.token) {
101
+ console.log('当前配置:');
102
+ console.log(`配置文件位置: ${configPath}`);
103
+ console.log(`Token: ${config.token.substring(0, 10)}...`);
104
+ } else {
105
+ console.log('未找到配置');
106
+ console.log('使用: dm-bot config --token <your-token>');
107
+ }
108
+ }
109
+ });
110
+
111
+ program
112
+ .command('reset')
113
+ .description('重置数据库(清空消息队列和会话)')
114
+ .action(() => {
115
+ console.log('正在重置数据库...');
116
+ resetDatabase();
117
+ console.log('数据库已重置');
118
+ });
119
+
120
+ program.parse();
package/db.js ADDED
@@ -0,0 +1,93 @@
1
+ const Database = require('better-sqlite3');
2
+ const path = require('path');
3
+
4
+ const dbPath = path.join(process.cwd(), '.discord-bot.db');
5
+ const db = new Database(dbPath);
6
+
7
+ // 启用 WAL 模式避免锁定问题
8
+ db.pragma('journal_mode = WAL');
9
+
10
+ // 创建表
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS message_queue (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ user_id TEXT NOT NULL,
15
+ channel_id TEXT NOT NULL,
16
+ content TEXT NOT NULL,
17
+ status TEXT DEFAULT 'pending',
18
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
19
+ processed_at DATETIME
20
+ )
21
+ `);
22
+
23
+ function addMessage(userId, channelId, content) {
24
+ const stmt = db.prepare('INSERT INTO message_queue (user_id, channel_id, content) VALUES (?, ?, ?)');
25
+ const result = stmt.run(userId, channelId, content);
26
+ return result.lastInsertRowid;
27
+ }
28
+
29
+ function getPendingMessage() {
30
+ const stmt = db.prepare("SELECT * FROM message_queue WHERE status = 'pending' ORDER BY id ASC LIMIT 1");
31
+ return stmt.get();
32
+ }
33
+
34
+ function hasProcessingMessage() {
35
+ const stmt = db.prepare("SELECT COUNT(*) as count FROM message_queue WHERE status = 'processing'");
36
+ const result = stmt.get();
37
+ return result.count > 0;
38
+ }
39
+
40
+ function markAsProcessing(id) {
41
+ const stmt = db.prepare("UPDATE message_queue SET status = 'processing' WHERE id = ?");
42
+ stmt.run(id);
43
+ }
44
+
45
+ function markAsCompleted(id) {
46
+ const stmt = db.prepare("UPDATE message_queue SET status = 'completed', processed_at = CURRENT_TIMESTAMP WHERE id = ?");
47
+ stmt.run(id);
48
+ }
49
+
50
+ function clearPendingMessages() {
51
+ db.prepare("DELETE FROM message_queue WHERE status IN ('pending', 'processing')").run();
52
+ }
53
+
54
+ // 用户 session 管理
55
+ db.exec(`
56
+ CREATE TABLE IF NOT EXISTS user_sessions (
57
+ user_id TEXT PRIMARY KEY,
58
+ session_id TEXT NOT NULL,
59
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
60
+ )
61
+ `);
62
+
63
+ function getUserSession(userId) {
64
+ const stmt = db.prepare("SELECT session_id FROM user_sessions WHERE user_id = ?");
65
+ const result = stmt.get(userId);
66
+ return result ? result.session_id : null;
67
+ }
68
+
69
+ function setUserSession(userId, sessionId) {
70
+ const stmt = db.prepare(`
71
+ INSERT INTO user_sessions (user_id, session_id) VALUES (?, ?)
72
+ ON CONFLICT(user_id) DO UPDATE SET session_id = ?, updated_at = CURRENT_TIMESTAMP
73
+ `);
74
+ stmt.run(userId, sessionId, sessionId);
75
+ }
76
+
77
+ function resetDatabase() {
78
+ db.prepare("DELETE FROM message_queue").run();
79
+ db.prepare("DELETE FROM user_sessions").run();
80
+ db.prepare("DELETE FROM sqlite_sequence WHERE name='message_queue'").run();
81
+ }
82
+
83
+ module.exports = {
84
+ addMessage,
85
+ getPendingMessage,
86
+ hasProcessingMessage,
87
+ markAsProcessing,
88
+ markAsCompleted,
89
+ clearPendingMessages,
90
+ getUserSession,
91
+ setUserSession,
92
+ resetDatabase
93
+ };
package/index.js ADDED
@@ -0,0 +1,196 @@
1
+ require('dotenv').config();
2
+ const { Client, GatewayIntentBits, Partials } = require('discord.js');
3
+ const { spawn } = require('child_process');
4
+ const db = require('./db');
5
+
6
+ const client = new Client({
7
+ intents: [
8
+ GatewayIntentBits.Guilds,
9
+ GatewayIntentBits.GuildMessages,
10
+ GatewayIntentBits.MessageContent,
11
+ GatewayIntentBits.DirectMessages
12
+ ],
13
+ partials: [Partials.Channel]
14
+ });
15
+
16
+ let isProcessing = false;
17
+
18
+ client.once('clientReady', async () => {
19
+ console.log(`机器人已上线!登录身份:${client.user.tag}`);
20
+
21
+ // 清空未完成的消息
22
+ db.clearPendingMessages();
23
+ isProcessing = false;
24
+
25
+ // 启动消息队列轮询
26
+ startPolling();
27
+ });
28
+
29
+ client.on('messageCreate', async message => {
30
+ // 忽略机器人消息
31
+ if (message.author.bot) return;
32
+
33
+ // 只处理私聊消息
34
+ if (message.guild) return;
35
+
36
+ // 处理命令
37
+ if (message.content.startsWith('!')) {
38
+ const command = message.content.slice(1).toLowerCase();
39
+
40
+ if (command === 'reset') {
41
+ db.clearPendingMessages();
42
+ await message.reply('队列已清空。');
43
+ } else if (command === 'status') {
44
+ const processing = db.hasProcessingMessage();
45
+ await message.reply(`状态: ${isProcessing ? '处理中' : '空闲'}\n队列中有处理中消息: ${processing ? '是' : '否'}`);
46
+ } else if (command === 'help') {
47
+ await message.reply('可用命令:\n!reset - 清空队列\n!status - 查看状态\n!help - 显示帮助\n\n直接发送消息,我会帮你处理。');
48
+ } else {
49
+ await message.reply('未知命令。输入 !help 查看可用命令。');
50
+ }
51
+ return;
52
+ }
53
+
54
+ // 将消息存入数据库队列
55
+ try {
56
+ const msgId = db.addMessage(message.author.id, message.channel.id, message.content);
57
+ console.log(`消息已存入队列 ID: ${msgId}, 来自用户: ${message.author.tag}`);
58
+ } catch (error) {
59
+ console.error('存入消息失败:', error);
60
+ await message.reply('抱歉,处理消息时出错,请稍后重试。');
61
+ }
62
+ });
63
+
64
+ // 轮询处理消息队列
65
+ function startPolling() {
66
+ setInterval(async () => {
67
+ if (isProcessing || db.hasProcessingMessage()) return;
68
+
69
+ const message = db.getPendingMessage();
70
+ if (message) await processMessage(message);
71
+ }, 2000);
72
+ }
73
+
74
+ // 处理单条消息
75
+ async function processMessage(message) {
76
+ isProcessing = true;
77
+ console.log(`[开始处理] 消息 ID: ${message.id}, 用户: ${message.user_id}`);
78
+
79
+ try {
80
+ db.markAsProcessing(message.id);
81
+
82
+ const channel = await client.channels.fetch(message.channel_id);
83
+ if (!channel) throw new Error('无法找到频道');
84
+
85
+ // 获取用户 session
86
+ const userSessionId = db.getUserSession(message.user_id);
87
+ if (userSessionId) {
88
+ console.log(`[使用 Session] ${userSessionId}`);
89
+ }
90
+
91
+ // 执行 opencode run
92
+ console.log(`[执行 opencode] 消息 ID: ${message.id}`);
93
+ const result = await runOpencode(message.content, userSessionId);
94
+ console.log(`[执行完成] 结果长度: ${result.text.length}`);
95
+
96
+ // 保存 session
97
+ if (result.sessionId) {
98
+ db.setUserSession(message.user_id, result.sessionId);
99
+ console.log(`[保存 Session] ${result.sessionId}`);
100
+ }
101
+
102
+ await channel.send(result.text);
103
+ db.markAsCompleted(message.id);
104
+ console.log(`[处理完成] 消息 ID: ${message.id}`);
105
+
106
+ } catch (error) {
107
+ console.error(`[处理失败] 消息 ID: ${message.id}:`, error);
108
+ try {
109
+ const channel = await client.channels.fetch(message.channel_id);
110
+ if (channel) await channel.send(`处理消息时出错: ${error.message}`);
111
+ } catch (e) {}
112
+ db.markAsCompleted(message.id);
113
+ } finally {
114
+ isProcessing = false;
115
+ }
116
+ }
117
+
118
+ // 调用 opencode run
119
+ function runOpencode(prompt, sessionId = null) {
120
+ return new Promise((resolve, reject) => {
121
+ const timeout = 120000;
122
+ let finished = false;
123
+
124
+ const args = ['run', '--format', 'json', prompt];
125
+ if (sessionId) args.push('--session', sessionId);
126
+
127
+ const childProcess = spawn('opencode', args, {
128
+ cwd: process.cwd(),
129
+ env: process.env,
130
+ stdio: ['ignore', 'pipe', 'pipe']
131
+ });
132
+
133
+ let stdout = '';
134
+ let stderr = '';
135
+ let newSessionId = sessionId;
136
+
137
+ childProcess.stdout.on('data', (data) => { stdout += data.toString(); });
138
+ childProcess.stderr.on('data', (data) => { stderr += data.toString(); });
139
+
140
+ childProcess.on('close', (code) => {
141
+ if (finished) return;
142
+ finished = true;
143
+
144
+ if (code === 0) {
145
+ let text = '';
146
+ for (const line of stdout.trim().split('\n')) {
147
+ try {
148
+ const json = JSON.parse(line);
149
+ if (!newSessionId && json.sessionID) newSessionId = json.sessionID;
150
+ if (json.type === 'text' && json.part?.text) text += json.part.text;
151
+ } catch (e) {}
152
+ }
153
+ resolve({ text: text || '处理完成', sessionId: newSessionId });
154
+ } else {
155
+ reject(new Error(stderr.trim() || `进程退出码: ${code}`));
156
+ }
157
+ });
158
+
159
+ childProcess.on('error', (error) => {
160
+ if (!finished) {
161
+ finished = true;
162
+ reject(new Error(`执行opencode失败: ${error.message}`));
163
+ }
164
+ });
165
+
166
+ setTimeout(() => {
167
+ if (!finished) {
168
+ finished = true;
169
+ childProcess.kill('SIGTERM');
170
+ reject(new Error('处理超时'));
171
+ }
172
+ }, timeout);
173
+ });
174
+ }
175
+
176
+ client.on('error', error => console.error('机器人发生错误:', error));
177
+
178
+ // 启动机器人
179
+ function startBot() {
180
+ return client.login(process.env.DISCORD_TOKEN)
181
+ .then(() => {
182
+ console.log('正在登录 Discord...');
183
+ return client;
184
+ })
185
+ .catch(error => {
186
+ console.error('登录失败:', error);
187
+ throw error;
188
+ });
189
+ }
190
+
191
+ module.exports = { startBot };
192
+
193
+ // 如果直接运行此文件,则启动机器人
194
+ if (require.main === module) {
195
+ startBot();
196
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "dm-bot",
3
+ "version": "1.0.0",
4
+ "description": "Interactive Discord bot with OpenCode integration",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "dm-bot": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js",
11
+ "dev": "nodemon index.js",
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/958877748/skills.git"
17
+ },
18
+ "keywords": [
19
+ "discord",
20
+ "bot",
21
+ "interactive",
22
+ "discord-bot",
23
+ "opencode",
24
+ "chatbot"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "dependencies": {
32
+ "better-sqlite3": "^12.6.2",
33
+ "commander": "^14.0.3",
34
+ "discord.js": "^14.14.1",
35
+ "dotenv": "^16.3.1"
36
+ },
37
+ "devDependencies": {
38
+ "nodemon": "^3.0.2"
39
+ }
40
+ }