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.
- package/CLIENT-CONTRACT.md +290 -30
- package/README.md +15 -14
- package/client-ui/pc/app.css +32 -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 +37 -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 +241 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +63 -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 +81 -0
- package/client-ui/shared/conv/conv.css +79 -19
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +336 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/index.html +2932 -333
- package/client-ui-mobile/reqs.css +102 -0
- package/client-ui-mobile/reqs.js +485 -70
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +21 -0
- package/client-ui-mobile/scripts.js +539 -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 +33 -9
- package/cmd/magic-client/main.go +57 -26
- 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_resume.go +13 -2
- 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/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- 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 +136 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +108 -6
- 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 +160 -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/events.go +4 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +122 -5
- package/internal/cli/session.go +7 -10
- 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/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/daemon.go +90 -17
- 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/jobs.go +294 -21
- 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 +193 -7
- package/internal/client/lan_test.go +110 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +198 -1
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/restart.go +134 -0
- package/internal/client/restart_test.go +151 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_other.go +27 -0
- package/internal/client/scripts_decode_test.go +125 -0
- package/internal/client/scripts_decode_windows.go +206 -0
- package/internal/client/scripts_test.go +726 -0
- package/internal/client/service.go +322 -18
- package/internal/client/service_test.go +51 -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/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/session.go +92 -0
- package/internal/session/sessionlog.go +23 -0
- package/internal/session/sessionlog_read_test.go +39 -0
- 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 +1 -5
- 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/package.json +8 -2
package/internal/team/execute.go
CHANGED
|
@@ -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
|
-
|
|
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 :=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/internal/team/planner.go
CHANGED
|
@@ -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
|
-
|
|
202
|
-
resp, err :=
|
|
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
|
-
|
|
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
|
package/internal/team/route.go
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
46
|
-
return Route{Engine: strings.TrimSpace(
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
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{
|
|
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("
|
|
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("
|
|
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("
|
|
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 {
|
package/internal/team/team.go
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
167
|
-
//
|
|
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
|
|
Binary file
|
package/npm/apk/version.json
CHANGED
|
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.
|
|
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/",
|