@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,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客户端配置写入/备份/删除
|
|
3
|
+
* 向 WorkBuddy / CodeBuddy 写入 API 配置文件
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
const { app, shell } = require('electron')
|
|
9
|
+
const scanner = require('./scanner')
|
|
10
|
+
|
|
11
|
+
/** 备份目录 */
|
|
12
|
+
function getBackupDir() {
|
|
13
|
+
return path.join(app.getPath('userData'), 'config-backups')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 客户端配置文件路径映射 */
|
|
17
|
+
function getClientConfigPath(targetKey) {
|
|
18
|
+
const configPath = scanner.getConfigPath(targetKey)
|
|
19
|
+
if (configPath) return configPath
|
|
20
|
+
|
|
21
|
+
// 降级:使用默认路径
|
|
22
|
+
const home = os.homedir()
|
|
23
|
+
const map = {
|
|
24
|
+
workbuddy: path.join(home, '.workbuddy', 'models.json'),
|
|
25
|
+
codebuddy: path.join(home, '.codebuddy', 'models.json'),
|
|
26
|
+
}
|
|
27
|
+
return map[targetKey] || null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 生成 WorkBuddy 自定义模型条目
|
|
32
|
+
* @param {{ apiEndpoint: string, apiKey: string }} config
|
|
33
|
+
* @param {Array<{ code: string }>} models - app.ts modelPage 返回的模型列表
|
|
34
|
+
* @returns {Array<object>}
|
|
35
|
+
*/
|
|
36
|
+
function buildWorkBuddyModels(config, models) {
|
|
37
|
+
return models.map((model) => ({
|
|
38
|
+
id: model.code,
|
|
39
|
+
name: model.code,
|
|
40
|
+
vendor: 'Custom',
|
|
41
|
+
url: config.apiEndpoint,
|
|
42
|
+
apiKey: config.apiKey,
|
|
43
|
+
supportsToolCall: true,
|
|
44
|
+
supportsImages: true,
|
|
45
|
+
supportsReasoning: true,
|
|
46
|
+
useCustomProtocol: false,
|
|
47
|
+
reasoning: {
|
|
48
|
+
supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
49
|
+
},
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 生成 CodeBuddy 自定义模型条目
|
|
55
|
+
* CodeBuddy 格式:vendor 为 'user',无 reasoning/useCustomProtocol 字段
|
|
56
|
+
* @param {{ apiEndpoint: string, apiKey: string }} config
|
|
57
|
+
* @param {Array<{ code: string }>} models
|
|
58
|
+
* @returns {Array<object>}
|
|
59
|
+
*/
|
|
60
|
+
function buildCodeBuddyModels(config, models) {
|
|
61
|
+
return models.map((model) => ({
|
|
62
|
+
id: model.code,
|
|
63
|
+
name: model.code,
|
|
64
|
+
vendor: 'user',
|
|
65
|
+
url: config.apiEndpoint,
|
|
66
|
+
apiKey: config.apiKey,
|
|
67
|
+
supportsToolCall: true,
|
|
68
|
+
supportsImages: true,
|
|
69
|
+
supportsReasoning: true,
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 读取 JSON 数组文件(不存在或格式异常时返回空数组) */
|
|
74
|
+
function readJsonArray(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(filePath)) {
|
|
77
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
78
|
+
if (Array.isArray(data)) return data
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// 文件损坏视为无内容
|
|
82
|
+
}
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 读取 { "models": [...] } 格式 JSON 文件(不存在或格式异常时返回空数组)
|
|
88
|
+
* @param {string} filePath
|
|
89
|
+
* @returns {Array<object>}
|
|
90
|
+
*/
|
|
91
|
+
function readJsonModelsWrapper(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(filePath)) {
|
|
94
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
95
|
+
if (data && Array.isArray(data.models)) return data.models
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// 文件损坏视为无内容
|
|
99
|
+
}
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 备份文件到备份目录 */
|
|
104
|
+
function backupFile(filePath, targetKey, suffix = '') {
|
|
105
|
+
if (!fs.existsSync(filePath)) return
|
|
106
|
+
const backupDir = getBackupDir()
|
|
107
|
+
const backupName = `${targetKey}${suffix}-${Date.now()}.json`
|
|
108
|
+
fs.mkdirSync(backupDir, { recursive: true })
|
|
109
|
+
fs.copyFileSync(filePath, path.join(backupDir, backupName))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 写入 WorkBuddy 模型配置(合并写入:保留用户其他模型,替换本软件管理的模型)
|
|
114
|
+
* @param {string} configPath
|
|
115
|
+
* @param {boolean} backup
|
|
116
|
+
* @param {{ apiEndpoint: string, apiKey: string }} config
|
|
117
|
+
* @param {Array<{ code: string }>} models
|
|
118
|
+
*/
|
|
119
|
+
function applyWorkBuddyConfig(configPath, backup, config, models) {
|
|
120
|
+
if (backup) {
|
|
121
|
+
backupFile(configPath, 'workbuddy')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const managedIds = models.map((m) => m.code)
|
|
125
|
+
const existing = readJsonArray(configPath)
|
|
126
|
+
// 移除同 ID 旧条目后追加最新配置
|
|
127
|
+
const others = existing.filter((m) => !managedIds.includes(m && m.id))
|
|
128
|
+
const merged = [...others, ...buildWorkBuddyModels(config, models)]
|
|
129
|
+
|
|
130
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
131
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf-8')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 删除 WorkBuddy 模型配置(仅移除本软件管理的模型条目)
|
|
136
|
+
* @param {string} configPath
|
|
137
|
+
* @param {Array<{ code: string }>} models
|
|
138
|
+
* @returns {boolean} 是否有文件被处理
|
|
139
|
+
*/
|
|
140
|
+
function deleteWorkBuddyConfig(configPath, models) {
|
|
141
|
+
if (!fs.existsSync(configPath)) return false
|
|
142
|
+
|
|
143
|
+
const managedIds = models.map((m) => m.code)
|
|
144
|
+
// 无托管模型信息时跳过,避免把用户配置原样重写却误报「已删除」
|
|
145
|
+
if (managedIds.length === 0) return false
|
|
146
|
+
|
|
147
|
+
backupFile(configPath, 'workbuddy', '-delete')
|
|
148
|
+
|
|
149
|
+
const existing = readJsonArray(configPath)
|
|
150
|
+
const remaining = existing.filter((m) => !managedIds.includes(m && m.id))
|
|
151
|
+
|
|
152
|
+
if (remaining.length === 0) {
|
|
153
|
+
fs.unlinkSync(configPath)
|
|
154
|
+
} else {
|
|
155
|
+
fs.writeFileSync(configPath, JSON.stringify(remaining, null, 2), 'utf-8')
|
|
156
|
+
}
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 写入 CodeBuddy 模型配置
|
|
162
|
+
* CodeBuddy 格式:{ "models": [...] },合并写入:保留用户其他模型,替换本软件管理的模型
|
|
163
|
+
* @param {string} configPath
|
|
164
|
+
* @param {boolean} backup
|
|
165
|
+
* @param {{ apiEndpoint: string, apiKey: string }} config
|
|
166
|
+
* @param {Array<{ code: string }>} models
|
|
167
|
+
*/
|
|
168
|
+
function applyCodeBuddyConfig(configPath, backup, config, models) {
|
|
169
|
+
if (backup) {
|
|
170
|
+
backupFile(configPath, 'codebuddy')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const managedIds = models.map((m) => m.code)
|
|
174
|
+
const existing = readJsonModelsWrapper(configPath)
|
|
175
|
+
// 移除同 ID 旧条目后追加最新配置
|
|
176
|
+
const others = existing.filter((m) => !managedIds.includes(m && m.id))
|
|
177
|
+
const merged = [...others, ...buildCodeBuddyModels(config, models)]
|
|
178
|
+
|
|
179
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
180
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: merged }, null, 2), 'utf-8')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 删除 CodeBuddy 模型配置(仅移除本软件管理的模型条目)
|
|
185
|
+
* @param {string} configPath
|
|
186
|
+
* @param {Array<{ code: string }>} models
|
|
187
|
+
* @returns {boolean} 是否有文件被处理
|
|
188
|
+
*/
|
|
189
|
+
function deleteCodeBuddyConfig(configPath, models) {
|
|
190
|
+
if (!fs.existsSync(configPath)) return false
|
|
191
|
+
|
|
192
|
+
const managedIds = models.map((m) => m.code)
|
|
193
|
+
// 无托管模型信息时跳过,避免把用户配置原样重写却误报「已删除」
|
|
194
|
+
if (managedIds.length === 0) return false
|
|
195
|
+
|
|
196
|
+
backupFile(configPath, 'codebuddy', '-delete')
|
|
197
|
+
|
|
198
|
+
const existing = readJsonModelsWrapper(configPath)
|
|
199
|
+
const remaining = existing.filter((m) => !managedIds.includes(m && m.id))
|
|
200
|
+
|
|
201
|
+
if (remaining.length === 0) {
|
|
202
|
+
fs.unlinkSync(configPath)
|
|
203
|
+
} else {
|
|
204
|
+
fs.writeFileSync(configPath, JSON.stringify({ models: remaining }, null, 2), 'utf-8')
|
|
205
|
+
}
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 生成通用客户端配置内容(CodeBuddy 等) */
|
|
210
|
+
function generateConfigContent(apiEndpoint, apiKey) {
|
|
211
|
+
return JSON.stringify(
|
|
212
|
+
{
|
|
213
|
+
apiEndpoint,
|
|
214
|
+
apiKey,
|
|
215
|
+
updatedAt: new Date().toISOString(),
|
|
216
|
+
},
|
|
217
|
+
null,
|
|
218
|
+
2
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 写入客户端配置
|
|
224
|
+
* @param {string[]} targets - 目标客户端 key 数组
|
|
225
|
+
* @param {boolean} backup - 是否备份原文件
|
|
226
|
+
* @param {{ apiEndpoint: string, apiKey: string }} config - 配置内容
|
|
227
|
+
* @param {Array<{ code: string }>} models - app.ts modelPage 返回的模型列表
|
|
228
|
+
* @returns {Array<{ target: string, success: boolean, message: string }>}
|
|
229
|
+
*/
|
|
230
|
+
function applyConfig(targets, backup, config, models = []) {
|
|
231
|
+
const results = []
|
|
232
|
+
|
|
233
|
+
for (const targetKey of targets) {
|
|
234
|
+
try {
|
|
235
|
+
const configPath = getClientConfigPath(targetKey)
|
|
236
|
+
if (!configPath) {
|
|
237
|
+
results.push({ target: targetKey, success: false, message: '未知的客户端' })
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 检查客户端是否已安装
|
|
242
|
+
const clients = scanner.scan()
|
|
243
|
+
const client = clients.find((c) => c.key === targetKey)
|
|
244
|
+
if (client && !client.installed) {
|
|
245
|
+
results.push({
|
|
246
|
+
target: targetKey,
|
|
247
|
+
success: false,
|
|
248
|
+
message: `${client.name} 未安装,无法配置`,
|
|
249
|
+
})
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (targetKey === 'workbuddy') {
|
|
254
|
+
// WorkBuddy:写入 ~/.workbuddy/models.json 自定义模型列表(顶层数组格式)
|
|
255
|
+
applyWorkBuddyConfig(configPath, backup, config, models)
|
|
256
|
+
} else if (targetKey === 'codebuddy') {
|
|
257
|
+
// CodeBuddy:写入 ~/.codebuddy/models.json({ "models": [...] } 包装格式)
|
|
258
|
+
applyCodeBuddyConfig(configPath, backup, config, models)
|
|
259
|
+
} else {
|
|
260
|
+
// 其他客户端:写入通用配置格式
|
|
261
|
+
if (backup) {
|
|
262
|
+
backupFile(configPath, targetKey)
|
|
263
|
+
}
|
|
264
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true })
|
|
265
|
+
fs.writeFileSync(configPath, generateConfigContent(config.apiEndpoint, config.apiKey), 'utf-8')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
results.push({
|
|
269
|
+
target: targetKey,
|
|
270
|
+
success: true,
|
|
271
|
+
message: `${client?.name || targetKey} 配置成功`,
|
|
272
|
+
})
|
|
273
|
+
} catch (err) {
|
|
274
|
+
results.push({
|
|
275
|
+
target: targetKey,
|
|
276
|
+
success: false,
|
|
277
|
+
message: `配置失败:${err.message}`,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return results
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 删除客户端配置
|
|
287
|
+
* @param {string[]} targets - 目标客户端 key 数组
|
|
288
|
+
* @param {Array<{ code: string }>} models - app.ts modelPage 返回的模型列表
|
|
289
|
+
* @returns {Array<{ target: string, success: boolean, message: string }>}
|
|
290
|
+
*/
|
|
291
|
+
function deleteConfig(targets, models = []) {
|
|
292
|
+
const results = []
|
|
293
|
+
|
|
294
|
+
for (const targetKey of targets) {
|
|
295
|
+
try {
|
|
296
|
+
const configPath = getClientConfigPath(targetKey)
|
|
297
|
+
if (!configPath) {
|
|
298
|
+
results.push({ target: targetKey, success: false, message: '未知的客户端' })
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (targetKey === 'workbuddy') {
|
|
303
|
+
// WorkBuddy:仅清除本软件写入的模型条目
|
|
304
|
+
const handled = deleteWorkBuddyConfig(configPath, models)
|
|
305
|
+
results.push({
|
|
306
|
+
target: targetKey,
|
|
307
|
+
success: true,
|
|
308
|
+
message: handled ? '配置已删除' : '无托管模型配置,跳过',
|
|
309
|
+
})
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (targetKey === 'codebuddy') {
|
|
314
|
+
// CodeBuddy:仅清除本软件写入的模型条目
|
|
315
|
+
const handled = deleteCodeBuddyConfig(configPath, models)
|
|
316
|
+
results.push({
|
|
317
|
+
target: targetKey,
|
|
318
|
+
success: true,
|
|
319
|
+
message: handled ? '配置已删除' : '无托管模型配置,跳过',
|
|
320
|
+
})
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (fs.existsSync(configPath)) {
|
|
325
|
+
// 备份后删除
|
|
326
|
+
backupFile(configPath, targetKey, '-delete')
|
|
327
|
+
fs.unlinkSync(configPath)
|
|
328
|
+
results.push({ target: targetKey, success: true, message: '配置已删除' })
|
|
329
|
+
} else {
|
|
330
|
+
results.push({ target: targetKey, success: true, message: '无配置文件,跳过' })
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
results.push({
|
|
334
|
+
target: targetKey,
|
|
335
|
+
success: false,
|
|
336
|
+
message: `删除失败:${err.message}`,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 获取目标客户端的最新一次备份文件路径
|
|
346
|
+
* 仅匹配「配置前备份」(如 workbuddy-1787847532450.json);
|
|
347
|
+
* 排除删除备份(workbuddy-delete-xxx.json)——它是删除前的快照(含托管配置),
|
|
348
|
+
* 还原它会把已删除的托管配置写回,导致「删除后配置复活」
|
|
349
|
+
* @param {string} targetKey
|
|
350
|
+
* @returns {string|null}
|
|
351
|
+
*/
|
|
352
|
+
function getLatestBackup(targetKey) {
|
|
353
|
+
const backupDir = getBackupDir()
|
|
354
|
+
if (!fs.existsSync(backupDir)) return null
|
|
355
|
+
|
|
356
|
+
// 严格匹配 `${targetKey}-时间戳.json`,排除 -delete 后缀备份
|
|
357
|
+
const backupPattern = new RegExp(`^${targetKey}-(\\d+)\\.json$`)
|
|
358
|
+
const files = fs
|
|
359
|
+
.readdirSync(backupDir)
|
|
360
|
+
.filter((f) => backupPattern.test(f))
|
|
361
|
+
.sort((a, b) => {
|
|
362
|
+
const tsA = parseInt(a.match(backupPattern)?.[1] || '0', 10)
|
|
363
|
+
const tsB = parseInt(b.match(backupPattern)?.[1] || '0', 10)
|
|
364
|
+
return tsB - tsA
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return files.length > 0 ? path.join(backupDir, files[0]) : null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 还原客户端配置(将最后一次自动备份的原文件复制回配置路径)
|
|
372
|
+
* @param {string[]} targets - 目标客户端 key 数组
|
|
373
|
+
* @returns {Array<{ target: string, success: boolean, message: string }>}
|
|
374
|
+
*/
|
|
375
|
+
function restoreConfig(targets) {
|
|
376
|
+
const results = []
|
|
377
|
+
|
|
378
|
+
for (const targetKey of targets) {
|
|
379
|
+
try {
|
|
380
|
+
const configPath = getClientConfigPath(targetKey)
|
|
381
|
+
if (!configPath) {
|
|
382
|
+
results.push({ target: targetKey, success: false, message: '未知的客户端' })
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const backupPath = getLatestBackup(targetKey)
|
|
387
|
+
if (!backupPath) {
|
|
388
|
+
// 无备份时降级为删除当前配置
|
|
389
|
+
if (fs.existsSync(configPath)) {
|
|
390
|
+
fs.unlinkSync(configPath)
|
|
391
|
+
results.push({ target: targetKey, success: true, message: '无备份,已删除当前配置' })
|
|
392
|
+
} else {
|
|
393
|
+
results.push({ target: targetKey, success: true, message: '无配置文件,跳过' })
|
|
394
|
+
}
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fs.copyFileSync(backupPath, configPath)
|
|
399
|
+
results.push({ target: targetKey, success: true, message: '配置已还原' })
|
|
400
|
+
} catch (err) {
|
|
401
|
+
results.push({ target: targetKey, success: false, message: `还原失败:${err.message}` })
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return results
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** 打开备份目录 */
|
|
409
|
+
async function openBackupDir() {
|
|
410
|
+
const backupDir = getBackupDir()
|
|
411
|
+
fs.mkdirSync(backupDir, { recursive: true })
|
|
412
|
+
await shell.openPath(backupDir)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = { applyConfig, deleteConfig, restoreConfig, openBackupDir, getBackupDir }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备标识生成(硬件指纹)
|
|
3
|
+
* 格式:ST-xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx
|
|
4
|
+
* 持久化到 userData/device-id.json
|
|
5
|
+
*/
|
|
6
|
+
const { app } = require('electron')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const crypto = require('crypto')
|
|
10
|
+
const os = require('os')
|
|
11
|
+
const { execSync } = require('child_process')
|
|
12
|
+
|
|
13
|
+
/** 获取硬件 UUID */
|
|
14
|
+
function getHardwareUUID() {
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform === 'darwin') {
|
|
17
|
+
return execSync(
|
|
18
|
+
'ioreg -d 2 -c IOPlatformExpertDevice | awk -F\\" \'/IOPlatformUUID/{print $4}\'',
|
|
19
|
+
{ encoding: 'utf-8' }
|
|
20
|
+
).trim()
|
|
21
|
+
} else if (process.platform === 'win32') {
|
|
22
|
+
// 优先 wmic(老版本 Windows)
|
|
23
|
+
try {
|
|
24
|
+
const wmicResult = execSync('wmic csproduct get UUID', { encoding: 'utf-8', timeout: 5000 })
|
|
25
|
+
.split('\n')
|
|
26
|
+
.filter((l) => l.trim() && !l.includes('UUID'))[0]
|
|
27
|
+
?.trim()
|
|
28
|
+
if (wmicResult) return wmicResult
|
|
29
|
+
} catch {
|
|
30
|
+
// wmic 不可用(Windows 11 25H2+ 已移除),降级到 PowerShell
|
|
31
|
+
}
|
|
32
|
+
return execSync(
|
|
33
|
+
'powershell -NoProfile -Command "(Get-CimInstance -ClassName Win32_ComputerSystemProduct).UUID"',
|
|
34
|
+
{ encoding: 'utf-8', timeout: 10000 }
|
|
35
|
+
).trim()
|
|
36
|
+
} else if (process.platform === 'linux') {
|
|
37
|
+
return (
|
|
38
|
+
fs.readFileSync('/etc/machine-id', 'utf-8').trim() ||
|
|
39
|
+
fs.readFileSync('/var/lib/dbus/machine-id', 'utf-8').trim()
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// 降级:使用 hostname + networkInterfaces
|
|
44
|
+
}
|
|
45
|
+
return ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 生成设备标识 */
|
|
49
|
+
function generateDeviceId() {
|
|
50
|
+
const uuid = getHardwareUUID()
|
|
51
|
+
const hostname = os.hostname()
|
|
52
|
+
const platform = process.platform
|
|
53
|
+
const nics = Object.values(os.networkInterfaces())
|
|
54
|
+
.flat()
|
|
55
|
+
.filter((ni) => ni && !ni.internal && ni.mac !== '00:00:00:00:00:00')
|
|
56
|
+
.map((ni) => ni.mac)
|
|
57
|
+
.join(',')
|
|
58
|
+
|
|
59
|
+
const raw = `${uuid}|${hostname}|${platform}|${nics}`
|
|
60
|
+
const hash = crypto.createHash('sha256').update(raw).digest('hex')
|
|
61
|
+
|
|
62
|
+
// 格式化为 ST-xxxxxxxx-xxxxxxxx-xxxxxxxx-xxxxxxxx
|
|
63
|
+
const parts = [
|
|
64
|
+
hash.slice(0, 8),
|
|
65
|
+
hash.slice(8, 16),
|
|
66
|
+
hash.slice(16, 24),
|
|
67
|
+
hash.slice(24, 32),
|
|
68
|
+
]
|
|
69
|
+
return `ST-${parts.join('-')}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 持久化文件路径 */
|
|
73
|
+
function getDeviceIdFilePath() {
|
|
74
|
+
return path.join(app.getPath('userData'), 'device-id.json')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 获取设备 ID(不存在则生成并持久化) */
|
|
78
|
+
function getDeviceId() {
|
|
79
|
+
const filePath = getDeviceIdFilePath()
|
|
80
|
+
|
|
81
|
+
// 尝试读取已保存的设备 ID
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(filePath)) {
|
|
84
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
85
|
+
if (data.deviceId && data.deviceId.startsWith('ST-')) {
|
|
86
|
+
return data.deviceId
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// 文件损坏,重新生成
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 生成新设备 ID 并保存
|
|
94
|
+
const deviceId = generateDeviceId()
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
97
|
+
fs.writeFileSync(filePath, JSON.stringify({ deviceId, createdAt: new Date().toISOString() }, null, 2))
|
|
98
|
+
} catch {
|
|
99
|
+
// 保存失败也返回设备 ID(内存中可用)
|
|
100
|
+
}
|
|
101
|
+
return deviceId
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { getDeviceId, generateDeviceId }
|