@yufengtadian/freedom-cli 1.12.15 → 1.12.17

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/lib/update.js CHANGED
@@ -1,21 +1,42 @@
1
1
  'use strict';
2
2
 
3
- // 版本检测:零依赖(https 内置),查询 npm registry 最新版本并对比本地版本。
4
- // - 结果缓存到 ~/.freedom/update-cache.json,24h 内不重复联网(离线不打扰)
5
- // - compareVersions 手写 semver 比较(仅处理 x.y.z 数字前缀,满足语义版本场景)
6
- // - 所有联网失败均静默降级,绝不阻塞主流程
3
+ // 版本检测与自动更新:零依赖(https 内置)
4
+ //
5
+ // 版本检测:
6
+ // - 查询 npm registry 最新版本并对比本地版本,结果缓存 24h(离线不打扰)
7
+ // 自动更新:
8
+ // - 检测到新版本时自动执行 `npm install -g <pkg>@latest`,无需用户手动升级
9
+ // - 仅当当前包为 npm 全局安装时自动更新(npm link / 本地目录安装仅提示手动命令)
10
+ // - 自动更新带 6h 频控(FREEDOM_AUTO_UPDATE_FORCE=1 可强制),失败静默降级不阻塞主流程
11
+ // 开关:
12
+ // - FREEDOM_AUTO_UPDATE=0 / false / off 可关闭自动更新(仅保留版本提示)
13
+ // 所有联网 / 更新失败均静默降级,绝不阻塞主流程。
14
+ // compareVersions 手写 semver 比较(仅处理 x.y.z 数字前缀,满足语义版本场景)。
7
15
 
8
16
  const fs = require('fs');
9
17
  const os = require('os');
10
18
  const path = require('path');
11
19
  const https = require('https');
20
+ const { spawn, execFileSync } = require('child_process');
12
21
  const { packageRoot } = require('./utils');
13
22
 
14
23
  const PKG_NAME = '@yufengtadian/freedom-cli';
15
24
  const REGISTRY = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
16
25
  const CACHE_FILE = path.join(os.homedir(), '.freedom', 'update-cache.json');
17
- const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 小时
26
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 版本检测缓存 24h
27
+ const AUTO_UPDATE_GAP = 6 * 60 * 60 * 1000; // 自动更新尝试频控 6h
18
28
  const REQUEST_TIMEOUT = 4000;
29
+ const UPDATE_TIMEOUT = 120000; // npm install 超时 120s
30
+
31
+ // 实际安装的包名:优先读 package.json(fork / 改名时自动跟随),兜底 PKG_NAME
32
+ function pkgName() {
33
+ try {
34
+ const pkg = require(path.join(packageRoot(), 'package.json'));
35
+ return typeof pkg.name === 'string' && pkg.name ? pkg.name : PKG_NAME;
36
+ } catch (e) {
37
+ return PKG_NAME;
38
+ }
39
+ }
19
40
 
20
41
  function currentVersion() {
21
42
  return require(path.join(packageRoot(), 'package.json')).version;
@@ -34,11 +55,22 @@ function compareVersions(a, b) {
34
55
  return 0;
35
56
  }
36
57
 
37
- function readCache() {
58
+ // ---- 缓存 ----
59
+
60
+ // 读取原始缓存(不校验 TTL),供自动更新频控使用;损坏返回 null
61
+ function readRawCache() {
38
62
  try {
39
63
  const j = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
40
- if (j && j.latest && Date.now() - j.ts < CACHE_TTL) return j;
41
- } catch (e) { /* 无缓存或损坏,忽略 */ }
64
+ return j && typeof j === 'object' ? j : null;
65
+ } catch (e) {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ // 读取有效缓存:latest 存在且未过期才返回(版本检测用)
71
+ function readCache() {
72
+ const j = readRawCache();
73
+ if (j && j.latest && Date.now() - j.ts < CACHE_TTL) return j;
42
74
  return null;
43
75
  }
44
76
 
@@ -49,30 +81,38 @@ function writeCache(data) {
49
81
  } catch (e) { /* 写缓存失败静默 */ }
50
82
  }
51
83
 
52
- // 从 npm registry 拉取 latest 版本;失败 / 超时返回 null
84
+ function clearCache() {
85
+ try { fs.unlinkSync(CACHE_FILE); } catch (e) { /* 忽略 */ }
86
+ }
87
+
88
+ // ---- 联网检测 ----
89
+ // 从 npm registry 拉取 latest 版本;失败 / 超时返回 null。
90
+ // B54:timeout 时显式 settle,避免 https 请求超时后 Promise 永不 resolve(悬空)。
53
91
  function fetchLatest(timeout = REQUEST_TIMEOUT) {
54
92
  return new Promise((resolve) => {
93
+ let settled = false;
94
+ const done = (v) => { if (!settled) { settled = true; resolve(v); } };
55
95
  const req = https.get(REGISTRY, {
56
96
  headers: { 'user-agent': 'freedom-cli', accept: 'application/json' },
57
97
  timeout,
58
98
  }, (res) => {
59
99
  if (res.statusCode !== 200) {
60
100
  res.resume();
61
- return resolve(null);
101
+ return done(null);
62
102
  }
63
103
  let body = '';
64
104
  res.setEncoding('utf8');
65
105
  res.on('data', (chunk) => { body += chunk; });
66
106
  res.on('end', () => {
67
107
  try {
68
- resolve(JSON.parse(body).version || null);
108
+ done(JSON.parse(body).version || null);
69
109
  } catch (e) {
70
- resolve(null);
110
+ done(null);
71
111
  }
72
112
  });
73
113
  });
74
- req.on('timeout', () => req.destroy());
75
- req.on('error', () => resolve(null));
114
+ req.on('timeout', () => { req.destroy(); done(null); });
115
+ req.on('error', () => done(null));
76
116
  });
77
117
  }
78
118
 
@@ -89,21 +129,131 @@ async function checkUpdate({ force = false } = {}) {
89
129
  return { current, latest, hasUpdate };
90
130
  }
91
131
 
92
- // 静默异步通知:仅在检测到新版本时打印一行升级提示,不阻塞调用方
93
- async function maybeNotifyUpdate() {
132
+ // ---- 全局安装判定 ----
133
+
134
+ function npmGlobalRoot() {
94
135
  try {
95
- const r = await checkUpdate();
96
- if (r.hasUpdate && r.latest) {
97
- const theme = require('./theme');
98
- console.log('');
99
- console.log(` ${theme.paint('➜', theme.C.fg.magenta, theme.C.bold)} ${theme.paint(`新版本可用 ${r.latest}(当前 ${r.current})`, theme.C.fg.yellow)}`);
100
- console.log(` ${theme.dim('运行')} ${theme.paint(`npm install -g ${PKG_NAME}@latest`, theme.C.fg.cyan)} ${theme.dim('升级,或')} ${theme.paint('freedom update', theme.C.fg.cyan)} ${theme.dim('查看详情')}`);
101
- console.log('');
102
- }
103
- } catch (e) { /* 检测失败静默 */ }
136
+ const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
137
+ const out = execFileSync(cmd, ['root', '-g'], {
138
+ encoding: 'utf8',
139
+ timeout: 10000,
140
+ windowsHide: true,
141
+ shell: process.platform === 'win32',
142
+ });
143
+ return out.trim() || null;
144
+ } catch (e) {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ // target 是否位于 root 目录内(含直接子级);任一为空返回 false
150
+ function isPathInside(root, target) {
151
+ if (!root || !target) return false;
152
+ const rel = path.relative(root, target);
153
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
154
+ }
155
+
156
+ // 当前安装是否为 npm 全局安装。
157
+ // 仅全局安装才允许自动更新;npm link / 本地目录安装时仅提示手动命令,
158
+ // 避免自动安装出一份与当前工作副本无关的全局副本(行为不可预期)。
159
+ function isGlobalInstall() {
160
+ const root = npmGlobalRoot();
161
+ if (!root) return false;
162
+ return isPathInside(root, packageRoot());
163
+ }
164
+
165
+ // ---- 自动更新执行 ----
166
+
167
+ // 执行 `npm install -g <pkg>@<latest>`;返回 { ok, detail }
168
+ function performAutoUpdate(latest, { timeoutMs = UPDATE_TIMEOUT } = {}) {
169
+ return new Promise((resolve) => {
170
+ let settled = false;
171
+ const done = (v) => { if (!settled) { settled = true; resolve(v); } };
172
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
173
+ const child = spawn(
174
+ npm,
175
+ ['install', '-g', `${pkgName()}@${latest}`, '--no-audit', '--no-fund', '--loglevel=error'],
176
+ { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, shell: process.platform === 'win32' }
177
+ );
178
+ let out = '';
179
+ let err = '';
180
+ child.stdout.on('data', (d) => { out += d; });
181
+ child.stderr.on('data', (d) => { err += d; });
182
+ const timer = setTimeout(() => {
183
+ try { child.kill(); } catch (e) { /* ignore */ }
184
+ done({ ok: false, detail: `更新超时(${Math.round(timeoutMs / 1000)}s)` });
185
+ }, timeoutMs);
186
+ child.on('error', (e) => { clearTimeout(timer); done({ ok: false, detail: e.message }); });
187
+ child.on('close', (code) => {
188
+ clearTimeout(timer);
189
+ if (code === 0) done({ ok: true, detail: (out.trim() || err.trim() || '') });
190
+ else done({ ok: false, detail: (err.trim() || out.trim() || `npm 退出码 ${code}`) });
191
+ });
192
+ });
193
+ }
194
+
195
+ // 自动更新频控是否到期:距上次尝试超过 AUTO_UPDATE_GAP 才算到期
196
+ function autoUpdateDue(now = Date.now()) {
197
+ const raw = readRawCache();
198
+ if (!raw || !raw.lastAutoAttempt) return true;
199
+ return now - raw.lastAutoAttempt >= AUTO_UPDATE_GAP;
200
+ }
201
+
202
+ // 自动更新完整流程。返回结构化结果供调用方打印。
203
+ // r:可传入预取的 checkUpdate 结果,避免重复联网;force:跳过缓存与频控。
204
+ async function maybeAutoUpdate({ force = false, r = null } = {}) {
205
+ // 开关:FREEDOM_AUTO_UPDATE=0 / false / off 关闭自动更新
206
+ const flag = (process.env.FREEDOM_AUTO_UPDATE || '').toLowerCase();
207
+ if (flag === '0' || flag === 'false' || flag === 'off') return { skipped: 'disabled' };
208
+
209
+ const info = r || (await checkUpdate({ force }));
210
+ if (!info.latest) return { offline: true };
211
+ if (!info.hasUpdate) return { upToDate: true, latest: info.latest };
212
+ if (!isGlobalInstall()) {
213
+ return { notGlobal: true, latest: info.latest, current: info.current };
214
+ }
215
+
216
+ const forceFlag = (process.env.FREEDOM_AUTO_UPDATE_FORCE || '').toLowerCase();
217
+ const skipThrottle = force || forceFlag === '1' || forceFlag === 'true';
218
+ if (!skipThrottle && !autoUpdateDue()) {
219
+ return { throttled: true, latest: info.latest, current: info.current };
220
+ }
221
+
222
+ // 先标记尝试再执行,防止多进程并发 / 失败后风暴重试
223
+ writeCache({ latest: info.latest, lastAutoAttempt: Date.now() });
224
+ const res = await performAutoUpdate(info.latest);
225
+ if (res.ok) {
226
+ clearCache(); // 更新成功后清缓存,下次以新版本为基准重新检测
227
+ return { updated: true, from: info.current, to: info.latest, detail: res.detail };
228
+ }
229
+ return { failed: true, latest: info.latest, current: info.current, detail: res.detail };
230
+ }
231
+
232
+ // 把自动更新结果渲染为终端提示行(供 CLI 打印;TUI 自行拼装)。
233
+ // upToDate / offline / skipped 不产生提示(静默),返回空数组。
234
+ function formatUpdateResult(res, theme) {
235
+ const { paint, C, ok, warn, tip, dim } = theme;
236
+ const lines = [];
237
+ if (res.updated) {
238
+ lines.push(` ${ok(`已自动更新到 v${res.to}(原 v${res.from})。`)}`);
239
+ lines.push(` ${dim('下次运行')} ${paint('freedom', C.fg.cyan)} ${dim('即为新版本。')}`);
240
+ } else if (res.notGlobal) {
241
+ lines.push(` ${warn(`检测到新版本 v${res.latest}(当前 v${res.current})。`)}`);
242
+ lines.push(` ${dim('当前为非全局安装,无法自动更新,请手动执行:')}${paint(`npm install -g ${pkgName()}@latest`, C.fg.cyan, C.bold)}`);
243
+ } else if (res.failed) {
244
+ lines.push(` ${warn('自动更新失败:')}${dim(res.detail || '未知原因')}`);
245
+ lines.push(` ${dim('可稍后运行')} ${paint('freedom update', C.fg.cyan)} ${dim('重试,或手动执行:')}${paint(`npm install -g ${pkgName()}@latest`, C.fg.cyan, C.bold)}`);
246
+ } else if (res.throttled) {
247
+ lines.push(` ${tip(`新版本 v${res.latest} 存在,6h 内已尝试过自动更新,本次跳过。`)}`);
248
+ lines.push(` ${dim('运行')} ${paint('freedom update', C.fg.cyan)} ${dim('可立即强制更新。')}`);
249
+ }
250
+ return lines;
104
251
  }
105
252
 
106
253
  module.exports = {
107
- PKG_NAME, REGISTRY, CACHE_FILE, CACHE_TTL,
108
- currentVersion, compareVersions, checkUpdate, maybeNotifyUpdate,
254
+ PKG_NAME, REGISTRY, CACHE_FILE, CACHE_TTL, AUTO_UPDATE_GAP,
255
+ pkgName, currentVersion, compareVersions, checkUpdate,
256
+ readRawCache, readCache, writeCache, clearCache,
257
+ npmGlobalRoot, isPathInside, isGlobalInstall,
258
+ performAutoUpdate, autoUpdateDue, maybeAutoUpdate, formatUpdateResult,
109
259
  };
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "@yufengtadian/freedom-cli",
3
- "version": "1.12.15",
4
- "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
- "keywords": [
6
- "desktop",
7
- "webview",
8
- "electron-alternative",
9
- "wails",
10
- "tauri",
11
- "frontend",
12
- "cross-platform"
13
- ],
14
- "license": "MIT",
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "bin": {
19
- "freedom": "bin/freedom.js"
20
- },
21
- "files": [
22
- "bin",
23
- "lib",
24
- "shell",
25
- "templates",
26
- "tutorial",
27
- "postinstall.js",
28
- "README.md"
29
- ],
30
- "scripts": {
31
- "postinstall": "node postinstall.js"
32
- },
33
- "engines": {
34
- "node": ">=18"
35
- },
36
- "dependencies": {
37
- "rcedit": "^4.0.1"
38
- }
39
- }
1
+ {
2
+ "name": "@yufengtadian/freedom-cli",
3
+ "version": "1.12.17",
4
+ "description": "Freedom WebView desktop shell packaging tool - no Go toolchain required, one command packs three-platform desktop apps",
5
+ "keywords": [
6
+ "desktop",
7
+ "webview",
8
+ "electron-alternative",
9
+ "wails",
10
+ "tauri",
11
+ "frontend",
12
+ "cross-platform"
13
+ ],
14
+ "license": "MIT",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "bin": {
19
+ "freedom": "bin/freedom.js"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "lib",
24
+ "shell",
25
+ "templates",
26
+ "tutorial",
27
+ "postinstall.js",
28
+ "README.md"
29
+ ],
30
+ "scripts": {
31
+ "postinstall": "node postinstall.js"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "dependencies": {
37
+ "rcedit": "^4.0.1"
38
+ }
39
+ }
Binary file
@@ -0,0 +1,8 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ // 反调试(非 Windows 平台空实现)。
6
+ // macOS/Linux 的调试器检测(ptrace / task_for_pid 等)依赖平台特定 API,
7
+ // 本版本不做;如需加强可在对应平台文件扩展。
8
+ func antiDebugCheck() {}
@@ -0,0 +1,39 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ // 反调试(Windows 实现):high 安全模式下调用。
6
+ // 检测常见用户态调试器,发现时静默退出,防止攻击者在调试器下
7
+ // 单步追踪资源解密 / 密钥派生逻辑。
8
+
9
+ import (
10
+ "os"
11
+ "syscall"
12
+ "unsafe"
13
+ )
14
+
15
+ var (
16
+ kernel32dbg = syscall.NewLazyDLL("kernel32.dll")
17
+ procIsDebuggerPresent = kernel32dbg.NewProc("IsDebuggerPresent")
18
+ procCheckRemoteDebuggerPresent = kernel32dbg.NewProc("CheckRemoteDebuggerPresent")
19
+ )
20
+
21
+ // antiDebugCheck 检测调试器;发现则立即退出进程(退出码 77,无提示)。
22
+ // 仅在 high 安全模式启用时由 freedom.go Run() 调用。
23
+ func antiDebugCheck() {
24
+ if isBeingDebugged() {
25
+ os.Exit(77)
26
+ }
27
+ }
28
+
29
+ // isBeingDebugged 同时检测本地调试器(IsDebuggerPresent)与远程/内核调试器
30
+ // (CheckRemoteDebuggerPresent),任一命中即认为正在被调试。
31
+ func isBeingDebugged() bool {
32
+ r, _, _ := procIsDebuggerPresent.Call()
33
+ if r != 0 {
34
+ return true
35
+ }
36
+ var present int32
37
+ procCheckRemoteDebuggerPresent.Call(uintptr(0), uintptr(unsafe.Pointer(&present)))
38
+ return present != 0
39
+ }
@@ -3,18 +3,9 @@
3
3
  package freedom
4
4
 
5
5
  import (
6
- "syscall"
7
6
  "unsafe"
8
7
  )
9
8
 
10
- var (
11
- user32 = syscall.NewLazyDLL("user32.dll")
12
- getSystemMetrics = user32.NewProc("GetSystemMetrics")
13
- moveWindow = user32.NewProc("MoveWindow")
14
- monitorFromWindow = user32.NewProc("MonitorFromWindow")
15
- getMonitorInfo = user32.NewProc("GetMonitorInfoW")
16
- )
17
-
18
9
  const (
19
10
  smCxScreen = 16
20
11
  smCyScreen = 17
@@ -38,19 +29,22 @@ type monitorInfo struct {
38
29
  // webview_go 未提供 SetPosition,这里通过原生 HWND + MoveWindow 定位。
39
30
  // 优先居中到窗口当前所在监视器的工作区(rcWork):多屏副屏(负坐标 /
40
31
  // 不同分辨率 / 任务栏遮挡)下也能正确居中;API 失败时回退主屏全屏居中。
32
+ //
33
+ // user32 过程句柄(procGetSystemMetrics / procMoveWindow / procMonitorFromWindow /
34
+ // procGetMonitorInfo)统一声明在 window_windows.go,避免两套重复 NewProc。
41
35
  func (a *App) applyCenter() {
42
- if !a.cfg.Center || a.view == nil {
36
+ if !a.cfg.Center {
43
37
  return
44
38
  }
45
- hwnd := uintptr(unsafe.Pointer(a.view.Window()))
39
+ hwnd := a.WindowHandle()
46
40
  if hwnd == 0 {
47
41
  return
48
42
  }
49
- mon, _, _ := monitorFromWindow.Call(hwnd, monitorDefaultToNear)
43
+ mon, _, _ := procMonitorFromWindow.Call(hwnd, monitorDefaultToNear)
50
44
  if mon != 0 {
51
45
  var mi monitorInfo
52
46
  mi.cbSize = uint32(unsafe.Sizeof(mi))
53
- if r1, _, _ := getMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r1 != 0 {
47
+ if r1, _, _ := procGetMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r1 != 0 {
54
48
  work := mi.rcWork
55
49
  sw := work.right - work.left
56
50
  sh := work.bottom - work.top
@@ -62,13 +56,13 @@ func (a *App) applyCenter() {
62
56
  if y < 0 {
63
57
  y = 0
64
58
  }
65
- moveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
59
+ procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
66
60
  return
67
61
  }
68
62
  }
69
63
  // 回退:主屏全屏尺寸居中(原实现)
70
- sw, _, _ := getSystemMetrics.Call(smCxScreen)
71
- sh, _, _ := getSystemMetrics.Call(smCyScreen)
64
+ sw, _, _ := procGetSystemMetrics.Call(smCxScreen)
65
+ sh, _, _ := procGetSystemMetrics.Call(smCyScreen)
72
66
  x := int(sw/2) - a.cfg.Width/2
73
67
  y := int(sh/2) - a.cfg.Height/2
74
68
  if x < 0 {
@@ -77,5 +71,5 @@ func (a *App) applyCenter() {
77
71
  if y < 0 {
78
72
  y = 0
79
73
  }
80
- moveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
74
+ procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
81
75
  }
@@ -18,6 +18,27 @@ import (
18
18
  "path/filepath"
19
19
  )
20
20
 
21
+ // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
22
+ // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
23
+ // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
24
+ func (a *App) loadRuntimeConfig() error {
25
+ // high 安全模式:配置与页面封装在加密容器 app.bin 内,优先内存解密加载。
26
+ if p, hit, err := loadSecureResources(); err != nil {
27
+ return err
28
+ } else if hit {
29
+ var rc runtimeConfigFile
30
+ if err := json.Unmarshal([]byte(p.Config), &rc); err != nil {
31
+ return fmt.Errorf("parse encrypted config: %w", err)
32
+ }
33
+ a.applyRuntimeConfig(&rc)
34
+ // high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
35
+ a.cfg.Debug = false
36
+ a.secure = true
37
+ return nil
38
+ }
39
+ return a.loadRuntimeConfigPlain()
40
+ }
41
+
21
42
  // runtimeBackend 描述 config.json 中的后端进程配置(任意语言,经 stdio NDJSON 桥接)。
22
43
  type runtimeBackend struct {
23
44
  Command string `json:"command"`
@@ -48,10 +69,8 @@ func resourcesDir() (string, error) {
48
69
  return filepath.Join(filepath.Dir(exe), "resources"), nil
49
70
  }
50
71
 
51
- // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
52
- // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
53
- // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
54
- func (a *App) loadRuntimeConfig() error {
72
+ // loadRuntimeConfigPlain 明文配置路径(非 high 模式):读取 resources/config.json。
73
+ func (a *App) loadRuntimeConfigPlain() error {
55
74
  dir, err := resourcesDir()
56
75
  if err != nil {
57
76
  return err
@@ -68,11 +87,14 @@ func (a *App) loadRuntimeConfig() error {
68
87
  if err := json.Unmarshal(data, &rc); err != nil {
69
88
  return fmt.Errorf("parse %s: %w", cfgPath, err)
70
89
  }
71
- if rc.Title != "" {
72
- a.cfg.Title = rc.Title
73
- } else if rc.Name != "" {
74
- a.cfg.Title = rc.Name
90
+ if err := a.applyRuntimeConfig(&rc); err != nil {
91
+ return err
75
92
  }
93
+ return nil
94
+ }
95
+
96
+ // applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
97
+ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
76
98
  switch rc.TitleBar {
77
99
  case "native":
78
100
  a.cfg.TitleBar = TitleBarNative
@@ -112,8 +134,14 @@ func (a *App) loadRuntimeConfig() error {
112
134
  return nil
113
135
  }
114
136
 
115
- // loadRuntimeHTML 读取 exe 同目录 resources/index.html;文件不存在时返回空串。
137
+ // loadRuntimeHTML 返回前端页面内容(high 模式:解密 app.bin 内的 html;
138
+ // 否则读取 exe 同目录 resources/index.html)。文件不存在时返回空串。
116
139
  func loadRuntimeHTML() (string, error) {
140
+ if p, hit, err := loadSecureResources(); err != nil {
141
+ return "", err
142
+ } else if hit {
143
+ return p.HTML, nil
144
+ }
117
145
  dir, err := resourcesDir()
118
146
  if err != nil {
119
147
  return "", err
@@ -69,6 +69,7 @@ type App struct {
69
69
  backend Backend
70
70
  backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
71
71
  onReady func(a *App)
72
+ secure bool // high 安全模式:资源加密 + 反调试 + 强制关闭 devtools(loadRuntimeConfig 置位)
72
73
  }
73
74
 
74
75
  // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
@@ -110,7 +111,14 @@ func (a *App) Bind(name string, fn interface{}) error {
110
111
  if !ok {
111
112
  return fmt.Errorf("freedom: Bind 仅适用于内嵌 Go 后端;当前后端为 %T,方法请在进程后端中注册", a.backend)
112
113
  }
113
- return eb.Bind(name, fn)
114
+ if err := eb.Bind(name, fn); err != nil {
115
+ return err
116
+ }
117
+ // B56:用户显式绑定方法 = 显式指定内嵌后端。置位后外部 resources/config.json
118
+ // 的 backend 配置不再覆盖(loadRuntimeConfig 仅当 !backendExplicit 时生效),
119
+ // 避免"用户 Bind 的方法被 config.json 的进程后端静默替换而全部失效"。
120
+ a.backendExplicit = true
121
+ return nil
114
122
  }
115
123
 
116
124
  // Unbind 移除先前 Bind 的方法(内嵌后端)。
@@ -144,6 +152,11 @@ func (a *App) Run() {
144
152
  fmt.Printf("freedom: warning: %v\n", err)
145
153
  }
146
154
 
155
+ // high 安全模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
156
+ if a.secure {
157
+ antiDebugCheck()
158
+ }
159
+
147
160
  html, err := a.resolveHTML()
148
161
  if err != nil {
149
162
  fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
@@ -152,7 +165,11 @@ func (a *App) Run() {
152
165
 
153
166
  w := webview.New(a.cfg.Debug)
154
167
  if w == nil {
155
- fmt.Println("freedom: failed to create webview")
168
+ if !webview2Available() {
169
+ fmt.Println("freedom: failed to create webview: 未检测到 WebView2 Runtime,请安装 Microsoft Edge WebView2 Runtime(https://go.microsoft.com/fwlink/?linkid=2124703)后重试")
170
+ } else {
171
+ fmt.Println("freedom: failed to create webview")
172
+ }
156
173
  return
157
174
  }
158
175
  a.setView(w)