@ytchuan/flowlink 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -0
- package/app-ui/dist/assets/BaseCard-BquKu8iW.js +1 -0
- package/app-ui/dist/assets/BaseCard-vDGVmnfA.css +1 -0
- package/app-ui/dist/assets/ConsumptionView-Bv1BmzJZ.css +1 -0
- package/app-ui/dist/assets/ConsumptionView-CKpT-R7X.js +1 -0
- package/app-ui/dist/assets/HomeView-BXZYBUAv.js +1 -0
- package/app-ui/dist/assets/HomeView-C0c8S1Ss.css +1 -0
- package/app-ui/dist/assets/MetricCard-CIRjcKHO.css +1 -0
- package/app-ui/dist/assets/MetricCard-irffmzJu.js +1 -0
- package/app-ui/dist/assets/PackageView-C0KTgqoW.css +1 -0
- package/app-ui/dist/assets/PackageView-DzGcYqiI.js +1 -0
- package/app-ui/dist/assets/SettingsView-CKxlifru.js +2 -0
- package/app-ui/dist/assets/SettingsView-Co_5arPi.css +1 -0
- package/app-ui/dist/assets/StatusBadge-BeZad7-E.js +1 -0
- package/app-ui/dist/assets/StatusBadge-jZK8Ckfn.css +1 -0
- package/app-ui/dist/assets/index-C2wBeAgU.js +2269 -0
- package/app-ui/dist/assets/index-EXys6EPN.css +1 -0
- package/app-ui/dist/index.html +13 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/logo256.png +0 -0
- package/assets/logo512.png +0 -0
- package/bin/flowlink.js +24 -0
- package/main.js +315 -0
- package/package.json +88 -0
- package/preload.js +103 -0
- package/resources/tray-icon.png +0 -0
- package/services/config-writer.js +415 -0
- package/services/device.js +104 -0
- package/services/scanner.js +254 -0
- package/services/scheduler.js +197 -0
- package/services/selfcheck.js +154 -0
- package/services/settings.js +58 -0
- package/services/tray.js +110 -0
- package/services/updater.js +26 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>FlowLink</title>
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-C2wBeAgU.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-EXys6EPN.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/assets/icon.icns
ADDED
|
Binary file
|
package/assets/icon.ico
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/flowlink.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FlowLink CLI 入口
|
|
5
|
+
* npm install -g @ytchuan/flowlink 后,用户直接执行 flowlink 即可启动
|
|
6
|
+
*/
|
|
7
|
+
const { spawn } = require('child_process')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
// 定位 electron 可执行文件
|
|
11
|
+
const electronPath = require('electron')
|
|
12
|
+
|
|
13
|
+
// 应用根目录(bin 的上一级)
|
|
14
|
+
const appDir = path.join(__dirname, '..')
|
|
15
|
+
|
|
16
|
+
// 启动 Electron
|
|
17
|
+
const child = spawn(electronPath, [appDir], {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
env: process.env,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
child.on('close', (code) => {
|
|
23
|
+
process.exitCode = code
|
|
24
|
+
})
|
package/main.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* main.js - Electron 主进程入口
|
|
3
|
+
* 窗口创建、IPC 处理器注册、托盘初始化、生命周期管理
|
|
4
|
+
*/
|
|
5
|
+
const { app, BrowserWindow, ipcMain, shell, nativeTheme } = require('electron')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
|
|
9
|
+
// 服务模块
|
|
10
|
+
const deviceService = require('./services/device')
|
|
11
|
+
const schedulerService = require('./services/scheduler')
|
|
12
|
+
const scannerService = require('./services/scanner')
|
|
13
|
+
const configWriter = require('./services/config-writer')
|
|
14
|
+
const settingsService = require('./services/settings')
|
|
15
|
+
const trayService = require('./services/tray')
|
|
16
|
+
const updaterService = require('./services/updater')
|
|
17
|
+
const selfcheckService = require('./services/selfcheck')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 解析渲染层产物入口(index.html)
|
|
21
|
+
* - electron-builder 打包模式:产物位于 extraResources(process.resourcesPath/app-ui/dist)
|
|
22
|
+
* - npm 安装模式:产物随包发布,位于包根目录 app-ui/dist 下
|
|
23
|
+
*/
|
|
24
|
+
function resolveUiIndexFile() {
|
|
25
|
+
if (app.isPackaged) {
|
|
26
|
+
return path.join(process.resourcesPath, 'app-ui', 'dist', 'index.html')
|
|
27
|
+
}
|
|
28
|
+
return path.join(__dirname, 'app-ui', 'dist', 'index.html')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const uiIndexFile = resolveUiIndexFile()
|
|
32
|
+
|
|
33
|
+
/** 开发环境:未打包且包内无渲染层产物(产物存在时为生产模式,如 npm 全局安装运行) */
|
|
34
|
+
const isDev = !app.isPackaged && !fs.existsSync(uiIndexFile)
|
|
35
|
+
|
|
36
|
+
/** 应用图标(开发/打包模式均在应用根目录的 assets 下,随 asar 打包) */
|
|
37
|
+
// 窗口图标用 PNG(跨平台兼容),exe 内嵌图标用 ICO(Windows 开始菜单/任务栏)
|
|
38
|
+
const appIconPath =
|
|
39
|
+
process.platform === 'win32'
|
|
40
|
+
? path.join(__dirname, 'assets', 'icon.ico')
|
|
41
|
+
: path.join(__dirname, 'assets', 'logo256.png')
|
|
42
|
+
|
|
43
|
+
// Windows: 设置 AppUserModelID,保证任务栏/开始菜单图标正确分组与展示
|
|
44
|
+
if (process.platform === 'win32') {
|
|
45
|
+
app.setAppUserModelId('com.ytchuan.flowlink')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 主窗口 */
|
|
49
|
+
let mainWindow = null
|
|
50
|
+
|
|
51
|
+
/** 单实例锁 */
|
|
52
|
+
const gotTheLock = app.requestSingleInstanceLock()
|
|
53
|
+
if (!gotTheLock) {
|
|
54
|
+
app.quit()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 创建主窗口 */
|
|
58
|
+
function createWindow() {
|
|
59
|
+
mainWindow = new BrowserWindow({
|
|
60
|
+
width: 1100,
|
|
61
|
+
height: 700,
|
|
62
|
+
minWidth: 1000,
|
|
63
|
+
minHeight: 700,
|
|
64
|
+
show: false,
|
|
65
|
+
title: 'FlowLink',
|
|
66
|
+
// 显式指定窗口图标,避免依赖 exe 内嵌图标(内嵌失败时回退 Electron 默认图标)
|
|
67
|
+
icon: appIconPath,
|
|
68
|
+
webPreferences: {
|
|
69
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
70
|
+
contextIsolation: true,
|
|
71
|
+
nodeIntegration: false,
|
|
72
|
+
sandbox: false,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// 加载 UI:开发模式连 Vite 开发服务器,生产模式(打包安装 / npm 安装)加载本地产物
|
|
77
|
+
if (isDev) {
|
|
78
|
+
mainWindow.loadURL('http://localhost:5173')
|
|
79
|
+
mainWindow.webContents.openDevTools({ mode: 'detach' })
|
|
80
|
+
} else {
|
|
81
|
+
mainWindow.loadFile(uiIndexFile)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 窗口准备好后显示
|
|
85
|
+
mainWindow.once('ready-to-show', () => {
|
|
86
|
+
mainWindow.show()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// 外部链接用系统浏览器打开
|
|
90
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
91
|
+
shell.openExternal(url)
|
|
92
|
+
return { action: 'deny' }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// 关闭行为:最小化到托盘 or 退出
|
|
96
|
+
mainWindow.on('close', (event) => {
|
|
97
|
+
const settings = settingsService.getSettings()
|
|
98
|
+
if (settings.closeBehavior === 'minimize' && !app.isQuitting) {
|
|
99
|
+
event.preventDefault()
|
|
100
|
+
mainWindow.hide()
|
|
101
|
+
}
|
|
102
|
+
// 否则不拦截,窗口正常关闭,触发 window-all-closed → app.quit()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
mainWindow.on('closed', () => {
|
|
106
|
+
mainWindow = null
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 注册所有 IPC 处理器 */
|
|
111
|
+
function registerIpcHandlers() {
|
|
112
|
+
// ---- 设备 ----
|
|
113
|
+
ipcMain.handle('device:get-id', () => {
|
|
114
|
+
return deviceService.getDeviceId()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ---- 调度服务 ----
|
|
118
|
+
ipcMain.handle('scheduler:start', async (_event, gatewayUrl) => {
|
|
119
|
+
const settings = settingsService.getSettings()
|
|
120
|
+
const deviceId = deviceService.getDeviceId()
|
|
121
|
+
// 读取缓存的 API Key(从配置文件或设置中)
|
|
122
|
+
const apiKey = settings.apiKey || ''
|
|
123
|
+
// 优先使用渲染进程从 app.ts config 接口获取到的中转站网关地址
|
|
124
|
+
const result = await schedulerService.start({
|
|
125
|
+
apiKey,
|
|
126
|
+
deviceId,
|
|
127
|
+
gatewayUrl: gatewayUrl || settings.gatewayUrl,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// 更新托盘菜单
|
|
131
|
+
trayService.updateTrayMenu(schedulerService.running)
|
|
132
|
+
|
|
133
|
+
// 通知渲染进程状态变更
|
|
134
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
135
|
+
mainWindow.webContents.send('scheduler:status-changed', {
|
|
136
|
+
running: schedulerService.running,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
ipcMain.handle('scheduler:stop', async () => {
|
|
144
|
+
const result = await schedulerService.stop()
|
|
145
|
+
|
|
146
|
+
trayService.updateTrayMenu(schedulerService.running)
|
|
147
|
+
|
|
148
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
149
|
+
mainWindow.webContents.send('scheduler:status-changed', {
|
|
150
|
+
running: schedulerService.running,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
ipcMain.handle('scheduler:status', () => {
|
|
158
|
+
return schedulerService.getStatus()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ---- 客户端扫描 ----
|
|
162
|
+
ipcMain.handle('scanner:scan', () => {
|
|
163
|
+
return scannerService.scan()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// ---- 配置写入 ----
|
|
167
|
+
ipcMain.handle('config:apply', (_event, { targets, backup, config, models }) => {
|
|
168
|
+
// 优先使用渲染进程传来的配置,缺省时回退到本地设置
|
|
169
|
+
const settings = settingsService.getSettings()
|
|
170
|
+
const apiEndpoint =
|
|
171
|
+
config?.apiEndpoint || settings.apiEndpoint || 'http://127.0.0.1:51173/v1/chat/completions'
|
|
172
|
+
const apiKey = config?.apiKey || settings.apiKey || ''
|
|
173
|
+
return configWriter.applyConfig(targets, backup, { apiEndpoint, apiKey }, models)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
ipcMain.handle('config:delete', (_event, { targets, models }) => {
|
|
177
|
+
return configWriter.deleteConfig(targets, models)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
ipcMain.handle('config:restore', (_event, { targets }) => {
|
|
181
|
+
return configWriter.restoreConfig(targets)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
ipcMain.handle('config:open-backup-dir', async () => {
|
|
185
|
+
await configWriter.openBackupDir()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// ---- 设置 ----
|
|
189
|
+
ipcMain.handle('settings:get', () => {
|
|
190
|
+
return settingsService.getSettings()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
ipcMain.handle('settings:save', (_event, partial) => {
|
|
194
|
+
settingsService.saveSettings(partial)
|
|
195
|
+
|
|
196
|
+
// 应用主题变更
|
|
197
|
+
if (partial.theme) {
|
|
198
|
+
if (partial.theme === 'dark') {
|
|
199
|
+
nativeTheme.themeSource = 'dark'
|
|
200
|
+
} else if (partial.theme === 'light') {
|
|
201
|
+
nativeTheme.themeSource = 'light'
|
|
202
|
+
} else {
|
|
203
|
+
nativeTheme.themeSource = 'system'
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 应用开机自启
|
|
208
|
+
if (partial.autoStart !== undefined) {
|
|
209
|
+
try {
|
|
210
|
+
app.setLoginItemSettings({ openAtLogin: partial.autoStart })
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.warn('设置开机自启失败:', err.message)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// ---- 托盘通知 ----
|
|
218
|
+
ipcMain.on('tray:notification', (_event, { title, body }) => {
|
|
219
|
+
trayService.showNotification(title, body)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
ipcMain.on('tray:alert', (_event, { title, body }) => {
|
|
223
|
+
// 主进程记录日志,UI 层自行处理弹窗显示
|
|
224
|
+
console.log(`[Alert] ${title}: ${body}`)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// ---- 更新检查 ----
|
|
228
|
+
ipcMain.handle('updater:check', () => {
|
|
229
|
+
return updaterService.checkForUpdate()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// ---- 外部链接 ----
|
|
233
|
+
ipcMain.handle('shell:open-external', async (_event, url) => {
|
|
234
|
+
await shell.openExternal(url)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// ---- 环境自检 ----
|
|
238
|
+
// renderer 传入最新账户数据,避免依赖本地 settings 中的缓存
|
|
239
|
+
ipcMain.handle('selfcheck:run', (_event, context) => {
|
|
240
|
+
const deviceId = deviceService.getDeviceId()
|
|
241
|
+
return selfcheckService.runSelfCheck({
|
|
242
|
+
deviceId,
|
|
243
|
+
...context,
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- App 生命周期 ----
|
|
249
|
+
app.whenReady().then(() => {
|
|
250
|
+
registerIpcHandlers()
|
|
251
|
+
createWindow()
|
|
252
|
+
|
|
253
|
+
// 调度服务不随软件启动,由主页"启动服务"按钮触发(scheduler:start)
|
|
254
|
+
|
|
255
|
+
// 初始化托盘
|
|
256
|
+
trayService.createTray(mainWindow)
|
|
257
|
+
trayService.updateTrayMenu(schedulerService.running)
|
|
258
|
+
|
|
259
|
+
// 应用保存的主题
|
|
260
|
+
const settings = settingsService.getSettings()
|
|
261
|
+
if (settings.theme === 'dark') {
|
|
262
|
+
nativeTheme.themeSource = 'dark'
|
|
263
|
+
} else if (settings.theme === 'light') {
|
|
264
|
+
nativeTheme.themeSource = 'light'
|
|
265
|
+
} else {
|
|
266
|
+
nativeTheme.themeSource = 'system'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 应用开机自启设置
|
|
270
|
+
try {
|
|
271
|
+
app.setLoginItemSettings({ openAtLogin: settings.autoStart })
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.warn('设置开机自启失败:', err.message)
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// macOS: 点击 Dock 图标时重新显示窗口
|
|
278
|
+
app.on('activate', () => {
|
|
279
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
280
|
+
createWindow()
|
|
281
|
+
} else if (mainWindow) {
|
|
282
|
+
mainWindow.show()
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// 第二实例启动时聚焦主窗口
|
|
287
|
+
app.on('second-instance', () => {
|
|
288
|
+
if (mainWindow) {
|
|
289
|
+
if (mainWindow.isMinimized()) {
|
|
290
|
+
mainWindow.restore()
|
|
291
|
+
}
|
|
292
|
+
mainWindow.show()
|
|
293
|
+
mainWindow.focus()
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// 所有窗口关闭时退出(除 macOS)
|
|
298
|
+
app.on('window-all-closed', () => {
|
|
299
|
+
if (process.platform !== 'darwin') {
|
|
300
|
+
app.quit()
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// 应用退出前停止调度服务
|
|
305
|
+
// 注意:Electron 不会等待 async handler,因此先同步标记停止,再手动退出
|
|
306
|
+
app.on('before-quit', () => {
|
|
307
|
+
app.isQuitting = true
|
|
308
|
+
if (schedulerService.running && schedulerService.server) {
|
|
309
|
+
try {
|
|
310
|
+
schedulerService.server.close()
|
|
311
|
+
} catch (_) {}
|
|
312
|
+
schedulerService.server = null
|
|
313
|
+
schedulerService.running = false
|
|
314
|
+
}
|
|
315
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ytchuan/flowlink",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "FlowLink 桌面客户端 - AI 服务调度与管理",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flowlink": "bin/flowlink.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"main.js",
|
|
12
|
+
"preload.js",
|
|
13
|
+
"services/",
|
|
14
|
+
"app-ui/dist/",
|
|
15
|
+
"assets/",
|
|
16
|
+
"resources/"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "electron .",
|
|
23
|
+
"dev": "concurrently -k \"npm:dev:ui\" \"npm:dev:electron\"",
|
|
24
|
+
"dev:ui": "cd ../app-ui && npm run dev",
|
|
25
|
+
"dev:electron": "wait-on tcp:5173 && electron .",
|
|
26
|
+
"build:ui": "cd ../app-ui && npm run build",
|
|
27
|
+
"build": "npm run build:ui && electron-builder",
|
|
28
|
+
"build:win": "npm run build:ui && electron-builder --win",
|
|
29
|
+
"build:win:portable": "npm run build:ui && electron-builder --win portable",
|
|
30
|
+
"build:mac": "npm run build:ui && electron-builder --mac",
|
|
31
|
+
"build:mac:zip": "npm run build:ui && electron-builder --mac zip",
|
|
32
|
+
"prepack": "npm run build:ui && node scripts/copy-ui.js && node scripts/manifest-electron.js rewrite",
|
|
33
|
+
"postpack": "node scripts/manifest-electron.js restore",
|
|
34
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
35
|
+
},
|
|
36
|
+
"author": "ytchuan",
|
|
37
|
+
"license": "ISC",
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"electron": "^43.2.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"concurrently": "^9.1.0",
|
|
46
|
+
"electron-builder": "^25.1.8",
|
|
47
|
+
"wait-on": "^8.0.1"
|
|
48
|
+
},
|
|
49
|
+
"build": {
|
|
50
|
+
"appId": "com.ytchuan.flowlink",
|
|
51
|
+
"productName": "FlowLink",
|
|
52
|
+
"directories": {
|
|
53
|
+
"output": "dist-electron"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"main.js",
|
|
57
|
+
"preload.js",
|
|
58
|
+
"services/**/*",
|
|
59
|
+
"assets/icon.ico",
|
|
60
|
+
"assets/logo256.png"
|
|
61
|
+
],
|
|
62
|
+
"extraResources": [
|
|
63
|
+
{
|
|
64
|
+
"from": "../app-ui/dist",
|
|
65
|
+
"to": "app-ui/dist"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"from": "resources/tray-icon.png",
|
|
69
|
+
"to": "resources/tray-icon.png"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"mac": {
|
|
73
|
+
"category": "public.app-category.developer-tools",
|
|
74
|
+
"target": [
|
|
75
|
+
"dmg",
|
|
76
|
+
"zip"
|
|
77
|
+
],
|
|
78
|
+
"icon": "assets/icon.icns"
|
|
79
|
+
},
|
|
80
|
+
"win": {
|
|
81
|
+
"icon": "assets/icon.ico",
|
|
82
|
+
"target": [
|
|
83
|
+
"nsis",
|
|
84
|
+
"portable"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preload.js
|
|
3
|
+
* 通过 contextBridge 暴露 electronAPI 给渲染进程
|
|
4
|
+
* 所有 IPC 通信均通过此文件中转
|
|
5
|
+
*/
|
|
6
|
+
const { contextBridge, ipcRenderer, clipboard } = require('electron')
|
|
7
|
+
|
|
8
|
+
/** 允许的事件通道白名单 */
|
|
9
|
+
const VALID_CHANNELS = [
|
|
10
|
+
'scheduler:status-changed',
|
|
11
|
+
'package:balance-low',
|
|
12
|
+
'tray:alert',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
/** 存储 callback → wrapped handler 的映射,用于正确 off */
|
|
16
|
+
const listenerMap = new Map()
|
|
17
|
+
|
|
18
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
19
|
+
// 设备
|
|
20
|
+
device: {
|
|
21
|
+
getDeviceId: () => ipcRenderer.invoke('device:get-id'),
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// 调度服务
|
|
25
|
+
scheduler: {
|
|
26
|
+
start: (gatewayUrl) => ipcRenderer.invoke('scheduler:start', gatewayUrl),
|
|
27
|
+
stop: () => ipcRenderer.invoke('scheduler:stop'),
|
|
28
|
+
getStatus: () => ipcRenderer.invoke('scheduler:status'),
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// 客户端扫描
|
|
32
|
+
scanner: {
|
|
33
|
+
scan: () => ipcRenderer.invoke('scanner:scan'),
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// 配置写入
|
|
37
|
+
config: {
|
|
38
|
+
apply: (targets, backup, config, models) =>
|
|
39
|
+
ipcRenderer.invoke('config:apply', { targets, backup, config, models }),
|
|
40
|
+
delete: (targets, models) => ipcRenderer.invoke('config:delete', { targets, models }),
|
|
41
|
+
restore: (targets) => ipcRenderer.invoke('config:restore', { targets }),
|
|
42
|
+
openBackupDir: () => ipcRenderer.invoke('config:open-backup-dir'),
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// 设置
|
|
46
|
+
settings: {
|
|
47
|
+
get: () => ipcRenderer.invoke('settings:get'),
|
|
48
|
+
save: (settings) => ipcRenderer.invoke('settings:save', settings),
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// 剪贴板
|
|
52
|
+
clipboard: {
|
|
53
|
+
write: (text) => {
|
|
54
|
+
clipboard.writeText(text)
|
|
55
|
+
return Promise.resolve()
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// 外部链接
|
|
60
|
+
shell: {
|
|
61
|
+
openExternal: (url) => ipcRenderer.invoke('shell:open-external', url),
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// 托盘通知
|
|
65
|
+
tray: {
|
|
66
|
+
showNotification: (title, body) => {
|
|
67
|
+
ipcRenderer.send('tray:notification', { title, body })
|
|
68
|
+
},
|
|
69
|
+
showAlertModal: (title, body) => {
|
|
70
|
+
ipcRenderer.send('tray:alert', { title, body })
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// 更新检查
|
|
75
|
+
updater: {
|
|
76
|
+
check: () => ipcRenderer.invoke('updater:check'),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// 环境自检
|
|
80
|
+
selfcheck: {
|
|
81
|
+
run: (context) => ipcRenderer.invoke('selfcheck:run', context),
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// 事件监听(main → renderer)
|
|
85
|
+
on: (channel, callback) => {
|
|
86
|
+
if (!VALID_CHANNELS.includes(channel)) return
|
|
87
|
+
// 包装 handler,剥离 event 参数
|
|
88
|
+
const handler = (_event, ...args) => callback(...args)
|
|
89
|
+
// 存储映射:channel + callback → handler
|
|
90
|
+
const key = `${channel}::${callback}`
|
|
91
|
+
listenerMap.set(key, handler)
|
|
92
|
+
ipcRenderer.on(channel, handler)
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
off: (channel, callback) => {
|
|
96
|
+
const key = `${channel}::${callback}`
|
|
97
|
+
const handler = listenerMap.get(key)
|
|
98
|
+
if (handler) {
|
|
99
|
+
ipcRenderer.removeListener(channel, handler)
|
|
100
|
+
listenerMap.delete(key)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
})
|
|
Binary file
|