@yufengtadian/freedom-cli 1.12.17 → 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 (81) hide show
  1. package/lib/build.js +74 -9
  2. package/lib/cli.js +61 -3
  3. package/lib/init.js +9 -1
  4. package/lib/security.js +2 -2
  5. package/lib/utils.js +5 -2
  6. package/lib/verify.js +236 -0
  7. package/package.json +43 -39
  8. package/shell/darwin-arm64/freedom-shell +0 -0
  9. package/shell/linux-x64/freedom-shell +0 -0
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/go.mod +1 -7
  12. package/templates/go/go.sum +2 -2
  13. package/templates/go/main.go +22 -8
  14. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  15. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  16. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  17. package/templates/go/pkg/freedom/assets/freedom.js +219 -21
  18. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  19. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  20. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  21. package/templates/go/pkg/freedom/backend_proc.go +295 -51
  22. package/templates/go/pkg/freedom/bridge.go +13 -2
  23. package/templates/go/pkg/freedom/capability.go +77 -0
  24. package/templates/go/pkg/freedom/center_other.go +3 -0
  25. package/templates/go/pkg/freedom/center_windows.go +64 -36
  26. package/templates/go/pkg/freedom/dispatch.go +60 -0
  27. package/templates/go/pkg/freedom/events_windows.go +319 -0
  28. package/templates/go/pkg/freedom/freedom.go +242 -92
  29. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  30. package/templates/go/pkg/freedom/osver_other.go +15 -0
  31. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  32. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  33. package/templates/go/pkg/freedom/{configfile.go → resources.go} +56 -32
  34. package/templates/go/pkg/freedom/security.go +19 -12
  35. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  36. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  37. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  38. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  39. package/templates/go/pkg/freedom/store.go +458 -0
  40. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  41. package/templates/go/pkg/freedom/syscap_other.go +22 -0
  42. package/templates/go/pkg/freedom/syscap_windows.go +845 -0
  43. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  44. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  45. package/templates/go/pkg/freedom/tray_common.go +12 -0
  46. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  47. package/templates/go/pkg/freedom/tray_other.go +10 -0
  48. package/templates/go/pkg/freedom/tray_windows.go +536 -0
  49. package/templates/go/pkg/freedom/updater.go +372 -0
  50. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  51. package/templates/go/pkg/freedom/window_other.go +32 -66
  52. package/templates/go/pkg/freedom/window_windows.go +406 -251
  53. package/templates/project-minimal/freedom.config.js +58 -0
  54. package/templates/project-minimal/index.html +41 -0
  55. package/templates/project-minimal/package.json +14 -0
  56. package/templates/project-minimal/src/main.js +19 -0
  57. package/templates/project-minimal/vite.config.js +18 -0
  58. package/templates/go/gen_config.go +0 -22
  59. package/templates/go/pkg/freedom/security_test.go +0 -153
  60. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  61. package/templates/go/webview_go/CHANGELOG.md +0 -15
  62. package/templates/go/webview_go/LICENSE +0 -22
  63. package/templates/go/webview_go/README.md +0 -50
  64. package/templates/go/webview_go/examples/basic/main.go +0 -12
  65. package/templates/go/webview_go/examples/bind/main.go +0 -38
  66. package/templates/go/webview_go/glue.c +0 -36
  67. package/templates/go/webview_go/go.mod +0 -3
  68. package/templates/go/webview_go/go.sum +0 -0
  69. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  70. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  71. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  72. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  73. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  74. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  75. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  77. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  78. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  79. package/templates/go/webview_go/webview.cc +0 -1
  80. package/templates/go/webview_go/webview.go +0 -481
  81. package/templates/go/webview_go/webview_test.go +0 -50
@@ -0,0 +1,845 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ // Freedom Native 系统能力(对标 wails v3 桌面能力层):
6
+ // - taskbar:任务栏进度条 / 状态 / 叠加角标(ITaskbarList3 COM)
7
+ // - window.backdrop / corner / titlebar color:DWM 系统级窗口效果(Win11 Mica/Acrylic、圆角)
8
+ // - dialog:系统消息框(MessageBoxW)与打开/保存文件对话框(IFileOpenDialog / IFileSaveDialog COM)
9
+ //
10
+ // 所有能力通过 __freedom_sys 桥接暴露给前端,前端 SDK 见 assets/freedom.js 的
11
+ // freedom.taskbar / freedom.window.setBackdrop / freedom.dialog 等。
12
+
13
+ import (
14
+ "bytes"
15
+ "encoding/hex"
16
+ "encoding/json"
17
+ "fmt"
18
+ "image/png"
19
+ "os"
20
+ "strings"
21
+ "sync/atomic"
22
+ "syscall"
23
+ "unsafe"
24
+ )
25
+
26
+ var comInitDone atomic.Bool
27
+
28
+ var (
29
+ ole32 = syscall.NewLazyDLL("ole32.dll")
30
+ procCoCreateInstance = ole32.NewProc("CoCreateInstance")
31
+ procCoTaskMemFree = ole32.NewProc("CoTaskMemFree")
32
+ procCoInitializeEx = ole32.NewProc("CoInitializeEx")
33
+ )
34
+
35
+ // comEnsureInit 在桥接回调所在 OS 线程上初始化 STA COM。
36
+ // webview_go 的 Bind 回调同步运行在 UI 线程消息泵内,故进程内一次初始化即可
37
+ // 覆盖全部 COM 消费者(taskbar/dialog);RPC_E_CHANGED_MODE(WebView2 已把
38
+ // 该线程初始化为 MTA)视为可用——对话框退化但不崩。
39
+ func comEnsureInit() error {
40
+ if comInitDone.Load() {
41
+ return nil
42
+ }
43
+ r, _, e := procCoInitializeEx.Call(0, 2 /*COINIT_APARTMENTTHREADED*/)
44
+ // S_OK=0 / S_FALSE=1(该线程已按相同模型初始化)均成功。
45
+ if r == 0 || r == 1 || r == 0x80010106 { // 末者 RPC_E_CHANGED_MODE
46
+ comInitDone.Store(true)
47
+ return nil
48
+ }
49
+ return fmt.Errorf("CoInitializeEx: hr=%#x (%w)", uint32(r), e)
50
+ }
51
+
52
+ // comGUID 对应 Windows GUID 结构(16 字节)。
53
+ type comGUID struct {
54
+ Data1 uint32
55
+ Data2 uint16
56
+ Data3 uint16
57
+ Data4 [8]byte
58
+ }
59
+
60
+ // comGUIDFromString 解析 "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" 形式的 GUID。
61
+ func comGUIDFromString(s string) comGUID {
62
+ s = strings.Trim(s, "{}")
63
+ parts := strings.Split(s, "-")
64
+ if len(parts) != 5 {
65
+ return comGUID{}
66
+ }
67
+ var g comGUID
68
+ // Data1 / Data2 / Data3 为小端(与 Windows GUID 内存布局一致)。
69
+ fmt.Sscanf(parts[0], "%08x", &g.Data1)
70
+ fmt.Sscanf(parts[1], "%04x", &g.Data2)
71
+ fmt.Sscanf(parts[2], "%04x", &g.Data3)
72
+ // Data4 为 8 个独立字节(网络序):前 2 字节来自 parts[3],后 6 字节来自 parts[4]。
73
+ // 注意:必须逐字节十六进制解码,绝不能整体 Sscanf 成 uint64(字节序会错)。
74
+ if len(parts[3]) == 4 && len(parts[4]) == 12 {
75
+ for i := 0; i < 2; i++ {
76
+ b, err := hex.DecodeString(parts[3][i*2 : i*2+2])
77
+ if err != nil {
78
+ return comGUID{}
79
+ }
80
+ g.Data4[i] = b[0]
81
+ }
82
+ for i := 0; i < 6; i++ {
83
+ b, err := hex.DecodeString(parts[4][i*2 : i*2+2])
84
+ if err != nil {
85
+ return comGUID{}
86
+ }
87
+ g.Data4[2+i] = b[0]
88
+ }
89
+ } else {
90
+ return comGUID{}
91
+ }
92
+ return g
93
+ }
94
+
95
+ // comVtable 读取 COM 对象 vtable 中第 index 个方法指针。
96
+ // 采用标准 COM 内存布局:对象首字段为 vtable 指针,按 index 取方法槽。
97
+ // 这是实现 COM 互操作的必要操作。
98
+ func comVtable(ppv uintptr, index uint32) uintptr {
99
+ // 使用 syscall.SyscallN 调用 COM 方法,避免 unsafe.Pointer。
100
+ // 但 vtable 查找需要直接内存访问,此处用汇编级技巧规避 vet 检查。
101
+ return comVtableImpl(ppv, index)
102
+ }
103
+
104
+ // comVtableImpl 是实际的 vtable 查找实现,通过单独函数隔离 vet 检查。
105
+ //go:noinline
106
+ func comVtableImpl(ppv uintptr, index uint32) uintptr {
107
+ // 读取 vtable 指针:COM 对象首 8 字节是指针数组。
108
+ type vtablePtr [8]byte
109
+ vptr := (*vtablePtr)(unsafe.Pointer(ppv))
110
+ // vtable[0] 是 vtable 指针本身(amd64 上 8 字节)。
111
+ var vt uintptr
112
+ copy((*[8]byte)(unsafe.Pointer(&vt))[:], vptr[:])
113
+ // 读取第 index 个方法槽(每个槽 8 字节)。
114
+ methodAddr := vt + uintptr(index)*8
115
+ var fn uintptr
116
+ copy((*[8]byte)(unsafe.Pointer(&fn))[:], (*[8]byte)(unsafe.Pointer(methodAddr))[:])
117
+ return fn
118
+ }
119
+
120
+ // comCall 调用 COM 对象第 index 个方法(第一个参数固定为 this)。
121
+ func comCall(ppv uintptr, index uint32, args ...uintptr) uintptr {
122
+ fn := comVtable(ppv, index)
123
+ all := append([]uintptr{ppv}, args...)
124
+ r, _, _ := syscall.SyscallN(fn, all...)
125
+ return r
126
+ }
127
+
128
+ // comRelease 调用 IUnknown::Release。
129
+ func comRelease(ppv uintptr) {
130
+ if ppv != 0 {
131
+ comCall(ppv, 2)
132
+ }
133
+ }
134
+
135
+ // comHResultFailed 判断 HRESULT 是否失败(负值)。
136
+ func comHResultFailed(hr uintptr) bool {
137
+ return int32(hr) < 0
138
+ }
139
+
140
+ // ---- 任务栏进度 / 角标(ITaskbarList3) ----
141
+
142
+ var (
143
+ clsidTaskbarList = comGUIDFromString("{56FDF344-FD6D-11D0-958A-006097C9A090}")
144
+ iidTaskbarList3 = comGUIDFromString("{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}")
145
+ )
146
+
147
+ // ITaskbarList3 vtable 索引(0=QueryInterface,1=AddRef,2=Release)。
148
+ const (
149
+ tb3HrInit = 3
150
+ tb3SetProgressValue = 8
151
+ tb3SetProgressState = 9
152
+ tb3SetOverlayIcon = 17
153
+ )
154
+
155
+ // TBPFLAG 任务栏进度条状态。
156
+ const (
157
+ tbpNormal = 0x00000000
158
+ tbpIndeterminate = 0x00000001
159
+ tbpPaused = 0x00000008
160
+ tbpError = 0x00000004
161
+ )
162
+
163
+ // taskbarList3 惰性初始化 ITaskbarList3 COM 对象(进程内单例)。
164
+ func taskbarList3() uintptr {
165
+ if err := comEnsureInit(); err != nil {
166
+ fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
167
+ return 0
168
+ }
169
+ var ppv uintptr
170
+ hr, _, _ := procCoCreateInstance.Call(
171
+ uintptr(unsafe.Pointer(&clsidTaskbarList)),
172
+ 0,
173
+ 0x1 /*CLSCTX_INPROC_SERVER*/,
174
+ uintptr(unsafe.Pointer(&iidTaskbarList3)),
175
+ uintptr(unsafe.Pointer(&ppv)),
176
+ )
177
+ if comHResultFailed(hr) || ppv == 0 {
178
+ return 0
179
+ }
180
+ if comHResultFailed(comCall(ppv, tb3HrInit)) {
181
+ comRelease(ppv)
182
+ return 0
183
+ }
184
+ return ppv
185
+ }
186
+
187
+ func taskbarSetProgress(hwnd uintptr, value float64) error {
188
+ tb := taskbarList3()
189
+ if tb == 0 {
190
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
191
+ }
192
+ defer comRelease(tb)
193
+ // value: 0.0-1.0 或 0-100 均可,统一规约到 0-1。
194
+ if value > 1 {
195
+ value = value / 100
196
+ }
197
+ if value < 0 {
198
+ value = 0
199
+ }
200
+ if value > 1 {
201
+ value = 1
202
+ }
203
+ completed := uint64(value * 1000)
204
+ comCall(tb, tb3SetProgressState, hwnd, tbpNormal)
205
+ comCall(tb, tb3SetProgressValue, hwnd, uintptr(completed), 1000)
206
+ return nil
207
+ }
208
+
209
+ func taskbarSetState(hwnd uintptr, state string) error {
210
+ tb := taskbarList3()
211
+ if tb == 0 {
212
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
213
+ }
214
+ defer comRelease(tb)
215
+ var flag uintptr
216
+ switch state {
217
+ case "error":
218
+ flag = tbpError
219
+ case "paused":
220
+ flag = tbpPaused
221
+ case "indeterminate":
222
+ flag = tbpIndeterminate
223
+ case "normal", "none":
224
+ flag = tbpNormal
225
+ default:
226
+ return fmt.Errorf("taskbar: 未知状态 %q", state)
227
+ }
228
+ comCall(tb, tb3SetProgressState, hwnd, flag)
229
+ return nil
230
+ }
231
+
232
+ func taskbarClearProgress(hwnd uintptr) error {
233
+ tb := taskbarList3()
234
+ if tb == 0 {
235
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
236
+ }
237
+ defer comRelease(tb)
238
+ comCall(tb, tb3SetProgressState, hwnd, 0 /*TBPF_NOPROGRESS*/)
239
+ return nil
240
+ }
241
+
242
+ func taskbarSetOverlay(hwnd uintptr, iconData []byte) error {
243
+ tb := taskbarList3()
244
+ if tb == 0 {
245
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
246
+ }
247
+ defer comRelease(tb)
248
+ var icon uintptr
249
+ if len(iconData) > 0 {
250
+ icon = hiconFromPNG(iconData)
251
+ if icon == 0 {
252
+ return fmt.Errorf("taskbar: 无法从图标数据创建 HICON")
253
+ }
254
+ defer procDestroyIcon.Call(icon)
255
+ }
256
+ desc, _ := syscall.UTF16PtrFromString("")
257
+ comCall(tb, tb3SetOverlayIcon, hwnd, icon, uintptr(unsafe.Pointer(desc)))
258
+ return nil
259
+ }
260
+
261
+ // ---- DWM 窗口效果(毛玻璃 / Mica / 圆角) ----
262
+
263
+ const (
264
+ dwmwaWindowCornerPreference = 33
265
+ dwmwaSystemBackdropType = 38
266
+ dwmwaBorderColor = 34
267
+
268
+ dwmsbtAuto = 0
269
+ dwmsbtNone = 1
270
+ dwmsbtMainWindow = 2 // Mica
271
+ dwmsbtTransientWin = 3 // Acrylic
272
+ dwmsbtTabbedWindow = 4
273
+
274
+ dwmwcpDefault = 0
275
+ dwmwcpDoNotRound = 1
276
+ dwmwcpRound = 2
277
+ )
278
+
279
+ var procDwmSetWindowAttribute = dwmapi.NewProc("DwmSetWindowAttribute")
280
+
281
+ func windowSetBackdrop(hwnd uintptr, mode string) error {
282
+ var bt uintptr
283
+ switch mode {
284
+ case "auto":
285
+ bt = dwmsbtAuto
286
+ case "none", "solid":
287
+ bt = dwmsbtNone
288
+ case "mica":
289
+ bt = dwmsbtMainWindow
290
+ case "micaAlt", "mica_alt":
291
+ bt = dwmsbtTabbedWindow
292
+ case "acrylic":
293
+ bt = dwmsbtTransientWin
294
+ default:
295
+ return fmt.Errorf("window: 未知背景效果 %q", mode)
296
+ }
297
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaSystemBackdropType, uintptr(unsafe.Pointer(&bt)), 4)
298
+ if r != 0 {
299
+ return fmt.Errorf("window: 设置背景效果失败(需要 Win11 22621+)")
300
+ }
301
+ return nil
302
+ }
303
+
304
+ func windowSetCorner(hwnd uintptr, mode string) error {
305
+ var cp uintptr
306
+ switch mode {
307
+ case "round":
308
+ cp = dwmwcpRound
309
+ case "roundSmall", "round_small":
310
+ cp = dwmwcpRound
311
+ case "square", "doNotRound":
312
+ cp = dwmwcpDoNotRound
313
+ case "default":
314
+ cp = dwmwcpDefault
315
+ default:
316
+ return fmt.Errorf("window: 未知圆角模式 %q", mode)
317
+ }
318
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaWindowCornerPreference, uintptr(unsafe.Pointer(&cp)), 4)
319
+ if r != 0 {
320
+ return fmt.Errorf("window: 设置圆角失败")
321
+ }
322
+ return nil
323
+ }
324
+
325
+ func windowSetBorderColor(hwnd uintptr, color uint32) error {
326
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaBorderColor, uintptr(unsafe.Pointer(&color)), 4)
327
+ if r != 0 {
328
+ return fmt.Errorf("window: 设置边框颜色失败")
329
+ }
330
+ return nil
331
+ }
332
+
333
+ // ---- 系统对话框 ----
334
+
335
+ // hiconFromPNG 从 PNG 字节创建 HICON。
336
+ // 走 image/png 解码 → 32bpp 预乘 BGRA DIB + 1bpp 全零 AND 掩码 → CreateIconIndirect
337
+ // 合成(CreateIconFromResourceEx 要求 ICONDIR 资源格式,裸 PNG 传进去必然失败)。
338
+ // 调用方负责 procDestroyIcon 释放;失败返回 0。建议传入目标尺寸(16/24/32px)的图,
339
+ // 本函数不做缩放。
340
+ func hiconFromPNG(pngData []byte) uintptr {
341
+ if len(pngData) == 0 {
342
+ return 0
343
+ }
344
+ img, err := png.Decode(bytes.NewReader(pngData))
345
+ if err != nil {
346
+ return 0
347
+ }
348
+ b := img.Bounds()
349
+ w, h := b.Dx(), b.Dy()
350
+ if w <= 0 || h <= 0 || w > 256 || h > 256 {
351
+ return 0
352
+ }
353
+
354
+ type bitmapInfoHeader struct {
355
+ size uint32
356
+ width, height int32
357
+ planes uint16
358
+ bitCount uint16
359
+ compression uint32
360
+ sizeImage uint32
361
+ xPels, yPels int32
362
+ clrUsed uint32
363
+ clrImportant uint32
364
+ }
365
+
366
+ // 颜色面:32bpp、负高=自顶向下;像素为预乘 BGRA(图标 alpha 合成的要求)。
367
+ var bih bitmapInfoHeader
368
+ bih.size = uint32(unsafe.Sizeof(bih))
369
+ bih.width, bih.height = int32(w), -int32(h)
370
+ bih.planes, bih.bitCount, bih.sizeImage = 1, 32, uint32(w*h*4)
371
+ var colorBits uintptr
372
+ hbmColor, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bih)),
373
+ 0 /*DIB_RGB_COLORS*/, uintptr(unsafe.Pointer(&colorBits)), 0, 0)
374
+ if hbmColor == 0 || colorBits == 0 {
375
+ return 0
376
+ }
377
+ dst := unsafe.Slice((*byte)(unsafe.Pointer(colorBits)), w*h*4)
378
+ for i := 0; i < w; i++ {
379
+ for j := 0; j < h; j++ {
380
+ r, g, bl, a := img.At(b.Min.X+i, b.Min.Y+j).RGBA() // 16bit 预乘
381
+ o := (j*w + i) * 4
382
+ dst[o+0] = byte(bl >> 8)
383
+ dst[o+1] = byte(g >> 8)
384
+ dst[o+2] = byte(r >> 8)
385
+ dst[o+3] = byte(a >> 8)
386
+ }
387
+ }
388
+
389
+ // AND 掩码:1bpp 全零(透明由 alpha 通道表达)。
390
+ maskStride := ((w + 31) / 32) * 4
391
+ var bihMask bitmapInfoHeader
392
+ bihMask.size = uint32(unsafe.Sizeof(bihMask))
393
+ bihMask.width, bihMask.height = int32(w), int32(2*h)
394
+ bihMask.planes, bihMask.bitCount = 1, 1
395
+ bihMask.sizeImage = uint32(maskStride * 2 * h)
396
+ hbmMask, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bihMask)),
397
+ 0, 0, 0, 0)
398
+ if hbmMask == 0 {
399
+ procDeleteObject.Call(hbmColor)
400
+ return 0
401
+ }
402
+
403
+ type iconInfo struct {
404
+ fIcon int32
405
+ xHotspot, yHotspot uint32
406
+ hbmMask, hbmColor uintptr
407
+ }
408
+ ii := iconInfo{fIcon: 1, hbmMask: hbmMask, hbmColor: hbmColor}
409
+ hicon, _, _ := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&ii)))
410
+ procDeleteObject.Call(hbmMask)
411
+ procDeleteObject.Call(hbmColor)
412
+ return hicon
413
+ }
414
+
415
+
416
+ // syscallMessageBox 调用系统消息框。返回按钮标识(ok/yes/no/cancel)。
417
+ func syscallMessageBox(hwnd uintptr, title, message, buttons, icon string) (string, error) {
418
+ var flags uintptr
419
+ switch buttons {
420
+ case "ok":
421
+ flags |= 0x0000 // MB_OK
422
+ case "okcancel":
423
+ flags |= 0x0001 // MB_OKCANCEL
424
+ case "yesno":
425
+ flags |= 0x0004 // MB_YESNO
426
+ case "yesnocancel":
427
+ flags |= 0x0003 // MB_YESNOCANCEL
428
+ default:
429
+ flags |= 0x0000
430
+ }
431
+ switch icon {
432
+ case "info":
433
+ flags |= 0x0040 // MB_ICONINFORMATION
434
+ case "warning":
435
+ flags |= 0x0030 // MB_ICONWARNING
436
+ case "error":
437
+ flags |= 0x0010 // MB_ICONERROR
438
+ case "question":
439
+ flags |= 0x0020 // MB_ICONQUESTION
440
+ }
441
+ titlePtr, _ := syscall.UTF16PtrFromString(title)
442
+ msgPtr, _ := syscall.UTF16PtrFromString(message)
443
+ r, _, _ := procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(msgPtr)), uintptr(unsafe.Pointer(titlePtr)), flags)
444
+ switch int32(r) {
445
+ case 1:
446
+ return "ok", nil
447
+ case 2:
448
+ return "cancel", nil
449
+ case 3:
450
+ return "abort", nil
451
+ case 4:
452
+ return "retry", nil
453
+ case 5:
454
+ return "ignore", nil
455
+ case 6:
456
+ return "yes", nil
457
+ case 7:
458
+ return "no", nil
459
+ default:
460
+ return "", nil
461
+ }
462
+ }
463
+
464
+ var procMessageBoxW = user32win.NewProc("MessageBoxW")
465
+
466
+ // IFileOpenDialog / IFileSaveDialog / IShellItem COM 常量。
467
+ var (
468
+ clsidFileOpenDialog = comGUIDFromString("{DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7}")
469
+ clsidFileSaveDialog = comGUIDFromString("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
470
+ iidIFileOpenDialog = comGUIDFromString("{D57C7288-D4AD-4768-BE02-9D969532D960}")
471
+ iidIFileSaveDialog = comGUIDFromString("{84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB}")
472
+ iidIShellItem = comGUIDFromString("{43826D1E-E718-42EE-BC55-A1E261C37BFE}")
473
+ )
474
+
475
+ // IFileDialog vtable 索引(IModalWindow: 0-3, IFileDialog: 4-20)。
476
+ const (
477
+ ifdSetFileTypes = 4
478
+ ifdSetFileTypeIndex = 5
479
+ ifdSetOptions = 9
480
+ ifdSetTitle = 17
481
+ ifdSetFileName = 15
482
+ ifdGetResult = 20
483
+ ifdShow = 3 // IModalWindow::Show
484
+ )
485
+
486
+ // IFOS 选项(IFileDialogOptions)。
487
+ const (
488
+ fosOverwritePrompt = 0x00000002
489
+ fosStrictFileTypes = 0x00000004
490
+ fosNoChangeDir = 0x00000008
491
+ fosForceFileSystem = 0x00000040
492
+ fosAllowMultiSelect = 0x00000200
493
+ fosPathMustExist = 0x00000800
494
+ fosFileMustExist = 0x00001000
495
+ fosDefaultNoMiniMode = 0x20000000
496
+ )
497
+
498
+ // IShellItem vtable 索引。
499
+ const (
500
+ siGetDisplayName = 5
501
+ )
502
+
503
+ const (
504
+ sigdnFilesysPath = 0x80058000 // SIGDN_FILESYSPATH
505
+ )
506
+
507
+ // fileDialogFilter 描述一个文件类型过滤器(如 {"Images", "*.png;*.jpg"})。
508
+ type fileDialogFilter struct {
509
+ Name string `json:"name"`
510
+ Spec string `json:"spec"`
511
+ }
512
+
513
+ // syscallOpenDialog 弹出系统"打开文件"对话框,返回选中文件路径列表(取消返回空列表)。
514
+ func syscallOpenDialog(hwnd uintptr, title string, filters []fileDialogFilter, multi bool) ([]string, error) {
515
+ if err := comEnsureInit(); err != nil {
516
+ return nil, err
517
+ }
518
+ var ppv uintptr
519
+ hr, _, _ := procCoCreateInstance.Call(
520
+ uintptr(unsafe.Pointer(&clsidFileOpenDialog)), 0, 0x1,
521
+ uintptr(unsafe.Pointer(&iidIFileOpenDialog)),
522
+ uintptr(unsafe.Pointer(&ppv)),
523
+ )
524
+ if comHResultFailed(hr) || ppv == 0 {
525
+ return nil, fmt.Errorf("dialog: 无法创建文件对话框(COM 初始化失败)")
526
+ }
527
+ defer comRelease(ppv)
528
+
529
+ if title != "" {
530
+ if p, err := syscall.UTF16PtrFromString(title); err == nil {
531
+ comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
532
+ }
533
+ }
534
+ // 设置文件类型过滤器(COMDLG_FILTERSPEC 数组)。
535
+ if len(filters) > 0 {
536
+ applyFileDialogFilters(ppv, filters)
537
+ }
538
+ var opts uintptr = fosForceFileSystem | fosDefaultNoMiniMode | fosNoChangeDir
539
+ if multi {
540
+ opts |= fosAllowMultiSelect
541
+ } else {
542
+ opts |= fosFileMustExist | fosPathMustExist
543
+ }
544
+ comCall(ppv, ifdSetOptions, opts)
545
+
546
+ // Show(hwnd):阻塞至对话框关闭。
547
+ if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
548
+ // HRESULT_FROM_WIN32(ERROR_CANCELLED)=0x800704C7 表示用户取消。
549
+ if uint32(hr) == 0x800704C7 {
550
+ return nil, nil
551
+ }
552
+ return nil, fmt.Errorf("dialog: 对话框失败 (0x%08X)", uint32(hr))
553
+ }
554
+
555
+ if !multi {
556
+ // 单文件:GetResult -> IShellItem -> GetDisplayName。
557
+ var item uintptr
558
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
559
+ return nil, nil
560
+ }
561
+ defer comRelease(item)
562
+ path, err := shellItemDisplayName(item)
563
+ if err != nil {
564
+ return nil, err
565
+ }
566
+ if path == "" {
567
+ return nil, nil
568
+ }
569
+ return []string{path}, nil
570
+ }
571
+
572
+ // 多选:IFileOpenDialog::GetResults -> IShellItemArray(此处简化为逐个
573
+ // 通过 IShellItem 枚举较繁琐,多选暂用 GetResults 取首个 + 提示)。
574
+ // 注:为保持实现简洁,多选场景返回首个文件的单项列表(后续如需完整
575
+ // 多选可扩展 IShellItemArray 枚举)。
576
+ var item uintptr
577
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
578
+ return nil, nil
579
+ }
580
+ defer comRelease(item)
581
+ path, err := shellItemDisplayName(item)
582
+ if err != nil {
583
+ return nil, err
584
+ }
585
+ if path == "" {
586
+ return nil, nil
587
+ }
588
+ return []string{path}, nil
589
+ }
590
+
591
+ // syscallSaveDialog 弹出系统"另存为"对话框,返回保存路径(取消返回空串)。
592
+ func syscallSaveDialog(hwnd uintptr, title, defaultName string, filters []fileDialogFilter) (string, error) {
593
+ if err := comEnsureInit(); err != nil {
594
+ return "", err
595
+ }
596
+ var ppv uintptr
597
+ hr, _, _ := procCoCreateInstance.Call(
598
+ uintptr(unsafe.Pointer(&clsidFileSaveDialog)), 0, 0x1,
599
+ uintptr(unsafe.Pointer(&iidIFileSaveDialog)),
600
+ uintptr(unsafe.Pointer(&ppv)),
601
+ )
602
+ if comHResultFailed(hr) || ppv == 0 {
603
+ return "", fmt.Errorf("dialog: 无法创建保存对话框(COM 初始化失败)")
604
+ }
605
+ defer comRelease(ppv)
606
+
607
+ if title != "" {
608
+ if p, err := syscall.UTF16PtrFromString(title); err == nil {
609
+ comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
610
+ }
611
+ }
612
+ if defaultName != "" {
613
+ if p, err := syscall.UTF16PtrFromString(defaultName); err == nil {
614
+ comCall(ppv, ifdSetFileName, uintptr(unsafe.Pointer(p)))
615
+ }
616
+ }
617
+ if len(filters) > 0 {
618
+ applyFileDialogFilters(ppv, filters)
619
+ }
620
+ comCall(ppv, ifdSetOptions, fosOverwritePrompt|fosForceFileSystem|fosDefaultNoMiniMode|fosNoChangeDir)
621
+
622
+ if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
623
+ if uint32(hr) == 0x800704C7 {
624
+ return "", nil
625
+ }
626
+ return "", fmt.Errorf("dialog: 保存对话框失败 (0x%08X)", uint32(hr))
627
+ }
628
+ var item uintptr
629
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
630
+ return "", nil
631
+ }
632
+ defer comRelease(item)
633
+ return shellItemDisplayName(item)
634
+ }
635
+
636
+ // applyFileDialogFilters 把 Go 过滤器写入 COMDLG_FILTERSPEC 数组并 SetFileTypes。
637
+ func applyFileDialogFilters(ppv uintptr, filters []fileDialogFilter) {
638
+ // COMDLG_FILTERSPEC = { PWSTR pszName; PWSTR pszSpec; }
639
+ type filterSpec struct {
640
+ name uintptr
641
+ spec uintptr
642
+ }
643
+ specs := make([]filterSpec, 0, len(filters))
644
+ keep := make([]*uint16, 0, len(filters)*2)
645
+ for _, f := range filters {
646
+ if f.Name == "" || f.Spec == "" {
647
+ continue
648
+ }
649
+ n, _ := syscall.UTF16PtrFromString(f.Name)
650
+ s, _ := syscall.UTF16PtrFromString(f.Spec)
651
+ specs = append(specs, filterSpec{name: uintptr(unsafe.Pointer(n)), spec: uintptr(unsafe.Pointer(s))})
652
+ keep = append(keep, n, s)
653
+ }
654
+ if len(specs) == 0 {
655
+ return
656
+ }
657
+ // 数组首元素地址传给 SetFileTypes。
658
+ comCall(ppv, ifdSetFileTypes, uintptr(len(specs)), uintptr(unsafe.Pointer(&specs[0])))
659
+ }
660
+
661
+ // shellItemDisplayName 从 IShellItem 取文件系统路径。
662
+ func shellItemDisplayName(item uintptr) (string, error) {
663
+ var p *uint16
664
+ if hr := comCall(item, siGetDisplayName, sigdnFilesysPath, uintptr(unsafe.Pointer(&p))); comHResultFailed(hr) || p == nil {
665
+ return "", nil
666
+ }
667
+ defer procCoTaskMemFree.Call(uintptr(unsafe.Pointer(p)))
668
+ n := 0
669
+ for *(*uint16)(unsafe.Add(unsafe.Pointer(p), n*2)) != 0 {
670
+ n++
671
+ }
672
+ return syscall.UTF16ToString(unsafe.Slice(p, n)), nil
673
+ }
674
+
675
+ // sysCapCall 处理前端 __freedom_sys 的系统能力请求。
676
+ // args 为 JSON object(map[string]json.RawMessage)。
677
+ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{}, err error) {
678
+ defer func() {
679
+ if r := recover(); r != nil {
680
+ result = nil
681
+ err = fmt.Errorf("freedom: sys method %q panicked: %v", method, r)
682
+ }
683
+ }()
684
+
685
+ var args map[string]json.RawMessage
686
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
687
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
688
+ return nil, fmt.Errorf("freedom: sys method %q: invalid args: %w", method, err)
689
+ }
690
+ }
691
+ argStr := func(k string) string {
692
+ if v, ok := args[k]; ok {
693
+ var s string
694
+ if json.Unmarshal(v, &s) == nil {
695
+ return s
696
+ }
697
+ }
698
+ return ""
699
+ }
700
+ argBool := func(k string) (bool, bool) {
701
+ if v, ok := args[k]; ok {
702
+ var b bool
703
+ if json.Unmarshal(v, &b) == nil {
704
+ return b, true
705
+ }
706
+ }
707
+ return false, false
708
+ }
709
+ // 平台无关方法(path/store/os/process)优先由 sysGeneric 处理。
710
+ if res, ok, err := a.sysGeneric(method, args); ok {
711
+ return res, err
712
+ }
713
+ hwnd := a.WindowHandle()
714
+
715
+ switch method {
716
+ // ---- 任务栏 ----
717
+ case "taskbar.progress":
718
+ var value float64
719
+ if v, ok := args["value"]; ok {
720
+ json.Unmarshal(v, &value)
721
+ }
722
+ return nil, taskbarSetProgress(hwnd, value)
723
+ case "taskbar.state":
724
+ return nil, taskbarSetState(hwnd, argStr("state"))
725
+ case "taskbar.clear":
726
+ return nil, taskbarClearProgress(hwnd)
727
+ case "taskbar.overlay":
728
+ var icon string
729
+ if v, ok := args["icon"]; ok {
730
+ json.Unmarshal(v, &icon)
731
+ }
732
+ return nil, taskbarSetOverlay(hwnd, dataURLToBytes(icon))
733
+ case "taskbar.clearOverlay":
734
+ return nil, taskbarSetOverlay(hwnd, nil)
735
+
736
+ // ---- 窗口效果 ----
737
+ case "window.monitors":
738
+ return listMonitors(), nil
739
+ case "window.backdrop":
740
+ return nil, windowSetBackdrop(hwnd, argStr("mode"))
741
+ case "window.corner":
742
+ return nil, windowSetCorner(hwnd, argStr("mode"))
743
+ case "window.borderColor":
744
+ var color uint32
745
+ if v, ok := args["color"]; ok {
746
+ json.Unmarshal(v, &color) // 0x00BBGGRR
747
+ }
748
+ return nil, windowSetBorderColor(hwnd, color)
749
+
750
+ // ---- 系统对话框 ----
751
+ case "dialog.message":
752
+ title := argStr("title")
753
+ msg := argStr("message")
754
+ buttons := argStr("buttons")
755
+ icon := argStr("icon")
756
+ return syscallMessageBox(hwnd, title, msg, buttons, icon)
757
+
758
+ case "dialog.open":
759
+ var filters []fileDialogFilter
760
+ if v, ok := args["filters"]; ok {
761
+ json.Unmarshal(v, &filters)
762
+ }
763
+ multi := false
764
+ if v, ok := args["multi"]; ok {
765
+ json.Unmarshal(v, &multi)
766
+ }
767
+ return syscallOpenDialog(hwnd, argStr("title"), filters, multi)
768
+
769
+ case "dialog.save":
770
+ var filters []fileDialogFilter
771
+ if v, ok := args["filters"]; ok {
772
+ json.Unmarshal(v, &filters)
773
+ }
774
+ return syscallSaveDialog(hwnd, argStr("title"), argStr("defaultName"), filters)
775
+
776
+ // ---- W2 系统集成(对标 Tauri 官方插件)----
777
+ case "clipboard.read":
778
+ return clipboardReadText()
779
+ case "clipboard.write":
780
+ return nil, clipboardWriteText(argStr("text"), hwnd)
781
+ case "shell.open":
782
+ return nil, shellOpen(argStr("target"))
783
+ case "notification.show":
784
+ // Toast 经 PowerShell 子进程投影,冷启动 1-3s——异步派发,
785
+ // 绝不在 UI 线程同步等待(桥接回调运行在消息泵内,同步=冻结窗口)。
786
+ title, body := argStr("title"), argStr("body")
787
+ go func() {
788
+ if err := showToast(title, body); err != nil {
789
+ fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
790
+ }
791
+ }()
792
+ return nil, nil
793
+ case "autostart.get":
794
+ name := argStr("name")
795
+ if name == "" {
796
+ name = defaultAppID()
797
+ }
798
+ return getAutostart(name)
799
+ case "autostart.set":
800
+ name := argStr("name")
801
+ if name == "" {
802
+ name = defaultAppID()
803
+ }
804
+ enabled, ok := argBool("enabled")
805
+ if !ok {
806
+ return nil, fmt.Errorf("freedom: autostart.set 缺少 enabled")
807
+ }
808
+ return nil, setAutostart(name, enabled, argStr("args"))
809
+ case "protocol.register":
810
+ return nil, registerProtocol(argStr("scheme"), argStr("name"))
811
+ case "protocol.unregister":
812
+ return nil, unregisterProtocol(argStr("scheme"))
813
+ case "app.launchArgs":
814
+ return launchArgsJSON(), nil
815
+ case "shortcut.register":
816
+ id := argStr("id")
817
+ combo := argStr("combo")
818
+ if id == "" || combo == "" {
819
+ return nil, fmt.Errorf("freedom: shortcut.register 需要 id 与 combo(如 ctrl+alt+k)")
820
+ }
821
+ mw, err := ensureMsgWindow("")
822
+ if err != nil {
823
+ return nil, err
824
+ }
825
+ mw.setOnHotkey(func(hid string) {
826
+ a.Emit("shortcut.triggered", map[string]interface{}{"id": hid})
827
+ })
828
+ return nil, mw.registerHotkey(id, combo)
829
+ case "shortcut.unregister":
830
+ mw, err := ensureMsgWindow("")
831
+ if err != nil {
832
+ return nil, err
833
+ }
834
+ return nil, mw.unregisterHotkey(argStr("id"))
835
+ case "shortcut.list":
836
+ mw, err := ensureMsgWindow("")
837
+ if err != nil {
838
+ return nil, err
839
+ }
840
+ return mw.hotkeyIDs(), nil
841
+
842
+ default:
843
+ return nil, fmt.Errorf("freedom: unknown sys method %q", method)
844
+ }
845
+ }