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' 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 { target_id, raw_message, message_type, user_id } = req.body; console.log(`\n[QQ机器人] 收到${message_type}消息`); console.log(`发送者ID: ${user_id || target_id}`); console.log(`消息内容: ${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 = raw_message.match(douyinUrlPattern); try { // 如果检测到抖音链接,调用解析API if (douyinMatch) { const douyinUrl = douyinMatch[0]; console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`); sendMsg(`[抖音链接检测] 发现抖音链接: ${douyinUrl},启动 Chromium 中...`, target_id); douyin.downloadDouyinMedia(douyinUrl, 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, 128) || "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, // 限制:单文件最大 1GB、最多 50 个字段(可按需调整) limits: { fileSize: 1024 * 1024 * 1024, fields: 50, files: 50 }, fileFilter: (_req, file, cb) => { // 只接受字段名以 file_ 开头的文件,其他拒绝 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) => { // 1) 取出文件:multer.any() 把所有文件放在 req.files const files = (req.files as Express.Multer.File[]) || []; // 只保留我们关心的 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(meta); 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 }); }); function sendMsg(msg: string, target_id: string) { const replyMessage = { user_id: String(target_id), message: [ { type: "text", data: { text: msg } } ] } const replyUrl = `http://localhost:30000/send_private_msg`; console.log(`[发送消息] ${msg} -> ${target_id}`); return fetch(replyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(replyMessage) }); } function sendMediaMsg(filePath: string, target_id: string, type: 'video' | 'image') { const mediaMessage = { user_id: String(target_id), message: [ { type: type, data: { file: `file://${filePath}` } } ] } const replyUrl = `http://localhost:30000/send_private_msg`; console.log(`[发送媒体消息] ${type} - ${filePath} -> ${target_id}`); return fetch(replyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(mediaMessage) }); } 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}`); console.log(`With env:`, { DOWNLOAD_DIR: process.env.DOWNLOAD_DIR, PORT: process.env.PORT }); });