@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.
Files changed (55) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +44 -14
  3. package/lib/agents.js +182 -52
  4. package/lib/build.js +22 -7
  5. package/lib/cli.js +100 -7
  6. package/lib/shell.js +165 -61
  7. package/lib/toolchain.js +339 -0
  8. package/lib/utils.js +51 -0
  9. package/lib/verify.js +5 -4
  10. package/lib/webkit.js +45 -0
  11. package/package.json +2 -2
  12. package/shell/linux-x64/freedom-shell +0 -0
  13. package/shell/win-x64/freedom-shell.exe +0 -0
  14. package/skill/freedom/SKILL.md +3 -3
  15. package/templates/desktop/backend/desktop.mjs +16 -6
  16. package/templates/desktop/freedom.config.js +4 -1
  17. package/templates/go/go.mod +3 -0
  18. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  19. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  20. package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
  21. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  22. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  23. package/templates/go/pkg/freedom/events_windows.go +5 -5
  24. package/templates/go/pkg/freedom/freedom.go +13 -2
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  26. package/templates/go/pkg/freedom/resources.go +52 -1
  27. package/templates/go/pkg/freedom/security.go +57 -7
  28. package/templates/go/pkg/freedom/shutdown.go +55 -0
  29. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  30. package/templates/go/pkg/freedom/store.go +3 -3
  31. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  32. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  33. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  34. package/templates/go/pkg/freedom/updater.go +3 -3
  35. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  36. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  37. package/templates/go/third_party/webview_go/LICENSE +22 -0
  38. package/templates/go/third_party/webview_go/README.md +50 -0
  39. package/templates/go/third_party/webview_go/glue.c +36 -0
  40. package/templates/go/third_party/webview_go/go.mod +3 -0
  41. package/templates/go/third_party/webview_go/go.sum +0 -0
  42. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  43. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  44. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  45. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  46. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  47. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  48. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  49. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  50. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  51. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  52. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  53. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  54. package/templates/go/third_party/webview_go/webview.cc +1 -0
  55. 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|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
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|darwin-arm64|linux-x64|linux-arm64>', C.fg.cyan)}`);
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 `https://github.com/${releaseRepo()}/releases/download/${releaseTag()}/freedom-shell-${plat}`;
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
- if (!ALL_PLATFORMS.includes(plat)) {
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
- buf = fs.readFileSync(tmp);
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
- // B13:下载后、落盘前校验格式/架构,防止代理劫持返回错误页或假壳被静默分发。
279
- const fmt = detectShellFormat(buf);
280
- const want = expectedFormat(plat);
281
- if (fmt === 'unknown' || formatFamily(fmt) !== formatFamily(want)) {
282
- throw new Error(
283
- `下载的壳格式异常:期望 ${want}(${plat}),实际 detect=${fmt}。` +
284
- `下载地址 ${url} 可能返回了错误页或非本平台假壳,请检查 GitHub Release 资产 ` +
285
- `${releaseRepo()} 的 ${releaseTag()}。已放弃本次写入。`
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
- return dest;
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
- if (!ALL_PLATFORMS.includes(plat)) {
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: { ...process.env, CGO_ENABLED: '1' },
426
+ env: Object.assign({ CGO_ENABLED: '1' }, webkit.env),
327
427
  });
328
428
  if (build.error || build.status !== 0) {
329
- throw new Error(`Go 编译失败:\n${build.stdout}\n${build.stderr}`);
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
  }