@yufengtadian/freedom-cli 1.12.17 → 1.12.18

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.
@@ -46,30 +46,48 @@ func (a *App) applyCenter() {
46
46
  mi.cbSize = uint32(unsafe.Sizeof(mi))
47
47
  if r1, _, _ := procGetMonitorInfo.Call(mon, uintptr(unsafe.Pointer(&mi))); r1 != 0 {
48
48
  work := mi.rcWork
49
- sw := work.right - work.left
50
- sh := work.bottom - work.top
51
- x := work.left + (sw-int32(a.cfg.Width))/2
52
- y := work.top + (sh-int32(a.cfg.Height))/2
53
- if x < 0 {
54
- x = 0
55
- }
56
- if y < 0 {
57
- y = 0
58
- }
49
+ sw := int(work.right - work.left)
50
+ sh := int(work.bottom - work.top)
51
+ // M5:窗口居中坐标双向 clamp。逻辑抽到 clampCentered 便于无头单测。
52
+ x, y := clampCentered(a.cfg.Width, a.cfg.Height, int(work.left), int(work.top), sw, sh)
59
53
  procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
60
54
  return
61
55
  }
62
56
  }
63
- // 回退:主屏全屏尺寸居中(原实现)
57
+ // 回退:主屏全屏尺寸居中(M5 同双向 clamp,上界不小于下界 0)
64
58
  sw, _, _ := procGetSystemMetrics.Call(smCxScreen)
65
59
  sh, _, _ := procGetSystemMetrics.Call(smCyScreen)
66
- x := int(sw/2) - a.cfg.Width/2
67
- y := int(sh/2) - a.cfg.Height/2
68
- if x < 0 {
69
- x = 0
60
+ // 主屏全屏居中:等价于左上 (0,0) 的工作区,同走 clampCentered 双向 clamp。
61
+ x, y := clampCentered(a.cfg.Width, a.cfg.Height, 0, 0, int(sw), int(sh))
62
+ procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
63
+ }
64
+
65
+ // clampCentered 计算窗口在其所在工作区内的居中左上角坐标,并做双向 clamp:
66
+ // 窗口小于工作区时严格居中;窗口大于工作区时贴左上缘,保证窗口整体落在
67
+ // 工作区内、可被拖拽恢复(M5——此前只 clamp 下界,窗口超屏时上界 work.right-w
68
+ // 越过下界 work.left,上下界互打架会把坐标钳回负值;现上界不小于下界)。
69
+ func clampCentered(winW, winH, left, top, workW, workH int) (int, int) {
70
+ x := left + (workW-winW)/2
71
+ y := top + (workH-winH)/2
72
+ upperX := left + workW - winW
73
+ if upperX < left {
74
+ upperX = left
70
75
  }
71
- if y < 0 {
72
- y = 0
76
+ upperY := top + workH - winH
77
+ if upperY < top {
78
+ upperY = top
73
79
  }
74
- procMoveWindow.Call(hwnd, uintptr(x), uintptr(y), uintptr(a.cfg.Width), uintptr(a.cfg.Height), 1)
80
+ if x < left {
81
+ x = left
82
+ }
83
+ if x > upperX {
84
+ x = upperX
85
+ }
86
+ if y < top {
87
+ y = top
88
+ }
89
+ if y > upperY {
90
+ y = upperY
91
+ }
92
+ return x, y
75
93
  }
@@ -0,0 +1,32 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import "testing"
6
+
7
+ // TestClampCentered 覆盖 M5 双向 clamp 的所有分支:
8
+ // 小窗严格居中 / 超屏贴左上(上下界互打架)/ 副屏负坐标 / 右缘与下缘不越界。
9
+ func TestClampCentered(t *testing.T) {
10
+ cases := []struct {
11
+ name string
12
+ winW, winH, left, top int
13
+ workW, workH, wantX, wantY int
14
+ }{
15
+ {"主屏小窗严格居中", 240, 180, 0, 0, 1920, 1040, 840, 430},
16
+ {"主屏超屏贴左上角", 3000, 2000, 0, 0, 1920, 1040, 0, 0},
17
+ {"副屏负坐标居中", 800, 600, -1920, 0, 1920, 1080, -1360, 240},
18
+ {"右缘不越界", 1900, 1000, 0, 0, 1920, 1080, 10, 40},
19
+ {"窗口恰等于工作区", 1920, 1040, 0, 0, 1920, 1040, 0, 0},
20
+ {"副屏超屏上界回贴左上", 3000, 2000, -1920, 0, 1920, 1040, -1920, 0},
21
+ {"高度触下缘", 200, 1040, 0, 0, 1920, 1040, 860, 0},
22
+ }
23
+ for _, c := range cases {
24
+ t.Run(c.name, func(t *testing.T) {
25
+ x, y := clampCentered(c.winW, c.winH, c.left, c.top, c.workW, c.workH)
26
+ if x != c.wantX || y != c.wantY {
27
+ t.Fatalf("clampCentered(%d,%d,%d,%d,%d,%d) = (%d,%d), want (%d,%d)",
28
+ c.winW, c.winH, c.left, c.top, c.workW, c.workH, x, y, c.wantX, c.wantY)
29
+ }
30
+ })
31
+ }
32
+ }
@@ -18,13 +18,28 @@ import (
18
18
  "path/filepath"
19
19
  )
20
20
 
21
+ // secureFatalError 表示 high 安全模式下资源加载/校验失败(app.bin 存在但读取、
22
+ // 解密或完整性校验不通过)。此类错误必须"拒绝运行"(不显示窗口、不回退占位页),
23
+ // 防止资源被篡改/替换/密钥不匹配后静默降级运行(H2)。
24
+ type secureFatalError struct{ err error }
25
+
26
+ func (e *secureFatalError) Error() string { return e.err.Error() }
27
+ func (e *secureFatalError) Unwrap() error { return e.err }
28
+
29
+ // secureFatal 将 loadSecureResources 的错误包装为安全致命错误。
30
+ // loadSecureResources 已区分:app.bin 不存在时返回 hit=false(非 high 产物),
31
+ // 其余错误均为"存在但读取/解密/校验失败",正是需要拒绝运行的场景。
32
+ func secureFatal(err error) error {
33
+ return &secureFatalError{err: err}
34
+ }
35
+
21
36
  // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
22
37
  // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
23
38
  // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
24
39
  func (a *App) loadRuntimeConfig() error {
25
40
  // high 安全模式:配置与页面封装在加密容器 app.bin 内,优先内存解密加载。
26
41
  if p, hit, err := loadSecureResources(); err != nil {
27
- return err
42
+ return secureFatal(err)
28
43
  } else if hit {
29
44
  var rc runtimeConfigFile
30
45
  if err := json.Unmarshal([]byte(p.Config), &rc); err != nil {
@@ -95,6 +110,12 @@ func (a *App) loadRuntimeConfigPlain() error {
95
110
 
96
111
  // applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
97
112
  func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
113
+ // 窗口标题:config.json 的 title 覆盖编译期默认("Freedom App")。
114
+ // 此前只解析未应用,导致改 freedom.config.js 的 name/title 无效、
115
+ // 打包产物窗口标题永远停留在默认值("无法修改程序名字")。
116
+ if rc.Title != "" {
117
+ a.cfg.Title = rc.Title
118
+ }
98
119
  switch rc.TitleBar {
99
120
  case "native":
100
121
  a.cfg.TitleBar = TitleBarNative
@@ -138,7 +159,7 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
138
159
  // 否则读取 exe 同目录 resources/index.html)。文件不存在时返回空串。
139
160
  func loadRuntimeHTML() (string, error) {
140
161
  if p, hit, err := loadSecureResources(); err != nil {
141
- return "", err
162
+ return "", secureFatal(err)
142
163
  } else if hit {
143
164
  return p.HTML, nil
144
165
  }
@@ -19,6 +19,7 @@ package freedom
19
19
 
20
20
  import (
21
21
  "encoding/json"
22
+ "errors"
22
23
  "fmt"
23
24
  "sync"
24
25
  "unsafe"
@@ -30,9 +31,10 @@ import (
30
31
  type TitleBarMode string
31
32
 
32
33
  const (
33
- // TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
34
+ // TitleBarNative 保留系统原生标题栏,标题栏图标与 exe 图标一致。
34
35
  TitleBarNative TitleBarMode = "native"
35
36
  // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
37
+ // 为框架默认策略(与 CLI 默认一致,见 M1)。
36
38
  // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
37
39
  // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
38
40
  TitleBarFrameless TitleBarMode = "frameless"
@@ -42,7 +44,7 @@ const (
42
44
  type Config struct {
43
45
  // Title 是窗口标题。
44
46
  Title string
45
- // TitleBar 指定标题栏策略(默认 TitleBarNative)。可通过 freedom CLI 一键切换。
47
+ // TitleBar 指定标题栏策略(默认 TitleBarFrameless,与 CLI 默认一致)。可通过 freedom CLI 一键切换。
46
48
  TitleBar TitleBarMode
47
49
  // Width / Height 是窗口初始尺寸(像素)。
48
50
  Width int
@@ -77,8 +79,11 @@ func New(cfg Config) *App {
77
79
  if cfg.Title == "" {
78
80
  cfg.Title = "Freedom App"
79
81
  }
82
+ // M1:Go 侧默认与 CLI 默认(freedom.config.js / build.js 均默认 frameless)统一,
83
+ // 避免 resources/config.json 缺失时 Go 走 native 而模板前端自绘标题栏,
84
+ // 造成双标题栏错位。
80
85
  if cfg.TitleBar == "" {
81
- cfg.TitleBar = TitleBarNative
86
+ cfg.TitleBar = TitleBarFrameless
82
87
  }
83
88
  if cfg.Width <= 0 {
84
89
  cfg.Width = 1024
@@ -149,6 +154,13 @@ func (a *App) Run() {
149
154
  //(CLI build 时写入;缺失则使用编译期/默认配置)。
150
155
  // 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
151
156
  if err := a.loadRuntimeConfig(); err != nil {
157
+ // H2:high 安全模式资源解密/完整性校验失败必须"拒绝运行"——
158
+ // 不显示窗口、不回退占位页,防止资源被篡改/替换后静默降级运行。
159
+ var se *secureFatalError
160
+ if errors.As(err, &se) {
161
+ fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
162
+ return
163
+ }
152
164
  fmt.Printf("freedom: warning: %v\n", err)
153
165
  }
154
166
 
@@ -224,11 +236,38 @@ func (a *App) Run() {
224
236
  fmt.Printf("freedom: failed to bind window control: %v\n", err)
225
237
  return
226
238
  }
227
-
228
- if a.onReady != nil {
229
- a.onReady(a)
239
+ // 框架内置方法:原生系统能力(wails v3 对标层)。
240
+ // 支持方法:taskbar.*(进度/状态/角标)、window.backdrop/corner/borderColor、
241
+ // dialog.message/open/save。平台实现分文件:syscap_windows.go / syscap_other.go。
242
+ if err := w.Bind("__freedom_sys", a.sysCapCall); err != nil {
243
+ fmt.Printf("freedom: failed to bind sys capability: %v\n", err)
244
+ return
245
+ }
246
+ // 框架内置方法:系统托盘与原生菜单栏。
247
+ // 支持方法: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 {
250
+ fmt.Printf("freedom: failed to bind tray: %v\n", err)
251
+ return
252
+ }
253
+ // 框架内置方法:页面就绪回调(H3)。前端 SDK 在 DOMContentLoaded 后调用,
254
+ // 保证 onReady 触发时页面已加载、监听器已注册;此前 onReady 在 SetHtml 前
255
+ // 触发,期间 Emit 的初始化事件因 window.freedom 尚未建立而丢失。
256
+ // 页面可能因导航/重载多次触发 DOMContentLoaded,用 once 兜底只执行一次。
257
+ var readyOnce sync.Once
258
+ if err := w.Bind("__freedom__ready", func() {
259
+ readyOnce.Do(func() {
260
+ if a.onReady != nil {
261
+ a.onReady(a)
262
+ }
263
+ })
264
+ }); err != nil {
265
+ fmt.Printf("freedom: failed to bind ready: %v\n", err)
266
+ return
230
267
  }
231
268
 
269
+ // 注:onReady 不再在 SetHtml 前触发(H3),改由前端 SDK 在页面
270
+ // DOMContentLoaded 后经 __freedom__ready 回调,保证初始化事件不丢失。
232
271
  w.SetHtml(html)
233
272
  w.Run()
234
273
  }
@@ -307,16 +346,33 @@ func (a *App) WindowHandle() uintptr {
307
346
 
308
347
  // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
309
348
  // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
310
- func (a *App) windowControl(action string) (interface{}, error) {
349
+ func (a *App) windowControl(action string) (result interface{}, err error) {
350
+ // H1:windowControl 直接经 w.Bind 暴露给前端,不经 bridge 的 recover 兜底;
351
+ // 平台实现(如 appIcon → dibToPNG 解析越界)一旦 panic 会直接崩掉整个壳进程。
352
+ // 这里统一加 recover,把 panic 转成错误回传前端(页面 catch 后提示,进程不崩)。
353
+ defer func() {
354
+ if r := recover(); r != nil {
355
+ result = nil
356
+ err = fmt.Errorf("freedom: window action %q panicked: %v", action, r)
357
+ }
358
+ }()
311
359
  return windowControl(a, action)
312
360
  }
313
361
 
314
362
  // resolveHTML 依据配置返回页面内容。
315
363
  // 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
316
364
  func (a *App) resolveHTML() (string, error) {
317
- if html, err := loadRuntimeHTML(); err == nil && html != "" {
365
+ html, err := loadRuntimeHTML()
366
+ if err == nil && html != "" {
318
367
  return html, nil
319
368
  }
369
+ // H2 双保险:high 安全模式资源解密/完整性校验失败时禁止回退占位页(拒绝运行)。
370
+ // 正常路径下 Run 已在 loadRuntimeConfig 捕获 secureFatalError 并提前 return,
371
+ // 此处防御未来调用顺序调整导致的静默降级。
372
+ var se *secureFatalError
373
+ if errors.As(err, &se) {
374
+ return "", err
375
+ }
320
376
  if a.cfg.HTML != nil {
321
377
  return a.cfg.HTML()
322
378
  }
@@ -0,0 +1,10 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ import "fmt"
6
+
7
+ // sysCapCall 非 Windows 平台暂不提供系统能力(任务栏/窗口效果/对话框)。
8
+ func (a *App) sysCapCall(method string, paramsJSON string) (interface{}, error) {
9
+ return nil, fmt.Errorf("freedom: sys method %q is not supported on this platform", method)
10
+ }