@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,70 @@
1
+ package freedom
2
+
3
+ // 跨平台纯函数助手(M4 起):Windows/Linux 两侧分发器共用的校验与解析,
4
+ // 无平台依赖,从 *_windows.go 抽出以单一实现(原 windows 侧引用不变)。
5
+
6
+ import (
7
+ "encoding/base64"
8
+ "encoding/json"
9
+ "os"
10
+ "strings"
11
+ )
12
+
13
+ // shellTargetAllowed 校验 openExternal 目标:仅放行 http/https/mailto URL。
14
+ // 前端传入不可全信,白名单之外的 scheme 与本地路径一律拒绝
15
+ // (Windows 侧 ShellExecuteW 可执行任意合法字符串;Linux 侧 xdg-open 同理)。
16
+ func shellTargetAllowed(target string) bool {
17
+ lower := strings.ToLower(target)
18
+ return strings.HasPrefix(lower, "http://") ||
19
+ strings.HasPrefix(lower, "https://") ||
20
+ strings.HasPrefix(lower, "mailto:")
21
+ }
22
+
23
+ // reservedSchemes 是系统/浏览器保留协议:注册它们会劫持网页链接或 shell 行为,一律拒绝。
24
+ var reservedSchemes = map[string]bool{
25
+ "http": true, "https": true, "file": true, "ftp": true, "mailto": true,
26
+ "shell": true, "search-ms": true, "javascript": true, "data": true,
27
+ "about": true, "resource": true, "res": true, "mhtml": true, "ms-appx": true,
28
+ }
29
+
30
+ // protocolSchemeAllowed 判定 scheme 是否允许注册(保留名单 + ms-/microsoft. 前缀排除)。
31
+ func protocolSchemeAllowed(scheme string) bool {
32
+ lower := strings.ToLower(scheme)
33
+ if reservedSchemes[lower] {
34
+ return false
35
+ }
36
+ return !strings.HasPrefix(lower, "ms-") && !strings.HasPrefix(lower, "microsoft.")
37
+ }
38
+
39
+ func validScheme(s string) bool {
40
+ if s == "" || !((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z')) {
41
+ return false
42
+ }
43
+ for i := 1; i < len(s); i++ {
44
+ c := s[i]
45
+ if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
46
+ c == '+' || c == '-' || c == '.') {
47
+ return false
48
+ }
49
+ }
50
+ return true
51
+ }
52
+
53
+ // launchArgsJSON 返回进程启动参数(不含 exe 自身),供前端解析 deep link。
54
+ func launchArgsJSON() json.RawMessage {
55
+ b, _ := json.Marshal(os.Args[1:])
56
+ return b
57
+ }
58
+
59
+ // dataURLToBytes 解析 "data:...;base64,XXXX" 为原始字节。
60
+ func dataURLToBytes(dataURL string) []byte {
61
+ idx := strings.Index(dataURL, "base64,")
62
+ if idx < 0 {
63
+ return nil
64
+ }
65
+ b, err := base64.StdEncoding.DecodeString(dataURL[idx+len("base64,"):])
66
+ if err != nil {
67
+ return nil
68
+ }
69
+ return b
70
+ }
@@ -0,0 +1,426 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "fmt"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+ "syscall"
11
+ "unsafe"
12
+ )
13
+
14
+ // W2 系统集成(对标 Tauri 官方插件集):
15
+ // - 热键组合解析(global-shortcut)
16
+ // - 剪贴板读写(clipboard-manager)
17
+ // - 系统通知 Toast(notification)
18
+ // - openExternal(opener/shell.open)
19
+ // - 开机自启(autostart,HKCU Run 键)
20
+ // - URL Scheme 注册(deep-link,HKCU Classes)
21
+ // 前端入口统一走 __freedom_sys(sysCapCall 分发)。
22
+
23
+ // ---- 热键 ----
24
+
25
+ const (
26
+ modAlt = 0x0001
27
+ modControl = 0x0002
28
+ modShift = 0x0004
29
+ modWin = 0x0008
30
+ )
31
+
32
+ var (
33
+ hotkeyModifiers = map[string]uint32{
34
+ "alt": modAlt, "option": modAlt,
35
+ "ctrl": modControl, "control": modControl,
36
+ "shift": modShift,
37
+ "win": modWin, "cmd": modWin, "meta": modWin, "super": modWin,
38
+ }
39
+ hotkeyVKs = map[string]uint32{}
40
+ )
41
+
42
+ func init() {
43
+ for c := 'A'; c <= 'Z'; c++ {
44
+ hotkeyVKs[string(rune(c))] = uint32(c) // VK_A..VK_Z 与 ASCII 大写字母同值
45
+ }
46
+ for d := '0'; d <= '9'; d++ {
47
+ hotkeyVKs[string(d)] = uint32(d) // VK_0..VK_9 与 ASCII 数字同值
48
+ }
49
+ for i := 1; i <= 24; i++ {
50
+ hotkeyVKs[fmt.Sprintf("F%d", i)] = uint32(0x70 + i - 1)
51
+ }
52
+ extra := map[string]uint32{
53
+ "SPACE": 0x20, "ENTER": 0x0D, "RETURN": 0x0D, "TAB": 0x09,
54
+ "ESC": 0x1B, "ESCAPE": 0x1B, "BACKSPACE": 0x08, "DELETE": 0x2E, "DEL": 0x2E,
55
+ "INSERT": 0x2D, "HOME": 0x24, "END": 0x23, "PAGEUP": 0x21, "PRIOR": 0x21,
56
+ "PAGEDOWN": 0x22, "NEXT": 0x22, "LEFT": 0x25, "UP": 0x26, "RIGHT": 0x27, "DOWN": 0x28,
57
+ "PRINTSCREEN": 0x2C, "SNAPSHOT": 0x2C,
58
+ }
59
+ for k, v := range extra {
60
+ hotkeyVKs[k] = v
61
+ }
62
+ }
63
+
64
+ // parseHotkey 解析 "ctrl+alt+k" 风格组合 → (modifiers, vk)。
65
+ func parseHotkey(combo string) (uint32, uint32, error) {
66
+ parts := strings.Split(combo, "+")
67
+ key := strings.ToUpper(strings.TrimSpace(parts[len(parts)-1]))
68
+ var mods uint32
69
+ for _, m := range parts[:len(parts)-1] {
70
+ flag, ok := hotkeyModifiers[strings.ToLower(strings.TrimSpace(m))]
71
+ if !ok {
72
+ return 0, 0, fmt.Errorf("未知修饰键 %q", m)
73
+ }
74
+ mods |= flag
75
+ }
76
+ vk, ok := hotkeyVKs[key]
77
+ if !ok {
78
+ return 0, 0, fmt.Errorf("未知按键 %q", key)
79
+ }
80
+ return mods, vk, nil
81
+ }
82
+
83
+ // ---- 剪贴板 ----
84
+
85
+ const (
86
+ cfText = 1
87
+ cfUnicodeText = 13
88
+ gmemMoveable = 0x0002
89
+ )
90
+
91
+ // clipboardReadText 读取系统剪贴板文本(优先 CF_UNICODETEXT,回退 CF_TEXT)。
92
+ func clipboardReadText() (string, error) {
93
+ if r, _, e := procOpenClipboard.Call(0); r == 0 {
94
+ return "", fmt.Errorf("freedom: OpenClipboard: %w", e)
95
+ }
96
+ defer procCloseClipboard.Call()
97
+ for _, fmtID := range []uintptr{cfUnicodeText, cfText} {
98
+ av, _, _ := procIsClipboardFormatAvai.Call(fmtID)
99
+ if av == 0 {
100
+ continue
101
+ }
102
+ h, _, _ := procGetClipboardData.Call(fmtID)
103
+ if h == 0 {
104
+ continue
105
+ }
106
+ p, _, _ := procGlobalLock.Call(h)
107
+ if p == 0 {
108
+ continue
109
+ }
110
+ var s string
111
+ size, _, _ := procGlobalSize.Call(h)
112
+ if size == 0 {
113
+ procGlobalUnlock.Call(h)
114
+ continue
115
+ }
116
+ if fmtID == cfUnicodeText {
117
+ u16 := unsafe.Slice((*uint16)(unsafe.Pointer(p)), size/2)
118
+ s = syscall.UTF16ToString(u16)
119
+ } else {
120
+ b := unsafe.Slice((*byte)(unsafe.Pointer(p)), size)
121
+ n := 0
122
+ for n < len(b) && b[n] != 0 {
123
+ n++
124
+ }
125
+ s = string(b[:n])
126
+ }
127
+ procGlobalUnlock.Call(h)
128
+ return s, nil
129
+ }
130
+ return "", nil
131
+ }
132
+
133
+ // clipboardWriteText 写入系统剪贴板文本。
134
+ // ownerHwnd 为剪贴板属主窗口(OpenClipboard 要求有效句柄,传 0 会静默失去属主语义)。
135
+ func clipboardWriteText(text string, ownerHwnd uintptr) error {
136
+ if ownerHwnd == 0 {
137
+ return fmt.Errorf("freedom: 剪贴板写入缺少属主窗口句柄")
138
+ }
139
+ u16, err := syscall.UTF16FromString(text)
140
+ if err != nil {
141
+ return fmt.Errorf("freedom: 剪贴板文本含 NUL: %w", err)
142
+ }
143
+ if r, _, e := procOpenClipboard.Call(ownerHwnd); r == 0 {
144
+ return fmt.Errorf("freedom: OpenClipboard: %w", e)
145
+ }
146
+ defer procCloseClipboard.Call()
147
+ if r, _, e := procEmptyClipboard.Call(); r == 0 {
148
+ return fmt.Errorf("freedom: EmptyClipboard: %w", e)
149
+ }
150
+ size := uintptr(len(u16) * 2)
151
+ h, _, e := procGlobalAlloc.Call(gmemMoveable, size)
152
+ if h == 0 {
153
+ return fmt.Errorf("freedom: GlobalAlloc: %w", e)
154
+ }
155
+ p, _, _ := procGlobalLock.Call(h)
156
+ if p == 0 {
157
+ procGlobalFree.Call(h)
158
+ return fmt.Errorf("freedom: GlobalLock 失败")
159
+ }
160
+ copy(unsafe.Slice((*uint16)(unsafe.Pointer(p)), len(u16)), u16)
161
+ procGlobalUnlock.Call(h)
162
+ // SetClipboardData 成功后系统接管内存所有权,不得再 GlobalFree。
163
+ if r, _, e := procSetClipboardData.Call(cfUnicodeText, h); r == 0 {
164
+ procGlobalFree.Call(h)
165
+ return fmt.Errorf("freedom: SetClipboardData: %w", e)
166
+ }
167
+ return nil
168
+ }
169
+
170
+ // ---- 系统通知(Toast via PowerShell WinRT 投影)----
171
+
172
+ // xmlEscape 转义 XML 文本节点内容(通知标题/正文注入面)。
173
+ func xmlEscape(s string) string {
174
+ r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;", "'", "&apos;")
175
+ return r.Replace(s)
176
+ }
177
+
178
+ // psSingleQuote 把字符串转义为 PowerShell 单引号字面量。
179
+ func psSingleQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
180
+
181
+ func showToast(title, body string) error {
182
+ toastXML := fmt.Sprintf(
183
+ `<toast><visual><binding template="ToastGeneric"><text>%s</text><text>%s</text></binding></visual></toast>`,
184
+ xmlEscape(title), xmlEscape(body))
185
+ // Windows.UI.Notifications 经 PowerShell 投影;AppId 用宿主 exe 名。
186
+ appID := "Freedom.App"
187
+ script := fmt.Sprintf(`[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; `+
188
+ `$xml = [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]::new(); `+
189
+ `$xml.LoadXml(%s); `+
190
+ `$t = [Windows.UI.Notifications.ToastNotification]::new($xml); `+
191
+ `[Windows.UI.Notifications.ToastNotificationManager]::GetToastNotifier(%s).Show($t)`,
192
+ psSingleQuote(toastXML), psSingleQuote(appID))
193
+ cmd := exec.Command("powershell.exe", "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script)
194
+ hideWindow(cmd)
195
+ if out, err := cmd.CombinedOutput(); err != nil {
196
+ return fmt.Errorf("freedom: 通知发送失败: %w (%s)", err, strings.TrimSpace(string(out)))
197
+ }
198
+ return nil
199
+ }
200
+
201
+ // ---- openExternal ----
202
+
203
+ // shellOpen 用系统默认关联程序打开 URL(白名单见 shellTargetAllowed,SW_SHOWNORMAL=1)。
204
+ func shellOpen(target string) error {
205
+ if target == "" {
206
+ return fmt.Errorf("freedom: shell.open 缺少 target")
207
+ }
208
+ if !shellTargetAllowed(target) {
209
+ return fmt.Errorf("freedom: 拒绝打开非白名单目标 %q(仅允许 http/https/mailto)", target)
210
+ }
211
+ p, err := syscall.UTF16PtrFromString(target)
212
+ if err != nil {
213
+ return fmt.Errorf("freedom: target 含 NUL")
214
+ }
215
+ r, _, e := procShellExecuteW.Call(0,
216
+ uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("open"))),
217
+ uintptr(unsafe.Pointer(p)), 0, 0, 1)
218
+ if int64(uintptr(r)) <= 32 {
219
+ return fmt.Errorf("freedom: ShellExecuteW(%s) 失败: code=%d (%w)", target, int64(int32(uint32(r))), e)
220
+ }
221
+ return nil
222
+ }
223
+
224
+ // ---- 开机自启(HKCU ...\Run)----
225
+
226
+ const (
227
+ hkeyCurrentUser = uintptr(0x80000001)
228
+ // KEY_SET_VALUE(0x2)|KEY_QUERY_VALUE(0x1)|KEY_WOW64_64KEY(0x100)
229
+ keyWrite = 0x103
230
+ keyRead = 0x101
231
+ regSz = 1
232
+
233
+ runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
234
+ classesBase = `Software\Classes`
235
+ )
236
+
237
+ func regOpen(base uintptr, path string, access uint32) (uintptr, error) {
238
+ p, err := syscall.UTF16PtrFromString(path)
239
+ if err != nil {
240
+ return 0, err
241
+ }
242
+ var hk uintptr
243
+ if hr, _, e := procRegOpenKeyExW.Call(base, uintptr(unsafe.Pointer(p)), 0, uintptr(access),
244
+ uintptr(unsafe.Pointer(&hk))); hr != 0 {
245
+ return 0, fmt.Errorf("RegOpenKeyExW(%s): errno=%d (%w)", path, hr, e)
246
+ }
247
+ return hk, nil
248
+ }
249
+
250
+ func regSetSz(hk uintptr, name, value string) error {
251
+ np, err := syscall.UTF16PtrFromString(name)
252
+ if err != nil {
253
+ return err
254
+ }
255
+ vb, err := syscall.UTF16FromString(value)
256
+ if err != nil {
257
+ return err
258
+ }
259
+ hr, _, e := procRegSetValueExW.Call(hk, uintptr(unsafe.Pointer(np)), 0, regSz,
260
+ uintptr(unsafe.Pointer(&vb[0])), uintptr(len(vb)*2))
261
+ if hr != 0 {
262
+ return fmt.Errorf("RegSetValueExW(%s): errno=%d (%w)", name, hr, e)
263
+ }
264
+ return nil
265
+ }
266
+
267
+ func regQuerySz(hk uintptr, name string) (string, bool, error) {
268
+ np, err := syscall.UTF16PtrFromString(name)
269
+ if err != nil {
270
+ return "", false, err
271
+ }
272
+ const max = 4096
273
+ buf := make([]uint16, max)
274
+ var sz uint32 = max * 2
275
+ var kind uint32
276
+ hr, _, e := procRegQueryValueExW.Call(hk, uintptr(unsafe.Pointer(np)), 0,
277
+ uintptr(unsafe.Pointer(&kind)), uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&sz)))
278
+ if hr == 2 { // ERROR_FILE_NOT_FOUND
279
+ return "", false, nil
280
+ }
281
+ if hr != 0 {
282
+ return "", false, fmt.Errorf("RegQueryValueExW(%s): errno=%d (%w)", name, hr, e)
283
+ }
284
+ return syscall.UTF16ToString(buf), true, nil
285
+ }
286
+
287
+ // setAutostart 写入/删除 HKCU Run 启动项;args 为附加启动参数。
288
+ // 名称经 sanitizeName 收敛;删除/覆盖前先做属主校验——现值指向非本程序时拒绝,
289
+ // 防止前端传入任意 name 破坏其他软件的启动项。
290
+ func setAutostart(name string, enabled bool, args string) error {
291
+ name = sanitizeName(name)
292
+ if name == "" {
293
+ return fmt.Errorf("freedom: 自启项名称为空或非法")
294
+ }
295
+ hk, err := regOpen(hkeyCurrentUser, runKeyPath, keyWrite)
296
+ if err != nil {
297
+ return err
298
+ }
299
+ defer procRegCloseKey.Call(hk)
300
+ exePath, err := os.Executable()
301
+ if err != nil {
302
+ return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
303
+ }
304
+ cur, found, err := regQuerySz(hk, name)
305
+ if err != nil {
306
+ return err
307
+ }
308
+ if found && !valueOwnedBySelf(cur, exePath) {
309
+ return fmt.Errorf("freedom: 启动项 %q 已存在且不属于本程序(值为 %q),拒绝覆盖", name, cur)
310
+ }
311
+ if !enabled {
312
+ if !found {
313
+ return nil
314
+ }
315
+ np, err := syscall.UTF16PtrFromString(name)
316
+ if err != nil {
317
+ return err
318
+ }
319
+ if hr, _, _ := procRegDeleteValueW.Call(hk, uintptr(unsafe.Pointer(np))); hr != 0 && hr != 2 {
320
+ return fmt.Errorf("RegDeleteValueW(%s): errno=%d", name, hr)
321
+ }
322
+ return nil
323
+ }
324
+ value := `"` + exePath + `"`
325
+ if a := strings.TrimSpace(args); a != "" {
326
+ value += " " + a
327
+ }
328
+ return regSetSz(hk, name, value)
329
+ }
330
+
331
+ // valueOwnedBySelf 判断 Run 键值是否指向本 exe(大小写不敏感前缀匹配引号内路径)。
332
+ func valueOwnedBySelf(value, exePath string) bool {
333
+ v := strings.ToLower(strings.TrimSpace(value))
334
+ p := strings.ToLower(exePath)
335
+ return strings.HasPrefix(v, `"`+p+`"`) || strings.HasPrefix(v, p)
336
+ }
337
+
338
+ func getAutostart(name string) (bool, error) {
339
+ name = sanitizeName(name)
340
+ if name == "" {
341
+ return false, fmt.Errorf("freedom: 自启项名称为空或非法")
342
+ }
343
+ hk, err := regOpen(hkeyCurrentUser, runKeyPath, keyRead)
344
+ if err != nil {
345
+ return false, err
346
+ }
347
+ defer procRegCloseKey.Call(hk)
348
+ _, found, err := regQuerySz(hk, name)
349
+ return found, err
350
+ }
351
+
352
+ // ---- URL Scheme(deep link 注册表侧)----
353
+
354
+ // registerProtocol 在 HKCU\Software\Classes 下注册 URL Scheme:
355
+ // 之后系统内任意处打开 "<scheme>:..." 都会带参数拉起本 exe。
356
+ func registerProtocol(scheme, displayName string) error {
357
+ if scheme == "" || !validScheme(scheme) {
358
+ return fmt.Errorf("freedom: 非法 scheme %q(需匹配 ^[a-zA-Z][a-zA-Z0-9+.-]*$)", scheme)
359
+ }
360
+ if !protocolSchemeAllowed(scheme) {
361
+ return fmt.Errorf("freedom: 拒绝注册保留 scheme %q", scheme)
362
+ }
363
+ exePath, err := os.Executable()
364
+ if err != nil {
365
+ return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
366
+ }
367
+ base := classesBase + `\` + scheme
368
+ // HKCU\Software 以 KEY_WRITE 打开后逐级 RegCreateKeyEx 更稳,
369
+ // 这里用 RegCreateKeyExW(advapi32)保证多级自动创建。
370
+ root, err := regCreate(hkeyCurrentUser, base)
371
+ if err != nil {
372
+ return err
373
+ }
374
+ defer procRegCloseKey.Call(root)
375
+ if displayName == "" {
376
+ displayName = "URL:" + scheme
377
+ }
378
+ if err := regSetSz(root, "", displayName); err != nil {
379
+ return err
380
+ }
381
+ if err := regSetSz(root, "URL Protocol", ""); err != nil {
382
+ return err
383
+ }
384
+ cmdKey, err := regCreate(root, `shell\open\command`)
385
+ if err != nil {
386
+ return err
387
+ }
388
+ defer procRegCloseKey.Call(cmdKey)
389
+ return regSetSz(cmdKey, "", `"`+exePath+`" "%1"`)
390
+ }
391
+
392
+ func unregisterProtocol(scheme string) error {
393
+ if scheme == "" || !validScheme(scheme) {
394
+ return fmt.Errorf("freedom: 非法 scheme %q", scheme)
395
+ }
396
+ if !protocolSchemeAllowed(scheme) {
397
+ return fmt.Errorf("freedom: 拒绝删除保留 scheme %q", scheme)
398
+ }
399
+ path := classesBase + `\` + scheme
400
+ sp, err := syscall.UTF16PtrFromString(path)
401
+ if err != nil {
402
+ return err
403
+ }
404
+ // SHDeleteKeyW 递归删除整棵子树(advapi 无递归删除,Win7 前 API)。
405
+ hr, _, e := procSHDeleteKeyW.Call(hkeyCurrentUser, uintptr(unsafe.Pointer(sp)))
406
+ if hr != 0 {
407
+ return fmt.Errorf("SHDeleteKeyW(%s): errno=%d (%w)", scheme, int64(int32(uint32(hr))), e)
408
+ }
409
+ return nil
410
+ }
411
+
412
+ // regCreate 逐级创建(打开已存在的键)并返回句柄。
413
+ func regCreate(base uintptr, path string) (uintptr, error) {
414
+ p, err := syscall.UTF16PtrFromString(path)
415
+ if err != nil {
416
+ return 0, err
417
+ }
418
+ var hk uintptr
419
+ var disp uint32
420
+ hr, _, e := procRegCreateKeyExW.Call(base, uintptr(unsafe.Pointer(p)), 0, 0, 0,
421
+ keyWrite, 0, uintptr(unsafe.Pointer(&hk)), uintptr(unsafe.Pointer(&disp)))
422
+ if hr != 0 {
423
+ return 0, fmt.Errorf("RegCreateKeyExW(%s): errno=%d (%w)", path, hr, e)
424
+ }
425
+ return hk, nil
426
+ }
@@ -0,0 +1,12 @@
1
+ package freedom
2
+
3
+ // menuItem 描述一个托盘/菜单栏条目(JSON 透传)。M4 起为跨平台公共结构
4
+ // (Windows 托盘/菜单栏与 Linux GTK 托盘共用;原声明在 tray_windows.go)。
5
+ type menuItem struct {
6
+ ID string `json:"id"`
7
+ Label string `json:"label"`
8
+ Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
9
+ Enabled *bool `json:"enabled"` // nil=默认启用
10
+ Checked *bool `json:"checked"`
11
+ Submenu []menuItem `json:"submenu"`
12
+ }