magic-agent 2.4.3 → 2.6.3

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 (207) hide show
  1. package/CLIENT-CONTRACT.md +467 -41
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +74 -0
  4. package/client-ui/pc/js/04-task-list.js +5 -1
  5. package/client-ui/pc/js/07-chat-stream-render.js +3 -3
  6. package/client-ui/pc/js/90-glue.js +40 -2
  7. package/client-ui/pc/js/92-composer.js +19 -4
  8. package/client-ui/pc/js/93-rail.js +16 -5
  9. package/client-ui/shared/conv/02-media-kinds.js +2 -2
  10. package/client-ui/shared/conv/03-chat-message.js +247 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +72 -7
  13. package/client-ui/shared/conv/19-attachments.js +157 -6
  14. package/client-ui/shared/conv/27-conv-ledger.js +5 -2
  15. package/client-ui/shared/conv/29-msg-variants.js +217 -0
  16. package/client-ui/shared/conv/conv.css +329 -174
  17. package/client-ui/shared/conv/dom-map.md +1 -0
  18. package/client-ui/shared/conversation.css +13 -2
  19. package/client-ui/shared/conversation.js +373 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/apps.css +16 -0
  22. package/client-ui-mobile/apps.js +406 -0
  23. package/client-ui-mobile/index.html +5554 -639
  24. package/client-ui-mobile/reqs.css +110 -1
  25. package/client-ui-mobile/reqs.js +533 -74
  26. package/client-ui-mobile/reqshot.js +505 -0
  27. package/client-ui-mobile/scripts.css +67 -0
  28. package/client-ui-mobile/scripts.js +599 -0
  29. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  30. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  31. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  32. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  33. package/cmd/magic-client/browsectl.go +199 -0
  34. package/cmd/magic-client/clientctl.go +69 -10
  35. package/cmd/magic-client/main.go +210 -32
  36. package/internal/agent/ask.go +45 -0
  37. package/internal/agent/ask_test.go +39 -0
  38. package/internal/agent/attachments_test.go +6 -2
  39. package/internal/agent/capability.go +7 -1
  40. package/internal/agent/claude.go +20 -15
  41. package/internal/agent/codebuddy.go +125 -34
  42. package/internal/agent/codebuddy_account_test.go +54 -0
  43. package/internal/agent/codebuddy_stream_test.go +103 -0
  44. package/internal/agent/credits.go +713 -0
  45. package/internal/agent/credits_minimax_test.go +97 -0
  46. package/internal/agent/credits_test.go +211 -0
  47. package/internal/agent/custom_models.go +155 -0
  48. package/internal/agent/custom_models_test.go +125 -0
  49. package/internal/agent/dsh.go +123 -49
  50. package/internal/agent/dsh_caps_cache_test.go +184 -0
  51. package/internal/agent/dsh_resume.go +157 -7
  52. package/internal/agent/dsh_test.go +108 -6
  53. package/internal/agent/engine.go +37 -1
  54. package/internal/agent/engine_base.go +21 -17
  55. package/internal/agent/engine_base_test.go +2 -2
  56. package/internal/agent/engine_test.go +47 -86
  57. package/internal/agent/env.go +173 -1
  58. package/internal/agent/env_test.go +114 -0
  59. package/internal/agent/fallback.go +313 -0
  60. package/internal/agent/fallback_test.go +340 -0
  61. package/internal/agent/launch.go +43 -1
  62. package/internal/agent/limitnotice.go +207 -1
  63. package/internal/agent/limitnotice_test.go +140 -0
  64. package/internal/agent/llmhttp.go +46 -4
  65. package/internal/agent/llmhttp_test.go +157 -0
  66. package/internal/agent/models.go +53 -1
  67. package/internal/agent/models_test.go +20 -5
  68. package/internal/agent/pidalive_windows_test.go +40 -0
  69. package/internal/agent/runcmd.go +26 -0
  70. package/internal/agent/runcmd_windows.go +36 -5
  71. package/internal/agent/stream.go +166 -6
  72. package/internal/agent/stream_test.go +25 -38
  73. package/internal/agent/stream_timeline_test.go +2 -37
  74. package/internal/agent/streamjson.go +119 -7
  75. package/internal/agent/tools_suffix_test.go +6 -3
  76. package/internal/agent/trae.go +50 -243
  77. package/internal/agent/trae_exec.go +433 -0
  78. package/internal/agent/trae_exec_test.go +181 -0
  79. package/internal/agent/wraplaunch_test.go +113 -0
  80. package/internal/cli/ask.go +168 -37
  81. package/internal/cli/ask_test.go +1 -1
  82. package/internal/cli/checkin.go +77 -0
  83. package/internal/cli/clientcmd.go +3 -2
  84. package/internal/cli/control.go +46 -5
  85. package/internal/cli/control_test.go +65 -0
  86. package/internal/cli/engines_test.go +103 -0
  87. package/internal/cli/events.go +15 -0
  88. package/internal/cli/events_test.go +50 -0
  89. package/internal/cli/judge.go +104 -0
  90. package/internal/cli/quotas.go +125 -0
  91. package/internal/cli/requirements.go +51 -0
  92. package/internal/cli/root.go +157 -11
  93. package/internal/cli/session.go +75 -3
  94. package/internal/cli/session_test.go +82 -3
  95. package/internal/cli/sessionlog.go +6 -0
  96. package/internal/cli/sessionlog_test.go +22 -2
  97. package/internal/cli/suggest.go +7 -5
  98. package/internal/cli/team.go +37 -9
  99. package/internal/cli/team_test.go +39 -0
  100. package/internal/client/ab.go +372 -0
  101. package/internal/client/ab_test.go +287 -0
  102. package/internal/client/apk.go +8 -0
  103. package/internal/client/apk_test.go +59 -0
  104. package/internal/client/apps.go +1078 -0
  105. package/internal/client/apps_test.go +516 -0
  106. package/internal/client/browse.go +84 -36
  107. package/internal/client/browse_test.go +107 -9
  108. package/internal/client/browseinput.go +481 -0
  109. package/internal/client/browseinput_test.go +216 -0
  110. package/internal/client/corecmd.go +23 -0
  111. package/internal/client/corecmd_other.go +8 -0
  112. package/internal/client/corecmd_windows.go +24 -0
  113. package/internal/client/coreexe.go +196 -0
  114. package/internal/client/coreexe_test.go +161 -0
  115. package/internal/client/coreupgrade.go +866 -0
  116. package/internal/client/coreupgrade_test.go +561 -0
  117. package/internal/client/daemon.go +297 -33
  118. package/internal/client/daemon_test.go +33 -0
  119. package/internal/client/engines_checkin_test.go +57 -0
  120. package/internal/client/fallback_flags_test.go +64 -0
  121. package/internal/client/feishu.go +1259 -0
  122. package/internal/client/feishu_test.go +649 -0
  123. package/internal/client/health_identity_test.go +84 -0
  124. package/internal/client/hostenv.go +68 -0
  125. package/internal/client/hostenv_test.go +37 -0
  126. package/internal/client/jobs.go +518 -24
  127. package/internal/client/jobs_auto.go +571 -0
  128. package/internal/client/jobs_auto_test.go +431 -0
  129. package/internal/client/jobs_test.go +299 -2
  130. package/internal/client/knowledge.go +854 -0
  131. package/internal/client/knowledge_test.go +510 -0
  132. package/internal/client/lan.go +3 -3
  133. package/internal/client/plugins.go +5 -1
  134. package/internal/client/prefs.go +67 -0
  135. package/internal/client/procstart_linux.go +59 -0
  136. package/internal/client/publicurl.go +82 -0
  137. package/internal/client/quotas_merge_test.go +107 -0
  138. package/internal/client/requirements.go +207 -2
  139. package/internal/client/requirements_shots_test.go +200 -0
  140. package/internal/client/requirements_test.go +103 -0
  141. package/internal/client/restart.go +288 -0
  142. package/internal/client/restart_handoff_test.go +230 -0
  143. package/internal/client/restart_test.go +290 -0
  144. package/internal/client/scripts.go +1047 -0
  145. package/internal/client/scripts_decode_implausible.go +38 -0
  146. package/internal/client/scripts_decode_other.go +31 -0
  147. package/internal/client/scripts_decode_test.go +130 -0
  148. package/internal/client/scripts_decode_windows.go +182 -0
  149. package/internal/client/scripts_test.go +739 -0
  150. package/internal/client/service.go +717 -97
  151. package/internal/client/service_test.go +217 -1
  152. package/internal/client/sessions_cache.go +148 -0
  153. package/internal/client/sessions_cache_test.go +163 -0
  154. package/internal/client/sessions_fold.go +165 -0
  155. package/internal/client/sessions_fold_test.go +204 -0
  156. package/internal/client/singleinstance.go +470 -0
  157. package/internal/client/singleinstance_test.go +607 -0
  158. package/internal/client/suggest.go +105 -2
  159. package/internal/client/suggest_test.go +86 -0
  160. package/internal/client/team_flags.go +25 -11
  161. package/internal/client/team_flags_test.go +39 -0
  162. package/internal/client/testmain_test.go +88 -0
  163. package/internal/client/workspace_default_test.go +158 -0
  164. package/internal/client/workspacelockenv_test.go +54 -0
  165. package/internal/config/config.go +48 -0
  166. package/internal/judge/judge.go +315 -0
  167. package/internal/requirements/bootstrap.go +16 -0
  168. package/internal/requirements/consolidate.go +100 -9
  169. package/internal/requirements/doc.go +154 -3
  170. package/internal/requirements/graph.go +138 -0
  171. package/internal/requirements/memory.go +7 -0
  172. package/internal/requirements/modules.go +312 -0
  173. package/internal/requirements/modules_test.go +373 -0
  174. package/internal/requirements/relations.go +155 -0
  175. package/internal/requirements/relations_test.go +231 -0
  176. package/internal/requirements/requirements.go +10 -0
  177. package/internal/requirements/shots.go +418 -0
  178. package/internal/requirements/shots_test.go +387 -0
  179. package/internal/session/multiclient_test.go +368 -0
  180. package/internal/session/session.go +175 -11
  181. package/internal/session/session_test.go +7 -0
  182. package/internal/session/status.go +21 -0
  183. package/internal/session/status_test.go +28 -0
  184. package/internal/session/title.go +16 -0
  185. package/internal/session/title_test.go +26 -0
  186. package/internal/session/workspace_lock.go +265 -28
  187. package/internal/suggest/suggest.go +23 -1
  188. package/internal/team/event.go +3 -1
  189. package/internal/team/execute.go +21 -6
  190. package/internal/team/fallback_route_test.go +80 -0
  191. package/internal/team/planner.go +15 -3
  192. package/internal/team/route.go +89 -17
  193. package/internal/team/team.go +18 -4
  194. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  195. package/npm/apk/version.json +6 -3
  196. package/npm/dist/darwin-arm64/magic-agent +0 -0
  197. package/npm/dist/darwin-arm64/magic-client +0 -0
  198. package/npm/dist/darwin-x64/magic-agent +0 -0
  199. package/npm/dist/darwin-x64/magic-client +0 -0
  200. package/npm/dist/linux-arm64/magic-agent +0 -0
  201. package/npm/dist/linux-arm64/magic-client +0 -0
  202. package/npm/dist/linux-x64/magic-agent +0 -0
  203. package/npm/dist/linux-x64/magic-client +0 -0
  204. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  205. package/npm/dist/win32-x64/magic-client.exe +0 -0
  206. package/npm/install.js +11 -0
  207. package/package.json +14 -3
@@ -0,0 +1,199 @@
1
+ // browse 驱动端:`magic-client browse <动作> …`。
2
+ //
3
+ // 「内置浏览器 MCP」的驱动/截图面(2026-10-25 用户定调):基于 **agent-browser**
4
+ // 插件(vercel-labs,Rust 原生 CLI,驱动真实 Chromium/CDP)实现。
5
+ //
6
+ // 与旧实现的差别:不再连本插件的 /browse-input WS 派发**合成事件**(isTrusted=false,
7
+ // 反自动化站点忽略),而是 spawn agent-browser 原生 exe —— 真实事件、CDP 级截图、
8
+ // PDF、a11y 快照。手机端「看页面 + 触控」仍走 /b/ 中转 + relay 远程鼠标(不在此列)。
9
+ //
10
+ // 用法:
11
+ //
12
+ // magic-client browse open <url> # 打开页面(daemon 未起自动拉起)
13
+ // magic-client browse state # 打印当前页面状态(标题/地址)
14
+ // magic-client browse click <x> <y> # 在页面坐标 (x,y) 点击
15
+ // magic-client browse move <x> <y> # 移动指针(不点击)
16
+ // magic-client browse dblclick <x> <y> # 双击
17
+ // magic-client browse scroll <dx> <dy> # 滚动(正数向下/右)
18
+ // magic-client browse type <文本> # 向焦点元素输入文本
19
+ // magic-client browse key <键名> # 如 Enter / Escape / a
20
+ // magic-client browse shot [--out 文件] [--full-page] # 截当前页面,存成 PNG(默认 shot.png)
21
+ // magic-client browse eval <js> # 在页面里跑一段 JS
22
+ // magic-client browse pdf <文件> # 存成 PDF
23
+ // magic-client browse snap [-i] # a11y 快照(-i 只出可交互元素)
24
+ //
25
+ // 会话固定 --session magic-client(与人类/其它工具的 default 会话隔离)。
26
+ // 探测:MAGIC_AGENT_AGENT_BROWSER_BIN > npm 全局前缀 > PATH;都没有时报安装提示。
27
+ package main
28
+
29
+ import (
30
+ "context"
31
+ "encoding/json"
32
+ "fmt"
33
+ "os"
34
+ "strconv"
35
+ "strings"
36
+
37
+ "github.com/darren/magic-agent/internal/client"
38
+ )
39
+
40
+ // browseTimeout 兼容旧名(新的超时在 client.ABTimeout)。
41
+ const browseTimeout = client.ABTimeout
42
+
43
+ func runBrowseDriver(args []string) int {
44
+ fs := newFlagSet("browse")
45
+ _ = fs.Parse(args)
46
+ rest := fs.Args()
47
+
48
+ if len(rest) == 0 {
49
+ fmt.Fprintln(os.Stderr, "用法: magic-client browse open|state|click|move|dblclick|scroll|type|key|shot|eval|pdf|snap …")
50
+ return 2
51
+ }
52
+
53
+ action := strings.ToLower(rest[0])
54
+ argv := rest[1:]
55
+
56
+ switch action {
57
+ case "open":
58
+ if len(argv) < 1 {
59
+ return browseUsage("open 需要 <url>")
60
+ }
61
+ return browseCall("open "+argv[0], func() (any, error) {
62
+ return nil, client.ABOpen(context.Background(), argv[0])
63
+ })
64
+ case "state":
65
+ return browseCall("state", func() (any, error) {
66
+ url, title, err := client.ABState(context.Background())
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ return map[string]any{"url": url, "title": title}, nil
71
+ })
72
+ case "click", "move", "dblclick":
73
+ if len(argv) < 2 {
74
+ return browseUsage(action + " 需要 x y")
75
+ }
76
+ x, e1 := strconv.ParseFloat(argv[0], 64)
77
+ y, e2 := strconv.ParseFloat(argv[1], 64)
78
+ if e1 != nil || e2 != nil {
79
+ return browseUsage(action + " 的坐标必须是数字")
80
+ }
81
+ return browseCall(action, func() (any, error) {
82
+ return nil, client.ABMouse(context.Background(), action, x, y)
83
+ })
84
+ case "scroll":
85
+ if len(argv) < 2 {
86
+ return browseUsage("scroll 需要 dx dy")
87
+ }
88
+ dx, e1 := strconv.ParseFloat(argv[0], 64)
89
+ dy, e2 := strconv.ParseFloat(argv[1], 64)
90
+ if e1 != nil || e2 != nil {
91
+ return browseUsage("scroll 的参数必须是数字")
92
+ }
93
+ return browseCall("scroll", func() (any, error) {
94
+ return nil, client.ABScroll(context.Background(), dx, dy)
95
+ })
96
+ case "type":
97
+ if len(argv) < 1 {
98
+ return browseUsage("type 需要 文本")
99
+ }
100
+ text := strings.Join(argv, " ")
101
+ return browseCall("type", func() (any, error) {
102
+ return nil, client.ABType(context.Background(), text)
103
+ })
104
+ case "key":
105
+ if len(argv) < 1 {
106
+ return browseUsage("key 需要 键名(如 Enter)")
107
+ }
108
+ return browseCall("key", func() (any, error) {
109
+ return nil, client.ABKey(context.Background(), argv[0])
110
+ })
111
+ case "shot":
112
+ return browseShot(argv)
113
+ case "eval":
114
+ if len(argv) < 1 {
115
+ return browseUsage("eval 需要 <js>")
116
+ }
117
+ return browseCall("eval", func() (any, error) {
118
+ return client.ABEval(context.Background(), strings.Join(argv, " "))
119
+ })
120
+ case "pdf":
121
+ if len(argv) < 1 {
122
+ return browseUsage("pdf 需要 <文件>")
123
+ }
124
+ out := argv[len(argv)-1]
125
+ return browseCall("pdf", func() (any, error) {
126
+ return nil, client.ABPDF(context.Background(), out)
127
+ })
128
+ case "snap":
129
+ interactive := false
130
+ for _, a := range argv {
131
+ if a == "-i" || a == "--interactive" {
132
+ interactive = true
133
+ }
134
+ }
135
+ return browseCall("snap", func() (any, error) {
136
+ return client.ABSnap(context.Background(), interactive)
137
+ })
138
+ default:
139
+ return browseUsage("未知动作 " + action)
140
+ }
141
+ }
142
+
143
+ func browseUsage(msg string) int {
144
+ fmt.Fprintln(os.Stderr, "magic-client browse:", msg)
145
+ return 2
146
+ }
147
+
148
+ // browseCall 统一跑一个 agent-browser 动作并把结果打印/报错。
149
+ func browseCall(name string, fn func() (any, error)) int {
150
+ v, err := fn()
151
+ if err != nil {
152
+ fmt.Fprintf(os.Stderr, "magic-client browse %s: %v\n", name, err)
153
+ return 1
154
+ }
155
+ if v == nil {
156
+ fmt.Println("ok")
157
+ return 0
158
+ }
159
+ out, merr := json.MarshalIndent(v, "", " ")
160
+ if merr != nil {
161
+ fmt.Println("ok")
162
+ return 0
163
+ }
164
+ fmt.Println(string(out))
165
+ return 0
166
+ }
167
+
168
+ // browseShot 截当前页面并存成 PNG(agent-browser 真实 Chromium 截图,直接落盘)。
169
+ func browseShot(argv []string) int {
170
+ out := "shot.png"
171
+ fullPage := false
172
+ for i := 0; i < len(argv); i++ {
173
+ switch {
174
+ case argv[i] == "--out" && i+1 < len(argv):
175
+ out = argv[i+1]
176
+ i++
177
+ case strings.HasPrefix(argv[i], "--out="):
178
+ out = strings.TrimPrefix(argv[i], "--out=")
179
+ case argv[i] == "--full-page" || argv[i] == "--fullpage":
180
+ fullPage = true
181
+ case strings.HasPrefix(argv[i], "--"):
182
+ // 未知 flag 忽略
183
+ default:
184
+ out = argv[i] // 位置参数也认(`browse shot a.png`)
185
+ }
186
+ }
187
+ v, err := browseShotRun(out, fullPage)
188
+ if err != nil {
189
+ fmt.Fprintln(os.Stderr, "magic-client browse shot:", err)
190
+ return 1
191
+ }
192
+ _ = v
193
+ fmt.Println(out)
194
+ return 0
195
+ }
196
+
197
+ func browseShotRun(out string, fullPage bool) (any, error) {
198
+ return nil, client.ABShot(context.Background(), out, fullPage)
199
+ }
@@ -47,7 +47,7 @@ func writeJSONStdout(v any) {
47
47
  _ = enc.Encode(v)
48
48
  }
49
49
 
50
- // runClientCommand 处理 start / stop / status;未知动作返回 false 交给 serve 分支。
50
+ // runClientCommand 处理 start / stop / status / restart;未知动作返回 false 交给 serve 分支。
51
51
  func runClientCommand(action string, args []string) (handled bool, code int) {
52
52
  switch action {
53
53
  case "start":
@@ -56,6 +56,8 @@ func runClientCommand(action string, args []string) (handled bool, code int) {
56
56
  return true, runClientStop(args)
57
57
  case "status":
58
58
  return true, runClientStatus(args)
59
+ case "restart":
60
+ return true, runClientRestart(args)
59
61
  }
60
62
  return false, 0
61
63
  }
@@ -75,11 +77,13 @@ const clientUsage = `magic-client - magic-agent 的客户端插件(桌面 /
75
77
  用法:
76
78
  magic-client serve [flags] 前台运行(看日志 / 排障用)
77
79
  magic-client start [flags] 后台常驻(关掉终端也还在),打印手机可用的地址
80
+ magic-client restart 按当前参数重启后台实例(端口 / 令牌 / 目录沿用,手机不用改地址)
78
81
  magic-client status [--json] 看状态(是否在跑、端口、手机地址、健康检查)
79
82
  magic-client stop 停掉后台实例(本来没跑也算成功)
80
83
 
81
- 手机要连**必须**用 start --lan(或 serve --lan):默认只绑 127.0.0.1,同机可用、手机不可达。
82
- --lan 时令牌是唯一门锁,至少 24 字符;start 会复用 ~/.magic-client/token,跨重启稳定
84
+ 手机要连**默认就行**:start / serve 默认绑 0.0.0.0:9100(--addr 可覆盖;端口跨重启稳定,
85
+ 手机里的地址不用改)。只想本机访问加 --local(绑 127.0.0.1:0)。
86
+ 局域网模式下令牌是唯一门锁,至少 24 字符;start 会复用 ~/.magic-client/token,跨重启稳定
83
87
  (手机里存的是「地址 + 令牌」,每次换就等于配对作废)。
84
88
 
85
89
  serve 的 flags 见 ` + "`magic-client serve --help`" + `;start 另有 --state / --log / --wait。
@@ -87,13 +91,14 @@ serve 的 flags 见 ` + "`magic-client serve --help`" + `;start 另有 --state
87
91
 
88
92
  func runClientStart(args []string) int {
89
93
  fs := newFlagSet("start")
90
- lan := fs.Bool("lan", false, "绑到局域网(0.0.0.0),让手机 / 平板连得上。⚠️ 开了之后令牌是唯一门锁")
91
- addr := fs.String("addr", "", "监听地址(默认:带 --lan 时 0.0.0.0:0,否则 127.0.0.1:0)")
94
+ lan := fs.Bool("lan", true, "绑到局域网(默认 0.0.0.0:9100),让手机 / 平板连得上。⚠️ 令牌是唯一门锁")
95
+ localOnly := fs.Bool("local", false, "只绑 127.0.0.1(关闭默认的局域网监听,手机连不上)")
96
+ addr := fs.String("addr", "", "监听地址(默认 0.0.0.0:9100;带 --local 时 127.0.0.1:0)")
92
97
  profilePath := fs.String("profile", "", "profile JSON 路径(空 = 内置默认)")
93
98
  uiDir := fs.String("ui-dir", "", "桌面端界面目录(空 = 自动找与本程序同级的 client-ui)")
94
99
  mobileDir := fs.String("mobile-dir", "", "移动端界面目录(空 = 自动找同级的 client-ui-mobile),/m/ 由它提供")
95
100
  proxyAddr := fs.String("proxy-addr", "", "SOCKS5 出口监听地址(移动端 VPN 隧道核心用),如 0.0.0.0:1080")
96
- token := fs.String("token", "", "访问令牌(空 = 复用 ~/.magic-client/token,没有就生成;--lan 时至少 24 字符)")
101
+ token := fs.String("token", "", "访问令牌(空 = 复用 ~/.magic-client/token,没有就生成;局域网模式至少 24 字符)")
97
102
  statePath := fs.String("state", "", "状态文件路径(默认 ~/.magic-client/client.json;也可用 MAGIC_CLIENT_STATE)")
98
103
  logPath := fs.String("log", "", "后台日志路径(默认 ~/.magic-client/logs/client.log;也可用 MAGIC_CLIENT_LOG)")
99
104
  wait := fs.Duration("wait", 15*time.Second, "等后台服务报端口的时长(超时即判定启动失败)")
@@ -115,7 +120,7 @@ func runClientStart(args []string) int {
115
120
  Token: *token,
116
121
  ProxyAddr: *proxyAddr,
117
122
  BrowseUA: *browseUA,
118
- Lan: *lan,
123
+ Lan: *lan && !*localOnly,
119
124
  StatePath: *statePath,
120
125
  LogPath: *logPath,
121
126
  Wait: *wait,
@@ -131,9 +136,18 @@ func runClientStart(args []string) int {
131
136
  func runClientStop(args []string) int {
132
137
  fs := newFlagSet("stop")
133
138
  statePath := fs.String("state", "", "状态文件路径(默认 ~/.magic-client/client.json)")
139
+ profilePath := fs.String("profile", "", "profile JSON 路径(决定单实例锁在哪个数据目录;空 = 内置默认)")
134
140
  _ = fs.Parse(args)
135
141
 
136
- stopped, st, err := client.StopDaemon(client.StopOptions{StatePath: *statePath})
142
+ /* AlsoLockHolder:前台 serve 起的实例、以及旧版(*.stale)起的实例都不写状态文件,
143
+ 只读状态文件的 stop 会答「本来就没在跑」,而端口与隧道实际还占着 —— 用户此时
144
+ 唯一的自救途径就是这条。DataDir / AppID 决定去查哪两处登记。 */
145
+ stopped, st, err := client.StopDaemon(client.StopOptions{
146
+ StatePath: *statePath,
147
+ AlsoLockHolder: true,
148
+ DataDir: dataDirOf(*profilePath),
149
+ AppID: appIDOf(*profilePath),
150
+ })
137
151
  if err != nil {
138
152
  fmt.Fprintln(os.Stderr, "magic-client:", err)
139
153
  return 1
@@ -149,6 +163,51 @@ func runClientStop(args []string) int {
149
163
  return 0
150
164
  }
151
165
 
166
+ // runClientRestart 按**当前实例自己的参数**重启:停旧 → 起新(见 client.RestartDaemon)。
167
+ //
168
+ // 参数全部来自状态文件,所以这里只接 --state 与 --delay:
169
+ // 手机点「重启服务」时由服务端 spawn 本动作,--delay 给那次 HTTP 响应留落地时间。
170
+ func runClientRestart(args []string) int {
171
+ fs := newFlagSet("restart")
172
+ statePath := fs.String("state", "", "状态文件路径(默认 ~/.magic-client/client.json)")
173
+ profilePath := fs.String("profile", "", "profile JSON 路径(决定单实例锁在哪个数据目录;空 = 内置默认)")
174
+ delay := fs.Duration("delay", 0, "停旧实例前等多久(服务端自重启用,给响应留时间)")
175
+ _ = fs.Parse(args)
176
+
177
+ st, err := client.RestartDaemon(client.RestartOptions{
178
+ StatePath: *statePath,
179
+ Delay: *delay,
180
+ DataDir: dataDirOf(*profilePath),
181
+ })
182
+ if err != nil {
183
+ fmt.Fprintln(os.Stderr, "magic-client:", err)
184
+ return 1
185
+ }
186
+ printStarted(st)
187
+ return 0
188
+ }
189
+
190
+ // dataDirOf 按 profile 算数据目录(单实例锁的位置由它决定)。
191
+ //
192
+ // 读不到 profile 就返回空串 —— 调用方会回落到「从状态文件目录推」的老行为,
193
+ // 不能因为 profile 路径写错就让 `stop` 变成「什么都不做」。
194
+ func dataDirOf(profilePath string) string {
195
+ prof, err := client.LoadProfile(profilePath)
196
+ if err != nil {
197
+ return ""
198
+ }
199
+ return prof.DataDir()
200
+ }
201
+
202
+ // appIDOf 按 profile 算 AppID(AppID 级占用登记的位置由它决定)。
203
+ func appIDOf(profilePath string) string {
204
+ prof, err := client.LoadProfile(profilePath)
205
+ if err != nil {
206
+ return ""
207
+ }
208
+ return prof.AppID()
209
+ }
210
+
152
211
  func runClientStatus(args []string) int {
153
212
  fs := newFlagSet("status")
154
213
  statePath := fs.String("state", "", "状态文件路径(默认 ~/.magic-client/client.json)")
@@ -181,7 +240,7 @@ func printStarted(st *client.DaemonState) {
181
240
  fmt.Printf(" 本机 %s\n", st.URL)
182
241
  switch {
183
242
  case len(st.LanAddrs) == 0:
184
- fmt.Println(" 局域网 (本次只绑了 127.0.0.1,手机连不上;要手机连请加 --lan)")
243
+ fmt.Println(" 局域网 (本次用了 --local,只绑 127.0.0.1;去掉 --local 重启即可让手机连)")
185
244
  default:
186
245
  for _, u := range st.LanAddrs {
187
246
  fmt.Printf(" 手机 %s\n", u)
@@ -214,7 +273,7 @@ func printStatus(st *client.DaemonStatus) {
214
273
  fmt.Printf(" 手机 %s\n", u)
215
274
  }
216
275
  if len(s.LanAddrs) == 0 {
217
- fmt.Println(" 局域网 (未绑局域网,手机连不上)")
276
+ fmt.Println(" 局域网 (未绑局域网,本次用了 --local)")
218
277
  }
219
278
  if st.Health != nil {
220
279
  fmt.Printf(" 应用 %s|契约 v%d|引擎 %d 个\n",