From 8d9d1d5867898e794a4e94b1917da4a91b1510f2 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 28 Jun 2025 11:09:52 +0800 Subject: [PATCH] feat: Douyin video reslove and download --- bun.lock | 3 + index.ts | 248 ++++++++++++++++++++++++++------------------------ package.json | 1 + pm2.config.js | 6 ++ 4 files changed, 140 insertions(+), 118 deletions(-) create mode 100644 pm2.config.js diff --git a/bun.lock b/bun.lock index e7535f5..47cf18e 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "qq-bot", "dependencies": { + "dotenv": "^17.0.0", "express": "^5.1.0", }, "devDependencies": { @@ -64,6 +65,8 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "dotenv": ["dotenv@17.0.0", "", {}, "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], diff --git a/index.ts b/index.ts index 100bd23..d97c550 100644 --- a/index.ts +++ b/index.ts @@ -1,8 +1,10 @@ import express from 'express'; import type { Request, Response, NextFunction } from 'express'; +import fs from 'fs'; +import path from 'path'; const app = express(); -const PORT = process.env.PORT || 3000; +const PORT = process.env.PORT || 6100; // 中间件:解析 JSON 请求体 app.use(express.json({ limit: '10mb' })); @@ -14,9 +16,9 @@ app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(express.raw({ type: '*/*', limit: '10mb' })); // 自定义中间件:打印所有请求的详细信息 -app.use((req: Request, res: Response, next: NextFunction) => { +/* app.use((req: Request, res: Response, next: NextFunction) => { const timestamp = new Date().toISOString(); - + console.log('\n' + '='.repeat(80)); console.log(`[${timestamp}] 收到请求:`); console.log(`方法: ${req.method}`); @@ -24,7 +26,7 @@ app.use((req: Request, res: Response, next: NextFunction) => { console.log(`完整URL: ${req.originalUrl}`); console.log(`查询参数:`, req.query); console.log(`请求头:`, req.headers); - + // 打印请求体(如果存在) if (req.body) { if (Buffer.isBuffer(req.body)) { @@ -35,131 +37,149 @@ app.use((req: Request, res: Response, next: NextFunction) => { console.log(`请求体:`, req.body); } } - + console.log(`客户端IP: ${req.ip}`); console.log(`User-Agent: ${req.get('User-Agent')}`); console.log('='.repeat(80)); - + next(); -}); +}); */ // 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 { - // 检查是否是消息类型的请求 - if (req.body && req.body.post_type === 'message') { - 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}`); - - // 构建回显消息体 - const replyMessage = { - user_id: String(target_id || user_id), - message: [ - { - type: "text", - data: { - text: raw_message || "收到空消息" - } - } - ] - }; - - // 发送回显消息 - 使用请求来源的IP地址 - const clientIP = req.ip || req.connection.remoteAddress || req.socket.remoteAddress; - // 清理IPv6映射的IPv4地址格式 - const cleanIP = clientIP?.replace(/^::ffff:/, '') || 'localhost'; - const replyUrl = `http://${cleanIP}:30000/send_private_msg`; - - console.log(`[QQ机器人] 发送回显消息到: ${replyUrl}`); - console.log(`[QQ机器人] 回显内容:`, JSON.stringify(replyMessage, null, 2)); - - // 使用 fetch 发送回显消息 - const response = await fetch(replyUrl, { - method: 'POST', + // 如果检测到抖音链接,调用解析API + if (douyinMatch) { + const douyinUrl = douyinMatch[0]; + console.log(`[抖音链接检测] 发现抖音链接: ${douyinUrl}`); + + // 调用抖音视频解析API + const apiUrl = `http://localhost:6101/api/hybrid/video_data?url=${encodeURIComponent(douyinUrl)}`; + console.log(`[抖音解析] 调用API: ${apiUrl}`); + + const apiResponse = await fetch(apiUrl, { + method: 'GET', headers: { 'Content-Type': 'application/json', - }, - body: JSON.stringify(replyMessage) + } }); - - if (response.ok) { - const responseData = await response.text(); - console.log(`[QQ机器人] 回显成功! 响应:`, responseData); - - res.status(200).json({ - success: true, - message: '消息已回显', - echo_sent: true, - original_message: raw_message, - target_user: target_id || user_id - }); - } else { - console.error(`[QQ机器人] 回显失败! HTTP状态:`, response.status); - const errorText = await response.text(); - console.error(`[QQ机器人] 错误响应:`, errorText); - - res.status(200).json({ - success: true, - message: '消息已接收,但回显失败', - echo_sent: false, - error: `HTTP ${response.status}: ${errorText}`, - original_message: raw_message - }); + + if (!apiResponse.ok) + throw new Error(`抖音API调用失败: ${apiResponse.status} ${apiResponse.statusText}`); + + let vD: any = await apiResponse.json(); + + if (!vD.data) + throw new Error('抖音API返回数据格式错误'); + + vD = vD.data; + + // 发送视频信息 + sendMsg(`[抖音下载] ${vD.author.nickname}: ${vD.caption}\n`, target_id); + + // 下载视频 + const downloadUrl = `http://localhost:6101/api/download?url=${encodeURIComponent(douyinUrl)}&prefix=true&with_watermark=false`; + console.log(`[抖音下载] 调用下载API: ${downloadUrl}`); + + const downloadResponse = await fetch(downloadUrl); + if (!downloadResponse.ok) { + throw new Error(`视频下载失败: ${downloadResponse.status} ${downloadResponse.statusText}`); } - } else { - // 非消息类型的请求,只记录不回显 - console.log(`\n[QQ机器人] 收到非消息类型请求: ${req.body?.post_type || '未知类型'}`); - - res.status(200).json({ - success: true, - message: '请求已接收(非消息类型,不回显)', - post_type: req.body?.post_type || 'unknown' - }); + + // 创建下载目录 + const downloadDir = process.env.DOWNLOAD_DIR || path.join(__dirname, 'downloads'); + if (!fs.existsSync(downloadDir)) { + fs.mkdirSync(downloadDir, { recursive: true }); + } + + // 生成文件名 + const timestamp = Date.now(); + // 清理文件名中的非法字符 + const cleanCaption = vD.caption.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50); + const fileName = `${cleanCaption}_${timestamp}.mp4`; + const filePath = path.join(downloadDir, fileName); + + // 保存视频文件 + console.log(`[抖音下载] 保存视频到: ${filePath}`); + const buffer = await downloadResponse.arrayBuffer(); + fs.writeFileSync(filePath, Buffer.from(buffer)); + + // 发送视频消息 + console.log(`[抖音发送] 发送视频文件`); + await sendVideoMsg(filePath, target_id); + } } catch (error) { - console.error(`\n[QQ机器人] 处理请求时发生错误:`, error); - - res.status(500).json({ - success: false, - message: '处理请求时发生错误', - error: error instanceof Error ? error.message : '未知错误' - }); + sendMsg(String(error), target_id); } + }); -/* // 捕获所有其他路径的处理器 -app.use((req: Request, res: Response) => { - const responseData = { - timestamp: new Date().toISOString(), - method: req.method, - path: req.path, - originalUrl: req.originalUrl, - query: req.query, - headers: req.headers, - body: req.body, - ip: req.ip, - userAgent: req.get('User-Agent'), - message: '请求已接收并记录' - }; - - console.log(`\n[通用处理器] 向客户端返回确认信息`); - - // 返回 JSON 响应 - res.status(200).json({ - success: true, - message: '请求已成功接收并记录到控制台', - data: responseData - }); -}); */ +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`; + + return fetch(replyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(replyMessage) + }); +} + +function sendVideoMsg(filePath: string, target_id: string) { + const videoMessage = { + user_id: String(target_id), + message: [ + { + type: "video", + data: { + file: `file://${filePath}` + } + } + ] + } + + const replyUrl = `http://localhost:30000/send_private_msg`; + + return fetch(replyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(videoMessage) + }); +} -// 错误处理中间件 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: '服务器内部错误', @@ -169,14 +189,6 @@ app.use((error: any, req: Request, res: Response, next: NextFunction) => { // 启动服务器 app.listen(PORT, () => { - console.log(`\n🚀 HTTP 调试服务器已启动!`); - console.log(`📡 监听端口: ${PORT}`); - console.log(`🌐 访问地址: http://localhost:${PORT}`); - console.log(`📝 所有请求都会在控制台显示详细信息`); - console.log(`\n支持的测试方法:`); - console.log(` GET: curl http://localhost:${PORT}/test`); - console.log(` POST: curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://localhost:${PORT}/api/test`); - console.log(` PUT: curl -X PUT -H "Content-Type: application/json" -d '{"data":"test"}' http://localhost:${PORT}/update`); - console.log(` DELETE: curl -X DELETE http://localhost:${PORT}/delete/123`); - console.log(`\n按 Ctrl+C 停止服务器\n`); + console.log(`Server is running on http://localhost:${PORT}`); + console.log(`With env:`, { DOWNLOAD_DIR: process.env.DOWNLOAD_DIR, PORT: process.env.PORT }); }); \ No newline at end of file diff --git a/package.json b/package.json index 681f49c..4e4dbdd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "typescript": "^5" }, "dependencies": { + "dotenv": "^17.0.0", "express": "^5.1.0" } } \ No newline at end of file diff --git a/pm2.config.js b/pm2.config.js new file mode 100644 index 0000000..53abc57 --- /dev/null +++ b/pm2.config.js @@ -0,0 +1,6 @@ +export const name = "QQBotServer"; +export const script = "index.ts"; +export const interpreter = "bun"; +export const env = { + PATH: `${process.env.HOME}/.bun/bin:${process.env.PATH}`, // Add "~/.bun/bin/bun" to PATH +}; \ No newline at end of file