Files
BaiLongma/activation.html
Geliebte a57c360b06 fix: 修复托盘菜单语法错误与激活页缺失,完善 Windows 打包流程
- 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>
2026-08-08 01:54:08 +08:00

266 lines
9.4 KiB
HTML
Raw Permalink 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>激活 · 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>