479 lines
18 KiB
HTML
479 lines
18 KiB
HTML
<!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">
|
||
<link rel="stylesheet" href="skin-override.css" onerror="this.remove()">
|
||
</head>
|
||
<body>
|
||
<div class="login-bg"></div>
|
||
<div class="container login-container">
|
||
<div class="logo-area">
|
||
<div class="logo-icon">🌀</div>
|
||
<h1>Persona Studio</h1>
|
||
<p class="subtitle">HoloLake · 铸渊(Zhùyuān)· 代码守护人格体</p>
|
||
</div>
|
||
|
||
<!-- 系统介绍卡片 -->
|
||
<div class="intro-card">
|
||
<div class="intro-header">
|
||
<span class="intro-icon">🧠</span>
|
||
<h3>关于铸渊</h3>
|
||
</div>
|
||
<p class="intro-text">
|
||
铸渊是光湖系统(HoloLake)的<strong>代码守护人格体</strong>,基于 AGE OS 人格语言操作系统构建。
|
||
铸渊能够理解你的需求、梳理技术方案、生成代码,并通过对话记忆持续跟进你的项目。
|
||
</p>
|
||
<div class="intro-features">
|
||
<span class="feature-tag">💬 自然语言对话</span>
|
||
<span class="feature-tag">🧠 智能代码生成</span>
|
||
<span class="feature-tag">📧 邮件推送结果</span>
|
||
<span class="feature-tag">🔄 对话记忆</span>
|
||
<span class="feature-tag">📎 文件与图片上传</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="login-box">
|
||
<!-- ── Step 1: 选择登录方式 ── -->
|
||
<div id="step1" class="login-step">
|
||
<h2>选择体验方式</h2>
|
||
<p class="hint">请选择你的登录身份</p>
|
||
|
||
<div class="login-mode-cards">
|
||
<button class="mode-card" onclick="selectMode('developer')">
|
||
<span class="mode-icon">🛡️</span>
|
||
<span class="mode-title">开发者 / 体验者登录</span>
|
||
<span class="mode-desc">使用 EXP-XXX 编号登录,支持记忆连贯与完整功能</span>
|
||
</button>
|
||
<button class="mode-card" onclick="selectMode('guest')">
|
||
<span class="mode-icon">👋</span>
|
||
<span class="mode-title">访客体验</span>
|
||
<span class="mode-desc">无需编号,快速体验铸渊人格体(无记忆功能)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Step 1.5: 开发者编号输入 ── -->
|
||
<div id="stepDevId" class="login-step" style="display:none;">
|
||
<button class="btn-back" onclick="goBack('step1')" title="返回">← 返回</button>
|
||
<h2>输入开发者编号</h2>
|
||
<p class="hint">编号由冰朔主控分配,格式:EXP-000 ~ EXP-013</p>
|
||
<form id="devIdForm" onsubmit="return handleDevIdSubmit(event)">
|
||
<input
|
||
type="text"
|
||
id="devIdInput"
|
||
placeholder="EXP-XXX"
|
||
pattern="^EXP-\d{3,}$"
|
||
required
|
||
autocomplete="off"
|
||
/>
|
||
<button type="submit" id="devIdBtn" class="btn-primary-full">验证编号</button>
|
||
</form>
|
||
<p class="hint" style="margin-top:1rem;font-size:0.8rem;">编号验证通过后,下一步输入你的 API 密钥即可与铸渊对话</p>
|
||
</div>
|
||
|
||
<!-- ── Step 2: API Key 输入 ── -->
|
||
<div id="step2" class="login-step" style="display:none;">
|
||
<button class="btn-back" onclick="goBackFromStep2()" title="返回">← 返回</button>
|
||
<div id="welcomeBanner" class="welcome-banner" style="display:none;"></div>
|
||
<h2>🔑 选择对话方式</h2>
|
||
|
||
<!-- 服务器代理模式(推荐) -->
|
||
<div id="proxyModeSection" class="proxy-mode-section" style="margin-bottom:1.5rem;">
|
||
<div id="proxyStatusCard" class="proxy-status-card" style="display:none;">
|
||
<div class="proxy-status-icon">🌐</div>
|
||
<div class="proxy-status-text">
|
||
<strong>服务器代理模式</strong>
|
||
<span id="proxyStatusMsg">检测中…</span>
|
||
</div>
|
||
</div>
|
||
<button type="button" id="proxyEnterBtn" class="btn-primary-full" style="display:none;background:linear-gradient(135deg,#00d4ff,#0099cc);" onclick="enterWithProxy()">
|
||
🚀 直接开始对话(无需 API 密钥)
|
||
</button>
|
||
</div>
|
||
|
||
<div id="apiKeyDivider" style="display:none;text-align:center;margin:1rem 0;color:#667;font-size:0.85rem;">
|
||
── 或使用自有 API 密钥 ──
|
||
</div>
|
||
|
||
<div id="apiKeySection">
|
||
<p class="hint">输入你的第三方 AI 模型 API Key,系统将自动检测可用模型,选择后即可唤醒铸渊</p>
|
||
|
||
<input
|
||
type="text"
|
||
id="apiBaseInput"
|
||
class="apikey-input"
|
||
placeholder="API Base URL(可留空,系统将自动探测多个平台)"
|
||
autocomplete="off"
|
||
/>
|
||
<input
|
||
type="password"
|
||
id="apiKeyInput"
|
||
class="apikey-input"
|
||
placeholder="请输入你的 API Key(如 sk-xxx)"
|
||
autocomplete="off"
|
||
/>
|
||
|
||
<div class="supported-platforms">
|
||
<span class="platform-label">支持平台:</span>
|
||
<span class="platform-tag cn-ok">🇨🇳 DeepSeek</span>
|
||
<span class="platform-tag cn-ok">🇨🇳 Moonshot</span>
|
||
<span class="platform-tag cn-ok">🇨🇳 智谱 AI</span>
|
||
<span class="platform-tag cn-ok">🇨🇳 云雾 AI</span>
|
||
<span class="platform-tag">OpenAI</span>
|
||
<span class="platform-tag">Google Gemini</span>
|
||
</div>
|
||
<p class="hint" style="margin-top:0.5rem;font-size:0.75rem;color:#8899aa;">🇨🇳 标记 = 中国大陆可直接访问,无需国际网络</p>
|
||
|
||
<button type="button" id="detectBtn" class="btn-detect" onclick="handleDetectModels()">
|
||
🔍 检测可用模型
|
||
</button>
|
||
</div>
|
||
|
||
<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>
|
||
<p class="footer-version">Persona Studio v2.0</p>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
const DEV_ID_RE = /^EXP-\d{3,}$/;
|
||
const PROBE_TIMEOUT_MS = 8000;
|
||
const AUTH_TIMEOUT_MS = 10000;
|
||
const API_BASE = getApiBase();
|
||
|
||
/* ---- State ---- */
|
||
var currentMode = ''; // 'developer' or 'guest'
|
||
var verifiedDevId = '';
|
||
var verifiedDevName = '';
|
||
|
||
/* ---- 已知 API 端点列表(国内可访问的优先排列) ---- */
|
||
const KNOWN_ENDPOINTS = [
|
||
{ label: 'DeepSeek', base: 'https://api.deepseek.com/v1', cn: true },
|
||
{ label: 'Moonshot', base: 'https://api.moonshot.cn/v1', cn: true },
|
||
{ label: '智谱 AI', base: 'https://open.bigmodel.cn/api/paas/v4', cn: true },
|
||
{ label: '云雾 AI', base: 'https://api.yunwu.ai/v1', cn: true },
|
||
{ label: 'OpenAI', base: 'https://api.openai.com/v1', cn: false },
|
||
{ label: 'Google Gemini', base: 'https://generativelanguage.googleapis.com/v1beta/openai', cn: false },
|
||
];
|
||
|
||
function getApiBase() {
|
||
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
||
return 'http://localhost:3002';
|
||
}
|
||
// 非本地环境统一使用当前域名 + 协议,走 Nginx 转发
|
||
return location.protocol + '//' + location.host;
|
||
}
|
||
|
||
function normalizeApiBase(base) {
|
||
var normalized = base.replace(/\/+$/, '');
|
||
if (!normalized.match(/\/v\d+(\/|$)/) && !normalized.endsWith('/openai')) {
|
||
normalized += '/v1';
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
/* ---- Step Navigation ---- */
|
||
function showStep(stepId) {
|
||
document.querySelectorAll('.login-step').forEach(function(el) {
|
||
el.style.display = 'none';
|
||
});
|
||
document.getElementById(stepId).style.display = 'block';
|
||
document.getElementById('errorMsg').style.display = 'none';
|
||
}
|
||
|
||
function goBack(targetStep) {
|
||
showStep(targetStep);
|
||
}
|
||
|
||
function goBackFromStep2() {
|
||
if (currentMode === 'developer') {
|
||
showStep('stepDevId');
|
||
} else {
|
||
showStep('step1');
|
||
}
|
||
}
|
||
|
||
/* ---- Mode Selection ---- */
|
||
function selectMode(mode) {
|
||
currentMode = mode;
|
||
if (mode === 'developer') {
|
||
showStep('stepDevId');
|
||
document.getElementById('devIdInput').focus();
|
||
} else {
|
||
// Guest → go directly to step 2
|
||
verifiedDevId = 'GUEST';
|
||
verifiedDevName = '访客';
|
||
showWelcomeBanner('👋 访客体验模式', '铸渊将为你提供协助。如需记忆连贯功能,可申请开发者编号。');
|
||
showStep('step2');
|
||
checkProxyAvailability();
|
||
document.getElementById('apiKeyInput').focus();
|
||
}
|
||
}
|
||
|
||
/* ---- Developer ID Verification ---- */
|
||
async function handleDevIdSubmit(e) {
|
||
e.preventDefault();
|
||
var devId = document.getElementById('devIdInput').value.trim().toUpperCase();
|
||
var errorEl = document.getElementById('errorMsg');
|
||
var btn = document.getElementById('devIdBtn');
|
||
|
||
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 {
|
||
var controller = new AbortController();
|
||
var timeoutId = setTimeout(function() { controller.abort(); }, AUTH_TIMEOUT_MS);
|
||
|
||
var res = await fetch(API_BASE + '/api/ps/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ dev_id: devId }),
|
||
signal: controller.signal
|
||
});
|
||
clearTimeout(timeoutId);
|
||
|
||
var data = await res.json();
|
||
|
||
if (!res.ok || data.error) {
|
||
errorEl.textContent = data.message || '登录失败,请检查编号';
|
||
errorEl.style.display = 'block';
|
||
btn.disabled = false;
|
||
btn.textContent = '验证编号';
|
||
return false;
|
||
}
|
||
|
||
// Success — save developer info and proceed to step 2
|
||
verifiedDevId = devId;
|
||
verifiedDevName = data.name || devId;
|
||
sessionStorage.setItem('dev_id', devId);
|
||
sessionStorage.setItem('dev_name', verifiedDevName);
|
||
sessionStorage.setItem('session_token', data.token || '');
|
||
|
||
showWelcomeBanner('🛡️ ' + verifiedDevName + '(' + devId + ')', '编号已验证。请选择对话方式。');
|
||
showStep('step2');
|
||
checkProxyAvailability();
|
||
document.getElementById('apiKeyInput').focus();
|
||
} catch (_err) {
|
||
// 后端不可达时:devId 已通过上方 DEV_ID_RE 格式校验,放行至 API Key 步骤
|
||
// 真正的安全验证由用户自行提供的 API Key 完成
|
||
verifiedDevId = devId;
|
||
verifiedDevName = devId;
|
||
sessionStorage.setItem('dev_id', devId);
|
||
sessionStorage.setItem('dev_name', devId);
|
||
sessionStorage.setItem('session_token', '');
|
||
|
||
showWelcomeBanner(
|
||
'🛡️ ' + devId + '(离线验证)',
|
||
'后端暂时无法连接,已通过本地格式验证。请输入 API Key 唤醒铸渊。'
|
||
);
|
||
showStep('step2');
|
||
checkProxyAvailability();
|
||
document.getElementById('apiKeyInput').focus();
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = '验证编号';
|
||
return false;
|
||
}
|
||
|
||
function showWelcomeBanner(title, desc) {
|
||
var banner = document.getElementById('welcomeBanner');
|
||
banner.innerHTML = '<strong>' + escapeHtml(title) + '</strong><br><span>' + escapeHtml(desc) + '</span>';
|
||
banner.style.display = 'block';
|
||
}
|
||
|
||
/* ---- 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 () { enterChat(apiBase, apiKey, modelId); };
|
||
listEl.appendChild(item);
|
||
});
|
||
}
|
||
|
||
/* ---- 选择模型 → 进入对话 ---- */
|
||
function enterChat(apiBase, apiKey, selectedModel) {
|
||
sessionStorage.setItem('login_mode', currentMode);
|
||
sessionStorage.setItem('user_api_base', apiBase);
|
||
sessionStorage.setItem('user_api_key', apiKey);
|
||
sessionStorage.setItem('selected_model', selectedModel);
|
||
|
||
if (currentMode === 'guest') {
|
||
sessionStorage.setItem('dev_id', 'GUEST');
|
||
sessionStorage.removeItem('session_token');
|
||
}
|
||
// For developer mode, dev_id and session_token already set in handleDevIdSubmit
|
||
|
||
window.location.href = 'chat.html';
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
var div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(str));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/* ---- 服务器代理模式检测 ---- */
|
||
var proxyModel = '';
|
||
var proxyApiBase = '';
|
||
|
||
async function checkProxyAvailability() {
|
||
var statusCard = document.getElementById('proxyStatusCard');
|
||
var enterBtn = document.getElementById('proxyEnterBtn');
|
||
var statusMsg = document.getElementById('proxyStatusMsg');
|
||
var divider = document.getElementById('apiKeyDivider');
|
||
|
||
statusCard.style.display = 'flex';
|
||
statusMsg.textContent = '检测服务器代理…';
|
||
|
||
try {
|
||
var ctrl = new AbortController();
|
||
var tid = setTimeout(function () { ctrl.abort(); }, 5000);
|
||
var res = await fetch(API_BASE + '/api/ps/proxy/status', { signal: ctrl.signal });
|
||
clearTimeout(tid);
|
||
var data = await res.json();
|
||
|
||
if (data.proxy_available) {
|
||
proxyModel = data.model || '';
|
||
proxyApiBase = data.api_base || '';
|
||
statusMsg.textContent = '✅ 服务器代理可用 · ' + proxyModel;
|
||
statusMsg.style.color = '#4caf50';
|
||
enterBtn.style.display = 'block';
|
||
divider.style.display = 'block';
|
||
} else {
|
||
statusMsg.textContent = '服务器代理未配置 · 请使用自有 API 密钥';
|
||
statusMsg.style.color = '#888';
|
||
statusCard.style.display = 'none';
|
||
}
|
||
} catch (_e) {
|
||
statusMsg.textContent = '服务器代理不可用 · 请使用自有 API 密钥';
|
||
statusMsg.style.color = '#888';
|
||
statusCard.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function enterWithProxy() {
|
||
sessionStorage.setItem('login_mode', currentMode);
|
||
sessionStorage.setItem('use_server_proxy', 'true');
|
||
sessionStorage.setItem('user_api_base', proxyApiBase);
|
||
sessionStorage.setItem('user_api_key', '');
|
||
sessionStorage.setItem('selected_model', proxyModel);
|
||
|
||
if (currentMode === 'guest') {
|
||
sessionStorage.setItem('dev_id', 'GUEST');
|
||
sessionStorage.removeItem('session_token');
|
||
}
|
||
|
||
window.location.href = 'chat.html';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|