claude-chat.js 9.73 KB
/**
 * 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;