Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rv-insights/electron",
"version": "0.9.11",
"version": "0.9.12",
"description": "RV-Insights next gen ai software with general agents - Electron App",
"main": "dist/main.cjs",
"author": {
Expand All @@ -14,7 +14,7 @@
"dev:split": "bash scripts/dev-split.sh",
"dev:vite": "vite dev",
"dev:kill": "bun run scripts/dev-kill.ts",
"dev:electron": "bun run dev:kill && bun run build:main && bun run build:preload && bun run build:preview-preload && bun run build:resources && bun run scripts/sleep.ts 2 && concurrently -k -n main,preload,preview,app -c yellow,magenta,cyan,cyan \"bun run watch:main\" \"bun run watch:preload\" \"bun run watch:preview-preload\" \"bunx electronmon .\"",
"dev:electron": "bun run dev:kill && bun run build:main && bun run build:preload && bun run build:preview-preload && bun run build:resources && bun run scripts/sleep.ts 2 && concurrently -k -n main,preload,preview,app -c yellow,magenta,cyan,cyan \"bun run watch:main\" \"bun run watch:preload\" \"bun run watch:preview-preload\" \"bun run scripts/run-electronmon.ts\"",
"build:main": "esbuild src/main/index.ts --bundle --platform=node --format=cjs --outfile=dist/main.cjs --external:electron --external:@anthropic-ai/claude-agent-sdk",
"build:preload": "esbuild src/preload/index.ts --bundle --platform=node --format=cjs --outfile=dist/preload.cjs --external:electron",
"build:preview-preload": "esbuild src/preload/file-preview-preload.ts --bundle --platform=node --format=cjs --outfile=dist/file-preview-preload.cjs --external:electron",
Expand Down
26 changes: 26 additions & 0 deletions apps/electron/scripts/run-electronmon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spawn } from 'child_process'

const child = spawn('bunx', ['electronmon', '.'], {
cwd: process.cwd(),
stdio: 'inherit',
shell: process.platform === 'win32',
env: {
...process.env,
RV_INSIGHTS_USE_DEV_SERVER: '1',
},
})

child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal)
return
}

process.exit(code ?? 0)
})

for (const event of ['SIGINT', 'SIGTERM'] as const) {
process.on(event, () => {
child.kill(event)
})
}
23 changes: 16 additions & 7 deletions apps/electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { wechatBridge } from './lib/wechat-bridge'
import { getWeChatConfig } from './lib/wechat-config'
import { createQuickTaskWindow, toggleQuickTaskWindow, destroyQuickTaskWindow } from './lib/quick-task-window'
import { registerGlobalShortcut, unregisterAllGlobalShortcuts } from './lib/global-shortcut-service'
import { loadRendererWindow } from './lib/renderer-loader'

// ===== Bridge 注册(新增 Bridge 只需在此添加一个 registerBridge 调用) =====

Expand Down Expand Up @@ -182,12 +183,20 @@ function createWindow(): void {
})

// Load the renderer
const isDev = !app.isPackaged
if (isDev) {
mainWindow.loadURL('http://localhost:5173')
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(join(__dirname, 'renderer', 'index.html'))
const useDevServer = process.env.RV_INSIGHTS_USE_DEV_SERVER === '1'

loadRendererWindow(mainWindow, {
isPackaged: app.isPackaged,
rendererBaseDir: __dirname,
useDevServer,
})

if (!app.isPackaged && useDevServer) {
mainWindow.webContents.once('did-finish-load', () => {
if (mainWindow?.webContents.getURL().startsWith('http://localhost:5173')) {
mainWindow.webContents.openDevTools()
}
})
}

// 窗口就绪后最大化显示
Expand All @@ -199,7 +208,7 @@ function createWindow(): void {
// 拦截页面内导航,外部链接用系统浏览器打开,防止 Electron 窗口被覆盖
mainWindow.webContents.on('will-navigate', (event, url) => {
// 允许开发模式下的 Vite HMR 热重载
if (isDev && url.startsWith('http://localhost:')) return
if (useDevServer && url.startsWith('http://localhost:')) return
event.preventDefault()
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url)
Expand Down
15 changes: 7 additions & 8 deletions apps/electron/src/main/lib/quick-task-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { BrowserWindow, screen } from 'electron'
import { join } from 'path'
import { app } from 'electron'
import { loadRendererWindow } from './renderer-loader'

/** 快速任务窗口单例 */
let quickTaskWindow: BrowserWindow | null = null
Expand Down Expand Up @@ -46,14 +47,12 @@ export function createQuickTaskWindow(): void {
})

// 加载渲染进程(附带 query 参数区分窗口类型)
const isDev = !app.isPackaged
if (isDev) {
quickTaskWindow.loadURL('http://localhost:5173?window=quick-task')
} else {
quickTaskWindow.loadFile(join(__dirname, 'renderer', 'index.html'), {
query: { window: 'quick-task' },
})
}
loadRendererWindow(quickTaskWindow, {
isPackaged: app.isPackaged,
rendererBaseDir: __dirname,
query: { window: 'quick-task' },
useDevServer: process.env.RV_INSIGHTS_USE_DEV_SERVER === '1',
})

// 失焦自动隐藏
quickTaskWindow.on('blur', () => {
Expand Down
123 changes: 123 additions & 0 deletions apps/electron/src/main/lib/renderer-loader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, expect, test } from 'bun:test'

import { loadRendererWindow } from './renderer-loader'

interface FakeLoadFileOptions {
query?: Record<string, string>
}

interface FailLoadListener {
(
event: unknown,
errorCode: number,
errorDescription: string,
validatedURL: string,
isMainFrame: boolean,
): void
}

function createFakeWindow() {
let failLoadListener: FailLoadListener | null = null

return {
loadURLCalls: [] as string[],
loadFileCalls: [] as Array<{ filePath: string; options?: FakeLoadFileOptions }>,
webContents: {
on(event: string, listener: FailLoadListener) {
if (event === 'did-fail-load') {
failLoadListener = listener
}
},
},
loadURL(url: string) {
this.loadURLCalls.push(url)
},
loadFile(filePath: string, options?: FakeLoadFileOptions) {
this.loadFileCalls.push({ filePath, options })
},
emitDidFailLoad(validatedURL: string, isMainFrame = true) {
failLoadListener?.({}, -102, 'ERR_CONNECTION_REFUSED', validatedURL, isMainFrame)
},
}
}

describe('loadRendererWindow', () => {
test('未打包启动时先尝试 dev server,并在主框架加载失败后回退到本地构建文件', () => {
const fakeWindow = createFakeWindow()

loadRendererWindow(fakeWindow, {
isPackaged: false,
rendererBaseDir: '/tmp/rv-insights/dist',
useDevServer: true,
})

expect(fakeWindow.loadURLCalls).toEqual(['http://localhost:5173'])
expect(fakeWindow.loadFileCalls).toEqual([])

fakeWindow.emitDidFailLoad('http://localhost:5173')

expect(fakeWindow.loadFileCalls).toEqual([
{
filePath: '/tmp/rv-insights/dist/renderer/index.html',
options: undefined,
},
])
})

test('快速任务窗口会保留 query 参数并在回退时继续传递', () => {
const fakeWindow = createFakeWindow()

loadRendererWindow(fakeWindow, {
isPackaged: false,
rendererBaseDir: '/tmp/rv-insights/dist',
query: { window: 'quick-task' },
useDevServer: true,
})

expect(fakeWindow.loadURLCalls).toEqual(['http://localhost:5173?window=quick-task'])

fakeWindow.emitDidFailLoad('http://localhost:5173?window=quick-task')

expect(fakeWindow.loadFileCalls).toEqual([
{
filePath: '/tmp/rv-insights/dist/renderer/index.html',
options: { query: { window: 'quick-task' } },
},
])
})

test('已打包启动时直接加载本地构建文件', () => {
const fakeWindow = createFakeWindow()

loadRendererWindow(fakeWindow, {
isPackaged: true,
rendererBaseDir: '/tmp/rv-insights/dist',
})

expect(fakeWindow.loadURLCalls).toEqual([])
expect(fakeWindow.loadFileCalls).toEqual([
{
filePath: '/tmp/rv-insights/dist/renderer/index.html',
options: undefined,
},
])
})

test('未显式启用 dev server 时直接加载本地构建文件,避免误连其他项目的 5173', () => {
const fakeWindow = createFakeWindow()

loadRendererWindow(fakeWindow, {
isPackaged: false,
rendererBaseDir: '/tmp/rv-insights/dist',
useDevServer: false,
})

expect(fakeWindow.loadURLCalls).toEqual([])
expect(fakeWindow.loadFileCalls).toEqual([
{
filePath: '/tmp/rv-insights/dist/renderer/index.html',
options: undefined,
},
])
})
})
78 changes: 78 additions & 0 deletions apps/electron/src/main/lib/renderer-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { join } from 'path'

interface RendererLoadFileOptions {
query?: Record<string, string>
}

interface DidFailLoadListener {
(
event: unknown,
errorCode: number,
errorDescription: string,
validatedURL: string,
isMainFrame: boolean,
): void
}

interface RendererWebContentsLike {
on(event: 'did-fail-load', listener: DidFailLoadListener): void
}

export interface RendererWindowLike {
webContents: RendererWebContentsLike
loadURL(url: string): Promise<unknown> | void
loadFile(filePath: string, options?: RendererLoadFileOptions): Promise<unknown> | void
}

export interface LoadRendererWindowOptions {
isPackaged: boolean
rendererBaseDir: string
query?: Record<string, string>
useDevServer?: boolean
}

const DEV_SERVER_URL = 'http://localhost:5173'

function buildRendererFilePath(rendererBaseDir: string): string {
return join(rendererBaseDir, 'renderer', 'index.html')
}

function buildRendererDevServerUrl(query?: Record<string, string>): string {
if (!query || Object.keys(query).length === 0) {
return DEV_SERVER_URL
}

const searchParams = new URLSearchParams(query)
return `${DEV_SERVER_URL}?${searchParams.toString()}`
}

export function loadRendererWindow(
window: RendererWindowLike,
options: LoadRendererWindowOptions,
): void {
const filePath = buildRendererFilePath(options.rendererBaseDir)
const fileOptions = options.query ? { query: options.query } : undefined

if (options.isPackaged || !options.useDevServer) {
void window.loadFile(filePath, fileOptions)
return
}

const devServerUrl = buildRendererDevServerUrl(options.query)
let hasFallenBack = false

window.webContents.on(
'did-fail-load',
(_event, _errorCode, errorDescription, validatedURL, isMainFrame) => {
if (hasFallenBack || !isMainFrame || validatedURL !== devServerUrl) {
return
}

hasFallenBack = true
console.warn(`[窗口] Dev server 加载失败,回退到本地构建文件: ${errorDescription}`)
void window.loadFile(filePath, fileOptions)
},
)

void window.loadURL(devServerUrl)
}
Loading