@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
@@ -0,0 +1,302 @@
1
+ //go:build linux
2
+
3
+ package freedom
4
+
5
+ /*
6
+ #cgo pkg-config: gtk+-3.0
7
+ #cgo CFLAGS: -Wno-deprecated-declarations
8
+ #include <gtk/gtk.h>
9
+
10
+ extern void goTrayClick(void);
11
+ extern void goTrayMenuItemActivate(int idx);
12
+
13
+ // GtkStatusIcon 自 GTK3.14 起标记 deprecated,但 legacy tray 协议在 KDE/XFCE/
14
+ // WSLg 仍广泛可用;AppIndicator(GDBus) 适配在未来自成一档。
15
+ static int ensureGtk(void) { return gtk_init_check(NULL, NULL) ? 1 : 0; }
16
+
17
+ static void onTrayActivate(GtkStatusIcon *icon, gpointer data) {
18
+ (void)icon; (void)data;
19
+ goTrayClick();
20
+ }
21
+
22
+ // 右键:弹出前端注册的菜单(popup-menu 信号),返回 TRUE 抑制默认行为。
23
+ static gboolean onTrayPopupMenu(GtkStatusIcon *icon, guint button, guint activate_time, gpointer data) {
24
+ (void)button; (void)activate_time; (void)data;
25
+ GtkWidget *menu = (GtkWidget*) g_object_get_data(G_OBJECT(icon), "freedom-menu");
26
+ if (menu != NULL) gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
27
+ return TRUE;
28
+ }
29
+
30
+ static void onMenuItemClick(GtkMenuItem *item, gpointer data) {
31
+ (void)item;
32
+ goTrayMenuItemActivate(GPOINTER_TO_INT(data));
33
+ }
34
+
35
+ static GtkStatusIcon* trayNewFromPng(const guint8 *png, gsize len) {
36
+ GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
37
+ gboolean ok = gdk_pixbuf_loader_write(loader, png, len, NULL);
38
+ ok = ok && gdk_pixbuf_loader_close(loader, NULL);
39
+ GdkPixbuf *pb = ok ? gdk_pixbuf_loader_get_pixbuf(loader) : NULL;
40
+ GtkStatusIcon *icon = pb ? gtk_status_icon_new_from_pixbuf(pb) : NULL;
41
+ g_object_unref(loader);
42
+ if (icon == NULL) return NULL;
43
+ g_signal_connect(icon, "activate", G_CALLBACK(onTrayActivate), NULL);
44
+ g_signal_connect(icon, "popup-menu", G_CALLBACK(onTrayPopupMenu), NULL);
45
+ gtk_status_icon_set_visible(icon, TRUE);
46
+ return icon;
47
+ }
48
+
49
+ static GtkStatusIcon* trayNewNamed(const char *name) {
50
+ GtkStatusIcon *icon = gtk_status_icon_new_from_icon_name(name);
51
+ g_signal_connect(icon, "activate", G_CALLBACK(onTrayActivate), NULL);
52
+ g_signal_connect(icon, "popup-menu", G_CALLBACK(onTrayPopupMenu), NULL);
53
+ gtk_status_icon_set_visible(icon, TRUE);
54
+ return icon;
55
+ }
56
+
57
+ static void traySetTooltip(GtkStatusIcon *icon, const char *tip) {
58
+ gtk_status_icon_set_tooltip_text(icon, tip);
59
+ }
60
+
61
+ static void trayDestroy(GtkStatusIcon *icon) {
62
+ gtk_status_icon_set_visible(icon, FALSE);
63
+ g_object_unref(icon);
64
+ }
65
+
66
+ // 菜单所有权:attach 时 ref_sink + set_data_full,重复 attach 自动释放旧菜单。
67
+ static void trayAttachMenu(GtkStatusIcon *icon, GtkWidget *menu) {
68
+ g_object_ref_sink(menu);
69
+ g_object_set_data_full(G_OBJECT(icon), "freedom-menu", menu, (GDestroyNotify) g_object_unref);
70
+ gtk_widget_show_all(menu);
71
+ }
72
+
73
+ static GtkWidget* menuNew(void) { return gtk_menu_new(); }
74
+
75
+ static GtkWidget* menuSubmenu(GtkWidget *parent, const char *label) {
76
+ GtkWidget *item = gtk_menu_item_new_with_label(label);
77
+ GtkWidget *sub = gtk_menu_new();
78
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
79
+ gtk_menu_shell_append(GTK_MENU_SHELL(parent), item);
80
+ return sub;
81
+ }
82
+
83
+ static void menuAppendItem(GtkWidget *menu, const char *label, int idx, gboolean sensitive) {
84
+ GtkWidget *item = gtk_menu_item_new_with_label(label);
85
+ gtk_widget_set_sensitive(item, sensitive);
86
+ g_signal_connect(item, "activate", G_CALLBACK(onMenuItemClick), GINT_TO_POINTER(idx));
87
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
88
+ }
89
+
90
+ static void menuAppendCheck(GtkWidget *menu, const char *label, int idx, gboolean checked, gboolean sensitive) {
91
+ GtkWidget *item = gtk_check_menu_item_new_with_label(label);
92
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), checked);
93
+ gtk_widget_set_sensitive(item, sensitive);
94
+ g_signal_connect(item, "activate", G_CALLBACK(onMenuItemClick), GINT_TO_POINTER(idx));
95
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
96
+ }
97
+
98
+ static void menuAppendSeparator(GtkWidget *menu) {
99
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
100
+ }
101
+ */
102
+ import "C"
103
+
104
+ import (
105
+ "encoding/json"
106
+ "fmt"
107
+ "sync"
108
+ "unsafe"
109
+ )
110
+
111
+ // M4 Linux 托盘(GtkStatusIcon,cgo):与 Windows 侧 trayCall 同一前端契约
112
+ // (tray.create/destroy/tooltip/menu;事件 tray:click / tray:menu({id}))。
113
+ // 限制:GtkStatusIcon 无双击信号(tray:double-click 不触发);menu.set(主窗口
114
+ // 原生菜单栏)与 GDBus StatusNotifierItem 适配未实装,托盘图标走 legacy
115
+ // system tray 协议(KDE/xfce/wmctrl 系支持;GNOME 需 AppIndicator 扩展)。
116
+ // GTK 调用须发生在 main线程:__freedom_tray 桥回调经 WebKit 消息处理器在
117
+ // GTK main loop 线程执行,天然满足;单测须先 gtk_init_check 且有 DISPLAY。
118
+
119
+ type linuxTrayState struct {
120
+ mu sync.Mutex
121
+ icon *C.GtkStatusIcon
122
+ items map[int32]string // 菜单条目序号 → 前端 id
123
+ emit func(event string, data interface{})
124
+ }
125
+
126
+ var linuxTray = &linuxTrayState{items: map[int32]string{}}
127
+
128
+ //export goTrayClick
129
+ func goTrayClick() {
130
+ linuxTray.mu.Lock()
131
+ emit := linuxTray.emit
132
+ linuxTray.mu.Unlock()
133
+ if emit != nil {
134
+ emit("tray:click", map[string]interface{}{"button": "left"})
135
+ }
136
+ }
137
+
138
+ //export goTrayMenuItemActivate
139
+ func goTrayMenuItemActivate(idx C.int) {
140
+ linuxTray.mu.Lock()
141
+ id := linuxTray.items[int32(idx)]
142
+ emit := linuxTray.emit
143
+ linuxTray.mu.Unlock()
144
+ if emit != nil {
145
+ emit("tray:menu", map[string]interface{}{"id": id})
146
+ }
147
+ }
148
+
149
+ // trayCall 处理前端 __freedom_tray 请求(Linux 实现,方法面与 Windows 对齐)。
150
+ func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
151
+ defer func() {
152
+ if r := recover(); r != nil {
153
+ result = nil
154
+ err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
155
+ }
156
+ }()
157
+
158
+ var args map[string]json.RawMessage
159
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
160
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
161
+ return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
162
+ }
163
+ }
164
+ argStr := func(k string) string {
165
+ if v, ok := args[k]; ok {
166
+ var s string
167
+ if json.Unmarshal(v, &s) == nil {
168
+ return s
169
+ }
170
+ }
171
+ return ""
172
+ }
173
+
174
+ switch method {
175
+ case "tray.create":
176
+ return nil, linuxTrayCreate(a, argStr("icon"), argStr("tooltip"))
177
+ case "tray.destroy":
178
+ linuxTrayDestroy()
179
+ return nil, nil
180
+ case "tray.tooltip":
181
+ linuxTray.mu.Lock()
182
+ icon := linuxTray.icon
183
+ linuxTray.mu.Unlock()
184
+ if icon == nil {
185
+ return nil, fmt.Errorf("tray: 托盘未创建")
186
+ }
187
+ tip := C.CString(argStr("tooltip"))
188
+ C.traySetTooltip(icon, tip)
189
+ C.free(unsafe.Pointer(tip))
190
+ return nil, nil
191
+ case "tray.menu":
192
+ linuxTray.mu.Lock()
193
+ icon := linuxTray.icon
194
+ linuxTray.mu.Unlock()
195
+ if icon == nil {
196
+ return nil, fmt.Errorf("tray: 托盘未创建")
197
+ }
198
+ var items []menuItem
199
+ if v, ok := args["items"]; ok {
200
+ if err := json.Unmarshal(v, &items); err != nil {
201
+ return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
202
+ }
203
+ }
204
+ linuxTraySetMenu(icon, items)
205
+ return nil, nil
206
+ case "menu.set":
207
+ return nil, fmt.Errorf("freedom: menu.set 在 linux 暂不支持(GTK 菜单栏待补)")
208
+ default:
209
+ return nil, fmt.Errorf("freedom: unknown tray method %q", method)
210
+ }
211
+ }
212
+
213
+ func linuxTrayCreate(a *App, iconDataURL, tooltip string) error {
214
+ if C.ensureGtk() == 0 {
215
+ return fmt.Errorf("freedom: 无可用显示服务(DISPLAY/Wayland),无法创建托盘")
216
+ }
217
+ png := dataURLToBytes(iconDataURL)
218
+ var icon *C.GtkStatusIcon
219
+ if len(png) > 0 {
220
+ icon = C.trayNewFromPng((*C.guint8)(unsafe.Pointer(&png[0])), C.gsize(len(png)))
221
+ } else {
222
+ name := C.CString("application-x-executable")
223
+ icon = C.trayNewNamed(name)
224
+ C.free(unsafe.Pointer(name))
225
+ }
226
+ if icon == nil {
227
+ return fmt.Errorf("freedom: 托盘图标创建失败(PNG 无法解析或无托盘协议支持)")
228
+ }
229
+ if tooltip != "" {
230
+ tip := C.CString(tooltip)
231
+ C.traySetTooltip(icon, tip)
232
+ C.free(unsafe.Pointer(tip))
233
+ }
234
+ linuxTray.mu.Lock()
235
+ old := linuxTray.icon
236
+ linuxTray.icon = icon
237
+ linuxTray.emit = a.Emit
238
+ linuxTray.mu.Unlock()
239
+ if old != nil {
240
+ C.trayDestroy(old)
241
+ }
242
+ return nil
243
+ }
244
+
245
+ func linuxTrayDestroy() {
246
+ linuxTray.mu.Lock()
247
+ icon := linuxTray.icon
248
+ linuxTray.icon = nil
249
+ linuxTray.items = map[int32]string{}
250
+ linuxTray.mu.Unlock()
251
+ if icon != nil {
252
+ C.trayDestroy(icon)
253
+ }
254
+ }
255
+
256
+ // linuxTraySetMenu 重建右键菜单并接管 icon 上的旧菜单释放(set_data_full)。
257
+ func linuxTraySetMenu(icon *C.GtkStatusIcon, items []menuItem) {
258
+ linuxTray.mu.Lock()
259
+ linuxTray.items = map[int32]string{}
260
+ var next int32
261
+ assign := func(id string) int32 {
262
+ h := next
263
+ next++
264
+ linuxTray.items[h] = id
265
+ return h
266
+ }
267
+ var build func(menu *C.GtkWidget, list []menuItem)
268
+ build = func(menu *C.GtkWidget, list []menuItem) {
269
+ for _, it := range list {
270
+ label := C.CString(it.Label)
271
+ switch {
272
+ case it.Type == "separator":
273
+ C.menuAppendSeparator(menu)
274
+ case it.Type == "submenu" || len(it.Submenu) > 0:
275
+ sub := C.menuSubmenu(menu, label)
276
+ // 父条目无动作,仅展开;子条目递归编号。
277
+ assign(it.ID)
278
+ build(sub, it.Submenu)
279
+ default:
280
+ idx := assign(it.ID)
281
+ sensitive := C.gboolean(1)
282
+ if it.Enabled != nil && !*it.Enabled {
283
+ sensitive = 0
284
+ }
285
+ if it.Checked != nil {
286
+ checked := C.gboolean(0)
287
+ if *it.Checked {
288
+ checked = 1
289
+ }
290
+ C.menuAppendCheck(menu, label, C.int(idx), checked, sensitive)
291
+ } else {
292
+ C.menuAppendItem(menu, label, C.int(idx), sensitive)
293
+ }
294
+ }
295
+ C.free(unsafe.Pointer(label))
296
+ }
297
+ }
298
+ menu := C.menuNew()
299
+ build(menu, items)
300
+ C.trayAttachMenu(icon, menu)
301
+ linuxTray.mu.Unlock()
302
+ }
@@ -1,10 +1,10 @@
1
- //go:build !windows
1
+ //go:build !windows && !linux
2
2
 
3
3
  package freedom
4
4
 
5
5
  import "fmt"
6
6
 
7
- // trayCall 非 Windows 平台暂不提供系统托盘与原生菜单栏。
7
+ // trayCall 非 Windows/Linux 平台暂不提供系统托盘与原生菜单栏。Linux 实装见 tray_linux.go(M4)。
8
8
  func (a *App) trayCall(method string, paramsJSON string) (interface{}, error) {
9
9
  return nil, fmt.Errorf("freedom: tray method %q is not supported on this platform", method)
10
10
  }
@@ -108,26 +108,18 @@ type notifyIconDataW struct {
108
108
  HBalloonIcon uintptr
109
109
  }
110
110
 
111
- // menuItem 描述一个托盘/菜单栏条目(JSON 透传)。
112
- type menuItem struct {
113
- ID string `json:"id"`
114
- Label string `json:"label"`
115
- Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
116
- Enabled *bool `json:"enabled"` // nil=默认启用
117
- Checked *bool `json:"checked"`
118
- Submenu []menuItem `json:"submenu"`
119
- }
120
-
121
111
  // trayState 保存当前托盘实例(单窗口框架内为单例)。
122
112
  type trayState struct {
123
- mu sync.Mutex
124
- hwnd uintptr // 隐藏消息窗口
125
- menu uintptr // 右键 popup 菜单
126
- menuBar uintptr // 原生菜单栏 HMENU
127
- items map[uint32]string // menu id -> 前端 id
128
- nextID uint32
129
- emit func(event string, data interface{})
130
- icon uintptr // 当前托盘图标 HICON
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
131
123
  }
132
124
 
133
125
  var (
@@ -282,32 +274,41 @@ func trayShowMenu(ts *trayState) {
282
274
  }
283
275
 
284
276
  // buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
285
- // 返回菜单句柄与 id 映射(追加到 ts.items)。
286
- func buildMenu(ts *trayState, items []menuItem) uintptr {
277
+ // 返回菜单句柄与本次分配的全部条目 id(供整体替换时清理旧映射)。
278
+ func buildMenu(ts *trayState, items []menuItem) (uintptr, []uint32) {
287
279
  hMenu, _, _ := procCreatePopupMenu.Call()
288
280
  if hMenu == 0 {
289
- return 0
281
+ return 0, nil
290
282
  }
291
- appendMenuItems(ts, hMenu, items, false)
292
- return hMenu
283
+ var ids []uint32
284
+ appendMenuItems(ts, hMenu, items, false, &ids)
285
+ return hMenu, ids
293
286
  }
294
287
 
295
- // appendMenuItems 把条目追加到指定菜单。
296
- func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool) {
297
- for i, it := range items {
288
+ // appendMenuItems 把条目追加到指定菜单;新分配的 id 记入 *ids。
289
+ func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool, ids *[]uint32) {
290
+ for _, it := range items {
298
291
  if it.Type == "separator" {
299
292
  procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
300
293
  continue
301
294
  }
302
295
  if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
303
- sub := buildMenu(ts, it.Submenu)
296
+ label, err := syscall.UTF16PtrFromString(it.Label)
297
+ if err != nil {
298
+ continue // 标签含 NUL:跳过该条目
299
+ }
300
+ sub, subIDs := buildMenu(ts, it.Submenu)
304
301
  if sub == 0 {
305
302
  continue
306
303
  }
307
- label, _ := syscall.UTF16PtrFromString(it.Label)
304
+ *ids = append(*ids, subIDs...)
308
305
  procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
309
306
  continue
310
307
  }
308
+ label, err := syscall.UTF16PtrFromString(it.Label)
309
+ if err != nil {
310
+ continue // 标签含 NUL:跳过该条目(id 不消耗)
311
+ }
311
312
  var flags uintptr = mfString | mfEnabled
312
313
  if it.Checked != nil && *it.Checked {
313
314
  flags |= mfChecked
@@ -315,46 +316,62 @@ func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar b
315
316
  if it.Enabled != nil && !*it.Enabled {
316
317
  flags |= mfGrayed
317
318
  }
319
+ ts.mu.Lock()
318
320
  id := ts.nextID
319
321
  ts.nextID++
320
- ts.mu.Lock()
321
322
  ts.items[id] = it.ID
322
323
  ts.mu.Unlock()
323
- label, _ := syscall.UTF16PtrFromString(it.Label)
324
- if isMenuBar && i == 0 {
325
- // 菜单栏首个条目用 MF_BYPOSITION 占位(顶层菜单由 SetMenu 接管)。
326
- }
324
+ *ids = append(*ids, id)
327
325
  procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
328
326
  }
329
327
  }
330
328
 
331
- // traySetMenu 设置托盘右键菜单(整体替换)。
329
+ // traySetMenu 设置托盘右键菜单(整体替换,旧 id 映射随之清理)。
332
330
  func traySetMenu(ts *trayState, items []menuItem) {
333
331
  ts.mu.Lock()
334
332
  old := ts.menu
333
+ oldIDs := ts.menuIDs
335
334
  ts.menu = 0
335
+ ts.menuIDs = nil
336
336
  ts.mu.Unlock()
337
337
  if old != 0 {
338
338
  procDestroyMenu.Call(old)
339
339
  }
340
+ releaseItemIDs(ts, oldIDs)
340
341
  if len(items) == 0 {
341
342
  return
342
343
  }
343
- h := buildMenu(ts, items)
344
+ h, ids := buildMenu(ts, items)
344
345
  ts.mu.Lock()
345
346
  ts.menu = h
347
+ ts.menuIDs = ids
346
348
  ts.mu.Unlock()
347
349
  }
348
350
 
349
- // menuBarSet 设置主窗口原生菜单栏(整体替换)。
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 映射随之清理)。
350
364
  func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
351
365
  ts.mu.Lock()
352
366
  old := ts.menuBar
367
+ oldIDs := ts.menuBarIDs
353
368
  ts.menuBar = 0
369
+ ts.menuBarIDs = nil
354
370
  ts.mu.Unlock()
355
371
  if old != 0 {
356
372
  procDestroyMenu.Call(old)
357
373
  }
374
+ releaseItemIDs(ts, oldIDs)
358
375
  if len(items) == 0 {
359
376
  procSetMenu.Call(hwnd, 0)
360
377
  procDrawMenuBar.Call(hwnd)
@@ -364,13 +381,21 @@ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
364
381
  if hBar == 0 {
365
382
  return
366
383
  }
384
+ var ids []uint32
367
385
  for _, top := range items {
368
- label, _ := syscall.UTF16PtrFromString(top.Label)
369
386
  if top.Type == "separator" {
370
387
  continue
371
388
  }
389
+ label, err := syscall.UTF16PtrFromString(top.Label)
390
+ if err != nil {
391
+ continue // 标签含 NUL:跳过该条目
392
+ }
372
393
  if len(top.Submenu) > 0 {
373
- sub := buildMenu(ts, top.Submenu)
394
+ sub, subIDs := buildMenu(ts, top.Submenu)
395
+ if sub == 0 {
396
+ continue
397
+ }
398
+ ids = append(ids, subIDs...)
374
399
  procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
375
400
  } else {
376
401
  // 顶层无子菜单:视为可点击菜单项。
@@ -378,16 +403,18 @@ func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
378
403
  if top.Checked != nil && *top.Checked {
379
404
  flags |= mfChecked
380
405
  }
406
+ ts.mu.Lock()
381
407
  id := ts.nextID
382
408
  ts.nextID++
383
- ts.mu.Lock()
384
409
  ts.items[id] = top.ID
385
410
  ts.mu.Unlock()
411
+ ids = append(ids, id)
386
412
  procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
387
413
  }
388
414
  }
389
415
  ts.mu.Lock()
390
416
  ts.menuBar = hBar
417
+ ts.menuBarIDs = ids
391
418
  ts.mu.Unlock()
392
419
  procSetMenu.Call(hwnd, hBar)
393
420
  procDrawMenuBar.Call(hwnd)
@@ -413,6 +440,9 @@ func trayDestroy(ts *trayState) {
413
440
  procDestroyMenu.Call(ts.menuBar)
414
441
  ts.menuBar = 0
415
442
  }
443
+ ts.menuIDs = nil
444
+ ts.menuBarIDs = nil
445
+ ts.items = map[uint32]string{} // 已持锁:直接重建映射,不留无主条目
416
446
  if ts.icon != 0 {
417
447
  procDestroyIcon.Call(ts.icon)
418
448
  ts.icon = 0