@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,536 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ // 系统托盘(Shell_NotifyIcon)与原生菜单栏(HMENU)。
6
+ //
7
+ // 托盘通过一个隐藏消息窗口接收回调消息(WM_APP+1),菜单项点击经 WM_COMMAND
8
+ // 分发,统一通过 App.Emit 推送到前端(freedom.tray.on('click'/'menu', ...))。
9
+ // 原生菜单栏通过 SetMenu 挂到主窗口,同样经 WM_COMMAND 回调。
10
+ //
11
+ // 与主窗口同一 UI 线程创建,消息由同一消息泵派发,无需额外线程。
12
+
13
+ import (
14
+ "encoding/json"
15
+ "fmt"
16
+ "sync"
17
+ "syscall"
18
+ "unsafe"
19
+ )
20
+
21
+ // ---- 常量 ----
22
+
23
+ const (
24
+ wmApp = 0x8000 // WM_APP
25
+ wmCommand = 0x0111
26
+ wmLButtonUp = 0x0202
27
+ wmLButtonDbl = 0x0203
28
+ wmRButtonUp = 0x0205
29
+ wmNull = 0x0000
30
+
31
+ nimAdd = 0x00000000
32
+ nimModify = 0x00000001
33
+ nimDelete = 0x00000002
34
+
35
+ nifMessage = 0x00000001
36
+ nifIcon = 0x00000002
37
+ nifTip = 0x00000004
38
+
39
+ tpmRightButton = 0x00000002
40
+ tpmLeftAlign = 0x00000000
41
+
42
+ mfString = 0x00000000
43
+ mfSeparator = 0x00000800
44
+ mfChecked = 0x00000008
45
+ mfGrayed = 0x00000001
46
+ mfEnabled = 0x00000000
47
+ mfPopup = 0x00000010
48
+ mfByPosition = 0x00000400
49
+ )
50
+
51
+ var (
52
+ shell32 = syscall.NewLazyDLL("shell32.dll")
53
+ procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW")
54
+
55
+ procRegisterClassExW = user32win.NewProc("RegisterClassExW")
56
+ procCreateWindowExW = user32win.NewProc("CreateWindowExW")
57
+ procDefWindowProcW = user32win.NewProc("DefWindowProcW")
58
+ procCreatePopupMenu = user32win.NewProc("CreatePopupMenu")
59
+ procCreateMenu = user32win.NewProc("CreateMenu")
60
+ procAppendMenuW = user32win.NewProc("AppendMenuW")
61
+ procInsertMenuW = user32win.NewProc("InsertMenuW")
62
+ procDestroyMenu = user32win.NewProc("DestroyMenu")
63
+ procTrackPopupMenu = user32win.NewProc("TrackPopupMenu")
64
+ procGetCursorPos = user32win.NewProc("GetCursorPos")
65
+ procSetForegroundWindow = user32win.NewProc("SetForegroundWindow")
66
+ procSetMenu = user32win.NewProc("SetMenu")
67
+ procDrawMenuBar = user32win.NewProc("DrawMenuBar")
68
+ )
69
+
70
+ // ---- 结构 ----
71
+
72
+ type point struct {
73
+ X, Y int32
74
+ }
75
+
76
+ type wndClassExW struct {
77
+ CbSize uint32
78
+ Style uint32
79
+ LpfnWndProc uintptr
80
+ CbClsExtra int32
81
+ CbWndExtra int32
82
+ HInstance uintptr
83
+ HIcon uintptr
84
+ HCursor uintptr
85
+ HbrBackground uintptr
86
+ LpszMenuName *uint16
87
+ LpszClassName *uint16
88
+ HIconSm uintptr
89
+ }
90
+
91
+ // notifyIconDataW 对应 NOTIFYICONDATAW(amd64 布局,cbSize=984)。
92
+ type notifyIconDataW struct {
93
+ CbSize uint32
94
+ _ uint32 // 对齐填充
95
+ HWnd uintptr
96
+ UID uint32
97
+ UFlags uint32
98
+ UCallbackMessage uint32
99
+ HIcon uintptr
100
+ SzTip [128]uint16
101
+ DwState uint32
102
+ DwStateMask uint32
103
+ SzInfo [256]uint16
104
+ UVersion uint32
105
+ SzInfoTitle [64]uint16
106
+ DwInfoFlags uint32
107
+ GuidItem [16]byte
108
+ HBalloonIcon uintptr
109
+ }
110
+
111
+ // trayState 保存当前托盘实例(单窗口框架内为单例)。
112
+ type trayState struct {
113
+ mu sync.Mutex
114
+ hwnd uintptr // 隐藏消息窗口
115
+ menu uintptr // 右键 popup 菜单
116
+ menuIDs []uint32 // menu 占用的条目 id(重建时清理映射)
117
+ menuBar uintptr // 原生菜单栏 HMENU
118
+ menuBarIDs []uint32 // menuBar 占用的条目 id
119
+ items map[uint32]string // menu id -> 前端 id
120
+ nextID uint32
121
+ emit func(event string, data interface{})
122
+ icon uintptr // 当前托盘图标 HICON
123
+ }
124
+
125
+ var (
126
+ trayProcMu sync.Mutex
127
+ trayStateInst *trayState
128
+ trayWndProcCB uintptr
129
+ )
130
+
131
+ // trayProcCallback 是隐藏窗口的 WndProc(Go 回调,经 syscall.NewCallback 注册)。
132
+ // 由 RegisterClassEx 注册到系统,消息泵在 UI 线程派发。
133
+ func trayProcCallback(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
134
+ ts := trayStateInst
135
+ if ts == nil {
136
+ r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
137
+ return r
138
+ }
139
+ switch msg {
140
+ case uint32(wmApp) + 1: // uCallbackMessage
141
+ switch lParam & 0xFFFF {
142
+ case wmLButtonUp:
143
+ ts.emit("tray:click", map[string]interface{}{"button": "left"})
144
+ case wmLButtonDbl:
145
+ ts.emit("tray:double-click", map[string]interface{}{"button": "left"})
146
+ case wmRButtonUp:
147
+ trayShowMenu(ts)
148
+ }
149
+ case uint32(wmCommand):
150
+ id := uint32(wParam & 0xFFFF)
151
+ ts.mu.Lock()
152
+ label, ok := ts.items[id]
153
+ ts.mu.Unlock()
154
+ if ok {
155
+ ts.emit("tray:menu", map[string]interface{}{"id": label})
156
+ }
157
+ }
158
+ r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
159
+ return r
160
+ }
161
+
162
+ // ensureTrayWindow 创建隐藏消息窗口(仅一次)。
163
+ func ensureTrayWindow(a *App) (*trayState, error) {
164
+ trayProcMu.Lock()
165
+ defer trayProcMu.Unlock()
166
+ if trayStateInst != nil {
167
+ return trayStateInst, nil
168
+ }
169
+ className, _ := syscall.UTF16PtrFromString("FreedomTrayWnd")
170
+ if trayWndProcCB == 0 {
171
+ trayWndProcCB = syscall.NewCallback(trayProcCallback)
172
+ }
173
+ hInst := uintptr(0)
174
+ if m, _, _ := procGetModuleHandle.Call(0); m != 0 {
175
+ hInst = m
176
+ }
177
+ wc := wndClassExW{
178
+ CbSize: uint32(unsafe.Sizeof(wndClassExW{})),
179
+ LpfnWndProc: trayWndProcCB,
180
+ HInstance: hInst,
181
+ LpszClassName: className,
182
+ }
183
+ if r, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
184
+ // 可能已注册,继续尝试。
185
+ }
186
+ ts := &trayState{
187
+ items: map[uint32]string{},
188
+ nextID: 1,
189
+ emit: a.Emit,
190
+ }
191
+ hwnd, _, _ := procCreateWindowExW.Call(
192
+ 0,
193
+ uintptr(unsafe.Pointer(className)),
194
+ uintptr(unsafe.Pointer(className)),
195
+ 0,
196
+ 0, 0, 0, 0,
197
+ 0, 0, hInst, 0,
198
+ )
199
+ if hwnd == 0 {
200
+ return nil, fmt.Errorf("tray: 创建隐藏窗口失败")
201
+ }
202
+ ts.hwnd = hwnd
203
+ trayStateInst = ts
204
+ return ts, nil
205
+ }
206
+
207
+ // traySetIcon 创建或更新托盘图标。
208
+ func traySetIcon(ts *trayState, iconData []byte, tooltip string) error {
209
+ ts.mu.Lock()
210
+ defer ts.mu.Unlock()
211
+ // 释放旧图标。
212
+ if ts.icon != 0 {
213
+ procDestroyIcon.Call(ts.icon)
214
+ ts.icon = 0
215
+ }
216
+ nid := &notifyIconDataW{
217
+ CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})),
218
+ HWnd: ts.hwnd,
219
+ UID: 1,
220
+ UFlags: nifMessage | nifIcon | nifTip,
221
+ UCallbackMessage: wmApp + 1,
222
+ }
223
+ if len(iconData) > 0 {
224
+ ic := hiconFromPNG(iconData)
225
+ if ic == 0 {
226
+ return fmt.Errorf("tray: 无法从图标数据创建 HICON")
227
+ }
228
+ nid.HIcon = ic
229
+ ts.icon = ic
230
+ } else {
231
+ // 无图标数据:使用 exe 内嵌图标。
232
+ cx, _, _ := procGetSystemMetrics.Call(smCXSmall)
233
+ cy, _, _ := procGetSystemMetrics.Call(smCYSmall)
234
+ if ic := loadExeIcon(cx, cy); ic != 0 {
235
+ nid.HIcon = ic
236
+ ts.icon = ic
237
+ }
238
+ }
239
+ if tooltip != "" {
240
+ tp, _ := syscall.UTF16FromString(tooltip)
241
+ copy(nid.SzTip[:], tp[:min(len(tp), 127)])
242
+ }
243
+ // 首次 NIM_ADD,之后 NIM_MODIFY。
244
+ if ts.hwnd == 0 || !trayIconAdded {
245
+ trayIconAdded = true
246
+ r, _, _ := procShellNotifyIconW.Call(nimAdd, uintptr(unsafe.Pointer(nid)))
247
+ if r == 0 {
248
+ return fmt.Errorf("tray: Shell_NotifyIcon(NIM_ADD) 失败")
249
+ }
250
+ } else {
251
+ r, _, _ := procShellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(nid)))
252
+ if r == 0 {
253
+ return fmt.Errorf("tray: Shell_NotifyIcon(NIM_MODIFY) 失败")
254
+ }
255
+ }
256
+ return nil
257
+ }
258
+
259
+ var trayIconAdded bool
260
+
261
+ // trayShowMenu 在鼠标位置弹出右键菜单。
262
+ func trayShowMenu(ts *trayState) {
263
+ if ts == nil || ts.menu == 0 {
264
+ return
265
+ }
266
+ var pt point
267
+ procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
268
+ procSetForegroundWindow.Call(ts.hwnd)
269
+ procTrackPopupMenu.Call(
270
+ ts.menu, tpmRightButton|tpmLeftAlign,
271
+ uintptr(pt.X), uintptr(pt.Y), 0, ts.hwnd, 0,
272
+ )
273
+ procPostMessage.Call(ts.hwnd, wmNull, 0, 0)
274
+ }
275
+
276
+ // buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
277
+ // 返回菜单句柄与本次分配的全部条目 id(供整体替换时清理旧映射)。
278
+ func buildMenu(ts *trayState, items []menuItem) (uintptr, []uint32) {
279
+ hMenu, _, _ := procCreatePopupMenu.Call()
280
+ if hMenu == 0 {
281
+ return 0, nil
282
+ }
283
+ var ids []uint32
284
+ appendMenuItems(ts, hMenu, items, false, &ids)
285
+ return hMenu, ids
286
+ }
287
+
288
+ // appendMenuItems 把条目追加到指定菜单;新分配的 id 记入 *ids。
289
+ func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool, ids *[]uint32) {
290
+ for _, it := range items {
291
+ if it.Type == "separator" {
292
+ procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
293
+ continue
294
+ }
295
+ if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
296
+ label, err := syscall.UTF16PtrFromString(it.Label)
297
+ if err != nil {
298
+ continue // 标签含 NUL:跳过该条目
299
+ }
300
+ sub, subIDs := buildMenu(ts, it.Submenu)
301
+ if sub == 0 {
302
+ continue
303
+ }
304
+ *ids = append(*ids, subIDs...)
305
+ procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
306
+ continue
307
+ }
308
+ label, err := syscall.UTF16PtrFromString(it.Label)
309
+ if err != nil {
310
+ continue // 标签含 NUL:跳过该条目(id 不消耗)
311
+ }
312
+ var flags uintptr = mfString | mfEnabled
313
+ if it.Checked != nil && *it.Checked {
314
+ flags |= mfChecked
315
+ }
316
+ if it.Enabled != nil && !*it.Enabled {
317
+ flags |= mfGrayed
318
+ }
319
+ ts.mu.Lock()
320
+ id := ts.nextID
321
+ ts.nextID++
322
+ ts.items[id] = it.ID
323
+ ts.mu.Unlock()
324
+ *ids = append(*ids, id)
325
+ procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
326
+ }
327
+ }
328
+
329
+ // traySetMenu 设置托盘右键菜单(整体替换,旧 id 映射随之清理)。
330
+ func traySetMenu(ts *trayState, items []menuItem) {
331
+ ts.mu.Lock()
332
+ old := ts.menu
333
+ oldIDs := ts.menuIDs
334
+ ts.menu = 0
335
+ ts.menuIDs = nil
336
+ ts.mu.Unlock()
337
+ if old != 0 {
338
+ procDestroyMenu.Call(old)
339
+ }
340
+ releaseItemIDs(ts, oldIDs)
341
+ if len(items) == 0 {
342
+ return
343
+ }
344
+ h, ids := buildMenu(ts, items)
345
+ ts.mu.Lock()
346
+ ts.menu = h
347
+ ts.menuIDs = ids
348
+ ts.mu.Unlock()
349
+ }
350
+
351
+ // releaseItemIDs 从 id→前端 id 映射中移除一批条目(防重建后残留无主映射无限增长)。
352
+ func releaseItemIDs(ts *trayState, ids []uint32) {
353
+ if len(ids) == 0 {
354
+ return
355
+ }
356
+ ts.mu.Lock()
357
+ for _, id := range ids {
358
+ delete(ts.items, id)
359
+ }
360
+ ts.mu.Unlock()
361
+ }
362
+
363
+ // menuBarSet 设置主窗口原生菜单栏(整体替换,旧 id 映射随之清理)。
364
+ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
365
+ ts.mu.Lock()
366
+ old := ts.menuBar
367
+ oldIDs := ts.menuBarIDs
368
+ ts.menuBar = 0
369
+ ts.menuBarIDs = nil
370
+ ts.mu.Unlock()
371
+ if old != 0 {
372
+ procDestroyMenu.Call(old)
373
+ }
374
+ releaseItemIDs(ts, oldIDs)
375
+ if len(items) == 0 {
376
+ procSetMenu.Call(hwnd, 0)
377
+ procDrawMenuBar.Call(hwnd)
378
+ return
379
+ }
380
+ hBar, _, _ := procCreateMenu.Call()
381
+ if hBar == 0 {
382
+ return
383
+ }
384
+ var ids []uint32
385
+ for _, top := range items {
386
+ if top.Type == "separator" {
387
+ continue
388
+ }
389
+ label, err := syscall.UTF16PtrFromString(top.Label)
390
+ if err != nil {
391
+ continue // 标签含 NUL:跳过该条目
392
+ }
393
+ if len(top.Submenu) > 0 {
394
+ sub, subIDs := buildMenu(ts, top.Submenu)
395
+ if sub == 0 {
396
+ continue
397
+ }
398
+ ids = append(ids, subIDs...)
399
+ procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
400
+ } else {
401
+ // 顶层无子菜单:视为可点击菜单项。
402
+ var flags uintptr = mfString | mfEnabled
403
+ if top.Checked != nil && *top.Checked {
404
+ flags |= mfChecked
405
+ }
406
+ ts.mu.Lock()
407
+ id := ts.nextID
408
+ ts.nextID++
409
+ ts.items[id] = top.ID
410
+ ts.mu.Unlock()
411
+ ids = append(ids, id)
412
+ procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
413
+ }
414
+ }
415
+ ts.mu.Lock()
416
+ ts.menuBar = hBar
417
+ ts.menuBarIDs = ids
418
+ ts.mu.Unlock()
419
+ procSetMenu.Call(hwnd, hBar)
420
+ procDrawMenuBar.Call(hwnd)
421
+ }
422
+
423
+ // trayDestroy 移除托盘图标并清理隐藏窗口。
424
+ func trayDestroy(ts *trayState) {
425
+ if ts == nil {
426
+ return
427
+ }
428
+ ts.mu.Lock()
429
+ defer ts.mu.Unlock()
430
+ if trayIconAdded {
431
+ nid := &notifyIconDataW{CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})), HWnd: ts.hwnd, UID: 1}
432
+ procShellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(nid)))
433
+ trayIconAdded = false
434
+ }
435
+ if ts.menu != 0 {
436
+ procDestroyMenu.Call(ts.menu)
437
+ ts.menu = 0
438
+ }
439
+ if ts.menuBar != 0 {
440
+ procDestroyMenu.Call(ts.menuBar)
441
+ ts.menuBar = 0
442
+ }
443
+ ts.menuIDs = nil
444
+ ts.menuBarIDs = nil
445
+ ts.items = map[uint32]string{} // 已持锁:直接重建映射,不留无主条目
446
+ if ts.icon != 0 {
447
+ procDestroyIcon.Call(ts.icon)
448
+ ts.icon = 0
449
+ }
450
+ if ts.hwnd != 0 {
451
+ procDestroyWindow.Call(ts.hwnd)
452
+ ts.hwnd = 0
453
+ }
454
+ trayStateInst = nil
455
+ }
456
+
457
+ var procDestroyWindow = user32win.NewProc("DestroyWindow")
458
+
459
+ // trayCall 处理前端 __freedom_tray 请求。
460
+ func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
461
+ defer func() {
462
+ if r := recover(); r != nil {
463
+ result = nil
464
+ err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
465
+ }
466
+ }()
467
+
468
+ var args map[string]json.RawMessage
469
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
470
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
471
+ return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
472
+ }
473
+ }
474
+ argStr := func(k string) string {
475
+ if v, ok := args[k]; ok {
476
+ var s string
477
+ if json.Unmarshal(v, &s) == nil {
478
+ return s
479
+ }
480
+ }
481
+ return ""
482
+ }
483
+
484
+ switch method {
485
+ case "tray.create":
486
+ ts, err := ensureTrayWindow(a)
487
+ if err != nil {
488
+ return nil, err
489
+ }
490
+ return nil, traySetIcon(ts, dataURLToBytes(argStr("icon")), argStr("tooltip"))
491
+ case "tray.destroy":
492
+ trayDestroy(trayStateInst)
493
+ return nil, nil
494
+ case "tray.tooltip":
495
+ ts := trayStateInst
496
+ if ts == nil {
497
+ return nil, fmt.Errorf("tray: 托盘未创建")
498
+ }
499
+ return nil, traySetIcon(ts, nil, argStr("tooltip"))
500
+ case "tray.menu":
501
+ ts := trayStateInst
502
+ if ts == nil {
503
+ return nil, fmt.Errorf("tray: 托盘未创建")
504
+ }
505
+ var items []menuItem
506
+ if v, ok := args["items"]; ok {
507
+ if err := json.Unmarshal(v, &items); err != nil {
508
+ return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
509
+ }
510
+ }
511
+ traySetMenu(ts, items)
512
+ return nil, nil
513
+ case "menu.set":
514
+ ts := trayStateInst
515
+ if ts == nil {
516
+ ts, err = ensureTrayWindow(a)
517
+ if err != nil {
518
+ return nil, err
519
+ }
520
+ }
521
+ var items []menuItem
522
+ if v, ok := args["items"]; ok {
523
+ if err := json.Unmarshal(v, &items); err != nil {
524
+ return nil, fmt.Errorf("menu: 菜单格式错误: %w", err)
525
+ }
526
+ }
527
+ hwnd := a.WindowHandle()
528
+ if hwnd == 0 {
529
+ return nil, fmt.Errorf("menu: 窗口未就绪")
530
+ }
531
+ menuBarSet(hwnd, ts, items)
532
+ return nil, nil
533
+ default:
534
+ return nil, fmt.Errorf("freedom: unknown tray method %q", method)
535
+ }
536
+ }