fix: address code review — CORS, input validation, timeout alignment, logging
- CORS: default whitelist restricted to project domains (not wildcard) - Input validation: clamp temperature [0,2] and max_tokens [1,8192] - Timeout: increase to 60s, align Nginx config (90s read / 60s send) - Logging: remove client IP and message count from logs - Frontend: move proxy option lower in dropdown, fix duplicate system prompt - Shorten proxy mode placeholder text Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
723f6b16bc
commit
466f344ef8
|
|
@ -40,7 +40,7 @@ const { URL } = require('url');
|
||||||
// 配置
|
// 配置
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
const PORT = parseInt(process.env.PROXY_PORT || '3721', 10);
|
const PORT = parseInt(process.env.PROXY_PORT || '3721', 10);
|
||||||
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || '*').split(',').map(s => s.trim());
|
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || 'https://qinfendebingshuo.github.io,https://guanghulab.com,http://localhost:8765').split(',').map(s => s.trim());
|
||||||
const RATE_LIMIT_RPM = parseInt(process.env.RATE_LIMIT_RPM || '10', 10);
|
const RATE_LIMIT_RPM = parseInt(process.env.RATE_LIMIT_RPM || '10', 10);
|
||||||
|
|
||||||
// 模型端点映射
|
// 模型端点映射
|
||||||
|
|
@ -243,8 +243,8 @@ function proxyStream(upstreamUrl, apiKey, requestBody, res) {
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set timeout (30 seconds for connection, but streaming can be longer)
|
// Connection timeout only — once response headers arrive, streaming can run longer
|
||||||
upstream.setTimeout(30000, () => {
|
upstream.setTimeout(60000, () => {
|
||||||
upstream.destroy(new Error('连接超时'));
|
upstream.destroy(new Error('连接超时'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -312,6 +312,10 @@ async function handleChat(req, res) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate numeric parameters
|
||||||
|
const temp = typeof temperature === 'number' ? Math.max(0, Math.min(2, temperature)) : 0.8;
|
||||||
|
const maxTok = typeof max_tokens === 'number' ? Math.max(1, Math.min(8192, Math.floor(max_tokens))) : 2000;
|
||||||
|
|
||||||
// Determine provider
|
// Determine provider
|
||||||
const prov = provider || 'deepseek';
|
const prov = provider || 'deepseek';
|
||||||
const config = MODEL_CONFIG[prov];
|
const config = MODEL_CONFIG[prov];
|
||||||
|
|
@ -341,8 +345,8 @@ async function handleChat(req, res) {
|
||||||
model: mdl,
|
model: mdl,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
stream: stream !== false, // Default to streaming
|
stream: stream !== false, // Default to streaming
|
||||||
temperature: temperature ?? 0.8,
|
temperature: temp,
|
||||||
max_tokens: max_tokens ?? 2000
|
max_tokens: maxTok
|
||||||
};
|
};
|
||||||
|
|
||||||
// If system prompt provided, prepend as system message
|
// If system prompt provided, prepend as system message
|
||||||
|
|
@ -352,7 +356,7 @@ async function handleChat(req, res) {
|
||||||
|
|
||||||
const upstreamUrl = config.base + '/chat/completions';
|
const upstreamUrl = config.base + '/chat/completions';
|
||||||
|
|
||||||
console.log(`[代理] ${clientId} → ${prov}/${mdl} (${messages.length}条消息)`);
|
console.log(`[代理] ${prov}/${mdl}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await proxyStream(upstreamUrl, apiKey, upstreamBody, res);
|
await proxyStream(upstreamUrl, apiKey, upstreamBody, res);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ location /api/ {
|
||||||
proxy_cache off;
|
proxy_cache off;
|
||||||
chunked_transfer_encoding on;
|
chunked_transfer_encoding on;
|
||||||
|
|
||||||
# 长连接超时(AI 生成可能需要较长时间)
|
# 长连接超时(与 Node.js api-proxy 的 60s 连接超时对齐,流式响应可更长)
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 90s;
|
||||||
proxy_send_timeout 120s;
|
proxy_send_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,13 +323,13 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
||||||
<div class="fg">
|
<div class="fg">
|
||||||
<label>选择 AI 提供商</label>
|
<label>选择 AI 提供商</label>
|
||||||
<select id="sp" onchange="onProv(this.value,'s')">
|
<select id="sp" onchange="onProv(this.value,'s')">
|
||||||
<option value="proxy">🔌 后端代理(国内直连·推荐)</option>
|
|
||||||
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||||
<option value="openai">OpenAI (ChatGPT)</option>
|
|
||||||
<option value="gemini">🔮 Google Gemini</option>
|
|
||||||
<option value="deepseek">DeepSeek(深度求索·国内直连)</option>
|
<option value="deepseek">DeepSeek(深度求索·国内直连)</option>
|
||||||
|
<option value="proxy">🔌 后端代理(guanghulab.com)</option>
|
||||||
<option value="moonshot">Moonshot(Kimi·国内直连)</option>
|
<option value="moonshot">Moonshot(Kimi·国内直连)</option>
|
||||||
<option value="zhipu">智谱 AI(GLM·国内直连)</option>
|
<option value="zhipu">智谱 AI(GLM·国内直连)</option>
|
||||||
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
|
<option value="gemini">🔮 Google Gemini</option>
|
||||||
<option value="custom">自定义 / 其他兼容接口</option>
|
<option value="custom">自定义 / 其他兼容接口</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -515,13 +515,13 @@ footer{padding:12px 18px 16px;background:var(--s1);border-top:1px solid var(--bo
|
||||||
<div class="sfg">
|
<div class="sfg">
|
||||||
<label>AI 提供商</label>
|
<label>AI 提供商</label>
|
||||||
<select id="cp" onchange="onProv(this.value,'c')">
|
<select id="cp" onchange="onProv(this.value,'c')">
|
||||||
<option value="proxy">🔌 后端代理(国内直连·推荐)</option>
|
|
||||||
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
<option value="yunwu">☁️ 云雾 AI(团队推荐)</option>
|
||||||
<option value="openai">OpenAI (ChatGPT)</option>
|
|
||||||
<option value="gemini">🔮 Google Gemini</option>
|
|
||||||
<option value="deepseek">DeepSeek(深度求索·国内直连)</option>
|
<option value="deepseek">DeepSeek(深度求索·国内直连)</option>
|
||||||
|
<option value="proxy">🔌 后端代理(guanghulab.com)</option>
|
||||||
<option value="moonshot">Moonshot(Kimi·国内直连)</option>
|
<option value="moonshot">Moonshot(Kimi·国内直连)</option>
|
||||||
<option value="zhipu">智谱 AI(GLM·国内直连)</option>
|
<option value="zhipu">智谱 AI(GLM·国内直连)</option>
|
||||||
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
|
<option value="gemini">🔮 Google Gemini</option>
|
||||||
<option value="custom">自定义</option>
|
<option value="custom">自定义</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1188,7 +1188,7 @@ function onProv(pv,ctx){
|
||||||
}
|
}
|
||||||
// Proxy mode: API key is optional (managed server-side)
|
// Proxy mode: API key is optional (managed server-side)
|
||||||
const ki = document.getElementById(keyInp);
|
const ki = document.getElementById(keyInp);
|
||||||
if(ki) ki.placeholder = isProxy ? '后端代理模式 — 无需填写 API 密钥(服务器端统一管理)' : (ctx==='s'?'粘贴任意 API 密钥,点击右侧按钮自动识别提供商和模型':'留空 = 保持当前密钥不变;粘贴新密钥即替换');
|
if(ki) ki.placeholder = isProxy ? '后端代理模式(无需密钥)' : (ctx==='s'?'粘贴任意 API 密钥,点击右侧按钮自动识别提供商和模型':'留空 = 保持当前密钥不变;粘贴新密钥即替换');
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillModels(selId, pv, cur){
|
function fillModels(selId, pv, cur){
|
||||||
|
|
@ -1584,9 +1584,10 @@ async function streamReply(txt){
|
||||||
const url = isProxy ? (A.base+'/chat') : (A.base+'/chat/completions');
|
const url = isProxy ? (A.base+'/chat') : (A.base+'/chat/completions');
|
||||||
const headers = {'Content-Type':'application/json'};
|
const headers = {'Content-Type':'application/json'};
|
||||||
if(!isProxy && A.key) headers['Authorization'] = 'Bearer '+A.key;
|
if(!isProxy && A.key) headers['Authorization'] = 'Bearer '+A.key;
|
||||||
|
const allMsgs = [{role:'system',content:sysPrompt()},...A.msgs];
|
||||||
const reqBody = isProxy
|
const reqBody = isProxy
|
||||||
? {provider:A.prov==='proxy'?undefined:A.prov, model:A.mdl, messages:[{role:'system',content:sysPrompt()},...A.msgs], stream:true, temperature:0.8, max_tokens:2000, system:sysPrompt()}
|
? {model:A.mdl, messages:allMsgs, stream:true, temperature:0.8, max_tokens:2000}
|
||||||
: {model:A.mdl, messages:[{role:'system',content:sysPrompt()},...A.msgs], stream:true, temperature:0.8, max_tokens:2000};
|
: {model:A.mdl, messages:allMsgs, stream:true, temperature:0.8, max_tokens:2000};
|
||||||
const res=await fetch(url,{
|
const res=await fetch(url,{
|
||||||
method:'POST',
|
method:'POST',
|
||||||
headers:headers,
|
headers:headers,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue