const express = require('express'); const path = require('path'); const axios = require('axios'); const cors = require('cors'); const dotenv = require('dotenv'); const OpenAI = require('openai'); dotenv.config(); const app = express(); const PORT = process.env.PORT || 4173; const SUANMING_API_BASE = process.env.SUANMING_API_BASE || 'http://localhost:3001/api'; const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ''; const DEEPSEEK_API_URL = process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com'; const DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat'; const DEEPSEEK_TIMEOUT_MS = Number(process.env.DEEPSEEK_TIMEOUT_MS || 20000); const SUANMING_TIMEOUT_MS = Number(process.env.SUANMING_TIMEOUT_MS || 15000); const SUANMING_EMAIL = process.env.SUANMING_EMAIL || ''; const SUANMING_PASSWORD = process.env.SUANMING_PASSWORD || ''; // Token 管理器 const tokenManager = { token: null, loginPromise: null, // 防止并发登录 async login() { // 如果已有登录请求在进行,等待其完成 if (this.loginPromise) { return this.loginPromise; } if (!SUANMING_EMAIL || !SUANMING_PASSWORD) { console.error('[tokenManager] SUANMING_EMAIL 或 SUANMING_PASSWORD 未配置'); return null; } // 创建登录 Promise 并保存引用 this.loginPromise = (async () => { try { console.log('[tokenManager] 正在登录获取 token...'); const response = await axios.post( `${SUANMING_API_BASE}/auth/login`, { email: SUANMING_EMAIL, password: SUANMING_PASSWORD }, { timeout: SUANMING_TIMEOUT_MS } ); this.token = response.data?.data?.token; if (this.token) { console.log('[tokenManager] Token 获取成功'); } return this.token; } catch (error) { console.error('[tokenManager] 登录失败:', error.message); return null; } finally { this.loginPromise = null; } })(); return this.loginPromise; }, async getToken() { if (!this.token) { await this.login(); } return this.token; }, async refreshToken() { this.token = null; return this.login(); } }; const DEEPSEEK_BASE_URL = DEEPSEEK_API_URL.replace( /\/v1\/chat\/completions\/?$/, '' ); const openaiClient = DEEPSEEK_API_KEY && new OpenAI({ baseURL: DEEPSEEK_BASE_URL, apiKey: DEEPSEEK_API_KEY, timeout: DEEPSEEK_TIMEOUT_MS, }); app.use(cors()); app.use(express.json({ limit: '1mb' })); app.use((req, _res, next) => { req.requestTime = Date.now(); next(); }); const publicDir = path.join(__dirname, 'public'); app.use(express.static(publicDir)); app.get('/api/health', (_req, res) => { res.json({ status: 'ok', time: new Date().toISOString(), }); }); app.post('/api/fortune', async (req, res) => { try { const validationError = validateFortuneRequest(req.body); if (validationError) { return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: validationError, }, }); } const { endpoint, payload } = buildSuanmingRequest(req.body); // 调用 suanming API,支持 token 过期自动重试 let suanmingResponse; try { suanmingResponse = await callSuanmingApi(endpoint, payload); } catch (error) { // 如果是 401 错误,刷新 token 后重试一次 if (error.response?.status === 401) { console.log('[fortune] Token 过期,正在刷新...'); await tokenManager.refreshToken(); suanmingResponse = await callSuanmingApi(endpoint, payload); } else { throw error; } } const rawSuanming = suanmingResponse.data; const deepseekResult = await buildDeepseekInterpretation(req.body, rawSuanming); return res.json({ success: true, data: { ai_text: deepseekResult.text, raw_suanming: rawSuanming, meta: { type: req.body.type || 'bazi', model: deepseekResult.model, elapsed_ms: Date.now() - req.requestTime, warnings: deepseekResult.warning ? [deepseekResult.warning] : [], }, }, }); } catch (error) { console.error('[fortune] request failed', error.message); const status = error.response?.status || error.statusCode || 500; const errorPayload = { success: false, error: { code: deriveErrorCode(error), message: error.response?.data?.error?.message || error.message || 'Unknown error', }, }; if (error.response?.data) { errorPayload.error.details = error.response.data; } return res.status(status).json(errorPayload); } }); // 调用 suanming API async function callSuanmingApi(endpoint, payload) { const token = await tokenManager.getToken(); const headers = { 'Content-Type': 'application/json' }; if (token) { headers.Authorization = `Bearer ${token}`; } return axios.post( `${SUANMING_API_BASE}${endpoint}`, payload, { timeout: SUANMING_TIMEOUT_MS, headers } ); } app.get('*', (_req, res) => { res.sendFile(path.join(publicDir, 'index.html')); }); app.listen(PORT, async () => { console.log(`🧙 Suanming gateway running at http://localhost:${PORT}`); // 启动时预热 Token await tokenManager.getToken(); }); function validateFortuneRequest(body = {}) { const requiredFields = ['name', 'birth_date', 'birth_time', 'gender']; const missing = requiredFields.filter((key) => !body[key]); if (missing.length) { return `缺少字段: ${missing.join(', ')}`; } const allowedTypes = ['bazi']; const type = body.type || 'bazi'; if (!allowedTypes.includes(type)) { return `暂不支持的 type: ${type},目前仅支持 ${allowedTypes.join(', ')}`; } return null; } function buildSuanmingRequest(body) { const type = body.type || 'bazi'; if (type === 'bazi') { return { endpoint: '/analysis/bazi', payload: { birth_data: { name: body.name, birth_date: body.birth_date, birth_time: body.birth_time, gender: body.gender, is_lunar: Boolean(body.is_lunar), }, }, }; } const error = new Error(`Unsupported type: ${type}`); error.statusCode = 400; throw error; } async function buildDeepseekInterpretation(body, rawSuanming) { const baseResult = { text: '', warning: null, model: DEEPSEEK_MODEL, }; if (!DEEPSEEK_API_KEY) { baseResult.warning = 'DeepSeek API Key 未配置,已跳过 AI 解读'; baseResult.text = '⚠️ DeepSeek API Key 未配置,无法生成 AI 解读。\n请在 .env 中设置 DEEPSEEK_API_KEY 后重试。'; return baseResult; } const prompt = [ `用户姓名: ${body.name}`, `出生日期: ${body.birth_date} ${body.birth_time}`, `性别: ${body.gender}`, `是否农历: ${body.is_lunar ? '是' : '否'}`, `用户问题: ${body.question || '无特定问题'}`, `额外关注点: ${JSON.stringify(body.extra_options || {})}`, '', '以下是 suanming 系统返回的原始数据,请结合命理知识给出通俗易懂且积极向上的解读,最后请附上声明:“以上内容由AI生成,仅供参考。”', JSON.stringify(rawSuanming, null, 2), ].join('\n'); const payload = { model: DEEPSEEK_MODEL, temperature: 0.7, max_tokens: 1024, messages: [ { role: 'system', content: '你是一位经验丰富的命理大师,擅长把专业的八字分析转化为温暖、易懂且具有行动建议的文字。回答时保持积极、真诚,并提醒内容仅供参考。', }, { role: 'user', content: prompt, }, ], }; try { if (!openaiClient) throw new Error('OpenAI client 未初始化'); const completion = await openaiClient.chat.completions.create(payload); baseResult.text = completion?.choices?.[0]?.message?.content?.trim() || 'DeepSeek 没有返回内容,请稍后再试。'; return baseResult; } catch (error) { console.error('[deepseek] failed', error.message); const errMsg = error?.error?.message || error?.message || 'DeepSeek 调用失败'; baseResult.warning = `DeepSeek 调用失败:${errMsg}`; baseResult.text = '⚠️ DeepSeek 调用失败,无法生成 AI 解读。请稍后重试或检查 API Key 配置。'; return baseResult; } } function deriveErrorCode(error) { if (error.code === 'ECONNABORTED') return 'UPSTREAM_TIMEOUT'; if (error.response?.status === 404) return 'ENDPOINT_NOT_FOUND'; if (error.response?.status === 401) return 'UNAUTHORIZED'; return 'GATEWAY_ERROR'; }