133 lines
4.4 KiB
TypeScript
133 lines
4.4 KiB
TypeScript
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'
|
||
|
||
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 } = req.body as { target_id: string, raw_message: string, message_type: string, user_id: string };
|
||
|
||
console.log(`\n[QQ机器人] 收到${message_type}消息`);
|
||
console.log(`发送者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);
|
||
return
|
||
}
|
||
if (raw_message.startsWith('/reset')) {
|
||
llm.resetChat(target_id)
|
||
return
|
||
}
|
||
|
||
// 使用 LLM 回答
|
||
llm.chat(raw_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, 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,
|
||
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}`);
|
||
}); |