initial: LAN IM desktop app (Electron 32 + Vue 3 + TS + SQLite)

This commit is contained in:
2026-07-15 18:31:13 +08:00
commit 07c7260a3c
43 changed files with 12320 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
// 主进程入口
import { app, dialog, BrowserWindow } from 'electron'
import { join } from 'node:path'
import { existsSync, writeFileSync, readFileSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import { hostname, platform } from 'node:os'
import { paths } from './paths'
import { getSettings, ensureDownloadDir } from './settings'
import { ensureIconFile } from './icons'
import { createMainWindow, broadcastToRenderer } from './window'
import { createTray } from './tray'
import { Discovery } from './discovery'
import { ChatServer } from './chat-server'
import { ChatClient } from './chat-client'
import { FileServer } from './file-server'
import { registerIpc, bindNetworkContext } from './ipc'
import { listDevices, totalUnread, getUnread } from './db'
import { setBadgeCount } from './notify'
import { DEFAULT_PORTS, PROTOCOL_VERSION } from './protocol'
import type { DeviceInfo } from './protocol'
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
;(global as any).__quitting = false
app.on('second-instance', () => {
const w = BrowserWindow.getAllWindows()[0]
if (w) { if (w.isMinimized()) w.restore(); w.show(); w.focus() }
})
app.whenReady().then(main).catch((e) => {
console.error('startup error', e)
dialog.showErrorBox('启动失败', String(e?.stack || e))
app.quit()
})
}
app.on('window-all-closed', () => {
if (!(global as any).__quitting) {
// 不退出, 隐藏到托盘
}
})
app.on('before-quit', () => { (global as any).__quitting = true })
app.on('will-quit', async () => { /* 网络资源由各模块 stop */ })
let stopFns: Array<() => Promise<void> | void> = []
async function main() {
ensureIconFile(paths.iconPng, 32)
ensureDownloadDir()
setBadgeCount(totalUnread())
const self = buildSelf()
const discovery = new Discovery(self)
const chatServer = new ChatServer(DEFAULT_PORTS.chat)
const chatClient = new ChatClient(self)
const fileServer = new FileServer(DEFAULT_PORTS.file, ensureDownloadDir())
const chatPort = await chatServer.start()
const filePort = await fileServer.start()
self.chatPort = chatPort
self.filePort = filePort
// 后台清理 24h+ 残留 .tmp (不阻塞启动)
fileServer.cleanupStaleTmp().catch(() => {})
stopFns.push(() => chatServer.stop(), () => chatClient.stop(), () => discovery.stop(), () => fileServer.stop())
await discovery.start()
bindNetworkContext({ self, discovery, chatServer, chatClient, fileServer })
// 已入库的设备: 启动后主动尝试连接
for (const d of listDevices()) {
if (d.device_id === self.deviceId) continue
chatClient.connectTo({
deviceId: d.device_id,
name: d.name,
hostname: d.hostname || '',
platform: d.platform || '',
appVersion: d.app_version || '',
address: d.last_ip || '127.0.0.1',
chatPort: DEFAULT_PORTS.chat,
filePort: DEFAULT_PORTS.file,
version: PROTOCOL_VERSION,
ts: d.last_seen,
} as DeviceInfo)
}
createMainWindow()
createTray()
registerIpc()
setTimeout(pushInitialState, 400)
}
function buildSelf(): DeviceInfo {
const idFile = join(paths.appData, 'device.id')
let id: string
if (existsSync(idFile)) id = readFileSync(idFile, 'utf8').trim()
else { id = randomUUID(); writeFileSync(idFile, id, 'utf8') }
const s = getSettings()
return {
deviceId: id,
name: s.deviceName || hostname() || 'User',
hostname: hostname() || '',
platform: platform(),
version: PROTOCOL_VERSION,
appVersion: app.getVersion(),
address: '0.0.0.0',
chatPort: 0,
filePort: 0,
ts: Date.now(),
}
}
function pushInitialState() {
broadcastToRenderer('boot:ready', {
settings: getSettings(),
unread: getUnread(),
})
}