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.
- package/CLIENT-CONTRACT.md +467 -41
- package/README.md +15 -14
- package/client-ui/pc/app.css +74 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +40 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +247 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +72 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +217 -0
- package/client-ui/shared/conv/conv.css +329 -174
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +373 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +5554 -639
- package/client-ui-mobile/reqs.css +110 -1
- package/client-ui-mobile/reqs.js +533 -74
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +67 -0
- package/client-ui-mobile/scripts.js +599 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +69 -10
- package/cmd/magic-client/main.go +210 -32
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +157 -7
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +166 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +119 -7
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +168 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +15 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +157 -11
- package/internal/cli/session.go +75 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +297 -33
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +518 -24
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +207 -2
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +288 -0
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +290 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +31 -0
- package/internal/client/scripts_decode_test.go +130 -0
- package/internal/client/scripts_decode_windows.go +182 -0
- package/internal/client/scripts_test.go +739 -0
- package/internal/client/service.go +717 -97
- package/internal/client/service_test.go +217 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +175 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +6 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +14 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
// ab_test.go - agent-browser 封装与 shot-remote 端点的测试。
|
|
4
|
+
//
|
|
5
|
+
// 不连真 daemon(那要真 Chromium):用 go build 现场编一个**假 agent-browser exe**
|
|
6
|
+
// 验证探测链、--json 解析与端点行为。go 编译器不在 PATH 时相关用例跳过
|
|
7
|
+
// (本机开发环境一定有;CI 沙箱也总有)。
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"context"
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"io"
|
|
13
|
+
"log"
|
|
14
|
+
"net/http"
|
|
15
|
+
"net/http/httptest"
|
|
16
|
+
"net/url"
|
|
17
|
+
"os"
|
|
18
|
+
"os/exec"
|
|
19
|
+
"path/filepath"
|
|
20
|
+
"runtime"
|
|
21
|
+
"strings"
|
|
22
|
+
"testing"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// abNativeExeName 当前平台 agent-browser 原生 exe 的文件名(与 ab.go 同规则)。
|
|
26
|
+
func abNativeExeName() string {
|
|
27
|
+
name := "agent-browser-" + runtime.GOOS + "-" + runtime.GOARCH
|
|
28
|
+
if runtime.GOOS == "windows" {
|
|
29
|
+
name += ".exe"
|
|
30
|
+
}
|
|
31
|
+
return name
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// fakeABBin 现场编一个假 agent-browser exe,返回其路径。
|
|
35
|
+
//
|
|
36
|
+
// 行为由环境变量 AB_FAKE_MODE 控制(abRun 会把调用方环境透传给子进程):
|
|
37
|
+
//
|
|
38
|
+
// ok → 按子命令吐 {"success":true,"data":…};screenshot 会真写出文件
|
|
39
|
+
// err → 吐 {"success":false,"error":"fake boom"}
|
|
40
|
+
// garbage → 吐非 JSON 乱码
|
|
41
|
+
// missing → 无输出直接退出 1
|
|
42
|
+
func fakeABBin(t *testing.T) string {
|
|
43
|
+
t.Helper()
|
|
44
|
+
src := `package main
|
|
45
|
+
|
|
46
|
+
import (
|
|
47
|
+
"fmt"
|
|
48
|
+
"os"
|
|
49
|
+
"strings"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
func main() {
|
|
53
|
+
mode := os.Getenv("AB_FAKE_MODE")
|
|
54
|
+
switch mode {
|
|
55
|
+
case "err":
|
|
56
|
+
fmt.Println(` + "`" + `{"success":false,"error":"fake boom"}` + "`" + `)
|
|
57
|
+
os.Exit(1)
|
|
58
|
+
case "garbage":
|
|
59
|
+
fmt.Println("garbage not json at all")
|
|
60
|
+
os.Exit(0)
|
|
61
|
+
case "missing":
|
|
62
|
+
os.Exit(1)
|
|
63
|
+
}
|
|
64
|
+
// 按子命令给一点可断言的 data(跳过全局 flag)。
|
|
65
|
+
sub := ""
|
|
66
|
+
for i, a := range os.Args[1:] {
|
|
67
|
+
if strings.HasPrefix(a, "-") || a == "magic-client" {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
switch a {
|
|
71
|
+
case "get", "open", "eval", "pdf", "snapshot":
|
|
72
|
+
continue
|
|
73
|
+
case "screenshot":
|
|
74
|
+
// 真的把文件写出来:screenshot 的**最后一个非 flag 参数**是路径
|
|
75
|
+
// (与真 agent-browser 一致 —— flag 必须放在路径前面)。
|
|
76
|
+
out := ""
|
|
77
|
+
for _, b := range os.Args[i+1:] {
|
|
78
|
+
if !strings.HasPrefix(b, "-") {
|
|
79
|
+
out = b
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if out != "" {
|
|
83
|
+
_ = os.WriteFile(out, []byte("png"), 0o600)
|
|
84
|
+
}
|
|
85
|
+
fmt.Println(` + "`" + `{"success":true,"data":{"path":"x"}}` + "`" + `)
|
|
86
|
+
return
|
|
87
|
+
case "url":
|
|
88
|
+
fmt.Println(` + "`" + `{"success":true,"data":"https://example.com/page"}` + "`" + `)
|
|
89
|
+
return
|
|
90
|
+
case "title":
|
|
91
|
+
fmt.Println(` + "`" + `{"success":true,"data":"Example"}` + "`" + `)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if sub == "" {
|
|
95
|
+
sub = a
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
fmt.Println(` + "`" + `{"success":true,"data":{"ok":true}}` + "`" + `)
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
dir := t.TempDir()
|
|
102
|
+
srcFile := filepath.Join(dir, "main.go")
|
|
103
|
+
if err := os.WriteFile(srcFile, []byte(src), 0o600); err != nil {
|
|
104
|
+
t.Fatal(err)
|
|
105
|
+
}
|
|
106
|
+
exe := filepath.Join(dir, abNativeExeName())
|
|
107
|
+
cmd := exec.Command("go", "build", "-o", exe, srcFile)
|
|
108
|
+
if out, err := cmd.CombinedOutput(); err != nil {
|
|
109
|
+
t.Skipf("go build 假 exe 失败(跳过,不影响其它用例): %v: %s", err, out)
|
|
110
|
+
}
|
|
111
|
+
return exe
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// TestABBin探测链 环境变量 > npm 前缀 > 都没有时空。
|
|
115
|
+
func TestABBin探测链(t *testing.T) {
|
|
116
|
+
exe := fakeABBin(t)
|
|
117
|
+
// 1) 显式环境变量最优先。
|
|
118
|
+
t.Setenv(EnvAgentBrowserBin, exe)
|
|
119
|
+
if got := abBin(); got != exe {
|
|
120
|
+
t.Fatalf("环境变量应最优先,实际 %q", got)
|
|
121
|
+
}
|
|
122
|
+
// 2) npm 全局前缀(<prefix>/node_modules/agent-browser/bin/<平台名>)。
|
|
123
|
+
t.Setenv(EnvAgentBrowserBin, "")
|
|
124
|
+
prefix := t.TempDir()
|
|
125
|
+
binDir := filepath.Join(prefix, "node_modules", "agent-browser", "bin")
|
|
126
|
+
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
127
|
+
t.Fatal(err)
|
|
128
|
+
}
|
|
129
|
+
b, err := os.ReadFile(exe)
|
|
130
|
+
if err != nil {
|
|
131
|
+
t.Fatal(err)
|
|
132
|
+
}
|
|
133
|
+
if err := os.WriteFile(filepath.Join(binDir, abNativeExeName()), b, 0o755); err != nil {
|
|
134
|
+
t.Fatal(err)
|
|
135
|
+
}
|
|
136
|
+
t.Setenv("NPM_CONFIG_PREFIX", prefix)
|
|
137
|
+
got := abBin()
|
|
138
|
+
if !strings.Contains(filepath.ToSlash(got), "node_modules/agent-browser/bin/"+abNativeExeName()) {
|
|
139
|
+
t.Fatalf("应从 npm 前缀探测到,实际 %q", got)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// TestABRunJSON 解析:--json 成功 / 失败 / 乱码 / 无输出四种形状。
|
|
144
|
+
func TestABRunJSON(t *testing.T) {
|
|
145
|
+
exe := fakeABBin(t)
|
|
146
|
+
t.Setenv(EnvAgentBrowserBin, exe)
|
|
147
|
+
|
|
148
|
+
t.Setenv("AB_FAKE_MODE", "ok")
|
|
149
|
+
data, err := abRun(context.Background(), "get", "url")
|
|
150
|
+
if err != nil {
|
|
151
|
+
t.Fatalf("ok 模式失败: %v", err)
|
|
152
|
+
}
|
|
153
|
+
if string(data) != `"https://example.com/page"` {
|
|
154
|
+
t.Fatalf("data 不对: %s", data)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
t.Setenv("AB_FAKE_MODE", "err")
|
|
158
|
+
if _, err := abRun(context.Background(), "get", "url"); err == nil || !strings.Contains(err.Error(), "fake boom") {
|
|
159
|
+
t.Fatalf("err 模式应报 fake boom: %v", err)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
t.Setenv("AB_FAKE_MODE", "garbage")
|
|
163
|
+
if _, err := abRun(context.Background(), "get", "url"); err == nil {
|
|
164
|
+
t.Fatal("乱码输出应报错")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
t.Setenv("AB_FAKE_MODE", "missing")
|
|
168
|
+
if _, err := abRun(context.Background(), "get", "url"); err == nil {
|
|
169
|
+
t.Fatal("无输出退出应报错")
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TestABShot 落盘:假 exe 真写出文件,abShot 返回 nil 且文件在。
|
|
174
|
+
func TestABShot(t *testing.T) {
|
|
175
|
+
exe := fakeABBin(t)
|
|
176
|
+
t.Setenv(EnvAgentBrowserBin, exe)
|
|
177
|
+
t.Setenv("AB_FAKE_MODE", "ok")
|
|
178
|
+
out := filepath.Join(t.TempDir(), "s.png")
|
|
179
|
+
if err := abShot(context.Background(), out, true); err != nil {
|
|
180
|
+
t.Fatalf("截图失败: %v", err)
|
|
181
|
+
}
|
|
182
|
+
if b, err := os.ReadFile(out); err != nil || string(b) != "png" {
|
|
183
|
+
t.Fatalf("截图文件没落盘: %v", err)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// abHideAll 把 abBin 的全部探测路径指到空目录(测「没装 agent-browser」的分支)。
|
|
188
|
+
// t.Setenv 自动在测试结束时还原。PATH 上可能有真安装(npm 全局 bin),一并清掉 ——
|
|
189
|
+
// 清 PATH 后 go build 假 exe 的用例会受影响,但 hide 只在 501 分支用,不冲突。
|
|
190
|
+
func abHideAll(t *testing.T) {
|
|
191
|
+
t.Helper()
|
|
192
|
+
t.Setenv(EnvAgentBrowserBin, "")
|
|
193
|
+
t.Setenv("NPM_CONFIG_PREFIX", "")
|
|
194
|
+
t.Setenv("npm_config_prefix", "")
|
|
195
|
+
t.Setenv("APPDATA", t.TempDir()) // Windows 默认前缀指空
|
|
196
|
+
t.Setenv("PATH", t.TempDir()) // LookPath 兜底也指空
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// TestShotRemoteEndpoint shot-remote 端点:能力位 gate / 参数校验 / 正常路径。
|
|
200
|
+
func TestShotRemoteEndpoint(t *testing.T) {
|
|
201
|
+
exe := fakeABBin(t)
|
|
202
|
+
s := &Service{log: log.New(io.Discard, "", 0), contract: Contract{Features: []string{FeatureRequirementShots}}}
|
|
203
|
+
mux := http.NewServeMux()
|
|
204
|
+
mux.HandleFunc("/desk/requirements/shot-remote", s.handleRequirementShotRemote)
|
|
205
|
+
srv := httptest.NewServer(mux)
|
|
206
|
+
defer srv.Close()
|
|
207
|
+
|
|
208
|
+
// 没配 agent-browser → 501 + 安装提示。
|
|
209
|
+
// ⚠️ 本机可能真装了 agent-browser(npm 默认前缀 / PATH 都能命中):
|
|
210
|
+
// 「没装」分支要把**所有**探测路径都指到空目录,否则测的是真安装。
|
|
211
|
+
abHideAll(t)
|
|
212
|
+
resp, err := http.Post(srv.URL+"/desk/requirements/shot-remote", "application/json",
|
|
213
|
+
strings.NewReader(`{"url":"https://example.com"}`))
|
|
214
|
+
if err != nil {
|
|
215
|
+
t.Fatal(err)
|
|
216
|
+
}
|
|
217
|
+
body := readAllBody(resp)
|
|
218
|
+
if resp.StatusCode != http.StatusNotImplemented || !strings.Contains(body, "agent-browser") {
|
|
219
|
+
t.Fatalf("没装 agent-browser 应 501 + 提示,实际 %d %s", resp.StatusCode, body)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 配上假 exe → 正常路径回 data(截图落 uploads 目录并真实存在)。
|
|
223
|
+
t.Setenv(EnvAgentBrowserBin, exe)
|
|
224
|
+
t.Setenv("AB_FAKE_MODE", "ok")
|
|
225
|
+
t.Setenv("TMP", t.TempDir()) // uploads 目录走 os.TempDir()
|
|
226
|
+
t.Setenv("TEMP", os.Getenv("TMP"))
|
|
227
|
+
resp2, err := http.Post(srv.URL+"/desk/requirements/shot-remote", "application/json",
|
|
228
|
+
strings.NewReader(`{"url":"https://example.com"}`))
|
|
229
|
+
if err != nil {
|
|
230
|
+
t.Fatal(err)
|
|
231
|
+
}
|
|
232
|
+
body2 := readAllBody(resp2)
|
|
233
|
+
if resp2.StatusCode != http.StatusOK {
|
|
234
|
+
t.Fatalf("正常路径应 200,实际 %d %s", resp2.StatusCode, body2)
|
|
235
|
+
}
|
|
236
|
+
var out struct {
|
|
237
|
+
OK bool `json:"ok"`
|
|
238
|
+
Data string `json:"data"`
|
|
239
|
+
}
|
|
240
|
+
if err := json.Unmarshal([]byte(body2), &out); err != nil || !out.OK || !strings.HasPrefix(out.Data, "/desk/uploads/") {
|
|
241
|
+
t.Fatalf("响应形状不对: %s", body2)
|
|
242
|
+
}
|
|
243
|
+
// data 指向的文件真实存在。
|
|
244
|
+
name, err := url.PathUnescape(strings.TrimPrefix(out.Data, "/desk/uploads/"))
|
|
245
|
+
if err != nil {
|
|
246
|
+
t.Fatal(err)
|
|
247
|
+
}
|
|
248
|
+
if _, err := os.Stat(filepath.Join(os.TempDir(), "magic-client-uploads", name)); err != nil {
|
|
249
|
+
t.Fatalf("截图文件不在 uploads: %v", err)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 非法 url → 400。
|
|
253
|
+
resp3, err := http.Post(srv.URL+"/desk/requirements/shot-remote", "application/json",
|
|
254
|
+
strings.NewReader(`{"url":"javascript:alert(1)"}`))
|
|
255
|
+
if err != nil {
|
|
256
|
+
t.Fatal(err)
|
|
257
|
+
}
|
|
258
|
+
resp3.Body.Close()
|
|
259
|
+
if resp3.StatusCode != http.StatusBadRequest {
|
|
260
|
+
t.Fatalf("非法 url 应 400,实际 %d", resp3.StatusCode)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// TestShotRemote能力位 缺 feature.requirement.shots 必须 501(与 /shots 端点同一杆秤)。
|
|
265
|
+
func TestShotRemote能力位(t *testing.T) {
|
|
266
|
+
s := &Service{log: log.New(io.Discard, "", 0), contract: Contract{Features: []string{}}}
|
|
267
|
+
mux := http.NewServeMux()
|
|
268
|
+
mux.HandleFunc("/desk/requirements/shot-remote", s.handleRequirementShotRemote)
|
|
269
|
+
srv := httptest.NewServer(mux)
|
|
270
|
+
defer srv.Close()
|
|
271
|
+
resp, err := http.Post(srv.URL+"/desk/requirements/shot-remote", "application/json",
|
|
272
|
+
strings.NewReader(`{"url":"https://example.com"}`))
|
|
273
|
+
if err != nil {
|
|
274
|
+
t.Fatal(err)
|
|
275
|
+
}
|
|
276
|
+
defer resp.Body.Close()
|
|
277
|
+
if resp.StatusCode != http.StatusNotImplemented {
|
|
278
|
+
t.Fatalf("缺能力位应 501,实际 %d", resp.StatusCode)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// readAllBody 把响应体读完转成字符串(测试用,错误一律 fatal)。
|
|
283
|
+
func readAllBody(resp *http.Response) string {
|
|
284
|
+
b, _ := io.ReadAll(resp.Body)
|
|
285
|
+
resp.Body.Close()
|
|
286
|
+
return string(b)
|
|
287
|
+
}
|
package/internal/client/apk.go
CHANGED
|
@@ -105,6 +105,11 @@ func (s *Service) handleAppInfo(w http.ResponseWriter, r *http.Request) {
|
|
|
105
105
|
Version string `json:"version"`
|
|
106
106
|
Size int64 `json:"size"`
|
|
107
107
|
URL string `json:"url"`
|
|
108
|
+
/* Upgrade(2026-10-26 只增字段):这个客户端**自己能不能**做桌面端 npm 一键升级
|
|
109
|
+
(npm 在 PATH 上、找得到目标二进制)。为 true 时界面的「检测更新」会顺手问
|
|
110
|
+
/desk/core,一次把「App 有没有新版」「桌面 CLI 有没有新版」都报出来。
|
|
111
|
+
旧插件没有这个字段 → 界面只查 APK,不假装能升桌面端。 */
|
|
112
|
+
Upgrade bool `json:"upgrade"`
|
|
108
113
|
}{}
|
|
109
114
|
if s.apkAvailable() {
|
|
110
115
|
if st, err := os.Stat(s.apkPath); err == nil {
|
|
@@ -113,6 +118,9 @@ func (s *Service) handleAppInfo(w http.ResponseWriter, r *http.Request) {
|
|
|
113
118
|
out.URL = "/desk/app.apk"
|
|
114
119
|
}
|
|
115
120
|
}
|
|
121
|
+
if s.upgradableCore() {
|
|
122
|
+
out.Upgrade = true
|
|
123
|
+
}
|
|
116
124
|
writeJSON(w, http.StatusOK, out)
|
|
117
125
|
}
|
|
118
126
|
|
|
@@ -41,6 +41,7 @@ func TestAppInfoAndAPK(t *testing.T) {
|
|
|
41
41
|
Version string `json:"version"`
|
|
42
42
|
Size int64 `json:"size"`
|
|
43
43
|
URL string `json:"url"`
|
|
44
|
+
Upgrade bool `json:"upgrade"`
|
|
44
45
|
}
|
|
45
46
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
46
47
|
t.Fatal(err)
|
|
@@ -48,6 +49,12 @@ func TestAppInfoAndAPK(t *testing.T) {
|
|
|
48
49
|
if got.Version != "2.1.2" || got.URL != "/desk/app.apk" || got.Size != int64(len("fake-apk-2.1.2")) {
|
|
49
50
|
t.Fatalf("app info = %+v", got)
|
|
50
51
|
}
|
|
52
|
+
// 2026-10-26:/desk/app 要顺手告诉界面「这份客户端能不能一键升桌面端 npm」,
|
|
53
|
+
// 界面据此一次检测 APK + npm。值与本机真实探测一致(测试机上有没有 npm 不定),
|
|
54
|
+
// 不写死。
|
|
55
|
+
if got.Upgrade != s.upgradableCore() {
|
|
56
|
+
t.Fatalf("/desk/app.upgrade 与本机探测不一致:%+v", got)
|
|
57
|
+
}
|
|
51
58
|
|
|
52
59
|
rec = httptest.NewRecorder()
|
|
53
60
|
s.handleAppAPK(rec, httptest.NewRequest(http.MethodGet, "/desk/app.apk", nil))
|
|
@@ -87,6 +94,58 @@ func TestAppInfoWithoutAPK(t *testing.T) {
|
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
// 2026-10-26 用户:「版本检测需要同时考虑 apk 和 npm,都要支持升级」。
|
|
98
|
+
// 升级链路(coreupgrade.go)早就写好、单测也一直在跑,但正式路由从没挂上 ——
|
|
99
|
+
// 手机问 /desk/core 拿到的是 404 页而不是版本信息。这条钉住「真的挂上去了」。
|
|
100
|
+
func TestVersionRoutesRegistered(t *testing.T) {
|
|
101
|
+
s, _ := newUpgradeService(t)
|
|
102
|
+
h := s.Handler()
|
|
103
|
+
hdr := map[string]string{"X-Magic-Token": s.opts.Token}
|
|
104
|
+
for _, path := range []string{"/desk/core", "/desk/core/upgrade"} {
|
|
105
|
+
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
106
|
+
for k, v := range hdr {
|
|
107
|
+
req.Header.Set(k, v)
|
|
108
|
+
}
|
|
109
|
+
rec := httptest.NewRecorder()
|
|
110
|
+
h.ServeHTTP(rec, req)
|
|
111
|
+
if rec.Code == http.StatusNotFound {
|
|
112
|
+
t.Fatalf("%s 没挂到正式路由上(404)", path)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// GET /desk/core 的响应形状:界面按这些字段决定「能不能一键升」。
|
|
116
|
+
req := httptest.NewRequest(http.MethodGet, "/desk/core", nil)
|
|
117
|
+
rec := httptest.NewRecorder()
|
|
118
|
+
h.ServeHTTP(rec, req)
|
|
119
|
+
if rec.Code != http.StatusOK {
|
|
120
|
+
t.Fatalf("/desk/core code = %d: %s", rec.Code, rec.Body.String())
|
|
121
|
+
}
|
|
122
|
+
var info coreInfo
|
|
123
|
+
if err := json.Unmarshal(rec.Body.Bytes(), &info); err != nil {
|
|
124
|
+
t.Fatal(err)
|
|
125
|
+
}
|
|
126
|
+
if !info.CanUpgrade || !info.Upgradable || info.Latest != "2.4.2" {
|
|
127
|
+
t.Fatalf("/desk/core = %+v", info)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 找得到 npm + 目标二进制时,/desk/app 必须明说「能升桌面端」,界面才会去问 /desk/core。
|
|
132
|
+
func TestAppInfoReportsNpmUpgradeAvailability(t *testing.T) {
|
|
133
|
+
s, _ := newUpgradeService(t)
|
|
134
|
+
apk := writeTestAPK(t, t.TempDir(), "2.4.1")
|
|
135
|
+
s.apkPath = apk
|
|
136
|
+
rec := httptest.NewRecorder()
|
|
137
|
+
s.handleAppInfo(rec, httptest.NewRequest(http.MethodGet, "/desk/app", nil))
|
|
138
|
+
var got struct {
|
|
139
|
+
Upgrade bool `json:"upgrade"`
|
|
140
|
+
}
|
|
141
|
+
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
if !got.Upgrade {
|
|
145
|
+
t.Fatalf("/desk/app.upgrade = false,但假 npm 与目标二进制都在:%s", rec.Body.String())
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
func TestAPKVersionFallsBackToFilename(t *testing.T) {
|
|
91
150
|
dir := t.TempDir()
|
|
92
151
|
apk := filepath.Join(dir, "magic-agent-mobile-3.2.1-release.apk")
|