magic-agent 1.0.49 → 1.0.108
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 +2168 -143
- 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/CLIENT-CONTRACT.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| --- | --- | --- | --- |
|
|
14
14
|
| 引擎调用 | `-e <engine> -p "<prompt>"`(含超时、重试、退出码) | 稳定 | `internal/cli/ask.go` |
|
|
15
15
|
| 探测与能力 | `--engines [--no-models]`:`ok` / `bin` / `install` / `version` / `models` / `permission` / `reasoning` / `capabilities` … | 稳定 | `internal/cli/root.go` |
|
|
16
|
-
| 契约 envelope | `--contract`:`{contractVersion, features, engines[]}`,`engines[]` 与 `--engines`
|
|
16
|
+
| 契约 envelope | `--contract`:`{contractVersion, version, features, engines[], teamRoles[]}`,`engines[]` 与 `--engines` **同构**;`version` 是 core 自身版本(2026-10-22 只增字段,与 `--version` 同一份,老 core 缺失时客户端显示「—」);`teamRoles[]` 为团队角色清单(2026-10-22 只增字段,字段缺失时客户端回落内置清单) | v2 | `internal/cli/root.go` + `internal/agent/capability.go` + `internal/team/role.go` |
|
|
17
17
|
| 事件流 | `--stream --events`:NDJSON,每行带 `v` / `seq`,首行 `ready`;文本/轮次事件带状态收敛四元组(`item_id` / `item_revision` / `status` / `snapshot`,见 1.1.1) | v1 | `internal/cli/events.go` |
|
|
18
18
|
| 控制回传 | `--stream --control`:stdin 收 NDJSON 命令(`ping` / `interrupt` / `stop` / `answer`) | v1 | `internal/cli/control.go` |
|
|
19
19
|
| 会话追加 | `--stream --keep-alive` + `--append <id>`;`--sessions` / `--stop <id>` | 稳定 | `internal/cli/session.go` |
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
并额外产出四类 core 自己的事件:`ready`(首行,带 `engine` / `model`)、
|
|
44
44
|
`pong`、`interrupted`、`stopped`、`answered`、`control_error`。
|
|
45
45
|
|
|
46
|
+
**`model` 一律是完整 id,可能带 `custom-local:` 前缀**(codebuddy / codebuddy-ai 的
|
|
47
|
+
自定义端点模型,见 `docs/engines.md`)。这个值要原样用于 `POST /desk/ask` 的 `model`
|
|
48
|
+
与 `-m`;**展示时必须剥掉前缀**(`custom-local:` 只是 App / CLI 内部的端点区分约定,
|
|
49
|
+
对用户是噪点)。`--engines` 的 `models[]` 与 `model_credits` 键**已经剥过**
|
|
50
|
+
(`agent.StripCustomLocalPrefix`),事件与 `/desk/sessions` 里的 `model` **没有** ——
|
|
51
|
+
客户端渲染前自己剥。
|
|
52
|
+
|
|
46
53
|
两条不变量(有测试钉住):**每轮一定以 `result` 或 `error` 收尾**;
|
|
47
54
|
`--events` 与 `--stream` 的字段集只差 `v` / `seq`。
|
|
48
55
|
|
|
@@ -57,8 +64,21 @@
|
|
|
57
64
|
| `item_revision` | 该 item 的第几个版本,从 1 起**单调递增**(每条 item 各自计数) |
|
|
58
65
|
| `status` | `running`(还在增长)/ `final`(已定型)/ `failed`(异常终止) |
|
|
59
66
|
| `snapshot` | 该 item 到此刻的**整段内容**(累积快照,不是增量)——⚠️ **只在实时流里**;落盘不写(见下方「落盘不存 snapshot」) |
|
|
67
|
+
| `parent` | 父委派 id(2026-10-21 只增字段,不升 `contractVersion`)。空 = 主会话自己的事件。语义与 claude 系原生的 `parent_tool_use_id` **完全一致**:带它的 `text` / `thinking` / `tool_use` / `tool_result` 属于父委派那次调用内部,渲染时挂进父卡的嵌套区而不是另起一笔 |
|
|
60
68
|
| `error` / `reason` | 仅 `turn_failed`:完整错误链 / 最内层根因 |
|
|
61
69
|
|
|
70
|
+
⚠️ **`parent` 不是团队模式专用**(这一点很关键,别把它设计成开关)。claude / codebuddy 原生
|
|
71
|
+
就会吐 `parent_tool_use_id`(子 agent 调用),core 只是把它**统一**成一个字段;
|
|
72
|
+
渲染层只需要一条规则:**有 `parent` 就挂进那张卡**,不必区分来源。
|
|
73
|
+
团队模式**不再**使用它:子任务是独立会话,主会话里只有 `tasks` 快照(见 1.4)。
|
|
74
|
+
|
|
75
|
+
`sub_session_id`(2026-10-21 只增字段,不升 `contractVersion`,omitempty):这条事件所属
|
|
76
|
+
**子会话**自己的引擎会话 id。它与 `session_id` 是两个轴 —— `session_id` 是本对话的锚点
|
|
77
|
+
(决定日志文件名、决定追问续接谁),`sub_session_id` 只是挂在别的会话下面的子会话,
|
|
78
|
+
**只用于展示与留档**,随主会话一起落盘、回放可复原。任何消费方都**不得**拿它当
|
|
79
|
+
`session_id` 用(拿去续接/命名会把主会话锚点改掉,点开历史就空了)。
|
|
80
|
+
团队模式已不再转发子任务事件,因此也不再发这个字段(见 1.4)。
|
|
81
|
+
|
|
62
82
|
⚠️ **`text` 仍是增量片段**,不是 snapshot。两者并存是刻意的:
|
|
63
83
|
`text` 服务终端直打与老消费者逐行拼接,`snapshot` 服务「按 item 覆盖」的
|
|
64
84
|
多端同步。消费方二选一即可。
|
|
@@ -145,12 +165,170 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
145
165
|
|
|
146
166
|
| 集合 | 位置 | 回答的问题 |
|
|
147
167
|
| --- | --- | --- |
|
|
148
|
-
| `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control`) |
|
|
168
|
+
| `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control` / `feature.session.team` / `feature.requirements`) |
|
|
149
169
|
| `engines[].capabilities[]` | 每个引擎 | 这个**引擎**能做什么(`session.stream` / `attachment.native` / `engine.install` …) |
|
|
150
170
|
|
|
151
171
|
静态与运行态严格分开:`capabilities` / `features` 是静态声明,`ok` / `streaming` / `models` /
|
|
152
172
|
`version` 是运行态。客户端按前者定界面形态,按后者定控件可用与降级提示。
|
|
153
173
|
|
|
174
|
+
`feature.session.team`(2026-10-21 只增量,**不**进 `requiredFeatures`):这台 core 认
|
|
175
|
+
`--team`。客户端据它决定**要不要显示团队模式开关** —— 没有就整个不显示(点了必然 exit 2 的
|
|
176
|
+
开关比没有更糟)。插件对带 `team` 的请求也先查这个位,缺了回 **501 + 中文原因**。
|
|
177
|
+
|
|
178
|
+
`feature.requirements`(2026-10-22 只增量,**不**进 `requiredFeatures`):这台 core 认
|
|
179
|
+
`--requirements` / `--consolidate-requirements`(见 §3.4.1)。插件对 `GET /desk/requirements`
|
|
180
|
+
与 `POST /desk/requirements/consolidate` 都先查这个位,缺了回 **501 + 中文原因**。
|
|
181
|
+
|
|
182
|
+
⚠️ **移动端的「需求」页签常驻,不按这个位显隐**(用户 2026-10-22 定:「移动端永远显示」)。
|
|
183
|
+
理由:页签是本产品的固定功能分区,忽隐忽现会让用户找不到入口;老 core 上点进去由 501
|
|
184
|
+
分支给出「当前 core 不支持需求聚合」的明确文案,比一个消失的入口更容易理解。
|
|
185
|
+
其他客户端可自行取舍,但**一旦显示就必须能说清为什么不工作**(不要把 501 吞成空列表)。
|
|
186
|
+
|
|
187
|
+
**移动端底栏(2026-10-22 定稿)**:`任务 · 需求 ·「+」· 发现 · 我的`。
|
|
188
|
+
|
|
189
|
+
- 「定时任务」页签**已删除**。它的功能没删:会话菜单里的「定时任务」仍然能唤起(现在给一句
|
|
190
|
+
「core 没有清单接口」的说明)。删的理由是那一页本来只有一个占位说明,占着导航反而误导。
|
|
191
|
+
- **底栏高度 68px**(不含 safe-area)。60 时图标顶距栏顶只有 8px、文字底距栏底 9px ——
|
|
192
|
+
视觉上"顶格",读作「图标掉进了一条缝里」。68 让上下各留 13px 对称边距;
|
|
193
|
+
`.tab` 另加 `padding:6px 0`,不靠 `min-height` 的余量吃饭
|
|
194
|
+
(换字号/字体时不会瞬间贴边)。
|
|
195
|
+
- 中间的「+」**不是页签**,是一个动作(新建会话),所以它没有选中态、不参与 `is-on`。
|
|
196
|
+
形态是移动端底栏主操作的**标准做法**:一枚**完整的圆**(直径 60),
|
|
197
|
+
圆心坐在底栏顶边线上(上露 30 / 下沉 30)—— 露出来的那半圈就是「半圆轮廓」。
|
|
198
|
+
⚠️ 不要把它直接造成半圆形状(直边躺在栏上会变成拱门/布片),也不要加描边
|
|
199
|
+
(用户明确否掉过描边版本)。
|
|
200
|
+
- **「嵌」成立的条件只有一条**:按钮与底栏的**接触边缘必须比四周暗**,阴影由接触处向外淡出。
|
|
201
|
+
这就是为什么按钮自己**不能加外投影**(外投影=浮起来),也不能靠 `box-shadow: inset`
|
|
202
|
+
做那圈槽影(inset 的暗边从元素边界向内,管不到元素外的那道槽;强行用会把亮度峰值
|
|
203
|
+
放在紧贴按钮的一圈,读成光晕)。实现改用 `radial-gradient` 把暗环锚在 r=30(按钮半径)、
|
|
204
|
+
到 33 淡完;槽底填 `--fab-well`(**栏面稍暗一档的同色系**,不是页面底色 `--canvas`:暗色下
|
|
205
|
+
canvas 比栏面暗 14 阶会成黑箍,浅色下只差 7 阶等于没画)。两个主题各一套令牌。
|
|
206
|
+
⚠️ **露出的槽宽只有 3px**(洞半径 33 − 按钮半径 30)。曾经做到 8px,那圈灰色槽底
|
|
207
|
+
读作「按钮下面垫了个盘子」—— 看得见槽、看不清槽底,才等于「切出来的」。
|
|
208
|
+
- **形态恒定**:进入会话页不换形状。会话页底部是 composer,靠给 `.composer` 加
|
|
209
|
+
`padding-bottom:60px` 让出按钮上露的 30px + 30px 空隙,
|
|
210
|
+
不要用「把按钮缩小/收进栏里」来躲 —— 同一个动作在两张页上长得不一样会让人怀疑点错。
|
|
211
|
+
⚠️ 这 60px **不叠加** `env(safe-area-inset-bottom)`:底栏自己已经吞掉安全区,
|
|
212
|
+
再叠一次会让输入框凭空上移一大截(v1.0.88 的错误之一)。
|
|
213
|
+
|
|
214
|
+
### 1.4 团队模式(`--team`)
|
|
215
|
+
|
|
216
|
+
**团队模式是会话层的一种运行模式,不是新引擎**:同一个会话、一个 `session_id`、一条事件流、
|
|
217
|
+
一条 `result` 收尾,变的只是「谁在干活」。
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
magic-agent -e claude --stream --events --team \
|
|
221
|
+
--team-route image=llm/hunyuan-image-3.0 --team-route video=arkclaw \
|
|
222
|
+
--team-parallel 4 --team-task-timeout 5m --team-permission accept-edits \
|
|
223
|
+
-p '做一套水世界的小游戏,附一张主视觉图'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| flag | 说明 |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `--team` | 开启。**必须配合 `--stream`**(过程靠事件流呈现,一次性 envelope 装不下)|
|
|
229
|
+
| `--team-route <key>=<engine>[/<model>]` | 给计划阶段、某类子任务或某个角色指定引擎/模型,可重复。`key ∈ plan\|text\|code\|image\|video\|game`(`plan` = 计划阶段,不是可执行子任务的类别),或 `role:<name>`(裸角色名也认:writer / researcher / reviewer / coder / designer / director / builder,与五类子任务一一覆盖;`video` / `code` 仍是类别键,不会被当成角色);**不给就是跟随主引擎**(六类一致)。优先级:任务写死 > 角色 > 类别 > 主引擎 |
|
|
230
|
+
| `--team-parallel` | 同时最多跑几个子任务(默认 4)|
|
|
231
|
+
| `--team-task-timeout` | 单个子任务超时(默认 = 总超时/2,夹在 1m..20m)|
|
|
232
|
+
| `--team-plan-only` | 只出计划与任务书,不执行(先看方案)|
|
|
233
|
+
| `--team-adaptive` | 第一轮执行后按「必需任务失败 + 交接单 open 项」再规划一轮补充任务(**默认开**;只补一轮、最多 3 个、id `a1..aN`)。`false` 关闭 |
|
|
234
|
+
| `--team-split` | 拆解粒度:`coarse`(最多 3 个)\| `standard`(6,默认)\| `fine`(10)。只改「最多拆几个」,其余拆解规则不变 |
|
|
235
|
+
| `--team-split-code` | 简单代码也拆成独立子任务(**默认关**:单文件脚本 / 几行改动由主引擎在最终回复里直接给出)|
|
|
236
|
+
| `--team-rule role:<name>=<规则>` | 改写某个角色「这类任务适合什么」,替掉角色表里的内置职责(进计划提示词;不改执行者的系统提示词)。可重复;规则折叠成一行、上限 500 字;未知角色 / 空规则 → 退出码 2 |
|
|
237
|
+
| `--team-permission` | 子任务的权限上限(**默认 `accept-edits`**,刻意低于主会话默认档:子任务不该继承完全访问)|
|
|
238
|
+
| `--team-run-dir` | 覆盖运行目录的父目录 |
|
|
239
|
+
|
|
240
|
+
**事件形状(客户端要认的只有一条)**:主会话里是**一张任务列表卡**,每个子任务是一条**普通会话**。
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
notice {text:"团队模式:正在生成计划…(已 N 秒)"} ← 计划阶段每 5s 一次
|
|
244
|
+
notice {text:"计划已生成(N 个子任务):<PLAN.md>"} ← 计划落定
|
|
245
|
+
tasks {text:"{\"tasks\":[ … ]}"} ← 任务列表**全量快照**
|
|
246
|
+
notice {text:"团队模式:t1「<标题>」仍在运行…(已 N 秒无新输出)"} ← 静默 >30s 时每 20s 一次
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`tasks` 快照(`agent.KindTasks`)的 `text` 是 JSON:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{"tasks":[{"id":"t1","title":"写正文","role":"writer","kind":"text",
|
|
253
|
+
"engine":"claude","model":"","status":"running","deps":["t0"],
|
|
254
|
+
"note":"","session_id":"…","run_id":"…","output":"…"}]}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| 字段 | 说明 |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `status` | `pending` / `running` / `done` / `failed` / `skipped`。渲染映射:`pending→pending`、`running→in_progress`、`done→completed`、`failed→failed`、`skipped→skipped` |
|
|
260
|
+
| `deps` | 上游任务 id(有依赖的行口径是「等待上游 t1」)|
|
|
261
|
+
| `note` | 失败 / 跳过的一句话原因(成功时空)|
|
|
262
|
+
| `session_id` / `run_id` | 这个子任务**自己的**会话(点进去看过程)。`run_id` 一开跑就有,`session_id` 等引擎给出;都可能为空(没开登记 / 引擎不给 id)|
|
|
263
|
+
| `output` | 该任务的 `OUT-<id>.md` 路径 |
|
|
264
|
+
|
|
265
|
+
**快照语义**:每一份都是全量,后来居上,客户端**原地替换**同一张卡即可(不要按行追加,
|
|
266
|
+
也不要为每行另开一张卡)。计划一落定就发第一份(此时全部行都在,含还在等上游的),
|
|
267
|
+
之后凡是「开跑 / 绑定子会话 / 收尾 / 再规划追加」都整份重发一次。
|
|
268
|
+
|
|
269
|
+
**子任务的过程事件不进主会话**:它们写进子会话自己的日志(那条会话的事件流与点进去看到的一样),
|
|
270
|
+
主会话里不再出现 `parent`、不再有合成的 `tool_use{name:"Task"}` 委派卡、也不再有
|
|
271
|
+
`sub_session_id` 转发。客户端**不需要识别团队模式**,按 `tasks` 快照画一张任务列表卡即可;
|
|
272
|
+
点某一行 = 打开对应 `run_id` 的会话。
|
|
273
|
+
|
|
274
|
+
再规划(`--team-adaptive`)只是**往同一张卡里追加行**(`a1..aN`)并发一条 `notice`:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
notice {text:"团队模式:发现 N 个未完成 / 未解决项,正在判断是否补充任务…"}
|
|
278
|
+
notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续执行"}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
不需要补充 / 再规划失败时只有一条说明性 `notice`,整轮仍以 `result` 收尾 —— 客户端无需为此加分支。
|
|
282
|
+
再规划给出的补充任务若与原任务**同名**(忽略大小写与首尾空白),服务端直接丢弃并不追加行
|
|
283
|
+
(那等价于重试,重试由执行器负责);客户端不需要做同名合并,收到的快照里不会出现重复标题行。
|
|
284
|
+
|
|
285
|
+
拆几个子任务由**需求里的交付物**决定,不由提示词里用户对分工/数量的自述决定:
|
|
286
|
+
五类产物(text / code / image / video / game)里出现哪几类、各有几件,就拆成对应数量的子任务
|
|
287
|
+
(用户写「这是两个独立的小任务」只是线索,不是上限也不是定额)。**`code` 门槛更高**:
|
|
288
|
+
只有多文件 / 要构建或跑测试 / 有明确改动面的复杂代码才拆子任务,简单代码(单文件脚本、
|
|
289
|
+
一次性片段、几行配置、问答式求改法)由主引擎在最终回复里直接给出,不拆。
|
|
290
|
+
|
|
291
|
+
**子任务 = 普通会话**(不是非交互的一次性运行):每个子任务都有一份独立的登记记录与
|
|
292
|
+
事件日志(自己的 `run_id` / `session_id` / pid),点进去看到的就是它完整的执行过程,也能
|
|
293
|
+
单独停止。它们通过父指针挂在这条主会话下面(见下方 `--sessions` 投影的
|
|
294
|
+
`parent_run_id` / `parent_session_id`),会话列表按父指针缩进展示;**主会话内部只留那张
|
|
295
|
+
任务列表卡**,子任务的过程事件不转发、不合成委派卡。
|
|
296
|
+
|
|
297
|
+
**上下文策略:不共享,但共享计划文档。** 运行目录(`--team-run-dir` > `<workspace>/.magic/team/<run_id>`
|
|
298
|
+
> `~/.magic-agent/team/<run_id>`)里:
|
|
299
|
+
|
|
300
|
+
| 文件 | 谁写 | 说明 |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| `PLAN.md` | core,**0444** | 全队共享的唯一事实源(再规划后**重写为完整集合**,含补充任务 `a1..aN`)|
|
|
303
|
+
| `TASK-<id>.md` | core,**0444** | 该任务的完整任务书(诉求 + 上游产出摘要 + 约束 + 交付物 + 验收)|
|
|
304
|
+
| `<task_id>/` | 子引擎 | 该子任务唯一可写目录(进程 cwd)|
|
|
305
|
+
| `OUT-<id>.md` | core | 子任务正文 + 交付物核对结果(不信任自报)|
|
|
306
|
+
| `ARTS-<id>.json` | core | 从事件里归拢的产物绝对路径 |
|
|
307
|
+
|
|
308
|
+
**降级(写死,客户端按此解释「为什么结果不完整」)**:
|
|
309
|
+
计划解析失败 → 明说一行,退化成单任务;**计划阶段/某类子任务没单独指定引擎 → 跟随主引擎**
|
|
310
|
+
(plan / text / code / image / video / game 六类一致,见「我的 → 团队模式」子页面);只有路由到的引擎不存在
|
|
311
|
+
才 `failed`(`reason = "引擎不存在:X"`);汇总失败 → 回落成清单式摘要。
|
|
312
|
+
三种降级都照常以 `result` 收尾。
|
|
313
|
+
|
|
314
|
+
`result` envelope 多三个字段(只增):`mode:"team"`、`plan_path`、`run_dir`,
|
|
315
|
+
以及 `tasks[]`(每项 `{id,kind,role,title,engine,model,state,reason,output,arts,missing,session_id}`;
|
|
316
|
+
**v2 起 `role` 必出**且是唯一作者字段(规范名,认不出兜底 `writer`),`kind` 由 `RoleKind(role)`
|
|
317
|
+
派生、只读(旧客户端把 `kind` 当作者字段解释,因此升了契约版本);`session_id` 是该子任务**自己的**
|
|
318
|
+
引擎会话 id,可能没有,只用于回查,不是本会话锚点)。
|
|
319
|
+
会话登记表里这条会话带 `mode:"team"` 与 `agents[]`(`{kind,engine,model,count,sessions}`,按类别归并;
|
|
320
|
+
`sessions` 是该类别下各子任务的引擎会话 id)。`--sessions` 的投影里还会多两个**父指针**字段:
|
|
321
|
+
`parent_run_id` / `parent_session_id`(空 = 顶层会话)—— 子会话这两项指向团队主会话,
|
|
322
|
+
客户端据此把它缩进挂在主会话下面(读侧优先 `parent_session_id`,缺失再按 `parent_run_id` 认亲;
|
|
323
|
+
父会话不在本次列表里就回落成顶层行,别丢)。`mode` / `agents` / 父指针都由 `--sessions`
|
|
324
|
+
**投影出来**(不是只存在登记表文件里),列表页读的就是它们。
|
|
325
|
+
`missing[]` 是「任务书要求的交付物没落地」的清单(引擎不认 `deliverables` 时就是全缺),
|
|
326
|
+
**不是失败**:`state` 仍是 `done`,正文照样在 `output` 里。
|
|
327
|
+
|
|
328
|
+
`--team-plan-only` 同样写**任务书 + 任务目录**(不只是 `PLAN.md`):它的用途是「先确认方案」,
|
|
329
|
+
用户要看的是「真要跑起来,每个子任务会收到什么」;`tasks[]` 在 plan-only 下为空数组
|
|
330
|
+
(没有执行),`plan_path` / `run_dir` 照常给。
|
|
331
|
+
|
|
154
332
|
## 二、插件承诺的面(`magic-client`)
|
|
155
333
|
|
|
156
334
|
| 面 | 路径 | 状态 |
|
|
@@ -164,7 +342,8 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
164
342
|
| 控制与回审批 | `POST /desk/ask/{id}/control`(与 core 的 `--control` 同协议,插件不解释 op) | 已实现 |
|
|
165
343
|
| 断线续传 | `GET /desk/ask/{id}/events?after=<seq>`(按**本轮流 seq** 增量补拉;带 `done` / `code` / `stderr` / `snapshotRequired`;`after=0` 时首条是本轮**合成的用户提问**,见 2.4) | 已实现 |
|
|
166
344
|
| 运行中列表 | `GET /desk/asks`(**本进程**正在跑的轮次,每项 `{id, engine, started, session?, run?}`;与 sessions 的区别见下) | 已实现 |
|
|
167
|
-
|
|
|
345
|
+
| 运行中会话尾随 | `GET /desk/session/{id}/follow?[after=<seq>][&lean=1]`(SSE:`open` / 增量事件 / `snapshot` / `end`;读磁盘日志,**跨进程**,客户端接续运行中会话的**首选**通道;见 3.4) | 已实现 |
|
|
346
|
+
| 本体文件出流 | `GET /desk/media?path=<绝对路径>[&w=<px>]`(`?token=` 供 `<img>` / `<video>` 直接引用;**只发 image / video / audio**,`Range` 可拖动)。带 `w` 时按宽缩放并重编码(`64..4096`,失败/更大则回退原图,结果内存缓存);移动端传输层自动追加 `w=480`(缩略图;点开看大图时不带 `w`,取原文件) | 已实现 |
|
|
168
347
|
| 上传文件回读 | `GET /desk/uploads/{name}[?w=<px>]`(同上 `w` 语义) | 已实现 |
|
|
169
348
|
| 产物存在性探测 | `POST /desk/path-probe`:`{base, paths[]}` → `{ok, items:[{path, abs, exists, size}]}`;相对路径按 `base` 拼、`~/` 展开、`file://` 剥前缀、只认 regular 文件、最多 50 条。对话内核的「本轮产物」靠它把正文里的路径变成卡片(宿主经 `window.desk.pathProbe` 供进来) | 已实现 |
|
|
170
349
|
| 产物文本回读 | `POST /desk/path-read`:`{path, base}` → `{ok:true, text, size, path}` / `{ok:true, tooBig:true, size}` / `{ok:true, binary:true, size}` / `{ok:false, error}`,统一 200。上限 512 KB;前 8000 字节含 NUL 判二进制;`~/`、`file://`、相对 `base` 同 `path-probe` 口径。移动端产物卡「查看」经 `window.desk.pathRead` 用它打开 Markdown / 文本 | 已实现 |
|
|
@@ -251,6 +430,54 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
251
430
|
| `reasoning` | 可选,思考档位(`off` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` \| `max`),透传给 core 的 `--reasoning`。**空 = 不传**,core 自己的默认档是 **`max`**(开着)—— 所以「关闭思考」必须**显式传 `off`**,省略等于开着 + 最高档 |
|
|
252
431
|
| `session` | 可选,续接一个已有会话(传 `/desk/sessions` 里的 id)→ core 的 `--session` |
|
|
253
432
|
| `idle` | 可选,常驻会话空闲收工时长;默认 `0` = 本轮结束就收工 |
|
|
433
|
+
| `team` | 可选,团队模式(见下)。**nil / 缺省 = 关闭**(默认关)|
|
|
434
|
+
|
|
435
|
+
`team`(2026-10-21 只增字段,不升 `contractVersion`):
|
|
436
|
+
|
|
437
|
+
```json
|
|
438
|
+
"team": {
|
|
439
|
+
"routes": { "role:coder": {"engine":"claude"},
|
|
440
|
+
"image": {"engine":"llm","model":"hunyuan-image-3.0"},
|
|
441
|
+
"video": {"engine":"arkclaw"} },
|
|
442
|
+
"rules": { "role:coder": "只拆多文件、要构建或测试的改动" },
|
|
443
|
+
"parallel": 4,
|
|
444
|
+
"planOnly": false,
|
|
445
|
+
"taskTimeout": "5m",
|
|
446
|
+
"split": "standard",
|
|
447
|
+
"splitCode": false,
|
|
448
|
+
"adaptive": true
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
| 子字段 | 说明 |
|
|
453
|
+
| --- | --- |
|
|
454
|
+
| `routes` | `kind → {engine, model?}`,`kind ∈ plan\|text\|code\|image\|video\|game`,或角色键 `role:<name>`(2026-10-22 只增字段,不升 `contractVersion`)。**类别名 / 角色名非法、只给 model 不给 engine、引擎不在 profile 白名单 → 400**(翻译由插件做,裁决由 core 做)。整条 `{engine:""}` 视为「该项不单独指定」:类别跟随主引擎,角色跟随其所属类别,插件不下发。角色名清单从 `/desk/health.teamRoles`(core `--contract.teamRoles` 转发)取;老 core 没有该字段时插件只做格式校验,名字交给 core 裁决 |
|
|
455
|
+
| `rules` | 可选,`role:<name> → 规则文本`(2026-10-22 只增字段,不升 `contractVersion`)→ `--team-rule`。改写该角色「这类任务适合什么」(计划提示词里的角色表),空文本不下发、留空 = core 内置 `duty`;键必须是已知角色(类别键 / 未知角色 → 400)|
|
|
456
|
+
| `parallel` | 0 / 缺省 = core 默认 4 |
|
|
457
|
+
| `planOnly` | 只出计划,不执行 |
|
|
458
|
+
| `taskTimeout` | Go duration 字符串;空 = core 按总超时推算 |
|
|
459
|
+
| `split` | 可选,`coarse` \| `standard` \| `fine`(大小写不敏感;空 = standard)→ `--team-split`;非法值 → 400 |
|
|
460
|
+
| `splitCode` | 可选,`true` = 简单代码也拆(→ `--team-split-code`);false / 缺省 = 由主引擎直接给出 |
|
|
461
|
+
| `adaptive` | 可选,`false` → `--team-adaptive=false`(关掉再规划);true / 缺省 = core 默认开 |
|
|
462
|
+
|
|
463
|
+
⚠️ **`team` 不落 core 的配置文件**:它是「这一轮怎么跑」,不是这台机器的属性。开关状态由
|
|
464
|
+
客户端自己**按会话**记(移动端 `localStorage` 的 `magic.team.sessions.v1`,`{sid: bool}`),
|
|
465
|
+
每轮随 body 下发 —— core 因此无状态。改 A 会话的开关只影响 A,新建任务从关起;
|
|
466
|
+
设备级偏好只有 `routes` / `rules` / `parallel` / `split` / `splitCode` / `adaptive`。
|
|
467
|
+
⚠️ **团队模式下 `idle` 无意义**:子任务不共享上下文、主引擎也不是每轮都能续上,没有常驻会话可追加。
|
|
468
|
+
开着 `team` 时客户端不该依赖 `answer` 控制命令(core 会回 `control_error`,不静默丢)。
|
|
469
|
+
|
|
470
|
+
`/desk/health.teamRoles`(2026-10-22 只增字段,不升 `contractVersion`):团队角色清单
|
|
471
|
+
`[{name, kind, duty}]`,顺序即界面顺序(来自 core `--contract.teamRoles`)。客户端的
|
|
472
|
+
「团队模式」页按**计划 + 7 个角色**共 8 行渲染(每行常显「这类适合什么」= 规则文本或
|
|
473
|
+
`duty`;2026-10-22 起不再并列展示类别行,类别仍是 core 的路由回退层);字段缺失
|
|
474
|
+
(老 core)时回落客户端内置清单。插件也用这份清单校验 `role:<name>`,不另维护一套角色表。
|
|
475
|
+
|
|
476
|
+
`/desk/health.version`(2026-10-22 只增字段,不升 `contractVersion`):插件转发 core
|
|
477
|
+
`--contract.version`(与 `magic-agent --version` 同一份)。移动端「我的」页用它在
|
|
478
|
+
「版本」一行显示 `v<version>` —— 界面本身是 `/m/` 从桌面端远程加载的、不带自己的版本,
|
|
479
|
+
用户要看的是「此刻连着的这份桌面端是哪一版」。老 core 没有该字段 → 空串,界面显示「—」,
|
|
480
|
+
客户端不写死版本号。
|
|
254
481
|
|
|
255
482
|
关于 `permission` 的两条语义,客户端必须照做:
|
|
256
483
|
|
|
@@ -350,7 +577,7 @@ core 的 stdout 事件流里没有 user 事件(它只写进会话日志),
|
|
|
350
577
|
| 文件 | 角色 |
|
|
351
578
|
| --- | --- |
|
|
352
579
|
| `client-ui/shared/conv/` | **对话内核**(分层分片):一条事件流 → 画成对话的**唯一实现**(消息 / 思考 / 工具组与「任务耗时」大组 / 提问卡 / 附件与产物 / 事件归一)。宿主只使用它,见 `client-ui/conv/README.md` 与 `manifest.json` 的 `hostSymbols`;校验 `npm run check:conv` |
|
|
353
|
-
| `client-ui/shared/conversation.js` | **组件本体**(渲染 + 流式 + 控制 +
|
|
580
|
+
| `client-ui/shared/conversation.js` | **组件本体**(渲染 + 流式 + 控制 + 三项选择器 + + 弹层的「插件」入口)。PC 与移动端共用,是唯一来源 |
|
|
354
581
|
| `client-ui/shared/conversation.css` | 组件样式。结构尺寸共用;颜色走 3.3 的变量契约,由宿主供值 |
|
|
355
582
|
| `client-ui/shared/transport-http.js` | 传输层的**可选参考实现**(HTTP 版,打 `/desk/*`)。组件不依赖它 |
|
|
356
583
|
| `client-ui/index.html` | PC 宿主:oklch 令牌 + 一条顶栏 + 挂组件 |
|
|
@@ -380,19 +607,73 @@ const conv = MagicConversation.create({
|
|
|
380
607
|
onEvent, // 可选:每个 core 事件透传一份(只读,不许改写对话流)
|
|
381
608
|
onBusy, // 可选:一轮开始/结束
|
|
382
609
|
onPick, // 可选:用户落定引擎/模型时回调 (engine, model),宿主可记成默认值;只浏览左列不回调
|
|
610
|
+
beforeAsk, // 可选:每轮下发前的闸门 async (info) => boolean|undefined,见 3.2.2
|
|
611
|
+
onInterrupt, // 可选:(prompt) => boolean。这一轮还没拿到 askId 时用户点了打断,宿主认领排队
|
|
612
|
+
team, // 可选:初始是否开启团队模式(默认 false)
|
|
613
|
+
onTeamChange, // 可选:(on) => void。用户在**组件自己的**团队面板里改了开关时回调
|
|
614
|
+
teamConfig, // 可选:() => {routes, rules, parallel, split, splitCode, adaptive},每轮下发时现读(宿主持有这份偏好)
|
|
615
|
+
features, // 可选:core 能力位数组,或 () => 数组(异步拉到之后才亮出团队入口)
|
|
383
616
|
});
|
|
384
617
|
conv.send('…');
|
|
385
618
|
conv.interrupt(); conv.stop(); conv.answer('…');
|
|
386
619
|
conv.setSession(sid, s); conv.newSession();
|
|
387
620
|
conv.setEngine(n); conv.setModel(n); conv.setPermission(v); conv.setEngines(list);
|
|
388
|
-
conv.
|
|
621
|
+
conv.setTeam(on); conv.teamOn(); conv.teamLabel(); conv.openTeamPicker();
|
|
622
|
+
conv.featureOn('feature.session.team');
|
|
623
|
+
conv.refreshEngines(force?); conv.loadHistory(sid, after?); conv.destroy();
|
|
389
624
|
```
|
|
390
625
|
|
|
626
|
+
**团队模式在组件里是薄的**(这是刻意的,见 1.4):它只做三件事 —— 记住开关、按 `teamConfig()`
|
|
627
|
+
拼 `payload.team`、按 `features` 决定开关亮不亮。**没有任何画法**:子任务的事件带 `parent`,
|
|
628
|
+
内核早就把它们画成委派卡的嵌套区了,所以 PC / 移动 / magic-test 三端换了这个开关都不用改渲染。
|
|
629
|
+
|
|
630
|
+
`featureOn(name)` 的取值:`features` 给函数时现调(推荐 —— 能力清单是异步拉到的),
|
|
631
|
+
给数组时直接比。**没给 / 空 = 不知道 = 当作不支持**(诚实降级:不显示一个必然失败的开关)。
|
|
632
|
+
|
|
633
|
+
`onTeamChange` 为什么必须有:开关的**权威状态在宿主**(新建任务要沿用、要落
|
|
634
|
+
`localStorage`),而面板是组件画的 —— 不回传的话只有组件知道用户开过,
|
|
635
|
+
宿主那一侧重新挂载就丢(实测现象:开了 → 跑完 → 新建任务又变回关闭)。
|
|
636
|
+
`conv.setTeam(on)` 是宿主灌进来的,**刻意不触发**它:那是「用户改了」的出口,
|
|
637
|
+
宿主自己改完再收到自己的回调只会绕一圈。同理 `teamConfig` 永远是宿主说了算,
|
|
638
|
+
组件不缓存它,每轮现读。
|
|
639
|
+
|
|
640
|
+
`setSession(sid, s)` 对**同一条会话是幂等的**:历史已经画在这块画布里时(`histSid === sid`)
|
|
641
|
+
只同步外壳(标题 / 引擎 / 模型),不 `clearThread()`、不重拉历史 —— 移动端「返回列表 → 再点进来」
|
|
642
|
+
靠这一条才不会把整段历史重放一遍。宿主因此**可以**复用同一个组件实例(`client-ui-mobile`
|
|
643
|
+
的 `mountConversation` 就是这么做的);换会话、`newSession()`、读历史失败之后仍会正常重载。
|
|
644
|
+
|
|
645
|
+
### 3.2.2 beforeAsk:下发前的闸门(串行排队的挂载点)
|
|
646
|
+
|
|
647
|
+
```js
|
|
648
|
+
beforeAsk(info) // info: {isNew, session, workspace, engine, model, permission, prompt, attachments, team, queued}
|
|
649
|
+
// → Promise<true|undefined> 继续下发;Promise<false> 这一轮不发了
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
组件在**拿到 askId 之前**、把「进行中」态点亮之后调用它,等 Promise resolve 才真正下发,
|
|
653
|
+
**不设超时**(排队等前一条跑完可以很久)。要点:
|
|
654
|
+
|
|
655
|
+
- `info` 是这一轮**完整的下发参数**,不是只有 prompt。宿主若要把它落库、App 重启后替用户重发,
|
|
656
|
+
`session` 少了那句追问就变成一段新会话,`model` / `permission` 少了就跑成别的东西。
|
|
657
|
+
- `info.queued(on)` 是**排队态的裁决权**:只有宿主知道这一轮到底要等多久。宿主确认要等时调
|
|
658
|
+
`queued(true)`,放行时调 `queued(false)`(组件放行后也会兜底摘一次)。
|
|
659
|
+
组件**不按时间猜**——空闲时那道门一个请求往返就放行,按时间盖标记会先闪一下假的「排队中」再切回。
|
|
660
|
+
- 返回 `false` = 宿主判定这一轮作废(移动端串行模式里用户排队期间点了停止):组件清掉 busy、
|
|
661
|
+
留一条「已取消排队」提示就返回,不发任何请求。
|
|
662
|
+
- 返回 `false` = 宿主判定这一轮作废(移动端串行模式里用户排队期间点了停止):组件清掉 busy、
|
|
663
|
+
留一条「已取消排队」提示就返回,不发任何请求。
|
|
664
|
+
- 这一轮**还在 `beforeAsk` 里排队**时没有 askId,`conv.interrupt()` 会先问 `onInterrupt(prompt)`:
|
|
665
|
+
宿主返回 `true`(已撤掉排队)就算处理完,不再走 `transport.control` —— 否则那条路只会报
|
|
666
|
+
「还没拿到轮次 id」并什么都不做,排队中的任务根本停不下来。
|
|
667
|
+
- 移动端串行模式的口径是**同一 workspace 同时只有一个轮次在跑,不分新任务还是历史会话追问**
|
|
668
|
+
(追加追问与上一句写的是同一份会话日志,两个进程并发正是最该避免的场合)。
|
|
669
|
+
|
|
391
670
|
### 3.2.1 transport 契约(宿主实现,四个方法)
|
|
392
671
|
|
|
393
672
|
```js
|
|
394
673
|
{
|
|
395
|
-
listEngines()
|
|
674
|
+
listEngines(force?) // → Promise<[{engine, ok, models, capabilities}]>
|
|
675
|
+
// force=true = 跳过缓存重探(组件里 refreshEngines(true) 透传)
|
|
676
|
+
// 现成的 transport-http 缓存 5 分钟(/desk/engines?models=1 要 1s)
|
|
396
677
|
ask(payload, {onOpen, onEvent, onEnd})// payload: {engine, model?, permission?, session?, prompt}
|
|
397
678
|
// onOpen(runID) 给控制通道的落点
|
|
398
679
|
// onEnd(exitCode) **必须**被调一次,否则界面会一直卡在 busy
|
|
@@ -403,6 +684,28 @@ conv.refreshEngines(); conv.loadHistory(sid, after?); conv.destroy();
|
|
|
403
684
|
|
|
404
685
|
掌天瓶那一侧的 transport 走 IPC → CLI;`transport-http.js` 是走 `/desk/*` 的现成实现。
|
|
405
686
|
|
|
687
|
+
### 3.2.2 transport 的可选能力:插件(加号 → 插件)
|
|
688
|
+
|
|
689
|
+
会话输入框的 + 弹层里,`插件` 这一行**只在 transport 提供下面两个方法时出现**
|
|
690
|
+
(宿主没实现就不画,而不是画一张空表):
|
|
691
|
+
|
|
692
|
+
```js
|
|
693
|
+
listPlugins() // → Promise<{plugins:[…]}>,形状同 GET /desk/plugins
|
|
694
|
+
pluginAction({id, action}, {onOpen, onStep, onLine, onEnd})
|
|
695
|
+
// action: install | upgrade;对应 POST /desk/plugins/{id}/{action} 的 SSE
|
|
696
|
+
// onEnd(result) 只会被调一次(含失败:{ok:false, code, error})
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**为什么命令不由组件拼**:安装命令来自服务端那张**固定表**,组件只传 `id` 与 `action`。
|
|
700
|
+
组件里写死 CLI 名/命令行,等于让前端决定在用户机器上执行什么。
|
|
701
|
+
|
|
702
|
+
组件不做的事:探测装没装(那是 `installed` / `version` / `bin`)、决定装什么包
|
|
703
|
+
(那是 `install` / `upgrade` 两串)。组件只负责摆出来、复制出去、发起执行、把
|
|
704
|
+
`end.plugin`(重探测后的完整信息)就地换掉那一行,让按钮从「安装」翻成「升级」。
|
|
705
|
+
|
|
706
|
+
> 发现页那套(`client-ui-mobile` 的 `#plugins` 视图)与这里**同一份数据源**,
|
|
707
|
+
> 两处不各自判状态 —— 否则会出现「手机上显示已安装、装的时候说没装」。
|
|
708
|
+
|
|
406
709
|
### 3.3 主题:颜色变量契约
|
|
407
710
|
|
|
408
711
|
宿主必须给出下面这组变量的值(组件样式只用不定义)。两套现成的值可抄:
|
|
@@ -434,12 +737,14 @@ magic-agent 把会话的查询与修改都做成 CLI 命令,调用方 exec 即
|
|
|
434
737
|
| 向前翻更早 | 追加 `--before <skip> --limit <n>` → 另带 `count / before / nextBefore / firstSeq / more`(与 `--after` 互斥) |
|
|
435
738
|
| 读历史(精简) | 追加 `--session-log-lean`:输出省掉 `snapshot`(**只增参数**,不传则行为与旧版一致) |
|
|
436
739
|
| 停止会话 | `magic-agent --stop <session_id\|run_id>`(只停得掉**已存档**的会话) |
|
|
437
|
-
| **删除会话** | `magic-agent --delete-session <session_id\|run_id>` → `{id, records_removed, logs_removed, titles_removed}` |
|
|
740
|
+
| **删除会话** | `magic-agent --delete-session <session_id\|run_id>` → `{id, records_removed, logs_removed, titles_removed, statuses_removed}`(`statuses_removed` 2026-10-21 只增) |
|
|
438
741
|
| 追加消息 | `magic-agent --append <session_id\|run_id> -p "…"` |
|
|
439
742
|
| 跑一轮(流式 + 控制) | `magic-agent --stream --events --control -e <engine> -p "…"`,控制命令从 stdin 进 |
|
|
440
743
|
| **存档这一轮** | 追加 `--save-registry`(登记表)与 `--save-history`(历史 jsonl),**两者默认都关** |
|
|
441
744
|
| **归纳会话标题** | `magic-agent --summarize-title <session_id\|run_id>` → `{id, title, cached?}` |
|
|
442
745
|
| 读已存标题 | `magic-agent --session-title <id>` → `{id, title}`(不调 LLM) |
|
|
746
|
+
| **需求清单** | `magic-agent --requirements all\|<ws_id\|目录>` → `{ok, root, workspaces[]}`(只读,不调 LLM,见 §3.4.1) |
|
|
747
|
+
| **归纳需求** | `magic-agent --consolidate-requirements <ws_id\|目录\|.>` → 一轮归纳结果(**会调模型**) |
|
|
443
748
|
|
|
444
749
|
`--session-log` 的输出形状与插件同名的 HTTP 接口**逐字段对齐**,两条路可互换。
|
|
445
750
|
HTTP 侧的精简开关是**查询参数**(2026-10-20,只增参数,不传则与旧版一致):
|
|
@@ -458,6 +763,89 @@ snapshot(回放只用增量 `text`),思考区本来也是「一段连续 t
|
|
|
458
763
|
边界语义:`after == lastSeq` 是**追平**(正常空增量,`snapshotRequired=false`);
|
|
459
764
|
`after > lastSeq` 才要求重拉全量。
|
|
460
765
|
|
|
766
|
+
⚠️ `lastSeq` 是**文件末尾那条事件**的 seq,**不是全文件最大 seq**(2026-10-21 修正):
|
|
767
|
+
续接会话(`--session <id>`)时旧版先把 `<run_id>.jsonl` 合并进 `<session_id>.jsonl`,
|
|
768
|
+
同一文件里 seq 会从 1 重来(实测一条 2 万行日志 17 处回落)。旧口径取全局最大值,
|
|
769
|
+
于是游标永远停在旧轮高位、`--after` 把新一轮整段滤掉,画布看着「已追平」但没有任何进度。
|
|
770
|
+
按末尾 seq 判定的另一个效果:陈旧游标(旧轮高位)会命中 `snapshotRequired=true`,
|
|
771
|
+
客户端全量重读后游标自动落回新一轮末位,无需人工清理。
|
|
772
|
+
|
|
773
|
+
#### 运行中会话的持续尾随:`GET /desk/session/{id}/follow`(2026-10-21 新增接口,不升 `contractVersion`)
|
|
774
|
+
|
|
775
|
+
`GET /desk/session/{id}/follow?lean=1[&after=<seq>]` → **SSE**,跨进程。用途:打开一条
|
|
776
|
+
**还在跑**的会话时把进度接上。与前三条通道的分工:
|
|
777
|
+
|
|
778
|
+
| 通道 | 数据源 | 跨进程 | 形态 |
|
|
779
|
+
| --- | --- | --- | --- |
|
|
780
|
+
| `GET /desk/asks` | 本插件进程内存 | ✗ | 一次性列表 |
|
|
781
|
+
| `GET /desk/ask/{id}/events` | 本插件进程内存 | ✗ | 一次性增量 |
|
|
782
|
+
| `GET /desk/session/{id}/messages\|events` | 磁盘 jsonl | ✓ | 一次性快照 / 增量 |
|
|
783
|
+
| **`GET /desk/session/{id}/follow`** | **磁盘 jsonl** | **✓** | **持续推送,直到终态** |
|
|
784
|
+
|
|
785
|
+
为什么需要:掌天瓶 Electron 壳直接 exec core(不经本插件),它发起的一轮在 `/desk/asks` 里
|
|
786
|
+
**从来不存在**;PC 端与移动端互相看不到进度就是这个原因。文件是两边唯一共享的进度源。
|
|
787
|
+
|
|
788
|
+
帧形状(与 `/desk/ask` 的 SSE 同形,解析器可复用):
|
|
789
|
+
|
|
790
|
+
```
|
|
791
|
+
event: open data: {"session":"<id>","after":<起点>}
|
|
792
|
+
data: <事件> 增量事件,形状与 /desk/session/{id}/events 的 events 逐条一致
|
|
793
|
+
event: snapshot data: {"session":"<id>","after":<起点>,"lastSeq":<文件末尾那条的 seq>}
|
|
794
|
+
event: end data: {"session":"<id>","after":<末位置>,"state":"done|failed|cancelled|interrupted|gone|idle","reason":"terminal|max_duration"}
|
|
795
|
+
: ping 心跳注释帧(约 10s 一次,无 data,客户端忽略)
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
客户端必须照做:
|
|
799
|
+
|
|
800
|
+
1. **只在会话还在跑时挂**(`--sessions` 行的 `status` ∈ `running` / `waiting_approval`,或 `alive=true`;
|
|
801
|
+
老版本 core 没有 `status` 时回落 `state`,见 3.4「会话状态」);
|
|
802
|
+
`idle`(常驻会话本轮已结束、进程还在等 `--append`)**不挂** —— 这一轮已经跑完,
|
|
803
|
+
直接走 `messages` 一次性快照。已收尾的会话同理。
|
|
804
|
+
2. **首选 `/follow`,`/desk/ask/{id}/events` 只做兜底**(2026-10-21 修正,原口径相反)。
|
|
805
|
+
只要清单说这条会话还在跑,就用 follow —— 哪怕本插件进程里也有它的运行中轮次。
|
|
806
|
+
为什么反过来:ask 缓冲有两个硬伤,长轮次下必然「点进去看一眼就静止」——
|
|
807
|
+
- 缓冲事件的 `seq` 是 **core 每轮的原始行号**(每轮从 1 起),而日志是**跨轮接号**的。
|
|
808
|
+
客户端拿缓冲 `seq` 当游标续拉,第二轮之后永远 `after >= lastSeq` → 一条都补不到;
|
|
809
|
+
- 缓冲有上限(`askBufMaxEvents` / `askBufMaxBytes`),超了从头部裁并置 `dropped`,
|
|
810
|
+
于是 `after=0` 直接 `snapshotRequired`,中间过程永久缺失。
|
|
811
|
+
|
|
812
|
+
两个前提,客户端必须遵守:
|
|
813
|
+
- 游标用**文件末尾那条**的 `seq`(不是全文件最大 —— 续跑会回落,见上一节);
|
|
814
|
+
- 跟着 follow 时那一轮**不封口**(不调 `finish` / `qaRunEnd`),收尾交给 `event: end`。
|
|
815
|
+
日志尾部常常还没有终态(1s 一批 flush),提前封口会让后续事件另起一张卡,
|
|
816
|
+
同一轮变成两张、正文各半截。
|
|
817
|
+
只有 `/follow` 建不起来(老版本 404)时,才退回 `/desk/ask/{id}/events`;那时历史回放
|
|
818
|
+
必须**截到这一轮那条 `user` 为止**,其余由缓冲从 0 补齐(两套 seq 口径不能混用)。
|
|
819
|
+
3. `event: snapshot` = 客户端给的 `after` **超前**了文件(日志被重置 / 换了一条会话)→
|
|
820
|
+
丢掉当前画面,按 `messages` 重拉一遍全量,再用新游标重新 follow。不要在半截上续写。
|
|
821
|
+
4. `event: end` 一定收摊:`reason=terminal` 按 `state` 落收尾(`failed`/`cancelled`/
|
|
822
|
+
`interrupted`/`gone` 不写作「已完成」,`done`/`idle` 写作「已完成」);
|
|
823
|
+
`reason=max_duration` 是服务端 30 分钟的上限(不是这一轮结束),提示用户重开再接。
|
|
824
|
+
5. 连接断开(网络 / 换会话 / 组件销毁)不是终态,别据此判定任务失败;重开一次即可续上。
|
|
825
|
+
6. 服务端没有这个路由(老版本,404)→ 退回旧行为(一次性快照),不要报错弹窗。
|
|
826
|
+
|
|
827
|
+
服务端行为:`?lean=1` 与 `messages/events` 同一口径;`after` 缺省 = 从文件头读。
|
|
828
|
+
日志尚未建立(core 刚起、还没落盘)但登记表已有这条会话 → **保持连接等待**,不 404;
|
|
829
|
+
两边都查不到 → 404。`<run_id>.jsonl` 在拿到 `session_id` 后改名,服务端每拍重解 id,
|
|
830
|
+
客户端不必重连。
|
|
831
|
+
|
|
832
|
+
⚠️ **同一条会话不再允许两个引擎进程同时续写(2026-10-21)**:core 在 `Begin` 时会停掉
|
|
833
|
+
同一 `session_id` 上还活着的上一次运行(写 `cancelled`),并且日志 writer 每条事件前比对
|
|
834
|
+
文件大小、被别的 writer 追加过就重新接号 —— 于是同一份日志的 `seq` **严格递增(允许跳号)**,
|
|
835
|
+
不会再出现两条 lane 成对重号的局面。客户端照旧按 `seq > after` 过滤即可;跳号不是丢事件,
|
|
836
|
+
**不要**据此判定快照缺失。背景:重号会让增量读把另一条 lane 的事件整段滤掉 ——
|
|
837
|
+
一端读到 `turn_end` 显示「已完成」,另一端永远等不到终态(用户 2026-10-21 报障
|
|
838
|
+
「两边状态不一致,一边结束了,一边还在显示运行中」)。
|
|
839
|
+
|
|
840
|
+
⚠️ **常驻会话轮次之间是 `idle`(2026-10-21 新增,非终态)**:`--keep-alive --idle > 0` 的
|
|
841
|
+
会话在 `turn_end` 时把登记表写成 `idle`(本轮结束、进程仍活着等 `--append`),下一轮第一个
|
|
842
|
+
事件到达再回到 `running`。此前这里一直写 `running`,于是同一份登记表在 PC(按事件流画
|
|
843
|
+
「已完成」)与别的端(读 `state` 以为还在跑)之间分叉:跟进连接空等到 30 分钟,移动端的
|
|
844
|
+
串行门(`state === 'running' || alive === true` 就排队)被一条已经跑完的会话永久挡住 ——
|
|
845
|
+
用户 2026-10-21 报障「pc是会话已结束 但是移动端状态未结束 不统一导致还在排队」。
|
|
846
|
+
客户端口径:`idle` **不算在跑**(`alive=false`),列表显示「已完成/空闲」,排队可以直接
|
|
847
|
+
放行;它也不是终态 —— 同一条会话还能被 `--append` / `--session` 续跑,别把它画成失败。
|
|
848
|
+
|
|
461
849
|
⚠️ **`--before` 的游标是「跳过条数」而不是 seq(2026-10-20)**:日志是多段 run 追加,
|
|
462
850
|
旧 writer 的 `seq` 每轮从 1 重新计数(实测一份 15737 行日志有 4 处回落),拿 seq 当反向
|
|
463
851
|
游标跨不过 run 边界。现在 `before` 表示**从文件尾跳过多少条非空行**(`0` = 最新一页),
|
|
@@ -511,6 +899,32 @@ core 侧只有一张空的 sid→title 覆盖表,于是:
|
|
|
511
899
|
⚠️ 老版本 core 没有 `--summarize-title` / `--sessions.title`(exit 1)—— 客户端应**如实回落**
|
|
512
900
|
(PC 用本地缓存的标题,移动端用 `prompt_head`),不要静默显示空标题。
|
|
513
901
|
|
|
902
|
+
#### 会话状态:与标题同一套存法,客户端只读(2026-10-21)
|
|
903
|
+
|
|
904
|
+
用户定调:「会话状态搞一个字段 跟会话标题一样存起来 更新这个字段来统一」。
|
|
905
|
+
此前状态只在**登记表**里,而登记表是 **run 级**的(一条会话每轮一个文件,各记各的 `state`):
|
|
906
|
+
客户端拿到哪一行取决于「它挑中了哪一轮」,于是同一会话在 PC(按事件流画「已完成」)与移动端
|
|
907
|
+
(读到上一轮的 `running`)分叉,移动端的串行门还会被已跑完的会话永久挡住 —— 就是 3.4 开头那条报障。
|
|
908
|
+
|
|
909
|
+
现在状态收成**一条会话一份**(`internal/session/status.go`),与标题同一套做法:core 在写登记表时
|
|
910
|
+
顺手更新,客户端只读一个字段。
|
|
911
|
+
|
|
912
|
+
| 事实 | 说明 |
|
|
913
|
+
| --- | --- |
|
|
914
|
+
| `--sessions` 给 `status` | **会话级唯一口径**,客户端状态判定一律先读它;`state` 降级为「这一轮的状态」,只在老版本 core 没有 `status` 时回落 |
|
|
915
|
+
| `alive` 与 `status` 同源 | `alive = (status == running)`。同一行不会出现 `status=idle` 却 `alive=true` |
|
|
916
|
+
| 覆盖表路径 | `~/.magic-agent/session-status.json`(与 `session-titles.json` 同目录),进程内缓存;`MAGIC_AGENT_SESSIONS` 指向别处时取它的父目录 |
|
|
917
|
+
| 表形状 | `{ "<session_id>": {"state","run_id","run_started","updated_at"} }`;键优先 `session_id`,引擎还没吐 id 时落在 `run_id` 上,拿到后迁移并删旧键(与标题同一坑:留双键就会按不同 id 读到两个状态) |
|
|
918
|
+
| `run_started` 排序守卫 | 写入方是**各轮的进程**:旧一轮收尾(`Finish` / `Stop` / 进程已不在 → `gone`)可能晚于新一轮开跑。只在写入轮**不早于**表里那一轮时才覆盖,迟到的 `done` 不会盖掉新一轮的 `running` |
|
|
919
|
+
| 写入时机 | 与登记表同一步(`Handle.write` + 所有 `writeRecord` 路径,含 `Stop` / 同会话旧轮被 supersede / 读侧归一 `gone`);不落登记表(`--save-registry` 未开)时状态也不留痕 |
|
|
920
|
+
| 删除会话一并清 | `--delete-session` / `DELETE /desk/session/{id}` 会删掉状态行(返回里的 `statuses_removed`),避免同 id 再建会话时继承一条过期状态 |
|
|
921
|
+
|
|
922
|
+
状态词表与登记表完全一致:`running` / `waiting_approval` / `idle`(非终态),
|
|
923
|
+
`done` / `failed` / `cancelled` / `interrupted` / `gone`(终态)。
|
|
924
|
+
|
|
925
|
+
⚠️ 老版本 core 的 `--sessions` 没有 `status` —— 客户端**如实回落 `state`**(行为与旧版一致,
|
|
926
|
+
两端可能仍分叉);**不要**因此自己按事件流另算一套状态。
|
|
927
|
+
|
|
514
928
|
⚠️ **存档是 opt-in 的**(用户 2026-10-12:「加一个参数 默认不存档」+「会话登记也不需要」):
|
|
515
929
|
CLI 默认**什么都不落**,因为绝大多数调用(手工跑一轮 / CI / 脚本编排)事后没人寻址。两份东西
|
|
516
930
|
互相独立,按需各开一个:
|
|
@@ -526,12 +940,105 @@ CLI 默认**什么都不落**,因为绝大多数调用(手工跑一轮 / CI
|
|
|
526
940
|
(2026-10-15:它的会话要能在移动端 H5 上被列出并点开)。`--append` 在没有登记记录时会按约定找 `<dir>/<run_id>.sock`
|
|
527
941
|
(`session.AppendSocketPath`),所以「默认开着常驻、却追加不了」不会发生。
|
|
528
942
|
|
|
943
|
+
### 3.4.1 需求聚合(`--requirements` / `--consolidate-requirements`,2026-10-22)
|
|
944
|
+
|
|
945
|
+
**能力定位**:把「会话里聊过的需求」自动归纳成功能点文档。这是从业务项目(掌天瓶 /
|
|
946
|
+
magic-test 的 `desktop/requirements.cjs`)收进 core 的**标准能力**,口径同「会话标题」:
|
|
947
|
+
只留一份实现,PC / 移动端 / 直接跑 CLI 的会话一视同仁。
|
|
948
|
+
|
|
949
|
+
```bash
|
|
950
|
+
magic-agent --requirements all # 列全部工作区(只读,不调模型)
|
|
951
|
+
magic-agent --requirements ~/work/proj # 只列这个目录(也认 ws id)
|
|
952
|
+
magic-agent --consolidate-requirements ~/work/proj # 手动整理一轮(**会调模型**;首次=按目录初始化)
|
|
953
|
+
magic-agent --consolidate-requirements . --requirement-force # 没有新记忆也跑一轮
|
|
954
|
+
magic-agent --consolidate-requirements . --requirement-all # 全量重放(回填/校正)
|
|
955
|
+
magic-agent --consolidate-requirements . --requirement-auto # 自动归纳:只增量,绝不初始化
|
|
956
|
+
magic-agent --consolidate-requirements . --requirement-engine claude # 用配置的默认引擎整理
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**引擎口径(2026-10-22 用户)**:整理的引擎就是**客户端配置的默认引擎**。移动端/PC 点「整理」
|
|
960
|
+
时把「我的 → 默认引擎/模型」放进 `POST /desk/requirements/consolidate` 的
|
|
961
|
+
`{"engine":"claude","model":"…"}`,插件原样组成 `--requirement-engine` / `--requirement-model`。
|
|
962
|
+
core 收到显式引擎就**只用它**:不能读目录(`llm` / `arkclaw` / `openclaw` / `codebuddy-gateway`)
|
|
963
|
+
或本机不可用会明确报错,不会偷偷换成别的引擎。只有 engine 为空(「自动(首个可用)」或直接跑
|
|
964
|
+
CLI)才按 `claude → codebuddy → codebuddy-ai → codex → trae → dsh` 自动挑。
|
|
965
|
+
|
|
966
|
+
**第一次整理 = 初始化(2026-10-22 用户口径)**:`--consolidate-requirements` 不是每次都从会话
|
|
967
|
+
全量重放。服务端按 `state.bootstrap_at` 判断:
|
|
968
|
+
|
|
969
|
+
- **从未初始化**(`bootstrap_at == 0`):先按**空间目录**反推一版需求基线(与 magic-test
|
|
970
|
+
的 `bootstrapWorkspace` 同一条路:有界扫描目录 + 带工具、能指定工作目录的引擎读代码,
|
|
971
|
+
产出用户视角的功能点)。初始化成功后**把此刻之前的会话记为已消费**,旧对话不再逐条重放。
|
|
972
|
+
引擎优先用调用方显式给的默认引擎;只有没给时才自动挑本机可用的 `claude / codebuddy /
|
|
973
|
+
codebuddy-ai / codex / trae / dsh`。默认引擎是 `llm` 这类不能读目录的会直接报错并提示换引擎。
|
|
974
|
+
目录为空不是错误:标记初始化完成,回落到会话增量。
|
|
975
|
+
- **已初始化**:只消费上次游标之后的**新会话**;没有新会话就是 `noop`(手动点「整理」也一样,
|
|
976
|
+
不会为了「点一下」去空跑一次模型)。
|
|
977
|
+
- **自动归纳**(插件轮末防抖 / 每日兜底)带 `--requirement-auto`:它**绝不触发初始化**,
|
|
978
|
+
只做增量。否则每轮对话都可能悄悄花几分钟读代码。
|
|
979
|
+
|
|
980
|
+
`initialized` 是清单里的显式字段(不要用 `last_consolidated_at > 0` 代替:自动归纳可以先
|
|
981
|
+
产出文档,但只有目录基线跑过才算初始化完成)。
|
|
982
|
+
|
|
983
|
+
`--requirements` 的输出(客户端只需认这三个字段):
|
|
984
|
+
|
|
985
|
+
```json
|
|
986
|
+
{"ok":true,"root":"~/.magic-agent/requirements","workspaces":[
|
|
987
|
+
{"id":"ws-8e190e35d7","dir":"/abs/proj","name":"proj",
|
|
988
|
+
"initialized":true,"pending":3,"memory":12,"feature_count":4,"last_consolidated_at":1761090000000,
|
|
989
|
+
"modules":[{"name":"登录","features":[
|
|
990
|
+
{"module":"登录","name":"手机号登录","file":"/abs/.../features/登录/手机号登录.md",
|
|
991
|
+
"desc":"用户用手机号登录","points":3,"conclusions":1,
|
|
992
|
+
"conclusion":"不做密码登录","sources":4,"has_final":true,
|
|
993
|
+
"final_preview":"用户用手机号登录,验证码 60 秒可重发","updated_at":1761090000000}]}]}]}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
`--consolidate-requirements` 的输出:`{ok, workspace, dir, noop?, bootstrap?, bootstrap_engine?,
|
|
997
|
+
scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pending, error?}`。
|
|
998
|
+
`noop=true` 是「没有未归纳记忆、也没 `--requirement-force`」的正常结果,不是错;
|
|
999
|
+
`bootstrap=true` 表示这一轮做了首次目录初始化(`bootstrap_engine` 是实际读目录的引擎,
|
|
1000
|
+
`scanned` 是扫描到的条目数,排障用)。
|
|
1001
|
+
|
|
1002
|
+
**三个必须知道的口径**:
|
|
1003
|
+
|
|
1004
|
+
- **记忆来源是 core 自己的会话日志**(`kind:"user"` 事件),不需要客户端埋点。会话按
|
|
1005
|
+
「`-w` 指定的 workspace,没有则它实际跑的 cwd」归到工作区 —— 于是任何客户端发起的会话都算。
|
|
1006
|
+
- **归纳是增量的、只增不减**:模型只给 `desc / points / conclusions / sources` 编号,
|
|
1007
|
+
骨架(最终版 / 需求描述 / 需求点 / 已定结论 / 来源对话 / 更新记录)与日期由代码写;
|
|
1008
|
+
已有文档的模块**不搬家**(模型每轮的 module 会漂,文档一旦归类就稳定)。
|
|
1009
|
+
- **存储**在 `~/.magic-agent/requirements/`(`MAGIC_AGENT_REQUIREMENTS` 可覆盖),
|
|
1010
|
+
与业务项目的 `~/.magic-test/requirements/` **互不相干、不自动迁移**:
|
|
1011
|
+
`<ws>/features/<模块>/<名称>.md` 是给人直接打开的 markdown。
|
|
1012
|
+
|
|
1013
|
+
**客户端要做的事**:列表页只调 `--requirements`(每刷一次都调,绝不顺手触发归纳);
|
|
1014
|
+
「整理」按钮调 `--consolidate-requirements`,并**传它要整理的那个空间 + 当前配置的默认引擎**
|
|
1015
|
+
(移动端需求页同时列出多个空间时,组头各给一枚「整理」,不要把「当前会话的空间」当成用户点的那个);
|
|
1016
|
+
顶栏另给「全部整理」:按空间**串行**逐个调同一个接口(有待整理的 → 未初始化的 → 其它),
|
|
1017
|
+
一个空间失败不阻断后面的,全部结束后汇总「成功/失败/新建/更新」。
|
|
1018
|
+
按钮在首次初始化时要允许几分钟等待(文案「初始化中…」),完成后再 `--requirements` 刷新。
|
|
1019
|
+
打开一篇文档读 `file` 绝对路径的正文即可(普通 markdown,走既有的阅读器)。插件在每轮 `/desk/ask` 收尾后**防抖 12s 自动归纳**
|
|
1020
|
+
该轮所在工作区(`MAGIC_CLIENT_NO_REQ_AUTO=1` 关闭),所以客户端不必自己轮询触发。
|
|
1021
|
+
移动端把「需求」做成常驻页签(见 §1.3 的说明),列表读失败要显示原因而不是空列表。
|
|
1022
|
+
**没有功能点的工作区不渲染**(用户 2026-10-22):「全空 + 一条灰字说明」比「一列空工作区」
|
|
1023
|
+
更像出了错;`feature_count == 0` 且模块里也没有行的那个 `workspaces[]` 项直接跳过,
|
|
1024
|
+
整页都为空时再合成一句提示(有 `pending` 就引导点「整理」,否则说「聊几句就会有」)。
|
|
1025
|
+
|
|
1026
|
+
|
|
529
1027
|
### 3.5 两条不变量
|
|
530
1028
|
|
|
531
1029
|
- **事件只读**:组件不接受对接方改写对话流,否则基础版行为会随对接方漂移。
|
|
532
1030
|
- **授权档位以 `internal/agent/permission.go` 为唯一真相**;`full` 等同
|
|
533
1031
|
`--dangerously-skip-permissions`,两端界面都必须把它显式标成危险档。
|
|
534
1032
|
|
|
1033
|
+
### 3.6 团队模式的界面归属(一条规则,别复制到别处)
|
|
1034
|
+
|
|
1035
|
+
| 东西 | 放哪 | 为什么 |
|
|
1036
|
+
| --- | --- | --- |
|
|
1037
|
+
| 开关 | **会话页右上角的运行设置里**,与「权限模式」并列(`conv.openTeamPicker()`)| 它是「这一轮怎么跑」,与授权档同级;用户 2026-10-21 原话「跟权限模式一样也支持选择」|
|
|
1038
|
+
| 配置(每类子任务的引擎/模型)| **「我的」页**(宿主自己的卡片,`conv.setTeam` / `teamConfig` 交给组件)| 它是**这台设备的偏好**,跨会话有效,与默认引擎/模型同一类东西 |
|
|
1039
|
+
| 渲染 | **主会话里一张任务列表卡**(`tasks` 快照 → 内核的 `todoList`),子任务本身是会话列表里的**子会话** | 三端(PC / 移动 / magic-test)都只用一份内核:团队模式只是「多一条 `tasks` 事件」,不新增第二套画法 |
|
|
1040
|
+
| 默认值 | **关闭** | 团队模式会起 N 个子进程、消耗 N 份额度,不能替用户默认打开 |
|
|
1041
|
+
|
|
535
1042
|
## 四、三层的责任边界
|
|
536
1043
|
|
|
537
1044
|
| 层 | 负责 | 明确不做 |
|