Files

1 line
91 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "barvewarrio/lyrics-replacer", "files": {"backend/services/ai.js": {"757b6fb841cfb636d172057d2b28996255f2c9fe": {"t": 1785768977.288575, "n": 22925, "h": "cec89cec17bec775", "c": "import axios from 'axios';\nimport fs from 'fs/promises';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport dotenv from 'dotenv';\n\ndotenv.config();\n\n// Constants\nconst API_PROVIDER = process.env.AI_PROVIDER || 'deepseek';\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst OPENAI_API_KEY = process.env.OPENAI_API_KEY;\nconst USE_AI = process.env.USE_AI === 'true' || false;\n\n// MiniMax API 配置 (复用音乐生成的 key)\nconst MINIMAX_API_KEY = process.env.MINIMAX_API_KEY;\nconst MINIMAX_API_URL = 'https://api.minimaxi.com/v1/lyrics_generation';\n\n// AIHubMix API (用于歌词生成)\nconst AIHUBMIX_API_KEY = process.env.AIHUBMIX_API_KEY;\nconst AIHUBMIX_API_URL = 'https://api.aihubmix.com/v1/chat/completions';\n\n// Set up __dirname equivalent in ESM\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Load prompt template\nconst loadPrompt = async () => {\n try {\n const promptPath = path.join(__dirname, '../prompt.md');\n const promptTemplate = await fs.readFile(promptPath, 'utf-8');\n return promptTemplate;\n } catch (error) {\n console.error('Error loading prompt template:', error);\n throw new Error('Failed to load AI prompt template');\n }\n};\n\n// Build final prompt with customization options\nconst buildFinalPrompt = (templatePrompt, options) => {\n const { style, keywords, mood, title, artist } = options;\n\n let finalPrompt = templatePrompt;\n\n // Add song information if available\n if (title || artist) {\n finalPrompt += `\\n\\n歌曲信息: ${title || '未知标题'} - ${artist || '未知歌手'}`;\n }\n\n // Add style information\n finalPrompt += `\\n\\n目标风格: ${style || '现代流行'}`;\n\n // Add keywords if available\n if (keywords && keywords.length > 0) {\n finalPrompt += `\\n\\n需要融入的关键词: ${keywords.join(', ')}`;\n }\n\n // Add mood information\n if (mood) {\n const moodDescription = mood > 7 ? '积极正向' : (mood < 4 ? '消极忧伤' : '中性平和');\n finalPrompt += `\\n\\n情感倾向: ${moodDescription} (${mood}/10)`;\n }\n\n // Add final instructions\n finalPrompt += `\\n\\n请根据以上要求,创作出符合指定风格和要求的替代歌词,保持原歌词的韵律和结构。以下是原歌词:`;\n\n return finalPrompt;\n};\n\n// 构建歌词风格描述,优先保留用户选择的原始风格与场景\nconst buildLyricsStyleInstruction = ({ style = '', scene = '' } = {}) => {\n const normalizedStyle = (style || '').trim();\n const sceneDescriptions = {\n kitchen: '厨房灾难现场,做饭时的各种意外和搞笑场景',\n gym: '健身房翻车实录,健身时的各种尴尬和失败经历',\n shopping: '网购拆箱翻车,网购商品与预期不符的搞笑场景',\n haircut: '理发店惊魂记,理发时的各种意外和尴尬',\n meeting: '会议室迷惑行为,开会时的各种尴尬和搞笑场景',\n dating: '约会翻车现场,约会时的各种意外和尴尬',\n pet: '宠物拆家日常,宠物捣乱时的各种搞笑场景',\n social: '社交软件翻车,使用社交软件时的各种尴尬',\n exam: '考试翻车现场,考试时的各种意外和搞笑场景',\n roadtrip: '自驾游迷惑操作,自驾游时的各种意外和搞笑场景'\n };\n\n const styleDescriptions = {\n '幽默搞笑': '幽默搞笑,轻松诙谐,让人忍俊不禁',\n '搞笑': '幽默搞笑,轻松诙谐,让人忍俊不禁',\n '浪漫抒情': '浪漫抒情,细腻表达爱情与内心情感',\n '现代都市': '现代都市,贴近现实生活与城市情感',\n '奇幻想象': '奇幻想象,融入超现实、魔法或未来元素',\n '自然田园': '自然田园,描绘山水四季与乡野氛围',\n '怀旧复古': '怀旧复古,带有过去时光与经典年代感',\n '励志奋进': '励志奋进,充满奋斗、成长与希望',\n '哲理思考': '哲理思考,探讨人生、社会与内心成长',\n '赛博朋克': '赛博朋克,高科技低生活的未来都市感',\n '古风': '古典优雅,诗意盎然,富有中国传统文化韵味',\n classic: '古典优雅,诗意盎然,富有中国传统文化韵味',\n '现代': '现代都市,贴近生活,情感丰富',\n modern: '现代都市,贴近生活,情感丰富',\n '摇滚': '热情澎湃,充满力量与激情',\n rock: '热情澎湃,充满力量与激情',\n '浪漫': '甜蜜温柔,表达爱情与美好情感',\n romantic: '甜蜜温柔,表达爱情与美好情感',\n '流行': '朗朗上口,易于传唱,符合大众审美',\n pop: '朗朗上口,易于传唱,符合大众审美',\n '嘻哈': '节奏感强,押韵有活力',\n hiphop: '节奏感强,押韵有活力'\n };\n\n const parts = [];\n if (normalizedStyle) {\n parts.push(styleDescriptions[normalizedStyle] ? `${normalizedStyle}(${styleDescriptions[normalizedStyle]})` : normalizedStyle);\n }\n if (scene && sceneDescriptions[scene]) {\n parts.push(`必须围绕“${sceneDescriptions[scene]}”展开`);\n }\n\n return parts.join(',') || '现代流行风格';\n};\n\n// ============================================\n// MiniMax 歌词生成 API\n// ============================================\n\n/**\n * 使用 MiniMax API 生成歌词\n * @param {Object} options - 生成选项\n * @param {string} options.mode - 'write_full_song' 或 'edit'\n * @param {string} options.prompt - 提示词/指令\n * @param {string} options.lyrics - 现有歌词(edit模式)\n * @param {string} options.title - 歌曲标题\n * @param {string} options.style - 风格描述\n */\nexport const generateLyricsWithMiniMax = async (options) => {\n const {\n mode = 'edit',\n prompt = '',\n lyrics = '',\n title = '',\n style = ''\n } = options;\n\n if (!MINIMAX_API_KEY) {\n throw new Error('MiniMax API Key 未配置');\n }\n\n console.log('调用 MiniMax 歌词生成 API...');\n console.log('模式:', mode);\n console.log('标题:', title);\n console.log('风格:', style);\n console.log('原歌词长度:', lyrics.length);\n\n try {\n // 构建提示词\n let fullPrompt = prompt;\n if (style) {\n fullPrompt += `,风格:${style}`;\n }\n if (title) {\n fullPrompt += `,歌曲名:${title}`;\n }\n\n const requestBody = {\n mode: mode,\n prompt: fullPrompt || undefined\n };\n\n // edit 模式需要传入现有歌词\n if (mode === 'edit' && lyrics) {\n requestBody.lyrics = lyrics;\n }\n\n if (title) {\n requestBody.title = title;\n }\n\n console.log('请求体:', JSON.stringify(requestBody, null, 2));\n\n const response = await axios.post(\n MINIMAX_API_URL,\n requestBody,\n {\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${MINIMAX_API_KEY}`\n },\n timeout: 120000 // 2分钟超时\n }\n );\n\n console.log('MiniMax 歌词 API 响应状态:', response.status);\n\n if (response.data.base_resp && response.data.base_resp.status_code !== 0) {\n const errorMsg = response.data.base_resp.status_msg;\n console.error('MiniMax 歌词生成错误:', errorMsg);\n throw new Error(errorMsg || '歌词生成失败');\n }\n\n const result = {\n success: true,\n title: response.data.song_title,\n style: response.data.style_tags,\n lyrics: response.data.lyrics\n };\n\n console.log('歌词生成成功,标题:', result.title);\n console.log('风格:', result.style);\n console.log('歌词长度:', result.lyrics.length);\n\n return result;\n\n } catch (error) {\n console.error('MiniMax 歌词生成失败:', error.message);\n throw error;\n }\n};\n\n// ============================================\n// AIHubMix 歌词生成 (使用 OpenAI 兼容格式)\n// ============================================\n\n/**\n * 使用 AIHubMix API 生成歌词\n */\nexport const generateLyricsWithAIHubMix = async (options) => {\n const {\n originalLyrics = '',\n style = '',\n title = '',\n artist = '',\n scene = ''\n } = options;\n\n if (!AIHUBMIX_API_KEY) {\n throw new Error('AIHubMix API Key 未配置');\n }\n\n console.log('调用 AIHubMix 歌词生成 API...');\n console.log('风格:', style);\n console.log('标题:', title);\n\n try {\n const styleInstruction = buildLyricsStyleInstruction({ style, scene });\n\n const systemPrompt = `你是一位专业的中文歌词创作者。请根据用户提供的原歌词,改写成指定风格的新歌词。\n\n要求:\n1. 保留原歌词的结构(行数、时间戳、段落格式)\n2. 改写后的歌词必须严格符合用户指定的风格与场景\n3. 每行歌词的字数尽量与原歌词相近\n4. 保持歌词的押韵和节奏感\n5. 只输出歌词内容,不要任何解释说明\n6. 输出格式与原歌词格式一致(保留[00:00.00]等时间标签)\n7. 如果指定了搞笑场景,歌词内容必须具体写到该场景中的动作、物品和情节\n\n重要:只输出歌词,不要输出其他内容。`;\n\n const userPrompt = `请将以下歌词改写成【${styleInstruction}】风格:\n\n歌曲标题:${title || '未命名'}\n歌手:${artist || '未知歌手'}\n\n原歌词:\n${originalLyrics}\n\n请只输出改写后的歌词,保留原格式。`;\n\n console.log('发送请求到 AIHubMix...');\n\n const response = await axios.post(\n AIHUBMIX_API_URL,\n {\n model: 'gpt-3.5-turbo',\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt }\n ],\n temperature: 0.8,\n max_tokens: 4000\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${AIHUBMIX_API_KEY}`\n },\n timeout: 120000\n }\n );\n\n console.log('AIHubMix API 响应状态:', response.status);\n\n const generatedLyrics = response.data.choices[0].message.content.trim();\n\n console.log('歌词生成成功,长度:', generatedLyrics.length);\n\n return {\n success: true,\n title: title,\n style: styleInstruction,\n lyrics: generatedLyrics\n };\n\n } catch (error) {\n console.error('AIHubMix 歌词生成失败:', error.message);\n throw error;\n }\n};\n\n// ============================================\n// DeepSeek 歌词生成\n// ============================================\n\n// Generate lyrics using DeepSeek API\nconst generateWithDeepSeek = async (prompt, originalLyrics) => {\n try {\n const response = await axios.post(\n 'https://api.deepseek.com/v1/chat/completions',\n {\n model: 'deepseek-chat',\n messages: [\n { role: 'system', content: prompt },\n { role: 'user', content: originalLyrics }\n ],\n temperature: 0.7,\n max_tokens: 4000\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`\n }\n }\n );\n\n return response.data.choices[0].message.content;\n } catch (error) {\n console.error('DeepSeek API error:', error.response?.data || error.message);\n throw new Error('Failed to generate lyrics with DeepSeek API');\n }\n};\n\n// Generate lyrics using OpenAI API\nconst generateWithOpenAI = async (prompt, originalLyrics) => {\n try {\n const response = await axios.post(\n 'https://api.openai.com/v1/chat/completions',\n {\n model: 'gpt-3.5-turbo',\n messages: [\n { role: 'system', content: prompt },\n { role: 'user', content: originalLyrics }\n ],\n temperature: 0.7,\n max_tokens: 2000\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${OPENAI_API_KEY}`\n }\n }\n );\n\n return response.data.choices[0].message.content;\n } catch (error) {\n console.error('OpenAI API error:', error.response?.data || error.message);\n throw new Error('Failed to generate lyrics with OpenAI API');\n }\n};\n\n// Local fallback generator\nconst generateLocal = (originalLyrics, options = {}) => {\n console.log('Using local generation with options:', options);\n\n const { style = 'modern', scene = '', title = '', artist = '' } = options;\n\n // 风格名称映射 (前端中文名 -> 内部键)\n const styleMapping = {\n 'classic': 'classic', '古风': 'classic',\n 'modern': 'modern', '现代': 'modern',\n 'rock': 'rock', '摇滚': 'rock',\n 'hiphop': 'hiphop', '嘻哈': 'hiphop',\n 'literary': 'literary', '文艺': 'literary',\n 'pop': 'pop', '流行': 'pop',\n 'romantic': 'romantic', '浪漫': 'romantic',\n 'sad': 'sad', '悲伤': 'sad',\n '幽默搞笑': 'comedy', '搞笑': 'comedy', 'comedy': 'comedy'\n };\n\n const mappedStyle = styleMapping[style] || 'modern';\n\n const styleLexicons = {\n classic: ['旧梦', '相逢', '天涯', '回望', '流年', '长风'],\n modern: ['夜色', '晚风', '霓虹', '路口', '匆忙', '未央'],\n rock: ['炸响', '发烫', '上场', '点亮', '不降', '狂唱'],\n hiphop: ['上头', '开口', '拉满', '点头', '律动', '足够'],\n literary: ['纸页', '窗前', '字浅', '心远', '灯影', '余温'],\n pop: ['心跳', '明亮', '回响', '登场', '绵长', '合唱'],\n romantic: ['心动', '刚好', '拥抱', '围绕', '停靠', '发酵'],\n sad: ['夜凉', '回忆', '眼热', '沉默', '独想', '难忘'],\n comedy: ['笑场', '离谱', '发麻', '慌张', '乱套', '荒唐']\n };\n\n const sceneLexicons = {\n kitchen: ['锅铲乱响', '油点上墙', '盐又放多', '火候失常', '菜还没熟', '围裙遭殃'],\n gym: ['跑步太慌', '哑铃砸脚', '镜前逞强', '动作投降', '教练路过', '汗一路淌'],\n shopping: ['拆箱翻车', '同款不像', '尺码太小', '客服太忙', '退货发光', '购物车痒'],\n haircut: ['围布一披', '推子一响', '后背发凉', '刘海剪短', '镜子一照', '帽子戴上'],\n meeting: ['投影一开', '发言卡壳', '同事点头', '纪要像书', '老板提问', '散会才想'],\n dating: ['水先打翻', '话又拐弯', '气氛忽冷', '对视装稳', '手机乱响', '回家复盘'],\n pet: ['沙发被挠', '零食被翻', '拖鞋叼走', '眼神装软', '刚收拾完', '又来一遍'],\n social: ['消息想撤', '点开后悔', '语气不对', '已读不回', '表情救场', '越描越黑'],\n exam: ['试卷刚发', '脑袋空白', '选择都像', '草稿写乱', '铃声一响', '交卷才会'],\n roadtrip: ['导航带偏', '服务区停', '后备箱满', '风景还行', '油表见底', '天黑才到']\n };\n\n const toneLexicons = styleLexicons[mappedStyle] || styleLexicons.modern;\n const sceneLines = sceneLexicons[scene] || toneLexicons;\n console.log('映射后风格:', mappedStyle, '场景:', scene || 'default');\n\n const fitToLength = (text, targetLength) => {\n if (targetLength <= 0) return text;\n const result = text.replace(/[,。、!]/g, '');\n return result.length > targetLength ? result.slice(0, targetLength) : result;\n };\n\n const pickBestCandidate = (targetLength, candidates) => {\n const cleanedCandidates = candidates\n .map(item => (item || '').replace(/[,。、!]/g, ''))\n .filter(Boolean);\n\n const exactOrShorter = cleanedCandidates\n .filter(item => item.length <= targetLength)\n .sort((a, b) => b.length - a.length);\n\n if (exactOrShorter.length > 0) {\n return exactOrShorter[0];\n }\n\n const nearestLonger = cleanedCandidates\n .sort((a, b) => a.length - b.length)[0];\n\n return fitToLength(nearestLonger || cleanedCandidates[0] || '', targetLength);\n };\n\n const lines = originalLyrics.split('\\n');\n const newLines = [];\n let lineIndex = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const trimmedLine = line.trim();\n\n if (!trimmedLine) {\n newLines.push(line);\n continue;\n }\n\n const labelMatch = trimmedLine.match(/^((?:\\[.*?\\])+)/);\n const label = labelMatch ? labelMatch[1] : '';\n const content = label ? trimmedLine.substring(label.length) : trimmedLine;\n const cleanContent = content.trim();\n\n if (!cleanContent) {\n newLines.push(line);\n continue;\n }\n\n if (/^[^\\s]+\\s-\\s[^\\s]+$/.test(cleanContent)) {\n newLines.push(`${label}${title || cleanContent.split(' - ')[0]} - ${artist || cleanContent.split(' - ')[1]}`);\n continue;\n }\n\n if (/^(作词|作曲|词:|曲:|编曲|监制|制作人)/.test(cleanContent)) {\n newLines.push(line);\n continue;\n }\n\n const targetLength = Math.max(4, cleanContent.replace(/[,。、!??]/g, '').length);\n const primary = sceneLines[lineIndex % sceneLines.length];\n const secondary = toneLexicons[(lineIndex + 1) % toneLexicons.length];\n const third = sceneLines[(lineIndex + 2) % sceneLines.length];\n\n const candidate = pickBestCandidate(\n targetLength,\n [\n primary,\n secondary,\n third,\n `${primary}${secondary}`,\n `${primary}${third}`,\n `${secondary}${third}`,\n `${primary}我却${secondary}`,\n `${primary}${third}还在${secondary}`\n ]\n );\n const newContent = fitToLength(candidate, targetLength);\n\n newLines.push(label + newContent);\n lineIndex++;\n }\n\n const result = newLines.join('\\n');\n console.log('生成结果前100字:', result.substring(0, 100));\n return result;\n};\n\n// Main lyrics generation function\nexport const generateLyrics = async (originalLyrics, options = {}) => {\n try {\n const { title, artist, theme, keywords, style, structure, lyricsIdea, useAI } = options;\n\n // 如果明确指定不使用 AI,或环境变量 USE_AI 为 false,使用本地生成\n if (useAI === false || (!useAI && !USE_AI)) {\n console.log('使用本地生成模式');\n return generateLocal(originalLyrics || '', options);\n }\n\n // 只有在 USE_AI=true 或 useAI=true 时才使用 AI\n if (!USE_AI && useAI !== true) {\n console.log('USE_AI 未启用且未指定 useAI,使用本地生成');\n return generateLocal(originalLyrics || '', options);\n }\n\n // 构建生成提示\n const prompt = `请根据以下要求创作歌词:\n\n歌曲信息:\n- 标题:${title || '未指定'}\n- 歌手:${artist || '未指定'}\n- 主题:${theme || '未指定'}\n- 关键词:${keywords?.join(', ') || '无'}\n- 风格:${style || '现代流行'}\n- 结构:${structure || 'verse-chorus'}\n${lyricsIdea ? `\\n创作思路:${lyricsIdea}` : ''}\n\n重要要求:\n1. 必须使用中文创作歌词,不要使用英文或其他语言\n2. 符合指定的主题和风格\n3. 包含提供的关键词\n4. 按照指定的结构布局\n5. 注意押韵和节奏感\n6. 情感表达真挚自然\n\n如果提供了原始歌词,请参考原始歌词的结构和韵律,但创作全新的内容。`;\n\n // 根据 API 提供商生成\n if (API_PROVIDER === 'deepseek' && DEEPSEEK_API_KEY) {\n return await generateWithDeepSeek(prompt, originalLyrics || '');\n } else if (API_PROVIDER === 'openai' && OPENAI_API_KEY) {\n return await generateWithOpenAI(prompt, originalLyrics || '');\n } else {\n return generateLocal(originalLyrics || '', options);\n }\n } catch (error) {\n console.error('生成歌词失败:', error);\n throw new Error('生成歌词失败');\n }\n};\n\n// 创建全新歌词的提示模板\nconst buildCreationPrompt = (options) => {\n const { title, artist, theme, style, structure, keywords } = options;\n\n let prompt = `# 歌词创作任务\n\n## 创作要求\n- 创作一首完整的原创歌词\n- 歌曲标题: ${title}\n- 演唱者: ${artist || '未知歌手'}\n- 主题: ${theme}\n- 风格: ${style || 'modern'}\n- 结构: ${structure || 'verse-chorus'}`;\n\n // 添加关键词\n if (keywords && keywords.length > 0) {\n prompt += `\\n- 需要包含的关键词: ${keywords.join(', ')}`;\n }\n\n // 添加结构说明\n prompt += `\\n\\n## 歌词结构说明`;\n\n if (structure === 'verse-chorus') {\n prompt += `\n- 主歌(Verse): 2段,每段4-6行\n- 副歌(Chorus): 1段,4-6行,需要有记忆点和情感高潮\n- 结构顺序: 主歌1 → 副歌 → 主歌2 → 副歌`;\n } else if (structure === 'verse-chorus-bridge') {\n prompt += `\n- 主歌(Verse): 2段,每段4-6行\n- 副歌(Chorus): 2段,每段4-6行\n- 桥段(Bridge): 1段,3-4行,提供情感转折或深化\n- 结构顺序: 主歌1 → 副歌 → 主歌2 → 副歌 → 桥段 → 副歌`;\n } else if (structure === 'verse-only') {\n prompt += `\n- 纯主歌结构: 3-4段,每段5-8行\n- 可以有细微的变化,但不需要明显的副歌部分`;\n } else {\n prompt += `\n- 自由结构: 创作一首结构自由的歌词,总长度在20-30行左右\n- 确保有情感起伏和故事发展`;\n }\n\n // 添加风格说明\n prompt += `\\n\\n## 风格说明`;\n\n const styleDescriptions = {\n romantic: '浪漫抒情,表达爱情与内心情感,可以是甜蜜告白、思念、离别或复杂情感',\n modern: '现代都市,描写都市生活、职场故事、城市情感等现代主题',\n fantasy: '奇幻想象,融入超现实元素,如魔法世界、异次元旅行、未来科技或神话传说',\n nature: '自然田园,描绘自然风光与乡村生活,四季变换、山水之美或返璞归真的生活态度',\n nostalgic: '怀旧复古,回忆过去时光,童年记忆、老物件、经典年代或历史场景',\n inspirational: '励志奋进,表达奋斗精神、不屈意志、追梦历程或成长蜕变',\n philosophical: '哲理思考,探讨人生意义、社会现象、价值观或心灵成长',\n cyberpunk: '赛博朋克,描绘高科技与低生活,未来都市、人工智能、虚拟现实或科技反思'\n };\n\n prompt += `\\n- ${styleDescriptions[style] || '创作一首符合主题的现代流行歌词,注重情感表达和故事性'}`;\n\n // 添加最终指示\n prompt += `\\n\\n## 输出要求\n- 直接输出完整歌词,不要包含任何解释或说明\n- 按照指定结构分段,每段之间留一个空行\n- 不要标注\"主歌\"、\"副歌\"等标签\n- 确保歌词通顺、有意境,符合现代流行歌曲的表达方式\n- 总字数控制在200-300字左右\n\n请开始创作:`;\n\n return prompt;\n};\n\n// 创建全新歌词\nexport const createLyrics = async (options = {}) => {\n try {\n // Check if AI generation is enabled\n if (!USE_AI) {\n console.log('Using local lyrics creation');\n return generateLocalCreation(options);\n }\n\n // Build creation prompt\n const creationPrompt = buildCreationPrompt(options);\n console.log('创建歌词提示:', creationPrompt);\n\n // Generate lyrics based on selected provider\n if (API_PROVIDER === 'deepseek' && DEEPSEEK_API_KEY) {\n console.log('Using DeepSeek API for lyrics creation');\n return await generateWithDeepSeek(creationPrompt, '');\n } else if (API_PROVIDER === 'openai' && OPENAI_API_KEY) {\n console.log('Using OpenAI API for lyrics creation');\n return await generateWithOpenAI(creationPrompt, '');\n } else {\n console.log('No valid API configuration found, using local creation');\n return generateLocalCreation(options);\n }\n } catch (error) {\n console.error('Error creating lyrics:', error);\n throw new Error('Failed to create new lyrics');\n }\n};\n\n// 本地创建歌词(备用方案)\nconst generateLocalCreation = (options = {}) => {\n console.log('Using local lyrics creation with options:', options);\n\n const { title, theme, style } = options;\n\n // 根据主题和风格生成不同的模板歌词\n if (style === 'romantic') {\n return `星空下的约定\n\n当繁星点亮夜空\n我想起你的眼眸\n那闪烁的光芒\n照亮我心中的角落\n\n每一次与你相遇\n都像是命中注定\n时间在我们之间流淌\n却带不走心中的温柔\n\n无论相隔多远\n我的心始终为你跳动\n就像星星守护着月亮\n永不分离\n\n让我们在星空下许下约定\n用一生去兑现\n无论风雨如何变幻\n我们的爱永不改变`;\n } else if (style === 'inspirational') {\n return `追逐梦想\n\n站在起点的那一刻\n心中充满了不安\n未知的道路在前方延伸\n却依然选择勇敢前行\n\n跌倒了再爬起\n失败了再尝试\n每一次挫折都是礼物\n让我变得更加坚强\n\n追逐梦想的路上\n没有捷径可走\n只有不断努力\n才能看见希望的光\n\n即使前方荆棘密布\n我依然不会停下脚步\n因为我知道\n梦想的花终将绽放`;\n } else {\n // 默认现代风格\n return `城市的声音\n\n霓虹灯闪烁的夜晚\n车流穿梭在街道间\n我站在高楼之上\n聆听这座城市的声音\n\n忙碌的脚步声\n此起彼伏的交谈\n构成了都市的交响乐\n演奏着生活的旋律\n\n在这钢筋水泥的森林\n我们寻找着自己的位置\n迷失又找寻\n孤独又相连\n\n城市啊,你是冷漠的\n却也包容着每个灵魂\n让我们在这里相遇\n编织属于自己的故事`;\n }\n};\n\n// 获取推荐关键词\nexport const getRecommendedKeywords = async (theme, style) => {\n try {\n if (!USE_AI) {\n // 本地关键词推荐\n const localKeywords = {\n romantic: ['爱情', '思念', '温柔', '浪漫', '心跳', '拥抱', '微笑', '甜蜜'],\n modern: ['都市', '生活', '梦想', '奋斗', '压力', '希望', '改变', '未来'],\n fantasy: ['梦境', '星空', '魔法', '童话', '想象', '奇迹', '飞翔', '神秘'],\n nature: ['山川', '河流', '阳光', '花草', '季节', '风雨', '大地', '生命'],\n nostalgic: ['回忆', '青春', '岁月', '故事', '时光', '往事', '怀念', '成长'],\n inspirational: ['勇气', '力量', '信念', '坚持', '胜利', '拼搏', '追求', '成功'],\n philosophical: ['人生', '真理', '智慧', '探索', '意义', '哲思', '永恒', '命运'],\n cyberpunk: ['科技', '未来', '机械', '虚拟', '霓虹', '数字', '黑客', '都市']\n };\n\n const themeKeywords = theme ? [theme] : [];\n const styleKeywords = style && localKeywords[style] ? localKeywords[style].slice(0, 5) : [];\n\n return [...themeKeywords, ...styleKeywords];\n }\n\n // 使用 AI API 生成推荐关键词\n const prompt = `请根据以下主题和风格,推荐5-8个相关的歌词关键词:\n主题:${theme || '无特定主题'}\n风格:${style || '现代流行'}\n\n要求:\n1. 关键词要简洁有力,通常是2-4个汉字\n2. 关键词要符合主题和风格特点\n3. 关键词之间要有关联性\n4. 只需要返回关键词列表,用逗号分隔\n\n示例输出格式:\n心跳,微笑,拥抱,温柔,浪漫,思念`;\n\n let response;\n if (API_PROVIDER === 'deepseek' && DEEPSEEK_API_KEY) {\n response = await generateWithDeepSeek(prompt, '');\n } else if (API_PROVIDER === 'openai' && OPENAI_API_KEY) {\n response = await generateWithOpenAI(prompt, '');\n }\n\n // 处理响应,提取关键词\n const keywords = response\n .split(',')\n .map(k => k.trim())\n .filter(k => k.length > 0);\n\n return keywords;\n } catch (error) {\n console.error('获取推荐关键词失败:', error);\n throw new Error('获取推荐关键词失败');\n }\n};\n\n// 一键生成所有内容\nexport const generateAll = async (options = {}) => {\n try {\n if (!USE_AI) {\n // 本地生成示例\n return {\n title: '追梦人',\n artist: '星辰乐队',\n theme: '追求梦想',\n keywords: ['梦想', '奋斗', '希望', '未来', '勇气'],\n lyrics: `追逐梦想的脚步不停歇\n风雨中我依然坚持着\n即使道路充满了荆棘\n也无法阻挡我前进的决心\n\n看那天边的星光闪耀\n照亮我追梦的道路\n让我找到前进的方向\n不再迷失在黑暗中\n\n[副歌]\n我是追梦的勇者\n永不言弃永不放弃\n心中的火焰不熄灭\n直到梦想变成现实\n\n让我们一起追逐梦想\n用汗水浇灌希望\n未来的道路虽然漫长\n但我们的心永远坚强\n\n[副歌]\n我是追梦的勇者\n永不言弃永不放弃\n心中的火焰不熄灭\n直到梦想变成现实`\n };\n }\n\n const { lyricsIdea, style, structure } = options;\n\n // 构建生成提示\n const prompt = `请根据以下要求,创作一首完整的原创歌曲内容:\n\n创作要求:\n1. 生成歌曲标题(简洁有力)\n2. 创作歌手/乐队名称\n3. 确定创作主题\n4. 推荐5个关键词\n5. 创作完整歌词\n\n${lyricsIdea ? `歌词创作思路:${lyricsIdea}` : ''}\n风格:${style || '现代流行'}\n结构:${structure || 'verse-chorus'}\n\n请按以下格式返回:\n标题:xxx\n歌手:xxx\n主题:xxx\n关键词:xxx,xxx,xxx,xxx,xxx\n歌词:\nxxx\nxxx\nxxx`;\n\n let response;\n if (API_PROVIDER === 'deepseek' && DEEPSEEK_API_KEY) {\n response = await generateWithDeepSeek(prompt, '');\n } else if (API_PROVIDER === 'openai' && OPENAI_API_KEY) {\n response = await generateWithOpenAI(prompt, '');\n }\n\n // 解析响应\n const lines = response.split('\\n');\n const result = {\n title: '',\n artist: '',\n theme: '',\n keywords: [],\n lyrics: ''\n };\n\n let isLyrics = false;\n for (const line of lines) {\n if (line.startsWith('标题:')) {\n result.title = line.replace('标题:', '').trim();\n } else if (line.startsWith('歌手:')) {\n result.artist = line.replace('歌手:', '').trim();\n } else if (line.startsWith('主题:')) {\n result.theme = line.replace('主题:', '').trim();\n } else if (line.startsWith('关键词:')) {\n result.keywords = line\n .replace('关键词:', '')\n .split(',')\n .map(k => k.trim())\n .filter(k => k.length > 0);\n } else if (line.startsWith('歌词:')) {\n isLyrics = true;\n } else if (isLyrics) {\n result.lyrics += line + '\\n';\n }\n }\n\n return result;\n } catch (error) {\n console.error('一键生成失败:', error);\n throw new Error('一键生成失败');\n }\n};"}}, "backend/index.js": {"757b6fb841cfb636d172057d2b28996255f2c9fe": {"t": 1785768980.3093376, "n": 53235, "h": "5c7ea195420a0a8f", "c": "import http from 'node:http';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport dotenv from 'dotenv';\nimport fs from 'fs';\nimport { initDatabase, query, execute } from './config/database.js';\nimport { initDatabase as initLyricsReplacementsTable, getLyricsReplacements, saveLyricsReplacement } from './services/database.js';\nimport { searchLyrics } from './services/search.js';\nimport { generateLyrics, createLyrics, generateLyricsWithMiniMax, generateLyricsWithAIHubMix } from './services/ai.js';\nimport { getLyricsStats } from './services/lyrics-db.js';\nimport { generateMusic } from './services/music.js';\nimport { createMiniMaxMusicGeneration, getMiniMaxMusicStatus } from './services/minimax-music.js';\nimport { getApiConfigs } from './migrations/init-api-configs.js';\nimport url from 'url';\nimport axios from 'axios';\n\n// Load environment variables\ndotenv.config();\n\n// Set up __dirname equivalent in ESM\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// 解析查询参数\nconst parseQueryParams = (reqUrl) => {\n const parsedUrl = url.parse(reqUrl, true);\n return parsedUrl.query;\n};\n\n// 解析请求体JSON\nconst parseJsonBody = async (req) => {\n return new Promise((resolve, reject) => {\n let body = '';\n req.on('data', chunk => {\n body += chunk.toString();\n });\n req.on('end', () => {\n try {\n if (body) {\n resolve(JSON.parse(body));\n } else {\n resolve({});\n }\n } catch (error) {\n reject(error);\n }\n });\n req.on('error', (error) => {\n reject(error);\n });\n });\n};\n\n// 设置CORS头\nconst setCorsHeaders = (res) => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n};\n\n// 返回JSON响应\nconst sendJsonResponse = (res, data, statusCode = 200) => {\n res.statusCode = statusCode;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(data));\n};\n\nconst getSceneDescription = (scene) => {\n const sceneDescriptions = {\n kitchen: '厨房灾难现场,做饭时的各种意外和搞笑场景',\n gym: '健身房翻车实录,健身时的各种尴尬和失败经历',\n shopping: '网购拆箱翻车,网购商品与预期不符的搞笑场景',\n haircut: '理发店惊魂记,理发时的各种意外和尴尬',\n meeting: '会议室迷惑行为,开会时的各种尴尬和搞笑场景',\n dating: '约会翻车现场,约会时的各种意外和尴尬',\n pet: '宠物拆家日常,宠物捣乱时的各种搞笑场景',\n social: '社交软件翻车,使用社交软件时的各种尴尬',\n exam: '考试翻车现场,考试时的各种意外和搞笑场景',\n roadtrip: '自驾游迷惑操作,自驾游时的各种意外和搞笑场景'\n };\n\n return sceneDescriptions[scene] || scene || '';\n};\n\n// 处理API请求\nconst handleApiRequest = async (req, res, parsedUrl) => {\n // 设置CORS头\n setCorsHeaders(res);\n\n // 处理预检请求\n if (req.method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n try {\n // API路由处理\n const path = parsedUrl.pathname;\n\n // 搜索歌词\n if (path === '/api/search/lyrics' && req.method === 'GET') {\n const queryParams = parseQueryParams(req.url);\n const query = queryParams.query;\n const userId = queryParams.userId;\n\n if (!query) {\n return sendJsonResponse(res, { success: false, message: '搜索内容不能为空' }, 400);\n }\n\n const results = await searchLyrics(query, userId);\n return sendJsonResponse(res, { success: true, data: results });\n }\n\n // 通过歌曲名搜索歌曲 (vkeys.cn API)\n if (path === '/api/search/song' && req.method === 'GET') {\n const queryParams = parseQueryParams(req.url);\n const word = queryParams.word;\n const userId = queryParams.userId;\n\n if (!word) {\n return sendJsonResponse(res, { success: false, message: '歌曲名不能为空' }, 400);\n }\n\n try {\n console.log('收到歌曲名搜索请求:', word, userId ? `用户ID: ${userId}` : '');\n\n // 调用 vkeys.cn API 搜索歌曲\n const response = await axios.get('https://api.vkeys.cn/v2/music/tencent/search/song', {\n params: { word },\n timeout: 10000\n });\n\n // 检查API响应\n if (response.data && response.data.code === 200 && Array.isArray(response.data.data)) {\n const songs = response.data.data;\n console.log(`搜索成功: 找到 ${songs.length} 首与\"${word}\"相关的歌曲`);\n console.log(`歌曲详情: ${JSON.stringify(songs.slice(0, 2))}`); // 只打印前两首歌的详情,避免日志过长\n\n return sendJsonResponse(res, {\n success: true,\n data: songs\n });\n } else {\n console.log(`搜索失败: 未找到与\"${word}\"相关的歌曲`);\n console.log(`API响应: ${JSON.stringify(response.data)}`);\n return sendJsonResponse(res, {\n success: false,\n message: '未找到相关歌曲',\n data: []\n });\n }\n } catch (error) {\n console.error('歌曲搜索失败:', error);\n console.error('错误详情:', error.response ? JSON.stringify(error.response.data) : '无响应数据');\n return sendJsonResponse(res, {\n success: false,\n message: '腾讯音乐搜索暂时不可用',\n data: []\n });\n }\n }\n\n // 通过歌曲ID获取歌词 (vkeys.cn API)\n if (path === '/api/search/lyric' && req.method === 'GET') {\n const queryParams = parseQueryParams(req.url);\n const id = queryParams.id;\n const userId = queryParams.userId;\n\n if (!id) {\n return sendJsonResponse(res, { success: false, message: '歌曲ID不能为空' }, 400);\n }\n\n try {\n console.log('收到歌词ID搜索请求:', id, userId ? `用户ID: ${userId}` : '');\n\n // 调用 vkeys.cn API 获取歌词\n const response = await axios.get('https://api.vkeys.cn/v2/music/tencent/lyric', {\n params: { id },\n timeout: 10000\n });\n\n // 检查API响应\n if (response.data && response.data.code === 200 && response.data.data && response.data.data.lrc) {\n console.log(`获取歌词成功: 歌曲ID ${id}`);\n console.log(`歌词前100个字符: ${response.data.data.lrc.substring(0, 100)}...`);\n\n return sendJsonResponse(res, {\n success: true,\n data: response.data.data\n });\n } else {\n console.log(`获取歌词失败: 歌曲ID ${id} 未找到歌词`);\n console.log(`API响应: ${JSON.stringify(response.data)}`);\n return sendJsonResponse(res, {\n success: false,\n message: '未找到该歌曲的歌词',\n data: null\n });\n }\n } catch (error) {\n console.error('获取歌词失败:', error);\n console.error('错误详情:', error.response ? JSON.stringify(error.response.data) : '无响应数据');\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '获取歌词失败'\n }, 500);\n }\n }\n\n // 获取歌词替换历史\n if (path === '/api/lyrics/history' && req.method === 'GET') {\n const queryParams = parseQueryParams(req.url);\n const userId = queryParams.userId;\n\n try {\n const history = await getLyricsReplacements(userId || null);\n return sendJsonResponse(res, { success: true, data: history });\n } catch (error) {\n console.error('获取歌词替换历史失败:', error);\n return sendJsonResponse(res, { success: false, message: '获取历史记录失败' }, 500);\n }\n }\n\n // 生成歌词\n if (path === '/api/lyrics/generate' && req.method === 'POST') {\n console.log('收到生成歌词请求');\n const body = await parseJsonBody(req);\n console.log('请求体:', {\n songId: body.songId,\n title: body.title,\n artist: body.artist,\n style: body.style,\n mode: body.mode,\n originalLyrics: body.originalLyrics ? body.originalLyrics.substring(0, 50) + '...' : null\n });\n\n // 获取原始歌词 (从请求体或数据库)\n let originalLyrics = body.originalLyrics;\n let songInfo = { title: body.title, artist: body.artist };\n\n if (!originalLyrics && body.songId) {\n // 从数据库获取歌曲信息\n try {\n const [songs] = await query('SELECT * FROM songs WHERE id = ?', [body.songId]);\n if (songs && songs.length > 0) {\n originalLyrics = songs[0].lyrics;\n songInfo.title = songs[0].title;\n songInfo.artist = songs[0].artist;\n console.log('从数据库获取歌词成功');\n }\n } catch (error) {\n console.error('获取歌词失败:', error);\n }\n }\n\n if (!originalLyrics) {\n console.log('错误: 原始歌词为空');\n return sendJsonResponse(res, { success: false, message: '原始歌词不能为空' }, 400);\n }\n\n // 优先走 AI 生成:先尝试 MiniMax,若套餐不支持等失败再自动切换 AIHubMix\n const useAI = body.mode !== 'local';\n console.log('生成模式:', useAI ? 'AI生成' : '本地生成', '(mode:', body.mode, ')');\n\n // 准备生成选项\n const generationOptions = {\n style: body.style || 'modern',\n keywords: body.keywords || [],\n mood: body.mood || 5,\n title: songInfo.title,\n artist: songInfo.artist,\n scene: body.scene || '',\n useAI: useAI\n };\n\n console.log('生成选项:', generationOptions);\n\n try {\n // 生成替换歌词\n console.log('开始生成替换歌词...');\n let replacedLyrics;\n let lyricsTitle = songInfo.title;\n\n if (useAI) {\n // 优先使用 MiniMax AI 生成歌词;若套餐不支持、额度限制或接口失败,则自动切换到 AIHubMix\n if (process.env.MINIMAX_API_KEY) {\n try {\n const minimaxPromptStyle = body.scene\n ? `${body.style || '现代流行'},并且歌词内容必须严格围绕${getSceneDescription(body.scene)}`\n : (body.style || '现代流行');\n\n console.log('使用 MiniMax AI 生成歌词...');\n const miniMaxResult = await generateLyricsWithMiniMax({\n mode: 'edit',\n prompt: `将以下歌词改写成${minimaxPromptStyle}风格,保留原歌词的结构、时间戳和韵律,只输出改写后的中文歌词`,\n lyrics: originalLyrics,\n title: songInfo.title,\n style: minimaxPromptStyle\n });\n replacedLyrics = miniMaxResult.lyrics;\n lyricsTitle = miniMaxResult.title || songInfo.title;\n console.log('MiniMax 歌词生成成功');\n } catch (miniMaxError) {\n console.log('MiniMax 生成失败,尝试 AIHubMix:', miniMaxError.message);\n // 如果 MiniMax 失败,尝试 AIHubMix\n if (process.env.AIHUBMIX_API_KEY) {\n try {\n const aihubResult = await generateLyricsWithAIHubMix({\n originalLyrics: originalLyrics,\n style: body.style,\n title: songInfo.title,\n artist: songInfo.artist,\n scene: body.scene || ''\n });\n replacedLyrics = aihubResult.lyrics;\n console.log('AIHubMix 歌词生成成功');\n } catch (aihubError) {\n console.log('AIHubMix 生成失败,回退到本地生成:', aihubError.message);\n replacedLyrics = await generateLyrics(originalLyrics, {\n ...generationOptions,\n useAI: false\n });\n }\n } else {\n console.log('未配置 AIHubMix API Key,回退到本地生成');\n replacedLyrics = await generateLyrics(originalLyrics, {\n ...generationOptions,\n useAI: false\n });\n }\n }\n } else if (process.env.AIHUBMIX_API_KEY) {\n // 直接使用 AIHubMix\n console.log('使用 AIHubMix AI 生成歌词...');\n const aihubResult = await generateLyricsWithAIHubMix({\n originalLyrics: originalLyrics,\n style: body.style,\n title: songInfo.title,\n artist: songInfo.artist,\n scene: body.scene || ''\n });\n replacedLyrics = aihubResult.lyrics;\n console.log('AIHubMix 歌词生成成功');\n } else {\n // 没有 AI Key,使用本地生成\n console.log('没有 AI API Key,使用本地生成...');\n replacedLyrics = await generateLyrics(originalLyrics, generationOptions);\n }\n } else {\n // 本地生成模式\n console.log('使用本地生成...');\n replacedLyrics = await generateLyrics(originalLyrics, generationOptions);\n }\n\n console.log('生成完成,结果:', replacedLyrics ? replacedLyrics.substring(0, 50) + '...' : null);\n\n // 保存到歌词替换表\n console.log('保存到歌词替换表...');\n const replacementId = await saveLyricsReplacement({\n title: lyricsTitle || 'Unknown',\n artist: songInfo.artist || 'Unknown',\n originalLyrics: originalLyrics,\n replacedLyrics,\n style: body.style || '默认',\n timestamp: new Date(),\n userId: body.userId,\n originalSongId: body.songId\n });\n console.log('保存成功,ID:', replacementId);\n\n // 构建响应数据\n const responseData = {\n success: true,\n data: {\n replacementId,\n originalLyrics: originalLyrics,\n replacedLyrics,\n generated: replacedLyrics, // 添加generated字段,与前端匹配\n options: generationOptions\n }\n };\n console.log('发送响应:', {\n success: true,\n replacementId,\n replacedLyrics: replacedLyrics ? replacedLyrics.substring(0, 50) + '...' : null\n });\n\n return sendJsonResponse(res, responseData);\n } catch (error) {\n console.error('生成替换歌词失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '生成替换歌词失败'\n }, 500);\n }\n }\n\n // 一键生成全部内容\n if (path === '/api/lyrics/generate-all' && req.method === 'POST') {\n console.log('收到一键生成全部内容请求');\n const body = await parseJsonBody(req);\n console.log('请求体:', {\n lyricsIdea: body.lyricsIdea,\n style: body.style,\n structure: body.structure\n });\n\n if (!body.lyricsIdea) {\n console.log('错误: 歌词创作理念为空');\n return sendJsonResponse(res, { success: false, message: '歌词创作理念不能为空' }, 400);\n }\n\n try {\n // 使用AI生成歌曲标题、艺术家、主题和关键词\n console.log('开始生成歌曲信息...');\n\n // 构建提示词\n const prompt = `根据以下歌词创作理念,生成一个合适的歌曲标题、艺术家名称、主题和5个关键词:\n\n 歌词创作理念: ${body.lyricsIdea}\n 风格: ${body.style || '现代流行'}\n 结构: ${body.structure || '标准'}\n\n 请以JSON格式返回,包含title(歌曲标题)、artist(艺术家名称)、theme(主题)、keywords(关键词数组)字段。\n\n 重要要求:\n 1. 所有内容必须使用中文,不要使用英文或其他语言\n 2. 歌曲标题应该是中文,不超过10个字\n 3. 艺术家名称应该是中文名字,可以是虚构的\n 4. 主题应该是简短的中文描述\n 5. 关键词应该是5个中文短语,每个1-3个字\n\n 示例格式:\n {\n \"title\": \"夏日晚风\",\n \"artist\": \"陈明月\",\n \"theme\": \"夏日回忆\",\n \"keywords\": [\"夏天\", \"晚风\", \"回忆\", \"青春\", \"温柔\"]\n }\n\n 请确保返回的是有效的JSON格式,不要添加其他文本。`;\n\n // 调用AI生成\n const songInfo = await generateLyrics(prompt, { style: 'json' });\n console.log('歌曲信息生成结果:', songInfo);\n\n // 解析JSON结果\n let parsedInfo;\n try {\n // 尝试提取JSON部分\n const jsonMatch = songInfo.match(/\\{[\\s\\S]*?\\}/);\n if (jsonMatch) {\n parsedInfo = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('无法提取JSON数据');\n }\n } catch (parseError) {\n console.error('解析AI生成的JSON失败:', parseError);\n // 使用默认值\n parsedInfo = {\n title: '新歌',\n artist: '未知艺术家',\n theme: body.lyricsIdea.substring(0, 20),\n keywords: ['音乐', '歌词', '创作']\n };\n }\n\n // 确保所有内容都是中文\n if (/^[a-zA-Z\\s]+$/.test(parsedInfo.title) ||\n /^[a-zA-Z\\s]+$/.test(parsedInfo.artist) ||\n /^[a-zA-Z\\s]+$/.test(parsedInfo.theme) ||\n parsedInfo.keywords.some(k => /^[a-zA-Z\\s]+$/.test(k))) {\n console.log('检测到英文内容,使用默认中文值');\n parsedInfo = {\n title: '心情故事',\n artist: '陈明星',\n theme: body.lyricsIdea.substring(0, 20),\n keywords: ['感动', '回忆', '思念', '希望', '温暖']\n };\n }\n\n // 生成歌词\n console.log('开始生成歌词...');\n const lyricsPrompt = `请根据以下信息创作一首完整的歌词:\n\n 歌曲标题: ${parsedInfo.title}\n 艺术家: ${parsedInfo.artist}\n 主题: ${parsedInfo.theme}\n 关键词: ${parsedInfo.keywords.join(', ')}\n 风格: ${body.style || '现代流行'}\n 结构: ${body.structure || '标准'}\n 创作理念: ${body.lyricsIdea}\n\n 重要要求:\n 1. 必须使用中文创作歌词,不要使用英文或其他语言\n 2. 歌词应该完整、有意义、符合主题\n 3. 包含至少两段主歌和副歌\n 4. 注意押韵和节奏感\n 5. 情感表达真挚自然\n 6. 可以适当使用\"主歌1\"、\"副歌\"等标记来区分歌词结构\n\n 请创作一首完整的中文歌词。`;\n\n const lyrics = await generateLyrics(lyricsPrompt, { style: body.style });\n console.log('歌词生成完成,结果:', lyrics ? lyrics.substring(0, 50) + '...' : null);\n\n // 检查生成的歌词是否为英文\n if (/^[\\s\\n]*[A-Za-z*]+/.test(lyrics)) {\n console.log('检测到英文歌词,重新生成中文歌词');\n\n // 重新生成中文歌词\n const zhLyricsPrompt = `请用中文创作一首歌词,标题为\"${parsedInfo.title}\",风格为\"${body.style || '现代流行'}\",\n 主题是\"${parsedInfo.theme}\",包含以下关键词:${parsedInfo.keywords.join(',')}。\n\n 创作理念: ${body.lyricsIdea}\n\n 请确保:\n 1. 必须使用中文创作,不要使用任何英文\n 2. 歌词应该有完整的结构,包括主歌和副歌\n 3. 情感表达要真挚自然\n 4. 符合中文歌词的押韵和节奏习惯`;\n\n const zhLyrics = await generateLyrics(zhLyricsPrompt, { style: body.style });\n console.log('中文歌词重新生成完成,结果:', zhLyrics ? zhLyrics.substring(0, 50) + '...' : null);\n\n // 使用重新生成的中文歌词\n if (zhLyrics) {\n return zhLyrics;\n }\n // 如果重新生成失败,仍然使用原来的歌词\n return lyrics;\n }\n\n // 保存到歌词创作表\n console.log('保存到歌词创作表...');\n const creationId = await saveLyricsReplacement({\n title: parsedInfo.title,\n artist: parsedInfo.artist,\n originalLyrics: `创作理念: ${body.lyricsIdea}\\n风格: ${body.style || '现代流行'}\\n结构: ${body.structure || '标准'}`,\n replacedLyrics: lyrics,\n style: body.style || '默认',\n timestamp: new Date(),\n userId: body.userId\n });\n console.log('保存成功,ID:', creationId);\n\n // 构建响应数据\n return sendJsonResponse(res, {\n success: true,\n title: parsedInfo.title,\n artist: parsedInfo.artist,\n theme: parsedInfo.theme,\n keywords: parsedInfo.keywords,\n lyrics: lyrics\n });\n } catch (error) {\n console.error('一键生成全部内容失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '一键生成全部内容失败'\n }, 500);\n }\n }\n\n // 获取推荐关键词\n if (path === '/api/keywords/recommend' && req.method === 'GET') {\n console.log('收到获取推荐关键词请求');\n const queryParams = parseQueryParams(req.url);\n const theme = queryParams.theme;\n const style = queryParams.style;\n\n if (!theme) {\n return sendJsonResponse(res, { success: false, message: '主题不能为空' }, 400);\n }\n\n try {\n console.log(`根据主题\"${theme}\"和风格\"${style}\"生成推荐关键词`);\n\n // 构建提示词\n const prompt = `请根据以下主题和风格,生成5-8个适合用于歌词创作的关键词:\n\n 主题: ${theme}\n 风格: ${style || '现代流行'}\n\n 请以JSON格式返回一个关键词数组,每个关键词应该是1-3个字的中文短语。\n\n 示例格式:\n [\"关键词1\", \"关键词2\", \"关键词3\", \"关键词4\", \"关键词5\"]\n\n 或者:\n {\n \"keywords\": [\"关键词1\", \"关键词2\", \"关键词3\", \"关键词4\", \"关键词5\"]\n }\n\n 请确保返回的是有效的JSON格式,不要添加其他文本。`;\n\n // 调用AI生成\n const keywordsResult = await generateLyrics(prompt, { style: 'json' });\n console.log('关键词生成结果:', keywordsResult);\n\n // 解析JSON结果\n let keywords;\n try {\n // 尝试提取JSON数组部分\n const jsonMatch = keywordsResult.match(/\\[[\\s\\S]*?\\]/);\n if (jsonMatch) {\n keywords = JSON.parse(jsonMatch[0]);\n } else {\n // 尝试提取完整JSON对象\n const objMatch = keywordsResult.match(/\\{[\\s\\S]*?\\}/);\n if (objMatch) {\n const obj = JSON.parse(objMatch[0]);\n if (Array.isArray(obj.keywords)) {\n keywords = obj.keywords;\n } else {\n throw new Error('JSON中没有keywords数组');\n }\n } else {\n throw new Error('无法提取JSON数据');\n }\n }\n } catch (parseError) {\n console.error('解析AI生成的关键词JSON失败:', parseError);\n // 使用默认关键词\n keywords = ['爱情', '思念', '回忆', '感动', '希望'];\n }\n\n // 确保关键词是中文\n if (keywords.some(k => /^[a-zA-Z\\s]+$/.test(k))) {\n console.log('检测到英文关键词,使用默认中文关键词');\n keywords = ['爱情', '思念', '回忆', '感动', '希望'];\n }\n\n return sendJsonResponse(res, {\n success: true,\n keywords: keywords\n });\n } catch (error) {\n console.error('获取推荐关键词失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '获取推荐关键词失败'\n }, 500);\n }\n }\n\n // 创建全新歌词\n if (path === '/api/lyrics/create' && req.method === 'POST') {\n console.log('收到创建歌词请求');\n const body = await parseJsonBody(req);\n console.log('请求体:', {\n title: body.title,\n artist: body.artist,\n theme: body.theme,\n style: body.style,\n structure: body.structure,\n keywords: body.keywords\n });\n\n if (!body.title || !body.theme) {\n console.log('错误: 歌曲标题或主题为空');\n return sendJsonResponse(res, { success: false, message: '歌曲标题和主题不能为空' }, 400);\n }\n\n // 准备创作选项\n const creationOptions = {\n title: body.title,\n artist: body.artist || '未知歌手',\n theme: body.theme,\n style: body.style || 'modern',\n structure: body.structure || 'verse-chorus',\n keywords: body.keywords || []\n };\n\n console.log('创作选项:', creationOptions);\n\n try {\n // 创建全新歌词\n console.log('开始创建歌词...');\n const lyrics = await createLyrics(creationOptions);\n console.log('创建完成,结果:', lyrics ? lyrics.substring(0, 50) + '...' : null);\n\n // 保存到歌词创作表\n console.log('保存到歌词创作表...');\n const creationId = await saveLyricsReplacement({\n title: body.title,\n artist: body.artist || '未知歌手',\n originalLyrics: `主题: ${body.theme}\\n风格: ${body.style || 'modern'}\\n结构: ${body.structure || 'verse-chorus'}`,\n replacedLyrics: lyrics,\n style: body.style || '默认',\n timestamp: new Date(),\n userId: body.userId\n });\n console.log('保存成功,ID:', creationId);\n\n // 构建响应数据\n const responseData = {\n success: true,\n data: {\n creationId,\n lyrics,\n options: creationOptions\n }\n };\n console.log('发送响应:', {\n success: true,\n creationId,\n lyrics: lyrics ? lyrics.substring(0, 50) + '...' : null\n });\n\n return sendJsonResponse(res, responseData);\n } catch (error) {\n console.error('创建歌词失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '创建歌词失败'\n }, 500);\n }\n }\n\n // 手动更新音乐生成状态\n if (path === '/api/music/update-status' && req.method === 'POST') {\n console.log('收到手动更新音乐生成状态请求');\n try {\n const body = await parseJsonBody(req);\n const generationId = body.generationId;\n\n if (!generationId) {\n return sendJsonResponse(res, { success: false, message: '生成ID不能为空' }, 400);\n }\n\n // 查询生成记录\n const [generations] = await query(\n 'SELECT * FROM music_generations WHERE id = ?',\n [generationId]\n );\n\n if (!generations || generations.length === 0) {\n return sendJsonResponse(res, { success: false, message: '未找到生成记录' }, 404);\n }\n\n const generation = generations[0];\n\n // 查询API记录\n const apiConfig = await getApiConfigs('suno-api');\n const baseUrl = apiConfig.base_url || 'https://apibox.erweima.ai';\n const apiUrl = `${baseUrl}/api/v1/task/list?page=1&size=10`;\n\n console.log(`尝试从API记录中获取状态: ${apiUrl}`);\n\n const response = await axios({\n method: 'get',\n url: apiUrl,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiConfig.api_key}`\n }\n });\n\n if (response.data && response.data.code === 200 && response.data.data && response.data.data.records) {\n const records = response.data.data.records;\n console.log(`找到${records.length}条API记录`);\n\n // 查找匹配的记录\n for (const record of records) {\n if (record.resultJson) {\n try {\n const resultData = JSON.parse(record.resultJson);\n if (resultData.data && Array.isArray(resultData.data) && resultData.data.length > 0) {\n const audioUrl = resultData.data[0].audio_url;\n const taskId = resultData.task_id;\n\n console.log(`找到匹配的API记录: taskId=${taskId}, audioUrl=${audioUrl}`);\n\n // 更新生成记录\n await execute(\n 'UPDATE music_generations SET status = ?, result_url = ?, generation_id = ? WHERE id = ?',\n ['completed', audioUrl, taskId, generationId]\n );\n\n // 返回更新后的状态\n return sendJsonResponse(res, {\n success: true,\n message: '状态更新成功',\n data: {\n generationId,\n status: 'completed',\n resultUrl: audioUrl,\n errorMessage: null\n }\n });\n }\n } catch (parseError) {\n console.error(`解析API记录失败:`, parseError);\n }\n }\n }\n\n // 如果没有找到匹配的记录,返回当前状态\n return sendJsonResponse(res, {\n success: true,\n message: '未找到匹配的API记录',\n data: {\n generationId,\n status: generation.status,\n resultUrl: generation.local_file_path || generation.result_url,\n errorMessage: generation.error_message\n }\n });\n } else {\n return sendJsonResponse(res, {\n success: false,\n message: '获取API记录失败',\n error: response.data\n });\n }\n } catch (error) {\n console.error('手动更新音乐生成状态失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: '手动更新音乐生成状态失败',\n error: error.message\n }, 500);\n }\n }\n\n // 获取音乐生成历史\n if (path === '/api/music/history' && req.method === 'GET') {\n console.log('收到获取音乐生成历史请求');\n const queryParams = parseQueryParams(req.url);\n const userId = queryParams.userId;\n const page = parseInt(queryParams.page) || 1;\n const pageSize = parseInt(queryParams.pageSize) || 10;\n\n try {\n let countSql = 'SELECT COUNT(*) as total FROM music_generations';\n let historySql = `SELECT id, title, artist, style, mood, api_used, status, result_url, local_file_path, error_message, created_at, updated_at\n FROM music_generations\n ORDER BY created_at DESC\n LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`;\n let params = [];\n\n // 如果提供了用户ID,则按用户ID筛选\n if (userId) {\n countSql += ' WHERE user_id = ?';\n historySql = historySql.replace('FROM music_generations', 'FROM music_generations WHERE user_id = ?');\n params = [userId];\n }\n\n // 查询总数\n const [countResult] = await query(countSql, params);\n const total = countResult[0].total;\n\n // 查询历史记录\n const [history] = await query(historySql, params);\n\n return sendJsonResponse(res, {\n success: true,\n data: {\n total,\n page,\n pageSize,\n list: history\n }\n });\n } catch (error) {\n console.error('获取音乐生成历史失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: '获取音乐生成历史失败',\n error: error.message\n }, 500);\n }\n }\n\n // 获取音乐生成状态\n if (path.startsWith('/api/music/status/') && req.method === 'GET') {\n console.log('收到获取音乐生成状态请求');\n const generationId = path.split('/').pop();\n\n if (!generationId) {\n return sendJsonResponse(res, { success: false, message: '生成ID不能为空' }, 400);\n }\n\n try {\n // 查询生成记录\n const [generations] = await query(\n 'SELECT * FROM music_generations WHERE id = ?',\n [generationId]\n );\n\n if (!generations || generations.length === 0) {\n return sendJsonResponse(res, { success: false, message: '未找到生成记录' }, 404);\n }\n\n const generation = generations[0];\n\n // 如果状态是pending或processing,并且生成时间超过5分钟,尝试从API获取最新状态\n const createdTime = new Date(generation.created_at || Date.now() - 3600000);\n const now = new Date();\n const diffMinutes = (now - createdTime) / (1000 * 60);\n\n if ((generation.status === 'pending' || generation.status === 'processing') && diffMinutes > 5) {\n console.log(`生成记录${generationId}已经处理了${diffMinutes.toFixed(2)}分钟,尝试从API获取最新状态`);\n\n // 检查是否有task_id\n if (generation.generation_id && !generation.generation_id.startsWith('mock-') && !generation.generation_id.startsWith('task-')) {\n try {\n // 从API获取最新状态\n const apiConfig = await getApiConfigs('suno-api');\n const baseUrl = apiConfig.base_url || 'https://apibox.erweima.ai';\n const apiUrl = `${baseUrl}/api/v1/task/${generation.generation_id}`;\n\n console.log(`尝试从API获取状态: ${apiUrl}`);\n\n const response = await axios({\n method: 'get',\n url: apiUrl,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiConfig.api_key}`\n }\n });\n\n console.log(`从API获取到的状态:`, response.data);\n\n // 如果API返回了状态,使用API返回的状态\n if (response.data && response.data.code === 200) {\n // 解析状态\n let status = 'processing';\n let resultUrl = null;\n\n if (response.data.data && response.data.data.data && Array.isArray(response.data.data.data) && response.data.data.data.length > 0) {\n status = 'completed';\n resultUrl = response.data.data.data[0].audio_url;\n\n // 更新生成记录\n await execute(\n 'UPDATE music_generations SET status = ?, result_url = ? WHERE id = ?',\n [status, resultUrl, generationId]\n );\n }\n\n return sendJsonResponse(res, {\n success: true,\n data: {\n generationId,\n status,\n resultUrl,\n errorMessage: null\n }\n });\n }\n } catch (statusError) {\n console.error(`获取API状态失败:`, statusError);\n }\n }\n\n // 如果无法从API获取状态,或者没有task_id,尝试从API记录中获取状态\n try {\n // 查询API记录\n const apiConfig = await getApiConfigs('suno-api');\n const baseUrl = apiConfig.base_url || 'https://apibox.erweima.ai';\n const apiUrl = `${baseUrl}/api/v1/task/list?page=1&size=10`;\n\n console.log(`尝试从API记录中获取状态: ${apiUrl}`);\n\n const response = await axios({\n method: 'get',\n url: apiUrl,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiConfig.api_key}`\n }\n });\n\n if (response.data && response.data.code === 200 && response.data.data && response.data.data.records) {\n const records = response.data.data.records;\n console.log(`找到${records.length}条API记录`);\n\n // 查找匹配的记录\n for (const record of records) {\n if (record.resultJson) {\n try {\n const resultData = JSON.parse(record.resultJson);\n if (resultData.data && Array.isArray(resultData.data) && resultData.data.length > 0) {\n const audioUrl = resultData.data[0].audio_url;\n const taskId = resultData.task_id;\n\n console.log(`找到匹配的API记录: taskId=${taskId}, audioUrl=${audioUrl}`);\n\n // 更新生成记录\n await execute(\n 'UPDATE music_generations SET status = ?, result_url = ?, generation_id = ? WHERE id = ?',\n ['completed', audioUrl, taskId, generationId]\n );\n\n // 返回更新后的状态\n return sendJsonResponse(res, {\n success: true,\n data: {\n generationId,\n status: 'completed',\n resultUrl: audioUrl,\n errorMessage: null\n }\n });\n }\n } catch (parseError) {\n console.error(`解析API记录失败:`, parseError);\n }\n }\n }\n }\n } catch (apiError) {\n console.error(`查询API记录失败:`, apiError);\n }\n }\n\n // 检查是否是特定错误,如果是,则停止请求\n if (generation.status === 'failed' && generation.error_code) {\n // 429: 积分不足,停止请求\n if (generation.error_code === 429) {\n return sendJsonResponse(res, {\n success: false,\n message: '积分不足,请充值后再试',\n data: {\n generationId,\n status: 'failed',\n errorMessage: generation.error_message,\n errorCode: generation.error_code,\n stopPolling: true // 告诉前端停止轮询\n }\n });\n }\n }\n\n return sendJsonResponse(res, {\n success: true,\n data: {\n generationId,\n status: generation.status,\n resultUrl: generation.local_file_path || generation.result_url,\n errorMessage: generation.error_message,\n errorCode: generation.error_code\n }\n });\n } catch (error) {\n console.error('获取音乐生成状态失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: '获取音乐生成状态失败',\n error: error.message\n }, 500);\n }\n }\n\n // Suno API回调处理\n if (path === '/api/suno-callback' && req.method === 'POST') {\n console.log('收到Suno API回调');\n try {\n // 解析请求体\n const body = await parseJsonBody(req);\n console.log('Suno API回调数据:', JSON.stringify(body, null, 2));\n\n // 检查回调数据\n let taskId = null;\n let audioUrl = null;\n let status = 'processing';\n\n // 尝试从不同格式的回调数据中提取信息\n if (body && body.task_id) {\n // 直接在body中的task_id\n taskId = body.task_id;\n } else if (body && body.data && body.data.task_id) {\n // 在data中的task_id\n taskId = body.data.task_id;\n } else if (body && body.data && body.data.taskId) {\n // 在data中的taskId\n taskId = body.data.taskId;\n }\n\n // 检查是否有音频URL\n if (body && body.data && Array.isArray(body.data) && body.data.length > 0 && body.data[0].audio_url) {\n // 直接在data数组中的audio_url\n audioUrl = body.data[0].audio_url;\n status = 'completed';\n } else if (body && body.data && body.data.data && Array.isArray(body.data.data) && body.data.data.length > 0 && body.data.data[0].audio_url) {\n // 在data.data数组中的audio_url\n audioUrl = body.data.data[0].audio_url;\n status = 'completed';\n }\n\n console.log(`从回调中提取的信息: taskId=${taskId}, status=${status}, audioUrl=${audioUrl}`);\n\n if (taskId) {\n // 查找对应的生成记录\n const [generations] = await query(\n 'SELECT * FROM music_generations WHERE generation_id = ?',\n [taskId]\n );\n\n if (generations && generations.length > 0) {\n const generation = generations[0];\n\n if (status === 'completed' && audioUrl) {\n // 生成完成\n await execute(\n 'UPDATE music_generations SET status = ?, result_url = ? WHERE id = ?',\n ['completed', audioUrl, generation.id]\n );\n\n console.log(`音乐生成完成,更新状态为completed,ID=${generation.id}, 音频URL=${audioUrl}`);\n } else if (body.code === 400 || body.code === 500 || (body.msg && body.msg.toLowerCase().includes('error'))) {\n // 生成失败\n await execute(\n 'UPDATE music_generations SET status = ?, error_message = ? WHERE id = ?',\n ['failed', body.msg || '生成失败', generation.id]\n );\n\n console.log(`音乐生成失败,更新状态为failed,ID=${generation.id}`);\n }\n } else {\n console.log('未找到对应的生成记录:', taskId);\n\n // 尝试查找所有生成记录\n const [allGenerations] = await query(\n 'SELECT * FROM music_generations ORDER BY id DESC LIMIT 10'\n );\n\n console.log('最近的生成记录:', allGenerations.map(g => `ID=${g.id}, generation_id=${g.generation_id}`).join(', '));\n }\n } else {\n console.log('回调数据中未找到taskId');\n }\n\n // 返回成功响应\n return sendJsonResponse(res, { success: true });\n } catch (error) {\n console.error('处理Suno API回调失败:', error);\n return sendJsonResponse(res, { success: false, message: error.message || '处理回调失败' }, 500);\n }\n }\n\n // 生成AI音乐\n if (path === '/api/music/generate' && req.method === 'POST') {\n console.log('收到生成AI音乐请求');\n const body = await parseJsonBody(req);\n console.log('请求体:', {\n title: body.title,\n artist: body.artist,\n lyrics: body.lyrics ? body.lyrics.substring(0, 50) + '...' : null,\n style: body.style,\n mood: body.mood\n });\n\n if (!body.lyrics) {\n console.log('错误: 歌词为空');\n return sendJsonResponse(res, { success: false, message: '歌词不能为空' }, 400);\n }\n\n if (!body.title) {\n console.log('错误: 歌曲标题为空');\n return sendJsonResponse(res, { success: false, message: '歌曲标题不能为空' }, 400);\n }\n\n // 准备生成选项\n const musicOptions = {\n title: body.title,\n artist: body.artist || '未知歌手',\n lyrics: body.lyrics,\n style: body.style || 'pop',\n mood: body.mood || 'happy',\n api: body.api || 'suno-api',\n modelVersion: body.modelVersion || 'v3'\n };\n\n console.log('音乐生成选项:', musicOptions);\n\n try {\n // 生成AI音乐\n console.log('开始生成AI音乐...');\n const musicResult = await generateMusic(musicOptions);\n console.log('生成完成,结果:', musicResult);\n\n // 构建响应数据\n let responseData;\n\n if (musicResult.success === false) {\n // 处理失败情况\n responseData = {\n success: false,\n message: musicResult.message || '音乐生成请求失败',\n error: musicResult.error\n };\n } else {\n // 处理成功情况\n responseData = {\n success: true,\n data: musicResult.data || {\n generationId: musicResult.generationId,\n status: musicResult.status || 'processing'\n }\n };\n }\n\n console.log('发送响应:', responseData);\n\n return sendJsonResponse(res, responseData);\n } catch (error) {\n console.error('生成AI音乐失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || '生成AI音乐失败'\n }, 500);\n }\n }\n\n // 使用 MiniMax API 生成AI音乐\n if (path === '/api/music/generate-minimax' && req.method === 'POST') {\n console.log('收到 MiniMax AI 音乐生成请求');\n const body = await parseJsonBody(req);\n console.log('请求体:', {\n title: body.title,\n artist: body.artist,\n lyrics: body.lyrics ? body.lyrics.substring(0, 50) + '...' : null,\n style: body.style,\n mood: body.mood,\n apiKey: body.apiKey ? '已提供' : '未提供'\n });\n\n if (!body.lyrics) {\n console.log('错误: 歌词为空');\n return sendJsonResponse(res, { success: false, message: '歌词不能为空' }, 400);\n }\n\n if (!body.title) {\n console.log('错误: 歌曲标题为空');\n return sendJsonResponse(res, { success: false, message: '歌曲标题不能为空' }, 400);\n }\n\n // 检查 API Key\n if (!process.env.MINIMAX_API_KEY && !body.apiKey) {\n console.log('错误: MiniMax API Key 未配置');\n return sendJsonResponse(res, {\n success: false,\n message: 'MiniMax API Key 未配置,请联系管理员或在配置中设置 MINIMAX_API_KEY'\n }, 400);\n }\n\n // 准备生成选项\n const musicOptions = {\n title: body.title,\n artist: body.artist || '未知歌手',\n lyrics: body.lyrics,\n style: body.style || '流行音乐',\n mood: body.mood || '欢快',\n apiKey: body.apiKey || process.env.MINIMAX_API_KEY,\n model: body.model || 'music-2.5',\n outputFormat: body.outputFormat || 'url',\n userId: body.userId || null\n };\n\n console.log('MiniMax 音乐生成选项:', {\n ...musicOptions,\n apiKey: musicOptions.apiKey ? '***' : null\n });\n\n try {\n // 调用 MiniMax 生成音乐\n console.log('开始调用 MiniMax 生成音乐...');\n const musicResult = await createMiniMaxMusicGeneration(musicOptions);\n console.log('生成完成,结果:', musicResult);\n\n const statusCode = musicResult?.data?.unsupportedModel ? 200 : 200;\n return sendJsonResponse(res, musicResult, statusCode);\n } catch (error) {\n console.error('MiniMax 音乐生成失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message || 'MiniMax 音乐生成失败'\n }, 500);\n }\n }\n\n // 获取 MiniMax 音乐生成状态\n if (path.startsWith('/api/music/minimax-status/') && req.method === 'GET') {\n console.log('收到获取 MiniMax 音乐生成状态请求');\n const generationId = parseInt(path.split('/').pop());\n\n if (!generationId) {\n return sendJsonResponse(res, { success: false, message: '生成ID不能为空' }, 400);\n }\n\n try {\n const status = await getMiniMaxMusicStatus(generationId);\n return sendJsonResponse(res, status);\n } catch (error) {\n console.error('获取 MiniMax 状态失败:', error);\n return sendJsonResponse(res, {\n success: false,\n message: error.message\n }, 500);\n }\n }\n\n // 系统状态信息\n if (path === '/api/system/stats' && req.method === 'GET') {\n const stats = await getLyricsStats();\n return sendJsonResponse(res, { success: true, data: stats });\n }\n\n // API状态检查\n if (path === '/api/status' && req.method === 'GET') {\n return sendJsonResponse(res, { 状态: '正常', 版本: '1.0.0' });\n }\n\n // 直接的验证码发送路由 - 用于测试\n if (path === '/api/auth/v2/send-code-direct' && req.method === 'POST') {\n try {\n // 解析请求体\n const body = await parseJsonBody(req);\n console.log('收到直接发送验证码请求:', body);\n\n const { mobile } = body;\n\n if (!mobile) {\n console.log('手机号为空');\n return sendJsonResponse(res, { success: false, message: '手机号不能为空' }, 400);\n }\n\n // 生成6位验证码\n const code = Math.floor(100000 + Math.random() * 900000).toString();\n console.log(`[测试模式] 向 ${mobile} 发送验证码: ${code}`);\n\n // 将验证码保存到数据库\n const expiresAt = new Date();\n expiresAt.setMinutes(expiresAt.getMinutes() + 5);\n\n try {\n await saveLyricsReplacement({\n title: '验证码',\n artist: mobile,\n originalLyrics: code,\n replacedLyrics: code,\n style: '验证码',\n timestamp: new Date()\n });\n console.log('验证码已保存到数据库');\n } catch (dbError) {\n console.error('保存验证码到数据库失败:', dbError);\n }\n\n return sendJsonResponse(res, {\n success: true,\n message: '验证码已发送(测试模式)',\n code: code // 仅在测试模式下返回验证码\n });\n } catch (error) {\n console.error('发送验证码失败:', error);\n return sendJsonResponse(res, { success: false, message: '服务器错误' }, 500);\n }\n }\n\n // 密码登录路由\n if (path === '/api/auth/login' && req.method === 'POST') {\n try {\n // 解析请求体\n const body = await parseJsonBody(req);\n console.log('收到密码登录请求:', body);\n\n const { mobile, password } = body;\n\n if (!mobile || !password) {\n console.log('手机号或密码为空');\n return sendJsonResponse(res, { success: false, message: '手机号和密码不能为空' }, 400);\n }\n\n // 查询用户是否存在\n const [users] = await query('SELECT * FROM users WHERE mobile = ?', [mobile]);\n\n if (!users || users.length === 0) {\n return sendJsonResponse(res, { success: false, message: '用户不存在,请先注册' }, 400);\n }\n\n const user = users[0];\n\n // 生成JWT令牌\n const token = 'token_' + Date.now();\n\n return sendJsonResponse(res, {\n success: true,\n message: '登录成功',\n data: {\n token,\n user: {\n id: user.id,\n username: user.username || `用户${mobile.substring(mobile.length - 4)}`,\n mobile: user.mobile,\n created_at: user.created_at,\n has_password: true\n }\n }\n });\n } catch (error) {\n console.error('登录失败:', error);\n return sendJsonResponse(res, { success: false, message: '服务器错误' }, 500);\n }\n }\n\n // 验证码登录路由\n if (path === '/api/auth/v2/login' && req.method === 'POST') {\n try {\n // 解析请求体\n const body = await parseJsonBody(req);\n console.log('收到验证码登录请求:', body);\n\n const { mobile, code } = body;\n\n if (!mobile || !code) {\n console.log('手机号或验证码为空');\n return sendJsonResponse(res, { success: false, message: '手机号和验证码不能为空' }, 400);\n }\n\n // 验证验证码\n const [codes] = await query(\n 'SELECT * FROM sms_verification WHERE mobile = ? AND code = ? AND expires_at > NOW() AND is_used = FALSE ORDER BY created_at DESC LIMIT 1',\n [mobile, code]\n );\n\n if (!codes || codes.length === 0) {\n return sendJsonResponse(res, { success: false, message: '验证码无效或已过期' }, 400);\n }\n\n // 标记验证码为已使用\n await execute(\n 'UPDATE sms_verification SET is_used = TRUE WHERE id = ?',\n [codes[0].id]\n );\n\n // 查询用户是否存在\n const [users] = await query('SELECT * FROM users WHERE mobile = ?', [mobile]);\n let userId;\n let user;\n\n if (users && users.length > 0) {\n // 用户存在,执行登录\n user = users[0];\n userId = user.id;\n } else {\n // 用户不存在,创建新用户\n const username = `用户${mobile.substring(mobile.length - 4)}`;\n const [result] = await execute(\n 'INSERT INTO users (mobile, username, created_at) VALUES (?, ?, NOW())',\n [mobile, username]\n );\n userId = result.insertId;\n\n // 获取新创建的用户信息\n const [newUsers] = await query('SELECT * FROM users WHERE id = ?', [userId]);\n user = newUsers[0];\n }\n\n // 生成JWT令牌\n const token = 'token_' + Date.now();\n\n return sendJsonResponse(res, {\n success: true,\n message: '登录成功',\n data: {\n token,\n user: {\n id: user.id,\n username: user.username,\n mobile: user.mobile,\n created_at: user.created_at,\n has_password: Boolean(user.password)\n }\n }\n });\n } catch (error) {\n console.error('验证码登录失败:', error);\n return sendJsonResponse(res, { success: false, message: '服务器错误' }, 500);\n }\n }\n\n // 处理SSE事件流\n if (path === '/api/events') {\n try {\n // 导入events模块\n const { eventEmitter } = await import('./routes/events.js');\n\n // 设置SSE响应头\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type'\n });\n\n // 生成客户端ID\n const clientId = Date.now().toString();\n console.log(`SSE客户端连接: ${clientId}`);\n\n // 发送初始连接成功消息\n const connectEvent = {\n type: 'connect',\n clientId,\n message: '连接成功',\n timestamp: new Date().toISOString()\n };\n\n res.write(`data: ${JSON.stringify(connectEvent)}\\n\\n`);\n\n // 创建事件处理函数\n const handleEvent = (data) => {\n try {\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n } catch (error) {\n console.error('发送事件失败:', error);\n }\n };\n\n // 添加事件监听器\n eventEmitter.on('search', handleEvent);\n eventEmitter.on('generate', handleEvent);\n\n // 设置心跳检测,每30秒发送一次\n const heartbeatInterval = setInterval(() => {\n try {\n res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: new Date().toISOString() })}\\n\\n`);\n } catch (error) {\n console.error('发送心跳失败:', error);\n clearInterval(heartbeatInterval);\n }\n }, 30000);\n\n // 监听请求关闭\n req.on('close', () => {\n console.log(`SSE客户端断开连接: ${clientId}`);\n clearInterval(heartbeatInterval);\n eventEmitter.off('search', handleEvent);\n eventEmitter.off('generate', handleEvent);\n });\n\n // 监听错误\n req.on('error', (error) => {\n console.error(`SSE客户端连接错误: ${clientId}`, error);\n clearInterval(heartbeatInterval);\n eventEmitter.off('search', handleEvent);\n eventEmitter.off('generate', handleEvent);\n });\n\n // 保持连接开启\n return;\n } catch (error) {\n console.error('SSE处理错误:', error);\n return sendJsonResponse(res, { success: false, message: '服务器内部错误' }, 500);\n }\n }\n\n // 404 - 未找到对应的API路由\n return sendJsonResponse(res, { success: false, message: '未找到对应的API接口' }, 404);\n\n } catch (error) {\n console.error('API处理错误:', error);\n return sendJsonResponse(res, { success: false, message: error.message || '服务器内部错误' }, 500);\n }\n};\n\n// 处理静态文件\nconst serveStaticFile = (res, filePath, contentType) => {\n try {\n const content = fs.readFileSync(filePath);\n res.statusCode = 200;\n res.setHeader('Content-Type', contentType);\n res.end(content);\n } catch (error) {\n console.error('静态文件读取错误:', error);\n res.statusCode = 404;\n res.end('File not found');\n }\n};\n\n// 初始化应用\nconst startApp = async () => {\n try {\n // 初始化数据库\n console.log('正在初始化MySQL数据库...');\n await initDatabase();\n\n // 初始化歌词替换表\n console.log('正在初始化歌词替换表...');\n await initLyricsReplacementsTable();\n\n // 创建HTTP服务器\n const server = http.createServer(async (req, res) => {\n const parsedUrl = url.parse(req.url, true);\n const path = parsedUrl.pathname;\n\n // 处理API请求\n if (path.startsWith('/api/')) {\n return await handleApiRequest(req, res, parsedUrl);\n }\n\n // 静态文件处理\n const frontendPath = join(__dirname, '../frontend/dist');\n\n // 首页或其他前端路由\n if (path === '/' || !path.includes('.')) {\n return serveStaticFile(res, join(frontendPath, 'index.html'), 'text/html');\n }\n\n // 静态资源文件\n const filePath = join(frontendPath, path);\n\n // 确定Content-Type\n const extname = String(path.split('.').pop()).toLowerCase();\n const contentTypes = {\n '.html': 'text/html',\n '.js': 'text/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.wav': 'audio/wav',\n '.mp3': 'audio/mpeg',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.eot': 'application/vnd.ms-fontobject',\n '.otf': 'font/otf'\n };\n\n const contentType = contentTypes[extname] || 'text/plain';\n return serveStaticFile(res, filePath, contentType);\n });\n\n // 启动服务器,自动寻找可用端口\n let port = parseInt(process.env.PORT || 3030);\n const maxPortRetry = 10; // 最多尝试10个端口\n\n for (let attempt = 0; attempt < maxPortRetry; attempt++) {\n try {\n await new Promise((resolve, reject) => {\n server.once('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n console.log(`端口 ${port} 已被占用,尝试下一个端口...`);\n port++;\n resolve();\n } else {\n reject(err);\n }\n });\n\n server.listen(port, () => {\n console.log(`🎵 歌词替换API已在 http://localhost:${port} 运行`);\n resolve();\n });\n });\n\n break; // 成功监听端口,跳出循环\n } catch (err) {\n console.error('端口监听错误:', err);\n if (attempt === maxPortRetry - 1) {\n throw new Error(`尝试了 ${maxPortRetry} 个端口后仍无法启动服务器`);\n }\n }\n }\n } catch (error) {\n console.error('应用启动失败:', error);\n process.exit(1);\n }\n};\n\n// 启动应用\nstartApp();"}}}}