lifekline / server /analyzeStream.js
xiaobo ren
Fix gpt-5-mini temperature parameter and update API configurations
1f1edd2
import fetch from 'node-fetch';
import { nanoid } from 'nanoid';
import {
updateUserPoints,
saveUserInput,
saveAnalysis,
logEvent,
} from './database.js';
import { BAZI_SYSTEM_INSTRUCTION, buildUserPrompt } from './prompt.js';
import { calculateLifeTimeline } from './baziCalculator.js';
import { buildApiRequest, parseApiResponse } from './apiConfig.js';
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gpt-5-mini';
// 备选模型列表 - 用于并发请求和降级
const ALL_MODELS = [
'gpt-5-mini',
'gpt-4.1',
'gpt-4o',
'grok-4-0709',
'grok-4-1-fast-reasoning',
'claude-sonnet-4-5',
'claude-3-5-haiku-20241022',
];
const COST_PER_ANALYSIS = process.env.COST_PER_ANALYSIS ? parseInt(process.env.COST_PER_ANALYSIS, 10) : 50;
/**
* 发送SSE事件到客户端
*/
const sendSSE = (res, event, data) => {
if (!res.writableEnded) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
};
/**
* 单次API请求 - 返回Promise
*/
const makeModelRequest = async (model, userPrompt, timeoutMs = 120000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
console.log(`[${model}] 开始请求...`);
const startTime = Date.now();
// 使用新的 API 配置系统
const apiRequest = buildApiRequest(model, BAZI_SYSTEM_INSTRUCTION, userPrompt, 0.7);
const response = await fetch(apiRequest.url, {
method: 'POST',
headers: apiRequest.headers,
signal: controller.signal,
body: JSON.stringify(apiRequest.body),
});
clearTimeout(timeoutId);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (!response.ok) {
const errText = await response.text();
console.warn(`[${model}] 请求失败 (${elapsed}s): ${response.status} - ${errText.substring(0, 100)}`);
return { success: false, model, error: `HTTP ${response.status}`, elapsed };
}
const responseText = await response.text();
let jsonResult;
try {
jsonResult = JSON.parse(responseText);
} catch (e) {
console.warn(`[${model}] JSON解析失败 (${elapsed}s): ${responseText.substring(0, 100)}`);
return { success: false, model, error: 'INVALID_API_RESPONSE', elapsed };
}
// 使用新的响应解析系统
const content = parseApiResponse(jsonResult, model);
if (!content) {
console.warn(`[${model}] 无内容返回 (${elapsed}s)`);
return { success: false, model, error: 'EMPTY_RESPONSE', elapsed };
}
// 清理内容
content = content.trim();
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
content = content.replace(/^[\s\S]*?(?=\{)/m, '');
if (content.startsWith('```json')) content = content.slice(7);
else if (content.startsWith('```')) content = content.slice(3);
if (content.endsWith('```')) content = content.slice(0, -3);
content = content.trim();
const jsonStart = content.indexOf('{');
const jsonEnd = content.lastIndexOf('}');
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
content = content.slice(jsonStart, jsonEnd + 1);
}
let data;
try {
data = JSON.parse(content);
} catch (parseErr) {
console.warn(`[${model}] 内容JSON解析失败 (${elapsed}s): ${content.substring(0, 100)}`);
return { success: false, model, error: 'INVALID_JSON_FORMAT', elapsed };
}
if (!data.chartPoints || !Array.isArray(data.chartPoints)) {
console.warn(`[${model}] 数据结构错误 (${elapsed}s): 缺少chartPoints`);
return { success: false, model, error: 'INVALID_DATA_STRUCTURE', elapsed };
}
console.log(`[${model}] ✓ 成功 (${elapsed}s)`);
return { success: true, model, data, elapsed };
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.warn(`[${model}] 请求超时`);
return { success: false, model, error: 'TIMEOUT' };
}
console.warn(`[${model}] 请求异常: ${error.message}`);
return { success: false, model, error: error.message };
}
};
/**
* 并发请求多个模型,返回第一个成功的结果
*/
const raceModels = async (models, userPrompt, onProgress) => {
onProgress(`正在并发请求 ${models.length} 个模型...`);
// 创建所有请求的Promise
const promises = models.map(model =>
makeModelRequest(model, userPrompt, 180000)
);
// 使用Promise.allSettled等待所有请求完成,但我们会在第一个成功时就返回
// 同时使用一个自定义的race逻辑
return new Promise((resolve) => {
let resolved = false;
const results = [];
let completedCount = 0;
promises.forEach((promise, index) => {
promise.then(result => {
completedCount++;
results.push(result);
if (result.success && !resolved) {
resolved = true;
onProgress(`✓ 模型 ${result.model} 响应成功 (${result.elapsed}s)`);
resolve(result);
} else if (!result.success) {
onProgress(`✗ 模型 ${result.model} 失败: ${result.error}`);
}
// 如果所有请求都完成了但没有成功的
if (completedCount === promises.length && !resolved) {
resolve({ success: false, results });
}
});
});
});
};
/**
* 流式分析处理器
*/
export const handleAnalyzeStream = async (req, res) => {
// 设置SSE响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
const body = req.body || {};
const useCustomApi = Boolean(body.useCustomApi);
let authedInfo = req.__authedInfo || null;
let apiBaseUrl = String(body.apiBaseUrl || '').trim().replace(/\/+$/, '');
let apiKey = String(body.apiKey || '').trim();
let modelName = String(body.modelName || '').trim();
const input = {
name: body.name || '',
birthPlace: body.birthPlace || '',
gender: body.gender,
birthYear: body.birthYear,
yearPillar: body.yearPillar,
monthPillar: body.monthPillar,
dayPillar: body.dayPillar,
hourPillar: body.hourPillar,
startAge: body.startAge,
firstDaYun: body.firstDaYun,
};
if (!useCustomApi) {
// 不再需要固定的 API URL 和 Key,由 apiConfig.js 根据模型自动选择
apiBaseUrl = null; // 不再使用
apiKey = null; // 不再使用
modelName = DEFAULT_MODEL;
// API 配置由 apiConfig.js 管理,无需检查
if (false) {
sendSSE(res, 'error', {
error: 'SERVER_DEFAULT_KEY_NOT_SET',
message: '服务器未配置API密钥,请使用自定义API或联系管理员'
});
return res.end();
}
} else {
if (!apiBaseUrl || !apiKey || !modelName) {
sendSSE(res, 'error', {
error: 'MISSING_CUSTOM_API_CONFIG',
message: '请完整填写自定义API配置'
});
return res.end();
}
}
const inputId = nanoid();
const startTime = Date.now();
// 发送初始化进度
sendSSE(res, 'progress', { message: '正在初始化...' });
// 启动心跳保活
const keepAliveInterval = setInterval(() => {
if (!res.writableEnded) {
res.write(': keep-alive\n\n');
}
}, 10000); // 每10秒
const cleanup = () => clearInterval(keepAliveInterval);
res.on('close', cleanup);
res.on('finish', cleanup);
// 进度回调
const onProgress = (message) => {
sendSSE(res, 'progress', { message });
};
// 预计算生命周期骨架 (Skeleton)
let skeletonData = null;
try {
skeletonData = calculateLifeTimeline(input);
onProgress('已生成 100 年流年骨架...');
} catch (err) {
console.error('骨架计算失败:', err);
sendSSE(res, 'error', {
error: 'SKELETON_CALC_FAILED',
message: '流年骨架计算失败,请检查输入数据'
});
return res.end();
}
const userPrompt = String(body.userPrompt || '').trim() || buildUserPrompt({ ...input, gender: input.gender }, skeletonData);
let result = null;
let usedModel = null;
if (useCustomApi) {
// 自定义API模式 - 只使用用户指定的模型,带重试
onProgress(`使用自定义模型: ${modelName}`);
for (let attempt = 1; attempt <= 3; attempt++) {
onProgress(`尝试第 ${attempt} 次...`);
const response = await makeModelRequest(modelName, userPrompt, 60000);
if (response.success) {
result = response.data;
usedModel = modelName;
onProgress(`✓ 成功获取结果`);
break;
} else {
onProgress(`✗ 第 ${attempt} 次失败: ${response.error}`);
if (attempt < 3) {
onProgress('等待1秒后重试...');
await new Promise(r => setTimeout(r, 1000));
}
}
}
} else {
// 免费模式 - 并发请求多个模型
onProgress('启动多模型并发请求策略...');
// 第一轮:并发请求主模型和一个备选模型
const firstRoundModels = [modelName, 'gpt-4o'];
let raceResult = await raceModels(firstRoundModels, userPrompt, onProgress);
if (raceResult.success) {
result = raceResult.data;
usedModel = raceResult.model;
} else {
// 第二轮:尝试其他模型
onProgress('第一轮失败,启动第二轮备选模型...');
const secondRoundModels = ['grok-4', 'claude-3-5-sonnet-20241022'];
raceResult = await raceModels(secondRoundModels, userPrompt, onProgress);
if (raceResult.success) {
result = raceResult.data;
usedModel = raceResult.model;
}
}
// 如果还是失败,最后尝试逐个请求
if (!result) {
onProgress('并发请求全部失败,尝试逐个请求...');
for (const model of ALL_MODELS) {
onProgress(`最后尝试: ${model}...`);
const response = await makeModelRequest(model, userPrompt, 45000);
if (response.success) {
result = response.data;
usedModel = model;
onProgress(`✓ 终于成功: ${model}`);
break;
}
}
}
}
if (!result) {
console.error('所有模型均失败');
sendSSE(res, 'error', {
error: 'ALL_MODELS_FAILED',
message: '所有AI模型均无法响应,请稍后重试或使用自定义API'
});
return res.end();
}
onProgress('正在处理命理数据...');
const finalResult = {
chartData: result.chartPoints,
analysis: {
bazi: result.bazi || [],
summary: result.summary || '无摘要',
summaryScore: result.summaryScore || 5,
personality: result.personality || '无性格分析',
personalityScore: result.personalityScore || 5,
industry: result.industry || '无',
industryScore: result.industryScore || 5,
fengShui: result.fengShui || '建议多亲近自然,保持心境平和。',
fengShuiScore: result.fengShuiScore || 5,
wealth: result.wealth || '无',
wealthScore: result.wealthScore || 5,
marriage: result.marriage || '无',
marriageScore: result.marriageScore || 5,
health: result.health || '无',
healthScore: result.healthScore || 5,
family: result.family || '无',
familyScore: result.familyScore || 5,
crypto: result.crypto || '暂无交易分析',
cryptoScore: result.cryptoScore || 5,
cryptoYear: result.cryptoYear || '待定',
cryptoStyle: result.cryptoStyle || '现货定投',
},
};
let user = null;
let cost = 0;
let isGuest = false;
onProgress('保存分析结果...');
// 保存数据
if (!useCustomApi) {
const info = authedInfo;
saveUserInput({
id: inputId,
userId: info ? info.user.id : null,
name: input.name,
gender: input.gender,
birthYear: input.birthYear,
yearPillar: input.yearPillar,
monthPillar: input.monthPillar,
dayPillar: input.dayPillar,
hourPillar: input.hourPillar,
startAge: input.startAge,
firstDaYun: input.firstDaYun,
modelName: usedModel,
apiBaseUrl: apiBaseUrl,
useCustomApi: false,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
});
const analysisId = nanoid();
if (info) {
const newPoints = Math.max(0, info.user.points - COST_PER_ANALYSIS);
updateUserPoints(info.user.id, newPoints);
cost = COST_PER_ANALYSIS;
saveAnalysis({
id: analysisId,
userId: info.user.id,
inputId: inputId,
cost,
modelUsed: usedModel,
chartData: finalResult.chartData,
analysisData: finalResult.analysis,
processingTimeMs: Date.now() - startTime,
status: 'completed',
});
logEvent('info', '生成分析', { analysisId, cost, model: usedModel }, info.user.id, req.ip);
user = { id: info.user.id, email: info.user.email, points: newPoints };
} else {
isGuest = true;
saveAnalysis({
id: analysisId,
userId: null,
inputId: inputId,
cost: 0,
modelUsed: usedModel,
chartData: finalResult.chartData,
analysisData: finalResult.analysis,
processingTimeMs: Date.now() - startTime,
status: 'completed',
});
logEvent('info', '游客体验', { analysisId, model: usedModel }, null, req.ip);
}
} else {
saveUserInput({
id: inputId,
userId: null,
name: input.name,
gender: input.gender,
birthYear: input.birthYear,
yearPillar: input.yearPillar,
monthPillar: input.monthPillar,
dayPillar: input.dayPillar,
hourPillar: input.hourPillar,
startAge: input.startAge,
firstDaYun: input.firstDaYun,
modelName: modelName,
apiBaseUrl: apiBaseUrl,
useCustomApi: true,
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
});
}
// 发送完成事件
sendSSE(res, 'complete', { result: finalResult, user, cost, isGuest, modelUsed: usedModel });
res.end();
};