🔍 升级 API 自动检测:一键检测可用模型(无需密钥)+ 网络环境自适应

- 新增 autoDetectModels() 函数:无密钥一键检测可用模型
- 新增 detectNetworkRegion():根据时区/语言检测国内/国际网络
- 新增 MODEL_CATALOG:内置模型目录(国内5家+国际3家,22个模型)
- 新增 SERVER_PROXY_BASE:staging/local 自动指向生产服务器代理
- 修改 autoDetectAPI():无密钥时走一键检测,有密钥走原探测逻辑
- 修改 doSetup():支持服务器代理模式(无密钥直接进入对话)
- 更新登录页 UI:一键检测按钮突出,自定义 API 折叠在 details 中
- 更新设置面板 UI:同步升级一键检测 + 自定义 API 折叠
- 修复侧边栏导航链接路径(../→相对路径)
- Patch GUEST_API/TEAM_API proxyUrl for staging → 生产服务器

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/2b0b1a22-c312-4b46-ae3b-72e830c4470b
This commit is contained in:
copilot-swe-agent[bot] 2026-03-25 22:58:33 +00:00
parent 2cdf0af54d
commit 136c32ac02
1 changed files with 238 additions and 55 deletions

View File

@ -424,7 +424,6 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
<!-- 自定义 API高级·使用自己的 API 密钥) -->
<div id="login-dev" class="login-mode">
<div class="sc-sec">🔒 API 密钥仅本次会话有效,关闭标签页即自动清除</div>
<div class="fg">
<label>开发者身份</label>
@ -445,27 +444,40 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
</div>
<div id="id-preview"></div>
<div class="fg">
<label>API 端点 URL</label>
<input type="text" id="sep" placeholder="https://api.example.com/v1">
<small>输入你的 API 提供商端点地址OpenAI 兼容格式)</small>
</div>
<div class="fg">
<label>API 密钥</label>
<div style="display:flex;gap:6px;align-items:stretch">
<input type="password" id="sk" placeholder="粘贴你的 API 密钥" style="flex:1">
<button onclick="autoDetectAPI('s')" id="sdet-btn" class="det-btn" title="自动检测可用模型">🔍 检测模型</button>
</div>
<small id="sk-err" style="color:var(--err);display:none">⚠️ 请输入 API 密钥</small>
<small id="sdet-status" style="display:block;margin-top:4px;min-height:16px"></small>
</div>
<div class="fg">
<label>选择模型</label>
<select id="sm" disabled><option value="">— 请先检测可用模型 —</option></select>
<small id="sm-hint">输入端点和密钥后,点击「🔍 检测模型」自动获取可用模型列表</small>
<!-- 🚀 一键检测 -->
<div style="background:rgba(79,142,247,0.08);border:1px solid rgba(79,142,247,0.25);border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:13px;color:var(--text);margin-bottom:8px">🚀 <strong>一键检测可用模型</strong></div>
<div style="font-size:12px;color:var(--dim);margin-bottom:10px;line-height:1.6">系统自动检测你的网络环境(国内/国际),连接服务器代理获取可用模型。无需输入 API 密钥。</div>
<button onclick="autoDetectAPI('s')" id="sdet-btn" class="det-btn" style="width:100%;padding:10px;font-size:14px;border-radius:8px;background:var(--accent);color:#fff;border:none;cursor:pointer" title="自动检测网络环境和可用模型">🔍 自动检测</button>
<small id="sdet-status" style="display:block;margin-top:8px;min-height:16px"></small>
</div>
<button class="sc-btn" onclick="doSetup()">开始与铸渊对话 →</button>
<div class="fg">
<label>选择模型</label>
<select id="sm" disabled onchange="onModelSelectChange('s')"><option value="">— 请先点击「🔍 自动检测」—</option></select>
<small id="sm-hint">点击上方「🔍 自动检测」一键获取可用模型列表</small>
</div>
<!-- 自定义 API 折叠区 -->
<details style="margin-top:8px">
<summary style="cursor:pointer;font-size:12px;color:var(--dim);padding:6px 0">🔧 自定义 API使用自己的密钥</summary>
<div style="padding-top:8px">
<div class="fg">
<label>API 端点 URL</label>
<input type="text" id="sep" placeholder="https://api.example.com/v1">
<small>输入你的 API 提供商端点地址OpenAI 兼容格式)</small>
</div>
<div class="fg">
<label>API 密钥</label>
<input type="password" id="sk" placeholder="粘贴你的 API 密钥" style="width:100%">
<small id="sk-err" style="color:var(--err);display:none">⚠️ 请输入 API 密钥</small>
<small style="color:var(--dim)">🔒 密钥仅本次会话有效,关闭标签页即自动清除</small>
</div>
<button onclick="autoDetectAPI('s')" class="det-btn" style="width:100%;margin-top:6px;padding:8px;border-radius:6px;cursor:pointer" title="使用自定义密钥检测模型">🔍 用自定义密钥检测模型</button>
</div>
</details>
<button class="sc-btn" onclick="doSetup()" style="margin-top:12px">开始与铸渊对话 →</button>
</div>
</div>
@ -490,9 +502,9 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
<nav class="ls-nav">
<button class="nav-tab active" data-view="announcements" onclick="navigateTo('announcements')">📢 公告栏</button>
<button class="nav-tab" data-view="chat" onclick="navigateTo('chat')">💬 聊天</button>
<button class="nav-tab" onclick="window.open('../persona-studio/frontend/index.html','_blank')">🧠 Studio</button>
<button class="nav-tab" onclick="window.open('../status-board/','_blank')">📊 看板</button>
<button class="nav-tab" onclick="window.open('../ticket-system/','_blank')">🎫 工单</button>
<button class="nav-tab" onclick="window.open('persona-studio/','_blank')">🧠 Studio</button>
<button class="nav-tab" onclick="window.open('status-board/','_blank')">📊 看板</button>
<button class="nav-tab" onclick="window.open('ticket-system/','_blank')">🎫 工单</button>
</nav>
<button class="ls-new-conv" onclick="newConvAndNav()">+ 新对话</button>
@ -616,23 +628,36 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
</div>
</div>
<div class="pb">
<div class="sfg">
<label>API 端点 URL</label>
<input type="text" id="cep" placeholder="https://api.example.com/v1">
</div>
<div class="sfg">
<label>选择模型</label>
<select id="cm"></select>
</div>
<div class="sfg">
<label>API 密钥</label>
<div style="display:flex;gap:6px;align-items:stretch">
<input type="password" id="ck" placeholder="留空 = 保持当前密钥不变;粘贴新密钥即替换" style="flex:1">
<button onclick="autoDetectAPI('c')" id="cdet-btn" class="det-btn" title="自动检测提供商和可用模型">🔍 自动检测</button>
<!-- 🚀 一键检测可用模型 -->
<div style="background:rgba(79,142,247,0.08);border:1px solid rgba(79,142,247,0.25);border-radius:10px;padding:12px;margin-bottom:12px">
<div style="display:flex;gap:8px;align-items:center">
<button onclick="autoDetectAPI('c')" id="cdet-btn" class="det-btn" style="flex:1;padding:10px;font-size:13px;border-radius:8px;background:var(--accent);color:#fff;border:none;cursor:pointer" title="自动检测网络环境和可用模型">🔍 一键检测可用模型</button>
</div>
<small id="ck-hint" style="color:var(--dim)">🔒 密钥仅本次会话有效,关闭标签页自动清除</small>
<small style="display:block;margin-top:6px;color:var(--dim);font-size:11px">自动检测网络环境,连接服务器代理获取可用模型(无需密钥)</small>
<small id="cdet-status" style="display:block;margin-top:4px;min-height:14px"></small>
</div>
<div class="sfg">
<label>选择模型</label>
<select id="cm" onchange="onModelSelectChange('c')"></select>
</div>
<!-- 自定义 API 折叠区 -->
<details style="margin-bottom:10px">
<summary style="cursor:pointer;font-size:12px;color:var(--dim);padding:6px 0">🔧 自定义 API使用自己的密钥</summary>
<div style="padding-top:8px">
<div class="sfg">
<label>API 端点 URL</label>
<input type="text" id="cep" placeholder="https://api.example.com/v1">
</div>
<div class="sfg">
<label>API 密钥</label>
<input type="password" id="ck" placeholder="留空 = 保持当前密钥不变;粘贴新密钥即替换" style="width:100%">
<small id="ck-hint" style="color:var(--dim)">🔒 密钥仅本次会话有效,关闭标签页自动清除</small>
</div>
</div>
</details>
<button class="sbtn2" onclick="saveSet()">💾 保存设置</button>
<div class="sec-ttl">身份设置</div>
@ -665,11 +690,11 @@ footer{padding:12px 18px 16px;padding-bottom:max(16px,env(safe-area-inset-bottom
<div class="sec-ttl">使用说明</div>
<p style="font-size:12px;color:var(--dim);line-height:1.8">
1. 输入你的 API 端点和密钥,点击检测模型<br>
2. 铸渊自动切换 💬 对话 / 🔨 构建 / 📋 审查 / 🧠 大脑 模式<br>
3. 支持上传图片和文件(点击 📎 按钮或拖拽到对话区域)<br>
4. 点 ⬇️ 下载单文件,放桌面直接打开使用<br>
5. 支持任意 OpenAI 兼容接口(输入端点 + 密钥即可
1. 点击「🔍 一键检测可用模型」自动获取模型列表(无需密钥)<br>
2. 如服务器代理可用,选择模型后直接对话<br>
3. 也可展开「🔧 自定义 API」输入自己的端点和密钥<br>
4. 铸渊自动切换 💬 对话 / 🔨 构建 / 📋 审查 / 🧠 大脑 模式<br>
5. 支持上传图片和文件(点击 📎 按钮或拖拽到对话区域
</p>
<div class="sec-ttl" style="color:#ef4444;margin-top:16px">⚠️ 危险操作</div>
@ -822,6 +847,25 @@ const KNOWN_ENDPOINTS = [
{label:'智谱 AI', base:'https://open.bigmodel.cn/api/paas/v4'},
];
// ═══════════════════════════════════════════════════════
// 内置模型目录(按网络区域分类)
// 一键检测时,根据用户网络环境自动推荐可用模型
// ═══════════════════════════════════════════════════════
const MODEL_CATALOG = {
domestic: [
{provider:'yunwu', label:'云雾 AI推荐·聚合', models:['deepseek-chat','deepseek-reasoner','gpt-4o','claude-sonnet-4-20250514','gemini-2.5-pro-preview-05-06'], endpoint:'https://api.yunwu.ai/v1'},
{provider:'deepseek', label:'DeepSeek国内直连', models:['deepseek-chat','deepseek-reasoner'], endpoint:'https://api.deepseek.com/v1'},
{provider:'qwen', label:'通义千问 Qwen', models:['qwen-max','qwen-plus','qwen-turbo'], endpoint:'https://dashscope.aliyuncs.com/compatible-mode/v1'},
{provider:'moonshot', label:'Moonshot / Kimi', models:['moonshot-v1-8k','moonshot-v1-32k','moonshot-v1-128k'], endpoint:'https://api.moonshot.cn/v1'},
{provider:'zhipu', label:'智谱 AI', models:['glm-4','glm-4-flash'], endpoint:'https://open.bigmodel.cn/api/paas/v4'},
],
international: [
{provider:'openai', label:'OpenAI', models:['gpt-4o','gpt-4-turbo','gpt-4o-mini'], endpoint:'https://api.openai.com/v1'},
{provider:'anthropic', label:'Anthropic', models:['claude-sonnet-4-20250514','claude-3-5-sonnet-20241022'], endpoint:'https://api.anthropic.com/v1'},
{provider:'google', label:'Google Gemini', models:['gemini-2.5-pro','gemini-2.0-flash'], endpoint:'https://generativelanguage.googleapis.com/v1beta/openai'},
]
};
const MODES = {
chat: {e:'💬',l:'对话模式', c:'var(--mc)'},
build: {e:'🔨',l:'构建模式', c:'var(--mb)'},
@ -938,6 +982,29 @@ const HOLOLAKE_ENV = (function(){
})();
window.HOLOLAKE_ENV = HOLOLAKE_ENV;
// ═══════════════════════════════════════════════════════
// 服务器代理基址staging/local → 调用生产服务器已配置的 API 密钥)
// ═══════════════════════════════════════════════════════
const SERVER_PROXY_BASE = HOLOLAKE_ENV === 'production' ? '' : 'https://guanghulab.com';
// Patch proxy URLs for staging/local: point to production server
if(HOLOLAKE_ENV !== 'production'){
GUEST_API.proxyUrl = SERVER_PROXY_BASE + '/api/chat';
TEAM_API.proxyUrl = SERVER_PROXY_BASE + '/api/chat';
}
// 检测用户网络环境(国内/国际)
function detectNetworkRegion(){
var tz = '';
try{ tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; }catch(e){}
var lang = (navigator.language || navigator.userLanguage || '').toLowerCase();
var chinaZones = ['Asia/Shanghai','Asia/Chongqing','Asia/Harbin','Asia/Urumqi','Asia/Hong_Kong','Asia/Macau','Asia/Taipei'];
if(chinaZones.some(function(z){ return tz.indexOf(z) >= 0; }) || lang.indexOf('zh') === 0){
return 'domestic';
}
return 'international';
}
function ls(k){return localStorage.getItem(k)}
function lss(k,v){localStorage.setItem(k,v)}
@ -1414,20 +1481,23 @@ function switchLoginMode(mode){
// ═══════════════════════════════════════════════════════
// AUTO-DETECT PROVIDER & MODELS
// v4.0: 无密钥一键检测 + 网络环境自适应 + 服务器代理
// 有密钥 → 原逻辑探测端点;无密钥 → 服务器代理 / 内置目录
// ═══════════════════════════════════════════════════════
async function autoDetectAPI(ctx){
const keyEl = document.getElementById(ctx==='s'?'sk':'ck');
const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status');
const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn');
const mdlSel = document.getElementById(ctx==='s'?'sm':'cm');
const key = keyEl.value.trim();
const key = keyEl ? keyEl.value.trim() : '';
// 无密钥 → 一键自动检测(服务器代理 / 内置目录)
if(!key){
statusEl.innerHTML='<span style="color:var(--warn)">⚠️ 请先输入 API 密钥</span>';
await autoDetectModels(ctx);
return;
}
// Build list of endpoints to probe: user's endpoint first, then known fallbacks
// 有密钥 → 探测用户端点获取模型列表
const userBase = (document.getElementById(ctx==='s'?'sep':'cep')?.value||'').trim();
const candidates = [];
if(userBase) candidates.push({label:userBase, base:userBase});
@ -1455,11 +1525,9 @@ async function autoDetectAPI(ctx){
const mdls = (data.data||[]).map(function(m){return m.id;}).filter(Boolean).sort();
if(!mdls.length) continue;
// Fill endpoint field with matched endpoint
const epEl = document.getElementById(ctx==='s'?'sep':'cep');
if(epEl) epEl.value = base;
// Fill model dropdown with detected models
if(mdlSel){
mdlSel.innerHTML = mdls.map(function(m){return '<option value="'+m+'">'+m+'</option>';}).join('');
mdlSel.disabled = false;
@ -1477,12 +1545,106 @@ async function autoDetectAPI(ctx){
}
btnEl.disabled = false;
btnEl.textContent = '🔍 检测模型';
btnEl.textContent = '🔍 自动检测';
}
// ═══════════════════════════════════════════════════════
// 一键检测可用模型(无需 API 密钥)
// 1. 检测网络环境(国内/国际)
// 2. 尝试连接服务器代理获取实时模型列表
// 3. 降级使用内置模型目录
// ═══════════════════════════════════════════════════════
async function autoDetectModels(ctx){
const statusEl = document.getElementById(ctx==='s'?'sdet-status':'cdet-status');
const btnEl = document.getElementById(ctx==='s'?'sdet-btn':'cdet-btn');
const mdlSel = document.getElementById(ctx==='s'?'sm':'cm');
const epEl = document.getElementById(ctx==='s'?'sep':'cep');
const hintEl = document.getElementById(ctx==='s'?'sm-hint':'');
btnEl.disabled = true;
btnEl.textContent = '⏳ 检测中…';
statusEl.innerHTML = '<span style="color:var(--dim)">🌐 正在检测网络环境…</span>';
const region = detectNetworkRegion();
const regionLabel = region === 'domestic' ? '🇨🇳 国内网络' : '🌍 国际网络';
statusEl.innerHTML = '<span style="color:var(--dim)">'+regionLabel+' · 正在获取可用模型…</span>';
// 尝试连接服务器代理获取实时模型列表
let serverModels = null;
const proxyBase = SERVER_PROXY_BASE || location.origin;
try{
const ctrl = new AbortController();
const tid = setTimeout(function(){ctrl.abort();}, 5000);
const res = await fetch(proxyBase+'/api/models',{signal:ctrl.signal});
clearTimeout(tid);
if(res.ok){
const data = await res.json();
const list = data.data || data.models || [];
serverModels = list.map(function(m){return typeof m==='string'?m:m.id;}).filter(Boolean).sort();
}
}catch(e){console.debug('[AutoDetect] Server probe failed:',e.message);}
let source, totalCount = 0;
if(serverModels && serverModels.length > 0){
// 服务器代理可用 → 使用服务器模型列表
source = '服务器代理';
totalCount = serverModels.length;
if(mdlSel){
mdlSel.innerHTML = serverModels.map(function(m){return '<option value="'+m+'" data-proxy="1">'+m+'</option>';}).join('');
mdlSel.disabled = false;
}
if(epEl) epEl.value = proxyBase + '/api/chat';
window._autoDetectServerProxy = true;
} else {
// 降级使用内置模型目录(按网络区域排序)
source = '内置模型库';
const primary = region === 'domestic' ? MODEL_CATALOG.domestic : MODEL_CATALOG.international;
const secondary = region === 'domestic' ? MODEL_CATALOG.international : MODEL_CATALOG.domestic;
const catalog = primary.concat(secondary);
if(mdlSel){
let html = '';
for(let i=0;i<catalog.length;i++){
const p = catalog[i];
html += '<optgroup label="'+p.label+'">';
for(let j=0;j<p.models.length;j++){
html += '<option value="'+p.models[j]+'" data-endpoint="'+p.endpoint+'" data-provider="'+p.provider+'">'+p.models[j]+'</option>';
totalCount++;
}
html += '</optgroup>';
}
mdlSel.innerHTML = html;
mdlSel.disabled = false;
// 自动选择第一个模型并设置对应端点
onModelSelectChange(ctx);
}
window._autoDetectServerProxy = false;
}
statusEl.innerHTML = '<span style="color:var(--ok)">✅ 检测成功('+source+' · '+regionLabel+')· 发现 <strong>'+totalCount+'</strong> 个可用模型</span>'
+ (serverModels ? '<br><span style="color:var(--ok);font-size:11px">🔑 已连接服务器代理,无需输入 API 密钥</span>' : '<br><span style="color:var(--warn);font-size:11px">⚠️ 服务器暂不可达,请输入对应提供商的 API 密钥后使用</span>');
if(hintEl) hintEl.textContent = serverModels ? '已连接服务器代理,选择模型后可直接开始对话' : '选择模型后,请输入对应提供商的 API 密钥';
btnEl.disabled = false;
btnEl.textContent = '🔍 自动检测';
}
// 模型选择变更 → 自动切换端点
function onModelSelectChange(ctx){
const mdlSel = document.getElementById(ctx==='s'?'sm':'cm');
const epEl = document.getElementById(ctx==='s'?'sep':'cep');
if(!mdlSel || !epEl) return;
const opt = mdlSel.options[mdlSel.selectedIndex];
if(!opt) return;
const ep = opt.getAttribute('data-endpoint');
if(ep) epEl.value = ep;
}
function doSetup(){
const un = document.getElementById('suid').value;
const k = document.getElementById('sk').value.trim();
const k = document.getElementById('sk')?.value?.trim() || '';
const base = (document.getElementById('sep')?.value||'').trim();
const md = document.getElementById('sm').value;
const errEl = document.getElementById('sk-err');
@ -1491,14 +1653,35 @@ function doSetup(){
alert('请选择你的开发者身份');
return;
}
if(!k){errEl.style.display='block';return;}
errEl.style.display='none';
if(!base){
alert('请输入 API 端点 URL');
if(!md){
alert('请先点击「🔍 自动检测」检测可用模型,然后选择一个模型');
return;
}
if(!md){
alert('请先点击「🔍 检测模型」检测可用模型,然后选择一个模型');
// 服务器代理模式(一键检测,无需密钥)
if(window._autoDetectServerProxy && !k){
A.demo = true;
A.teamMode = true;
A.key = '';
A.prov = 'custom';
A.mdl = md;
A.base = base;
lss('zy_teammode','1');
lss('zy_base',base); lss('zy_mdl',md); lss('zy_prov','proxy');
setIdentity(un, '');
showApp();
return;
}
// 自定义 API 模式(需要密钥)
if(!k){
if(errEl) errEl.style.display='block';
alert('请输入 API 密钥,或点击「🔍 自动检测」使用服务器代理');
return;
}
if(errEl) errEl.style.display='none';
if(!base){
alert('请输入 API 端点 URL');
return;
}