{"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();"}}}}