import express from 'express'; import type { Request, Response, NextFunction } from 'express'; import fs from 'fs'; import path from 'path'; import cors from "cors"; import multer from "multer"; import * as douyin from './douyin' import { sendMediaMsg, sendMsg } from './lib/qq'; import * as llm from './llm' import { testNetwork } from './lib/network'; const app = express(); const PORT = process.env.PORT || 6100; const __dirname = path.dirname(new URL(import.meta.url).pathname); app.use(cors()); // 仅解析明确类型;不要用 '*/*' app.use(express.json({ limit: '10mb', type: ['application/json'] })); app.use(express.urlencoded({ extended: true, limit: '10mb', type: ['application/x-www-form-urlencoded'] })); // QQ机器人回显功能 app.post('/', async (req: Request, res: Response) => { // 检查是否是消息类型的请求 if (!req.body || req.body.post_type != 'message') return const msgData = req.body as MessagePayload const text = msgData.message.filter(m => m.type === 'text').map(m => m.data.text).join("\n").trim(); const target_id = String(msgData.target_id); console.log(`\n[QQ机器人] 收到消息:`); // console.log(JSON.stringify(req.body, null, 0)); console.log(`消息内容: ${msgData.raw_message}`); // Match Douyin URL // Like: https://v.douyin.com/YqgJL_phY_k/ const douyinUrlPattern = /https?:\/\/v\.douyin\.com\/[a-zA-Z0-9_-]+/; const douyinMatch = text.match(douyinUrlPattern); try { // 如果检测到抖音链接,调用解析API if (douyinMatch) { const douyinUrl = douyinMatch[0]; console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`); sendMsg(`[抖音链接检测] 发现抖音链接: ${douyinUrl},启动 Chromium 中...`, target_id); douyin.downloadDouyinMedia(douyinUrl, target_id); return } if (text.startsWith('/reset')) { llm.resetChat(target_id) return } // 使用 LLM 回答 llm.chat(msgData.message, target_id); } catch (error) { console.error(`[错误] 处理消息时发生错误:`, error); sendMsg(String(error), target_id); } }); function sanitizeDirname(input: string) { // 基础清洗:去除路径分隔符、控制字符,并截断长度(防止奇怪标题) const s = (input || "default") .trim() .replace(/[\\/]+/g, "_") .replace(/[<>:"|?*\u0000-\u001F]/g, "_") .slice(0, 32) || "default"; return s; } const storage = multer.diskStorage({ destination: (req, _file, cb) => { const title = sanitizeDirname(req.query.title as string); const baseDir = process.env.DOWNLOAD_DIR || path.join(__dirname, "downloads"); const dest = req.query.type == 'image' ? path.join(baseDir, title) : baseDir; fs.mkdir(dest, { recursive: true }, (err) => cb(err, dest)); }, filename: (req, file, cb) => { const ext = path.extname(file.originalname || ""); const title = sanitizeDirname(req.query.title as string); cb(null, `${title}_${Date.now()}_${Math.random().toString(36).slice(2)}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 1024 * 1024 * 1024, fields: 50, files: 50 }, fileFilter: (_req, file, cb) => { if (/^file_\d+$/i.test(file.fieldname)) cb(null, true); else cb(new Error(`Unexpected file field: ${file.fieldname}`)); } }); app.post("/upload", upload.any(), (req, res) => { const files = (req.files as Express.Multer.File[]) || []; const accepted = files.filter(f => /^file_\d+$/i.test(f.fieldname)); let meta = { title: req.body.title, type: req.query.type as 'video' | 'image', target_id: req.query.target_id }; console.log(`收到上传: ${accepted.length} 个文件`, files.map(f => f.path)); if (meta.target_id) { const totalSize = accepted.reduce((sum, f) => sum + f.size, 0); sendMsg(`[抖音下载] ${meta.title},已下载 ${accepted.length} 个文件,类型 ${meta.type},共 ${(totalSize / 1024 / 1024).toFixed(2)} MB,上传中...`, meta.target_id as string); accepted.forEach(f => { sendMediaMsg(f.path, meta.target_id as string, meta.type); }) } res.json({ ok: true, files: accepted.length, meta }); }); app.use((error: any, req: Request, res: Response, next: NextFunction) => { const timestamp = new Date().toISOString(); console.error(`\n[${timestamp}] 错误:`, error); res.status(500).json({ success: false, message: '服务器内部错误', error: error.message }); }); // 启动服务器 app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); testNetwork(); });