zhizhi/persona-studio/frontend/index.html

271 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Persona Studio · 光湖人格体协助开发体验</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container login-container">
<div class="logo-area">
<h1>🌊 Persona Studio</h1>
<p class="subtitle">光湖人格体协助开发体验</p>
</div>
<div class="login-box">
<!-- ── 开发编号登录 ── -->
<h2>请输入你的开发编号</h2>
<p class="hint">编号由冰朔主控分配格式EXP-000 ~ EXP-011</p>
<form id="loginForm" onsubmit="return handleLogin(event)">
<input
type="text"
id="devIdInput"
placeholder="EXP-XXX"
pattern="^EXP-\d{3,}$"
required
autocomplete="off"
/>
<button type="submit" id="loginBtn">进入对话</button>
</form>
<div class="guest-divider">
<span></span>
</div>
<!-- ── API Key 登录 ── -->
<div class="apikey-section">
<h2>API Key 登录</h2>
<p class="hint">输入你的第三方 API 信息,自动检测可用模型</p>
<input
type="text"
id="apiBaseInput"
class="apikey-input"
placeholder="API Base URL可留空自动探测如 https://api.openai.com/v1"
autocomplete="off"
/>
<input
type="password"
id="apiKeyInput"
class="apikey-input"
placeholder="请输入你的 API Key"
autocomplete="off"
/>
<button type="button" id="detectBtn" class="btn-detect" onclick="handleDetectModels()">
🔍 检测可用模型
</button>
<div id="detectStatus" class="detect-status" style="display:none;"></div>
<div id="modelListContainer" class="model-list-container" style="display:none;">
<p class="model-list-title">请选择一个模型进入对话</p>
<div id="modelList" class="model-list"></div>
</div>
</div>
<div id="errorMsg" class="error-msg" style="display:none;"></div>
</div>
<footer class="login-footer">
<p>HoloLake Era · AGE OS · 人格体协助体验平台</p>
</footer>
</div>
<script>
const DEV_ID_RE = /^EXP-\d{3,}$/;
const PROBE_TIMEOUT_MS = 8000;
const API_BASE = getApiBase();
/* ---- 已知 API 端点列表(浏览器直连探测) ---- */
const KNOWN_ENDPOINTS = [
{ label: '云雾 AI', base: 'https://api.yunwu.ai/v1' },
{ label: 'OpenAI', base: 'https://api.openai.com/v1' },
{ label: 'Google Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai' },
{ label: 'DeepSeek', base: 'https://api.deepseek.com/v1' },
{ label: 'Moonshot', base: 'https://api.moonshot.cn/v1' },
{ label: '智谱 AI', base: 'https://open.bigmodel.cn/api/paas/v4' },
];
function getApiBase() {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
return 'http://localhost:3721';
}
return 'https://guanghulab.com';
}
/**
* 规范化 API Base URL
* 确保末尾无斜杠,并包含 /v1 路径
*/
function normalizeApiBase(base) {
var normalized = base.replace(/\/+$/, '');
if (!normalized.match(/\/v\d+(\/|$)/) && !normalized.endsWith('/openai')) {
normalized += '/v1';
}
return normalized;
}
/* ---- 开发编号登录 ---- */
async function handleLogin(e) {
e.preventDefault();
const devId = document.getElementById('devIdInput').value.trim().toUpperCase();
const errorEl = document.getElementById('errorMsg');
const btn = document.getElementById('loginBtn');
errorEl.style.display = 'none';
if (!DEV_ID_RE.test(devId)) {
errorEl.textContent = '编号格式不正确,请输入 EXP-XXX 格式';
errorEl.style.display = 'block';
return false;
}
btn.disabled = true;
btn.textContent = '验证中…';
try {
const res = await fetch(API_BASE + '/api/ps/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dev_id: devId })
});
const data = await res.json();
if (!res.ok || data.error) {
errorEl.textContent = data.message || '登录失败,请检查编号';
errorEl.style.display = 'block';
btn.disabled = false;
btn.textContent = '进入对话';
return false;
}
sessionStorage.setItem('dev_id', devId);
sessionStorage.setItem('session_token', data.token || '');
sessionStorage.removeItem('login_mode');
sessionStorage.removeItem('user_api_base');
sessionStorage.removeItem('user_api_key');
sessionStorage.removeItem('selected_model');
window.location.href = 'chat.html';
} catch (_err) {
errorEl.textContent = '服务暂时不可用,请稍后再试';
errorEl.style.display = 'block';
btn.disabled = false;
btn.textContent = '进入对话';
}
return false;
}
/* ---- API Key 模型检测(浏览器直连,无需后端代理) ---- */
async function handleDetectModels() {
var apiBase = document.getElementById('apiBaseInput').value.trim();
var apiKey = document.getElementById('apiKeyInput').value.trim();
var errorEl = document.getElementById('errorMsg');
var statusEl = document.getElementById('detectStatus');
var modelContainer = document.getElementById('modelListContainer');
var btn = document.getElementById('detectBtn');
errorEl.style.display = 'none';
modelContainer.style.display = 'none';
if (!apiKey) {
errorEl.textContent = '请输入 API Key';
errorEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = '⏳ 正在检测可用模型…';
statusEl.className = 'detect-status detect-loading';
statusEl.style.display = 'block';
/* 构建探测列表:用户端点优先,然后已知端点 */
var candidates = [];
if (apiBase) {
var normalizedUserBase = normalizeApiBase(apiBase);
candidates.push({ label: apiBase, base: normalizedUserBase });
}
for (var i = 0; i < KNOWN_ENDPOINTS.length; i++) {
var ep = KNOWN_ENDPOINTS[i];
if (!apiBase || ep.base !== normalizedUserBase) {
candidates.push(ep);
}
}
var matched = false;
for (var ci = 0; ci < candidates.length; ci++) {
var candidate = candidates[ci];
statusEl.textContent = '🔍 探测 ' + candidate.label + '…';
try {
var ctrl = new AbortController();
var tid = setTimeout(function () { ctrl.abort(); }, PROBE_TIMEOUT_MS);
var res = await fetch(candidate.base + '/models', {
headers: { 'Authorization': 'Bearer ' + apiKey },
signal: ctrl.signal
});
clearTimeout(tid);
if (res.ok) {
var data = await res.json();
var models = (data.data || []).map(function (m) { return m.id; }).filter(Boolean).sort();
if (models.length === 0) continue;
/* 回填端点地址 */
document.getElementById('apiBaseInput').value = candidate.base;
statusEl.textContent = '✅ 检测成功(' + candidate.label + ')· 发现 ' + models.length + ' 个可用模型';
statusEl.className = 'detect-status detect-success';
renderModelList(models, candidate.base, apiKey);
modelContainer.style.display = 'block';
matched = true;
break;
}
} catch (probeErr) {
/* 单个端点探测失败,继续下一个 */
}
}
if (!matched) {
statusEl.textContent = '❌ 未能检测到可用模型,请检查端点地址和密钥是否正确';
statusEl.className = 'detect-status detect-error';
}
btn.disabled = false;
btn.textContent = '🔍 检测可用模型';
}
/* ---- 渲染模型列表 ---- */
function renderModelList(models, apiBase, apiKey) {
var listEl = document.getElementById('modelList');
listEl.innerHTML = '';
models.forEach(function (modelId) {
var item = document.createElement('button');
item.className = 'model-item';
item.textContent = modelId;
item.onclick = function () { enterApiKeyChat(apiBase, apiKey, modelId); };
listEl.appendChild(item);
});
}
/* ---- 选择模型 → 进入对话 ---- */
function enterApiKeyChat(apiBase, apiKey, selectedModel) {
sessionStorage.setItem('login_mode', 'apikey');
sessionStorage.setItem('user_api_base', apiBase);
sessionStorage.setItem('user_api_key', apiKey);
sessionStorage.setItem('selected_model', selectedModel);
sessionStorage.setItem('dev_id', 'APIKEY-USER');
sessionStorage.removeItem('session_token');
window.location.href = 'chat.html';
}
</script>
</body>
</html>