{"repo": "bingyanglu/claudio-fm", "files": {"llm.js": {"4939b1adc5ddc5ed1289a1e93cd0235b98634abe": {"t": 1785745516.9611857, "n": 5307, "h": "d10cd19b8a965cac", "c": "const { spawn } = require('child_process');\n\nconst DEFAULT_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS || 120000);\nconst DEFAULT_PROVIDER = process.env.LLM_PROVIDER || 'deepseek';\nconst DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash';\nconst DEEPSEEK_REASONING_EFFORT = process.env.DEEPSEEK_REASONING_EFFORT || '';\nconst DEEPSEEK_THINKING = process.env.DEEPSEEK_THINKING || '';\n\nasync function generateJson(prompt, options = {}) {\n const provider = options.provider || DEFAULT_PROVIDER;\n if (provider === 'deepseek') return callDeepSeek(prompt, options);\n if (provider === 'claude_cli') return callClaudeCli(prompt, options);\n throw new Error(`Unsupported LLM_PROVIDER: ${provider}`);\n}\n\nasync function callDeepSeek(prompt, options = {}) {\n if (!process.env.DEEPSEEK_API_KEY) {\n throw new Error('DEEPSEEK_API_KEY not set');\n }\n\n const OpenAI = await loadOpenAI();\n const client = new OpenAI({\n baseURL: process.env.DEEPSEEK_BASE_URL || DEEPSEEK_BASE_URL,\n apiKey: process.env.DEEPSEEK_API_KEY,\n });\n const model = options.model || process.env.DEEPSEEK_MODEL || DEEPSEEK_MODEL;\n const startAt = Date.now();\n console.log(`[LLM:deepseek] 调用中,model ${model},prompt ${prompt.length} 字符…`);\n\n const request = {\n model,\n messages: [\n { role: 'system', content: 'You are Claudio FM. Return strict JSON only.' },\n { role: 'user', content: prompt },\n ],\n stream: false,\n };\n if (DEEPSEEK_THINKING) request.thinking = { type: DEEPSEEK_THINKING };\n if (DEEPSEEK_REASONING_EFFORT) request.reasoning_effort = DEEPSEEK_REASONING_EFFORT;\n\n const completion = await withTimeout(\n client.chat.completions.create(request),\n options.timeoutMs || DEFAULT_TIMEOUT_MS,\n `DeepSeek request timed out after ${Math.round((options.timeoutMs || DEFAULT_TIMEOUT_MS) / 1000)}s`\n );\n const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);\n const raw = completion.choices?.[0]?.message?.content?.trim() || '';\n const parsed = parseResponse(raw);\n logParsedResponse('deepseek', elapsed, parsed, raw);\n return parsed;\n}\n\nasync function loadOpenAI() {\n try {\n const mod = await import('openai');\n return mod.default || mod.OpenAI || mod;\n } catch (err) {\n throw new Error('OpenAI SDK not installed. Run `yarn add openai` or `npm install openai`.');\n }\n}\n\nfunction callClaudeCli(prompt, options = {}) {\n const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;\n const startAt = Date.now();\n console.log(`[LLM:claude_cli] 调用中,prompt ${prompt.length} 字符…`);\n return new Promise((resolve, reject) => {\n const proc = spawn('claude', ['-p', prompt], {\n env: { ...process.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n const timer = setTimeout(() => {\n proc.kill();\n const stderrPreview = stderr.trim().slice(-800);\n console.error(`[LLM:claude_cli] 超时(${Math.round(timeoutMs / 1000)}s),已终止;prompt ${prompt.length} 字符`);\n if (stderrPreview) console.error(`[LLM:claude_cli] stderr 摘要: ${stderrPreview}`);\n reject(new Error('Claude subprocess timed out'));\n }, timeoutMs);\n\n proc.stdout.on('data', d => { stdout += d.toString(); });\n proc.stderr.on('data', d => {\n stderr += d.toString();\n process.stderr.write(d);\n });\n\n proc.on('close', () => {\n clearTimeout(timer);\n const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);\n const raw = stdout.trim();\n const parsed = parseResponse(raw);\n logParsedResponse('claude_cli', elapsed, parsed, raw);\n if (!raw) console.warn('[LLM:claude_cli] 警告:返回内容为空');\n resolve(parsed);\n });\n\n proc.on('error', err => {\n clearTimeout(timer);\n console.error('[LLM:claude_cli] 进程错误:', err.message);\n reject(err);\n });\n });\n}\n\nfunction withTimeout(promise, timeoutMs, message) {\n let timer;\n const timeout = new Promise((_, reject) => {\n timer = setTimeout(() => reject(new Error(message)), timeoutMs);\n });\n return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));\n}\n\nfunction parseResponse(raw) {\n const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n return {\n title: parsed.title || '',\n say: parsed.say || '',\n play: Array.isArray(parsed.play) ? parsed.play : [],\n segments: Array.isArray(parsed.segments) ? parsed.segments : [],\n intros: Array.isArray(parsed.intros) ? parsed.intros : [],\n reason: parsed.reason || '',\n mode: parsed.mode || '',\n };\n } catch {}\n }\n return { title: '', say: raw || 'Okay.', play: [], segments: [], intros: [], reason: '', segue: '', mode: '' };\n}\n\nfunction logParsedResponse(provider, elapsed, parsed, raw) {\n const firstSegment = parsed.segments?.find(s => s?.text)?.text || parsed.say || '';\n const preview = firstSegment.slice(0, 60);\n console.log(`[LLM:${provider}] 响应 (${elapsed}s) → 「${parsed.title || '无标题'}」| ${parsed.play?.length || 0} 首 | segments: ${parsed.segments?.length || 0} | \"${preview}${preview.length >= 60 ? '…' : ''}\"`);\n if (!raw) console.warn(`[LLM:${provider}] 警告:返回内容为空`);\n}\n\nmodule.exports = { generateJson, parseResponse };\n"}, "1ffdd7c1051b5a447028fff57e99f461a085b36f": {"t": 1785753769.1064897, "n": 5307, "h": "d10cd19b8a965cac", "c": "const { spawn } = require('child_process');\n\nconst DEFAULT_TIMEOUT_MS = Number(process.env.LLM_TIMEOUT_MS || 120000);\nconst DEFAULT_PROVIDER = process.env.LLM_PROVIDER || 'deepseek';\nconst DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';\nconst DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash';\nconst DEEPSEEK_REASONING_EFFORT = process.env.DEEPSEEK_REASONING_EFFORT || '';\nconst DEEPSEEK_THINKING = process.env.DEEPSEEK_THINKING || '';\n\nasync function generateJson(prompt, options = {}) {\n const provider = options.provider || DEFAULT_PROVIDER;\n if (provider === 'deepseek') return callDeepSeek(prompt, options);\n if (provider === 'claude_cli') return callClaudeCli(prompt, options);\n throw new Error(`Unsupported LLM_PROVIDER: ${provider}`);\n}\n\nasync function callDeepSeek(prompt, options = {}) {\n if (!process.env.DEEPSEEK_API_KEY) {\n throw new Error('DEEPSEEK_API_KEY not set');\n }\n\n const OpenAI = await loadOpenAI();\n const client = new OpenAI({\n baseURL: process.env.DEEPSEEK_BASE_URL || DEEPSEEK_BASE_URL,\n apiKey: process.env.DEEPSEEK_API_KEY,\n });\n const model = options.model || process.env.DEEPSEEK_MODEL || DEEPSEEK_MODEL;\n const startAt = Date.now();\n console.log(`[LLM:deepseek] 调用中,model ${model},prompt ${prompt.length} 字符…`);\n\n const request = {\n model,\n messages: [\n { role: 'system', content: 'You are Claudio FM. Return strict JSON only.' },\n { role: 'user', content: prompt },\n ],\n stream: false,\n };\n if (DEEPSEEK_THINKING) request.thinking = { type: DEEPSEEK_THINKING };\n if (DEEPSEEK_REASONING_EFFORT) request.reasoning_effort = DEEPSEEK_REASONING_EFFORT;\n\n const completion = await withTimeout(\n client.chat.completions.create(request),\n options.timeoutMs || DEFAULT_TIMEOUT_MS,\n `DeepSeek request timed out after ${Math.round((options.timeoutMs || DEFAULT_TIMEOUT_MS) / 1000)}s`\n );\n const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);\n const raw = completion.choices?.[0]?.message?.content?.trim() || '';\n const parsed = parseResponse(raw);\n logParsedResponse('deepseek', elapsed, parsed, raw);\n return parsed;\n}\n\nasync function loadOpenAI() {\n try {\n const mod = await import('openai');\n return mod.default || mod.OpenAI || mod;\n } catch (err) {\n throw new Error('OpenAI SDK not installed. Run `yarn add openai` or `npm install openai`.');\n }\n}\n\nfunction callClaudeCli(prompt, options = {}) {\n const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;\n const startAt = Date.now();\n console.log(`[LLM:claude_cli] 调用中,prompt ${prompt.length} 字符…`);\n return new Promise((resolve, reject) => {\n const proc = spawn('claude', ['-p', prompt], {\n env: { ...process.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let stdout = '';\n let stderr = '';\n const timer = setTimeout(() => {\n proc.kill();\n const stderrPreview = stderr.trim().slice(-800);\n console.error(`[LLM:claude_cli] 超时(${Math.round(timeoutMs / 1000)}s),已终止;prompt ${prompt.length} 字符`);\n if (stderrPreview) console.error(`[LLM:claude_cli] stderr 摘要: ${stderrPreview}`);\n reject(new Error('Claude subprocess timed out'));\n }, timeoutMs);\n\n proc.stdout.on('data', d => { stdout += d.toString(); });\n proc.stderr.on('data', d => {\n stderr += d.toString();\n process.stderr.write(d);\n });\n\n proc.on('close', () => {\n clearTimeout(timer);\n const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);\n const raw = stdout.trim();\n const parsed = parseResponse(raw);\n logParsedResponse('claude_cli', elapsed, parsed, raw);\n if (!raw) console.warn('[LLM:claude_cli] 警告:返回内容为空');\n resolve(parsed);\n });\n\n proc.on('error', err => {\n clearTimeout(timer);\n console.error('[LLM:claude_cli] 进程错误:', err.message);\n reject(err);\n });\n });\n}\n\nfunction withTimeout(promise, timeoutMs, message) {\n let timer;\n const timeout = new Promise((_, reject) => {\n timer = setTimeout(() => reject(new Error(message)), timeoutMs);\n });\n return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));\n}\n\nfunction parseResponse(raw) {\n const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]);\n return {\n title: parsed.title || '',\n say: parsed.say || '',\n play: Array.isArray(parsed.play) ? parsed.play : [],\n segments: Array.isArray(parsed.segments) ? parsed.segments : [],\n intros: Array.isArray(parsed.intros) ? parsed.intros : [],\n reason: parsed.reason || '',\n mode: parsed.mode || '',\n };\n } catch {}\n }\n return { title: '', say: raw || 'Okay.', play: [], segments: [], intros: [], reason: '', segue: '', mode: '' };\n}\n\nfunction logParsedResponse(provider, elapsed, parsed, raw) {\n const firstSegment = parsed.segments?.find(s => s?.text)?.text || parsed.say || '';\n const preview = firstSegment.slice(0, 60);\n console.log(`[LLM:${provider}] 响应 (${elapsed}s) → 「${parsed.title || '无标题'}」| ${parsed.play?.length || 0} 首 | segments: ${parsed.segments?.length || 0} | \"${preview}${preview.length >= 60 ? '…' : ''}\"`);\n if (!raw) console.warn(`[LLM:${provider}] 警告:返回内容为空`);\n}\n\nmodule.exports = { generateJson, parseResponse };\n"}}}}