310 lines
9.4 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
*
* 🇨🇳 CN LLM Relay · 国内模型API代理中继服务
*
*
* 编号: ZY-CN-LLM-RELAY-001
* 服务器: ZY-SVR-003 · 43.138.243.30 · 广州
* 端口: 3900
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 功能:
* - 接收新加坡服务器转发的LLM API请求
* - 在国内网络环境下直连国内模型API零跨境延迟
* - Bearer Token鉴权防止外部滥用
* - 返回AI回复给新加坡服务器
*
* 架构 (与硅谷Claude中继对称):
* 用户 SG(ZY-SVR-002) 广州(ZY-SVR-003):3900 国内API
* 硅谷模式: SG SV(SSH隧道) Claude API
* 广州模式: SG 广州(HTTP代理) 国内API
*/
'use strict';
const express = require('express');
const https = require('https');
const crypto = require('crypto');
const app = express();
const PORT = process.env.RELAY_PORT || 3900;
const RELAY_API_KEY = process.env.ZY_CN_RELAY_API_KEY || '';
// 启动时校验: 鉴权密钥必须配置
if (!RELAY_API_KEY) {
console.error('[CN中继] ❌ ZY_CN_RELAY_API_KEY 未配置 · 服务拒绝启动');
console.error('[CN中继] 请在 .env.relay 中设置 ZY_CN_RELAY_API_KEY');
process.exit(1);
}
// API请求超时可通过环境变量调整
const API_TIMEOUT_MS = parseInt(process.env.ZY_CN_API_TIMEOUT || '60000', 10);
// ─── 国内模型配置 ───
const DOMESTIC_MODELS = {
ds: {
model: 'deepseek-chat',
endpoint: 'https://api.deepseek.com/v1/chat/completions',
envKey: 'ZY_DEEPSEEK_API_KEY'
},
qw: {
model: 'qwen-turbo',
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
envKey: 'ZY_QIANWEN_API_KEY'
},
km: {
model: 'moonshot-v1-8k',
endpoint: 'https://api.moonshot.cn/v1/chat/completions',
envKey: 'ZY_KIMI_API_KEY'
},
zp: {
model: 'glm-4-flash',
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
envKey: 'ZY_QINGYAN_API_KEY'
}
};
// ─── 速率限制 ───
const rateLimit = require('express-rate-limit');
app.use(rateLimit({
windowMs: 60 * 1000,
max: 120,
standardHeaders: true,
legacyHeaders: false,
message: { error: true, message: '请求过于频繁' }
}));
app.use(express.json({ limit: '2mb' }));
// ─── 中继状态 ───
const relayState = {
totalCalls: 0,
successCalls: 0,
failedCalls: 0,
startTime: Date.now(),
lastCall: null,
modelStats: {}
};
// ─── Bearer Token 鉴权 ───
function apiKeyAuth(req, res, next) {
// 健康检查免鉴权
if (req.path === '/health') return next();
if (!RELAY_API_KEY) {
return res.status(503).json({
error: true,
message: '中继服务未配置鉴权密钥 (ZY_CN_RELAY_API_KEY)'
});
}
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
if (!token) {
return res.status(401).json({ error: true, message: '缺少Authorization头' });
}
// 常数时间比较防止时序攻击
try {
const a = Buffer.from(token);
const b = Buffer.from(RELAY_API_KEY);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(403).json({ error: true, message: '鉴权失败' });
}
} catch {
return res.status(403).json({ error: true, message: '鉴权失败' });
}
next();
}
app.use(apiKeyAuth);
// ─── 健康检查 ───
app.get('/health', (_req, res) => {
const available = Object.entries(DOMESTIC_MODELS).filter(([, cfg]) => {
const key = process.env[cfg.envKey];
return key && key.length > 5;
}).map(([id]) => id);
res.json({
status: 'ok',
service: 'cn-llm-relay',
server: 'ZY-SVR-003',
location: 'Guangzhou, China',
port: PORT,
availableModels: available.length,
totalModels: Object.keys(DOMESTIC_MODELS).length,
uptime: Math.floor((Date.now() - relayState.startTime) / 1000),
stats: {
total: relayState.totalCalls,
success: relayState.successCalls,
failed: relayState.failedCalls
}
});
});
// ─── 调用国内模型API ───
function callDomesticAPI(modelConfig, apiKey, requestBody) {
return new Promise((resolve, reject) => {
const url = new URL(modelConfig.endpoint);
const bodyStr = JSON.stringify(requestBody);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(bodyStr)
}
};
const req = https.request(options, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
try {
const body = JSON.parse(Buffer.concat(chunks).toString());
if (body.error) {
reject(new Error(`${modelConfig.endpoint}: ${body.error.message || JSON.stringify(body.error)}`));
} else {
resolve(body);
}
} catch (e) {
reject(new Error(`${modelConfig.endpoint} 响应解析失败 (HTTP ${res.statusCode})`));
}
});
});
req.setTimeout(API_TIMEOUT_MS, () => {
req.destroy();
reject(new Error(`${modelConfig.endpoint} 请求超时(${API_TIMEOUT_MS}ms)`));
});
req.on('error', (err) => {
reject(new Error(`${modelConfig.endpoint} 连接失败: ${err.message}`));
});
req.write(bodyStr);
req.end();
});
}
// ─── 核心中继接口 ───
app.post('/llm/chat', async (req, res) => {
const startTime = Date.now();
relayState.totalCalls++;
relayState.lastCall = new Date().toISOString();
try {
const { messages, model_id, temperature, max_tokens, fallback_order } = req.body;
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({
error: true,
message: '缺少messages参数或消息列表为空'
});
}
// 确定模型尝试顺序
const tryOrder = fallback_order && Array.isArray(fallback_order)
? fallback_order
: (model_id ? [model_id] : Object.keys(DOMESTIC_MODELS));
let lastError = null;
for (const mid of tryOrder) {
const modelCfg = DOMESTIC_MODELS[mid];
if (!modelCfg) continue;
const apiKey = process.env[modelCfg.envKey];
if (!apiKey || apiKey.length < 5) continue;
try {
const requestBody = {
model: modelCfg.model,
messages,
temperature: temperature || 0.7,
max_tokens: max_tokens || 2000,
stream: false
};
const response = await callDomesticAPI(modelCfg, apiKey, requestBody);
const elapsed = Date.now() - startTime;
// 统计
relayState.successCalls++;
if (!relayState.modelStats[mid]) {
relayState.modelStats[mid] = { calls: 0, tokens: 0 };
}
relayState.modelStats[mid].calls++;
relayState.modelStats[mid].tokens += (response.usage?.total_tokens || 0);
return res.json({
success: true,
relay: 'cn-direct',
model_id: mid,
choices: response.choices,
usage: response.usage,
elapsed_ms: elapsed
});
} catch (err) {
lastError = err;
console.error(`[CN中继] ${mid} 调用失败: ${err.message}`);
continue;
}
}
// 所有模型都失败
relayState.failedCalls++;
res.status(502).json({
error: true,
message: '所有国内模型通道均失败',
last_error: lastError?.message,
elapsed_ms: Date.now() - startTime
});
} catch (err) {
relayState.failedCalls++;
res.status(500).json({
error: true,
message: `中继内部错误: ${err.message}`
});
}
});
// ─── 中继状态接口 ───
app.get('/llm/stats', (_req, res) => {
res.json({
...relayState,
uptimeMs: Date.now() - relayState.startTime,
availableModels: Object.entries(DOMESTIC_MODELS)
.filter(([, cfg]) => {
const key = process.env[cfg.envKey];
return key && key.length > 5;
})
.map(([id, cfg]) => ({ id, model: cfg.model }))
});
});
// ─── 启动 ───
app.listen(PORT, '0.0.0.0', () => {
const available = Object.entries(DOMESTIC_MODELS).filter(([, cfg]) => {
const key = process.env[cfg.envKey];
return key && key.length > 5;
});
console.log(`
🇨🇳 CN LLM Relay · 国内模型中继服务
端口: ${PORT}
鉴权: ${RELAY_API_KEY ? '✅ 已配置' : '❌ 未配置'}
可用模型: ${available.length}/${Object.keys(DOMESTIC_MODELS).length}
${available.map(([id, cfg]) => ` · ${id}: ${cfg.model}`).join('\n')}
`);
});