@yufengtadian/freedom-cli 1.13.2 → 1.13.3
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/LICENSE +40 -0
- package/README.md +44 -14
- package/lib/agents.js +182 -52
- package/lib/build.js +22 -7
- package/lib/cli.js +100 -7
- package/lib/shell.js +165 -61
- package/lib/toolchain.js +339 -0
- package/lib/utils.js +51 -0
- package/lib/verify.js +5 -4
- package/lib/webkit.js +45 -0
- package/package.json +2 -2
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +3 -3
- package/templates/desktop/backend/desktop.mjs +16 -6
- package/templates/desktop/freedom.config.js +4 -1
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +13 -2
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +52 -1
- package/templates/go/pkg/freedom/security.go +57 -7
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +3 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
package/lib/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { init } = require('./init');
|
|
6
6
|
const { build } = require('./build');
|
|
7
7
|
const { setConfig, showConfig } = require('./config');
|
|
8
|
-
const { packageRoot, tutorialFile } = require('./utils');
|
|
8
|
+
const { packageRoot, tutorialFile, normalizePlatform } = require('./utils');
|
|
9
9
|
const theme = require('./theme');
|
|
10
10
|
const update = require('./update');
|
|
11
11
|
|
|
@@ -56,6 +56,11 @@ function help() {
|
|
|
56
56
|
L.push(` ${paint('freedom skill install --agent <key|all> [--dry-run] [--skills-dir <path>]', C.fg.cyan)} ${dim('把 Freedom 使用技能装入 agent 的 skills 目录')}`);
|
|
57
57
|
L.push(` ${paint('freedom mcp install --agent <key|all> [--dry-run] [--config <path> --format json|toml|yaml]', C.fg.cyan)} ${dim('把 Freedom MCP 服务写入 agent 的 MCP 配置(幂等合并、写前 .bak 备份)')}`);
|
|
58
58
|
L.push(` ${paint('freedom mcp serve', C.fg.cyan)} ${dim('stdio MCP 服务本体(一般由 agent 自动拉起,无需手敲)')}`);
|
|
59
|
+
L.push(section('工具链'));
|
|
60
|
+
L.push(` ${paint('freedom toolchain', C.fg.cyan)} ${dim('探测 Go / Rust / C++ 工具链是否就位(已配置好则走缓存、不重复探测)')}`);
|
|
61
|
+
L.push(` ${paint('freedom toolchain install [go,rust,cpp|missing] [--apply]', C.fg.cyan)} ${dim('缺失时自动安装;缺省只打印命令,--apply 才真跑(联网 + 提权)')}`);
|
|
62
|
+
L.push(` ${paint('freedom toolchain optimize [--apply]', C.fg.cyan)} ${dim('自动优化配置:Go GOPROXY 镜像、cargo 源换 rsproxy(幂等 + .bak)')}`);
|
|
63
|
+
L.push(` ${paint('freedom toolchain status --refresh', C.fg.cyan)} ${dim('强制重新探测并刷新缓存(默认缓存 7 天,PATH 变更即失效)')}`);
|
|
59
64
|
L.push(section('版本'));
|
|
60
65
|
L.push(` ${paint('freedom version', C.fg.cyan)} ${dim('显示版本并检测最新版本')}`);
|
|
61
66
|
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('检查新版本并立即自动更新')}`);
|
|
@@ -154,7 +159,7 @@ async function run(argv) {
|
|
|
154
159
|
if (home && !home.startsWith('--')) process.env.FREEDOM_AGENT_HOME = path.resolve(home.replace(/^--home=/, ''));
|
|
155
160
|
if (!sub || sub === 'list') return require('./agents').printMatrix();
|
|
156
161
|
if (sub !== 'install') {
|
|
157
|
-
console.error(`${err(`未知 agents 子命令:${sub}`)} ${dim('可用:list / install --what <mcp|skill> --agent <key|all> [--dry-run]')}`);
|
|
162
|
+
console.error(`${err(`未知 agents 子命令:${sub}`)} ${dim('可用:list / install --what <mcp|skill> --agent <key|all> [--dry-run] [--force]')}`);
|
|
158
163
|
return 1;
|
|
159
164
|
}
|
|
160
165
|
const wi = flags.indexOf('--what');
|
|
@@ -166,6 +171,10 @@ async function run(argv) {
|
|
|
166
171
|
return await runAgents(what === 'skill' ? 'skill' : 'mcp', ['install', ...flags]);
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
case 'toolchain':
|
|
175
|
+
case 'tc':
|
|
176
|
+
return toolchainCommand(rest);
|
|
177
|
+
|
|
169
178
|
case 'skill':
|
|
170
179
|
case 'mcp':
|
|
171
180
|
return await runAgents(cmd, rest);
|
|
@@ -462,21 +471,21 @@ async function runShell(rest) {
|
|
|
462
471
|
case 'download': {
|
|
463
472
|
const plat = rest[1];
|
|
464
473
|
if (!plat) {
|
|
465
|
-
console.error(`${err('用法:')}${paint('freedom shell download <win-x64
|
|
474
|
+
console.error(`${err('用法:')}${paint('freedom shell download <win|mac|linux|all 或 win-x64 / darwin-arm64 / linux-x64 / linux-arm64>', C.fg.cyan)}`);
|
|
466
475
|
return 1;
|
|
467
476
|
}
|
|
468
477
|
const dest = await downloadShell(plat);
|
|
469
|
-
console.log(`${ok('已下载')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
478
|
+
console.log(`${ok('已下载')} ${paint(normalizePlatform(plat) || plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
470
479
|
return 0;
|
|
471
480
|
}
|
|
472
481
|
case 'build': {
|
|
473
482
|
const plat = rest[1];
|
|
474
483
|
if (!plat) {
|
|
475
|
-
console.error(`${err('用法:')}${paint('freedom shell build <win-x64
|
|
484
|
+
console.error(`${err('用法:')}${paint('freedom shell build <win|mac|linux 或 win-x64 / darwin-arm64 / linux-x64 / linux-arm64>', C.fg.cyan)}`);
|
|
476
485
|
return 1;
|
|
477
486
|
}
|
|
478
487
|
const dest = buildShell(plat);
|
|
479
|
-
console.log(`${ok('已编译')} ${paint(plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
488
|
+
console.log(`${ok('已编译')} ${paint(normalizePlatform(plat) || plat, C.fg.magenta, C.bold)} ${dim('壳:')}${paint(dest, C.fg.white)}`);
|
|
480
489
|
console.log(` ${dim('提示:壳已预编译随包分发,一般无需本地编译。')}`);
|
|
481
490
|
return 0;
|
|
482
491
|
}
|
|
@@ -499,7 +508,7 @@ async function runAgents(cmd, rest) {
|
|
|
499
508
|
return await require('./mcp').serve();
|
|
500
509
|
}
|
|
501
510
|
if (sub !== 'install') {
|
|
502
|
-
console.error(`${err('用法:')}${paint(`freedom ${cmd} install --agent <key|all> [--dry-run]`, C.fg.cyan)}`);
|
|
511
|
+
console.error(`${err('用法:')}${paint(`freedom ${cmd} install --agent <key|all> [--dry-run] [--force]`, C.fg.cyan)}`);
|
|
503
512
|
if (cmd === 'mcp') {
|
|
504
513
|
console.error(` ${paint('freedom mcp serve', C.fg.cyan)} ${dim('(stdio MCP 服务,通常由 agent 配置自动拉起)')}`);
|
|
505
514
|
}
|
|
@@ -515,6 +524,7 @@ async function runAgents(cmd, rest) {
|
|
|
515
524
|
config: optVal('config'),
|
|
516
525
|
format: optVal('format'),
|
|
517
526
|
skillsDir: optVal('skills-dir'),
|
|
527
|
+
force: rest.includes('--force'),
|
|
518
528
|
home: optVal('home'),
|
|
519
529
|
});
|
|
520
530
|
return r.code;
|
|
@@ -563,6 +573,89 @@ async function runDmg(rest) {
|
|
|
563
573
|
}
|
|
564
574
|
}
|
|
565
575
|
|
|
576
|
+
// freedom toolchain —— 工具链自动装配:探测(带缓存)/ 安装(默认只打印)/ 配置优化。
|
|
577
|
+
function toolchainCommand(rest) {
|
|
578
|
+
const tc = require('./toolchain');
|
|
579
|
+
const flags = new Set();
|
|
580
|
+
const pos = [];
|
|
581
|
+
let homeArg;
|
|
582
|
+
for (let i = 0; i < rest.length; i++) {
|
|
583
|
+
const a = rest[i];
|
|
584
|
+
if (a === '--home') { homeArg = rest[++i]; continue; }
|
|
585
|
+
if (a.startsWith('--home=')) { homeArg = a.slice('--home='.length); continue; }
|
|
586
|
+
if (a.startsWith('--')) { flags.add(a); continue; }
|
|
587
|
+
pos.push(a);
|
|
588
|
+
}
|
|
589
|
+
const sub = pos[0];
|
|
590
|
+
const opts = {
|
|
591
|
+
refresh: flags.has('--refresh'),
|
|
592
|
+
apply: flags.has('--apply'),
|
|
593
|
+
home: homeArg ? path.resolve(homeArg) : undefined,
|
|
594
|
+
};
|
|
595
|
+
const out = [];
|
|
596
|
+
switch (sub) {
|
|
597
|
+
case undefined:
|
|
598
|
+
case 'status': {
|
|
599
|
+
const { rows, skipped } = tc.status(opts);
|
|
600
|
+
for (const r of rows) {
|
|
601
|
+
if (r.ok) out.push(` ${ok('已就位')} ${paint(r.key, C.fg.cyan, C.bold)} ${dim(r.version + ' ' + r.exe)} ${r.cached ? dim('[缓存]') : dim('[探测]')}`);
|
|
602
|
+
else out.push(` ${err('缺失')} ${paint(r.key, C.fg.cyan, C.bold)} ${dim(r.reason || '未检测到')} ${dim('· ' + r.why)}`);
|
|
603
|
+
}
|
|
604
|
+
out.push('');
|
|
605
|
+
const cacheNote = skipped ? `本轮跳过 ${skipped} 次探测(缓存命中,PATH 变更或 --refresh 即失效)` : '本轮全部实探';
|
|
606
|
+
out.push([' ', dim(cacheNote) + ';', dim('安装:'), paint('freedom toolchain install missing', C.fg.cyan),
|
|
607
|
+
dim('(默认只打印命令,加 --apply 才执行)')].join(' '));
|
|
608
|
+
console.log(out.join('\n'));
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
611
|
+
case 'install': {
|
|
612
|
+
const res = tc.install(pos[1] || 'missing', opts);
|
|
613
|
+
for (const r of res.rows) {
|
|
614
|
+
if (r.action === 'skip') console.log(` ${ok(`[${r.key}] 已就位`)} ${dim(r.version || '')}`);
|
|
615
|
+
else if (r.action === 'manual') console.log(` ${warn(`[${r.key}] 本平台无自动安装方式`)} ${dim(r.note)}`);
|
|
616
|
+
else if (r.action === 'plan') {
|
|
617
|
+
console.log(` ${warn(`[${r.key}] 待安装`)} ${dim('以下为将执行的命令:')}`);
|
|
618
|
+
for (const c of r.commands) console.log(` ${paint(c, C.fg.white)}`);
|
|
619
|
+
} else if (r.action === 'applied') {
|
|
620
|
+
for (const s of r.ran) {
|
|
621
|
+
console.log(` ${s.status === 0 ? ok('[完成]') : err('[失败]')} ${dim(s.command)} ${s.status === 0 ? '' : err(`退出码 ${s.status}`)}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (res.dryRun) {
|
|
626
|
+
console.log('');
|
|
627
|
+
console.log(` ${tip('确认无误后加 --apply 真正执行安装(联网、可能需要管理员权限)。')}`);
|
|
628
|
+
} else {
|
|
629
|
+
console.log(` ${dim('安装后若 PATH 未刷新,重新打开终端或执行 freedom toolchain status --refresh。')}`);
|
|
630
|
+
}
|
|
631
|
+
return res.rows.some((r) => r.action === 'applied' && r.ran.some((s) => s.status !== 0)) ? 1 : 0;
|
|
632
|
+
}
|
|
633
|
+
case 'optimize': {
|
|
634
|
+
const res = tc.optimize(opts);
|
|
635
|
+
for (const r of res.rows) {
|
|
636
|
+
if (r.action === 'skip') console.log(` ${ok(`[${r.key}] 无需优化`)} ${dim(r.note)}`);
|
|
637
|
+
else if (r.action === 'blocked') console.log(` ${warn(`[${r.key}] 跳过`)} ${dim(r.note)}`);
|
|
638
|
+
else if (r.action === 'report') console.log(` ${r.ok ? ok(`[${r.key}] 已就位`) : warn(`[${r.key}] 未就位`)} ${dim(r.note)}`);
|
|
639
|
+
else if (r.action === 'plan') {
|
|
640
|
+
console.log(` ${warn(`[${r.key}] 将优化`)} ${dim(r.from ? `当前:${r.from}` : '')}`);
|
|
641
|
+
for (const c of (r.commands || [])) console.log(` ${paint(c, C.fg.white)}`);
|
|
642
|
+
if (r.file) console.log(` ${dim(r.file)} ${dim('(幂等合并,写前 .bak)')}`);
|
|
643
|
+
} else if (r.action === 'applied') {
|
|
644
|
+
console.log(` ${ok(`[${r.key}] 已优化`)} ${dim(r.to || r.file || '')}${r.backup ? dim(' 备份 ' + r.backup) : ''}${r.status ? err(` 退出码 ${r.status}`) : ''}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (res.dryRun) console.log(`\n ${tip('以上为拟写入内容,未改动任何文件;加 --apply 生效(写入前自动 .bak 备份)。')}`);
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
case 'clear':
|
|
651
|
+
console.log(tc.clearCache(opts) ? `${ok('缓存已清除,下一次 status 将重新探测。')}` : `${dim('无缓存可清除。')}`);
|
|
652
|
+
return 0;
|
|
653
|
+
default:
|
|
654
|
+
console.error(`${err(`未知 toolchain 子命令:${sub}`)} ${dim('可用:status / install [go,rust,cpp|missing] / optimize / clear')}`);
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
566
659
|
function openBrowser(file) {
|
|
567
660
|
const { spawn } = require('child_process');
|
|
568
661
|
const plat = process.platform;
|
package/lib/shell.js
CHANGED
|
@@ -23,7 +23,9 @@ const {
|
|
|
23
23
|
SHELL_EXE_NAME,
|
|
24
24
|
localShellPath,
|
|
25
25
|
nativePlatform,
|
|
26
|
+
normalizePlatform,
|
|
26
27
|
} = require('./utils');
|
|
28
|
+
const { applyWebkitTags } = require('./webkit');
|
|
27
29
|
|
|
28
30
|
// GitHub Releases 下载源(可用环境变量覆盖)。
|
|
29
31
|
// 资产命名约定:freedom-shell-<plat>(单文件二进制,不压缩)。
|
|
@@ -31,6 +33,17 @@ function releaseRepo() {
|
|
|
31
33
|
return process.env.FREEDOM_SHELL_REPO || 'YUfeng12TA/freedom';
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
// 直连下载域与 API 域(测试可指向本地桩服务;默认走 GitHub)。
|
|
37
|
+
function releaseBase() {
|
|
38
|
+
return (process.env.FREEDOM_SHELL_BASE || 'https://github.com').replace(/\/+$/, '');
|
|
39
|
+
}
|
|
40
|
+
function apiBase() {
|
|
41
|
+
return (process.env.FREEDOM_GITHUB_API || 'https://api.github.com').replace(/\/+$/, '');
|
|
42
|
+
}
|
|
43
|
+
function shellAssetName(plat) {
|
|
44
|
+
return `freedom-shell-${plat}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
// 远程壳下载的默认版本:与当前包版本保持一致。
|
|
35
48
|
// 通过环境变量 FREEDOM_SHELL_TAG 可强制覆盖;默认动态读取 package.json 的 version,
|
|
36
49
|
// 避免与包版本漂移(历史曾硬编码 v1.1.10 导致下载到旧壳)。
|
|
@@ -46,7 +59,7 @@ function releaseTag() {
|
|
|
46
59
|
return process.env.FREEDOM_SHELL_TAG || `v${pkgVersion()}`;
|
|
47
60
|
}
|
|
48
61
|
function releaseUrl(plat) {
|
|
49
|
-
return
|
|
62
|
+
return `${releaseBase()}/${releaseRepo()}/releases/download/${releaseTag()}/${shellAssetName(plat)}`;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
// ---- 壳二进制平台格式校验 ----
|
|
@@ -193,13 +206,14 @@ function hasCurl() {
|
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
// 用 curl 下载到 dest;成功返回 null,失败返回错误信息
|
|
196
|
-
function curlDownload(url, dest, proxy) {
|
|
209
|
+
function curlDownload(url, dest, proxy, extraHeaders) {
|
|
197
210
|
const args = [
|
|
198
211
|
'-L', '--fail', '--silent', '--show-error',
|
|
199
212
|
'--connect-timeout', '20', '--max-time', '180', '--retry', '2',
|
|
200
213
|
'--output', dest,
|
|
201
214
|
];
|
|
202
215
|
if (proxy) args.push('--proxy', proxy);
|
|
216
|
+
for (const h of extraHeaders || []) args.push('-H', h);
|
|
203
217
|
if (process.platform === 'win32' && /^https:/i.test(url)) {
|
|
204
218
|
args.push('--ssl-no-revoke'); // 规避 Windows schannel CRYPT_E_REVOCATION_OFFLINE
|
|
205
219
|
}
|
|
@@ -212,18 +226,124 @@ function curlDownload(url, dest, proxy) {
|
|
|
212
226
|
return null;
|
|
213
227
|
}
|
|
214
228
|
|
|
229
|
+
// 平台参数收口:win / mac / linux 等别名归一到平台 key,识别不了给可读错误。
|
|
230
|
+
function requirePlatform(plat) {
|
|
231
|
+
const key = normalizePlatform(plat);
|
|
232
|
+
if (!key) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`未知平台:${plat}。可选:${ALL_PLATFORMS.join(' / ')}` +
|
|
235
|
+
`(也接受 win / mac / linux 别名,可带架构后缀,如 mac-arm64)`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return key;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 单次 HTTP GET 到 Buffer:配了代理就走 curl(原生支持 socks5),否则用内置 fetch。
|
|
242
|
+
// API 端点需要 Accept 头,故 headers 显式传入。
|
|
243
|
+
async function httpGetBuffer(url, headers) {
|
|
244
|
+
const proxy = resolveProxy();
|
|
245
|
+
if (proxy) {
|
|
246
|
+
if (!hasCurl()) throw new Error(`已配置代理 ${proxy} 但本机无 curl`);
|
|
247
|
+
const tmp = path.join(os.tmpdir(), `freedom-dl-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
248
|
+
const err = curlDownload(url, tmp, proxy, Object.entries(headers || {}).map(([k, v]) => `${k}: ${v}`));
|
|
249
|
+
try {
|
|
250
|
+
if (err) throw new Error(err);
|
|
251
|
+
return fs.readFileSync(tmp);
|
|
252
|
+
} finally {
|
|
253
|
+
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const res = await fetch(url, {
|
|
257
|
+
headers: Object.assign({ 'User-Agent': 'freedom-cli' }, headers),
|
|
258
|
+
redirect: 'follow',
|
|
259
|
+
signal: AbortSignal.timeout(60000),
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
262
|
+
return Buffer.from(await res.arrayBuffer());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 从 release 资产列表里挑出本平台的壳资产(需带 API url 字段)。
|
|
266
|
+
function selectReleaseAsset(assets, plat) {
|
|
267
|
+
const name = shellAssetName(plat);
|
|
268
|
+
return (Array.isArray(assets) ? assets : []).find(
|
|
269
|
+
(a) => a && a.name === name && typeof a.url === 'string'
|
|
270
|
+
) || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 经 GitHub Release API 取壳:先按 tag 查 release 拿资产 id,再以 octet-stream 拉字节。
|
|
274
|
+
// 背景(实测):releases/download 会 302 到 S3 签名域,受限网络下响应头拿得到、
|
|
275
|
+
// body 永远不结束;api.github.com 的资产端点由 GitHub 自己代理字节流,可稳定取回。
|
|
276
|
+
async function downloadShellViaApi(plat) {
|
|
277
|
+
const token = process.env.FREEDOM_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
|
|
278
|
+
const auth = token ? { Authorization: `Bearer ${token}` } : {};
|
|
279
|
+
const metaUrl = `${apiBase()}/repos/${releaseRepo()}/releases/tags/${releaseTag()}`;
|
|
280
|
+
let meta;
|
|
281
|
+
try {
|
|
282
|
+
meta = JSON.parse((await httpGetBuffer(metaUrl, Object.assign({ Accept: 'application/json' }, auth))).toString('utf8'));
|
|
283
|
+
} catch (e) {
|
|
284
|
+
throw new Error(`查询 release 元数据失败(${metaUrl}):${e.message}`);
|
|
285
|
+
}
|
|
286
|
+
const asset = selectReleaseAsset(meta && meta.assets, plat);
|
|
287
|
+
if (!asset) {
|
|
288
|
+
throw new Error(`${releaseTag()} 中没有资产 ${shellAssetName(plat)}`);
|
|
289
|
+
}
|
|
290
|
+
return await httpGetBuffer(asset.url, Object.assign({ Accept: 'application/octet-stream' }, auth));
|
|
291
|
+
}
|
|
292
|
+
|
|
215
293
|
// 下载指定平台壳到包内 shell/<plat>/
|
|
216
294
|
// 返回下载后的绝对路径;失败抛错。
|
|
295
|
+
// 两条路径:直连 releases/download(有代理时经 curl)→ 失败回退 Release API 资产端点。
|
|
217
296
|
async function downloadShell(plat) {
|
|
218
|
-
|
|
219
|
-
throw new Error(`未知平台:${plat}。可选:${ALL_PLATFORMS.join(' / ')}`);
|
|
220
|
-
}
|
|
297
|
+
plat = requirePlatform(plat);
|
|
221
298
|
const url = releaseUrl(plat);
|
|
222
299
|
const dest = localShellPath(plat);
|
|
223
300
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
224
301
|
|
|
302
|
+
let buf = null;
|
|
303
|
+
let primaryErr = null;
|
|
304
|
+
try {
|
|
305
|
+
buf = await fetchShellDirect(plat, url);
|
|
306
|
+
} catch (e) {
|
|
307
|
+
primaryErr = e;
|
|
308
|
+
process.stdout.write(
|
|
309
|
+
`[freedom] 直连下载壳 ${plat} 未完成(${String(e.message).split('\n')[0]}),回退 GitHub API 资产端点…\n`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!buf) {
|
|
313
|
+
try {
|
|
314
|
+
buf = await downloadShellViaApi(plat);
|
|
315
|
+
} catch (apiErr) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`下载壳 ${plat} 失败:直连与 Release API 两条路径都不通。\n` +
|
|
318
|
+
` 直连 ${url}:${primaryErr ? primaryErr.message : '未返回数据'}\n` +
|
|
319
|
+
` API ${apiBase()}/repos/${releaseRepo()}:${apiErr.message}\n` +
|
|
320
|
+
`可设置代理重试(FREEDOM_SHELL_PROXY=socks5h://127.0.0.1:10808),` +
|
|
321
|
+
`或手动将壳二进制放入 ${dest}。`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// B13:下载后、落盘前校验格式/架构,防止代理劫持返回错误页或假壳被静默分发。
|
|
327
|
+
const fmt = detectShellFormat(buf);
|
|
328
|
+
const want = expectedFormat(plat);
|
|
329
|
+
if (fmt === 'unknown' || formatFamily(fmt) !== formatFamily(want)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`下载的壳格式异常:期望 ${want}(${plat}),实际 detect=${fmt}。` +
|
|
332
|
+
`下载地址 ${url} 可能返回了错误页或非本平台假壳,请检查 GitHub Release 资产 ` +
|
|
333
|
+
`${releaseRepo()} 的 ${releaseTag()}。已放弃本次写入。`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
fs.writeFileSync(dest, buf);
|
|
337
|
+
if (process.platform !== 'win32') {
|
|
338
|
+
fs.chmodSync(dest, 0o755);
|
|
339
|
+
}
|
|
340
|
+
return dest;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 直连路径:代理在位时用 curl(原生支持 socks5/http 代理),否则用内置 fetch。
|
|
344
|
+
// 返回内容 Buffer;网络失败、超时、非 2xx 一律抛错(由 downloadShell 决定回退)。
|
|
345
|
+
async function fetchShellDirect(plat, url, dest) {
|
|
225
346
|
const proxy = resolveProxy();
|
|
226
|
-
let buf;
|
|
227
347
|
if (proxy) {
|
|
228
348
|
process.stdout.write(`[freedom] 下载壳 ${plat}(经代理 ${proxy})<- ${url}\n`);
|
|
229
349
|
if (!hasCurl()) {
|
|
@@ -238,74 +358,40 @@ async function downloadShell(plat) {
|
|
|
238
358
|
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
239
359
|
throw new Error(
|
|
240
360
|
`${curlErr}\n下载地址 ${url} 不可达。` +
|
|
241
|
-
`请确认代理 ${proxy} 可用(可执行 curl --proxy ${proxy} ${url} -I
|
|
242
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
361
|
+
`请确认代理 ${proxy} 可用(可执行 curl --proxy ${proxy} ${url} -I 自测)。`
|
|
243
362
|
);
|
|
244
363
|
}
|
|
245
364
|
try {
|
|
246
|
-
|
|
365
|
+
return fs.readFileSync(tmp);
|
|
247
366
|
} finally {
|
|
248
367
|
try { fs.unlinkSync(tmp); } catch (e) { /* 清理失败忽略 */ }
|
|
249
368
|
}
|
|
250
|
-
} else {
|
|
251
|
-
process.stdout.write(`[freedom] 下载壳 ${plat} <- ${url}\n`);
|
|
252
|
-
let res;
|
|
253
|
-
try {
|
|
254
|
-
// 30s 超时:网络挂起时明确报错,避免构建进程无限阻塞
|
|
255
|
-
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30000) });
|
|
256
|
-
} catch (e) {
|
|
257
|
-
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
258
|
-
throw new Error(
|
|
259
|
-
`下载壳 ${plat} 超时(30s)。请检查网络后重试,` +
|
|
260
|
-
`或设置代理变量 FREEDOM_SHELL_PROXY=socks5h://127.0.0.1:10808 重试,` +
|
|
261
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
throw new Error(
|
|
265
|
-
`下载壳 ${plat} 失败:${e.message}。请检查网络(必要时设置 FREEDOM_SHELL_PROXY 走代理),` +
|
|
266
|
-
`或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
if (!res.ok) {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`下载壳失败:HTTP ${res.status}。请确认 GitHub 仓库 ${releaseRepo()} 已发布 ` +
|
|
272
|
-
`${releaseTag()} 的资产 freedom-shell-${plat},或手动将壳二进制放入 ${localShellPath(plat)}。`
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
buf = Buffer.from(await res.arrayBuffer());
|
|
276
369
|
}
|
|
277
370
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
fs.writeFileSync(dest, buf);
|
|
289
|
-
if (process.platform !== 'win32') {
|
|
290
|
-
fs.chmodSync(dest, 0o755);
|
|
371
|
+
process.stdout.write(`[freedom] 下载壳 ${plat} <- ${url}\n`);
|
|
372
|
+
let res;
|
|
373
|
+
try {
|
|
374
|
+
// 30s 超时:网络挂起时明确报错,避免构建进程无限阻塞
|
|
375
|
+
res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30000) });
|
|
376
|
+
} catch (e) {
|
|
377
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
378
|
+
throw new Error('下载超时(30s)');
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`请求失败:${e.message}`);
|
|
291
381
|
}
|
|
292
|
-
|
|
382
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
383
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
384
|
+
if (!buf.length) throw new Error('响应体为空');
|
|
385
|
+
return buf;
|
|
293
386
|
}
|
|
294
387
|
|
|
295
388
|
// 本地用 Go 编译指定平台壳(需要 Go + 该平台编译环境)。
|
|
296
389
|
// Windows 产物以 GUI 子系统编译(-H windowsgui),运行时无 cmd 黑窗。
|
|
297
390
|
function buildShell(plat) {
|
|
298
|
-
|
|
299
|
-
throw new Error(`未知平台:${plat}。可选:${ALL_PLATFORMS.join(' / ')}`);
|
|
300
|
-
}
|
|
301
|
-
const res = spawnSync('go', ['version'], { encoding: 'utf8' });
|
|
302
|
-
if (res.error || res.status !== 0) {
|
|
303
|
-
throw new Error('未检测到 Go 工具链。请先安装 Go(https://go.dev/dl/),或改用 freedom shell download。');
|
|
304
|
-
}
|
|
305
|
-
const dest = localShellPath(plat);
|
|
306
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
307
|
-
|
|
391
|
+
plat = requirePlatform(plat);
|
|
308
392
|
// 本机只能编译本机平台(webview_go 依赖系统 WebView 框架,无法交叉编译)。
|
|
393
|
+
// 这道判断刻意前置于 Go 探测:平台不匹配是与工具有关的硬事实,先报工具有关
|
|
394
|
+
// 的错误会把「装个 Go 也没用」的场景误导成装 Go 能解决。
|
|
309
395
|
// 统一走 utils.nativePlatform:Intel Mac 会明确抛"已不支持",避免两套映射语义不一(B44)。
|
|
310
396
|
const native = nativePlatform();
|
|
311
397
|
if (plat !== native) {
|
|
@@ -314,19 +400,37 @@ function buildShell(plat) {
|
|
|
314
400
|
`请在目标平台执行 freedom shell build ${plat},或用 freedom shell download ${plat} 拉取 CI 预编译产物。`
|
|
315
401
|
);
|
|
316
402
|
}
|
|
403
|
+
const res = spawnSync('go', ['version'], { encoding: 'utf8' });
|
|
404
|
+
if (res.error || res.status !== 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
'未检测到 Go 工具链。执行 freedom toolchain install go 自动安装(默认只打印命令,加 --apply 才执行),' +
|
|
407
|
+
'或手动获取 https://go.dev/dl/ ,或改用 freedom shell download 拉取预编译壳。'
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const dest = localShellPath(plat);
|
|
411
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
317
412
|
|
|
318
413
|
const buildDir = goTemplateDir();
|
|
319
414
|
// 剥离符号与调试信息(-s -w)+ 抹掉构建期绝对路径(-trimpath):
|
|
320
415
|
// 通用壳里编译进了 high 模式的密钥派生逻辑,函数名/DWARF 是给逆向者的地图。
|
|
321
416
|
const ldflags = ['-s', '-w'];
|
|
322
417
|
if (plat.startsWith('win')) ldflags.push('-H', 'windowsgui');
|
|
418
|
+
// Linux:新发行版只有 webkit2gtk-4.1,需要 -tags webkit2_41(探测与注入见 lib/webkit.js)。
|
|
419
|
+
const webkit = applyWebkitTags(process.env);
|
|
420
|
+
if (webkit.tags.length) {
|
|
421
|
+
process.stdout.write(`[freedom] 本机 WebKitGTK 仅有 4.1,构建加 -tags ${webkit.tags.join(',')}\n`);
|
|
422
|
+
}
|
|
323
423
|
const build = spawnSync('go', ['build', '-trimpath', '-ldflags', ldflags.join(' '), '-o', dest, '.'], {
|
|
324
424
|
cwd: buildDir,
|
|
325
425
|
encoding: 'utf8',
|
|
326
|
-
env: {
|
|
426
|
+
env: Object.assign({ CGO_ENABLED: '1' }, webkit.env),
|
|
327
427
|
});
|
|
328
428
|
if (build.error || build.status !== 0) {
|
|
329
|
-
|
|
429
|
+
const hint = plat.startsWith('linux') && /webkit2gtk-4\.0/.test(`${build.stdout}${build.stderr}`)
|
|
430
|
+
? `\n提示:本机未装 webkit2gtk-4.0/4.1 开发库。Ubuntu/Debian 执行 ` +
|
|
431
|
+
`sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev(老发行版用 4.0)后重试。`
|
|
432
|
+
: '';
|
|
433
|
+
throw new Error(`Go 编译失败:\n${build.stdout}\n${build.stderr}${hint}`);
|
|
330
434
|
}
|
|
331
435
|
return dest;
|
|
332
436
|
}
|