2026-05-10 13:12:44 +08:00

479 lines
18 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">
<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>