feat: 白龙马数字意识框架 v2.2.0 - 3D意识空间、多技能协同、自我进化系统

This commit is contained in:
xiaoyuanda666-ship-it
2026-06-07 19:43:22 +08:00
commit bf73366e81
234 changed files with 219763 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('focusBanner', {
close: () => ipcRenderer.send('focus-banner:close'),
toggleTask: (idx, done) => ipcRenderer.send('focus-banner:toggle-task', { idx, done }),
setExpanded: (expanded) => ipcRenderer.send('focus-banner:set-expanded', { expanded }),
requestResize: () => ipcRenderer.send('focus-banner:request-resize'),
onUpdate: (handler) => {
ipcRenderer.on('focus-banner:update', (_e, data) => handler(data))
},
onConfig: (handler) => {
ipcRenderer.on('focus-banner:config', (_e, cfg) => handler(cfg))
},
})

499
electron/main.cjs Normal file
View File

@@ -0,0 +1,499 @@
// Windows: 把控制台代码页切到 UTF-8避免中文 stdout 显示为乱码
if (process.platform === 'win32') {
try {
require('child_process').execSync('chcp 65001', { stdio: 'ignore', windowsHide: true })
} catch (_) {}
}
const { app, BrowserWindow, shell, dialog, Menu, ipcMain, Tray, nativeImage } = require('electron')
const path = require('path')
const fs = require('fs')
const net = require('net')
const http = require('http')
const { EventEmitter } = require('events')
const { pathToFileURL } = require('url')
const { autoUpdater } = require('electron-updater')
const IS_DEV = !app.isPackaged
const WINDOWS_APP_USER_MODEL_ID = 'com.xiaoyuanda.bailongma'
const USER_DIR = app.getPath('userData')
const CODE_ROOT = app.getAppPath()
const RESOURCE_ROOT = CODE_ROOT
const BACKEND_ENTRY = path.join(CODE_ROOT, 'src', 'index.js')
// 持久化日志:把 console.* 镜像到 USER_DIR/logs/bailongma.log
// 安装版没有 stdout 的情况下,卡死/崩溃后还能 tail 这个文件复盘。
// 简易 rotate> 5MB 时把当前文件改名 .old覆盖上一份 .old下次写入重开。
const LOG_DIR = path.join(USER_DIR, 'logs')
const LOG_FILE = path.join(LOG_DIR, 'bailongma.log')
const LOG_FILE_OLD = path.join(LOG_DIR, 'bailongma.old.log')
const LOG_MAX_BYTES = 5 * 1024 * 1024
try { fs.mkdirSync(LOG_DIR, { recursive: true }) } catch {}
function rotateLogIfNeeded() {
try {
const stat = fs.statSync(LOG_FILE)
if (stat.size > LOG_MAX_BYTES) {
try { fs.rmSync(LOG_FILE_OLD, { force: true }) } catch {}
try { fs.renameSync(LOG_FILE, LOG_FILE_OLD) } catch {}
}
} catch {}
}
function writeLog(level, args) {
let line
try {
line = args.map(a => {
if (typeof a === 'string') return a
if (a instanceof Error) return a.stack || a.message
try { return JSON.stringify(a) } catch { return String(a) }
}).join(' ')
} catch { line = '[log-serialize-failed]' }
const ts = new Date().toISOString()
const out = `${ts} [${level}] ${line}\n`
try { fs.appendFileSync(LOG_FILE, out) } catch {}
}
// Hijack 一次就够;后端 import 在同一进程console.* 引用的是同一个 console 对象。
// 把原始方法存起来appendFile 失败时仍能输出到 stdout/stderr开发模式可见
;(function installLogHijack() {
const levels = ['log', 'info', 'warn', 'error', 'debug']
for (const level of levels) {
const original = console[level]?.bind(console) || (() => {})
console[level] = (...args) => {
try { original(...args) } catch {}
try {
rotateLogIfNeeded()
writeLog(level, args)
} catch {}
}
}
})()
process.on('unhandledRejection', (reason) => {
console.error('[unhandledRejection]', reason instanceof Error ? (reason.stack || reason.message) : String(reason))
})
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err?.stack || err?.message || String(err))
})
console.log(`[main] Bailongma ${app.getVersion()} starting, logs → ${LOG_FILE}`)
let mainWindow = null
let backendPort = 0
let tray = null
let focusBannerWindow = null
// 后端通过 global.focusBannerBridge 控制横幅窗口
const focusBannerBridge = new EventEmitter()
global.focusBannerBridge = focusBannerBridge
global.bailongmaAppControl = {
restart() {
console.log('[main] restart requested')
app.isQuiting = true
app.relaunch()
app.quit()
},
}
if (process.platform === 'win32') {
app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID)
}
function sendUpdaterStatus(payload = {}) {
if (!mainWindow || mainWindow.isDestroyed()) return
mainWindow.webContents.send('updater:status', {
currentVersion: app.getVersion(),
...payload,
})
}
async function bootstrapBackend(port) {
process.env.BAILONGMA_USER_DIR ||= USER_DIR
process.env.BAILONGMA_RESOURCES_DIR ||= RESOURCE_ROOT
process.env.BAILONGMA_PORT = String(port)
await import(pathToFileURL(BACKEND_ENTRY).href)
}
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
process.exit(0)
}
async function findFreePort(preferred = 3721) {
for (const port of [preferred, 0]) {
try {
const actual = await new Promise((resolve, reject) => {
const server = net.createServer()
server.once('error', reject)
server.listen(port, '127.0.0.1', () => {
const address = server.address()
server.close(() => resolve(address.port))
})
})
return actual
} catch {}
}
throw new Error('Unable to find a free local port')
}
function waitForBackend(port, timeoutMs = 30000) {
const startedAt = Date.now()
const url = `http://127.0.0.1:${port}/activation-status`
return new Promise((resolve, reject) => {
const tick = () => {
if (Date.now() - startedAt > timeoutMs) {
reject(new Error('Backend startup timed out'))
return
}
const req = http.get(url, res => {
res.resume()
resolve()
})
req.on('error', () => setTimeout(tick, 300))
req.setTimeout(1500, () => {
req.destroy()
setTimeout(tick, 300)
})
}
tick()
})
}
async function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 840,
minWidth: 900,
minHeight: 600,
backgroundColor: '#0b0b0e',
title: 'Bailongma',
icon: path.join(RESOURCE_ROOT, 'build', 'icon.png'),
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.cjs'),
},
})
// 授予麦克风权限(语音输入需要)
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
if (permission === 'media') return callback(true)
callback(false)
})
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission) => {
if (permission === 'media') return true
return false
})
// 窗口级快捷键(不用 globalShortcut避免劫持其他应用的 F11/Ctrl+R 等)
// F12 → 切换 DevTools
// F11 → 切换全屏
// Ctrl+R → reload仅 dev
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.type !== 'keyDown') return
if (input.key === 'F12') {
mainWindow.webContents.toggleDevTools()
event.preventDefault()
return
}
if (input.key === 'F11') {
mainWindow.setFullScreen(!mainWindow.isFullScreen())
event.preventDefault()
return
}
if (IS_DEV && (input.control || input.meta) && input.key.toLowerCase() === 'r') {
mainWindow.webContents.reload()
event.preventDefault()
return
}
})
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:\/\//i.test(url)) {
shell.openExternal(url)
return { action: 'deny' }
}
return { action: 'allow' }
})
await mainWindow.loadURL(`http://127.0.0.1:${backendPort}/`)
// 关闭主窗口时最小化到托盘,不退出
mainWindow.on('close', (e) => {
if (!app.isQuiting) {
e.preventDefault()
mainWindow.hide()
}
})
mainWindow.on('closed', () => {
mainWindow = null
})
}
function setupTray() {
const iconPath = path.join(RESOURCE_ROOT, 'build', 'icon.ico')
tray = new Tray(nativeImage.createFromPath(iconPath))
tray.setToolTip('Bailongma')
const contextMenu = Menu.buildFromTemplate([
{
label: '显示主界面',
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
},
},
{ type: 'separator' },
{
{
label: '3D 意识空间',
click: () => {
if (mainWindow) {
mainWindow.loadURL(`http://127.0.0.1:`+port+`/consciousness-3d`)
mainWindow.show()
mainWindow.focus()
}
},
},
{ type: 'separator' }, label: '退出',
click: () => {
app.isQuiting = true
app.quit()
},
},
])
tray.setContextMenu(contextMenu)
tray.on('double-click', () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
})
}
function createFocusBannerWindow({ task = '', current_step = '', tasks = [] } = {}) {
if (focusBannerWindow && !focusBannerWindow.isDestroyed()) {
focusBannerWindow.webContents.send('focus-banner:update', { task, current_step, tasks })
return
}
const { width: screenW } = require('electron').screen.getPrimaryDisplay().workAreaSize
focusBannerWindow = new BrowserWindow({
width: 280,
height: 60,
x: Math.round(screenW / 2 - 140),
y: 48,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
movable: true,
minimizable: false,
maximizable: false,
focusable: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.join(__dirname, 'focus-banner-preload.cjs'),
},
})
// 给 banner 窗口的 session 也授权麦克风
focusBannerWindow.webContents.session.setPermissionRequestHandler((wc, permission, callback) => {
if (permission === 'media') return callback(true)
callback(false)
})
focusBannerWindow.webContents.session.setPermissionCheckHandler((wc, permission) => {
if (permission === 'media') return true
return false
})
focusBannerWindow.loadFile(path.join(RESOURCE_ROOT, 'focus-banner.html'))
focusBannerWindow.webContents.once('did-finish-load', () => {
if (!focusBannerWindow || focusBannerWindow.isDestroyed()) return
// 先发端口配置,让语音识别结果能发回后端
focusBannerWindow.webContents.send('focus-banner:config', { port: backendPort })
focusBannerWindow.webContents.send('focus-banner:update', { task, current_step, tasks })
autoResizeBannerWindow()
})
focusBannerWindow.on('closed', () => {
focusBannerWindow = null
})
}
function autoResizeBannerWindow() {
if (!focusBannerWindow || focusBannerWindow.isDestroyed()) return
focusBannerWindow.webContents.executeJavaScript(`
(() => {
const b = document.getElementById('banner')
return b ? { w: b.offsetWidth, h: b.offsetHeight } : null
})()
`).then(size => {
if (!size || !focusBannerWindow || focusBannerWindow.isDestroyed()) return
const padW = 0
const padH = 0
focusBannerWindow.setSize(Math.max(160, size.w + padW), Math.max(40, size.h + padH))
}).catch(() => {})
}
// Focus Banner IPC handlers
ipcMain.on('focus-banner:close', () => {
if (focusBannerWindow && !focusBannerWindow.isDestroyed()) {
focusBannerWindow.close()
focusBannerWindow = null
}
})
ipcMain.on('focus-banner:set-expanded', (_e, { expanded }) => {
if (!focusBannerWindow || focusBannerWindow.isDestroyed()) return
setTimeout(() => autoResizeBannerWindow(), 50)
})
ipcMain.on('focus-banner:request-resize', () => {
setTimeout(() => autoResizeBannerWindow(), 30)
})
ipcMain.on('focus-banner:toggle-task', (_e, { idx, done }) => {
// 任务勾选状态更改,横幅已在前端自行更新,无需额外操作
})
// 后端 bridge 事件监听
focusBannerBridge.on('command', ({ action, task, current_step, tasks }) => {
if (action === 'show' || action === 'update') {
createFocusBannerWindow({ task, current_step, tasks })
}
})
focusBannerBridge.on('hide', () => {
if (focusBannerWindow && !focusBannerWindow.isDestroyed()) {
focusBannerWindow.close()
focusBannerWindow = null
}
})
function setupAutoUpdater() {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('checking-for-update', () => {
sendUpdaterStatus({ stage: 'checking' })
})
autoUpdater.on('update-available', info => {
console.log('[updater] update available', info?.version)
sendUpdaterStatus({ stage: 'available', version: info?.version })
})
autoUpdater.on('download-progress', progress => {
sendUpdaterStatus({
stage: 'downloading',
percent: Number(progress?.percent || 0),
transferred: progress?.transferred || 0,
total: progress?.total || 0,
})
})
autoUpdater.on('update-downloaded', info => {
console.log('[updater] update downloaded', info?.version)
sendUpdaterStatus({ stage: 'downloaded', version: info?.version })
})
autoUpdater.on('update-not-available', info => {
sendUpdaterStatus({
stage: 'up-to-date',
version: info?.version || app.getVersion(),
})
})
autoUpdater.on('error', err => {
const message = err?.message || String(err || 'Update failed')
console.warn('[updater] update failed', message)
sendUpdaterStatus({ stage: 'error', message })
})
if (!IS_DEV) {
autoUpdater.checkForUpdates().catch(() => {})
}
}
ipcMain.handle('app:get-version', () => app.getVersion())
ipcMain.handle('updater:check-for-updates', async () => {
if (IS_DEV) {
sendUpdaterStatus({ stage: 'dev' })
return { ok: false, skipped: true, reason: 'dev' }
}
try {
sendUpdaterStatus({ stage: 'checking' })
const result = await autoUpdater.checkForUpdates()
return { ok: true, updateInfo: result?.updateInfo || null }
} catch (error) {
const message = error?.message || String(error || 'Update check failed')
sendUpdaterStatus({ stage: 'error', message })
return { ok: false, message }
}
})
ipcMain.handle('updater:start-download', async () => {
try {
await autoUpdater.downloadUpdate()
return { ok: true }
} catch (error) {
const message = error?.message || String(error || 'Download failed')
sendUpdaterStatus({ stage: 'error', message })
return { ok: false, message }
}
})
ipcMain.handle('updater:quit-and-install', () => {
autoUpdater.quitAndInstall()
})
app.on('second-instance', () => {
if (!mainWindow) return
if (mainWindow.isMinimized()) mainWindow.restore()
if (!mainWindow.isVisible()) mainWindow.show()
mainWindow.focus()
})
app.on('window-all-closed', () => {
// 主窗口关闭后保持后台运行Focus Banner 等桌面功能继续工作)
// 只有托盘菜单「退出」才真正退出
})
app.whenReady().then(async () => {
ipcMain.on('open-consciousness-3d', () => {
if (mainWindow) {
mainWindow.loadURL(`http://127.0.0.1:`+port+`/consciousness-3d`)
}
})
Menu.setApplicationMenu(null)
try {
backendPort = await findFreePort(3721)
await bootstrapBackend(backendPort)
await waitForBackend(backendPort)
} catch (err) {
dialog.showErrorBox('Startup failed', `Unable to start the Bailongma backend:\n${err.message}`)
app.quit()
return
}
await createWindow()
setupTray()
setupAutoUpdater()
// 不再注册任何系统级 globalShortcutF11 / F12 / Ctrl+R 已由 mainWindow
// 的 before-input-event 处理(见 createWindow只在窗口获焦时生效
// 不会劫持浏览器/IDE 等其他应用的同键操作。
})

18
electron/preload.cjs Normal file
View File

@@ -0,0 +1,18 @@
const { contextBridge, ipcRenderer, webFrame } = require('electron')
contextBridge.exposeInMainWorld('bailongma', {
platform: process.platform,
isElectron: true,
getVersion: () => ipcRenderer.invoke('app:get-version'),
checkForUpdates: () => ipcRenderer.invoke('updater:check-for-updates'),
startDownload: () => ipcRenderer.invoke('updater:start-download'),
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
getZoomFactor: () => webFrame.getZoomFactor(),
setZoomFactor: (factor) => webFrame.setZoomFactor(factor),
onUpdaterStatus: (handler) => {
if (typeof handler !== 'function') return () => {}
const listener = (_event, payload) => handler(payload)
ipcRenderer.on('updater:status', listener)
return () => ipcRenderer.removeListener('updater:status', listener)
},
})