xiaobo ren commited on
Commit ·
7ce979b
1
Parent(s): ded900d
Implement multi-API support: ChatGPT, Grok, Claude, and Gemini with automatic routing
Browse files- server/analyzeStream.js +29 -61
- server/apiConfig.js +135 -0
- server/celebrityAnalyzer.js +16 -47
- server/index.js +57 -106
- server/parallelAnalyzer.js +26 -61
- server/testApi.js +71 -0
server/analyzeStream.js
CHANGED
|
@@ -9,15 +9,16 @@ import {
|
|
| 9 |
import { BAZI_SYSTEM_INSTRUCTION, buildUserPrompt } from './prompt.js';
|
| 10 |
import { calculateLifeTimeline } from './baziCalculator.js';
|
| 11 |
|
| 12 |
-
|
| 13 |
-
const DEFAULT_API_KEY = process.env.API_KEY || 'AIzaSyBiz_9_zhb3c9mxl3dePVfIv1iiao-_6dw'; // Google Gemini API密钥
|
| 14 |
-
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gemini-1.5-pro';
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
const ALL_MODELS = [
|
|
|
|
|
|
|
|
|
|
| 18 |
'gemini-1.5-pro',
|
| 19 |
-
'gemini-1.5-flash',
|
| 20 |
-
'gemini-pro',
|
| 21 |
];
|
| 22 |
|
| 23 |
const COST_PER_ANALYSIS = process.env.COST_PER_ANALYSIS ? parseInt(process.env.COST_PER_ANALYSIS, 10) : 50;
|
|
@@ -35,7 +36,7 @@ const sendSSE = (res, event, data) => {
|
|
| 35 |
/**
|
| 36 |
* 单次API请求 - 返回Promise
|
| 37 |
*/
|
| 38 |
-
const makeModelRequest = async (model,
|
| 39 |
const controller = new AbortController();
|
| 40 |
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
| 41 |
|
|
@@ -43,44 +44,14 @@ const makeModelRequest = async (model, apiBaseUrl, apiKey, userPrompt, timeoutMs
|
|
| 43 |
console.log(`[${model}] 开始请求...`);
|
| 44 |
const startTime = Date.now();
|
| 45 |
|
| 46 |
-
//
|
| 47 |
-
const
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
: `${apiBaseUrl}/chat/completions`;
|
| 51 |
-
|
| 52 |
-
const headers = isGeminiApi
|
| 53 |
-
? { 'Content-Type': 'application/json' }
|
| 54 |
-
: {
|
| 55 |
-
'Content-Type': 'application/json',
|
| 56 |
-
Authorization: `Bearer ${apiKey}`,
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
const requestBody = isGeminiApi
|
| 60 |
-
? {
|
| 61 |
-
contents: [{
|
| 62 |
-
parts: [{
|
| 63 |
-
text: `${BAZI_SYSTEM_INSTRUCTION}\n\n${userPrompt}`
|
| 64 |
-
}]
|
| 65 |
-
}],
|
| 66 |
-
generationConfig: {
|
| 67 |
-
temperature: 0.7,
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
: {
|
| 71 |
-
model: model,
|
| 72 |
-
messages: [
|
| 73 |
-
{ role: 'system', content: BAZI_SYSTEM_INSTRUCTION },
|
| 74 |
-
{ role: 'user', content: userPrompt },
|
| 75 |
-
],
|
| 76 |
-
temperature: 0.7,
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
const response = await fetch(apiUrl, {
|
| 80 |
method: 'POST',
|
| 81 |
-
headers: headers,
|
| 82 |
signal: controller.signal,
|
| 83 |
-
body: JSON.stringify(
|
| 84 |
});
|
| 85 |
|
| 86 |
clearTimeout(timeoutId);
|
|
@@ -101,13 +72,8 @@ const makeModelRequest = async (model, apiBaseUrl, apiKey, userPrompt, timeoutMs
|
|
| 101 |
return { success: false, model, error: 'INVALID_API_RESPONSE', elapsed };
|
| 102 |
}
|
| 103 |
|
| 104 |
-
//
|
| 105 |
-
|
| 106 |
-
if (isGeminiApi && jsonResult.candidates?.[0]?.content?.parts?.[0]?.text) {
|
| 107 |
-
content = jsonResult.candidates[0].content.parts[0].text;
|
| 108 |
-
} else {
|
| 109 |
-
content = jsonResult.choices?.[0]?.message?.content;
|
| 110 |
-
}
|
| 111 |
if (!content) {
|
| 112 |
console.warn(`[${model}] 无内容返回 (${elapsed}s)`);
|
| 113 |
return { success: false, model, error: 'EMPTY_RESPONSE', elapsed };
|
|
@@ -158,12 +124,12 @@ const makeModelRequest = async (model, apiBaseUrl, apiKey, userPrompt, timeoutMs
|
|
| 158 |
/**
|
| 159 |
* 并发请求多个模型,返回第一个成功的结果
|
| 160 |
*/
|
| 161 |
-
const raceModels = async (models,
|
| 162 |
onProgress(`正在并发请求 ${models.length} 个模型...`);
|
| 163 |
|
| 164 |
// 创建所有请求的Promise
|
| 165 |
const promises = models.map(model =>
|
| 166 |
-
makeModelRequest(model,
|
| 167 |
);
|
| 168 |
|
| 169 |
// 使用Promise.allSettled等待所有请求完成,但我们会在第一个成功时就返回
|
|
@@ -228,11 +194,13 @@ export const handleAnalyzeStream = async (req, res) => {
|
|
| 228 |
};
|
| 229 |
|
| 230 |
if (!useCustomApi) {
|
| 231 |
-
|
| 232 |
-
|
|
|
|
| 233 |
modelName = DEFAULT_MODEL;
|
| 234 |
|
| 235 |
-
|
|
|
|
| 236 |
sendSSE(res, 'error', {
|
| 237 |
error: 'SERVER_DEFAULT_KEY_NOT_SET',
|
| 238 |
message: '服务器未配置API密钥,请使用自定义API或联系管理员'
|
|
@@ -296,7 +264,7 @@ export const handleAnalyzeStream = async (req, res) => {
|
|
| 296 |
|
| 297 |
for (let attempt = 1; attempt <= 3; attempt++) {
|
| 298 |
onProgress(`尝试第 ${attempt} 次...`);
|
| 299 |
-
const response = await makeModelRequest(modelName,
|
| 300 |
|
| 301 |
if (response.success) {
|
| 302 |
result = response.data;
|
|
@@ -316,8 +284,8 @@ export const handleAnalyzeStream = async (req, res) => {
|
|
| 316 |
onProgress('启动多模型并发请求策略...');
|
| 317 |
|
| 318 |
// 第一轮:并发请求主模型和一个备选模型
|
| 319 |
-
const firstRoundModels = [modelName, '
|
| 320 |
-
let raceResult = await raceModels(firstRoundModels,
|
| 321 |
|
| 322 |
if (raceResult.success) {
|
| 323 |
result = raceResult.data;
|
|
@@ -325,8 +293,8 @@ export const handleAnalyzeStream = async (req, res) => {
|
|
| 325 |
} else {
|
| 326 |
// 第二轮:尝试其他模型
|
| 327 |
onProgress('第一轮失败,启动第二轮备选模型...');
|
| 328 |
-
const secondRoundModels = ['
|
| 329 |
-
raceResult = await raceModels(secondRoundModels,
|
| 330 |
|
| 331 |
if (raceResult.success) {
|
| 332 |
result = raceResult.data;
|
|
@@ -339,7 +307,7 @@ export const handleAnalyzeStream = async (req, res) => {
|
|
| 339 |
onProgress('并发请求全部失败,尝试逐个请求...');
|
| 340 |
for (const model of ALL_MODELS) {
|
| 341 |
onProgress(`最后尝试: ${model}...`);
|
| 342 |
-
const response = await makeModelRequest(model,
|
| 343 |
if (response.success) {
|
| 344 |
result = response.data;
|
| 345 |
usedModel = model;
|
|
|
|
| 9 |
import { BAZI_SYSTEM_INSTRUCTION, buildUserPrompt } from './prompt.js';
|
| 10 |
import { calculateLifeTimeline } from './baziCalculator.js';
|
| 11 |
|
| 12 |
+
import { buildApiRequest, parseApiResponse } from './apiConfig.js';
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gpt-4o';
|
| 15 |
+
|
| 16 |
+
// 备选模型列表 - 用于并发请求和降级
|
| 17 |
const ALL_MODELS = [
|
| 18 |
+
'gpt-4o',
|
| 19 |
+
'grok-4',
|
| 20 |
+
'claude-3-5-sonnet-20241022',
|
| 21 |
'gemini-1.5-pro',
|
|
|
|
|
|
|
| 22 |
];
|
| 23 |
|
| 24 |
const COST_PER_ANALYSIS = process.env.COST_PER_ANALYSIS ? parseInt(process.env.COST_PER_ANALYSIS, 10) : 50;
|
|
|
|
| 36 |
/**
|
| 37 |
* 单次API请求 - 返回Promise
|
| 38 |
*/
|
| 39 |
+
const makeModelRequest = async (model, userPrompt, timeoutMs = 120000) => {
|
| 40 |
const controller = new AbortController();
|
| 41 |
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
| 42 |
|
|
|
|
| 44 |
console.log(`[${model}] 开始请求...`);
|
| 45 |
const startTime = Date.now();
|
| 46 |
|
| 47 |
+
// 使用新的 API 配置系统
|
| 48 |
+
const apiRequest = buildApiRequest(model, BAZI_SYSTEM_INSTRUCTION, userPrompt, 0.7);
|
| 49 |
+
|
| 50 |
+
const response = await fetch(apiRequest.url, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
method: 'POST',
|
| 52 |
+
headers: apiRequest.headers,
|
| 53 |
signal: controller.signal,
|
| 54 |
+
body: JSON.stringify(apiRequest.body),
|
| 55 |
});
|
| 56 |
|
| 57 |
clearTimeout(timeoutId);
|
|
|
|
| 72 |
return { success: false, model, error: 'INVALID_API_RESPONSE', elapsed };
|
| 73 |
}
|
| 74 |
|
| 75 |
+
// 使用新的响应解析系统
|
| 76 |
+
const content = parseApiResponse(jsonResult, model);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
if (!content) {
|
| 78 |
console.warn(`[${model}] 无内容返回 (${elapsed}s)`);
|
| 79 |
return { success: false, model, error: 'EMPTY_RESPONSE', elapsed };
|
|
|
|
| 124 |
/**
|
| 125 |
* 并发请求多个模型,返回第一个成功的结果
|
| 126 |
*/
|
| 127 |
+
const raceModels = async (models, userPrompt, onProgress) => {
|
| 128 |
onProgress(`正在并发请求 ${models.length} 个模型...`);
|
| 129 |
|
| 130 |
// 创建所有请求的Promise
|
| 131 |
const promises = models.map(model =>
|
| 132 |
+
makeModelRequest(model, userPrompt, 180000)
|
| 133 |
);
|
| 134 |
|
| 135 |
// 使用Promise.allSettled等待所有请求完成,但我们会在第一个成功时就返回
|
|
|
|
| 194 |
};
|
| 195 |
|
| 196 |
if (!useCustomApi) {
|
| 197 |
+
// 不再需要固定的 API URL 和 Key,由 apiConfig.js 根据模型自动选择
|
| 198 |
+
apiBaseUrl = null; // 不再使用
|
| 199 |
+
apiKey = null; // 不再使用
|
| 200 |
modelName = DEFAULT_MODEL;
|
| 201 |
|
| 202 |
+
// API 配置由 apiConfig.js 管理,无需检查
|
| 203 |
+
if (false) {
|
| 204 |
sendSSE(res, 'error', {
|
| 205 |
error: 'SERVER_DEFAULT_KEY_NOT_SET',
|
| 206 |
message: '服务器未配置API密钥,请使用自定义API或联系管理员'
|
|
|
|
| 264 |
|
| 265 |
for (let attempt = 1; attempt <= 3; attempt++) {
|
| 266 |
onProgress(`尝试第 ${attempt} 次...`);
|
| 267 |
+
const response = await makeModelRequest(modelName, userPrompt, 60000);
|
| 268 |
|
| 269 |
if (response.success) {
|
| 270 |
result = response.data;
|
|
|
|
| 284 |
onProgress('启动多模型并发请求策略...');
|
| 285 |
|
| 286 |
// 第一轮:并发请求主模型和一个备选模型
|
| 287 |
+
const firstRoundModels = [modelName, 'gpt-4o'];
|
| 288 |
+
let raceResult = await raceModels(firstRoundModels, userPrompt, onProgress);
|
| 289 |
|
| 290 |
if (raceResult.success) {
|
| 291 |
result = raceResult.data;
|
|
|
|
| 293 |
} else {
|
| 294 |
// 第二轮:尝试其他模型
|
| 295 |
onProgress('第一轮失败,启动第二轮备选模型...');
|
| 296 |
+
const secondRoundModels = ['grok-4', 'claude-3-5-sonnet-20241022'];
|
| 297 |
+
raceResult = await raceModels(secondRoundModels, userPrompt, onProgress);
|
| 298 |
|
| 299 |
if (raceResult.success) {
|
| 300 |
result = raceResult.data;
|
|
|
|
| 307 |
onProgress('并发请求全部失败,尝试逐个请求...');
|
| 308 |
for (const model of ALL_MODELS) {
|
| 309 |
onProgress(`最后尝试: ${model}...`);
|
| 310 |
+
const response = await makeModelRequest(model, userPrompt, 45000);
|
| 311 |
if (response.success) {
|
| 312 |
result = response.data;
|
| 313 |
usedModel = model;
|
server/apiConfig.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 多 API 配置和路由
|
| 3 |
+
* 根据模型名称自动选择正确的 API 端点和密钥
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// API 配置
|
| 7 |
+
const API_CONFIGS = {
|
| 8 |
+
// Google Gemini
|
| 9 |
+
gemini: {
|
| 10 |
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
| 11 |
+
apiKey: process.env.GEMINI_API_KEY || 'AIzaSyBiz_9_zhb3c9mxl3dePVfIv1iiao-_6dw',
|
| 12 |
+
models: ['gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro', 'gemini-3-pro-preview'],
|
| 13 |
+
format: 'gemini', // 使用 Gemini 原生格式
|
| 14 |
+
},
|
| 15 |
+
|
| 16 |
+
// OpenAI (ChatGPT)
|
| 17 |
+
openai: {
|
| 18 |
+
baseUrl: 'https://api.openai.com/v1',
|
| 19 |
+
apiKey: process.env.OPENAI_API_KEY || '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',
|
| 20 |
+
models: ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo', 'gpt-4o', 'gpt-4o-mini'],
|
| 21 |
+
format: 'openai', // 使用 OpenAI 格式
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
// Grok (xAI)
|
| 25 |
+
grok: {
|
| 26 |
+
baseUrl: 'https://api.x.ai/v1',
|
| 27 |
+
apiKey: process.env.GROK_API_KEY || 'xai-5nPioW6r81dYiWzr6jYkLewyjM32YHEP2PWU2oRAcIpwHsaqL2azw3mnLu8PDNuI7ErkkHxn7jjejWpR',
|
| 28 |
+
models: ['grok-4', 'grok-4-auto', 'grok-4-1-non-thinking-w-tool', 'grok-4-mini-thinking-tahoe', 'grok-beta'],
|
| 29 |
+
format: 'openai', // Grok 使用 OpenAI 兼容格式
|
| 30 |
+
},
|
| 31 |
+
|
| 32 |
+
// Claude (Anthropic)
|
| 33 |
+
claude: {
|
| 34 |
+
baseUrl: 'https://api.anthropic.com/v1',
|
| 35 |
+
apiKey: process.env.CLAUDE_API_KEY || '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',
|
| 36 |
+
models: ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-5-haiku-20241022', 'claude-haiku-4-5-20251001', 'claude-3-5-sonnet'],
|
| 37 |
+
format: 'claude', // 使用 Claude 格式
|
| 38 |
+
},
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* 根据模型名称获取 API 配置
|
| 43 |
+
*/
|
| 44 |
+
export function getApiConfigForModel(modelName) {
|
| 45 |
+
// 检查每个 API 提供商的模型列表
|
| 46 |
+
for (const [provider, config] of Object.entries(API_CONFIGS)) {
|
| 47 |
+
if (config.models.includes(modelName)) {
|
| 48 |
+
return { ...config, provider };
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// 默认使用 Gemini
|
| 53 |
+
return { ...API_CONFIGS.gemini, provider: 'gemini' };
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/**
|
| 57 |
+
* 构建 API 请求
|
| 58 |
+
*/
|
| 59 |
+
export function buildApiRequest(modelName, systemPrompt, userPrompt, temperature = 0.7) {
|
| 60 |
+
const config = getApiConfigForModel(modelName);
|
| 61 |
+
|
| 62 |
+
if (config.format === 'gemini') {
|
| 63 |
+
// Google Gemini 格式
|
| 64 |
+
return {
|
| 65 |
+
url: `${config.baseUrl}/models/${modelName}:generateContent?key=${config.apiKey}`,
|
| 66 |
+
headers: { 'Content-Type': 'application/json' },
|
| 67 |
+
body: {
|
| 68 |
+
contents: [{
|
| 69 |
+
parts: [{
|
| 70 |
+
text: `${systemPrompt}\n\n${userPrompt}`
|
| 71 |
+
}]
|
| 72 |
+
}],
|
| 73 |
+
generationConfig: {
|
| 74 |
+
temperature: temperature,
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
} else if (config.format === 'claude') {
|
| 79 |
+
// Claude 格式
|
| 80 |
+
return {
|
| 81 |
+
url: `${config.baseUrl}/messages`,
|
| 82 |
+
headers: {
|
| 83 |
+
'Content-Type': 'application/json',
|
| 84 |
+
'x-api-key': config.apiKey,
|
| 85 |
+
'anthropic-version': '2023-06-01',
|
| 86 |
+
},
|
| 87 |
+
body: {
|
| 88 |
+
model: modelName,
|
| 89 |
+
max_tokens: 8192,
|
| 90 |
+
temperature: temperature,
|
| 91 |
+
messages: [
|
| 92 |
+
{ role: 'user', content: `${systemPrompt}\n\n${userPrompt}` }
|
| 93 |
+
]
|
| 94 |
+
}
|
| 95 |
+
};
|
| 96 |
+
} else {
|
| 97 |
+
// OpenAI 兼容格式 (OpenAI, Grok)
|
| 98 |
+
return {
|
| 99 |
+
url: `${config.baseUrl}/chat/completions`,
|
| 100 |
+
headers: {
|
| 101 |
+
'Content-Type': 'application/json',
|
| 102 |
+
'Authorization': `Bearer ${config.apiKey}`,
|
| 103 |
+
},
|
| 104 |
+
body: {
|
| 105 |
+
model: modelName,
|
| 106 |
+
messages: [
|
| 107 |
+
{ role: 'system', content: systemPrompt },
|
| 108 |
+
{ role: 'user', content: userPrompt },
|
| 109 |
+
],
|
| 110 |
+
temperature: temperature,
|
| 111 |
+
}
|
| 112 |
+
};
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/**
|
| 117 |
+
* 解析 API 响应
|
| 118 |
+
*/
|
| 119 |
+
export function parseApiResponse(responseJson, modelName) {
|
| 120 |
+
const config = getApiConfigForModel(modelName);
|
| 121 |
+
|
| 122 |
+
if (config.format === 'gemini') {
|
| 123 |
+
// Google Gemini 响应格式
|
| 124 |
+
return responseJson.candidates?.[0]?.content?.parts?.[0]?.text || null;
|
| 125 |
+
} else if (config.format === 'claude') {
|
| 126 |
+
// Claude 响应格式
|
| 127 |
+
return responseJson.content?.[0]?.text || null;
|
| 128 |
+
} else {
|
| 129 |
+
// OpenAI 兼容格式
|
| 130 |
+
return responseJson.choices?.[0]?.message?.content || null;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
export default API_CONFIGS;
|
| 135 |
+
|
server/celebrityAnalyzer.js
CHANGED
|
@@ -5,12 +5,11 @@
|
|
| 5 |
import fetch from 'node-fetch';
|
| 6 |
import { AGENT_CELEBRITY_ANALYSIS_PROMPT } from './agentPrompts.js';
|
| 7 |
|
| 8 |
-
|
| 9 |
-
const DEFAULT_API_KEY = process.env.API_KEY || 'AIzaSyBiz_9_zhb3c9mxl3dePVfIv1iiao-_6dw'; // Google Gemini API密钥
|
| 10 |
|
| 11 |
// 名人分析使用的模型 - 需要高质量输出
|
| 12 |
-
const CELEBRITY_ANALYSIS_MODEL = '
|
| 13 |
-
const FALLBACK_MODELS = ['
|
| 14 |
|
| 15 |
/**
|
| 16 |
* 构建名人分析的用户提示词
|
|
@@ -127,46 +126,21 @@ async function callLLMApi(model, userPrompt, timeoutMs = 120000) {
|
|
| 127 |
console.log(`[celebrityAnalyzer] 使用模型 ${model} 开始生成分析...`);
|
| 128 |
const startTime = Date.now();
|
| 129 |
|
| 130 |
-
//
|
| 131 |
-
const
|
| 132 |
-
const apiUrl = isGeminiApi
|
| 133 |
-
? `${DEFAULT_API_BASE_URL}/models/${model}:generateContent?key=${DEFAULT_API_KEY}`
|
| 134 |
-
: `${DEFAULT_API_BASE_URL}/chat/completions`;
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
const requestBody = isGeminiApi
|
| 144 |
-
? {
|
| 145 |
-
contents: [{
|
| 146 |
-
parts: [{
|
| 147 |
-
text: `${AGENT_CELEBRITY_ANALYSIS_PROMPT}\n\n${userPrompt}`
|
| 148 |
-
}]
|
| 149 |
-
}],
|
| 150 |
-
generationConfig: {
|
| 151 |
-
temperature: 0.7,
|
| 152 |
-
maxOutputTokens: 8000,
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
: {
|
| 156 |
-
model: model,
|
| 157 |
-
messages: [
|
| 158 |
-
{ role: 'system', content: AGENT_CELEBRITY_ANALYSIS_PROMPT },
|
| 159 |
-
{ role: 'user', content: userPrompt },
|
| 160 |
-
],
|
| 161 |
-
temperature: 0.7,
|
| 162 |
-
max_tokens: 8000,
|
| 163 |
-
};
|
| 164 |
|
| 165 |
-
const response = await fetch(
|
| 166 |
method: 'POST',
|
| 167 |
-
headers: headers,
|
| 168 |
signal: controller.signal,
|
| 169 |
-
body: JSON.stringify(
|
| 170 |
});
|
| 171 |
|
| 172 |
clearTimeout(timeoutId);
|
|
@@ -180,13 +154,8 @@ async function callLLMApi(model, userPrompt, timeoutMs = 120000) {
|
|
| 180 |
|
| 181 |
const responseJson = await response.json();
|
| 182 |
|
| 183 |
-
//
|
| 184 |
-
|
| 185 |
-
if (isGeminiApi && responseJson.candidates?.[0]?.content?.parts?.[0]?.text) {
|
| 186 |
-
content = responseJson.candidates[0].content.parts[0].text;
|
| 187 |
-
} else {
|
| 188 |
-
content = responseJson.choices?.[0]?.message?.content;
|
| 189 |
-
}
|
| 190 |
|
| 191 |
if (!content) {
|
| 192 |
return { success: false, error: 'EMPTY_RESPONSE', elapsed };
|
|
|
|
| 5 |
import fetch from 'node-fetch';
|
| 6 |
import { AGENT_CELEBRITY_ANALYSIS_PROMPT } from './agentPrompts.js';
|
| 7 |
|
| 8 |
+
import { buildApiRequest, parseApiResponse } from './apiConfig.js';
|
|
|
|
| 9 |
|
| 10 |
// 名人分析使用的模型 - 需要高质量输出
|
| 11 |
+
const CELEBRITY_ANALYSIS_MODEL = 'gpt-4o';
|
| 12 |
+
const FALLBACK_MODELS = ['grok-4', 'claude-3-5-sonnet-20241022', 'gemini-1.5-pro'];
|
| 13 |
|
| 14 |
/**
|
| 15 |
* 构建名人分析的用户提示词
|
|
|
|
| 126 |
console.log(`[celebrityAnalyzer] 使用模型 ${model} 开始生成分析...`);
|
| 127 |
const startTime = Date.now();
|
| 128 |
|
| 129 |
+
// 使用新的 API 配置系统
|
| 130 |
+
const apiRequest = buildApiRequest(model, AGENT_CELEBRITY_ANALYSIS_PROMPT, userPrompt, 0.7);
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
+
// 如果是 OpenAI 格式,添加 max_tokens
|
| 133 |
+
if (apiRequest.body.max_tokens === undefined && apiRequest.body.generationConfig) {
|
| 134 |
+
apiRequest.body.generationConfig.maxOutputTokens = 8000;
|
| 135 |
+
} else if (apiRequest.body.max_tokens === undefined) {
|
| 136 |
+
apiRequest.body.max_tokens = 8000;
|
| 137 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
+
const response = await fetch(apiRequest.url, {
|
| 140 |
method: 'POST',
|
| 141 |
+
headers: apiRequest.headers,
|
| 142 |
signal: controller.signal,
|
| 143 |
+
body: JSON.stringify(apiRequest.body),
|
| 144 |
});
|
| 145 |
|
| 146 |
clearTimeout(timeoutId);
|
|
|
|
| 154 |
|
| 155 |
const responseJson = await response.json();
|
| 156 |
|
| 157 |
+
// 使用新的响应解析系统
|
| 158 |
+
const content = parseApiResponse(responseJson, model);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
if (!content) {
|
| 161 |
return { success: false, error: 'EMPTY_RESPONSE', elapsed };
|
server/index.js
CHANGED
|
@@ -82,21 +82,24 @@ import { AGENT_DAILY_FORTUNE_PROMPT } from './agentPrompts.js';
|
|
| 82 |
import { generateCelebrityAnalysis } from './celebrityAnalyzer.js';
|
| 83 |
import { regenerateCoreDocument } from './coreDocumentEngine.js';
|
| 84 |
import { startEmailScheduler } from './emailScheduler.js';
|
|
|
|
| 85 |
|
| 86 |
dotenv.config();
|
| 87 |
|
| 88 |
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : (process.env.SPACE_ID ? 7860 : 3000);
|
| 89 |
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
|
| 90 |
|
| 91 |
-
//
|
| 92 |
-
|
| 93 |
-
const DEFAULT_API_KEY = process.env.API_KEY || 'AIzaSyBiz_9_zhb3c9mxl3dePVfIv1iiao-_6dw'; // Google Gemini API密钥
|
| 94 |
-
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gemini-1.5-pro';
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
const FALLBACK_MODELS = [
|
| 98 |
-
'
|
| 99 |
-
'
|
|
|
|
|
|
|
| 100 |
];
|
| 101 |
|
| 102 |
const FREE_INIT_POINTS = process.env.FREE_INIT_POINTS ? parseInt(process.env.FREE_INIT_POINTS, 10) : 1000;
|
|
@@ -133,6 +136,25 @@ const getAuthedUser = (req) => {
|
|
| 133 |
|
| 134 |
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
app.post('/api/auth/register', async (req, res) => {
|
| 137 |
const email = sanitizeEmail(req.body?.email);
|
| 138 |
const password = String(req.body?.password || '');
|
|
@@ -310,17 +332,12 @@ app.post('/api/analyze', async (req, res) => {
|
|
| 310 |
}
|
| 311 |
// 未登录用户可以免费体验一次(游客模式,不扣点)
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
|
|
|
| 315 |
modelName = DEFAULT_MODEL;
|
| 316 |
|
| 317 |
-
//
|
| 318 |
-
if (!DEFAULT_API_KEY || DEFAULT_API_KEY === 'sk-example-key') {
|
| 319 |
-
return res.status(500).json({
|
| 320 |
-
error: 'SERVER_DEFAULT_KEY_NOT_SET',
|
| 321 |
-
message: 'Please configure API key on server or use custom API with your own key'
|
| 322 |
-
});
|
| 323 |
-
}
|
| 324 |
} else {
|
| 325 |
if (!apiBaseUrl || !apiKey || !modelName) return res.status(400).json({ error: 'MISSING_CUSTOM_API_CONFIG' });
|
| 326 |
}
|
|
@@ -347,50 +364,19 @@ app.post('/api/analyze', async (req, res) => {
|
|
| 347 |
}
|
| 348 |
|
| 349 |
// 单次请求函数
|
| 350 |
-
const makeRequest = async (currentModel
|
| 351 |
const controller = new AbortController();
|
| 352 |
const timeoutId = setTimeout(() => controller.abort(), 180000); // 180秒超时
|
| 353 |
|
| 354 |
try {
|
| 355 |
-
//
|
| 356 |
-
|
| 357 |
-
const isGeminiApi = currentApiKey.startsWith('AIzaSy');
|
| 358 |
-
const apiUrl = isGeminiApi
|
| 359 |
-
? `${currentApiBaseUrl}/models/${currentModel}:generateContent?key=${currentApiKey}`
|
| 360 |
-
: `${currentApiBaseUrl}/chat/completions`;
|
| 361 |
|
| 362 |
-
const
|
| 363 |
-
? { 'Content-Type': 'application/json' }
|
| 364 |
-
: {
|
| 365 |
-
'Content-Type': 'application/json',
|
| 366 |
-
Authorization: `Bearer ${currentApiKey}`,
|
| 367 |
-
};
|
| 368 |
-
|
| 369 |
-
const requestBody = isGeminiApi
|
| 370 |
-
? {
|
| 371 |
-
contents: [{
|
| 372 |
-
parts: [{
|
| 373 |
-
text: `${BAZI_SYSTEM_INSTRUCTION}\n\n${userPrompt}`
|
| 374 |
-
}]
|
| 375 |
-
}],
|
| 376 |
-
generationConfig: {
|
| 377 |
-
temperature: 0.7,
|
| 378 |
-
}
|
| 379 |
-
}
|
| 380 |
-
: {
|
| 381 |
-
model: currentModel,
|
| 382 |
-
messages: [
|
| 383 |
-
{ role: 'system', content: BAZI_SYSTEM_INSTRUCTION },
|
| 384 |
-
{ role: 'user', content: userPrompt },
|
| 385 |
-
],
|
| 386 |
-
temperature: 0.7,
|
| 387 |
-
};
|
| 388 |
-
|
| 389 |
-
const response = await fetch(apiUrl, {
|
| 390 |
method: 'POST',
|
| 391 |
-
headers: headers,
|
| 392 |
signal: controller.signal,
|
| 393 |
-
body: JSON.stringify(
|
| 394 |
});
|
| 395 |
|
| 396 |
clearTimeout(timeoutId);
|
|
@@ -412,7 +398,7 @@ app.post('/api/analyze', async (req, res) => {
|
|
| 412 |
while (retryCount <= maxRetries) {
|
| 413 |
try {
|
| 414 |
console.log(`尝试模型: ${currentModel} (第${retryCount + 1}次)`);
|
| 415 |
-
const response = await makeRequest(currentModel
|
| 416 |
|
| 417 |
if (response.ok) {
|
| 418 |
return { success: true, response, model: currentModel };
|
|
@@ -495,14 +481,8 @@ app.post('/api/analyze', async (req, res) => {
|
|
| 495 |
});
|
| 496 |
}
|
| 497 |
|
| 498 |
-
//
|
| 499 |
-
const
|
| 500 |
-
let content;
|
| 501 |
-
if (isGeminiApi && jsonResult.candidates?.[0]?.content?.parts?.[0]?.text) {
|
| 502 |
-
content = jsonResult.candidates[0].content.parts[0].text;
|
| 503 |
-
} else {
|
| 504 |
-
content = jsonResult.choices?.[0]?.message?.content;
|
| 505 |
-
}
|
| 506 |
if (!content) return res.status(502).json({ error: 'EMPTY_MODEL_RESPONSE' });
|
| 507 |
|
| 508 |
// 清理可能的 markdown 代码块标记
|
|
@@ -2531,45 +2511,21 @@ async function generateDailyFortuneAI(bazi, dateKey, dayGanZhi, lunarDateStr, pr
|
|
| 2531 |
`;
|
| 2532 |
|
| 2533 |
try {
|
| 2534 |
-
//
|
| 2535 |
-
const
|
| 2536 |
-
const
|
| 2537 |
-
? `${DEFAULT_API_BASE_URL}/models/gemini-1.5-pro:generateContent?key=${DEFAULT_API_KEY}`
|
| 2538 |
-
: `${DEFAULT_API_BASE_URL}/chat/completions`;
|
| 2539 |
-
|
| 2540 |
-
const headers = isGeminiApi
|
| 2541 |
-
? { 'Content-Type': 'application/json' }
|
| 2542 |
-
: {
|
| 2543 |
-
'Content-Type': 'application/json',
|
| 2544 |
-
'Authorization': `Bearer ${DEFAULT_API_KEY}`,
|
| 2545 |
-
};
|
| 2546 |
|
| 2547 |
-
|
| 2548 |
-
|
| 2549 |
-
|
| 2550 |
-
|
| 2551 |
-
|
| 2552 |
-
|
| 2553 |
-
|
| 2554 |
-
|
| 2555 |
-
temperature: 0.7,
|
| 2556 |
-
maxOutputTokens: 4000,
|
| 2557 |
-
}
|
| 2558 |
-
}
|
| 2559 |
-
: {
|
| 2560 |
-
model: 'gemini-1.5-pro',
|
| 2561 |
-
messages: [
|
| 2562 |
-
{ role: 'system', content: AGENT_DAILY_FORTUNE_PROMPT },
|
| 2563 |
-
{ role: 'user', content: userPrompt },
|
| 2564 |
-
],
|
| 2565 |
-
max_tokens: 4000,
|
| 2566 |
-
temperature: 0.7,
|
| 2567 |
-
};
|
| 2568 |
-
|
| 2569 |
-
const response = await fetch(apiUrl, {
|
| 2570 |
method: 'POST',
|
| 2571 |
-
headers: headers,
|
| 2572 |
-
body: JSON.stringify(
|
| 2573 |
});
|
| 2574 |
|
| 2575 |
if (!response.ok) {
|
|
@@ -2578,13 +2534,8 @@ async function generateDailyFortuneAI(bazi, dateKey, dayGanZhi, lunarDateStr, pr
|
|
| 2578 |
|
| 2579 |
const data = await response.json();
|
| 2580 |
|
| 2581 |
-
//
|
| 2582 |
-
|
| 2583 |
-
if (isGeminiApi && data.candidates?.[0]?.content?.parts?.[0]?.text) {
|
| 2584 |
-
content = data.candidates[0].content.parts[0].text;
|
| 2585 |
-
} else {
|
| 2586 |
-
content = data.choices?.[0]?.message?.content;
|
| 2587 |
-
}
|
| 2588 |
|
| 2589 |
if (!content) {
|
| 2590 |
throw new Error('Empty AI response');
|
|
|
|
| 82 |
import { generateCelebrityAnalysis } from './celebrityAnalyzer.js';
|
| 83 |
import { regenerateCoreDocument } from './coreDocumentEngine.js';
|
| 84 |
import { startEmailScheduler } from './emailScheduler.js';
|
| 85 |
+
import { buildApiRequest, parseApiResponse, getApiConfigForModel } from './apiConfig.js';
|
| 86 |
|
| 87 |
dotenv.config();
|
| 88 |
|
| 89 |
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : (process.env.SPACE_ID ? 7860 : 3000);
|
| 90 |
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
|
| 91 |
|
| 92 |
+
// 多 API 配置 - 使用 apiConfig.js 进行智能路由
|
| 93 |
+
import { buildApiRequest, parseApiResponse, getApiConfigForModel } from './apiConfig.js';
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'gpt-4o';
|
| 96 |
+
|
| 97 |
+
// 模型降级列表:当主模型失败时依次尝试
|
| 98 |
const FALLBACK_MODELS = [
|
| 99 |
+
'gpt-4o',
|
| 100 |
+
'grok-4',
|
| 101 |
+
'claude-3-5-sonnet-20241022',
|
| 102 |
+
'gemini-1.5-pro',
|
| 103 |
];
|
| 104 |
|
| 105 |
const FREE_INIT_POINTS = process.env.FREE_INIT_POINTS ? parseInt(process.env.FREE_INIT_POINTS, 10) : 1000;
|
|
|
|
| 136 |
|
| 137 |
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
| 138 |
|
| 139 |
+
// API 连接测试端点
|
| 140 |
+
app.get('/api/test-apis', async (_req, res) => {
|
| 141 |
+
try {
|
| 142 |
+
const { testAllApis } = await import('./testApi.js');
|
| 143 |
+
const results = await testAllApis();
|
| 144 |
+
return res.json({
|
| 145 |
+
success: true,
|
| 146 |
+
results,
|
| 147 |
+
timestamp: new Date().toISOString(),
|
| 148 |
+
});
|
| 149 |
+
} catch (error) {
|
| 150 |
+
console.error('API 测试失败:', error);
|
| 151 |
+
return res.status(500).json({
|
| 152 |
+
success: false,
|
| 153 |
+
error: error.message
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
app.post('/api/auth/register', async (req, res) => {
|
| 159 |
const email = sanitizeEmail(req.body?.email);
|
| 160 |
const password = String(req.body?.password || '');
|
|
|
|
| 332 |
}
|
| 333 |
// 未登录用户可以免费体验一次(游客模式,不扣点)
|
| 334 |
|
| 335 |
+
// 不再需要固定的 API URL 和 Key,由 apiConfig.js 根据模型自动选择
|
| 336 |
+
apiBaseUrl = null; // 不再使用
|
| 337 |
+
apiKey = null; // 不再使用
|
| 338 |
modelName = DEFAULT_MODEL;
|
| 339 |
|
| 340 |
+
// API 配置由 apiConfig.js 管理,无需检查
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
} else {
|
| 342 |
if (!apiBaseUrl || !apiKey || !modelName) return res.status(400).json({ error: 'MISSING_CUSTOM_API_CONFIG' });
|
| 343 |
}
|
|
|
|
| 364 |
}
|
| 365 |
|
| 366 |
// 单次请求函数
|
| 367 |
+
const makeRequest = async (currentModel) => {
|
| 368 |
const controller = new AbortController();
|
| 369 |
const timeoutId = setTimeout(() => controller.abort(), 180000); // 180秒超时
|
| 370 |
|
| 371 |
try {
|
| 372 |
+
// 使用新的 API 配置系统
|
| 373 |
+
const apiRequest = buildApiRequest(currentModel, BAZI_SYSTEM_INSTRUCTION, userPrompt, 0.7);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
+
const response = await fetch(apiRequest.url, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
method: 'POST',
|
| 377 |
+
headers: apiRequest.headers,
|
| 378 |
signal: controller.signal,
|
| 379 |
+
body: JSON.stringify(apiRequest.body),
|
| 380 |
});
|
| 381 |
|
| 382 |
clearTimeout(timeoutId);
|
|
|
|
| 398 |
while (retryCount <= maxRetries) {
|
| 399 |
try {
|
| 400 |
console.log(`尝试模型: ${currentModel} (第${retryCount + 1}次)`);
|
| 401 |
+
const response = await makeRequest(currentModel);
|
| 402 |
|
| 403 |
if (response.ok) {
|
| 404 |
return { success: true, response, model: currentModel };
|
|
|
|
| 481 |
});
|
| 482 |
}
|
| 483 |
|
| 484 |
+
// 使用新的响应解析系统
|
| 485 |
+
const content = parseApiResponse(jsonResult, usedModel);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
if (!content) return res.status(502).json({ error: 'EMPTY_MODEL_RESPONSE' });
|
| 487 |
|
| 488 |
// 清理可能的 markdown 代码块标记
|
|
|
|
| 2511 |
`;
|
| 2512 |
|
| 2513 |
try {
|
| 2514 |
+
// 使用新的 API 配置系统
|
| 2515 |
+
const model = 'gpt-4o'; // 使用 GPT-4o 作为默认模型
|
| 2516 |
+
const apiRequest = buildApiRequest(model, AGENT_DAILY_FORTUNE_PROMPT, userPrompt, 0.7);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2517 |
|
| 2518 |
+
// 如果是 OpenAI 格式,添加 max_tokens
|
| 2519 |
+
if (apiRequest.body.max_tokens === undefined && apiRequest.body.generationConfig) {
|
| 2520 |
+
apiRequest.body.generationConfig.maxOutputTokens = 4000;
|
| 2521 |
+
} else if (apiRequest.body.max_tokens === undefined) {
|
| 2522 |
+
apiRequest.body.max_tokens = 4000;
|
| 2523 |
+
}
|
| 2524 |
+
|
| 2525 |
+
const response = await fetch(apiRequest.url, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2526 |
method: 'POST',
|
| 2527 |
+
headers: apiRequest.headers,
|
| 2528 |
+
body: JSON.stringify(apiRequest.body),
|
| 2529 |
});
|
| 2530 |
|
| 2531 |
if (!response.ok) {
|
|
|
|
| 2534 |
|
| 2535 |
const data = await response.json();
|
| 2536 |
|
| 2537 |
+
// 使用新的响应解析系统
|
| 2538 |
+
const content = parseApiResponse(data, model);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2539 |
|
| 2540 |
if (!content) {
|
| 2541 |
throw new Error('Empty AI response');
|
server/parallelAnalyzer.js
CHANGED
|
@@ -15,25 +15,24 @@ import {
|
|
| 15 |
AGENT_CRYPTO_PROMPT,
|
| 16 |
} from './agentPrompts.js';
|
| 17 |
import { generateFallbackKLine } from './baziCalculator.js';
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
-
const DEFAULT_API_KEY = process.env.API_KEY || 'AIzaSyBiz_9_zhb3c9mxl3dePVfIv1iiao-_6dw'; // Google Gemini API密钥
|
| 21 |
-
|
| 22 |
-
// 为不同Agent分配最适合的模型(使用 Google Gemini 支持的模型)
|
| 23 |
const AGENT_MODEL_ASSIGNMENT = {
|
| 24 |
-
core: '
|
| 25 |
-
kline_past: '
|
| 26 |
-
kline_future: '
|
| 27 |
-
career: '
|
| 28 |
-
marriage: '
|
| 29 |
-
crypto: '
|
| 30 |
};
|
| 31 |
|
| 32 |
-
// 备用模型列表
|
| 33 |
const FALLBACK_MODELS = [
|
| 34 |
-
'
|
|
|
|
|
|
|
| 35 |
'gemini-1.5-pro',
|
| 36 |
-
'gemini-pro',
|
| 37 |
];
|
| 38 |
|
| 39 |
/**
|
|
@@ -49,7 +48,7 @@ export const sendSSE = (res, event, data) => {
|
|
| 49 |
/**
|
| 50 |
* 单个Agent请求
|
| 51 |
*/
|
| 52 |
-
const makeAgentRequest = async (agentType, model,
|
| 53 |
const controller = new AbortController();
|
| 54 |
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
| 55 |
|
|
@@ -57,44 +56,14 @@ const makeAgentRequest = async (agentType, model, apiBaseUrl, apiKey, systemProm
|
|
| 57 |
console.log(`[Agent:${agentType}] 使用模型 ${model} 开始请求...`);
|
| 58 |
const startTime = Date.now();
|
| 59 |
|
| 60 |
-
//
|
| 61 |
-
const
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
: `${apiBaseUrl}/chat/completions`;
|
| 65 |
-
|
| 66 |
-
const headers = isGeminiApi
|
| 67 |
-
? { 'Content-Type': 'application/json' }
|
| 68 |
-
: {
|
| 69 |
-
'Content-Type': 'application/json',
|
| 70 |
-
Authorization: `Bearer ${apiKey}`,
|
| 71 |
-
};
|
| 72 |
-
|
| 73 |
-
const requestBody = isGeminiApi
|
| 74 |
-
? {
|
| 75 |
-
contents: [{
|
| 76 |
-
parts: [{
|
| 77 |
-
text: `${systemPrompt}\n\n${userPrompt}`
|
| 78 |
-
}]
|
| 79 |
-
}],
|
| 80 |
-
generationConfig: {
|
| 81 |
-
temperature: 0.6,
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
: {
|
| 85 |
-
model: model,
|
| 86 |
-
messages: [
|
| 87 |
-
{ role: 'system', content: systemPrompt },
|
| 88 |
-
{ role: 'user', content: userPrompt },
|
| 89 |
-
],
|
| 90 |
-
temperature: 0.6, // 稍低的温度以保持一致性
|
| 91 |
-
};
|
| 92 |
-
|
| 93 |
-
const response = await fetch(apiUrl, {
|
| 94 |
method: 'POST',
|
| 95 |
-
headers: headers,
|
| 96 |
signal: controller.signal,
|
| 97 |
-
body: JSON.stringify(
|
| 98 |
});
|
| 99 |
|
| 100 |
clearTimeout(timeoutId);
|
|
@@ -102,7 +71,7 @@ const makeAgentRequest = async (agentType, model, apiBaseUrl, apiKey, systemProm
|
|
| 102 |
|
| 103 |
if (!response.ok) {
|
| 104 |
const errText = await response.text();
|
| 105 |
-
console.warn(`[Agent:${agentType}] 请求失败 (${elapsed}s): ${response.status}`);
|
| 106 |
return { success: false, agentType, error: `HTTP ${response.status}`, elapsed };
|
| 107 |
}
|
| 108 |
|
|
@@ -115,13 +84,8 @@ const makeAgentRequest = async (agentType, model, apiBaseUrl, apiKey, systemProm
|
|
| 115 |
return { success: false, agentType, error: 'INVALID_API_RESPONSE', elapsed };
|
| 116 |
}
|
| 117 |
|
| 118 |
-
//
|
| 119 |
-
|
| 120 |
-
if (isGeminiApi && jsonResult.candidates?.[0]?.content?.parts?.[0]?.text) {
|
| 121 |
-
content = jsonResult.candidates[0].content.parts[0].text;
|
| 122 |
-
} else {
|
| 123 |
-
content = jsonResult.choices?.[0]?.message?.content;
|
| 124 |
-
}
|
| 125 |
if (!content) {
|
| 126 |
return { success: false, agentType, error: 'EMPTY_RESPONSE', elapsed };
|
| 127 |
}
|
|
@@ -197,7 +161,7 @@ const makeAgentRequestWithRetry = async (agentType, apiBaseUrl, apiKey, systemPr
|
|
| 197 |
|
| 198 |
for (const model of modelsToTry) {
|
| 199 |
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
| 200 |
-
const result = await makeAgentRequest(agentType, model,
|
| 201 |
|
| 202 |
if (result.success) {
|
| 203 |
// 验证返回数据是否完整
|
|
@@ -307,8 +271,9 @@ const buildAgentUserPrompt = (input, skeletonData, agentType) => {
|
|
| 307 |
* @param {function} onProgress - 进度回调
|
| 308 |
*/
|
| 309 |
export const runParallelAgents = async (input, skeletonData, res, onProgress) => {
|
| 310 |
-
|
| 311 |
-
const
|
|
|
|
| 312 |
|
| 313 |
const agents = [
|
| 314 |
{ type: 'core', prompt: AGENT_CORE_PROMPT, priority: 1 },
|
|
|
|
| 15 |
AGENT_CRYPTO_PROMPT,
|
| 16 |
} from './agentPrompts.js';
|
| 17 |
import { generateFallbackKLine } from './baziCalculator.js';
|
| 18 |
+
import { buildApiRequest, parseApiResponse } from './apiConfig.js';
|
| 19 |
|
| 20 |
+
// 为不同Agent分配最适合的模型
|
|
|
|
|
|
|
|
|
|
| 21 |
const AGENT_MODEL_ASSIGNMENT = {
|
| 22 |
+
core: 'grok-4', // 核心命理 - 逻辑推理强
|
| 23 |
+
kline_past: 'gpt-4o', // 过去K线 - 数据结构化强
|
| 24 |
+
kline_future: 'gpt-4o', // 未来K线 - 数据结构化强
|
| 25 |
+
career: 'claude-3-5-sonnet-20241022', // 事业财富 - 综合能力
|
| 26 |
+
marriage: 'grok-4-auto', // 婚姻健康 - 快速响应
|
| 27 |
+
crypto: 'grok-4', // 币圈分析 - 币圈知识丰富
|
| 28 |
};
|
| 29 |
|
| 30 |
+
// 备用模型列表
|
| 31 |
const FALLBACK_MODELS = [
|
| 32 |
+
'gpt-4o',
|
| 33 |
+
'grok-4',
|
| 34 |
+
'claude-3-5-sonnet-20241022',
|
| 35 |
'gemini-1.5-pro',
|
|
|
|
| 36 |
];
|
| 37 |
|
| 38 |
/**
|
|
|
|
| 48 |
/**
|
| 49 |
* 单个Agent请求
|
| 50 |
*/
|
| 51 |
+
const makeAgentRequest = async (agentType, model, systemPrompt, userPrompt, timeoutMs = 60000) => {
|
| 52 |
const controller = new AbortController();
|
| 53 |
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
| 54 |
|
|
|
|
| 56 |
console.log(`[Agent:${agentType}] 使用模型 ${model} 开始请求...`);
|
| 57 |
const startTime = Date.now();
|
| 58 |
|
| 59 |
+
// 使用新的 API 配置系统
|
| 60 |
+
const apiRequest = buildApiRequest(model, systemPrompt, userPrompt, 0.6);
|
| 61 |
+
|
| 62 |
+
const response = await fetch(apiRequest.url, {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
method: 'POST',
|
| 64 |
+
headers: apiRequest.headers,
|
| 65 |
signal: controller.signal,
|
| 66 |
+
body: JSON.stringify(apiRequest.body),
|
| 67 |
});
|
| 68 |
|
| 69 |
clearTimeout(timeoutId);
|
|
|
|
| 71 |
|
| 72 |
if (!response.ok) {
|
| 73 |
const errText = await response.text();
|
| 74 |
+
console.warn(`[Agent:${agentType}] 请求失败 (${elapsed}s): ${response.status} - ${errText.substring(0, 200)}`);
|
| 75 |
return { success: false, agentType, error: `HTTP ${response.status}`, elapsed };
|
| 76 |
}
|
| 77 |
|
|
|
|
| 84 |
return { success: false, agentType, error: 'INVALID_API_RESPONSE', elapsed };
|
| 85 |
}
|
| 86 |
|
| 87 |
+
// 使用新的响应解析系统
|
| 88 |
+
const content = parseApiResponse(jsonResult, model);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if (!content) {
|
| 90 |
return { success: false, agentType, error: 'EMPTY_RESPONSE', elapsed };
|
| 91 |
}
|
|
|
|
| 161 |
|
| 162 |
for (const model of modelsToTry) {
|
| 163 |
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
| 164 |
+
const result = await makeAgentRequest(agentType, model, systemPrompt, userPrompt);
|
| 165 |
|
| 166 |
if (result.success) {
|
| 167 |
// 验证返回数据是否完整
|
|
|
|
| 271 |
* @param {function} onProgress - 进度回调
|
| 272 |
*/
|
| 273 |
export const runParallelAgents = async (input, skeletonData, res, onProgress) => {
|
| 274 |
+
// 不再需要固定的 API URL 和 Key,由 apiConfig.js 根据模型自动选择
|
| 275 |
+
const apiBaseUrl = null; // 不再使用
|
| 276 |
+
const apiKey = null; // 不再使用
|
| 277 |
|
| 278 |
const agents = [
|
| 279 |
{ type: 'core', prompt: AGENT_CORE_PROMPT, priority: 1 },
|
server/testApi.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API 连接测试端点
|
| 3 |
+
*/
|
| 4 |
+
import { buildApiRequest, parseApiResponse, getApiConfigForModel } from './apiConfig.js';
|
| 5 |
+
import fetch from 'node-fetch';
|
| 6 |
+
|
| 7 |
+
export async function testAllApis() {
|
| 8 |
+
const testModels = [
|
| 9 |
+
{ name: 'gpt-4o', provider: 'OpenAI' },
|
| 10 |
+
{ name: 'grok-4', provider: 'Grok' },
|
| 11 |
+
{ name: 'claude-3-5-sonnet-20241022', provider: 'Claude' },
|
| 12 |
+
{ name: 'gemini-1.5-pro', provider: 'Gemini' },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
const results = [];
|
| 16 |
+
|
| 17 |
+
for (const { name, provider } of testModels) {
|
| 18 |
+
try {
|
| 19 |
+
console.log(`测试 ${provider} (${name})...`);
|
| 20 |
+
const config = getApiConfigForModel(name);
|
| 21 |
+
const testPrompt = 'Say "Hello" in one word.';
|
| 22 |
+
|
| 23 |
+
const apiRequest = buildApiRequest(name, 'You are a helpful assistant.', testPrompt, 0.7);
|
| 24 |
+
|
| 25 |
+
const startTime = Date.now();
|
| 26 |
+
const response = await fetch(apiRequest.url, {
|
| 27 |
+
method: 'POST',
|
| 28 |
+
headers: apiRequest.headers,
|
| 29 |
+
body: JSON.stringify(apiRequest.body),
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
| 33 |
+
|
| 34 |
+
if (!response.ok) {
|
| 35 |
+
const errorText = await response.text();
|
| 36 |
+
results.push({
|
| 37 |
+
provider,
|
| 38 |
+
model: name,
|
| 39 |
+
status: 'FAILED',
|
| 40 |
+
statusCode: response.status,
|
| 41 |
+
error: errorText.substring(0, 200),
|
| 42 |
+
elapsed,
|
| 43 |
+
});
|
| 44 |
+
continue;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const data = await response.json();
|
| 48 |
+
const content = parseApiResponse(data, name);
|
| 49 |
+
|
| 50 |
+
results.push({
|
| 51 |
+
provider,
|
| 52 |
+
model: name,
|
| 53 |
+
status: 'SUCCESS',
|
| 54 |
+
statusCode: response.status,
|
| 55 |
+
response: content?.substring(0, 100) || 'Empty response',
|
| 56 |
+
elapsed,
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
} catch (error) {
|
| 60 |
+
results.push({
|
| 61 |
+
provider,
|
| 62 |
+
model: name,
|
| 63 |
+
status: 'ERROR',
|
| 64 |
+
error: error.message,
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
return results;
|
| 70 |
+
}
|
| 71 |
+
|