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:
copilot-swe-agent[bot] 2026-03-07 07:34:59 +00:00
parent 723f6b16bc
commit 466f344ef8
3 changed files with 23 additions and 18 deletions

View File

@ -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);

View File

@ -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;
} }

View File

@ -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,