magic-agent 1.0.49 → 1.0.109
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 +514 -7
- package/README.md +31 -2
- package/client-ui/pc/app.css +3 -41
- package/client-ui/pc/js/04-task-list.js +6 -2
- package/client-ui/pc/js/90-glue.js +23 -4
- package/client-ui/pc/js/92-composer.js +11 -4
- package/client-ui/pc/js/93-rail.js +178 -19
- package/client-ui/shared/conv/03-chat-message.js +145 -25
- package/client-ui/shared/conv/09-stream-apply.js +55 -6
- package/client-ui/shared/conv/19-attachments.js +70 -6
- package/client-ui/shared/conv/27-conv-ledger.js +18 -0
- package/client-ui/shared/conv/29-msg-variants.js +10 -0
- package/client-ui/shared/conv/README.md +6 -0
- package/client-ui/shared/conv/conv.css +77 -0
- package/client-ui/shared/conversation.css +229 -43
- package/client-ui/shared/conversation.js +1316 -131
- package/client-ui/shared/transport-http.js +164 -12
- package/client-ui-mobile/index.html +2443 -178
- package/internal/agent/capability.go +16 -2
- package/internal/agent/claude.go +3 -2
- package/internal/agent/codebuddy.go +7 -1
- package/internal/agent/codex_test.go +3 -0
- package/internal/agent/custom_models_test.go +5 -0
- package/internal/agent/dsh.go +18 -2
- package/internal/agent/dsh_test.go +53 -2
- package/internal/agent/engine_test.go +34 -19
- package/internal/agent/env_test.go +14 -9
- package/internal/agent/models_test.go +9 -0
- package/internal/agent/runcmd_test.go +24 -3
- package/internal/agent/stream.go +33 -0
- package/internal/cli/ask.go +94 -4
- package/internal/cli/contract_test.go +26 -0
- package/internal/cli/events.go +18 -7
- package/internal/cli/keepalive_test.go +69 -0
- package/internal/cli/requirements.go +119 -0
- package/internal/cli/requirements_test.go +36 -0
- package/internal/cli/root.go +56 -4
- package/internal/cli/session.go +64 -25
- package/internal/cli/sessionlog.go +76 -7
- package/internal/cli/sessionlog_test.go +58 -0
- package/internal/cli/team.go +374 -0
- package/internal/cli/team_session.go +156 -0
- package/internal/cli/team_test.go +360 -0
- package/internal/client/plugins.go +7 -4
- package/internal/client/requirements.go +309 -0
- package/internal/client/requirements_test.go +239 -0
- package/internal/client/service.go +545 -19
- package/internal/client/service_test.go +47 -2
- package/internal/client/session_events_test.go +320 -0
- package/internal/client/team_flags.go +204 -0
- package/internal/client/team_flags_test.go +254 -0
- package/internal/requirements/bootstrap.go +423 -0
- package/internal/requirements/consolidate.go +908 -0
- package/internal/requirements/doc.go +473 -0
- package/internal/requirements/memory.go +324 -0
- package/internal/requirements/requirements.go +169 -0
- package/internal/requirements/requirements_test.go +530 -0
- package/internal/requirements/store.go +289 -0
- package/internal/session/delete.go +8 -4
- package/internal/session/session.go +194 -17
- package/internal/session/session_append.go +4 -1
- package/internal/session/session_append_test.go +19 -0
- package/internal/session/session_mark_test.go +42 -3
- package/internal/session/session_test.go +43 -0
- package/internal/session/sessionlog.go +50 -34
- package/internal/session/sessionlog_read_test.go +69 -11
- package/internal/session/status.go +205 -0
- package/internal/session/status_test.go +144 -0
- package/internal/team/adaptive.go +293 -0
- package/internal/team/board.go +216 -0
- package/internal/team/event.go +28 -0
- package/internal/team/execute.go +648 -0
- package/internal/team/plan.go +903 -0
- package/internal/team/planner.go +294 -0
- package/internal/team/reduce.go +192 -0
- package/internal/team/role.go +334 -0
- package/internal/team/route.go +206 -0
- package/internal/team/schedule.go +241 -0
- package/internal/team/schedule_test.go +286 -0
- package/internal/team/subsession.go +27 -0
- package/internal/team/team.go +401 -0
- package/internal/team/team_test.go +1997 -0
- package/npm/bin/magic-client.js +0 -0
- 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 +1 -1
package/internal/cli/ask.go
CHANGED
|
@@ -133,6 +133,41 @@ type askOptions struct {
|
|
|
133
133
|
autoModeEnv []string // 第 3 档分类器的受信边界描述(autoMode.environment)
|
|
134
134
|
permDeny []string // 追加 deny 规则(permissions.deny,所有档位生效)
|
|
135
135
|
permAsk []string // 追加 ask 规则(permissions.ask,强制人工审批)
|
|
136
|
+
|
|
137
|
+
/* 团队模式(2026-10-21,见 internal/team)。
|
|
138
|
+
*
|
|
139
|
+
* 它是**会话层的一种运行模式**,不是新引擎:主引擎(-e)拆任务 → 子引擎分头执行
|
|
140
|
+
* → 主引擎汇总。默认关(用户定稿:「默认不开启」);开关在移动端右上角的运行设置里。
|
|
141
|
+
*
|
|
142
|
+
* 与 --keep-alive 的关系:团队模式不接追加(子任务不共享上下文、也不该被追问),
|
|
143
|
+
* 所以开了它之后常驻会话自动不生效(见 runStreamAsk 里的 kaEnabled 判定)。 */
|
|
144
|
+
team bool // --team:开启团队模式
|
|
145
|
+
teamPlanOnly bool // --team-plan-only:只出计划与任务书,不执行
|
|
146
|
+
teamParallel int // --team-parallel:并发度(默认 4)
|
|
147
|
+
teamTaskTO time.Duration // --team-task-timeout:单子任务超时(0 = 按总超时推算)
|
|
148
|
+
teamRoutes []string // --team-route <kind>=<engine>[/<model>],可重复
|
|
149
|
+
teamRunDir string // --team-run-dir:覆盖运行目录父级
|
|
150
|
+
teamPerm string // --team-permission:子任务权限上限(默认 accept-edits)
|
|
151
|
+
teamAdaptive bool // --team-adaptive:执行后按失败 / open 项再规划一轮
|
|
152
|
+
teamSplit string // --team-split:拆解粒度 coarse | standard | fine
|
|
153
|
+
teamSplitCode bool // --team-split-code:简单代码也拆成独立子任务
|
|
154
|
+
teamRules []string // --team-rule role:<name>=<规则>,可重复
|
|
155
|
+
|
|
156
|
+
/* 会话自动聚合成需求(2026-10-22;见 internal/requirements)。
|
|
157
|
+
*
|
|
158
|
+
* 读与写分成两个 flag:读(列表)客户端每刷一次都调,绝不能顺手触发模型;
|
|
159
|
+
* 写(归纳)几秒到几分钟,只在轮末防抖或用户手点时跑。
|
|
160
|
+
*
|
|
161
|
+
* 值都可以留空 —— 读留空 = 列全部工作区,写留空 = 当前目录,
|
|
162
|
+
* 所以派发一律用 flagChanged 判断(看「传过没有」而不是「值非空」)。 */
|
|
163
|
+
requirements string // --requirements [<ws|dir>]:读清单
|
|
164
|
+
consolidateReqs string // --consolidate-requirements [<ws|dir>]:跑一轮归纳
|
|
165
|
+
reqForce bool // --requirement-force
|
|
166
|
+
reqAll bool // --requirement-all:全量重放
|
|
167
|
+
reqEngine string // --requirement-engine(默认 llm)
|
|
168
|
+
reqModel string // --requirement-model
|
|
169
|
+
reqTimeout time.Duration // --requirement-timeout
|
|
170
|
+
reqAuto bool // --requirement-auto:自动归纳,不触发首次初始化
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
// newAskOptions 返回带默认值的选项集。
|
|
@@ -202,9 +237,37 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
202
237
|
f.BoolVar(&opts.events, "events", false, "配合 --stream:事件流带契约版本与行号(每行加 v / seq,并先发一行 ready),供客户端消费;老消费者不要开(形状与 --stream 不同)")
|
|
203
238
|
f.BoolVar(&opts.control, "control", false, "配合 --stream:从 stdin 读 NDJSON 控制命令(ping / interrupt / stop / answer),让调用方能打断长任务、回审批")
|
|
204
239
|
f.BoolVar(&opts.noThinking, "no-thinking", false, "流式模式下不转发思考过程增量")
|
|
240
|
+
bindTeamFlags(cmd, opts)
|
|
241
|
+
bindRequirementFlags(cmd, opts)
|
|
205
242
|
bindPermissionFlags(cmd, opts)
|
|
206
243
|
}
|
|
207
244
|
|
|
245
|
+
// bindTeamFlags 注册团队模式的 flags(默认全关 —— 用户定稿「默认不开启」)。
|
|
246
|
+
//
|
|
247
|
+
// 为什么这些参数要能一个一个给:客户端(移动端的「我的 → 团队模式」)把各类任务 / 各角色的
|
|
248
|
+
// 引擎/模型配置存在本地,每次请求随 body 下发,插件再翻译成 --team-route。core 侧
|
|
249
|
+
// 因此必须是**无状态**的:配置跟着请求走,不落 core 配置文件。
|
|
250
|
+
func bindTeamFlags(cmd *cobra.Command, opts *askOptions) {
|
|
251
|
+
f := cmd.PersistentFlags()
|
|
252
|
+
f.BoolVar(&opts.team, "team", false, "团队模式:主引擎拆任务 → 子引擎分头执行 → 主引擎汇总(需配合 --stream;默认关)")
|
|
253
|
+
f.BoolVar(&opts.teamPlanOnly, "team-plan-only", false, "配合 --team:只生成计划文档与任务书,不执行子任务(调试 / 先确认方案)")
|
|
254
|
+
f.IntVar(&opts.teamParallel, "team-parallel", 4, "配合 --team:同时最多跑几个子任务(默认 4)")
|
|
255
|
+
f.DurationVar(&opts.teamTaskTO, "team-task-timeout", 0, "配合 --team:单个子任务超时(0 = 总超时的 1/2,上限 20m)")
|
|
256
|
+
f.StringArrayVar(&opts.teamRoutes, "team-route", nil,
|
|
257
|
+
"配合 --team:给某类子任务或某个角色指定引擎/模型,形如 image=llm/hunyuan-image-3.0 或 role:coder=claude/sonnet(可重复;类别:plan|text|code|image|video|game;角色用 role:<name>)")
|
|
258
|
+
f.StringVar(&opts.teamRunDir, "team-run-dir", "", "配合 --team:覆盖运行目录的父目录(空 = 选了空间落 <空间>/.magic/team,否则落 ~/.magic-agent/team)")
|
|
259
|
+
f.StringVar(&opts.teamPerm, "team-permission", string(agent.PermissionAcceptEdits),
|
|
260
|
+
"配合 --team:子任务的权限上限(默认 accept-edits —— 子任务不该继承主会话的完全访问)")
|
|
261
|
+
f.BoolVar(&opts.teamAdaptive, "team-adaptive", true,
|
|
262
|
+
"配合 --team:第一轮执行后按失败与未解决项再规划一轮补充任务(默认开;只补一轮、最多 3 个)")
|
|
263
|
+
f.StringVar(&opts.teamSplit, "team-split", "",
|
|
264
|
+
"配合 --team:拆解粒度 coarse(最多 3 个)| standard(6,默认)| fine(10)")
|
|
265
|
+
f.BoolVar(&opts.teamSplitCode, "team-split-code", false,
|
|
266
|
+
"配合 --team:简单代码也拆成独立子任务(默认关:简单代码由主引擎直接给出)")
|
|
267
|
+
f.StringArrayVar(&opts.teamRules, "team-rule", nil,
|
|
268
|
+
"配合 --team:改写某个角色「这类任务适合什么」,形如 role:coder=只拆多文件、要构建或测试的改动(可重复)")
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
// bindPermissionFlags 注册四档权限模型的 flags(仅 claude / codebuddy / dsh 生效)。
|
|
209
272
|
//
|
|
210
273
|
// 默认档 full 是**刻意**的:改造前 claude/codebuddy 在 --tools 非 off 时恒传
|
|
@@ -462,6 +525,13 @@ func resolvePermissionTier(engineName, raw string, explicit bool) (agent.Permiss
|
|
|
462
525
|
|
|
463
526
|
// runAsk ask 主流程(非流式)。
|
|
464
527
|
func runAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
528
|
+
if opts.team {
|
|
529
|
+
// 团队模式靠事件流才能把「谁在干什么」呈现出来(子任务的委派卡、增量正文),
|
|
530
|
+
// 一次性 envelope 装不下这些过程。明确拒绝,而不是悄悄忽略 --team。
|
|
531
|
+
return &usageError{fmt.Errorf(
|
|
532
|
+
"--team 需要 --stream(团队模式的过程靠事件流呈现:每个子任务在界面上是一张委派卡);" +
|
|
533
|
+
"加上 --stream 即可,或去掉 --team 走单引擎")}
|
|
534
|
+
}
|
|
465
535
|
engine, format, req, err := prepareAsk(cmd, args, opts)
|
|
466
536
|
if err != nil {
|
|
467
537
|
return err
|
|
@@ -609,6 +679,14 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
609
679
|
} else {
|
|
610
680
|
sessionLog = sw
|
|
611
681
|
defer func() { sessionLog.close() }()
|
|
682
|
+
// 续接(`--session <id>`)时 id 一开始就知道:先把它推给 writer,
|
|
683
|
+
// 让日志**从头就写进终名文件**。否则会先落 <run_id>.jsonl(seq 从 1),
|
|
684
|
+
// 等引擎吐 id 再合并进 <session_id>.jsonl —— 同一文件里 seq 出现回落,
|
|
685
|
+
// 客户端的增量游标(after=旧轮末位)会把新一轮整段滤掉,画布看着
|
|
686
|
+
// 「永远已追平」(用户实测:掌天瓶任务对话不刷新)。
|
|
687
|
+
if id := strings.TrimSpace(opts.session); id != "" {
|
|
688
|
+
sessionLog.setSessionID(id)
|
|
689
|
+
}
|
|
612
690
|
}
|
|
613
691
|
}
|
|
614
692
|
// 把 session_id 推到 writer:CLI 这边的 OnSessionID 回调(引擎流里见到 init 行就调)
|
|
@@ -662,8 +740,9 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
662
740
|
Kind: string(ev.Kind), Text: ev.Text, Name: ev.Name, ID: ev.ID,
|
|
663
741
|
ToolKind: toolKindOfEvent(ev), Ask: ev.Ask,
|
|
664
742
|
ItemID: ev.ItemID, ItemRevision: ev.ItemRevision,
|
|
665
|
-
Status: string(ev.Status),
|
|
666
|
-
|
|
743
|
+
Status: string(ev.Status), Parent: ev.Parent,
|
|
744
|
+
SubSessionID: ev.SubSessionID,
|
|
745
|
+
Error: ev.Error, Reason: ev.Reason,
|
|
667
746
|
})
|
|
668
747
|
}
|
|
669
748
|
if format == agent.FormatJSON {
|
|
@@ -822,16 +901,27 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
822
901
|
界面上该显示「等审批」而不是「生成中」。用户作答、或这一轮的任何后续事件到达时
|
|
823
902
|
回到 running。落点选在这里,是因为它同时罩住两条 onEvent(--stream 与 --events),
|
|
824
903
|
不必在两个分支里各写一遍(漏一个就会有一条通道的状态是假的)。
|
|
904
|
+
⚠️ 常驻会话**本轮结束**时要写 idle(2026-10-21):进程还在等 --append,但这一轮
|
|
905
|
+
已经跑完 —— 继续写 running 会让别的端(移动端跟进 / 串行门)以为还在跑,PC 却按
|
|
906
|
+
事件流显示「已完成」,两边状态就此分叉(用户 2026-10-21 报障:移动端排队追问永不
|
|
907
|
+
下发)。下一轮第一个事件到达自然回到 running。
|
|
908
|
+
条件只看 ka 是否存在、**不看 --idle**:桌面端/移动端恒传 `--idle 0`(本轮结束就
|
|
909
|
+
收工),但引擎收尾与进程退出可能拖上几分钟 —— 这段空窗正是登记表 running、界面
|
|
910
|
+
「已完成」并存的来源。写 idle 之后再收尾写 done,两步都覆盖。
|
|
825
911
|
写入很快(状态没变时 Mark 直接返回,一轮最多两次落盘),见 session.Handle.Mark。 */
|
|
912
|
+
idleAfterTurn := ka != nil
|
|
826
913
|
if h != nil {
|
|
827
914
|
inner := onEvent
|
|
828
915
|
onEvent = func(ev agent.StreamEvent) {
|
|
829
916
|
// 会话锚点:引擎把 id 挂在事件上时同样立刻回填(覆盖不调 OnSessionID 的引擎,
|
|
830
917
|
// 见上面那段注释)。空 id 由 SetSessionID 自己跳过。
|
|
831
918
|
h.SetSessionID(ev.SessionID)
|
|
832
|
-
|
|
919
|
+
switch {
|
|
920
|
+
case ev.Kind == agent.KindAsk:
|
|
833
921
|
h.Mark(session.StateWaitingApproval)
|
|
834
|
-
|
|
922
|
+
case ev.Kind == agent.KindTurnEnd && idleAfterTurn:
|
|
923
|
+
h.Mark(session.StateIdle)
|
|
924
|
+
default:
|
|
835
925
|
h.Mark(session.StateRunning)
|
|
836
926
|
}
|
|
837
927
|
inner(ev)
|
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
"testing"
|
|
7
7
|
|
|
8
8
|
"github.com/darren/magic-agent/internal/agent"
|
|
9
|
+
"github.com/darren/magic-agent/internal/team"
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
// contractRow 是 --engines / --contract 每一行里本测试关心的字段。
|
|
@@ -27,8 +28,14 @@ func TestContractEnvelope(t *testing.T) {
|
|
|
27
28
|
}
|
|
28
29
|
var env struct {
|
|
29
30
|
ContractVersion int `json:"contractVersion"`
|
|
31
|
+
Version string `json:"version"`
|
|
30
32
|
Features []string `json:"features"`
|
|
31
33
|
Engines []contractRow `json:"engines"`
|
|
34
|
+
TeamRoles []struct {
|
|
35
|
+
Name string `json:"name"`
|
|
36
|
+
Kind string `json:"kind"`
|
|
37
|
+
Duty string `json:"duty"`
|
|
38
|
+
} `json:"teamRoles"`
|
|
32
39
|
}
|
|
33
40
|
if err := json.Unmarshal([]byte(stdout), &env); err != nil {
|
|
34
41
|
t.Fatalf("解析 --contract 输出失败: %v(原始输出:%s)", err, stdout)
|
|
@@ -36,6 +43,11 @@ func TestContractEnvelope(t *testing.T) {
|
|
|
36
43
|
if env.ContractVersion != agent.ContractVersion {
|
|
37
44
|
t.Errorf("contractVersion = %d want %d", env.ContractVersion, agent.ContractVersion)
|
|
38
45
|
}
|
|
46
|
+
/* 版本号(2026-10-22 只增字段):移动端「我的」页显示它。空 = 客户端只能显示「—」,
|
|
47
|
+
所以这里必须非空(测试里是 internal/cli.Version 的默认值,构建时由 ldflags 覆盖)。 */
|
|
48
|
+
if env.Version == "" {
|
|
49
|
+
t.Error("version 不应为空:移动端「我的」页要显示当前 core 版本")
|
|
50
|
+
}
|
|
39
51
|
// 核心级通道能力:客户端据此判断这台 core 支不支持 --events / --control。
|
|
40
52
|
for _, want := range []string{agent.FeatureSessionEvents, agent.FeatureSessionControl} {
|
|
41
53
|
found := false
|
|
@@ -52,6 +64,20 @@ func TestContractEnvelope(t *testing.T) {
|
|
|
52
64
|
if got, want := len(env.Engines), len(agent.Engines()); got != want {
|
|
53
65
|
t.Errorf("engines 行数 = %d want %d", got, want)
|
|
54
66
|
}
|
|
67
|
+
/* 团队角色清单(2026-10-22 新增,只增字段不升 contractVersion):
|
|
68
|
+
移动端「团队模式」页据此渲染角色路由行(缩进在所属类别下),客户端不许硬编码角色名。 */
|
|
69
|
+
names := team.RoleNames()
|
|
70
|
+
if got, want := len(env.TeamRoles), len(names); got != want {
|
|
71
|
+
t.Fatalf("teamRoles 行数 = %d want %d", got, want)
|
|
72
|
+
}
|
|
73
|
+
for i, r := range env.TeamRoles {
|
|
74
|
+
if r.Name != names[i] {
|
|
75
|
+
t.Errorf("teamRoles[%d].name = %q want %q(顺序即界面顺序)", i, r.Name, names[i])
|
|
76
|
+
}
|
|
77
|
+
if r.Kind == "" || r.Duty == "" {
|
|
78
|
+
t.Errorf("%s: kind/duty 不应为空(移动端要展示):%+v", r.Name, r)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
55
81
|
for _, row := range env.Engines {
|
|
56
82
|
if row.Engine == "" {
|
|
57
83
|
t.Errorf("有一行缺 engine 字段:%+v", row)
|
package/internal/cli/events.go
CHANGED
|
@@ -191,13 +191,17 @@ func (s *eventSink) notice(kind string, attrs map[string]any) error {
|
|
|
191
191
|
// 老事件(turn_end 之外的轮次事件)不带这些字段时与历史逐字节一致。
|
|
192
192
|
func streamEventPayload(ev agent.StreamEvent) any {
|
|
193
193
|
return struct {
|
|
194
|
-
Type string
|
|
195
|
-
Text string
|
|
196
|
-
Name string
|
|
197
|
-
ID string
|
|
198
|
-
ToolKind string
|
|
199
|
-
SessionID string
|
|
200
|
-
|
|
194
|
+
Type string `json:"type"`
|
|
195
|
+
Text string `json:"text"`
|
|
196
|
+
Name string `json:"name,omitempty"`
|
|
197
|
+
ID string `json:"id,omitempty"`
|
|
198
|
+
ToolKind string `json:"tool_kind,omitempty"`
|
|
199
|
+
SessionID string `json:"session_id,omitempty"`
|
|
200
|
+
/* 子会话 id(2026-10-21,团队模式):非空 = 这条事件来自某个**绑定在主会话
|
|
201
|
+
下面的子任务会话**。它只是展示/留档用,不参与会话锚点(锚点只认
|
|
202
|
+
session_id)。omitempty:非团队事件不带它,老消费者字段集不变。 */
|
|
203
|
+
SubSessionID string `json:"sub_session_id,omitempty"`
|
|
204
|
+
Ask *agent.AskRequest `json:"ask,omitempty"`
|
|
201
205
|
/* 状态收敛四元组(2026-10-02,对齐 agents-anywhere 的 timeline item)。
|
|
202
206
|
消费方按 (item_id, item_revision) 覆盖式 upsert 即可:
|
|
203
207
|
同一 item 的多个 partial 帧 + 最后一个 final 帧,
|
|
@@ -206,6 +210,11 @@ func streamEventPayload(ev agent.StreamEvent) any {
|
|
|
206
210
|
ItemRevision uint64 `json:"item_revision,omitempty"`
|
|
207
211
|
Status string `json:"status,omitempty"`
|
|
208
212
|
Snapshot string `json:"snapshot,omitempty"`
|
|
213
|
+
/* 父委派 id(2026-10-21,团队模式):非空 = 这条事件属于 id 为它的委派卡。
|
|
214
|
+
语义与 claude 系原生的 parent_tool_use_id 一致,渲染层本来就按它把子事件
|
|
215
|
+
挂进委派卡的嵌套区(见 shared/conv/09-stream-apply.js 的 evParent)。
|
|
216
|
+
omitempty:主会话自己的事件不带它,老消费者字段集不变。 */
|
|
217
|
+
Parent string `json:"parent,omitempty"`
|
|
209
218
|
/* 异常收尾(仅 turn_failed):Error 是完整错误链,Reason 是最内层根因,
|
|
210
219
|
与 -o json 的失败 envelope 同源(都走 agent.ReasonOf)。 */
|
|
211
220
|
Error string `json:"error,omitempty"`
|
|
@@ -217,11 +226,13 @@ func streamEventPayload(ev agent.StreamEvent) any {
|
|
|
217
226
|
ID: ev.ID,
|
|
218
227
|
ToolKind: toolKindOfEvent(ev),
|
|
219
228
|
SessionID: ev.SessionID,
|
|
229
|
+
SubSessionID: ev.SubSessionID,
|
|
220
230
|
Ask: ev.Ask,
|
|
221
231
|
ItemID: ev.ItemID,
|
|
222
232
|
ItemRevision: ev.ItemRevision,
|
|
223
233
|
Status: string(ev.Status),
|
|
224
234
|
Snapshot: ev.Snapshot,
|
|
235
|
+
Parent: ev.Parent,
|
|
225
236
|
Error: ev.Error,
|
|
226
237
|
Reason: ev.Reason,
|
|
227
238
|
}
|
|
@@ -177,6 +177,75 @@ func TestKeepAliveIsDefaultForStreaming(t *testing.T) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// idleWindowEngine 模拟真实引擎的收尾空窗:turn_end 之后要过一阵才返回
|
|
181
|
+
// (claude 收到 result 后进程还可能拖几分钟)。测试就在这段窗口里读登记表。
|
|
182
|
+
type idleWindowEngine struct {
|
|
183
|
+
name string
|
|
184
|
+
after time.Duration
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func (e *idleWindowEngine) Name() string { return e.name }
|
|
188
|
+
func (e *idleWindowEngine) Detect() (bool, string) { return true, "fake://" + e.name }
|
|
189
|
+
|
|
190
|
+
// CapabilityFamily:常驻会话只对 claude / codebuddy 家族生效(agent.AppendSupportOf),
|
|
191
|
+
// 假引擎用这个名字接到 claude 的能力表上,不必借用会被别的用例先注册的真名。
|
|
192
|
+
func (e *idleWindowEngine) CapabilityFamily() string { return "claude" }
|
|
193
|
+
func (e *idleWindowEngine) Complete(context.Context, agent.Request) (agent.Response, error) {
|
|
194
|
+
return agent.Response{Text: "ok", Model: "fake-model"}, nil
|
|
195
|
+
}
|
|
196
|
+
func (e *idleWindowEngine) Stream(_ context.Context, _ agent.Request, onEvent func(agent.StreamEvent)) (agent.StreamResult, error) {
|
|
197
|
+
onEvent(agent.StreamEvent{Kind: agent.KindText, Text: "ok"})
|
|
198
|
+
onEvent(agent.StreamEvent{Kind: agent.KindTurnEnd})
|
|
199
|
+
time.Sleep(e.after)
|
|
200
|
+
return agent.StreamResult{
|
|
201
|
+
Response: agent.Response{Engine: e.name, Text: "ok", Model: "fake-model", SessionID: "sid-idle-window"},
|
|
202
|
+
}, nil
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 常驻会话本轮结束后必须写 idle,且**与 --idle 取值无关**(2026-10-21 报障):
|
|
206
|
+
// 桌面端恒传 `--idle 0`,写 idle 的条件若带上 opts.idle > 0 就对真实调用完全不生效 ——
|
|
207
|
+
// 登记表在引擎收尾那几分钟里仍是 running,移动端跟进据此认为上一轮还在跑,
|
|
208
|
+
// 排队追问永不下发(PC 却按事件流显示「已完成」)。
|
|
209
|
+
func TestIdleMarkedAfterTurnWithZeroIdle(t *testing.T) {
|
|
210
|
+
dir := shortSessionsDir(t)
|
|
211
|
+
t.Setenv(session.EnvDir, dir)
|
|
212
|
+
eng := &idleWindowEngine{name: "fake-idle-zero", after: 2 * time.Second}
|
|
213
|
+
registerFake(eng)
|
|
214
|
+
|
|
215
|
+
done := make(chan error, 1)
|
|
216
|
+
go func() {
|
|
217
|
+
_, _, err := runAskCmd(t, "", "--save-registry", "-e", eng.name, "--stream", "--idle", "0", "-p", "一轮")
|
|
218
|
+
done <- err
|
|
219
|
+
}()
|
|
220
|
+
|
|
221
|
+
var got string
|
|
222
|
+
deadline := time.Now().Add(10 * time.Second)
|
|
223
|
+
for time.Now().Before(deadline) {
|
|
224
|
+
if recs := mustListSessions(t, dir); len(recs) == 1 {
|
|
225
|
+
got = recs[0].State
|
|
226
|
+
if got == session.StateIdle {
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
time.Sleep(20 * time.Millisecond)
|
|
231
|
+
}
|
|
232
|
+
if got != session.StateIdle {
|
|
233
|
+
t.Fatalf("--idle 0 时本轮结束后登记表应为 idle,实际 %q", got)
|
|
234
|
+
}
|
|
235
|
+
select {
|
|
236
|
+
case err := <-done:
|
|
237
|
+
if err != nil {
|
|
238
|
+
t.Fatalf("收尾报错: %v", err)
|
|
239
|
+
}
|
|
240
|
+
case <-time.After(20 * time.Second):
|
|
241
|
+
t.Fatal("没收工")
|
|
242
|
+
}
|
|
243
|
+
// idle 只是中途态:进程退出后要落到终态 done。
|
|
244
|
+
if recs := mustListSessions(t, dir); len(recs) != 1 || recs[0].State != session.StateDone {
|
|
245
|
+
t.Fatalf("收尾后应为 done,实际 %+v", recs)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
180
249
|
// --keep-alive=false 关掉常驻:不建 socket,turn 结束即退出(老行为)。
|
|
181
250
|
func TestKeepAliveCanBeDisabled(t *testing.T) {
|
|
182
251
|
dir := shortSessionsDir(t)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
// requirements.go - `--requirements` / `--consolidate-requirements`:
|
|
4
|
+
// 「把会话自动聚合成需求」收成 core 标准能力后的**CLI 出口**。
|
|
5
|
+
//
|
|
6
|
+
// 为什么是两条命令而不是一条带动作参数的:
|
|
7
|
+
//
|
|
8
|
+
// · `--requirements [<ws|dir>]` 读:工作区 → 模块 → 功能点清单(和 --sessions 同族,
|
|
9
|
+
// 客户端列表页每刷一次都会调,**绝不能**顺手触发 LLM);
|
|
10
|
+
// · `--consolidate-requirements [<ws|dir>]` 写:跑一轮归纳(会调模型,几秒到几分钟)。
|
|
11
|
+
//
|
|
12
|
+
// 读与写的代价差三个数量级,混成一个命令之后「刷新列表」就变成了「每次都烧一次模型」。
|
|
13
|
+
//
|
|
14
|
+
// 与 `--summarize-title` 同一套理由:core 不跑服务,调用方(magic-client / 桌面 / 移动端)
|
|
15
|
+
// exec 这两条命令即可,不必碰 ~/.magic-agent/requirements 的磁盘格式。
|
|
16
|
+
//
|
|
17
|
+
// 退出码:
|
|
18
|
+
//
|
|
19
|
+
// 0 成功(读:拿到清单;写:归纳完成或无事可做 noop)
|
|
20
|
+
// 1 失败(引擎不可用 / 模型输出不可解析 / 工作区不存在)
|
|
21
|
+
// 2 参数错(只给了 --requirement-force 之类没给主 flag)
|
|
22
|
+
|
|
23
|
+
import (
|
|
24
|
+
"fmt"
|
|
25
|
+
"strings"
|
|
26
|
+
|
|
27
|
+
"github.com/spf13/cobra"
|
|
28
|
+
|
|
29
|
+
"github.com/darren/magic-agent/internal/requirements"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// runRequirements 列需求清单:`--requirements all|<ws_id|目录>`。
|
|
33
|
+
//
|
|
34
|
+
// 值必须是显式的(all = 全部工作区,或某个 ws id / 目录路径):
|
|
35
|
+
// cobra 的 string flag 不能"传了 flag 不带值",与其让 `--requirements` 报
|
|
36
|
+
// "flag needs an argument",不如把「全部」写成一个能看见的取值。
|
|
37
|
+
//
|
|
38
|
+
// ⚠️ 只读:不摄入新记忆、不触发归纳。清单里的 pending 是「已摄入但还没归纳」的条数,
|
|
39
|
+
// 摄入发生在归纳那一轮(见 requirements.Consolidate),所以这里读到的 pending 反映的是
|
|
40
|
+
// 上一次归纳之后的状态 —— 客户端据此显示「有 N 条待整理」已经够用。
|
|
41
|
+
func runRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
42
|
+
where := strings.TrimSpace(opts.requirements)
|
|
43
|
+
listAll := where == "" || where == "all" || where == "*"
|
|
44
|
+
|
|
45
|
+
all, err := requirements.Workspaces()
|
|
46
|
+
if err != nil {
|
|
47
|
+
return fmt.Errorf("--requirements: 读取需求清单失败: %w", err)
|
|
48
|
+
}
|
|
49
|
+
out := struct {
|
|
50
|
+
OK bool `json:"ok"`
|
|
51
|
+
Root string `json:"root"`
|
|
52
|
+
Workspaces []requirements.Workspace `json:"workspaces"`
|
|
53
|
+
}{OK: true, Root: requirements.Root(), Workspaces: []requirements.Workspace{}}
|
|
54
|
+
|
|
55
|
+
if listAll {
|
|
56
|
+
out.Workspaces = all
|
|
57
|
+
return printJSON(cmd.OutOrStdout(), out)
|
|
58
|
+
}
|
|
59
|
+
// 按 id 或目录过滤:客户端手里通常只有目录,两种写法都认。
|
|
60
|
+
id := requirements.DirID(where)
|
|
61
|
+
for _, w := range all {
|
|
62
|
+
if w.ID == where || (id != "" && w.ID == id) || (w.Dir != "" && w.Dir == where) {
|
|
63
|
+
out.Workspaces = append(out.Workspaces, w)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if len(out.Workspaces) == 0 {
|
|
67
|
+
return fmt.Errorf("--requirements %s: 不认识这个工作区(既不是 ws id,也没有任何会话/文档过这个目录)", where)
|
|
68
|
+
}
|
|
69
|
+
return printJSON(cmd.OutOrStdout(), out)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// runConsolidateRequirements 立即归纳一轮:`--consolidate-requirements <ws_id|目录|.>`。
|
|
73
|
+
//
|
|
74
|
+
// `.` = 当前目录(与 `-w` 的默认语义一致:用户站在哪个项目里,整理的就是哪个项目)。
|
|
75
|
+
func runConsolidateRequirements(cmd *cobra.Command, opts *askOptions) error {
|
|
76
|
+
where := strings.TrimSpace(opts.consolidateReqs)
|
|
77
|
+
if where == "." {
|
|
78
|
+
where = ""
|
|
79
|
+
}
|
|
80
|
+
timeout := opts.reqTimeout
|
|
81
|
+
if !flagChanged(cmd, "requirement-timeout") {
|
|
82
|
+
timeout = requirements.DefaultTimeout
|
|
83
|
+
}
|
|
84
|
+
res, err := requirements.Consolidate(cmd.Context(), where, requirements.Options{
|
|
85
|
+
Force: opts.reqForce,
|
|
86
|
+
All: opts.reqAll,
|
|
87
|
+
Engine: opts.reqEngine,
|
|
88
|
+
Model: opts.reqModel,
|
|
89
|
+
Timeout: timeout,
|
|
90
|
+
Auto: opts.reqAuto,
|
|
91
|
+
})
|
|
92
|
+
// busy 不是错误:另一个进程正在归纳(客户端的防抖 + 用户手点撞上了),
|
|
93
|
+
// 这一轮跳过即可 —— 把它当失败会让界面弹一个没有意义的红字。
|
|
94
|
+
if err != nil && !requirements.IsBusy(err) {
|
|
95
|
+
if res.Error == "" {
|
|
96
|
+
res.Error = err.Error()
|
|
97
|
+
}
|
|
98
|
+
// 先输出结构化结果(客户端要能显示 last_error),再返回错误拿非零退出码。
|
|
99
|
+
_ = printJSON(cmd.OutOrStdout(), res)
|
|
100
|
+
return fmt.Errorf("--consolidate-requirements %s: %s", strings.TrimSpace(opts.consolidateReqs), err)
|
|
101
|
+
}
|
|
102
|
+
return printJSON(cmd.OutOrStdout(), res)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// bindRequirementFlags 注册需求聚合的 flags。
|
|
106
|
+
//
|
|
107
|
+
// --requirements / --consolidate-requirements 的值都可以留空(空串是合法输入:
|
|
108
|
+
// 前者 = 列全部,后者 = 当前目录),所以派发用 flagChanged 判断,不看值。
|
|
109
|
+
func bindRequirementFlags(cmd *cobra.Command, opts *askOptions) {
|
|
110
|
+
f := cmd.PersistentFlags()
|
|
111
|
+
f.StringVar(&opts.requirements, "requirements", "", "列出需求清单(工作区 → 模块 → 功能点,JSON:{ok,root,workspaces})。值留空 = 列全部;给 ws id 或目录 = 只列那一个。只读,不调模型")
|
|
112
|
+
f.StringVar(&opts.consolidateReqs, "consolidate-requirements", "", "立即把会话记忆归纳成需求文档(会调模型)。值留空 = 当前目录;给 ws id 或目录 = 归纳那一个")
|
|
113
|
+
f.BoolVar(&opts.reqForce, "requirement-force", false, "配合 --consolidate-requirements:没有未归纳的记忆也跑一轮(手动「立即整理」)")
|
|
114
|
+
f.BoolVar(&opts.reqAll, "requirement-all", false, "配合 --consolidate-requirements:全量重放历史记忆(回填 / 滚动校正)。仍走增量合并,只增不减,不会推翻已有内容")
|
|
115
|
+
f.StringVar(&opts.reqEngine, "requirement-engine", "", "--consolidate-requirements 用哪个引擎(空 = 自动:首次初始化挑能读目录的引擎,增量归纳用 llm);客户端应传自己的默认引擎")
|
|
116
|
+
f.StringVar(&opts.reqModel, "requirement-model", "", "配合 --consolidate-requirements:增量归纳用的模型(空 = 引擎默认;首次目录初始化始终用初始化引擎的默认模型)")
|
|
117
|
+
f.DurationVar(&opts.reqTimeout, "requirement-timeout", requirements.DefaultTimeout, "配合 --consolidate-requirements:单次模型调用超时(默认归纳 4m / 首次目录初始化 9m)")
|
|
118
|
+
f.BoolVar(&opts.reqAuto, "requirement-auto", false, "配合 --consolidate-requirements:自动归纳(不触发首次按目录初始化;插件轮末 / 每日兜底用)")
|
|
119
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"path/filepath"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/darren/magic-agent/internal/requirements"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// TestRequirementModifiersDispatchWithMainFlag 修饰 flag 与主 flag 同时出现时,
|
|
12
|
+
// 必须先走「归纳」分支,而不是被修饰项的分支误判成「只给了修饰项」。
|
|
13
|
+
//
|
|
14
|
+
// 2026-10-22 实测:`--consolidate-requirements <dir> --requirement-force` 直接
|
|
15
|
+
// exit 2(报「只在 --consolidate-requirements 下成立」)—— 手机端点「整理」时
|
|
16
|
+
// 带 force 就是这条路径。用例只断言错误**不是**那句误判;目录不存在时真正的错误
|
|
17
|
+
// 应该是「工作区不存在」。
|
|
18
|
+
func TestRequirementModifiersDispatchWithMainFlag(t *testing.T) {
|
|
19
|
+
t.Setenv(requirements.EnvDir, t.TempDir())
|
|
20
|
+
missing := filepath.Join(t.TempDir(), "not-a-workspace")
|
|
21
|
+
_, _, err := runAskCmd(t, "", "--consolidate-requirements", missing, "--requirement-force")
|
|
22
|
+
if err == nil {
|
|
23
|
+
t.Fatal("不存在的目录应该报错(说明真的走到了归纳分支)")
|
|
24
|
+
}
|
|
25
|
+
if strings.Contains(err.Error(), "只在 --consolidate-requirements 下成立") {
|
|
26
|
+
t.Fatalf("主 flag + 修饰 flag 被误判成只有修饰 flag:%v", err)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// TestRequirementModifierAloneStillFails 单给修饰项仍然要明确报错(不静默忽略)。
|
|
31
|
+
func TestRequirementModifierAloneStillFails(t *testing.T) {
|
|
32
|
+
_, _, err := runAskCmd(t, "", "--requirement-force")
|
|
33
|
+
if err == nil || !strings.Contains(err.Error(), "只在 --consolidate-requirements 下成立") {
|
|
34
|
+
t.Fatalf("单给 --requirement-force 应明确报错,实际 %v", err)
|
|
35
|
+
}
|
|
36
|
+
}
|
package/internal/cli/root.go
CHANGED
|
@@ -78,6 +78,7 @@ import (
|
|
|
78
78
|
"github.com/spf13/cobra"
|
|
79
79
|
|
|
80
80
|
"github.com/darren/magic-agent/internal/agent"
|
|
81
|
+
"github.com/darren/magic-agent/internal/team"
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
// Version 版本号。
|
|
@@ -386,6 +387,26 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
386
387
|
session_id 到手后改名为 <session_id>.jsonl(读侧两个名字都认)——
|
|
387
388
|
于是「正在跑的会话」在别的客户端里当场可读,引擎整轮不给 id 的形态也不白开存档。
|
|
388
389
|
|
|
390
|
+
需求聚合(--requirements / --consolidate-requirements):
|
|
391
|
+
把**会话里聊过的需求**自动归纳成功能点文档(两级:模块 → 功能点)。口径与「会话标题」
|
|
392
|
+
同源:只留一份实现,PC / 移动端 / 直接跑 CLI 的会话一视同仁(无论谁发起的都算)。
|
|
393
|
+
magic-agent --requirements all # 列全部工作区(只读,不调模型)
|
|
394
|
+
magic-agent --requirements ~/work/proj # 只列这个目录(也认 ws id)
|
|
395
|
+
magic-agent --consolidate-requirements ~/work/proj # 手动整理一轮(**会调模型**)
|
|
396
|
+
magic-agent --consolidate-requirements . --requirement-force # 没有新记忆也跑一轮
|
|
397
|
+
magic-agent --consolidate-requirements . --requirement-all # 全量重放(回填 / 校正)
|
|
398
|
+
第一次整理 = 初始化:state 里没有 bootstrap_at 时,先按**空间目录**反推一版需求基线
|
|
399
|
+
(有界扫描 + 带工具的引擎读代码,自动选 claude/codebuddy/codex/dsh 等能读目录的引擎),
|
|
400
|
+
并把此前旧会话记为已消费;之后只处理上次游标之后的新会话(没有新会话就 noop)。
|
|
401
|
+
目录为空不是错误(回落到会话增量)。插件的自动归纳带 --requirement-auto,只做增量、
|
|
402
|
+
绝不触发初始化 —— 否则每轮对话都可能悄悄花几分钟读代码。
|
|
403
|
+
记忆来源是 core 自己的会话日志(kind:"user"),会话按「-w 指定的 workspace,没有则它跑的
|
|
404
|
+
当前目录」归到工作区。存储 ~/.magic-agent/requirements/(MAGIC_AGENT_REQUIREMENTS 可覆盖),
|
|
405
|
+
每个功能点一篇 markdown(features/<模块>/<名称>.md),可直接用访达打开。
|
|
406
|
+
归纳是**增量的、只增不减**:模型只给 desc / 需求点 / 结论 / 来源编号,骨架与日期由代码写;
|
|
407
|
+
已有文档的模块不搬家(模型每轮的 module 会漂)。
|
|
408
|
+
⚠️ 读清单绝不触发归纳 —— 只有 --consolidate-requirements(或插件的轮末防抖)才调模型。
|
|
409
|
+
|
|
389
410
|
附件(-a/--attach:文件+提示词,比如截图加提示词):
|
|
390
411
|
每个引擎尽量用**自己的原生方式**收附件,能力见 --engines 的 attachments 字段:
|
|
391
412
|
codex flag:-i 原生 -i/--image(图片;resume 轮退化为提示词里的路径)
|
|
@@ -467,6 +488,23 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
467
488
|
case flagChanged(cmd, "before"), flagChanged(cmd, "limit"):
|
|
468
489
|
// 同上:--before / --limit 是 --session-log 的翻页参数,单给必然是误解。
|
|
469
490
|
return &usageError{fmt.Errorf("--before / --limit 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
|
|
491
|
+
/* ⚠️ 主 flag 必须排在修饰 flag 之前:下面那串 --requirement-* 是
|
|
492
|
+
--consolidate-requirements 的修饰项,但它们在命令行上**同时出现**时也各自
|
|
493
|
+
Changed=true。先判修饰项会把「主 flag + force」误判成「只给了 force」,
|
|
494
|
+
调用方带 force 手动整理时直接 exit 2(2026-10-22 修)。 */
|
|
495
|
+
case flagChanged(cmd, "requirements"):
|
|
496
|
+
// 读清单:值留空 = 列全部工作区(`--requirements ""` 也是合法调用)。
|
|
497
|
+
return runRequirements(cmd, opts)
|
|
498
|
+
case flagChanged(cmd, "consolidate-requirements"):
|
|
499
|
+
// 写:跑一轮归纳。值留空 = 当前目录。
|
|
500
|
+
return runConsolidateRequirements(cmd, opts)
|
|
501
|
+
case flagChanged(cmd, "requirement-force"), flagChanged(cmd, "requirement-all"),
|
|
502
|
+
flagChanged(cmd, "requirement-engine"), flagChanged(cmd, "requirement-model"),
|
|
503
|
+
flagChanged(cmd, "requirement-timeout"), flagChanged(cmd, "requirement-auto"):
|
|
504
|
+
// 这些只修饰「归纳」这一轮。单给它们必然是调用方想归纳却写漏了主 flag ——
|
|
505
|
+
// 静默忽略就等于「传了参数什么都没发生」。
|
|
506
|
+
return &usageError{fmt.Errorf("--requirement-force / -all / -engine / -model / -timeout / -auto " +
|
|
507
|
+
"只在 --consolidate-requirements 下成立:请配合它使用")}
|
|
470
508
|
case flagChanged(cmd, "stop"):
|
|
471
509
|
// 用「传过没有」而不是「值非空」判断:`--stop ""` 也要走参数校验
|
|
472
510
|
//(exit 2 提示要 id),而不是掉进「提问」分支报 empty prompt。
|
|
@@ -480,6 +518,10 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
480
518
|
return runContract(cmd, opts)
|
|
481
519
|
case opts.engines:
|
|
482
520
|
return runEngines(cmd, opts)
|
|
521
|
+
case opts.team:
|
|
522
|
+
// 团队模式必须排在 --stream 之前:它自己就是流式路径的一种运行模式
|
|
523
|
+
//(同一套 ready / seq / result 收尾),但内部编排完全不同。
|
|
524
|
+
return runTeamAsk(cmd, args, opts)
|
|
483
525
|
case opts.stream:
|
|
484
526
|
return runStreamAsk(cmd, args, opts)
|
|
485
527
|
}
|
|
@@ -908,8 +950,18 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
908
950
|
/* 匿名结构体就地引用函数内的 row 类型:不必把 row 提到包级,
|
|
909
951
|
也就不会把上面那张字段注释表搬来搬去。 */
|
|
910
952
|
return printJSON(cmd.OutOrStdout(), struct {
|
|
911
|
-
ContractVersion int
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
953
|
+
ContractVersion int `json:"contractVersion"`
|
|
954
|
+
// Version 本 CLI 的版本号(ldflags 从 package.json 注入 internal/cli.Version)。
|
|
955
|
+
// 新增(2026-10-22,只增字段不升 contractVersion):客户端「我的」页要显示
|
|
956
|
+
// 当前连着的这份 magic-agent 是哪一版 —— 移动端界面是桌面端 `/m/` 远程加载的,
|
|
957
|
+
// 自己不带版本;core 的版本才是用户真正在跑的那一份。老 core 没有该字段 → 空串。
|
|
958
|
+
Version string `json:"version,omitempty"`
|
|
959
|
+
Features []string `json:"features"`
|
|
960
|
+
Engines []row `json:"engines"`
|
|
961
|
+
// TeamRoles 团队子任务的角色清单(name / kind / duty)。
|
|
962
|
+
// 新增(2026-10-22,只增字段不升 contractVersion):客户端「团队模式」页
|
|
963
|
+
// 要按角色配置路由(`--team-route role:writer=…`),角色名不能由客户端硬编码 ——
|
|
964
|
+
// 老 core 没有这个字段,客户端回落到内置清单。
|
|
965
|
+
TeamRoles []team.RoleInfo `json:"teamRoles"`
|
|
966
|
+
}{ContractVersion: agent.ContractVersion, Version: Version, Features: agent.CoreFeatures(), Engines: rows, TeamRoles: team.RoleInfos()})
|
|
915
967
|
}
|