feat: add trust proxy fix + CN LLM relay service for Guangzhou

工单一: Add app.set('trust proxy', 1) to server.js (prevents PM2 crash)
Phase 1: Create server/cn-llm-relay/ - lightweight LLM proxy for Guangzhou
Phase 2: Modify domestic-llm-gateway.js - relay through Guangzhou with fallback
Phase 3: Add deploy-cn-llm-relay.yml workflow for automated deployment
Update server-registry.json with ZY-SVR-003 and cn-llm-relay config
Update deploy-to-zhuyuan-server.yml to inject CN relay env vars

Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/f6db708e-8b10-4339-b48f-db5f111ae5cd

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-11 14:18:27 +00:00 committed by GitHub
parent 173cb74358
commit 91c957d85e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 799 additions and 8 deletions

View File

@ -0,0 +1,173 @@
name: 🇨🇳 CN LLM中继 · 部署
# ═══════════════════════════════════════════════════════════
# 广州国内LLM API中继服务部署
# 编号: ZY-WF-CN-RELAY
# 服务器: ZY-SVR-003 · 43.138.243.30 · 广州
# 守护: 铸渊 · ICE-GL-ZY001
# 版权: 国作登字-2026-A-00037559
#
# 功能: 将 cn-llm-relay 代理服务部署到广州服务器
# 注入国内LLM API密钥 + 中继鉴权密钥
# PM2 守护进程管理
# ═══════════════════════════════════════════════════════════
on:
workflow_dispatch:
inputs:
action:
description: '部署动作'
required: true
default: 'deploy'
type: choice
options:
- deploy
- health-check
- restart
concurrency:
group: cn-llm-relay-deploy
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy:
name: 🚀 部署CN中继
if: github.event.inputs.action == 'deploy' || github.event.inputs.action == ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 🔐 配置SSH
env:
SSH_KEY: ${{ secrets.ZY_CN_LLM_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/cn_key
chmod 600 ~/.ssh/cn_key
if head -1 ~/.ssh/cn_key | grep -q "BEGIN" && tail -1 ~/.ssh/cn_key | grep -q "END"; then
echo "✅ SSH私钥格式正确"
else
echo "❌ SSH私钥格式异常 — 请检查 ZY_CN_LLM_KEY"
exit 1
fi
ssh-keyscan -H ${{ secrets.ZY_CN_LLM_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: 📦 同步代码到广州
run: |
rsync -avz --delete \
-e "ssh -i ~/.ssh/cn_key" \
--exclude='node_modules' \
--exclude='.env.relay' \
--exclude='logs' \
server/cn-llm-relay/ \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }}:/opt/cn-llm-relay/
- name: 📥 安装依赖 & 配置 & 启动
env:
ZY_DEEPSEEK_API_KEY: ${{ secrets.ZY_DEEPSEEK_API_KEY }}
ZY_QIANWEN_API_KEY: ${{ secrets.ZY_QIANWEN_API_KEY }}
ZY_KIMI_API_KEY: ${{ secrets.ZY_KIMI_API_KEY }}
ZY_QINGYAN_API_KEY: ${{ secrets.ZY_QINGYAN_API_KEY }}
ZY_CN_RELAY_API_KEY: ${{ secrets.ZY_CN_LLM_RELAY_KEY }}
run: |
ssh -i ~/.ssh/cn_key \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }} << 'DEPLOY_CMD'
set -e
cd /opt/cn-llm-relay
# 创建日志目录
mkdir -p /opt/cn-llm-relay/logs
# 安装依赖
npm install --production 2>&1
DEPLOY_CMD
# 写入环境变量在本地构建后通过SSH写入避免heredoc变量展开问题
ssh -i ~/.ssh/cn_key \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }} \
"cat > /opt/cn-llm-relay/.env.relay << 'ENVEOF'
RELAY_PORT=3900
ZY_CN_RELAY_API_KEY=${ZY_CN_RELAY_API_KEY}
ZY_DEEPSEEK_API_KEY=${ZY_DEEPSEEK_API_KEY}
ZY_QIANWEN_API_KEY=${ZY_QIANWEN_API_KEY}
ZY_KIMI_API_KEY=${ZY_KIMI_API_KEY}
ZY_QINGYAN_API_KEY=${ZY_QINGYAN_API_KEY}
ENVEOF
chmod 600 /opt/cn-llm-relay/.env.relay"
# PM2 启动
ssh -i ~/.ssh/cn_key \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }} << 'PM2_CMD'
set -e
cd /opt/cn-llm-relay
pm2 delete cn-llm-relay 2>/dev/null || true
pm2 start ecosystem.config.js
pm2 save
# 健康检查
sleep 3
if curl -sf http://127.0.0.1:3900/health > /dev/null 2>&1; then
echo "✅ CN LLM Relay 健康检查通过"
curl -s http://127.0.0.1:3900/health | head -c 500
else
echo "⚠️ 健康检查未通过 · 查看日志"
pm2 logs cn-llm-relay --lines 20 --nostream
fi
PM2_CMD
health-check:
name: 🩺 健康检查
if: github.event.inputs.action == 'health-check'
runs-on: ubuntu-latest
steps:
- name: 🔐 配置SSH
env:
SSH_KEY: ${{ secrets.ZY_CN_LLM_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/cn_key
chmod 600 ~/.ssh/cn_key
ssh-keyscan -H ${{ secrets.ZY_CN_LLM_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: 🩺 检查服务状态
run: |
ssh -i ~/.ssh/cn_key \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }} << 'CMD'
echo "═══ PM2 状态 ═══"
pm2 list
echo ""
echo "═══ 健康检查 ═══"
curl -s http://127.0.0.1:3900/health || echo "❌ 服务未响应"
echo ""
echo "═══ 最近日志 ═══"
pm2 logs cn-llm-relay --lines 10 --nostream 2>/dev/null || echo "无日志"
CMD
restart:
name: 🔄 重启服务
if: github.event.inputs.action == 'restart'
runs-on: ubuntu-latest
steps:
- name: 🔐 配置SSH
env:
SSH_KEY: ${{ secrets.ZY_CN_LLM_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/cn_key
chmod 600 ~/.ssh/cn_key
ssh-keyscan -H ${{ secrets.ZY_CN_LLM_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
- name: 🔄 重启
run: |
ssh -i ~/.ssh/cn_key \
${{ secrets.ZY_CN_LLM_USER }}@${{ secrets.ZY_CN_LLM_HOST }} << 'CMD'
cd /opt/cn-llm-relay
pm2 restart cn-llm-relay
sleep 2
curl -s http://127.0.0.1:3900/health || echo "❌ 重启后服务未响应"
CMD

View File

@ -304,6 +304,8 @@ jobs:
ZY_OSS_SECRET: ${{ secrets.ZY_OSS_SECRET }}
ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }}
ZY_NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }}
ZY_CN_LLM_RELAY_HOST: ${{ secrets.ZY_CN_LLM_RELAY_HOST }}
ZY_CN_LLM_RELAY_KEY: ${{ secrets.ZY_CN_LLM_RELAY_KEY }}
run: |
COMMIT_SHA="${{ github.sha }}"
REF_NAME="${{ github.ref_name }}"
@ -326,6 +328,15 @@ jobs:
echo "ZY_KIMI_API_KEY=${ZY_KIMI_API_KEY}" >> /opt/zhuyuan/app/.env.app
echo "ZY_QINGYAN_API_KEY=${ZY_QINGYAN_API_KEY}" >> /opt/zhuyuan/app/.env.app
echo "ZY_NOTION_TOKEN=${ZY_NOTION_TOKEN}" >> /opt/zhuyuan/app/.env.app
# ─── 广州CN中继配置可选·配置后国内API走广州中继 ───
if [ -n "${ZY_CN_LLM_RELAY_HOST}" ]; then
echo "ZY_CN_LLM_RELAY_HOST=${ZY_CN_LLM_RELAY_HOST}" >> /opt/zhuyuan/app/.env.app
echo "ZY_CN_LLM_RELAY_KEY=${ZY_CN_LLM_RELAY_KEY}" >> /opt/zhuyuan/app/.env.app
echo "ZY_CN_LLM_RELAY_PORT=3900" >> /opt/zhuyuan/app/.env.app
echo "✅ 广州CN中继已配置: ${ZY_CN_LLM_RELAY_HOST}:3900"
else
echo " 广州CN中继未配置·国内API走直连"
fi
chmod 600 /opt/zhuyuan/app/.env.app
# 复制Nginx配置

View File

@ -23,6 +23,15 @@
'use strict';
const https = require('https');
const http = require('http');
// ─── 广州CN中继配置 ───
// 当配置了 ZY_CN_LLM_RELAY_HOST 时,请求走广州中继(国内直连·低延迟)
// 广州不可达时降级为直连国内API跨境·高延迟但可用
const CN_RELAY_HOST = process.env.ZY_CN_LLM_RELAY_HOST || '';
const CN_RELAY_PORT = parseInt(process.env.ZY_CN_LLM_RELAY_PORT || '3900', 10);
const CN_RELAY_KEY = process.env.ZY_CN_LLM_RELAY_KEY || '';
const CN_RELAY_TIMEOUT = parseInt(process.env.ZY_CN_LLM_RELAY_TIMEOUT || '30000', 10);
// ─── 国内模型配置(不对外暴露模型名称) ───
const DOMESTIC_MODELS = [
@ -195,6 +204,62 @@ function callDomesticLLM(modelConfig, messages) {
});
}
/**
* 通过广州CN中继调用国内模型API
* 架构: SG(新加坡) 广州(ZY-SVR-003):3900 国内API
* 对称于硅谷Claude中继: SG SV(SSH隧道) Claude API
*/
function callViaCNRelay(messages, selected, fallbackOrder) {
return new Promise((resolve, reject) => {
const requestBody = JSON.stringify({
messages,
model_id: selected.id,
temperature: selected.temperature || 0.7,
max_tokens: selected.selectedMaxTokens || selected.maxTokens,
fallback_order: fallbackOrder
});
const options = {
hostname: CN_RELAY_HOST,
port: CN_RELAY_PORT,
path: '/llm/chat',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CN_RELAY_KEY}`,
'Content-Length': Buffer.byteLength(requestBody)
}
};
const req = http.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(body.message || '中继返回错误'));
} else {
resolve(body);
}
} catch (e) {
reject(new Error('中继响应解析失败'));
}
});
});
req.setTimeout(CN_RELAY_TIMEOUT, () => {
req.destroy();
reject(new Error(`广州中继超时(${CN_RELAY_TIMEOUT}ms)`));
});
req.on('error', (err) => {
reject(new Error(`广州中继连接失败: ${err.message}`));
});
req.write(requestBody);
req.end();
});
}
/**
* 通感语言核心系统提示词
*/
@ -262,7 +327,11 @@ const _cleanupTimer = setInterval(() => {
if (_cleanupTimer.unref) _cleanupTimer.unref();
/**
* 国内模型智能对话带自动降级
* 国内模型智能对话带广州中继 + 自动降级
*
* 调用链:
* 1. 广州CN中继如已配置 国内直连·低延迟
* 2. 降级: 直连国内API 跨境·高延迟但可用
*/
async function chat(userId, message) {
const ctx = getContext(userId);
@ -284,12 +353,57 @@ async function chat(userId, message) {
};
}
// 尝试调用,失败则降级
// 获取可用模型的降级顺序
const available = DOMESTIC_MODELS.filter(m => {
const key = process.env[m.envKey];
return key && key.length > 5;
});
const fallbackOrder = [selected, ...available.filter(m => m.id !== selected.id)].map(m => m.id);
// ── 优先走广州CN中继 ──
if (CN_RELAY_HOST && CN_RELAY_KEY) {
try {
const relayResponse = await callViaCNRelay(messages, selected, fallbackOrder);
const content = relayResponse.choices?.[0]?.message?.content || '铸渊暂时无法回应...';
const usage = relayResponse.usage || {};
// 记录上下文
ctx.messages.push({ role: 'user', content: message });
ctx.messages.push({ role: 'assistant', content });
ctx.count++;
if (ctx.messages.length > MAX_HISTORY * 2) {
ctx.messages = ctx.messages.slice(-MAX_HISTORY * 2);
}
// 统计
gatewayState.totalCalls++;
gatewayState.successCalls++;
const modelId = relayResponse.model_id || selected.id;
if (!gatewayState.modelStats[modelId]) {
gatewayState.modelStats[modelId] = { calls: 0, tokens: 0 };
}
gatewayState.modelStats[modelId].calls++;
gatewayState.modelStats[modelId].tokens += (usage.total_tokens || 0);
return {
success: true,
message: content,
model: '智能路由', // 不暴露具体模型名称
tier: 'economy',
reason: selected.reason,
relay: 'cn-relay',
usage: {
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0
}
};
} catch (relayErr) {
console.error(`[国内网关] 广州中继失败,降级为直连: ${relayErr.message}`);
// 继续走直连降级路径
}
}
// ── 降级: 直连国内API (从新加坡跨境调用) ──
let lastError = null;
const tried = [selected, ...available.filter(m => m.id !== selected.id)];
@ -324,6 +438,7 @@ async function chat(userId, message) {
model: '智能路由', // 不暴露具体模型名称
tier: model.tier,
reason: selected.reason,
relay: 'direct',
usage: {
prompt_tokens: usage.prompt_tokens || 0,
completion_tokens: usage.completion_tokens || 0
@ -360,7 +475,12 @@ function getGatewayStats() {
const key = process.env[m.envKey];
return key && key.length > 5;
}).length,
totalModels: DOMESTIC_MODELS.length
totalModels: DOMESTIC_MODELS.length,
cnRelay: {
configured: !!(CN_RELAY_HOST && CN_RELAY_KEY),
host: CN_RELAY_HOST || null,
port: CN_RELAY_PORT
}
};
}

View File

@ -38,6 +38,10 @@ const PORT = process.env.PORT || 3800;
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// ─── 信任代理 (Nginx反代 → Express正确读取客户端IP) ───
// 冰朔 D67 实机排查: 缺少此行会导致 express-rate-limit 无法获取真实IP → PM2崩溃
app.set('trust proxy', 1);
// ─── 速率限制 ───
const rateLimit = require('express-rate-limit');

View File

@ -0,0 +1,56 @@
/**
* CN LLM Relay · PM2 配置
*
* 编号: ZY-SVR-PM2-003
* 服务器: ZY-SVR-003 · 43.138.243.30 · 广州
* 守护: 铸渊 · ICE-GL-ZY001
*/
const fs = require('fs');
const path = require('path');
function loadEnvFile(filePath) {
const env = {};
try {
if (fs.existsSync(filePath)) {
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const idx = trimmed.indexOf('=');
if (idx > 0) {
env[trimmed.substring(0, idx).trim()] = trimmed.substring(idx + 1).trim();
}
}
}
} catch (e) {
console.error(`加载环境变量失败: ${e.message}`);
}
return env;
}
const relayEnv = loadEnvFile(path.join(__dirname, '.env.relay'));
module.exports = {
apps: [
{
name: 'cn-llm-relay',
script: 'relay-server.js',
cwd: '/opt/cn-llm-relay',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '128M',
env: {
NODE_ENV: 'production',
RELAY_PORT: 3900,
...relayEnv
},
log_file: '/opt/cn-llm-relay/logs/relay-combined.log',
error_file: '/opt/cn-llm-relay/logs/relay-error.log',
out_file: '/opt/cn-llm-relay/logs/relay-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
time: true
}
]
};

View File

@ -0,0 +1,20 @@
{
"name": "cn-llm-relay",
"version": "1.0.0",
"description": "铸渊国内LLM代理中继 · 广州→国内API · ZY-SVR-003",
"main": "relay-server.js",
"scripts": {
"start": "node relay-server.js",
"health": "curl -s http://localhost:3900/health | jq ."
},
"dependencies": {
"express": "^4.21.0",
"express-rate-limit": "^7.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"author": "铸渊 · ICE-GL-ZY001",
"license": "UNLICENSED",
"private": true
}

View File

@ -0,0 +1,297 @@
#!/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 || '';
// ─── 国内模型配置 ───
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(body.error.message || JSON.stringify(body.error)));
} else {
resolve(body);
}
} catch (e) {
reject(new Error('响应解析失败'));
}
});
});
req.setTimeout(60000, () => {
req.destroy();
reject(new Error('国内API请求超时(60s)'));
});
req.on('error', reject);
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)) {
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')}
`);
});

View File

@ -0,0 +1,79 @@
{
"_sovereign": "TCS-0002∞ | SYS-GLW-0001",
"_copyright": "国作登字-2026-A-00037559",
"_description": "广州国内LLM中继配置 · API走广州·算力走新加坡",
"version": "1.0",
"relay_architecture": {
"description": "反向中继: 新加坡(算力) → 广州(国内网络出口) → 国内LLM API",
"note": "与硅谷Claude中继对称: SG→SV→Claude = SG→GZ→国内API",
"singapore_servers": {
"ZY-SVR-002": {
"role": "面孔·前端+专线主入口",
"location": "Singapore",
"spec": "2核8G"
},
"ZY-SVR-005": {
"role": "大脑·DB+MCP+Agent+订阅",
"location": "Singapore",
"spec": "4核8G"
}
},
"guangzhou_server": {
"ZY-SVR-003": {
"role": "国内LLM API中继·国内网络出口",
"location": "Guangzhou, China",
"ipv4": "43.138.243.30",
"purpose": "在国内网络环境下直连国内LLM API零跨境延迟",
"github_secrets": {
"host": "ZY_CN_LLM_HOST",
"ssh_key": "ZY_CN_LLM_KEY",
"user": "ZY_CN_LLM_USER"
}
}
}
},
"domestic_routing": {
"trigger_models": [
"deepseek-chat",
"qwen-turbo",
"moonshot-v1-8k",
"glm-4-flash"
],
"relay_method": "http_proxy",
"relay_config": {
"description": "新加坡服务器通过HTTP将国内LLM请求转发到广州中继服务",
"relay_port": 3900,
"relay_path": "/llm/chat",
"auth_method": "Bearer Token (ZY_CN_LLM_RELAY_KEY)",
"protocol": "HTTP (内部通信·服务器间)"
},
"fallback": "direct",
"fallback_note": "如广州中继不可用降级为直连国内API从新加坡跨境调用·延迟高但可用"
},
"bandwidth_optimization": {
"strategy": "广州国内出口中继",
"description": "请求体通常很小(几KB)广州中继延迟可忽略。国内API调用从跨境秒级降到毫秒级。",
"gz_bandwidth": "国内直连·零跨境延迟",
"sg_bandwidth": "高带宽·算力中心·会话管理"
},
"setup_requirements": {
"guangzhou_server": [
"安装Node.js 20+",
"部署 cn-llm-relay 服务 (端口3900)",
"配置 .env.relay (国内API密钥 + 中继鉴权密钥)",
"PM2守护进程·开机自启",
"防火墙放行3900端口 (仅允许新加坡服务器IP)"
],
"singapore_server": [
"配置 ZY_CN_LLM_RELAY_HOST (广州服务器IP)",
"配置 ZY_CN_LLM_RELAY_KEY (中继鉴权密钥)",
"domestic-llm-gateway.js 自动检测并使用中继"
],
"github_secrets": [
"ZY_CN_LLM_HOST — 广州服务器IP (43.138.243.30)",
"ZY_CN_LLM_KEY — SSH私钥 (部署用)",
"ZY_CN_LLM_USER — SSH用户名",
"ZY_CN_LLM_RELAY_KEY — 中继API鉴权密钥 (新加坡→广州通信用)"
]
}
}

View File

@ -1,9 +1,9 @@
{
"_sovereign": "TCS-0002∞ | SYS-GLW-0001",
"_copyright": "国作登字-2026-A-00037559",
"_description": "光湖语言世界 · 台服务器注册表",
"version": "1.1",
"updated_at": "2026-04-10",
"_description": "光湖语言世界 · 台服务器注册表",
"version": "1.2",
"updated_at": "2026-04-11",
"servers": [
{
"id": "ZY-SVR-002",
@ -27,6 +27,25 @@
"user": "SERVER_USER"
}
},
{
"id": "ZY-SVR-003",
"name": "国内出口",
"role": "国内LLM API中继+VPN中转",
"location": "Guangzhou, China",
"ipv4": "43.138.243.30",
"spec": "低配",
"status": "active",
"services": [
"cn-llm-relay (端口3900·国内LLM API代理中继)",
"nginx stream (VPN中转·端口2053→SG:443)"
],
"github_secrets": {
"host": "ZY_CN_LLM_HOST",
"key": "ZY_CN_LLM_KEY",
"user": "ZY_CN_LLM_USER"
},
"notes": "肥猫广州服务器·复用为国内LLM API中继·冰朔D67批准"
},
{
"id": "ZY-SVR-005",
"name": "大脑",
@ -36,7 +55,7 @@
"status": "active",
"services": [
"postgresql (人格体数据库)",
"mcp-server (端口3100·99个工具)",
"mcp-server (端口3100·121个工具)",
"subscription-v3 (端口3805·多用户专线)",
"bandwidth-pool-agent (带宽汇聚)",
"email-hub (邮件通信中枢)",
@ -103,13 +122,25 @@
}
]
},
"llm_relay": {
"description": "双向LLM API中继架构",
"routes": {
"domestic_api": "ZY-SVR-002 → ZY-SVR-003(广州) → 国内API (DeepSeek/千问/Kimi/智谱)",
"claude_api": "ZY-SVR-002 → ZY-SVR-SV(硅谷SSH隧道) → Claude API"
},
"config_files": {
"domestic": "server/proxy/config/cn-llm-relay-config.json",
"claude": "server/proxy/config/claude-relay-config.json"
}
},
"bandwidth_strategy": {
"description": "四台服务器按用途分流·带宽最大化",
"description": "台服务器按用途分流·带宽最大化",
"routing": {
"static_frontend": "ZY-SVR-002",
"vpn_main_entry": "ZY-SVR-002",
"database_mcp": "ZY-SVR-005",
"subscription_service": "ZY-SVR-005",
"domestic_llm_relay": "ZY-SVR-003",
"claude_api_relay": "ZY-SVR-SV",
"international_api": "ZY-SVR-SV",
"awen_team": "ZY-SVR-AWEN",