/** * Claude Chat REST API 路由 * 管理会话列表、获取历史消息、Sub-Agent 任务调度 */ const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const multer = require('multer'); const crypto = require('crypto'); const { service } = require('../services/claude-chat-service'); const { TaskQueue } = require('../services/task-queue'); const { requireAuth } = require('../middleware/jwt-auth'); const skillDb = require('../services/skill-db'); const config = require('../config'); // ── Multer 文件上传配置 ── const UPLOAD_MAX_SIZE = 10 * 1024 * 1024; // 10MB const UPLOAD_MAX_FILES = 5; const ALLOWED_MIMES = new Set([ // 图片 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', // 文档 'application/pdf', 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/xml', // 代码 'application/json', 'application/javascript', 'text/javascript', 'application/xml', 'application/x-yaml', 'text/yaml', // 表格 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', ]); const uploadStorage = multer.diskStorage({ destination: (req, file, cb) => { const userId = req.user?.id || 'anonymous'; const uploadDir = path.join( path.resolve(__dirname, '../../conversations'), `user-${userId}`, 'uploads' ); fs.mkdirSync(uploadDir, { recursive: true }); cb(null, uploadDir); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname); const hash = crypto.randomBytes(8).toString('hex'); cb(null, `${Date.now()}-${hash}${ext}`); } }); const upload = multer({ storage: uploadStorage, limits: { fileSize: UPLOAD_MAX_SIZE, files: UPLOAD_MAX_FILES }, fileFilter: (req, file, cb) => { // 宽松策略:允许常见 MIME 或文本类文件 if (ALLOWED_MIMES.has(file.mimetype) || file.mimetype.startsWith('text/')) { cb(null, true); } else { cb(new Error(`Unsupported file type: ${file.mimetype}`)); } } }); // 创建并启动 TaskQueue const taskQueue = new TaskQueue(service); taskQueue.start(); /** * GET /api/claude-chat/skills * 获取所有可用的业务 Skill(供 /skills 命令使用) */ router.get('/skills', requireAuth, (req, res) => { try { const db = skillDb.getDb(); const skills = db.prepare( `SELECT skill_name, description, domain, scene, layer, tags FROM skills WHERE status = 'enable' ORDER BY skill_name` ).all(); res.json(skills.map(s => ({ name: s.skill_name, description: s.description || '', domain: s.domain || '', scene: s.scene || '', layer: s.layer, tags: s.tags ? JSON.parse(s.tags) : [], }))); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/claude-chat/skills/:name/content * 获取指定 Skill 的 SKILL.md 内容(用于注入 system prompt) */ router.get('/skills/:name/content', requireAuth, (req, res) => { try { const { name } = req.params; const skillMdPath = path.join(config.skillsDataDir, name, 'SKILL.md'); if (!fs.existsSync(skillMdPath)) { return res.status(404).json({ error: `Skill "${name}" not found` }); } const content = fs.readFileSync(skillMdPath, 'utf-8'); res.json({ name, content }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * POST /api/claude-chat/upload * 上传文件(图片、文档、代码等) * * Returns: { files: [{ fileId, fileName, fileType, fileSize, url }] } */ router.post('/upload', requireAuth, (req, res) => { upload.array('files', UPLOAD_MAX_FILES)(req, res, (err) => { if (err) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: `File too large (max ${UPLOAD_MAX_SIZE / 1024 / 1024}MB)` }); } if (err.code === 'LIMIT_FILE_COUNT') { return res.status(400).json({ error: `Too many files (max ${UPLOAD_MAX_FILES})` }); } return res.status(400).json({ error: err.message }); } if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'No files uploaded' }); } const files = req.files.map(f => ({ fileId: path.basename(f.filename, path.extname(f.filename)), fileName: f.originalname, fileType: f.mimetype, fileSize: f.size, storedPath: f.path, url: `/api/claude-chat/uploads/${req.user.id}/${f.filename}`, })); res.json({ files }); }); }); /** * GET /api/claude-chat/uploads/:userId/:filename * 访问上传的文件(用于前端预览) */ router.get('/uploads/:userId/:filename', requireAuth, (req, res) => { // 只能访问自己的文件 if (String(req.user.id) !== req.params.userId) { return res.status(403).json({ error: 'Forbidden' }); } const filePath = path.join( path.resolve(__dirname, '../../conversations'), `user-${req.params.userId}`, 'uploads', req.params.filename ); if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'File not found' }); } res.sendFile(filePath); }); /** * GET /api/claude-chat/sessions * 获取所有活跃会话 */ router.get('/sessions', requireAuth, (req, res) => { try { const sessions = service.getSessionsByUser(req.user.id); res.json({ sessions }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/claude-chat/sessions/:sessionId * 获取会话详情 */ router.get('/sessions/:sessionId', requireAuth, (req, res) => { try { const { sessionId } = req.params; const session = service.getSession(sessionId); if (!session || session.userId !== String(req.user.id)) { return res.status(404).json({ error: 'Session not found' }); } res.json(session.getInfo()); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/claude-chat/sessions/:sessionId/messages * 获取会话消息历史 */ router.get('/sessions/:sessionId/messages', requireAuth, (req, res) => { try { const { sessionId } = req.params; const session = service.getSession(sessionId); if (!session || session.userId !== String(req.user.id)) { return res.status(404).json({ error: 'Session not found' }); } res.json({ messages: session.getMessages() }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * PATCH /api/claude-chat/sessions/:sessionId/title * 更新会话标题 */ router.patch('/sessions/:sessionId/title', requireAuth, (req, res) => { try { const { sessionId } = req.params; const { title } = req.body; const session = service.getSession(sessionId); if (!session || session.userId !== String(req.user.id)) { return res.status(404).json({ error: 'Session not found' }); } session.title = title || ''; session.updatedAt = new Date().toISOString(); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * DELETE /api/claude-chat/sessions/:sessionId * 删除会话 */ router.delete('/sessions/:sessionId', requireAuth, (req, res) => { try { const { sessionId } = req.params; const session = service.getSession(sessionId); if (!session || session.userId !== String(req.user.id)) { return res.status(404).json({ error: 'Session not found' }); } service.deleteSession(sessionId); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * POST /api/claude-chat/cleanup * 清理空闲会话 */ router.post('/cleanup', (req, res) => { try { service.cleanup(); res.json({ success: true, sessions: service.getSessions() }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * POST /api/claude-chat/send * 发送消息 — 15s 快速超时策略 * * Body: { message, sessionId?, provider? } * * Returns (同步, <15s): * { mode: 'sync', sessionId, result, is_error, duration_ms } * * Returns (异步, >15s): * { mode: 'async', taskId, sessionId, message } */ router.post('/send', requireAuth, async (req, res) => { try { const { message, sessionId, provider = 'claude' } = req.body; if (!message) { return res.status(400).json({ error: 'message is required' }); } const result = await taskQueue.sendWithTimeout(message, { sessionId, provider, userId: req.user.id }); if (result.mode === 'sync') { res.json({ mode: 'sync', sessionId: result.sessionId, result: result.result?.result || '', is_error: result.result?.is_error || false, duration_ms: result.result?.duration_ms, }); } else { res.json(result); } } catch (error) { console.error('POST /api/claude-chat/send error:', error); res.status(500).json({ error: error.message }); } }); /** * GET /api/claude-chat/tasks * 查询任务列表 * * Query: ?sessionId=xxx&status=running&limit=20 */ router.get('/tasks', (req, res) => { try { const { sessionId, status, limit } = req.query; const tasks = taskQueue.listTasks({ sessionId: sessionId ? String(sessionId) : undefined, status: status ? String(status) : undefined, limit: limit ? parseInt(String(limit)) : 50, }); res.json({ tasks }); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * GET /api/claude-chat/tasks/:taskId * 查询单个任务状态 + 结果 */ router.get('/tasks/:taskId', (req, res) => { try { const task = taskQueue.getTask(req.params.taskId); if (!task) { return res.status(404).json({ error: 'Task not found' }); } res.json(task); } catch (error) { res.status(500).json({ error: error.message }); } }); module.exports = router;