magic-agent 2.3.2 → 2.4.2

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 (35) hide show
  1. package/CLIENT-CONTRACT.md +30 -4
  2. package/README.md +1 -1
  3. package/client-ui/shared/conv/07-stream-delegation.js +15 -0
  4. package/client-ui/shared/conversation.js +39 -0
  5. package/client-ui/shared/transport-http.js +10 -0
  6. package/client-ui-mobile/index.html +431 -46
  7. package/cmd/magic-client/main.go +5 -0
  8. package/internal/agent/engine.go +2 -2
  9. package/internal/agent/llmengine.go +0 -8
  10. package/internal/agent/llmengine_test.go +0 -17
  11. package/internal/agent/llmmodels.go +0 -35
  12. package/internal/client/cors_test.go +32 -0
  13. package/internal/client/instance.go +59 -0
  14. package/internal/client/jobs.go +1160 -0
  15. package/internal/client/jobs_test.go +369 -0
  16. package/internal/client/service.go +251 -141
  17. package/internal/client/service_test.go +46 -1
  18. package/internal/session/session.go +15 -1
  19. package/internal/session/workspace_lock.go +165 -0
  20. package/internal/session/workspace_lock_test.go +90 -0
  21. package/internal/suggest/suggest.go +8 -25
  22. package/internal/suggest/suggest_test.go +0 -6
  23. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  24. package/npm/apk/version.json +3 -3
  25. package/npm/dist/darwin-arm64/magic-agent +0 -0
  26. package/npm/dist/darwin-arm64/magic-client +0 -0
  27. package/npm/dist/darwin-x64/magic-agent +0 -0
  28. package/npm/dist/darwin-x64/magic-client +0 -0
  29. package/npm/dist/linux-arm64/magic-agent +0 -0
  30. package/npm/dist/linux-arm64/magic-client +0 -0
  31. package/npm/dist/linux-x64/magic-agent +0 -0
  32. package/npm/dist/linux-x64/magic-client +0 -0
  33. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  34. package/npm/dist/win32-x64/magic-client.exe +0 -0
  35. package/package.json +1 -1
@@ -62,6 +62,7 @@ func main() {
62
62
  proxyAddr = fs.String("proxy-addr", "", "SOCKS5 出口监听地址(供移动端 VPN 隧道核心使用),如 0.0.0.0:1080")
63
63
  browseUA = fs.String("browse-ua", "", "内置浏览器中转的 User-Agent:mobile(默认)/ desktop / 任意自定义串")
64
64
  readyFile = fs.String("ready-file", "", "监听成功后把 {pid,port,url,lanAddrs} 原子写到这个路径(`magic-client start` 用它拿端口,别手工传)")
65
+ anyOrigin = fs.Bool("allow-any-origin", false, "对任意 Origin 回 CORS 头(默认只放行本机页面)。--lan 时自动开启;令牌仍是唯一门锁")
65
66
  )
66
67
  _ = fs.Parse(os.Args[2:])
67
68
 
@@ -90,6 +91,10 @@ func main() {
90
91
  Token: tok,
91
92
  Lan: *lan,
92
93
  BrowseUA: *browseUA,
94
+ /* 绑到局域网 = 「允许来自任意 IP 的页面访问」:再按 loopback 卡 CORS Origin
95
+ 等于把手机 / 别的设备的页面全判成跨域(用户侧就是「只能本地访问」)。
96
+ 显式 --allow-any-origin 也可以单独打开(绑回环但要给容器页面用)。 */
97
+ AllowAnyOrigin: *anyOrigin || *lan,
93
98
  })
94
99
  if err != nil {
95
100
  // 契约不匹配是**要立刻停**的错误:带着不匹配的能力跑起来只会让人以为是界面 bug。
@@ -159,8 +159,8 @@ type Request struct {
159
159
  // codex -c model_reasoning_effort="none" + model_reasoning_summary="none"
160
160
  // codebuddy settings alwaysThinkingEnabled=false(官方 settings / SDK thinking:disabled)
161
161
  // dsh InitializeParams.reasoningEffort="off"(官方 SDK;dsh-llm-deepseek 会发 thinking.type=disabled)
162
- // llm 选同线 thinking.disabled 变体;没有变体时传 reasoning_effort="none"
163
- // (CLI 委托路径用 llm 的官方 -o reasoning_effort none)
162
+ // llm 直连请求体传 reasoning_effort="none";CLI 委托路径传
163
+ // llm 官方 -o reasoning_effort none。不自动换模型。
164
164
  // 其余引擎没有已知开关;调用方(建议生成)仍会标记,失败后再降级 llm。
165
165
  NoThinking bool
166
166
 
@@ -213,11 +213,6 @@ func (e *LLMEngine) ListModels(ctx context.Context) ([]string, error) {
213
213
  // --json 输出同 `llm logs --json` 的结构(数组,每项含 response /
214
214
  // input_tokens / output_tokens),解析失败时退回纯文本路径,不引入新的失败点。
215
215
  func (e *LLMEngine) Complete(ctx context.Context, req Request) (Response, error) {
216
- // --disable-thinking:优先换成同一条线上的 no-think 变体(MiniMax-M3.1-Flash
217
- // 这类模型对 thinking.disabled 直接 400,不能靠请求体硬塞)。
218
- if req.NoThinking {
219
- req.Model = NoThinkLLMModel(req.Model)
220
- }
221
216
  // 配置文件模型(models.json):命中 id → 直连端点(见 llmhttp.go)。
222
217
  // 为什么必须按 id:id 唯一,而真实模型名会重复(MiniMax-M3 与 minimax-nothink
223
218
  // 真实名相同,只差 extraBody)—— 用名字做参数就无法区分。
@@ -328,9 +323,6 @@ func parseLLMJSONEntry(raw string) (llmJSONEntry, bool) {
328
323
  // Stream 实现 Streamer:llm prompt 默认流式,逐行读纯文本 stdout,
329
324
  // thinkSplitter 把标签块路由到 thinking 通道。
330
325
  func (e *LLMEngine) Stream(ctx context.Context, req Request, onEvent func(StreamEvent)) (StreamResult, error) {
331
- if req.NoThinking {
332
- req.Model = NoThinkLLMModel(req.Model)
333
- }
334
326
  // 配置文件模型:命中 id → 直连 SSE(见 llmhttp.go / completeConfigured 上方注释)
335
327
  if m, ok := ResolveLLMConfigModel(req.Model); ok {
336
328
  return e.streamConfigured(ctx, m, req, onEvent)
@@ -556,20 +556,3 @@ func TestLLMDetectMissingCli(t *testing.T) {
556
556
  t.Errorf("note = %q, want the missing path", note)
557
557
  }
558
558
  }
559
-
560
- func TestNoThinkLLMModelPicksAnyVariant(t *testing.T) {
561
- dir := t.TempDir()
562
- p := filepath.Join(dir, "models.json")
563
- raw := `[
564
- {"id":"MiniMax-M3.1-Flash","model":"MiniMax-M3.1-Flash","url":"http://x/v1","apiKey":"k"},
565
- {"id":"minimax-nothink","model":"MiniMax-M3","url":"http://x/v1","apiKey":"k",
566
- "extraBody":{"thinking":{"type":"disabled"}}}
567
- ]`
568
- if err := os.WriteFile(p, []byte(raw), 0o600); err != nil {
569
- t.Fatal(err)
570
- }
571
- t.Setenv("MAGIC_AGENT_MODELS", p)
572
- if got := NoThinkLLMModel("MiniMax-M3.1-Flash"); got != "minimax-nothink" {
573
- t.Fatalf("NoThinkLLMModel = %q, want minimax-nothink", got)
574
- }
575
- }
@@ -89,41 +89,6 @@ func LLMModelDisablesThinking(m LLMConfigModel) bool {
89
89
  return strings.EqualFold(strings.TrimSpace(fmt.Sprint(th["type"])), "disabled")
90
90
  }
91
91
 
92
- // NoThinkLLMModel 给 --disable-thinking 选 no-think 变体:
93
- // 用户已选 no-think → 原样;否则先找 WireModel 相同、声明 thinking.disabled 的条目,
94
- // 再退到任意 no-think 条目(M3.1-Flash 只能换模型才能关思考)。都没有则保留原模型
95
- // (由调用方决定是否再传 reasoning_effort=none)。
96
- func NoThinkLLMModel(selected string) string {
97
- m, ok := ResolveLLMConfigModel(selected)
98
- if !ok {
99
- return strings.TrimSpace(selected)
100
- }
101
- if LLMModelDisablesThinking(m) {
102
- return m.ID
103
- }
104
- list, err := LoadLLMConfigModels()
105
- if err != nil {
106
- return m.ID
107
- }
108
- // 先找同一条线上的变体(不换真实模型),再退到任意 no-think 变体 ——
109
- // 显式 --disable-thinking 的目标是「不思考」,MiniMax-M3.1-Flash 这类模型
110
- // 对 thinking.disabled / reasoning_effort=none 直接 400,只能换到 no-think 条目。
111
- for _, alt := range list {
112
- if alt.ID == m.ID || alt.WireModel() != m.WireModel() {
113
- continue
114
- }
115
- if LLMModelDisablesThinking(alt) {
116
- return alt.ID
117
- }
118
- }
119
- for _, alt := range list {
120
- if alt.ID != m.ID && LLMModelDisablesThinking(alt) {
121
- return alt.ID
122
- }
123
- }
124
- return m.ID
125
- }
126
-
127
92
  // LLMModelsPath 返回 models.json 的路径($MAGIC_AGENT_MODELS 优先)。
128
93
  func LLMModelsPath() string {
129
94
  if p := strings.TrimSpace(os.Getenv("MAGIC_AGENT_MODELS")); p != "" {
@@ -97,6 +97,38 @@ func TestCORSForLocalOrigins(t *testing.T) {
97
97
  })
98
98
  }
99
99
 
100
+ /* 写方法(DELETE / PUT)必须出现在预检的 Allow-Methods 里(2026-10-23)。
101
+ *
102
+ * 为什么单独钉:容器里的页面源是 https://localhost,接口在局域网 IP,天生跨源;
103
+ * 删除会话(DELETE)、重命名 / 置顶同步(PUT)都带自定义头 X-Magic-Token ⇒ 必发预检。
104
+ * 预检里没点名的方法,浏览器会判定「未授权」而**不发出真请求** —— 用户侧就是
105
+ * 「确认删除了,但什么都没发生」。上面那条测试只协商 GET,所以一直没照到这里。
106
+ */
107
+ func TestCORSPreflightAllowsWriteMethods(t *testing.T) {
108
+ svc := newTestService(t, fakeCore(t, goodContract))
109
+ srv := httptest.NewServer(svc.Handler())
110
+ defer srv.Close()
111
+
112
+ for _, method := range []string{"DELETE", "PUT", "POST"} {
113
+ req, err := http.NewRequest(http.MethodOptions, srv.URL+"/desk/session/x", nil)
114
+ if err != nil {
115
+ t.Fatal(err)
116
+ }
117
+ req.Header.Set("Origin", "https://localhost")
118
+ req.Header.Set("Access-Control-Request-Method", method)
119
+ req.Header.Set("Access-Control-Request-Headers", "x-magic-token")
120
+ res, err := http.DefaultClient.Do(req)
121
+ if err != nil {
122
+ t.Fatal(err)
123
+ }
124
+ res.Body.Close()
125
+ allowed := res.Header.Get("Access-Control-Allow-Methods")
126
+ if !containsFold(allowed, method) {
127
+ t.Fatalf("预检必须允许 %s,得到 Allow-Methods=%q(浏览器会拦掉真请求)", method, allowed)
128
+ }
129
+ }
130
+ }
131
+
100
132
  func containsFold(s, sub string) bool {
101
133
  return len(s) >= len(sub) && (indexFold(s, sub) >= 0)
102
134
  }
@@ -0,0 +1,59 @@
1
+ package client
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "sync"
10
+ )
11
+
12
+ /*
13
+ 服务实例的稳定身份(2026-10-23)。
14
+
15
+ 为什么需要:同一台桌面可能有多条入口(公司本地 127.0.0.1、局域网 IP、cloudflared
16
+ 公网域名),它们在客户端看来是不同 URL。移动端要用「这是不是同一台桌面」来决定
17
+ 队列与设置要不要共用一份 —— 只看地址会把同一台机器拆成好几个环境。
18
+
19
+ id 落在 profile 的 DataDir 下(`instance-id`),第一次生成后不再变:
20
+
21
+ · 改端口 / 换隧道域名 / 手机换 Wi-Fi 都不影响;
22
+ · 只删 `instance-id` 才会换身份(等同于「这是一台新桌面」)。
23
+
24
+ /desk/health 把它报给客户端(`instanceId`,只增字段)。老客户端忽略;
25
+ 新客户端据此把同一台桌面的多个入口并成一个环境身份。
26
+ */
27
+ type instanceIDCache struct {
28
+ once sync.Once
29
+ id string
30
+ }
31
+
32
+ func (s *Service) instanceID() string {
33
+ s.inst.once.Do(func() {
34
+ dir := s.opts.Profile.DataDir()
35
+ if strings.TrimSpace(dir) == "" {
36
+ return
37
+ }
38
+ path := filepath.Join(dir, "instance-id")
39
+ if b, err := os.ReadFile(path); err == nil {
40
+ if v := strings.TrimSpace(string(b)); len(v) >= 8 {
41
+ s.inst.id = v
42
+ return
43
+ }
44
+ }
45
+ var buf [8]byte
46
+ if _, err := rand.Read(buf[:]); err != nil {
47
+ return
48
+ }
49
+ id := hex.EncodeToString(buf[:])
50
+ if err := os.MkdirAll(dir, 0o700); err != nil {
51
+ return
52
+ }
53
+ if err := os.WriteFile(path, []byte(id+"\n"), 0o600); err != nil {
54
+ return
55
+ }
56
+ s.inst.id = id
57
+ })
58
+ return s.inst.id
59
+ }