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>
This commit is contained in:
265
activation.html
Normal file
265
activation.html
Normal file
@@ -0,0 +1,265 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user