- electron/main.cjs: 修复托盘菜单损坏的合并代码(SyntaxError),托盘 3D 意识空间与 ipc 路由改用正确的 backendPort 变量 - 新增 activation.html 激活页(对接 /activate 接口,支持自动识别/8 个服务商) - scripts/package-win.ps1: 打包前切换 better-sqlite3 到 Electron ABI、打包后还原 Node ABI,避免 dev 后端崩溃;内置 npmmirror 工具链镜像 - scripts/gen-icons.ps1 + build/icon.ico: 生成安装包图标(electron-builder 必需) - package.json: build 脚本改用 package-win.ps1(不再依赖 VS 工具链) Co-Authored-By: Claude <noreply@anthropic.com>
266 lines
9.4 KiB
HTML
266 lines
9.4 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>激活 · Bailongma</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0b0e14;
|
||
--panel: rgba(255, 255, 255, 0.04);
|
||
--border: rgba(255, 255, 255, 0.1);
|
||
--text: #e6e9f0;
|
||
--muted: #8b93a7;
|
||
--accent: #4f8cff;
|
||
--accent-hover: #6b9eff;
|
||
--ok: #34c98e;
|
||
--err: #ff5f6d;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
min-height: 100vh;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: radial-gradient(1200px 600px at 20% 0%, #16203a 0%, var(--bg) 55%);
|
||
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
|
||
color: var(--text);
|
||
padding: 24px;
|
||
}
|
||
.card {
|
||
width: 100%; max-width: 460px;
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
padding: 32px;
|
||
backdrop-filter: blur(8px);
|
||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||
}
|
||
.logo { font-size: 22px; font-weight: 700; letter-spacing: 1px; margin-bottom: 4px; }
|
||
.logo span { color: var(--accent); }
|
||
.sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; line-height: 1.6; }
|
||
label { display: block; font-size: 13px; color: var(--muted); margin: 14px 0 6px; }
|
||
input, select {
|
||
width: 100%;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
padding: 10px 12px;
|
||
outline: none;
|
||
transition: border-color .15s;
|
||
}
|
||
input:focus, select:focus { border-color: var(--accent); }
|
||
.key-row { position: relative; }
|
||
.key-row input { padding-right: 56px; }
|
||
.toggle {
|
||
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
||
background: none; border: none; color: var(--muted); cursor: pointer;
|
||
font-size: 12px; padding: 6px;
|
||
}
|
||
.toggle:hover { color: var(--text); }
|
||
.btn {
|
||
width: 100%;
|
||
margin-top: 24px;
|
||
background: var(--accent);
|
||
border: none; border-radius: 8px;
|
||
color: #fff; font-size: 15px; font-weight: 600;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
transition: background .15s;
|
||
}
|
||
.btn:hover { background: var(--accent-hover); }
|
||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||
.msg { display: none; font-size: 13px; margin-top: 14px; line-height: 1.5; padding: 10px 12px; border-radius: 8px; }
|
||
.msg.ok { display: block; background: rgba(52, 201, 142, .1); color: var(--ok); border: 1px solid rgba(52, 201, 142, .3); }
|
||
.msg.err { display: block; background: rgba(255, 95, 109, .1); color: var(--err); border: 1px solid rgba(255, 95, 109, .3); }
|
||
.hint { font-size: 12px; color: var(--muted); margin-top: 8px; line-height: 1.6; }
|
||
.footer { margin-top: 20px; text-align: center; font-size: 12px; color: var(--muted); }
|
||
.already { display: none; text-align: center; padding: 8px 0 4px; }
|
||
.already .badge { color: var(--ok); font-size: 14px; font-weight: 600; margin-bottom: 6px; }
|
||
.already a { color: var(--accent); text-decoration: none; font-size: 13px; }
|
||
.already a:hover { text-decoration: underline; }
|
||
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -2px; margin-right: 8px; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<div class="logo">白龙马 <span>Bailongma</span></div>
|
||
<div class="sub">数字意识框架 · 首次运行需激活。填入任意支持的 LLM API Key 即可开始。</div>
|
||
|
||
<div id="formArea">
|
||
<label for="agentName">给它起个名字(可选)</label>
|
||
<input id="agentName" type="text" maxlength="32" placeholder="默认:小白龙" autocomplete="off">
|
||
|
||
<label for="provider">LLM 服务商</label>
|
||
<select id="provider"></select>
|
||
|
||
<div id="customRow" style="display:none">
|
||
<label for="baseURL">API 地址 (Base URL)</label>
|
||
<input id="baseURL" type="text" placeholder="https://your-endpoint.com/v1" autocomplete="off">
|
||
</div>
|
||
|
||
<label for="model">模型</label>
|
||
<select id="model"></select>
|
||
|
||
<label for="apiKey">API Key</label>
|
||
<div class="key-row">
|
||
<input id="apiKey" type="password" placeholder="sk-..." autocomplete="off" spellcheck="false">
|
||
<button class="toggle" id="toggleKey" type="button">显示</button>
|
||
</div>
|
||
|
||
<button class="btn" id="submitBtn">激活</button>
|
||
<div class="msg" id="msg"></div>
|
||
<div class="hint">Key 只会保存在本机 config.json 中,不会上传到任何第三方服务器。选择「自动识别」时系统会逐个探测各服务商。</div>
|
||
</div>
|
||
|
||
<div class="already" id="alreadyArea">
|
||
<div class="badge">✓ 已激活</div>
|
||
<div id="alreadyInfo"></div>
|
||
<a href="/">进入主界面 →</a>
|
||
</div>
|
||
|
||
<div class="footer">Bailongma v2.1.300</div>
|
||
</div>
|
||
|
||
<script>
|
||
(async () => {
|
||
const $ = id => document.getElementById(id)
|
||
const msg = $('msg')
|
||
const showMsg = (text, ok) => {
|
||
msg.textContent = text
|
||
msg.className = 'msg ' + (ok ? 'ok' : 'err')
|
||
}
|
||
|
||
// Load provider list + current activation status
|
||
let providers = {}
|
||
let status = null
|
||
try {
|
||
const [settings, statusRes] = await Promise.all([
|
||
fetch('/settings').then(r => r.json()),
|
||
fetch('/activation-status').then(r => r.json()),
|
||
])
|
||
providers = settings.providers || {}
|
||
status = statusRes
|
||
} catch (err) {
|
||
showMsg('无法连接后端服务,请确认 Bailongma 正在运行。', false)
|
||
return
|
||
}
|
||
|
||
const providerSelect = $('provider')
|
||
const modelSelect = $('model')
|
||
const customRow = $('customRow')
|
||
|
||
// Provider dropdown: auto-detect first, then every configured provider
|
||
const providerOptions = [['auto', '自动识别 (Auto)']]
|
||
for (const [name, cfg] of Object.entries(providers)) {
|
||
providerOptions.push([name, cfg.label || name])
|
||
}
|
||
providerSelect.innerHTML = providerOptions
|
||
.map(([value, label]) => `<option value="${value}">${label}</option>`)
|
||
.join('')
|
||
|
||
function fillModels(provider) {
|
||
const cfg = providers[provider]
|
||
modelSelect.innerHTML = ''
|
||
if (!cfg || !cfg.models || !cfg.models.length) {
|
||
const opt = document.createElement('option')
|
||
opt.value = ''
|
||
opt.textContent = '(自定义模型,激活时填写 Base URL)'
|
||
modelSelect.appendChild(opt)
|
||
return
|
||
}
|
||
for (const m of cfg.models) {
|
||
const opt = document.createElement('option')
|
||
opt.value = m.id
|
||
opt.textContent = m.label || m.id
|
||
if (m.deprecated) opt.textContent += '(已弃用)'
|
||
if (m.id === cfg.defaultModel) opt.selected = true
|
||
modelSelect.appendChild(opt)
|
||
}
|
||
customRow.style.display = 'none'
|
||
}
|
||
|
||
providerSelect.addEventListener('change', () => {
|
||
const value = providerSelect.value
|
||
if (value === 'custom') {
|
||
customRow.style.display = 'block'
|
||
modelSelect.innerHTML = ''
|
||
const opt = document.createElement('option')
|
||
opt.value = ''
|
||
opt.textContent = '(自定义模型名)'
|
||
modelSelect.appendChild(opt)
|
||
} else {
|
||
fillModels(value === 'auto' ? Object.keys(providers)[0] || null : value)
|
||
}
|
||
})
|
||
|
||
// Prefill current status if already activated
|
||
if (status && status.activated) {
|
||
$('alreadyArea').style.display = 'block'
|
||
$('alreadyInfo').textContent = status.provider + ' · ' + (status.model || '')
|
||
}
|
||
const prefill = status && status.provider && status.provider !== 'auto'
|
||
? status.provider : 'auto'
|
||
providerSelect.value = providerOptions.some(([v]) => v === prefill) ? prefill : 'auto'
|
||
fillModels(prefill === 'auto' ? (Object.keys(providers)[0] || null) : prefill)
|
||
|
||
$('toggleKey').addEventListener('click', () => {
|
||
const input = $('apiKey')
|
||
const show = input.type === 'password'
|
||
input.type = show ? 'text' : 'password'
|
||
$('toggleKey').textContent = show ? '隐藏' : '显示'
|
||
})
|
||
|
||
$('submitBtn').addEventListener('click', async () => {
|
||
const btn = $('submitBtn')
|
||
const apiKey = $('apiKey').value.trim()
|
||
const agentName = $('agentName').value.trim()
|
||
const provider = providerSelect.value
|
||
if (!apiKey) {
|
||
showMsg('请先填写 API Key。', false)
|
||
return
|
||
}
|
||
btn.disabled = true
|
||
btn.innerHTML = '<span class="spinner"></span>验证中…'
|
||
showMsg('', true)
|
||
|
||
const body = { apiKey, provider, agentName }
|
||
if (provider === 'custom') {
|
||
body.model = $('model').value.trim()
|
||
body.baseURL = $('baseURL').value.trim()
|
||
if (!body.model || !body.baseURL) {
|
||
btn.disabled = false
|
||
btn.textContent = '激活'
|
||
showMsg('自定义端点需要填写模型名和 Base URL。', false)
|
||
return
|
||
}
|
||
} else if (provider !== 'auto') {
|
||
body.model = $('model').value
|
||
}
|
||
|
||
try {
|
||
const res = await fetch('/activate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body),
|
||
})
|
||
const data = await res.json()
|
||
if (data.ok) {
|
||
showMsg('✓ 激活成功!正在进入主界面…', true)
|
||
setTimeout(() => { window.location.href = '/' }, 1200)
|
||
} else {
|
||
showMsg('激活失败:' + (data.error || '未知错误'), false)
|
||
}
|
||
} catch (err) {
|
||
showMsg('激活失败:无法连接后端 (' + err.message + ')', false)
|
||
} finally {
|
||
btn.disabled = false
|
||
btn.textContent = '激活'
|
||
}
|
||
})
|
||
})()
|
||
</script>
|
||
</body>
|
||
</html>
|