@yufengtadian/freedom-cli 1.12.18 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/lib/build.js +1 -1
  2. package/lib/security.js +1 -1
  3. package/package.json +5 -1
  4. package/shell/darwin-arm64/freedom-shell +0 -0
  5. package/shell/linux-x64/freedom-shell +0 -0
  6. package/shell/win-x64/freedom-shell.exe +0 -0
  7. package/templates/go/go.mod +1 -7
  8. package/templates/go/go.sum +2 -2
  9. package/templates/go/main.go +22 -8
  10. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  11. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  12. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  13. package/templates/go/pkg/freedom/assets/freedom.js +200 -5
  14. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  15. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  16. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  17. package/templates/go/pkg/freedom/backend_proc.go +291 -53
  18. package/templates/go/pkg/freedom/bridge.go +13 -2
  19. package/templates/go/pkg/freedom/capability.go +77 -0
  20. package/templates/go/pkg/freedom/center_other.go +3 -0
  21. package/templates/go/pkg/freedom/center_windows.go +45 -35
  22. package/templates/go/pkg/freedom/dispatch.go +60 -0
  23. package/templates/go/pkg/freedom/events_windows.go +319 -0
  24. package/templates/go/pkg/freedom/freedom.go +193 -99
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  26. package/templates/go/pkg/freedom/osver_other.go +15 -0
  27. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  28. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  29. package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
  30. package/templates/go/pkg/freedom/security.go +19 -12
  31. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  32. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  33. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  34. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  35. package/templates/go/pkg/freedom/store.go +458 -0
  36. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  37. package/templates/go/pkg/freedom/syscap_other.go +15 -3
  38. package/templates/go/pkg/freedom/syscap_windows.go +228 -36
  39. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  40. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  41. package/templates/go/pkg/freedom/tray_common.go +12 -0
  42. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  43. package/templates/go/pkg/freedom/tray_other.go +2 -2
  44. package/templates/go/pkg/freedom/tray_windows.go +69 -39
  45. package/templates/go/pkg/freedom/updater.go +372 -0
  46. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  47. package/templates/go/pkg/freedom/window_other.go +32 -66
  48. package/templates/go/pkg/freedom/window_windows.go +397 -254
  49. package/templates/go/build_all.txt +0 -0
  50. package/templates/go/build_err.txt +0 -0
  51. package/templates/go/gen_config.go +0 -22
  52. package/templates/go/pkg/freedom/center_windows_test.go +0 -32
  53. package/templates/go/pkg/freedom/security_test.go +0 -153
  54. package/templates/go/temp_vet_test/main.go +0 -40
  55. package/templates/go/test_err.txt +0 -0
  56. package/templates/go/vet_err.txt +0 -1
  57. package/templates/go/vet_err2.txt +0 -2
  58. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  59. package/templates/go/webview_go/CHANGELOG.md +0 -15
  60. package/templates/go/webview_go/LICENSE +0 -22
  61. package/templates/go/webview_go/README.md +0 -50
  62. package/templates/go/webview_go/examples/basic/main.go +0 -12
  63. package/templates/go/webview_go/examples/bind/main.go +0 -38
  64. package/templates/go/webview_go/glue.c +0 -36
  65. package/templates/go/webview_go/go.mod +0 -3
  66. package/templates/go/webview_go/go.sum +0 -0
  67. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  68. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  69. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  70. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  71. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  72. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  73. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  74. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  75. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  77. package/templates/go/webview_go/webview.cc +0 -1
  78. package/templates/go/webview_go/webview.go +0 -481
  79. package/templates/go/webview_go/webview_test.go +0 -50
@@ -7,10 +7,10 @@
7
7
  // 编译三平台。
8
8
  // - backend:后端抽象。任意时刻绑定一个后端,前端 window.freedom.call 全部路由过去。
9
9
  // 内置两种实现:
10
- // * EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
11
- // * ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
12
- // 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
13
- // 换一种后端语言无需改壳与前端。
10
+ // - EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
11
+ // - ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
12
+ // 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
13
+ // 换一种后端语言无需改壳与前端。
14
14
  // - ipc:双向桥接。前端 window.freedom.call(method, ...args) -> Promise;
15
15
  // 后端 app.Emit(event, data) 向所有前端监听器推送事件。
16
16
  // - assets:前端编译产物(Vite 单文件 HTML)通过 go:embed 嵌入二进制,运行时
@@ -21,22 +21,29 @@ import (
21
21
  "encoding/json"
22
22
  "errors"
23
23
  "fmt"
24
+ "os"
24
25
  "sync"
25
- "unsafe"
26
+ "sync/atomic"
26
27
 
27
28
  webview "github.com/webview/webview_go"
28
29
  )
29
30
 
31
+ // Version 是应用版本号,构建期经 -ldflags "-X freedom.Version=x.y.z" 注入
32
+ // (见 build.ps1/build.sh 的 -Version/VERSION)。前端经 os.info 的 appVersion 读取。
33
+ var Version = "dev"
34
+
30
35
  // TitleBarMode 描述窗口标题栏策略。
31
36
  type TitleBarMode string
32
37
 
33
38
  const (
34
- // TitleBarNative 保留系统原生标题栏,标题栏图标与 exe 图标一致。
39
+ // TitleBarNative 保留系统原生标题栏(非默认;默认见 TitleBarFrameless)。
35
40
  TitleBarNative TitleBarMode = "native"
41
+ // TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
42
+ // 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
43
+ TitleBarHidden TitleBarMode = "hidden"
36
44
  // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
37
- // 为框架默认策略(与 CLI 默认一致,见 M1)。
38
- // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
39
- // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
45
+ // 最小化 / 最大化 / 关闭按钮需由前端自绘(通过 window.freedom.window.* 控制),
46
+ // 三平台行为一致。
40
47
  TitleBarFrameless TitleBarMode = "frameless"
41
48
  )
42
49
 
@@ -44,14 +51,15 @@ const (
44
51
  type Config struct {
45
52
  // Title 是窗口标题。
46
53
  Title string
47
- // TitleBar 指定标题栏策略(默认 TitleBarFrameless,与 CLI 默认一致)。可通过 freedom CLI 一键切换。
54
+ // TitleBar 指定标题栏策略(默认 TitleBarFrameless)。可通过 freedom CLI 一键切换。
48
55
  TitleBar TitleBarMode
49
56
  // Width / Height 是窗口初始尺寸(像素)。
50
57
  Width int
51
58
  Height int
52
59
  // Center 为 true 时窗口在屏幕居中(Windows 生效;macOS/Linux 由窗口管理器决定)。
53
60
  Center bool
54
- // MinWidth / MinHeight 为窗口最小尺寸(<=0 表示不限制)。
61
+ // MinWidth / MinHeight 为窗口最小尺寸。两个字段均 >0 时作为一组约束生效
62
+ //(webview 的 HintMin 按整组应用);任一 <=0 表示不启用最小尺寸限制。
55
63
  MinWidth int
56
64
  MinHeight int
57
65
  // Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
@@ -61,17 +69,86 @@ type Config struct {
61
69
  // HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
62
70
  // 为 nil 时使用框架内置的默认占位页。
63
71
  HTML func() (string, error)
72
+ // AppID 是应用标识,决定数据/配置目录名与单实例窗口类标题。
73
+ // 为空时回退可执行文件名(去扩展名)。一经发布不要更改,否则用户数据"丢失"。
74
+ AppID string
75
+ // RememberWindowState 为 true 时窗口位置/尺寸/最大化状态跨启动记忆
76
+ // (存于 <dataDir>/window-state.json;Windows 生效,其他平台暂不持久化)。
77
+ RememberWindowState bool
78
+ // Update 配置自动更新(ed25519 验签 manifest + sha256 校验产物)。
79
+ // 为 nil 时更新能力关闭。见 updater.go。
80
+ Update *UpdateConfig
81
+ // Capabilities 收口前端 sys/tray/window 三桥的可调用面(M3)。
82
+ // nil(默认)全开;语义见 capability.go。os.info 回显生效配置。
83
+ Capabilities *Capabilities
64
84
  }
65
85
 
66
86
  // App 是 Freedom 应用实例。
67
87
  type App struct {
68
- cfg Config
69
- viewMu sync.Mutex // 保护 view 字段(Run 写 / Emit·Quit·WindowHandle 任意 goroutine 读)
70
- view webview.WebView
71
- backend Backend
72
- backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
73
- onReady func(a *App)
74
- secure bool // high 安全模式:资源加密 + 反调试 + 强制关闭 devtools(loadRuntimeConfig 置位)
88
+ cfg Config
89
+ view webview.WebView
90
+ viewMu sync.RWMutex // 保护 view:Emit/Quit/WindowHandle 承诺可从任意 goroutine 调用
91
+ wsMu sync.Mutex // 串行化 window-state 落盘(拖拽异步保存可与销毁保存交叠)
92
+ backend Backend
93
+ onReady func(a *App)
94
+
95
+ // 运行时资源覆盖(resources.go):secure = high 模式(app.bin 解密成功),
96
+ // backendExplicit = 用户经 Bind 显式使用内嵌后端(config.json 的 backend 不得再替换)。
97
+ secure bool
98
+ backendExplicit bool
99
+
100
+ // upMu 保护 upPending:CheckUpdate 验签通过的更新条目,install 桥接只认它
101
+ //(前端无法注入未验签的 URL/哈希)。见 updater.go。
102
+ upMu sync.Mutex
103
+ upPending *UpdateInfo
104
+ upInstalling bool // 单飞:并发 update.install 只放行一个换装
105
+
106
+ // M2 多窗口注册表:windows 含 "main",次级窗口关闭后自行摘除。
107
+ // winMu 同时保护 map、winSeq 与 running 标志(runningMu 并入,减少锁面)。
108
+ winMu sync.Mutex
109
+ windows map[string]*Window
110
+ winSeq int
111
+ running bool
112
+
113
+ // viewTearing 在 Run 的 Destroy 前置位(语义同 Window.tearing):
114
+ // 主窗口已入队的 dispatch 回调会被销毁路径泵出执行,闭包凭此原子量自我作废。
115
+ viewTearing atomic.Bool
116
+ }
117
+
118
+ // appForEvents 指向当前运行的 App,供平台层回调(单实例/热键等)向 Emit 事件。
119
+ // 契约:一个进程只跑一个 App——托盘/消息窗口/热键等平台层状态均为进程级单例,
120
+ // 并发创建多个 App 会让事件串门。
121
+ var appForEvents atomic.Pointer[App]
122
+
123
+ func currentApp() *App { return appForEvents.Load() }
124
+
125
+ // setView 设置主窗口 webview 引用。
126
+ func (a *App) setView(w webview.WebView) {
127
+ a.viewMu.Lock()
128
+ a.view = w
129
+ a.viewMu.Unlock()
130
+ }
131
+
132
+ // withView 在 viewMu 读锁内对主窗口 webview 执行 f;未创建/已摘除时静默跳过。
133
+ // 与 Run 的拆除序列(写锁内摘引用 + Destroy)互斥:调用方拿到引用并 Dispatch 的
134
+ // 窗口期内实例不可能被销毁,杜绝 check-then-destroy 竞态下的 use-after-free。
135
+ func (a *App) withView(f func(view webview.WebView)) {
136
+ a.viewMu.RLock()
137
+ defer a.viewMu.RUnlock()
138
+ if a.view != nil {
139
+ f(a.view)
140
+ }
141
+ }
142
+
143
+ // teardownView 是 Run 的回收 defer 与测试的同构入口:写锁内立拆除旗标、摘引用、
144
+ // Destroy。Destroy 会泵出队列里的 dispatch 回调(闭包读 viewTearing 自我作废,
145
+ // 不得再取 viewMu——回调就跑在本临界区内)。
146
+ func (a *App) teardownView(view webview.WebView) {
147
+ a.viewMu.Lock()
148
+ defer a.viewMu.Unlock()
149
+ a.viewTearing.Store(true)
150
+ a.view = nil
151
+ view.Destroy()
75
152
  }
76
153
 
77
154
  // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
@@ -79,9 +156,9 @@ func New(cfg Config) *App {
79
156
  if cfg.Title == "" {
80
157
  cfg.Title = "Freedom App"
81
158
  }
82
- // M1:Go 侧默认与 CLI 默认(freedom.config.js / build.js 均默认 frameless)统一,
83
- // 避免 resources/config.json 缺失时 Go 走 native 而模板前端自绘标题栏,
84
- // 造成双标题栏错位。
159
+ // M1:默认无边框(frameless),与 freedom-cli 模板默认一致。
160
+ // 此前默认 native 而 CLI 默认 frameless,resources 缺失/配置不完整时
161
+ // 会出现"系统原生标题栏 + 页面自绘标题栏"双标题栏错位。
85
162
  if cfg.TitleBar == "" {
86
163
  cfg.TitleBar = TitleBarFrameless
87
164
  }
@@ -91,11 +168,10 @@ func New(cfg Config) *App {
91
168
  if cfg.Height <= 0 {
92
169
  cfg.Height = 768
93
170
  }
94
- explicit := cfg.Backend != nil
95
- if !explicit {
171
+ if cfg.Backend == nil {
96
172
  cfg.Backend = NewEmbedBackend()
97
173
  }
98
- return &App{cfg: cfg, backend: cfg.Backend, backendExplicit: explicit}
174
+ return &App{cfg: cfg, backend: cfg.Backend, windows: map[string]*Window{}}
99
175
  }
100
176
 
101
177
  // OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
@@ -111,6 +187,9 @@ func (a *App) OnReady(fn func(a *App)) {
111
187
  // - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
112
188
  // 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
113
189
  // - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
190
+ // - 执行线程(M1 起):绑定函数在 worker goroutine 中执行,UI 线程不被
191
+ // 长任务阻塞;多次调用可并发、完成顺序不保证。handler 内调用 Emit 等
192
+ // 线程安全 API 即可回推结果。
114
193
  func (a *App) Bind(name string, fn interface{}) error {
115
194
  eb, ok := a.backend.(*EmbedBackend)
116
195
  if !ok {
@@ -119,9 +198,8 @@ func (a *App) Bind(name string, fn interface{}) error {
119
198
  if err := eb.Bind(name, fn); err != nil {
120
199
  return err
121
200
  }
122
- // B56:用户显式绑定方法 = 显式指定内嵌后端。置位后外部 resources/config.json
123
- // 的 backend 配置不再覆盖(loadRuntimeConfig 仅当 !backendExplicit 时生效),
124
- // 避免"用户 Bind 的方法被 config.json 的进程后端静默替换而全部失效"。
201
+ // 用户显式绑定方法 = 显式指定内嵌后端:resources/config.json 的 backend
202
+ // 配置不再覆盖,避免内嵌方法被进程后端静默替换而全部失效。
125
203
  a.backendExplicit = true
126
204
  return nil
127
205
  }
@@ -133,29 +211,13 @@ func (a *App) Unbind(name string) {
133
211
  }
134
212
  }
135
213
 
136
- // getView / setView 以互斥锁保护 view 字段:Run 在主 goroutine 写,
137
- // Emit/Quit/WindowHandle 可能被任意 goroutine(如后端事件推送)读,
138
- // 无保护时 race detector 报数据竞争。
139
- func (a *App) getView() webview.WebView {
140
- a.viewMu.Lock()
141
- defer a.viewMu.Unlock()
142
- return a.view
143
- }
144
-
145
- func (a *App) setView(v webview.WebView) {
146
- a.viewMu.Lock()
147
- a.view = v
148
- a.viewMu.Unlock()
149
- }
150
-
151
214
  // Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
152
215
  func (a *App) Run() {
153
- // 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
154
- //(CLI build 时写入;缺失则使用编译期/默认配置)。
155
- // 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
216
+ // 通用壳:先加载 exe 同目录 resources/ 覆盖窗口与后端配置(CLI build 写入;
217
+ // 缺失则使用编译期/默认配置)。配置存在但非法时打印告警不中断启动。
156
218
  if err := a.loadRuntimeConfig(); err != nil {
157
- // H2:high 安全模式资源解密/完整性校验失败必须"拒绝运行"——
158
- // 不显示窗口、不回退占位页,防止资源被篡改/替换后静默降级运行。
219
+ // H2:high 模式资源解密/完整性校验失败必须拒绝运行——不显示窗口、
220
+ // 不回退占位页,防止资源被篡改/替换后静默降级运行。
159
221
  var se *secureFatalError
160
222
  if errors.As(err, &se) {
161
223
  fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
@@ -163,12 +225,28 @@ func (a *App) Run() {
163
225
  }
164
226
  fmt.Printf("freedom: warning: %v\n", err)
165
227
  }
166
-
167
- // high 安全模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
228
+ // high 模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
168
229
  if a.secure {
169
230
  antiDebugCheck()
170
231
  }
171
-
232
+ // 平台层回调(单实例转发/热键)经 currentApp 找到本实例推送事件。
233
+ appForEvents.Store(a)
234
+ defer appForEvents.Store(nil)
235
+ // M2:标记运行中(NewWindow 的前置条件)并登记主窗口;退出前回收次级窗口。
236
+ a.winMu.Lock()
237
+ a.running = true
238
+ a.windows[mainWindowID] = &Window{id: mainWindowID, app: a, doneCh: make(chan struct{})}
239
+ a.winMu.Unlock()
240
+ defer func() {
241
+ a.CloseWindows()
242
+ a.winMu.Lock()
243
+ if mw := a.windows[mainWindowID]; mw != nil {
244
+ close(mw.doneCh) // 主窗口随 Run 退出回收:Window("main").WaitClosed() 可等待
245
+ }
246
+ delete(a.windows, mainWindowID)
247
+ a.running = false
248
+ a.winMu.Unlock()
249
+ }()
172
250
  html, err := a.resolveHTML()
173
251
  if err != nil {
174
252
  fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
@@ -177,21 +255,18 @@ func (a *App) Run() {
177
255
 
178
256
  w := webview.New(a.cfg.Debug)
179
257
  if w == nil {
180
- if !webview2Available() {
181
- fmt.Println("freedom: failed to create webview: 未检测到 WebView2 Runtime,请安装 Microsoft Edge WebView2 Runtime(https://go.microsoft.com/fwlink/?linkid=2124703)后重试")
182
- } else {
183
- fmt.Println("freedom: failed to create webview")
184
- }
258
+ fmt.Println("freedom: failed to create webview")
185
259
  return
186
260
  }
187
261
  a.setView(w)
188
262
  defer func() {
189
- _ = a.backend.Close()
190
- // 先清空视图引用再销毁:Destroy 后 C 层 webview 对象已被 delete,
191
- // 若此时仍有并发 goroutine(如后端事件推送触发的 Emit/Quit/WindowHandle)
192
- // 持旧引用调用会访问悬垂指针导致随机崩溃。
193
- a.setView(nil)
194
- w.Destroy()
263
+ if err := a.backend.Close(); err != nil {
264
+ fmt.Fprintf(os.Stderr, "freedom: backend close: %v\n", err)
265
+ }
266
+ a.uninstallWindowEvents()
267
+ // 摘引用与 Destroy 必须同处一个写锁临界区:读锁侧(withView)判空后
268
+ // Dispatch 的窗口期内实例不会被销毁(否则 Emit/Quit/pushResolve UAF)。
269
+ a.teardownView(w)
195
270
  }()
196
271
 
197
272
  // 进程后端:启动后端进程并把其推送的事件转发到前端。
@@ -207,21 +282,24 @@ func (a *App) Run() {
207
282
 
208
283
  w.SetTitle(a.cfg.Title)
209
284
  w.SetSize(a.cfg.Width, a.cfg.Height, webview.HintNone)
210
- // 最小尺寸:任一项 >0 即生效;未设置的一项传 0 表示该维度不限制
211
- //(GTK gtk_widget_set_size_request / Cocoa minSize 对 0 均视为"未指定")。
285
+ // 最小尺寸:任一项 >0 即生效;未设置的一项传 0 表示该维度不限制。
212
286
  if a.cfg.MinWidth > 0 || a.cfg.MinHeight > 0 {
213
287
  w.SetSize(a.cfg.MinWidth, a.cfg.MinHeight, webview.HintMin)
214
288
  }
215
289
  a.applyCenter()
216
290
  a.applyTitleBar()
217
- a.setWindowIcon()
291
+ // 窗口事件与关闭拦截(Windows 经 WndProc 子类化;其他平台 no-op)。
292
+ a.installWindowEvents()
293
+ // 恢复上次退出的窗口几何(仅 Config.RememberWindowState 时生效)。
294
+ a.restoreWindowState()
218
295
 
219
296
  // 注入前端 SDK:window.freedom 全局对象。
220
297
  w.Init(jsSDK)
221
298
 
222
- // 绑定 IPC 桥接入口:前端 window.__freedom_bridge(method, paramsJson)。
223
- // webview_go 的 Bind 会让前端调用返回 Promise,Go 侧结果自动 JSON 序列化回传。
224
- if err := w.Bind("__freedom_bridge", a.bridge); err != nil {
299
+ // 绑定 IPC 桥接入口:前端 window.__freedom_bridge(id, method, paramsJson)。
300
+ // M1 异步化:调用立即返回,handler 在 worker goroutine 执行,结果经
301
+ // __freedom__resolve 回写前端 Promise(见 dispatch.go)。
302
+ if err := w.Bind("__freedom_bridge", a.bridgeAsync); err != nil {
225
303
  fmt.Printf("freedom: failed to bind bridge: %v\n", err)
226
304
  return
227
305
  }
@@ -230,8 +308,13 @@ func (a *App) Run() {
230
308
  fmt.Printf("freedom: failed to bind ping: %v\n", err)
231
309
  return
232
310
  }
233
- // 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用)。
234
- // 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized / isFrameless。
311
+ // 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用 + 运行时窗口能力)。
312
+ // 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized /
313
+ // isFrameless / setPosition / setSize / getPosition / getSize / innerSize / center /
314
+ // setTitle / show / hide / focus / isVisible / isFocused / isMinimized /
315
+ // setAlwaysOnTop / setSkipTaskbar / setResizable / setMaximizable / setMinimizable /
316
+ // setFullscreen / isFullscreen / interceptClose / getInfo。
317
+ // 第二参数为 JSON object 字符串(SDK 始终传入,可解析为空对象)。
235
318
  if err := w.Bind("__freedom_window", a.windowControl); err != nil {
236
319
  fmt.Printf("freedom: failed to bind window control: %v\n", err)
237
320
  return
@@ -239,14 +322,15 @@ func (a *App) Run() {
239
322
  // 框架内置方法:原生系统能力(wails v3 对标层)。
240
323
  // 支持方法:taskbar.*(进度/状态/角标)、window.backdrop/corner/borderColor、
241
324
  // dialog.message/open/save。平台实现分文件:syscap_windows.go / syscap_other.go。
242
- if err := w.Bind("__freedom_sys", a.sysCapCall); err != nil {
325
+ // 绑定入口带 M3 能力闸(见 capability.go)。
326
+ if err := w.Bind("__freedom_sys", a.sysCapGated); err != nil {
243
327
  fmt.Printf("freedom: failed to bind sys capability: %v\n", err)
244
328
  return
245
329
  }
246
330
  // 框架内置方法:系统托盘与原生菜单栏。
247
331
  // 支持方法:tray.create/destroy/tooltip/menu、menu.set。
248
- // 平台实现分文件:tray_windows.go / tray_other.go。
249
- if err := w.Bind("__freedom_tray", a.trayCall); err != nil {
332
+ // 平台实现分文件:tray_windows.go / tray_other.go。绑定入口带 M3 能力闸。
333
+ if err := w.Bind("__freedom_tray", a.trayGated); err != nil {
250
334
  fmt.Printf("freedom: failed to bind tray: %v\n", err)
251
335
  return
252
336
  }
@@ -284,6 +368,12 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
284
368
  }
285
369
  }()
286
370
 
371
+ // 框架内置健康检查:__freedom__ping 同时以 JS 直调全局函数与桥接路由两种方式
372
+ // 暴露(占位页/文档示例走桥接路由),此处兜底保证两条链路行为一致。
373
+ if method == "__freedom__ping" {
374
+ return json.RawMessage(`"pong"`), nil
375
+ }
376
+
287
377
  var params []json.RawMessage
288
378
  if len(paramsJSON) > 0 && paramsJSON != "null" {
289
379
  if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
@@ -291,13 +381,6 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
291
381
  }
292
382
  }
293
383
 
294
- // 框架内置方法特判:__freedom__ping 由 webview Bind 注册为全局函数
295
- // window.__freedom__ping(),同时兼容经 freedom.call()/__freedom_bridge 路由的旧写法,
296
- // 避免模板"测试桥接"自检报"method not bound"。
297
- if method == "__freedom__ping" {
298
- return json.RawMessage(`"pong"`), nil
299
- }
300
-
301
384
  raw, err := a.backend.Handle(method, params)
302
385
  if err != nil {
303
386
  return nil, err
@@ -312,41 +395,45 @@ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage,
312
395
  // Emit 把事件推送到前端。前端通过 window.freedom.on(event, cb) 订阅。
313
396
  // 线程安全:可从任意 goroutine 调用(进程后端推送的事件亦经由本函数)。
314
397
  func (a *App) Emit(event string, data interface{}) {
315
- v := a.getView()
316
- if v == nil {
317
- return
318
- }
319
398
  eb, _ := json.Marshal(event)
320
399
  db, err := json.Marshal(data)
321
400
  if err != nil {
322
401
  db = []byte("null")
323
402
  }
324
403
  js := "window.freedom && window.freedom.emit(" + string(eb) + "," + string(db) + ");"
325
- v.Dispatch(func() {
326
- v.Eval(js)
404
+ a.withView(func(view webview.WebView) {
405
+ view.Dispatch(func() {
406
+ if !a.viewTearing.Load() {
407
+ view.Eval(js)
408
+ }
409
+ })
327
410
  })
411
+ a.emitSecondary(js) // M2:事件广播到全部次级窗口
328
412
  }
329
413
 
330
414
  // Quit 关闭窗口并退出应用。可从任意 goroutine 调用。
331
415
  func (a *App) Quit() {
332
- if v := a.getView(); v != nil {
333
- v.Dispatch(func() {
334
- v.Terminate()
416
+ a.withView(func(view webview.WebView) {
417
+ view.Dispatch(func() {
418
+ if !a.viewTearing.Load() {
419
+ view.Terminate()
420
+ }
335
421
  })
336
- }
422
+ })
337
423
  }
338
424
 
339
425
  // WindowHandle 返回底层原生窗口句柄(Windows 上为 HWND)。
340
426
  func (a *App) WindowHandle() uintptr {
341
- if v := a.getView(); v != nil {
342
- return uintptr(unsafe.Pointer(v.Window()))
343
- }
344
- return 0
427
+ var h uintptr
428
+ a.withView(func(view webview.WebView) {
429
+ h = uintptr(view.Window())
430
+ })
431
+ return h
345
432
  }
346
433
 
347
434
  // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
348
435
  // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
349
- func (a *App) windowControl(action string) (result interface{}, err error) {
436
+ func (a *App) windowControl(action string, paramsJSON string) (result interface{}, err error) {
350
437
  // H1:windowControl 直接经 w.Bind 暴露给前端,不经 bridge 的 recover 兜底;
351
438
  // 平台实现(如 appIcon → dibToPNG 解析越界)一旦 panic 会直接崩掉整个壳进程。
352
439
  // 这里统一加 recover,把 panic 转成错误回传前端(页面 catch 后提示,进程不崩)。
@@ -356,19 +443,26 @@ func (a *App) windowControl(action string) (result interface{}, err error) {
356
443
  err = fmt.Errorf("freedom: window action %q panicked: %v", action, r)
357
444
  }
358
445
  }()
359
- return windowControl(a, action)
446
+ // M3 能力闸先于一切动作处理(拒绝路径不触平台层,无副作用)。
447
+ if err := a.capCheckWindow(action); err != nil {
448
+ return nil, err
449
+ }
450
+ if res, ok, merr := a.windowManage(action, paramsJSON); ok {
451
+ return res, merr
452
+ }
453
+ return windowControl(a.WindowHandle(), action, a.cfg.TitleBar, paramsJSON)
360
454
  }
361
455
 
362
456
  // resolveHTML 依据配置返回页面内容。
363
- // 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
457
+ // 优先级:exe 同目录 resources/(high 模式 app.bin 或 index.html,预编译通用壳)
458
+ // > cfg.HTML(go:embed 源码构建)> 内置占位页。
364
459
  func (a *App) resolveHTML() (string, error) {
365
460
  html, err := loadRuntimeHTML()
366
461
  if err == nil && html != "" {
367
462
  return html, nil
368
463
  }
369
- // H2 双保险:high 安全模式资源解密/完整性校验失败时禁止回退占位页(拒绝运行)。
370
- // 正常路径下 Run 已在 loadRuntimeConfig 捕获 secureFatalError 并提前 return,
371
- // 此处防御未来调用顺序调整导致的静默降级。
464
+ // H2 双保险:high 模式校验失败时禁止回退 cfg.HTML/占位页(拒绝运行)。
465
+ // 正常路径下 Run 已捕获 secureFatalError 提前 return,此处防御调用顺序调整。
372
466
  var se *secureFatalError
373
467
  if errors.As(err, &se) {
374
468
  return "", err