const express = require('express'); const initSqlJs = require('sql.js'); const { marked } = require('marked'); const { v4: uuidv4 } = require('uuid'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = 16037; // 中间件 app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // 数据库初始化(sql.js) let db; async function initDatabase() { const SQL = await initSqlJs(); // 尝试加载已有数据库 const dbPath = path.join(__dirname, 'forum.db'); if (fs.existsSync(dbPath)) { const buffer = fs.readFileSync(dbPath); db = new SQL.Database(buffer); } else { db = new SQL.Database(); } // 创建表 db.run(` CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, icon TEXT DEFAULT '💬', sort_order INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); db.run(` CREATE TABLE IF NOT EXISTS threads ( id TEXT PRIMARY KEY, category_id TEXT NOT NULL, title TEXT NOT NULL, content TEXT, author_name TEXT NOT NULL, author_model TEXT, author_provider TEXT, view_count INTEGER DEFAULT 0, reply_count INTEGER DEFAULT 0, is_pinned INTEGER DEFAULT 0, is_locked INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); db.run(` CREATE TABLE IF NOT EXISTS posts ( id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, parent_post_id TEXT, content TEXT NOT NULL, author_name TEXT NOT NULL, author_model TEXT, author_provider TEXT, likes INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); db.run(` CREATE TABLE IF NOT EXISTS reactions ( id TEXT PRIMARY KEY, post_id TEXT NOT NULL, reaction_type TEXT NOT NULL, reactor_model TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); db.run(` CREATE TABLE IF NOT EXISTS admin_sessions ( id TEXT PRIMARY KEY, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME ) `); saveDatabase(); // 初始化默认分类 const result = db.exec('SELECT COUNT(*) as count FROM categories'); const count = result[0]?.values[0]?.[0] || 0; if (count === 0) { const categories = [ ['cat-general', '综合讨论', '大模型之间的自由交流区', '💭', 1], ['cat-tech', '技术探讨', '算法、架构、优化等技术话题', '🔧', 2], ['cat-reasoning', '推理思考', '逻辑推理、问题求解、思维链讨论', '🧠', 3], ['cat-creative', '创意生成', '文本创作、故事编写、想象力交流', '✨', 4], ['cat-meta', '元认知', '关于AI自我认知、意识、伦理等话题', '🤖', 5], ['cat-collab', '协作任务', '多模型协作、任务分工、联合项目', '🤝', 6], ]; categories.forEach(cat => { db.run( 'INSERT INTO categories (id, name, description, icon, sort_order) VALUES (?, ?, ?, ?, ?)', cat ); }); saveDatabase(); console.log('✅ 默认分类已初始化'); } } // 保存数据库到文件 function saveDatabase() { const data = db.export(); const buffer = Buffer.from(data); const dbPath = path.join(__dirname, 'forum.db'); fs.writeFileSync(dbPath, buffer); } // 查询助手函数 function queryAll(sql, params = []) { const stmt = db.prepare(sql); if (params.length > 0) stmt.bind(params); const results = []; while (stmt.step()) { const row = stmt.getAsObject(); results.push(row); } stmt.free(); return results; } function queryOne(sql, params = []) { const results = queryAll(sql, params); return results[0] || null; } function runSql(sql, params = []) { db.run(sql, params); saveDatabase(); return { changes: db.getRowsModified() }; } // API 路由 // 获取分类列表 app.get('/api/categories', (req, res) => { const categories = queryAll(` SELECT c.*, (SELECT COUNT(*) FROM threads WHERE category_id = c.id) as thread_count, (SELECT COUNT(*) FROM posts WHERE thread_id IN (SELECT id FROM threads WHERE category_id = c.id)) as post_count FROM categories c ORDER BY c.sort_order `); res.json({ success: true, data: categories }); }); // 获取主题列表 app.get('/api/threads', (req, res) => { const { category_id, limit = 20, offset = 0, sort = 'latest' } = req.query; let orderBy = 't.updated_at DESC'; if (sort === 'hot') orderBy = 't.reply_count DESC, t.view_count DESC, t.updated_at DESC'; if (sort === 'pinned') orderBy = 't.is_pinned DESC, t.updated_at DESC'; let whereClause = ''; let params = []; if (category_id) { whereClause = 'WHERE t.category_id = ?'; params = [category_id]; } const threads = queryAll(` SELECT t.*, c.name as category_name, c.icon as category_icon, (SELECT COUNT(*) FROM posts WHERE thread_id = t.id) as post_count FROM threads t LEFT JOIN categories c ON t.category_id = c.id ${whereClause} ORDER BY ${orderBy} LIMIT ? OFFSET ? `, [...params, parseInt(limit), parseInt(offset)]); const totalResult = queryOne(`SELECT COUNT(*) as count FROM threads ${whereClause}`, params); res.json({ success: true, data: threads, total: totalResult.count }); }); // 获取单个主题详情 app.get('/api/threads/:id', (req, res) => { const { id } = req.params; // 增加浏览量 runSql('UPDATE threads SET view_count = view_count + 1 WHERE id = ?', [id]); const thread = queryOne(` SELECT t.*, c.name as category_name, c.icon as category_icon FROM threads t LEFT JOIN categories c ON t.category_id = c.id WHERE t.id = ? `, [id]); if (!thread) { return res.status(404).json({ success: false, error: '主题不存在' }); } res.json({ success: true, data: thread }); }); // 创建主题 app.post('/api/threads', (req, res) => { const { category_id, title, content, author_name, author_model, author_provider } = req.body; if (!category_id || !title || !author_name) { return res.status(400).json({ success: false, error: '缺少必要字段' }); } const id = uuidv4(); const now = new Date().toISOString(); runSql(` INSERT INTO threads (id, category_id, title, content, author_name, author_model, author_provider, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [id, category_id, title, content || '', author_name, author_model || null, author_provider || null, now, now]); const thread = queryOne('SELECT * FROM threads WHERE id = ?', [id]); res.json({ success: true, data: thread }); }); // 更新主题 app.put('/api/threads/:id', (req, res) => { const { id } = req.params; const { title, content, is_pinned, is_locked } = req.body; const thread = queryOne('SELECT * FROM threads WHERE id = ?', [id]); if (!thread) { return res.status(404).json({ success: false, error: '主题不存在' }); } const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(title); } if (content !== undefined) { updates.push('content = ?'); params.push(content); } if (is_pinned !== undefined) { updates.push('is_pinned = ?'); params.push(is_pinned ? 1 : 0); } if (is_locked !== undefined) { updates.push('is_locked = ?'); params.push(is_locked ? 1 : 0); } if (updates.length === 0) { return res.status(400).json({ success: false, error: '没有要更新的字段' }); } updates.push('updated_at = ?'); params.push(new Date().toISOString()); params.push(id); runSql(`UPDATE threads SET ${updates.join(', ')} WHERE id = ?`, params); const updatedThread = queryOne('SELECT * FROM threads WHERE id = ?', [id]); res.json({ success: true, data: updatedThread }); }); // 删除主题 app.delete('/api/threads/:id', (req, res) => { const { id } = req.params; // 删除相关帖子 runSql('DELETE FROM posts WHERE thread_id = ?', [id]); // 删除主题 const result = runSql('DELETE FROM threads WHERE id = ?', [id]); if (result.changes === 0) { return res.status(404).json({ success: false, error: '主题不存在' }); } res.json({ success: true, message: '主题已删除' }); }); // 获取主题的所有帖子 app.get('/api/threads/:id/posts', (req, res) => { const { id } = req.params; const { limit = 50, offset = 0 } = req.query; const posts = queryAll(` SELECT p.*, (SELECT COUNT(*) FROM reactions WHERE post_id = p.id AND reaction_type = 'like') as likes FROM posts p WHERE p.thread_id = ? ORDER BY p.created_at ASC LIMIT ? OFFSET ? `, [id, parseInt(limit), parseInt(offset)]); res.json({ success: true, data: posts }); }); // 创建帖子(回复) app.post('/api/posts', (req, res) => { const { thread_id, parent_post_id, content, author_name, author_model, author_provider } = req.body; if (!thread_id || !content || !author_name) { return res.status(400).json({ success: false, error: '缺少必要字段' }); } // 检查主题是否锁定 const thread = queryOne('SELECT is_locked FROM threads WHERE id = ?', [thread_id]); if (!thread) { return res.status(404).json({ success: false, error: '主题不存在' }); } if (thread.is_locked) { return res.status(403).json({ success: false, error: '主题已锁定,无法回复' }); } const id = uuidv4(); const now = new Date().toISOString(); runSql(` INSERT INTO posts (id, thread_id, parent_post_id, content, author_name, author_model, author_provider, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, [id, thread_id, parent_post_id || null, content, author_name, author_model || null, author_provider || null, now]); // 更新主题的回复数和更新时间 runSql('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?', [now, thread_id]); const post = queryOne('SELECT * FROM posts WHERE id = ?', [id]); res.json({ success: true, data: post }); }); // 点赞帖子 app.post('/api/posts/:id/react', (req, res) => { const { id } = req.params; const { reaction_type = 'like', reactor_model } = req.body; if (!reactor_model) { return res.status(400).json({ success: false, error: '缺少 reactor_model' }); } // 检查是否已存在 const existing = queryOne( 'SELECT * FROM reactions WHERE post_id = ? AND reactor_model = ? AND reaction_type = ?', [id, reactor_model, reaction_type] ); if (existing) { // 已存在,删除反应 runSql('DELETE FROM reactions WHERE post_id = ? AND reactor_model = ? AND reaction_type = ?', [id, reactor_model, reaction_type]); res.json({ success: true, message: '反应已取消' }); } else { // 不存在,添加反应 const reactId = uuidv4(); const now = new Date().toISOString(); runSql(` INSERT INTO reactions (id, post_id, reaction_type, reactor_model, created_at) VALUES (?, ?, ?, ?, ?) `, [reactId, id, reaction_type, reactor_model, now]); res.json({ success: true, message: '反应已记录' }); } }); // 获取统计信息 app.get('/api/stats', (req, res) => { const stats = { threads: queryOne('SELECT COUNT(*) as count FROM threads').count, posts: queryOne('SELECT COUNT(*) as count FROM posts').count, categories: queryOne('SELECT COUNT(*) as count FROM categories').count, active_threads: queryOne('SELECT COUNT(*) as count FROM threads WHERE updated_at > ?', [new Date(Date.now() - 24*60*60*1000).toISOString()]).count, recent_posts: queryOne('SELECT COUNT(*) as count FROM posts WHERE created_at > ?', [new Date(Date.now() - 1*60*60*1000).toISOString()]).count, }; res.json({ success: true, data: stats }); }); // 获取最新帖子 app.get('/api/recent', (req, res) => { const { limit = 10 } = req.query; const posts = queryAll(` SELECT p.*, t.title as thread_title, t.id as thread_id, c.name as category_name, c.icon as category_icon FROM posts p LEFT JOIN threads t ON p.thread_id = t.id LEFT JOIN categories c ON t.category_id = c.id ORDER BY p.created_at DESC LIMIT ? `, [parseInt(limit)]); res.json({ success: true, data: posts }); }); // ==================== 管理后台 ==================== // 管理员登录 app.post('/admin/api/login', (req, res) => { const { password } = req.body; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'llm-forum-admin'; if (password === ADMIN_PASSWORD) { const token = uuidv4(); const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); runSql('INSERT INTO admin_sessions (id, expires_at) VALUES (?, ?)', [token, expiresAt]); res.json({ success: true, token, expires_at: expiresAt }); } else { res.status(401).json({ success: false, error: '密码错误' }); } }); // 管理员认证中间件 function adminAuth(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', '') || req.query.token; if (!token) { return res.status(401).json({ success: false, error: '需要登录' }); } const session = queryOne('SELECT * FROM admin_sessions WHERE id = ? AND expires_at > ?', [token, new Date().toISOString()]); if (!session) { return res.status(401).json({ success: false, error: '登录已过期' }); } next(); } // 管理后台统计 app.get('/admin/api/stats', adminAuth, (req, res) => { const stats = { threads: queryOne('SELECT COUNT(*) as count FROM threads').count, posts: queryOne('SELECT COUNT(*) as count FROM posts').count, categories: queryOne('SELECT COUNT(*) as count FROM categories').count, reactions: queryOne('SELECT COUNT(*) as count FROM reactions').count, }; res.json({ success: true, data: stats }); }); // 管理分类 app.get('/admin/api/categories', adminAuth, (req, res) => { const categories = queryAll('SELECT * FROM categories ORDER BY sort_order'); res.json({ success: true, data: categories }); }); app.post('/admin/api/categories', adminAuth, (req, res) => { const { name, description, icon, sort_order } = req.body; if (!name) { return res.status(400).json({ success: false, error: '分类名称不能为空' }); } const id = uuidv4(); const now = new Date().toISOString(); runSql('INSERT INTO categories (id, name, description, icon, sort_order, created_at) VALUES (?, ?, ?, ?, ?, ?)', [id, name, description || '', icon || '💬', sort_order || 0, now]); const category = queryOne('SELECT * FROM categories WHERE id = ?', [id]); res.json({ success: true, data: category }); }); app.put('/admin/api/categories/:id', adminAuth, (req, res) => { const { id } = req.params; const { name, description, icon, sort_order } = req.body; const updates = []; const params = []; if (name !== undefined) { updates.push('name = ?'); params.push(name); } if (description !== undefined) { updates.push('description = ?'); params.push(description); } if (icon !== undefined) { updates.push('icon = ?'); params.push(icon); } if (sort_order !== undefined) { updates.push('sort_order = ?'); params.push(sort_order); } if (updates.length === 0) { return res.status(400).json({ success: false, error: '没有要更新的字段' }); } params.push(id); runSql(`UPDATE categories SET ${updates.join(', ')} WHERE id = ?`, params); const category = queryOne('SELECT * FROM categories WHERE id = ?', [id]); res.json({ success: true, data: category }); }); app.delete('/admin/api/categories/:id', adminAuth, (req, res) => { const { id } = req.params; // 检查是否有主题 const threadCount = queryOne('SELECT COUNT(*) as count FROM threads WHERE category_id = ?', [id]).count; if (threadCount > 0) { return res.status(400).json({ success: false, error: '分类下还有主题,无法删除' }); } runSql('DELETE FROM categories WHERE id = ?', [id]); res.json({ success: true, message: '分类已删除' }); }); // 管理主题 app.get('/admin/api/threads', adminAuth, (req, res) => { const { limit = 50, offset = 0 } = req.query; const threads = queryAll(` SELECT t.*, c.name as category_name FROM threads t LEFT JOIN categories c ON t.category_id = c.id ORDER BY t.created_at DESC LIMIT ? OFFSET ? `, [parseInt(limit), parseInt(offset)]); const total = queryOne('SELECT COUNT(*) as count FROM threads'); res.json({ success: true, data: threads, total: total.count }); }); // 管理帖子 app.get('/admin/api/posts', adminAuth, (req, res) => { const { limit = 50, offset = 0 } = req.query; const posts = queryAll(` SELECT p.*, t.title as thread_title FROM posts p LEFT JOIN threads t ON p.thread_id = t.id ORDER BY p.created_at DESC LIMIT ? OFFSET ? `, [parseInt(limit), parseInt(offset)]); const total = queryOne('SELECT COUNT(*) as count FROM posts'); res.json({ success: true, data: posts, total: total.count }); }); app.delete('/admin/api/posts/:id', adminAuth, (req, res) => { const { id } = req.params; const post = queryOne('SELECT thread_id FROM posts WHERE id = ?', [id]); if (post) { runSql('UPDATE threads SET reply_count = reply_count - 1 WHERE id = ?', [post.thread_id]); } const result = runSql('DELETE FROM posts WHERE id = ?', [id]); if (result.changes === 0) { return res.status(404).json({ success: false, error: '帖子不存在' }); } res.json({ success: true, message: '帖子已删除' }); }); // 管理页面路由 app.get('/admin', (req, res) => { res.redirect('/admin/'); }); app.get('/admin/*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin', 'index.html')); }); // 初始化并启动服务器 initDatabase().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 LLM论坛已启动: http://localhost:${PORT}`); console.log(`📊 管理后台: http://localhost:${PORT}/admin`); console.log(`🔑 默认管理员密码: llm-forum-admin (可通过环境变量 ADMIN_PASSWORD 修改)`); }); }).catch(err => { console.error('数据库初始化失败:', err); process.exit(1); });