@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/README.md +42 -2
- package/bin/freedom.js +13 -3
- package/lib/build.js +59 -4
- package/lib/cli.js +53 -11
- package/lib/config.js +12 -1
- package/lib/security.js +150 -0
- package/lib/tui.js +31 -14
- package/lib/update.js +177 -27
- package/package.json +39 -39
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
- package/templates/go/pkg/freedom/center_windows.go +11 -17
- package/templates/go/pkg/freedom/configfile.go +37 -9
- package/templates/go/pkg/freedom/freedom.go +19 -2
- package/templates/go/pkg/freedom/security.go +227 -0
- package/templates/go/pkg/freedom/security_test.go +153 -0
- package/templates/go/pkg/freedom/window_other.go +4 -0
- package/templates/go/pkg/freedom/window_windows.go +20 -0
- package/templates/go/webview_go/webview.go +51 -0
- package/templates/project/freedom.config.js +8 -0
package/lib/update.js
CHANGED
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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; //
|
|
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
|
-
|
|
58
|
+
// ---- 缓存 ----
|
|
59
|
+
|
|
60
|
+
// 读取原始缓存(不校验 TTL),供自动更新频控使用;损坏返回 null
|
|
61
|
+
function readRawCache() {
|
|
38
62
|
try {
|
|
39
63
|
const j = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
108
|
+
done(JSON.parse(body).version || null);
|
|
69
109
|
} catch (e) {
|
|
70
|
-
|
|
110
|
+
done(null);
|
|
71
111
|
}
|
|
72
112
|
});
|
|
73
113
|
});
|
|
74
|
-
req.on('timeout', () => req.destroy());
|
|
75
|
-
req.on('error', () =>
|
|
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
|
-
|
|
132
|
+
// ---- 全局安装判定 ----
|
|
133
|
+
|
|
134
|
+
function npmGlobalRoot() {
|
|
94
135
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
|
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.
|
|
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,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
|
|
36
|
+
if !a.cfg.Center {
|
|
43
37
|
return
|
|
44
38
|
}
|
|
45
|
-
hwnd :=
|
|
39
|
+
hwnd := a.WindowHandle()
|
|
46
40
|
if hwnd == 0 {
|
|
47
41
|
return
|
|
48
42
|
}
|
|
49
|
-
mon, _, _ :=
|
|
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, _, _ :=
|
|
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
|
-
|
|
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, _, _ :=
|
|
71
|
-
sh, _, _ :=
|
|
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
|
-
|
|
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
|
-
//
|
|
52
|
-
|
|
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
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|