magic-agent 2.4.2 → 2.6.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 (156) hide show
  1. package/CLIENT-CONTRACT.md +290 -30
  2. package/README.md +15 -14
  3. package/client-ui/pc/app.css +32 -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 +37 -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 +241 -23
  11. package/client-ui/shared/conv/06-ask-answer.js +75 -1
  12. package/client-ui/shared/conv/09-stream-apply.js +63 -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 +81 -0
  16. package/client-ui/shared/conv/conv.css +79 -19
  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 +336 -50
  20. package/client-ui/shared/transport-http.js +14 -0
  21. package/client-ui-mobile/index.html +2932 -333
  22. package/client-ui-mobile/reqs.css +102 -0
  23. package/client-ui-mobile/reqs.js +485 -70
  24. package/client-ui-mobile/reqshot.js +505 -0
  25. package/client-ui-mobile/scripts.css +21 -0
  26. package/client-ui-mobile/scripts.js +539 -0
  27. package/client-ui-mobile/vendor/cose-base.js +3214 -0
  28. package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
  29. package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
  30. package/client-ui-mobile/vendor/layout-base.js +5230 -0
  31. package/cmd/magic-client/browsectl.go +199 -0
  32. package/cmd/magic-client/clientctl.go +33 -9
  33. package/cmd/magic-client/main.go +57 -26
  34. package/internal/agent/ask.go +45 -0
  35. package/internal/agent/ask_test.go +39 -0
  36. package/internal/agent/attachments_test.go +6 -2
  37. package/internal/agent/capability.go +7 -1
  38. package/internal/agent/claude.go +20 -15
  39. package/internal/agent/codebuddy.go +125 -34
  40. package/internal/agent/codebuddy_account_test.go +54 -0
  41. package/internal/agent/codebuddy_stream_test.go +103 -0
  42. package/internal/agent/credits.go +713 -0
  43. package/internal/agent/credits_minimax_test.go +97 -0
  44. package/internal/agent/credits_test.go +211 -0
  45. package/internal/agent/custom_models.go +155 -0
  46. package/internal/agent/custom_models_test.go +125 -0
  47. package/internal/agent/dsh.go +123 -49
  48. package/internal/agent/dsh_resume.go +13 -2
  49. package/internal/agent/dsh_test.go +108 -6
  50. package/internal/agent/engine.go +37 -1
  51. package/internal/agent/engine_base.go +21 -17
  52. package/internal/agent/engine_base_test.go +2 -2
  53. package/internal/agent/engine_test.go +47 -86
  54. package/internal/agent/fallback.go +313 -0
  55. package/internal/agent/fallback_test.go +340 -0
  56. package/internal/agent/launch.go +43 -1
  57. package/internal/agent/llmhttp.go +46 -4
  58. package/internal/agent/llmhttp_test.go +157 -0
  59. package/internal/agent/models.go +53 -1
  60. package/internal/agent/models_test.go +20 -5
  61. package/internal/agent/pidalive_windows_test.go +40 -0
  62. package/internal/agent/runcmd.go +26 -0
  63. package/internal/agent/runcmd_windows.go +36 -5
  64. package/internal/agent/stream.go +136 -6
  65. package/internal/agent/stream_test.go +25 -38
  66. package/internal/agent/stream_timeline_test.go +2 -37
  67. package/internal/agent/streamjson.go +108 -6
  68. package/internal/agent/tools_suffix_test.go +6 -3
  69. package/internal/agent/trae.go +50 -243
  70. package/internal/agent/trae_exec.go +433 -0
  71. package/internal/agent/trae_exec_test.go +181 -0
  72. package/internal/agent/wraplaunch_test.go +113 -0
  73. package/internal/cli/ask.go +160 -37
  74. package/internal/cli/ask_test.go +1 -1
  75. package/internal/cli/checkin.go +77 -0
  76. package/internal/cli/clientcmd.go +3 -2
  77. package/internal/cli/control.go +46 -5
  78. package/internal/cli/control_test.go +65 -0
  79. package/internal/cli/events.go +4 -0
  80. package/internal/cli/quotas.go +125 -0
  81. package/internal/cli/requirements.go +51 -0
  82. package/internal/cli/root.go +122 -5
  83. package/internal/cli/session.go +7 -10
  84. package/internal/cli/suggest.go +7 -5
  85. package/internal/cli/team.go +37 -9
  86. package/internal/cli/team_test.go +39 -0
  87. package/internal/client/ab.go +372 -0
  88. package/internal/client/ab_test.go +287 -0
  89. package/internal/client/browse.go +84 -36
  90. package/internal/client/browse_test.go +107 -9
  91. package/internal/client/browseinput.go +481 -0
  92. package/internal/client/browseinput_test.go +216 -0
  93. package/internal/client/corecmd.go +23 -0
  94. package/internal/client/corecmd_other.go +8 -0
  95. package/internal/client/corecmd_windows.go +24 -0
  96. package/internal/client/daemon.go +90 -17
  97. package/internal/client/daemon_test.go +33 -0
  98. package/internal/client/engines_checkin_test.go +57 -0
  99. package/internal/client/fallback_flags_test.go +64 -0
  100. package/internal/client/jobs.go +294 -21
  101. package/internal/client/jobs_test.go +299 -2
  102. package/internal/client/knowledge.go +854 -0
  103. package/internal/client/knowledge_test.go +510 -0
  104. package/internal/client/lan.go +193 -7
  105. package/internal/client/lan_test.go +110 -0
  106. package/internal/client/quotas_merge_test.go +107 -0
  107. package/internal/client/requirements.go +198 -1
  108. package/internal/client/requirements_shots_test.go +200 -0
  109. package/internal/client/restart.go +134 -0
  110. package/internal/client/restart_test.go +151 -0
  111. package/internal/client/scripts.go +1047 -0
  112. package/internal/client/scripts_decode_other.go +27 -0
  113. package/internal/client/scripts_decode_test.go +125 -0
  114. package/internal/client/scripts_decode_windows.go +206 -0
  115. package/internal/client/scripts_test.go +726 -0
  116. package/internal/client/service.go +322 -18
  117. package/internal/client/service_test.go +51 -0
  118. package/internal/client/suggest.go +105 -2
  119. package/internal/client/suggest_test.go +86 -0
  120. package/internal/client/team_flags.go +25 -11
  121. package/internal/client/team_flags_test.go +39 -0
  122. package/internal/requirements/bootstrap.go +16 -0
  123. package/internal/requirements/consolidate.go +100 -9
  124. package/internal/requirements/doc.go +154 -3
  125. package/internal/requirements/graph.go +138 -0
  126. package/internal/requirements/memory.go +7 -0
  127. package/internal/requirements/modules.go +312 -0
  128. package/internal/requirements/modules_test.go +373 -0
  129. package/internal/requirements/relations.go +155 -0
  130. package/internal/requirements/relations_test.go +231 -0
  131. package/internal/requirements/requirements.go +10 -0
  132. package/internal/requirements/shots.go +418 -0
  133. package/internal/requirements/shots_test.go +387 -0
  134. package/internal/session/session.go +92 -0
  135. package/internal/session/sessionlog.go +23 -0
  136. package/internal/session/sessionlog_read_test.go +39 -0
  137. package/internal/suggest/suggest.go +23 -1
  138. package/internal/team/event.go +3 -1
  139. package/internal/team/execute.go +21 -6
  140. package/internal/team/fallback_route_test.go +80 -0
  141. package/internal/team/planner.go +15 -3
  142. package/internal/team/route.go +89 -17
  143. package/internal/team/team.go +18 -4
  144. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  145. package/npm/apk/version.json +1 -5
  146. package/npm/dist/darwin-arm64/magic-agent +0 -0
  147. package/npm/dist/darwin-arm64/magic-client +0 -0
  148. package/npm/dist/darwin-x64/magic-agent +0 -0
  149. package/npm/dist/darwin-x64/magic-client +0 -0
  150. package/npm/dist/linux-arm64/magic-agent +0 -0
  151. package/npm/dist/linux-arm64/magic-client +0 -0
  152. package/npm/dist/linux-x64/magic-agent +0 -0
  153. package/npm/dist/linux-x64/magic-client +0 -0
  154. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  155. package/npm/dist/win32-x64/magic-client.exe +0 -0
  156. package/package.json +8 -2
@@ -153,7 +153,11 @@ func runTask(ctx context.Context, opts Options, plan Plan, t Task, runDir string
153
153
  got.ID, got.Kind, got.Title = t.ID, t.EffectiveKind(), t.Title
154
154
  got.Role = t.Role
155
155
  got.OutputPath = OutputPath(runDir, t.ID)
156
- got.Engine, got.Model = eng.Name(), route.Model
156
+ // 引擎标签以**尝试自己记的**为准:runAttempt 在备用接上时已改成备用那一路,
157
+ // 这里若再盖回主引擎,用户就看不到「这一行是备用给的」。
158
+ if got.Engine == "" {
159
+ got.Engine, got.Model = eng.Name(), route.Model
160
+ }
157
161
  got.Attempts = attempt
158
162
  r = got
159
163
 
@@ -235,7 +239,7 @@ func runAttempt(ctx context.Context, opts Options, t Task, runDir string, route
235
239
  board.setSession(t.ID, id, "")
236
240
  }
237
241
 
238
- res, err := streamer.Stream(tctx, agent.Request{
242
+ res, fbInfo, err := agent.StreamWithFallback(tctx, eng, agent.Request{
239
243
  Engine: eng.Name(),
240
244
  Model: route.Model,
241
245
  // 角色片段追加在主系统提示词之后(见 role.go);未设角色时与原来一致。
@@ -249,7 +253,11 @@ func runAttempt(ctx context.Context, opts Options, t Task, runDir string, route
249
253
  Permission: permissionFor(opts, t),
250
254
  PermissionOptions: opts.PermissionOptions,
251
255
  OnSessionID: setSID,
252
- Messages: []agent.Message{{Role: "user", Content: prompt}},
256
+ // 主引擎只有过程、没有正文时降级到这一路的备用(额度用尽 / 单模型故障)。
257
+ // 降级发生时 agent 层已经发过 fallback 帧(下面原样交给子会话)。
258
+ FallbackEngine: route.FallbackEngine,
259
+ FallbackModel: route.FallbackModel,
260
+ Messages: []agent.Message{{Role: "user", Content: prompt}},
253
261
  }, func(ev agent.StreamEvent) {
254
262
  if ev.SessionID != "" {
255
263
  setSID(ev.SessionID)
@@ -270,6 +278,12 @@ func runAttempt(ctx context.Context, opts Options, t Task, runDir string, route
270
278
  }
271
279
  board.touch(t.ID, ev)
272
280
  })
281
+ /* 备用接上了:任务结果如实记成「谁给出的答案」(主备没生效时用户只能看到一个
282
+ 失败的子任务,分不清是没配还是配了没试)。只改展示字段,State 由下面的
283
+ err 决定 —— 备用成功时 err 必然为 nil。 */
284
+ if fbInfo.Used {
285
+ r.Engine, r.Model = fbInfo.Engine, fbInfo.Model
286
+ }
273
287
 
274
288
  if err != nil {
275
289
  r.State = TaskFailed
@@ -396,8 +410,7 @@ func completeTask(ctx context.Context, opts Options, eng agent.Engine, route Rou
396
410
  r.Engine, r.Model = eng.Name(), route.Model
397
411
  rctx, cancel := context.WithTimeout(ctx, taskTimeout(opts))
398
412
  defer cancel()
399
- runner := &agent.Runner{Engine: eng, Timeout: taskTimeout(opts)}
400
- resp, err := runner.Run(rctx, agent.Request{
413
+ resp, _, err := agent.RunWithFallback(rctx, eng, agent.Request{
401
414
  Engine: eng.Name(),
402
415
  Model: route.Model,
403
416
  SystemPrompt: systemPromptFor(opts, t),
@@ -406,8 +419,10 @@ func completeTask(ctx context.Context, opts Options, eng agent.Engine, route Rou
406
419
  Reasoning: opts.Reasoning,
407
420
  Permission: permissionFor(opts, t),
408
421
  PermissionOptions: opts.PermissionOptions,
422
+ FallbackEngine: route.FallbackEngine,
423
+ FallbackModel: route.FallbackModel,
409
424
  Messages: []agent.Message{{Role: "user", Content: prompt}},
410
- })
425
+ }, agent.RunOptions{Timeout: taskTimeout(opts)}, nil)
411
426
  r.SessionID = resp.SessionID
412
427
  r.Text = resp.Text
413
428
  collectDeliverables(TaskDir(runDir, t.ID), t.Deliverables, &r)
@@ -0,0 +1,80 @@
1
+ package team
2
+
3
+ // fallback_route_test.go - 团队路由的主备解析(2026-10-24)。
4
+ //
5
+ // 关注三件事:某一类/角色可以单独配「备」;只配备用 = 主引擎跟随主引擎;
6
+ // 配错的备用引擎在开跑前就报(别等主引擎失败才发现降级目标也没有)。
7
+
8
+ import (
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ func TestRouteForFallback(t *testing.T) {
14
+ main := &fakeEngine{name: "main"}
15
+ opts := Options{
16
+ Engine: main,
17
+ FallbackEngine: "main-backup",
18
+ Routes: map[string]Route{
19
+ "role:coder": {Engine: "coder-engine", FallbackEngine: "coder-backup"},
20
+ "image": {FallbackEngine: "image-backup"}, // 只配备用
21
+ },
22
+ }
23
+
24
+ // 角色路由:主备都取角色那一路。
25
+ r := RouteFor(Task{ID: "t1", Role: "coder"}, opts)
26
+ if r.Engine != "coder-engine" || r.FallbackEngine != "coder-backup" {
27
+ t.Errorf("coder 路由 = %+v", r)
28
+ }
29
+
30
+ // 只配备用:主引擎仍是主引擎,备用是这一类自己的。
31
+ r = RouteFor(Task{ID: "t2", Role: "designer"}, opts)
32
+ if r.Engine != "main" || r.FallbackEngine != "image-backup" {
33
+ t.Errorf("只配了备用的 image 路由 = %+v", r)
34
+ }
35
+
36
+ // 完全没配:主备都跟随主引擎。
37
+ r = RouteFor(Task{ID: "t3", Role: "writer"}, opts)
38
+ if r.Engine != "main" || r.FallbackEngine != "main-backup" {
39
+ t.Errorf("跟随主引擎的 writer 路由 = %+v", r)
40
+ }
41
+ }
42
+
43
+ func TestPlanRouteFallback(t *testing.T) {
44
+ main := &fakeEngine{name: "main"}
45
+ r := PlanRoute(Options{Engine: main, FallbackEngine: "main-backup"})
46
+ if r.Engine != "main" || r.FallbackEngine != "main-backup" {
47
+ t.Errorf("plan 跟随主引擎 = %+v", r)
48
+ }
49
+ r = PlanRoute(Options{Engine: main, Routes: map[string]Route{KindPlan: {Engine: "planner", FallbackEngine: "planner-bk"}}})
50
+ if r.Engine != "planner" || r.FallbackEngine != "planner-bk" {
51
+ t.Errorf("plan 显式路由 = %+v", r)
52
+ }
53
+ }
54
+
55
+ func TestApplyFallbacks(t *testing.T) {
56
+ routes, err := ParseRoutes([]string{"image=img"})
57
+ if err != nil {
58
+ t.Fatalf("ParseRoutes: %v", err)
59
+ }
60
+ routes, err = ApplyFallbacks(routes, []string{"image=img-bk/m2", "role:coder=cd-bk"})
61
+ if err != nil {
62
+ t.Fatalf("ApplyFallbacks: %v", err)
63
+ }
64
+ if r := routes["image"]; r.Engine != "img" || r.FallbackEngine != "img-bk" || r.FallbackModel != "m2" {
65
+ t.Errorf("image = %+v", r)
66
+ }
67
+ if r := routes["role:coder"]; r.Engine != "" || r.FallbackEngine != "cd-bk" {
68
+ t.Errorf("只给角色配备用时应保留空主引擎:%+v", r)
69
+ }
70
+ if _, err := ApplyFallbacks(nil, []string{"audio=x"}); err == nil {
71
+ t.Error("未知类别应当报错")
72
+ }
73
+ }
74
+
75
+ func TestValidateRoutesChecksFallback(t *testing.T) {
76
+ err := ValidateRoutes(map[string]Route{"image": {Engine: "llm", FallbackEngine: "不存在的引擎"}})
77
+ if err == nil || !strings.Contains(err.Error(), "备用") {
78
+ t.Fatalf("备用引擎不存在应当在开跑前报出来,得到 %v", err)
79
+ }
80
+ }
@@ -198,8 +198,8 @@ func makePlan(ctx context.Context, opts Options) (Plan, string, error) {
198
198
  prompt = strings.Join(hints, "\n\n") + "\n\n用户诉求:\n" + prompt
199
199
  }
200
200
 
201
- runner := &agent.Runner{Engine: eng, Timeout: taskTimeout(opts)}
202
- resp, err := runner.Run(pctx, agent.Request{
201
+ runnerOpts := agent.RunOptions{Timeout: taskTimeout(opts)}
202
+ resp, _, err := agent.RunWithFallback(pctx, eng, agent.Request{
203
203
  Engine: eng.Name(),
204
204
  Model: route.Model,
205
205
  SystemPrompt: strings.TrimSpace(opts.SystemPrompt + "\n\n" + plannerPrompt(opts)),
@@ -212,12 +212,24 @@ func makePlan(ctx context.Context, opts Options) (Plan, string, error) {
212
212
  Reasoning: opts.Reasoning,
213
213
  SessionID: sid,
214
214
  Continue: sid == "" && onMain && opts.Continue,
215
+ // 计划阶段同样主备:`plan` 行的备用引擎(没配就跟随主引擎的备用)。
216
+ // 这是最常见的故障点 —— 主引擎额度用尽时,计划都出不来(2026-10-24 报障)。
217
+ FallbackEngine: route.FallbackEngine,
218
+ FallbackModel: route.FallbackModel,
219
+ }, runnerOpts, func(fe agent.FallbackEvent) {
220
+ opts.OnEvent(agent.StreamEvent{
221
+ Kind: agent.KindFallback,
222
+ Text: "团队模式:" + agent.FallbackNoticeText(fe),
223
+ Fallback: &fe,
224
+ })
215
225
  })
216
226
  if err != nil {
217
227
  return Plan{}, "", err
218
228
  }
219
229
  plan, perr := parsePlan(resp.Text)
220
- if !onMain {
230
+ /* 计划由**备用引擎**跑出来时,resp.SessionID 是备用引擎的会话:不能交给主引擎汇总
231
+ (跨引擎续接是错的)。汇总改开新会话,计划内容本身不受影响。 */
232
+ if !onMain || !strings.EqualFold(resp.Engine, opts.Engine.Name()) {
221
233
  return plan, "", perr
222
234
  }
223
235
  return plan, resp.SessionID, perr
@@ -33,17 +33,32 @@ func RouteFor(t Task, opts Options) Route {
33
33
  return Route{Engine: e, Model: strings.TrimSpace(t.Model)}
34
34
  }
35
35
  role := LookupRole(t.Role)
36
+ kind := t.EffectiveKind()
37
+ roleRoute, roleOK := Route{}, false
36
38
  if role != nil {
37
- if r, ok := routeForRole(opts.Routes, role.Name); ok {
38
- m := strings.TrimSpace(r.Model)
39
- if m == "" {
40
- m = strings.TrimSpace(role.Model)
41
- }
42
- return Route{Engine: strings.TrimSpace(r.Engine), Model: m}
39
+ roleRoute, roleOK = routeForRole(opts.Routes, role.Name)
40
+ }
41
+ kindRoute, kindOK := opts.Routes[kind]
42
+
43
+ /* 备用取「最终选中那一路」的:角色 > 类别 > 主引擎。
44
+ 允许只配备用(主引擎留空 = 跟随主引擎 + 这一路的备)—— 界面上「主/备」
45
+ 两格本来就都可能单独填。 */
46
+ fbE, fbM := strings.TrimSpace(opts.FallbackEngine), strings.TrimSpace(opts.FallbackModel)
47
+ if e, m, ok := fallbackOf(roleRoute); ok {
48
+ fbE, fbM = e, m
49
+ } else if e, m, ok := fallbackOf(kindRoute); ok {
50
+ fbE, fbM = e, m
51
+ }
52
+
53
+ if roleOK && strings.TrimSpace(roleRoute.Engine) != "" {
54
+ m := strings.TrimSpace(roleRoute.Model)
55
+ if m == "" {
56
+ m = strings.TrimSpace(role.Model)
43
57
  }
58
+ return Route{Engine: strings.TrimSpace(roleRoute.Engine), Model: m, FallbackEngine: fbE, FallbackModel: fbM}
44
59
  }
45
- if r, ok := opts.Routes[t.EffectiveKind()]; ok && strings.TrimSpace(r.Engine) != "" {
46
- return Route{Engine: strings.TrimSpace(r.Engine), Model: strings.TrimSpace(r.Model)}
60
+ if kindOK && strings.TrimSpace(kindRoute.Engine) != "" {
61
+ return Route{Engine: strings.TrimSpace(kindRoute.Engine), Model: strings.TrimSpace(kindRoute.Model), FallbackEngine: fbE, FallbackModel: fbM}
47
62
  }
48
63
  // 默认「跟随主引擎」:不分类别,text / image / video / game 一视同仁。
49
64
  if opts.Engine != nil {
@@ -52,13 +67,24 @@ func RouteFor(t Task, opts Options) Route {
52
67
  if strings.TrimSpace(m) == "" && role != nil {
53
68
  m = strings.TrimSpace(role.Model)
54
69
  }
55
- return Route{Engine: opts.Engine.Name(), Model: m}
70
+ return Route{Engine: opts.Engine.Name(), Model: m, FallbackEngine: fbE, FallbackModel: fbM}
56
71
  }
57
72
  return Route{}
58
73
  }
59
74
 
75
+ // fallbackOf 取一条路由上配置的备用引擎(空 = 没配)。
76
+ func fallbackOf(r Route) (string, string, bool) {
77
+ e := strings.TrimSpace(r.FallbackEngine)
78
+ if e == "" {
79
+ return "", "", false
80
+ }
81
+ return e, strings.TrimSpace(r.FallbackModel), true
82
+ }
83
+
60
84
  // routeForRole 在路由表里找角色配置:规范键 `role:<name>` 优先,裸角色名也认
61
85
  // (客户端旧配置 / 手写 routes 里可能不带前缀)。
86
+ //
87
+ // 返回的引擎可能为空(只给这一路配了备用 = 主引擎仍跟随主引擎);调用方按需判断。
62
88
  func routeForRole(routes map[string]Route, name string) (Route, bool) {
63
89
  if len(routes) == 0 {
64
90
  return Route{}, false
@@ -67,7 +93,7 @@ func routeForRole(routes map[string]Route, name string) (Route, bool) {
67
93
  if key == "" {
68
94
  continue
69
95
  }
70
- if r, ok := routes[key]; ok && strings.TrimSpace(r.Engine) != "" {
96
+ if r, ok := routes[key]; ok {
71
97
  return r, true
72
98
  }
73
99
  }
@@ -79,11 +105,22 @@ func routeForRole(routes map[string]Route, name string) (Route, bool) {
79
105
  // 与子任务同一套语义:显式配了 plan 就用它,没配跟随主引擎 —— 计划本来就是主引擎的活。
80
106
  // 返回空引擎只发生在主引擎也为空时(Run 已提前拦掉)。
81
107
  func PlanRoute(opts Options) Route {
82
- if r, ok := opts.Routes[KindPlan]; ok && strings.TrimSpace(r.Engine) != "" {
83
- return Route{Engine: strings.TrimSpace(r.Engine), Model: strings.TrimSpace(r.Model)}
108
+ planRoute, hasPlan := opts.Routes[KindPlan]
109
+ fbE, fbM := strings.TrimSpace(opts.FallbackEngine), strings.TrimSpace(opts.FallbackModel)
110
+ if e, m, ok := fallbackOf(planRoute); ok {
111
+ fbE, fbM = e, m
112
+ }
113
+ if hasPlan && strings.TrimSpace(planRoute.Engine) != "" {
114
+ return Route{
115
+ Engine: strings.TrimSpace(planRoute.Engine), Model: strings.TrimSpace(planRoute.Model),
116
+ FallbackEngine: fbE, FallbackModel: fbM,
117
+ }
84
118
  }
85
119
  if opts.Engine != nil {
86
- return Route{Engine: opts.Engine.Name(), Model: opts.Model}
120
+ return Route{
121
+ Engine: opts.Engine.Name(), Model: opts.Model,
122
+ FallbackEngine: fbE, FallbackModel: fbM,
123
+ }
87
124
  }
88
125
  return Route{}
89
126
  }
@@ -102,6 +139,11 @@ func ValidateRoutes(routes map[string]Route) error {
102
139
  if agent.Lookup(name) == nil {
103
140
  bad = append(bad, fmt.Sprintf("%s→%s", kind, name))
104
141
  }
142
+ // 备用引擎同样要真实存在:配错了要在开跑前报,而不是等主引擎失败后
143
+ // 才发现「降级目标也没有」(那时用户已经白等一轮)。
144
+ if fb := strings.TrimSpace(r.FallbackEngine); fb != "" && agent.Lookup(fb) == nil {
145
+ bad = append(bad, fmt.Sprintf("%s→备用 %s", kind, fb))
146
+ }
105
147
  }
106
148
  if len(bad) > 0 {
107
149
  return fmt.Errorf("未知引擎:%s(可用引擎见 magic-agent --engines)", strings.Join(bad, "、"))
@@ -114,6 +156,36 @@ func ValidateRoutes(routes map[string]Route) error {
114
156
  // 模型名里可能有 `/`(如 `anthropic/claude-x`),所以只按**第一个** `/` 切:
115
157
  // 前段是引擎,其余全是模型名。
116
158
  func ParseRoutes(values []string) (map[string]Route, error) {
159
+ return parseRouteSpecs(values, "--team-route")
160
+ }
161
+
162
+ // ApplyFallbacks 把 `--team-fallback <key>=<engine>[/<model>]` 合并进路由表:
163
+ // 只给某一类 / 某个角色配备用引擎,主引擎仍由 routes 决定。
164
+ //
165
+ // 与 --team-route 同一套键(plan + 五类 + role:<name>)与同一套取值语法 ——
166
+ // 客户端「团队模式」里每行一个「主 / 备」,下发时就是这两组 flag。
167
+ func ApplyFallbacks(routes map[string]Route, values []string) (map[string]Route, error) {
168
+ specs, err := parseRouteSpecs(values, "--team-fallback")
169
+ if err != nil {
170
+ return routes, err
171
+ }
172
+ if len(specs) == 0 {
173
+ return routes, nil
174
+ }
175
+ if routes == nil {
176
+ routes = make(map[string]Route, len(specs))
177
+ }
178
+ for key, fb := range specs {
179
+ base := routes[key]
180
+ base.FallbackEngine = strings.TrimSpace(fb.Engine)
181
+ base.FallbackModel = strings.TrimSpace(fb.Model)
182
+ routes[key] = base
183
+ }
184
+ return routes, nil
185
+ }
186
+
187
+ // parseRouteSpecs 解析 `<key>=<engine>[/<model>]` 形态的重复 flag(路线 / 备用共用)。
188
+ func parseRouteSpecs(values []string, flag string) (map[string]Route, error) {
117
189
  if len(values) == 0 {
118
190
  return nil, nil
119
191
  }
@@ -125,16 +197,16 @@ func ParseRoutes(values []string) (map[string]Route, error) {
125
197
  }
126
198
  eq := strings.IndexByte(s, '=')
127
199
  if eq <= 0 {
128
- return nil, fmt.Errorf("--team-route 取值应为 <类别>=<引擎>[/<模型>],收到 %q", v)
200
+ return nil, fmt.Errorf("%s 取值应为 <类别>=<引擎>[/<模型>],收到 %q", flag, v)
129
201
  }
130
202
  kind := normalizeRouteKind(s[:eq])
131
203
  if kind == "" {
132
- return nil, fmt.Errorf("--team-route 的类别 %q 不支持(可选:%s;角色用 role:<%s>)",
133
- s[:eq], strings.Join(RouteKinds, " / "), strings.Join(RoleNames(), " / "))
204
+ return nil, fmt.Errorf("%s 的类别 %q 不支持(可选:%s;角色用 role:<%s>)",
205
+ flag, s[:eq], strings.Join(RouteKinds, " / "), strings.Join(RoleNames(), " / "))
134
206
  }
135
207
  rest := strings.TrimSpace(s[eq+1:])
136
208
  if rest == "" {
137
- return nil, fmt.Errorf("--team-route %s 缺少引擎名", kind)
209
+ return nil, fmt.Errorf("%s %s 缺少引擎名", flag, kind)
138
210
  }
139
211
  r := Route{Engine: rest}
140
212
  if i := strings.IndexByte(rest, '/'); i > 0 {
@@ -81,6 +81,12 @@ var RouteKinds = []string{KindPlan, KindText, KindCode, KindImage, KindVideo, Ki
81
81
  type Route struct {
82
82
  Engine string
83
83
  Model string
84
+ // FallbackEngine / FallbackModel 这一路的备用引擎(2026-10-24)。
85
+ //
86
+ // 主引擎只有过程、没有正文时自动降级一次(见 agent/fallback.go)。空 = 不降级。
87
+ // 客户端「团队模式」里每个角色都可以配主 / 备两档。
88
+ FallbackEngine string
89
+ FallbackModel string
84
90
  }
85
91
 
86
92
  // Options 一次团队调用的全部输入。
@@ -89,6 +95,12 @@ type Options struct {
89
95
  Engine agent.Engine
90
96
  // Model 主引擎的模型(空 = 引擎默认)。
91
97
  Model string
98
+ // FallbackEngine / FallbackModel 主引擎的备用(2026-10-24;口径见 agent/fallback.go)。
99
+ //
100
+ // 「跟随主引擎」的那几类子任务与计划阶段都用它做降级目标;某一类/角色单独配了
101
+ // route 时,以 route 自己的 Fallback* 为准。空 = 不降级。
102
+ FallbackEngine string
103
+ FallbackModel string
92
104
  // Prompt 用户原始诉求。
93
105
  Prompt string
94
106
  // SystemPrompt 主引擎的系统提示词(计划与汇总都注入)。
@@ -127,7 +139,8 @@ type Options struct {
127
139
  // BaseDir 覆盖运行目录的父目录(空 = 按 chooseRunDir 推断)。
128
140
  BaseDir string
129
141
 
130
- // Permission 子任务的权限上限(默认 accept-edits,见 DefaultTaskPermission)。
142
+ // Permission 子任务的权限上限(空 = 用 DefaultTaskPermission;CLI 会传主会话的档位,
143
+ // 即默认**继承主会话**)。
131
144
  Permission agent.PermissionTier
132
145
  // PermissionOptions 与 Permission 配套的四档选项。
133
146
  PermissionOptions agent.PermissionOptions
@@ -161,10 +174,11 @@ type Options struct {
161
174
  OpenSubSession func(t Task, runDir, engine, model string) SubSession
162
175
  }
163
176
 
164
- // DefaultTaskPermission 子任务的默认权限上限。
177
+ // DefaultTaskPermission Go 调用方的零值兜底(CLI 不传时会**继承主会话**,不走这里)。
165
178
  //
166
- // 刻意**低于**主会话的默认档(full):子任务是自己拆出来的一段活,让它带
167
- // 「完全访问」等于把一次授权扩成 N 份。要放开就显式传 --team-permission。
179
+ // 为什么兜底是 accept-edits 而不是 full:Options.Permission 为空说明调用方没表态,
180
+ // 静默给「完全访问」等于把一次授权扩成 N 份。CLI 走的是继承路径(见 internal/cli/team.go),
181
+ // 子任务权限与主会话一致;要把子任务压得更严再用 --team-permission 显式指定。
168
182
  const DefaultTaskPermission = agent.PermissionAcceptEdits
169
183
 
170
184
  const defaultParallel = 4
@@ -1,5 +1 @@
1
- {
2
- "version": "2.4.2",
3
- "versionCode": 20402,
4
- "builtAt": "2026-10-23T11:49:45.889Z"
5
- }
1
+ { "versionCode": 20600, "version": "2.6.0", "builtAt": "2026-10-25T09:20:06.000Z" }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-agent",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "description": "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw 引擎",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -14,13 +14,19 @@
14
14
  "prepack": "node npm/build.js",
15
15
  "postinstall": "node npm/install.js",
16
16
  "check:ui": "node tests/check-ui-spec.mjs",
17
+ "check:notify": "node tests/check-notify.mjs",
17
18
  "check:pin": "node tests/check-task-pin.mjs",
19
+ "check:space": "node tests/check-space-label.mjs",
20
+ "check:space-ui": "node tests/check-space-label-ui.mjs",
21
+ "check:scripts": "node tests/check-scripts-ui.mjs",
22
+ "check:scripts-ui": "node tests/check-scripts-ui-browser.mjs",
18
23
  "check:conv": "node tests/check-conv-shards.mjs",
19
24
  "check:conv-host": "node tests/check-conv-shared-host.mjs",
20
25
  "check:conv-mobile": "node tests/check-conv-mobile-host.mjs",
21
26
  "check:conv-comp": "node tests/check-conv-component.mjs",
22
27
  "check:conv-queue": "node tests/check-conv-queue.mjs",
23
- "check:follow": "node tests/check-follow-retry.mjs"
28
+ "check:follow": "node tests/check-follow-retry.mjs",
29
+ "check:req-shot": "node tests/check-req-shot.mjs"
24
30
  },
25
31
  "files": [
26
32
  "npm/bin/",