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:
2026-08-08 01:54:08 +08:00
parent bf73366e81
commit a57c360b06
10 changed files with 432 additions and 47 deletions

265
activation.html Normal file
View 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>