magic-agent 2.4.3 → 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 +284 -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 +6 -0
- 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 +3 -3
- 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/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/CLIENT-CONTRACT.md
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
| 面 | 命令 / 通道 | 状态 | 实现 |
|
|
13
13
|
| --- | --- | --- | --- |
|
|
14
14
|
| 引擎调用 | `-e <engine> -p "<prompt>"`(含超时、重试、退出码) | 稳定 | `internal/cli/ask.go` |
|
|
15
|
-
| 探测与能力 | `--engines [--no-models]`:`ok` / `bin` / `install` / `version` / `models` / `permission` / `reasoning` / `capabilities`
|
|
15
|
+
| 探测与能力 | `--engines [--no-models]`:`ok` / `bin` / `install` / `version` / `models` / `model_credits` / `credit_left` / `credit_suffix` / `model_quota` / `checkin_today` / `checkin_credit` / `permission` / `reasoning` / `capabilities` …(`credit_left` 为账号剩余额度,`credit_suffix` 为它的单位:codebuddy 系省略=积分,llm 的 MiniMax 套餐给 `%`;`model_quota` 为**按模型**的套餐额度快照 `{model: {interval_left, weekly_left}}`,仅 llm 的 MiniMax 模型给出,**按各自 apiKey 查**;`checkin_today`/`checkin_credit` 为每日签到状态,仅 codebuddy 系给出,查不到则省略;`logged_in` 为多账号槽位是否已登录,仅 codebuddy / codebuddy-ai 给出,客户端**只显示已登录的账号行**;`account` 为该槽位登录的账号昵称(票据里的 `account.nickname`,如 `Darren`,客户端画在引擎名下面)) | 稳定 | `internal/cli/root.go` + `internal/agent/credits.go` |
|
|
16
|
+
| 额度 / 签到现查 | `--quotas`:只查额度与签到,**不探测引擎、不启动任何 CLI**(`{engine, credit_left?, credit_suffix?, model_quota?, checkin_today?, checkin_credit?, logged_in?, account?}[]`,字段与 `--engines` 同名同义;秒级)。客户端用它把最新额度合并进缓存的引擎清单 —— 模型清单可缓存,额度不能 | 稳定 | `internal/cli/quotas.go` |
|
|
17
|
+
| 每日积分 | `--checkin <engine>`:领取该引擎账号的每日积分(codebuddy / codebuddy-ai),输出 `{engine, ok, credit, streak_days, checked_today, message?, credit_left?}`;业务失败(今天已领)也是 exit 0 + `ok:false` | 稳定 | `internal/cli/checkin.go` |
|
|
16
18
|
| 契约 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
19
|
| 事件流 | `--stream --events`:NDJSON,每行带 `v` / `seq`,首行 `ready`;文本/轮次事件带状态收敛四元组(`item_id` / `item_revision` / `status` / `snapshot`,见 1.1.1) | v1 | `internal/cli/events.go` |
|
|
18
20
|
| 控制回传 | `--stream --control`:stdin 收 NDJSON 命令(`ping` / `interrupt` / `stop` / `answer`) | v1 | `internal/cli/control.go` |
|
|
@@ -41,15 +43,49 @@
|
|
|
41
43
|
|
|
42
44
|
开 `--events` 后**字段集不变**,每行多 `v` / `seq`(`seq` 从 1 单调 +1),
|
|
43
45
|
并额外产出四类 core 自己的事件:`ready`(首行,带 `engine` / `model`)、
|
|
44
|
-
`pong`、`interrupted`、`stopped`、`answered`、`control_error`。
|
|
46
|
+
`pong`、`interrupted`、`stopped`、`answered`、`ask_answered`、`control_error`。
|
|
47
|
+
|
|
48
|
+
**`ask` 有两种 kind(2026-10-25 补 permission 的落地口径)**:
|
|
49
|
+
|
|
50
|
+
| kind | 含义 | `ask` 里带什么 | 客户端动作 |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `question` | 模型提问(claude 的 AskUserQuestion) | `questions[]`(题面 + options) | 画提问卡;答案以 `【用户选择】…` 作为下一轮 user 消息(`--append` / `op:"answer"`)送回 |
|
|
53
|
+
| `permission` | **待授权**:计划审批 `ExitPlanMode`、manual 档的工具授权 | `tool_name` / `request_id` / `tool_input`,**没有 questions** | 画「允许 / 拒绝」裁决卡;点选后走 `op:"ask_answer"`(见下) |
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"type":"ask","text":"需要授权:ExitPlanMode(等待用户选择)","ask":{
|
|
57
|
+
"engine":"codebuddy","kind":"permission","source":"control_request",
|
|
58
|
+
"tool_name":"ExitPlanMode","tool_use_id":"call_…","request_id":"perm_1792896027947_1",
|
|
59
|
+
"tool_input":{"allowedPrompts":[…]}}}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
⚠️ `permission` **不能**用 `op:"answer"` 作答:引擎这时正阻塞在子进程 stdin 的
|
|
63
|
+
`control_response` 上,user 消息要等这一轮结束才会被读 —— 发过去只会「答了也继续转圈」。
|
|
64
|
+
必须用 `POST /desk/ask/{id}/control`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{"op":"ask_answer","request_id":"perm_1792896027947_1","allowed":true,"message":"","tool_name":"ExitPlanMode"}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
core 把它编码成引擎自己的控制应答(codebuddy:`{"allowed":true}` / `{"allowed":false,"reason":"…"}`,
|
|
71
|
+
实测口径见 `internal/agent/ask.go`)写进子进程 stdin;成功回 `ask_answered`,
|
|
72
|
+
写不回去(本轮已结束 / 引擎不支持)回 `control_error` —— 客户端要把失败如实告诉用户,
|
|
73
|
+
不要假装批准成功。旧版 core 没有这条 op(回 `control_error`「未知 op」),客户端据此回落文本追问。
|
|
45
74
|
|
|
46
75
|
**`model` 一律是完整 id,可能带 `custom-local:` 前缀**(codebuddy / codebuddy-ai 的
|
|
47
76
|
自定义端点模型,见 `docs/engines.md`)。这个值要原样用于 `POST /desk/ask` 的 `model`
|
|
48
77
|
与 `-m`;**展示时必须剥掉前缀**(`custom-local:` 只是 App / CLI 内部的端点区分约定,
|
|
49
|
-
对用户是噪点)。`--engines` 的 `models[]` 与 `
|
|
78
|
+
对用户是噪点)。`--engines` 的 `models[]`、`model_credits` 与 `model_vendors` 的键
|
|
79
|
+
**已经剥过**
|
|
50
80
|
(`agent.StripCustomLocalPrefix`),事件与 `/desk/sessions` 里的 `model` **没有** ——
|
|
51
81
|
客户端渲染前自己剥。
|
|
52
82
|
|
|
83
|
+
`--engines` 的 `model_vendors`(2026-10-24,只增字段,不升 `contractVersion`)是
|
|
84
|
+
`{模型 id → 供应商展示名}`,只给模型选择器的**第二行**用(与 dsh 的 `route/model`
|
|
85
|
+
路由行同一位置、同一视觉),不参与 `-m` / 落库。目前只有 codebuddy / codebuddy-ai
|
|
86
|
+
给出(取自它自己 `models.json` 的 `vendor`,空值 / `Custom` 已被 core 过滤);
|
|
87
|
+
字段缺失 = 不画第二行。
|
|
88
|
+
|
|
53
89
|
两条不变量(有测试钉住):**每轮一定以 `result` 或 `error` 收尾**;
|
|
54
90
|
`--events` 与 `--stream` 的字段集只差 `v` / `seq`。
|
|
55
91
|
|
|
@@ -165,7 +201,7 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
165
201
|
|
|
166
202
|
| 集合 | 位置 | 回答的问题 |
|
|
167
203
|
| --- | --- | --- |
|
|
168
|
-
| `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control` / `feature.session.hosted` / `feature.session.team` / `feature.requirements`) |
|
|
204
|
+
| `features[]` | `--contract` 顶层 | 这台 core 支不支持某条**通道**(`feature.session.events` / `feature.session.control` / `feature.session.hosted` / `feature.session.team` / `feature.requirements` / `feature.requirement.shots`) |
|
|
169
205
|
| `engines[].capabilities[]` | 每个引擎 | 这个**引擎**能做什么(`session.stream` / `attachment.native` / `engine.install` …) |
|
|
170
206
|
|
|
171
207
|
静态与运行态严格分开:`capabilities` / `features` 是静态声明,`ok` / `streaming` / `models` /
|
|
@@ -189,6 +225,12 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
189
225
|
分支给出「当前 core 不支持需求聚合」的明确文案,比一个消失的入口更容易理解。
|
|
190
226
|
其他客户端可自行取舍,但**一旦显示就必须能说清为什么不工作**(不要把 501 吞成空列表)。
|
|
191
227
|
|
|
228
|
+
`feature.requirement.shots`(2026-10-25 只增量,**不**进 `requiredFeatures`):这台 core 认
|
|
229
|
+
`--requirement-shot`(见 §3.4.1.1)。它与 `feature.requirements` **分开** —— core 可能支持
|
|
230
|
+
需求聚合但还不支持截图。插件对 `POST /desk/requirements/shots` 先查这个位,缺了回
|
|
231
|
+
**501 + 中文原因**。界面侧:功能点行的**相机徽标**只在这个位存在时才出现
|
|
232
|
+
(不给「点了必然 501」的死入口),但「需求」页签本身**照常显示**(理由同上)。
|
|
233
|
+
|
|
192
234
|
**移动端底栏(2026-10-22 定稿)**:`任务 · 需求 ·「+」· 发现 · 我的`。
|
|
193
235
|
|
|
194
236
|
- 「定时任务」页签**已删除**。它的功能没删:会话菜单里的「定时任务」仍然能唤起(现在给一句
|
|
@@ -224,7 +266,7 @@ codex / dsh(+sdk) / llm / openclaw 暂不带 `item_id`,
|
|
|
224
266
|
```bash
|
|
225
267
|
magic-agent -e claude --stream --events --team \
|
|
226
268
|
--team-route image=llm/hunyuan-image-3.0 --team-route video=arkclaw \
|
|
227
|
-
--team-parallel 4 --team-task-timeout 5m
|
|
269
|
+
--team-parallel 4 --team-task-timeout 5m \
|
|
228
270
|
-p '做一套水世界的小游戏,附一张主视觉图'
|
|
229
271
|
```
|
|
230
272
|
|
|
@@ -232,6 +274,8 @@ magic-agent -e claude --stream --events --team \
|
|
|
232
274
|
| --- | --- |
|
|
233
275
|
| `--team` | 开启。**必须配合 `--stream`**(过程靠事件流呈现,一次性 envelope 装不下)|
|
|
234
276
|
| `--team-route <key>=<engine>[/<model>]` | 给计划阶段、某类子任务或某个角色指定引擎/模型,可重复。`key ∈ plan\|text\|code\|image\|video\|game`(`plan` = 计划阶段,不是可执行子任务的类别),或 `role:<name>`(裸角色名也认:writer / researcher / reviewer / coder / designer / director / builder,与五类子任务一一覆盖;`video` / `code` 仍是类别键,不会被当成角色);**不给就是跟随主引擎**(六类一致)。优先级:任务写死 > 角色 > 类别 > 主引擎 |
|
|
277
|
+
| `--team-fallback <key>=<engine>[/<model>]` | 给计划阶段 / 某类子任务 / 某个角色指定**备用**引擎(2026-10-24)。键与语法同 `--team-route`,可重复;只配它不配 `--team-route` = 主引擎跟随主引擎 + 这一路的备。主引擎失败且**零产出**时降级一次,计划阶段也不例外(`plan` 行)|
|
|
278
|
+
| `--fallback-engine` / `--fallback-model` | 主引擎(`-e` / `-m`)的备用。影响主会话与所有「跟随主引擎」的类别;某一类/角色另有 `--team-fallback` 时以那一路为准 |
|
|
235
279
|
| `--team-parallel` | 同时最多跑几个子任务(默认 4)|
|
|
236
280
|
| `--team-task-timeout` | 单个子任务超时(默认 = 总超时/2,夹在 1m..20m)|
|
|
237
281
|
| `--team-plan-only` | 只出计划与任务书,不执行(先看方案)|
|
|
@@ -239,7 +283,7 @@ magic-agent -e claude --stream --events --team \
|
|
|
239
283
|
| `--team-split` | 拆解粒度:`coarse`(最多 3 个)\| `standard`(6,默认)\| `fine`(10)。只改「最多拆几个」,其余拆解规则不变 |
|
|
240
284
|
| `--team-split-code` | 简单代码也拆成独立子任务(**默认关**:单文件脚本 / 几行改动由主引擎在最终回复里直接给出)|
|
|
241
285
|
| `--team-rule role:<name>=<规则>` | 改写某个角色「这类任务适合什么」,替掉角色表里的内置职责(进计划提示词;不改执行者的系统提示词)。可重复;规则折叠成一行、上限 500 字;未知角色 / 空规则 → 退出码 2 |
|
|
242
|
-
| `--team-permission` |
|
|
286
|
+
| `--team-permission` | 子任务的权限上限。**不给 = 继承主会话 `--permission`**(会话设成完全访问,子任务也是完全访问);显式给档位可把子任务压得更严(如 `accept-edits`)。⚠️ 团队模式没有作答通道:`manual` / `accept-edits` 下子任务一旦要跑命令就会卡在「等待授权」直到单任务超时,**不要**用它给命令类任务设这两档 |
|
|
243
287
|
| `--team-run-dir` | 覆盖运行目录的父目录 |
|
|
244
288
|
|
|
245
289
|
**事件形状(客户端要认的只有一条)**:主会话里是**一张任务列表卡**,每个子任务是一条**普通会话**。
|
|
@@ -342,6 +386,34 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
342
386
|
用户要看的是「真要跑起来,每个子任务会收到什么」;`tasks[]` 在 plan-only 下为空数组
|
|
343
387
|
(没有执行),`plan_path` / `run_dir` 照常给。
|
|
344
388
|
|
|
389
|
+
### 1.2 主备降级的口径(2026-10-26,只增 kind,不升 `contractVersion`)
|
|
390
|
+
|
|
391
|
+
用户在一次设置里配「主 + 备」(`fallbackEngine` / `fallbackModel`)。主的那条不行时 core
|
|
392
|
+
自动换备用再跑一次,口径写死在 `internal/agent/fallback.go`:
|
|
393
|
+
|
|
394
|
+
| 情形 | 行为 |
|
|
395
|
+
|---|---|
|
|
396
|
+
| **同引擎换模型**(`fallbackEngine == engine`,`fallbackModel` 不同) | **降级**,按备用模型重跑(手机最常见的配法:dsh 主 modelverse、备 volcengine) |
|
|
397
|
+
| **换引擎** | 降级;备用模型**不往下传**(各引擎的模型清单互不相通),备用用它自己的默认模型 |
|
|
398
|
+
| 判定「主引擎失败」 | 返回错误,**或**正常结束但只有过程(思考 / 工具 / 计划)没有正文 / 图片 / 工具结果 |
|
|
399
|
+
| 已经出了正文 / 图片 / 工具结果 | **不降级**(否则用户会看到两段互相矛盾的答案),保留原始错误 |
|
|
400
|
+
| 备用就是主引擎同模型 / 备用引擎不存在 / 备用不支持流式 | 不降级,失败时补一条 `notice`:「本轮没有降级:<原因>」 |
|
|
401
|
+
| `ctx` 取消 / 超时、用户打断 | 不降级 |
|
|
402
|
+
|
|
403
|
+
降级发生时,事件流里**先出这一条**,再跟备用引擎的事件:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{"type":"fallback","text":"「dsh」执行失败(…),已切到备用模型「dsh」的 volcengine/x 重试","fallback":{
|
|
407
|
+
"from_engine":"dsh","from_model":"modelverse/x","to_engine":"dsh","to_model":"volcengine/x",
|
|
408
|
+
"switched_engine":false,"reason":"…"}}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
- `switched_engine:false` = 同引擎换模型;`true` = 换了引擎。
|
|
412
|
+
- 主引擎残留的 `turn_end` / `turn_failed` **不会**再出现在流里(备用紧接着还要跑)。
|
|
413
|
+
- 团队模式:计划阶段同样降级(降级帧 text 前缀「团队模式:」),子任务结果里的
|
|
414
|
+
`engine` / `model` 是**实际作答**的那一路。
|
|
415
|
+
- 没降级的原因只在**失败**时给(`notice`),成功轮不打扰。
|
|
416
|
+
|
|
345
417
|
## 二、插件承诺的面(`magic-client`)
|
|
346
418
|
|
|
347
419
|
| 面 | 路径 | 状态 |
|
|
@@ -352,22 +424,36 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
352
424
|
| 会话登记表 | `GET /desk/sessions`(core 的 `--sessions`,跨进程持久;**只回本客户端发起的会话**) | 已实现 |
|
|
353
425
|
| 项目业务接口 | `ANY /api/*` → 原样转发到 profile 的 `backend.url` | 已实现 |
|
|
354
426
|
| 发起一轮 | `POST /desk/ask`(SSE 中继 core 的 NDJSON,前置 `event: open`,收尾 `event: end`)。**服务端会先入队**:同 workspace 串行 + `request_id` 幂等;真在等时先推 `event: queued`(`{job, state, position}`,客户端可忽略),准入后照旧 `open` | 已实现 |
|
|
355
|
-
| 作业队列(多客户端) | `POST /desk/jobs`:提交(不挂长连接,2xx 即可);`GET /desk/jobs[?workspace=][&limit=]`:队列快照(含他人任务与 `position`);`GET /desk/jobs/{id}`;`GET /desk/jobs/{id}/events?after=<seq>`:按 seq 回放本轮事件(形状同 `ask/{id}/events` + `job` 字段);`POST /desk/jobs/{id}/cancel`;`POST /desk/jobs/cancel {request_id}`(还没拿到 job id 时取消);`GET /desk/jobs/events?[workspace=]`:队列变更 SSE(每 2s 一帧 `event: jobs`)。持久化在 `<profile data dir>/jobs/`,服务重启后 queued 保留、running
|
|
356
|
-
| 控制与回审批 | `POST /desk/ask/{id}/control`(与 core 的 `--control` 同协议,插件不解释 op) | 已实现 |
|
|
427
|
+
| 作业队列(多客户端) | `POST /desk/jobs`:提交(不挂长连接,2xx 即可);`GET /desk/jobs[?workspace=][&limit=]`:队列快照(含他人任务与 `position`);`GET /desk/jobs/{id}`;`GET /desk/jobs/{id}/events?after=<seq>`:按 seq 回放本轮事件(形状同 `ask/{id}/events` + `job` 字段);`POST /desk/jobs/{id}/cancel`;`POST /desk/jobs/cancel {request_id}`(还没拿到 job id 时取消);`POST /desk/jobs/{id}/update {serial}` 与 `POST /desk/jobs/update {request_id, serial}`(2026-10-24,只增:改**排队中**作业的串行/并行 —— 客户端切任务模式时同步旧排队条目,串行↔并行马上生效;已开跑的作业回 `updated:false` 不打断);`GET /desk/jobs/events?[workspace=]`:队列变更 SSE(每 2s 一帧 `event: jobs`)。持久化在 `<profile data dir>/jobs/`,服务重启后 queued 保留、running 残留标失败。作业记录里 `fallback_engine` / `fallback_model` 是**配置**;`fallback_used` / `fallback_engine_used` / `fallback_model_used`(2026-10-26 只增)是**本轮真的由备用接上了**(服务端扫 `type:"fallback"` 帧回填),没降级就不出现 —— 排查「主备生效没有」看这三个 | 已实现 |
|
|
428
|
+
| 控制与回审批 | `POST /desk/ask/{id}/control`(与 core 的 `--control` 同协议,插件不解释 op;`ping` / `interrupt` / `stop` / `answer` / `ask_answer`,后两者见 1.1) | 已实现 |
|
|
357
429
|
| 断线续传 | `GET /desk/ask/{id}/events?after=<seq>`(按**本轮流 seq** 增量补拉;带 `done` / `code` / `stderr` / `snapshotRequired`;`after=0` 时首条是本轮**合成的用户提问**,见 2.4) | 已实现 |
|
|
358
430
|
| 运行中列表 | `GET /desk/asks`(**本进程**正在跑的轮次,每项 `{id, engine, started, session?, run?}`;与 sessions 的区别见下) | 已实现 |
|
|
359
431
|
| 运行中会话尾随 | `GET /desk/session/{id}/follow?[after=<seq>][&lean=1]`(SSE:`open` / 增量事件 / `snapshot` / `end`;读磁盘日志,**跨进程**,客户端接续运行中会话的**首选**通道;见 3.4) | 已实现 |
|
|
360
432
|
| 本体文件出流 | `GET /desk/media?path=<绝对路径>[&w=<px>]`(`?token=` 供 `<img>` / `<video>` 直接引用;**只发 image / video / audio**,`Range` 可拖动)。带 `w` 时按宽缩放并重编码(`64..4096`,失败/更大则回退原图,结果内存缓存);移动端传输层自动追加 `w=480`(缩略图;点开看大图时不带 `w`,取原文件) | 已实现 |
|
|
361
433
|
| 上传文件回读 | `GET /desk/uploads/{name}[?w=<px>]`(同上 `w` 语义) | 已实现 |
|
|
362
434
|
| 产物存在性探测 | `POST /desk/path-probe`:`{base, paths[]}` → `{ok, items:[{path, abs, exists, size}]}`;相对路径按 `base` 拼、`~/` 展开、`file://` 剥前缀、只认 regular 文件、最多 50 条。对话内核的「本轮产物」靠它把正文里的路径变成卡片(宿主经 `window.desk.pathProbe` 供进来) | 已实现 |
|
|
363
|
-
| 下一轮快捷回复 | `POST /desk/suggest`:`{user, reply, engine?, model?}` → `{suggestions:[string], engine, model, elapsedMs}`。服务端 exec core 的 `--suggest-followups`(**旁路调用,不续接会话**);`suggestions` 可以为空数组(= 这轮没有下一步,客户端不画按钮),失败如实 4xx/5xx。客户端**只在成功收尾后**调用,点击即发。并发上限 2(超出 429),超时 25s | 已实现 |
|
|
435
|
+
| 下一轮快捷回复 | `POST /desk/suggest`:`{user, reply, engine?, model?, fallbackEngine?, fallbackModel?}` → `{suggestions:[string], engine, model, elapsedMs}`。服务端 exec core 的 `--suggest-followups`(**旁路调用,不续接会话**);`suggestions` 可以为空数组(= 这轮没有下一步,客户端不画按钮),失败如实 4xx/5xx。客户端**只在成功收尾后**调用,点击即发。并发上限 2(超出 429),超时 25s。`fallbackEngine`(2026-10-24 只增字段)= 主建议引擎失败后先试它,再落到 core 内置的 llm 兜底 | 已实现 |
|
|
364
436
|
| 产物文本回读 | `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 / 文本 | 已实现 |
|
|
365
437
|
| 插件目录 | `GET /desk/plugins` → `{plugins:[{id, name, cli, tagline, description, requirements?, homepage?, installed, version?, bin?, sourcePath?, sourceExists, install, upgrade, examples[], usePrompt, note?}]}`。**这不是引擎**(core 的 `--engines` 里没有它们):它是「发现 → 插件」里那两个工程 CLI(`magic-shorts` / `magic-video`)的介绍与安装状态。`installed` / `version` 由服务端探测(`exec.LookPath` + 可执行文件所属 npm 包的 `package.json`,不跑 `--version`),客户端只显示不推断 | 已实现 |
|
|
366
438
|
| 插件安装/升级 | `POST /desk/plugins/{id}/{install\|upgrade}` → SSE:`event: open`(`{id, action, commands[]}`)→ 每条命令 `event: step` → 输出逐行 `event: line` → `event: end`(`{ok, code, error?, plugin}`)。**命令只来自服务端那张固定表**(argv,不过 shell),客户端只传 id 与动作,未知 id/动作一律 404 —— 这条接口不接受客户端传命令 | 已实现 |
|
|
439
|
+
| 知识库目录 | `GET /desk/knowledge` → `{knowledge:[{id, name, cli, tagline, description, requirements?, homepage?, installed, version?, bin?, binSource?, latestVersion?, upgradeAvailable, pythonPath?, pythonVersion?, install, upgrade, examples[], usePrompt, note?}]}`。**这不是引擎**(core 的 `--engines` 里没有它):它是「发现 → 知识库」里的 OpenViking(面向 Agent 的上下文数据库)。`installed` / `version` / `latestVersion` / `upgradeAvailable` **全部由服务端探测**,客户端只显示不推断。探测顺序:环境变量 `MAGIC_CLIENT_OPENVIKING_BIN` → 自建 venv(家目录 `.openviking-venv`)→ PATH(命令名 `ov` 与 `openviking` 都认);**venv 排在 PATH 前** —— 一键安装装进 venv 后命令不在 PATH 上,只查 PATH 会把「刚装好」误报成「未安装」。`version` 分四层兜底(`--version` → dist-info 目录名 → `importlib.metadata` → `pip show`),读不到就留空 + `note` 说明,**绝不编**。`latestVersion` 走 PyPI JSON 实时查(**代码里不写死版本号**),查不到就留空 + `note`,HTTP 仍 200 | 已实现 |
|
|
440
|
+
| 知识库安装/升级 | `POST /desk/knowledge/{id}/{install\|upgrade}` → SSE:`event: open`(`{id, action, commands[]}`)→ 每条命令 `event: step` → 输出逐行 `event: line` → `event: end`(`{ok, code, error?, knowledge}`),形状与插件那条一致。命令由服务端按本机 python(需 ≥3.10)**动态构造**(建 venv → `pip install [--upgrade] openviking` → `pip show`),argv 数组不过 shell,客户端只传 id 与动作,未知 id/动作一律 404 —— 这条接口不接受客户端传命令。没有可用 python 时回 400(装不了,不给一条跑不通的命令);已有安装在进行时回 409 | 已实现 |
|
|
441
|
+
| 脚本清单 | `GET /desk/scripts` → `{scripts:[{id, name, path, args[], cwd?, interpreter?, env[]?, note?, state:{state, pid?, exitCode, startedAt?, endedAt?, logSeq, error?}}]}`。`state` ∈ `stopped`/`running`/`done`/`failed`(**服务端给的,客户端不推断**)。脚本是**用户自己登记**的(路径 + 参数),不是服务端固定表、也不是目录扫描 | 已实现 |
|
|
442
|
+
| 脚本增删改 | `POST /desk/scripts`(`{name?, path, args[], cwd?, interpreter?, env[]?, note?}`)→ 带 id 的条目;`PUT /desk/scripts/{id}`(改定义,**不动运行态**);`DELETE /desk/scripts/{id}`(运行中 → 409,先停再删;**配置与落盘日志一起删**)。**路径必须是绝对路径、必须是存在的文件、扩展名在白名单内**(`.sh .bash .cmd .bat .ps1 .py .js .mjs`,**不含 `.exe`**)—— 白名单比黑名单稳;命令**不过 shell**(`.cmd/.bat/.ps1` 由服务端包成 PowerShell 单引号字面量调用)。**这条接口不接受客户端传任意命令行** | 已实现 |
|
|
443
|
+
| 脚本解释器与环境变量 | `interpreter`(可空)= 解释器绝对路径,填了就跑 `<解释器> <脚本路径> [参数…]`,不填就直接跑脚本文件本身。**解释器不受上面的扩展名白名单约束**(白名单里没有 `.exe`,而 Python/Node 脚本在 Windows 上没法直接 exec),但**必须是绝对路径、必须是存在的文件、不能是目录**。`env`(可空)= 追加的环境变量数组,逐条 `"KEY=VALUE"`(KEY 不能含空白,条数 ≤ 32),会覆盖同名的父进程环境变量。典型用途:Python 脚本要 `PYTHONUNBUFFERED=1`(否则 stdout 块缓冲,实时日志变成「跑完一大半才刷出来」)+ `PYTHONIOENCODING=utf-8`(否则中文日志乱码) | 已实现 |
|
|
444
|
+
| 脚本启停 | `POST /desk/scripts/{id}/start` → 200(条目 + 运行态,**不挂长连接**);`POST /desk/scripts/{id}/stop` → 200。启动 = spawn 一个**独立进程组**的后台进程(停止时整组杀,不留孤儿 worker);状态机 `stopped → running → (done \| failed \| stopped)`。运行中重复 start → 409。**用户主动停的记 `stopped`**(不是 `failed`) | 已实现 |
|
|
445
|
+
| 脚本日志 | `GET /desk/scripts/{id}/log?after=<seq>` → SSE:`event: line`(`{seq, text}`)逐行 → `event: state`(运行态)收尾。`after=<seq>` 只补之后的行(断线重连用;**序号跨重启单调递增**,否则重连会一行都收不到)。服务端内存留最近 500 行(环),完整副本落 `~/.magic-agent/scripts/logs/<id>.log`(**追加**)。脚本在**这台桌面**上跑,不是手机 | 已实现 |
|
|
367
446
|
| 移动端 APK 分发 | `GET /desk/app` → `{version, size, url}`(`version` 取自随包的 `npm/apk/version.json`,与 npm 包同一个版本号;没带 APK 时 `version` 为空串、`url` 为空);`GET /desk/app.apk[?token=]` → 安装包本体(`application/vnd.android.package-archive`)。移动端「我的」→「版本」点一下先读前者,发现更高版本后由系统浏览器/安装器下载后者 | 已实现 |
|
|
368
447
|
| 移动端环境配置 | `GET /desk/prefs` → 这台桌面的移动端配置 JSON(没存过是 `{}`);`PUT /desk/prefs` 整份覆盖(≤256KB,原子写)。字段由移动端定义、桌面端原样存(`~/.magic-agent/mobile-prefs.json`),当前是「默认引擎/模型 + 团队配置 + 空间选择 + 置顶」。**多客户端连同一台桌面共用这一份** —— 不再是各存各的浏览器 localStorage | 已实现 |
|
|
448
|
+
| 重启后台服务 | `POST /desk/restart` → `{ok:true, restarting:true, pid}`。桌面端**先回响应**,再由一个脱离终端的 `magic-client restart --delay 1s` 助手停旧实例、按 `client.json` 里的参数拉起新实例(端口 / 令牌 / 界面目录 / profile 沿用,手机里的地址不用改)。⚠️ 会话是它起的子进程 → 正在跑的会话会被中断(状态落 `interrupted`/`gone`)。只有**后台实例**(`magic-client start` 起的)能自重启:状态文件缺失或 `pid` 不是本进程时回 409,前台 `serve` 请到桌面执行 `magic-client restart` | 已实现 |
|
|
449
|
+
| 内置浏览器中转 | `GET/POST /b/<scheme>/<host><path>[?magicToken=]`:服务端代取页面并就地改写(链接/资源指回中转、剥 `X-Frame-Options`/CSP、注入 `<base>` 与 relay 脚本),iframe 的流量走**桌面端网络**。鉴权自管(不套 guard):**改写出的每条 URL 都带 `?magicToken=`**(转发上游前由服务端剥掉,不泄漏给目标站);另在首帧下发 cookie `magic_browse` 作辅助凭据 —— 跨站 iframe(APK 壳 origin 与桌面端 LAN 不同源)下 SameSite=Lax cookie 不随子资源请求发送,不能只靠它。`GET /b/relay.js` 是注入脚本本体(见下一行)。⚠️ 移动端 UI **默认直连**(2026-10-25 起):不开浏览器里的「桌面端中转」开关时 iframe 直接加载目标站、不走本中转;开关打开后才走 `/b/`(桌面独有站点 / 拒绝内嵌的站点 / 需要 relay 注入时用) | 已实现 |
|
|
450
|
+
| 浏览器输入中继 | `WS /browse-input?role=viewer\|driver[&token=]`(鉴权同 `/b/`;`role` 缺省按 driver)。页面内 relay 脚本连 `viewer`(同一时刻至多一个,后连顶前连),手机 UI 连 `driver`(可多个)。JSON 文本帧:viewer→server 发 `hello`/`state`(`{title,url,w,h}`,页面 CSS 像素)/`ack`(`{seq,ok,reason?}`);server→viewer 发 `input`(`{seq,action,x?,y?,dx?,dy?,text?,key?}`,`action` ∈ `move\|down\|up\|click\|dblclick\|scroll\|type\|key`)与 `ping`;driver→server 发 `input`(server 重编 seq 后转发,无 viewer 时回 `ack{ok:false}`)与 `ping`(转发给 viewer 请求一帧 `state`,无 viewer 回 `ack{ok:false}`);server→driver 发 `state`/`ack`/`viewer`(`{online}` 上下线通知)。坐标 = 被代理页面自身 CSS 像素。**2026-10-25 起 relay 只承担「远程触控」**:`shot` 动作已退役(页面内 html2canvas 有跨域污染 / iframe 抓不到 / 慢页超时三个硬伤),截图与程序化驱动走 agent-browser(见下一行)。已知边界:移动端 UI 默认直连(不走 `/b/`),只有经中转加载的页面才注入 relay 脚本、中继才可用;合成事件 `isTrusted=false`,个别反自动化站点会忽略 | 已实现 |
|
|
451
|
+
| 浏览器 MCP(agent-browser,2026-10-25) | 驱动/截图面基于 **agent-browser 插件**(vercel-labs,Rust 原生 CLI,真实 Chromium/CDP,默认 headless,daemon 模型:首次 `open` 自动拉起、闲置约 1h 自动退出)。CLI:`magic-client browse open <url> \| state \| click <x> <y> \| move <x> <y> \| dblclick <x> <y> \| scroll <dx> <dy> \| type <文本> \| key <键名> \| shot [--out 文件] [--full-page] \| eval <js> \| pdf <文件> \| snap [-i]`。会话固定 `--session magic-client`(与人类/其它工具的 default 会话隔离,登录态可跨动作复用)。探测链:环境变量 `MAGIC_AGENT_AGENT_BROWSER_BIN` > npm 全局前缀(`<prefix>/node_modules/agent-browser/bin/agent-browser-<goos>-<goarch>[.exe]`,**绝对路径 spawn 原生 exe,零 node 依赖**)> PATH;都没有时 CLI 退出码 1 + 安装提示、HTTP 端点回 **501 + 中文原因**(`npm install -g agent-browser && agent-browser install`)。单次调用统一 `--json`(`{"success":true,"data":…}`),超时 60s。⚠️ 与 `/b/` 中转是**两个独立浏览器上下文**:手机 iframe 看的页面走中转 + relay 远程触控;agent-browser 截的是它在服务端自己打开的页面(独立 profile,不共享中转 cookie jar,需登录的页面截出来可能是登录页) | 已实现 |
|
|
452
|
+
| 需求截图服务端截取 | `POST /desk/requirements/shot-remote`(body = `{"url":"http(s)://…","full_page":true}`):服务端 agent-browser 打开 url 截**全页图**,落 `/desk/uploads/<纳秒>-reqshot.png`(`os.TempDir()/magic-client-uploads`,与 `/desk/upload` 同一个家),回 `{ok:true, data:"/desk/uploads/…png", size}` —— `data` 直接喂标注浮层的 `<img>`(带 `?token=` 回读)。能力位 `feature.requirement.shots` 缺失回 501;服务端没装 agent-browser 回 501 + 安装提示;url 非 http/https 回 400;agent-browser 打开/截图失败回 502 + 原因 | 已实现 |
|
|
453
|
+
| 需求截图写入 | `POST /desk/requirements/shots?ws=&module=&name=&caption=`(body = **PNG 原始字节**,不是 multipart)。能力位 `feature.requirement.shots` 缺失回 501;`name` 必填;空 body 回 400;超 8MB 回 **413**。core 落盘并把一行追加进该功能点文档的「页面完成情况」段(见 §3.4.1.1) | 已实现 |
|
|
454
|
+
| 需求关系图谱 | `GET /desk/requirements/graph?ws=<ws_id\|目录>`(留空 = 全部)。能力位同 `feature.requirements`(缺了回 501)。core `--requirements-graph` 纯读转发 → `{ok, root, workspaces:[{id, name, dir?, nodes, edges}]}`(形状见 §3.4.1「会话↔需求关系」) | 已实现 |
|
|
369
455
|
| 静态资源 | `GET /`(`--ui-dir` + profile 的 `ui.entry`) | 已实现 |
|
|
370
|
-
| 鉴权 | `X-Magic-Token` 头或 `?token
|
|
456
|
+
| 鉴权 | `X-Magic-Token` 头或 `?token=`;默认绑 `0.0.0.0:9100` 且令牌强制 ≥24 字符;`--local` 时只绑 `127.0.0.1`(见 2.4) | 已实现 |
|
|
371
457
|
|
|
372
458
|
### 2.1 扩展接口(业务项目对着写)
|
|
373
459
|
|
|
@@ -421,17 +507,16 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
421
507
|
| 未配置后端 | `501` + 一条点名 `backend.url` 的说明,不静默 404 |
|
|
422
508
|
| 地址非法 | **启动即失败**(比运行时 502 好查) |
|
|
423
509
|
|
|
424
|
-
### 2.5
|
|
510
|
+
### 2.5 局域网接入(默认开启,`--local` 关闭)
|
|
425
511
|
|
|
426
|
-
|
|
427
|
-
手机 / 平板要连就得绑局域网,而这一开,**令牌就是唯一的门锁** —— 所以两者是配套的:
|
|
512
|
+
默认绑 `0.0.0.0:9100`(手机 / 平板开箱可连);这一开,**令牌就是唯一的门锁** —— 所以两者是配套的:
|
|
428
513
|
|
|
429
514
|
| 行为 | 说明 |
|
|
430
515
|
| --- | --- |
|
|
431
|
-
| `--lan
|
|
516
|
+
| 默认(或 `--lan`) | 绑 `0.0.0.0:9100`,启动时把本机**所有**可用局域网地址连令牌一起打印(一台机器常有多个网卡) |
|
|
432
517
|
| 令牌下限 | 24 字符;弱令牌**拒绝启动**(自动生成的默认令牌是 32 位十六进制,够用) |
|
|
433
518
|
| 无令牌访问 | `401` —— 不因为是内网就放行 |
|
|
434
|
-
|
|
|
519
|
+
| `--local` | 只绑 `127.0.0.1`,局域网地址**连不上** |
|
|
435
520
|
| 地址的挑选与顺序 | 虚拟网卡(容器 / 虚拟交换机 / 隧道)的地址**不报**;公网段地址在还有私有地址时也不报;剩下的按「出站主地址(默认路由那张网卡)→ 其余」排。**第一个 = 手机最可能连上的那个** —— 移动端只认 `[0]` |
|
|
436
521
|
|
|
437
522
|
⚠️ **地址顺序是契约的一部分(2026-10-23 修正)**:以前直接按网卡枚举顺序返回,
|
|
@@ -449,21 +534,22 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
449
534
|
| `engine` | 必填;profile 的 `engines` 白名单之外的会被 403 |
|
|
450
535
|
| `prompt` | 必填 |
|
|
451
536
|
| `model` / `workspace` | 可选,透传给 core 的 `-m` / `-w` |
|
|
537
|
+
| `fallbackEngine` / `fallbackModel` | 可选(2026-10-24,只增字段),主引擎的**备用**。口径见 1.2:主引擎失败或**只有过程没有正文**时降级一次(**同引擎换模型也算**),发一条 `fallback` 帧;已经出了正文 / 图片 / 工具结果则不降级。`fallbackEngine` 同样要过 profile 白名单(否则 403)。空 = 不降级(老客户端行为不变)|
|
|
452
538
|
| `permission` | 可选,四档授权模型(`manual` \| `accept-edits` \| `auto` \| `full`),透传给 core 的 `--permission`。**空 = 不传**,由 core 用它自己的默认档(`agent.DefaultPermissionTier`)—— 刻意没有默认预设,见下 |
|
|
453
539
|
| `reasoning` | 可选,思考档位(`off` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` \| `max`),透传给 core 的 `--reasoning`。**空 = 不传**,core 自己的默认档是 **`max`**(开着)—— 所以「关闭思考」必须**显式传 `off`**,省略等于开着 + 最高档 |
|
|
454
540
|
| `session` | 可选,续接一个已有会话(传 `/desk/sessions` 里的 id)→ core 的 `--session` |
|
|
455
541
|
| `idle` | 可选,常驻会话空闲收工时长;默认 `0` = 本轮结束就收工 |
|
|
456
542
|
| `team` | 可选,团队模式(见下)。**nil / 缺省 = 关闭**(默认关)|
|
|
457
543
|
| `request_id` | 可选(2026-10-23,只增字段),客户端生成的**幂等键**。同一个 id 重复提交(断线重连 / App 重启 / 两个标签页)只执行一次:服务端返回原作业并接回去。空 = 旧客户端,仍参与 workspace 串行,但没有幂等保护 |
|
|
458
|
-
| `serial` | 可选(2026-10-23,只增字段),默认 `true`:参与「同一 workspace 单槽」串行。客户端「并行模式」显式传 `false`(服务端仍套全局并发上限,`MAGIC_CLIENT_MAX_RUNNING`,默认 4
|
|
544
|
+
| `serial` | 可选(2026-10-23,只增字段),默认 `true`:参与「同一 workspace 单槽」串行。客户端「并行模式」显式传 `false`(服务端仍套全局并发上限,`MAGIC_CLIENT_MAX_RUNNING`,默认 4)。**并行作业不注入 core 工作空间锁**(2026-10-24 起,锁只对串行注入)—— 否则同空间「并行」会在 core Begin 处被锁重新排成队;排队中的作业可用 `POST /desk/jobs/update` 改这个标志,模式切换即时生效 |
|
|
459
545
|
|
|
460
546
|
`team`(2026-10-21 只增字段,不升 `contractVersion`):
|
|
461
547
|
|
|
462
548
|
```json
|
|
463
549
|
"team": {
|
|
464
|
-
"routes": { "role:coder": {"engine":"claude"},
|
|
465
|
-
"image": {"engine":"llm","model":"hunyuan-image-3.0"},
|
|
466
|
-
"video": {"engine":"arkclaw"} },
|
|
550
|
+
"routes": { "role:coder": {"engine":"claude","fallbackEngine":"codebuddy"},
|
|
551
|
+
"image": {"engine":"llm","model":"hunyuan-image-3.0","fallbackEngine":"arkclaw"},
|
|
552
|
+
"video": {"engine":"arkclaw","fallbackEngine":"codebuddy"} },
|
|
467
553
|
"rules": { "role:coder": "只拆多文件、要构建或测试的改动" },
|
|
468
554
|
"parallel": 4,
|
|
469
555
|
"planOnly": false,
|
|
@@ -476,7 +562,7 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
476
562
|
|
|
477
563
|
| 子字段 | 说明 |
|
|
478
564
|
| --- | --- |
|
|
479
|
-
| `routes` | `kind → {engine, model?}`,`kind ∈ plan\|text\|code\|image\|video\|game`,或角色键 `role:<name>`(2026-10-22 只增字段,不升 `contractVersion`)。**类别名 / 角色名非法、只给 model 不给 engine
|
|
565
|
+
| `routes` | `kind → {engine, model?, fallbackEngine?, fallbackModel?}`,`kind ∈ plan\|text\|code\|image\|video\|game`,或角色键 `role:<name>`(2026-10-22 只增字段,不升 `contractVersion`)。**类别名 / 角色名非法、只给 model 不给 engine、引擎(含备用引擎)不在 profile 白名单 → 400**(翻译由插件做,裁决由 core 做)。整条 `{engine:""}` 视为「该项不单独指定」:类别跟随主引擎,角色跟随其所属类别,插件不下发;**只配 `fallbackEngine`** 是合法的(主引擎跟随主引擎 + 这一路的备)。角色名清单从 `/desk/health.teamRoles`(core `--contract.teamRoles` 转发)取;老 core 没有该字段时插件只做格式校验,名字交给 core 裁决 |
|
|
480
566
|
| `rules` | 可选,`role:<name> → 规则文本`(2026-10-22 只增字段,不升 `contractVersion`)→ `--team-rule`。改写该角色「这类任务适合什么」(计划提示词里的角色表),空文本不下发、留空 = core 内置 `duty`;键必须是已知角色(类别键 / 未知角色 → 400)|
|
|
481
567
|
| `parallel` | 0 / 缺省 = core 默认 4 |
|
|
482
568
|
| `planOnly` | 只出计划,不执行 |
|
|
@@ -508,6 +594,15 @@ notice {text:"团队模式:追加 N 个补充任务(a1、a2),继续
|
|
|
508
594
|
**服务端作业队列**(`true` / 缺失)。有它时客户端应把排队交给服务端(`/desk/jobs` 或带
|
|
509
595
|
`request_id` 的 `/desk/ask`),本地不再自行判忙;缺失时客户端退回本地串行门(老插件)。
|
|
510
596
|
|
|
597
|
+
`/desk/health.restarting`(2026-10-25 只增字段,不升 `contractVersion`):本实例是否
|
|
598
|
+
已受理「重启服务」并在等定时器拉起助手(`true` / 缺失 = 否)。`POST /desk/restart` 的
|
|
599
|
+
链路(见 `internal/client/restart.go`):桌面端**先立标志、先答 200**(重复点击响应带
|
|
600
|
+
`already_pending: true`,不再起第二个助手),默认 1 秒后由**进程内定时器**拉起一个
|
|
601
|
+
脱离终端的 `magic-client restart` 助手停旧实例、按状态文件参数起新实例。探活语义:
|
|
602
|
+
带 `restarting: true` 的 200 = 旧实例认账、即将下线(可当作「看到下线」);再探到
|
|
603
|
+
`restarting` 为假(缺失)的 200 = 新一代已起来。老插件没有该字段 → 客户端照旧
|
|
604
|
+
「先看到连不上、再看到 200」的判据,两种字段都要认。
|
|
605
|
+
|
|
511
606
|
⚠️ **`/desk/health.version` 是插件启动时缓存的 core 版本**:只升级 CLI、没重启插件时它会
|
|
512
607
|
一直报旧值(2026-10-22 实测:CLI 已 2.1.3,手机仍显示 1.0.111)。要显示「此刻真正的
|
|
513
608
|
core 版本」必须读 `/desk/contract.version`(每次都重新问 core);`health.version` 只适合
|
|
@@ -622,11 +717,73 @@ core 的 stdout 事件流里没有 user 事件(它只写进会话日志),
|
|
|
622
717
|
| `client-ui/shared/conversation.css` | 组件样式。结构尺寸共用;颜色走 3.3 的变量契约,由宿主供值 |
|
|
623
718
|
| `client-ui/shared/transport-http.js` | 传输层的**可选参考实现**(HTTP 版,打 `/desk/*`)。组件不依赖它 |
|
|
624
719
|
| `client-ui/index.html` | PC 宿主:oklch 令牌 + 一条顶栏 + 挂组件 |
|
|
625
|
-
| `client-ui-mobile/index.html` | 移动端宿主:AA 安卓端那套十六进制令牌 + 移动形态顶栏 + 挂组件;「我的」里可设默认引擎/模型(`localStorage` 的 `magic.engine` / `magic.model`,新建任务作为 `engine` / `model` 初值,会话内落定后由 `onPick` 回写);「发现 → 插件」是 `GET /desk/plugins` 的目录页(介绍 / 安装状态 / 安装升级 /
|
|
720
|
+
| `client-ui-mobile/index.html` | 移动端宿主:AA 安卓端那套十六进制令牌 + 移动形态顶栏 + 挂组件;「我的」里可设默认引擎/模型(`localStorage` 的 `magic.engine` / `magic.model`,新建任务作为 `engine` / `model` 初值,会话内落定后由 `onPick` 回写);「发现 → 插件」是 `GET /desk/plugins` 的目录页(介绍 / 安装状态 / 安装升级 / 让它干活);「发现 → 知识库」是 `GET /desk/knowledge` 的目录页(OpenViking 的本机安装状态 / 当前版本 / PyPI 最新版本 / 一键安装与升级 / 让它干活),三态(未安装 / 已安装 vX / 可升级 vY)都由服务端探测后宣布,页面不自己推断;「发现 → 脚本」是 `GET /desk/scripts` 的清单页(登记脚本 / 启停 / 看实时日志),内容与逻辑在 `client-ui-mobile/scripts.js` + `scripts.css`(独立子页面模块,与 `reqs.js` 同一纪律) |
|
|
626
721
|
|
|
627
722
|
移动端打包时(magic-test 的 `tools/build-mobile-www.cjs`)会把 `client-ui/shared/`
|
|
628
723
|
一并复制进 `www/shared/` —— 页面按同级 `shared/` 引用它,缺了这个复制对话区会是空白。
|
|
629
724
|
|
|
725
|
+
#### 移动端「消息通知」通道(2026-10-24 补记,2026-10-25 修正)
|
|
726
|
+
|
|
727
|
+
「我的 → 消息通知」有三个开关(任务结果 / 任务待确认 / 定时任务),存在
|
|
728
|
+
`localStorage` 的 `magic.notify`(**纯设备偏好,不进 `/desk/prefs` 远端同步**)。
|
|
729
|
+
但**通知能不能真的弹出来,取决于设备上有没有一条通着的原生通道**,不是看开关。
|
|
730
|
+
|
|
731
|
+
通道只有两条,按环境自动选:
|
|
732
|
+
|
|
733
|
+
| 环境 | 通道 | 说明 |
|
|
734
|
+
| --- | --- | --- |
|
|
735
|
+
| APK 壳(Capacitor) | `window.Capacitor.Plugins.LocalNotifications` | **系统通知**(通知栏里那条)。插件在 APK 的 dex 里,`POST_NOTIFICATIONS` 权限也已声明 |
|
|
736
|
+
| 真浏览器打开 `/m/` | `window.Notification` | 系统通知。**Android WebView 不支持 Notifications API** —— 所以这条在 App 内根本不存在,不许拿它当 App 内的兜底 |
|
|
737
|
+
| 两条都不通 | 应用内 toast | 一条灰条。**不是**系统通知 |
|
|
738
|
+
|
|
739
|
+
⚠️ **远程页里 `window.Capacitor` 根本不存在**(根因 2026-10-25 从 APK smali 逐行确认):
|
|
740
|
+
APK 壳配对后加载的是桌面的 `/m/` 远程页。Capacitor 的 JS 注入有两条路,对远程页**全都不通**:
|
|
741
|
+
① `DOCUMENT_START_SCRIPT`(现代 WebView 都支持)—— 脚本只注册给壳自己的 origin
|
|
742
|
+
(`http://localhost`),远程页不在集合里;② 这条分支生效时本地服务器的 `jsInjector`
|
|
743
|
+
被置成 **null**(`Bridge.loadWebView` 里 `v4 = null`),`handleProxyRequest` 的注入条件
|
|
744
|
+
① `jsInjector != null` 直接失败 —— 远程页被原样代理,**一个字节的 JS 都没注入**。
|
|
745
|
+
但**传输层**在:`useLegacyBridge = true` 走 `addJavascriptInterface("androidBridge")`,
|
|
746
|
+
注入**所有 frame**(App 内 APK 下载用的 `window.MagicUpdate` 就是这么工作的)。
|
|
747
|
+
即使运行时在(如 App 加载本地页 `ui.html`),**传输层 `androidBridge` 也是按 origin
|
|
748
|
+
白名单注入的** —— 远程桌面地址(`http://<局域网IP>:9100`)不在白名单里时,
|
|
749
|
+
传输层不注入,插件调用**既不报错也不回话**(promise 永不 settle)。
|
|
750
|
+
|
|
751
|
+
**兜底桥(2026-10-25 补,纯前端、无需重打 APK)**:`notifyNativeShim()` 在
|
|
752
|
+
`window.Capacitor` 不在、`window.androidBridge` 在时自建最小 Capacitor 响应面 ——
|
|
753
|
+
`androidBridge.postMessage(JSON.stringify({callbackId, pluginId, methodName, options}))`
|
|
754
|
+
发原生消息,`window.Capacitor.fromNative({callbackId, success, data|error})` 收响应,
|
|
755
|
+
协议与 MessageHandler 的 legacy 桥逐字段对齐。**只挂 `fromNative`/`nativePromise`,
|
|
756
|
+
不伪造 `window.Capacitor.Plugins.*`** —— `notifyPlugin()` 永远只认真运行时,
|
|
757
|
+
自建桥带 `__shim` 标记,真伪可区分。调用通道一律走 `notifyNativePlugin()`
|
|
758
|
+
(真运行时优先,自建桥兜底)。
|
|
759
|
+
|
|
760
|
+
所以移动端这一段的实现必须遵守:
|
|
761
|
+
|
|
762
|
+
- **一律超时即失败**(`notifyWithTimeout`),不靠「插件对象在不在」猜通不通;
|
|
763
|
+
超时按失败处理、落到 toast,**不能因为「调了没抛错」就当成功**。
|
|
764
|
+
- **状态位必须问原生**:`checkPermissions()` / `requestPermissions()` 返回
|
|
765
|
+
`{display: 'granted' | 'denied' | 'prompt'}`,页面显示的「已开启 / 已拒绝 / 去授权」
|
|
766
|
+
全部由它推导 —— 不许拿「插件对象在」当「已接入」。
|
|
767
|
+
- **`schedule()` 不传 `schedule` 字段 = 立即显示**(插件 `isScheduled()` 为 false 时直接
|
|
768
|
+
`notificationManager.notify()`);传了 `schedule.at` 反而是「排到那个时刻」,那是定时
|
|
769
|
+
任务的事、不是「现在弹一条」。
|
|
770
|
+
- 通知 `id` 必须是 Java int 范围内的数字(超了整个 call 被 reject),且不重复
|
|
771
|
+
(同 id 会覆盖前一条)。
|
|
772
|
+
- **通道自检与诊断入口(2026-10-25 撤)**:「发一条测试通知」与「通道诊断」曾作为
|
|
773
|
+
设置页行露出(静默失败过一次后的教训);通道打通后用户定调正式 UI 不要调试工具,
|
|
774
|
+
两个入口撤掉。底层 `notifyTest()` / `notifyDiagnose()` **保留**(行为由契约检查
|
|
775
|
+
第 ⑦~⑩ 组钉住)—— 通道再出问题时挂回任意入口就能当场自证,诊断分档
|
|
776
|
+
(no-runtime / shim / no-plugin / transport-dead)与 origin/插件清单报告不变。
|
|
777
|
+
|
|
778
|
+
APK 侧让传输层通到远程页的办法:`assets/capacitor.config.json` 里
|
|
779
|
+
`android.useLegacyBridge = true`(走 `addJavascriptInterface`,注入所有 frame;
|
|
780
|
+
现代的 `addWebMessageListener` 按 origin 白名单匹配,表达不了「任意局域网 IP」)。
|
|
781
|
+
⚠️ **这只解决传输层,不解决注入层** —— 注入(`window.Capacitor` 存不存在)由
|
|
782
|
+
Capacitor 自己的本地服务器决定,远程页可能压根没被注入。两者要分开看。
|
|
783
|
+
2026-10-25 起注入层断的那一档由上面的兜底桥在页面侧补齐。
|
|
784
|
+
校验:`npm run check:notify`(纯逻辑)+ 真机验收(移动端设置页的
|
|
785
|
+
「发一条测试通知」与「通道诊断」两个入口)。
|
|
786
|
+
|
|
630
787
|
> **组件宿主页必须先引**:`shared/vendor/marked.min.js` + `shared/vendor/purify.min.js`
|
|
631
788
|
> (并设 `window.QA_HLJS_URL` 指向 highlight.js),再引内核分片 `shared/conv/01-dom.js`、
|
|
632
789
|
> `02-media-kinds.js`、`08-ask-event-map.js`、`10-markdown-render.js`、`29-msg-variants.js`
|
|
@@ -837,6 +994,7 @@ magic-agent 把会话的查询与修改都做成 CLI 命令,调用方 exec 即
|
|
|
837
994
|
| 读已存标题 | `magic-agent --session-title <id>` → `{id, title}`(不调 LLM) |
|
|
838
995
|
| **需求清单** | `magic-agent --requirements all\|<ws_id\|目录>` → `{ok, root, workspaces[]}`(只读,不调 LLM,见 §3.4.1) |
|
|
839
996
|
| **归纳需求** | `magic-agent --consolidate-requirements <ws_id\|目录\|.>` → 一轮归纳结果(**会调模型**) |
|
|
997
|
+
| **追加需求截图** | `magic-agent --requirement-shot <ws_id\|目录> --requirement-name <功能点名> [--requirement-module <模块>] [--requirement-caption <说明>] < shot.png` → `{ok, path, …}`(PNG 从 **stdin** 读,见 §3.4.1.1) |
|
|
840
998
|
|
|
841
999
|
`--session-log` 的输出形状与插件同名的 HTTP 接口**逐字段对齐**,两条路可互换。
|
|
842
1000
|
其中 `kind:"user"` 事件新增可选 `hosted:true`(2026-10-23 只增字段,`omitempty`):
|
|
@@ -922,7 +1080,8 @@ event: end data: {"session":"<id>","after":<末位置>,"state":"done|faile
|
|
|
922
1080
|
- 游标 = 最后一条已收到事件的 `seq`,重连请求带 `?after=<游标>`(断点续传,不重放已画过的帧);
|
|
923
1081
|
- 指数退避:0.8s 起、翻倍、单次 15s 封顶;连续 12 次或 6 分钟仍连不上才按失败收尾;
|
|
924
1082
|
- 重连期间那一轮**不封口**(不调 `finish` / `qaRunEnd`),底部继续显示运行态;
|
|
925
|
-
|
|
1083
|
+
界面插一行「连接中断,正在自动重连…」,恢复后就地改成「已重新连接,继续同步」,
|
|
1084
|
+
短暂提示后自动收走(不是这一轮的历史,别长期挂在画布上);
|
|
926
1085
|
- 4xx(404 老网关 / 401 令牌失效等)是确定性失败,**不重试**,立即 fallback / 如实报错。
|
|
927
1086
|
参考实现:`client-ui/shared/transport-http.js` 的 `followSession`(`onRetry` / `onResume`
|
|
928
1087
|
可选回调,老宿主不实现也不影响续流);闸门 `npm run check:follow`。
|
|
@@ -1021,11 +1180,11 @@ core 侧只有一张空的 sid→title 覆盖表,于是:
|
|
|
1021
1180
|
| 生成在 core | `magic-agent --suggest-followups <file\|->`(`internal/suggest`):提示词、截断、JSON 解析、去重与长度上限都只有一份;移动端 / PC 拿到的建议不会各说各话 |
|
|
1022
1181
|
| **默认关闭** | 建议引擎**不配置 = 不开启建议功能**(2026-10-23 用户定调)。组件 `suggestEngine` 为空时不发 `/desk/suggest`、不画建议、不补历史建议、托管也不自动执行;老用户升级后默认没有建议,需去「我的」里显式选引擎 |
|
|
1023
1182
|
| 配置入口 | 移动端「我的 → 建议引擎 / 建议模型」两行,点开的是与「默认引擎 / 模型」相同的**二列抽屉**:左列首项「不开启」(默认),其余是 `/desk/engines` 探测到的可用引擎;点左列只切右列,点右列才落定。引擎存 `magic.suggest.engine`、模型存 `magic.suggest.model`,都属于环境配置(`ENV_SCOPE_KEYS` + `/desk/prefs` 同步);改完立刻 `conv.setSuggestEngine(engine, model)` 生效,下一轮收尾开始生成 |
|
|
1024
|
-
| 选引擎 / 模型 | `llm`
|
|
1025
|
-
| **思考固定关闭** | 建议调用一律带 `NoThinking`,各引擎走官方开关:`claude` → `--thinking disabled`;`codex` → `model_reasoning_effort="none"`;`llm` → `reasoning_effort="none"`(直连请求体;CLI 委托路径用 `-o reasoning_effort none
|
|
1183
|
+
| 选引擎 / 模型 | `llm` 显式选中的模型**先**按它自己跑(只传 `reasoning_effort=none`,不预先换模型);若该模型拒绝该参数(`MiniMax-M3.1-Flash` 要求 adaptive thinking,HTTP 400),**失败后降级到 `models.json` 里第一条 `thinking.disabled` 条目重试一次**(本机 `minimax-nothink`)—— 建议链路要求「一律不思考」,显式选的模型撑不住时必须换到能关思考的那条,否则整条建议链路静默失败(2026-10-24 修)。**建议模型留空**时默认就用那条 `thinking.disabled` 条目。`claude` / `codex` 可用各自的关闭档 |
|
|
1184
|
+
| **思考固定关闭** | 建议调用一律带 `NoThinking`,各引擎走官方开关:`claude` → `--thinking disabled`;`codex` → `model_reasoning_effort="none"`;`llm` → `reasoning_effort="none"`(直连请求体;CLI 委托路径用 `-o reasoning_effort none`);空建议模型用 `thinking.disabled` 变体作默认值,显式模型撑不住时降级到该变体;`codebuddy` → settings `alwaysThinkingEnabled=false`;`dsh` → SDK `InitializeParams.reasoningEffort="off"`。`trae`/`openclaw`/`arkclaw` 没有已知开关,仍保留 `NoThinking` 标记,失败后再降级 `llm` |
|
|
1026
1185
|
| **不续接会话** | 独立进程、新会话、`ToolsOff`、不带 `SessionID` / `Continue` —— 「请生成建议」这类指令不会写进用户正在聊的上下文,也不进会话登记表 / 日志 |
|
|
1027
|
-
| 引擎走「无工具 + JSON」通道 | core 的 `suggest.Generate` 固定 **`ToolsOff` + `JSONSchema` + `NoThinking`**,引擎以 `--print --output-format json` 返回一小段结果,不走流式、不带工具。默认选 `llm`;建议模型留空时用 `models.json` 第一条 `thinking.disabled` 变体(本机 `minimax-nothink
|
|
1028
|
-
| 慢引擎不拖界面 | `claude` / `codex` / `codebuddy` / `dsh` 等非 `llm` 引擎给 10s 主引擎预算;失败/超时才降级 `llm`,并清掉当前引擎的 model
|
|
1186
|
+
| 引擎走「无工具 + JSON」通道 | core 的 `suggest.Generate` 固定 **`ToolsOff` + `JSONSchema` + `NoThinking`**,引擎以 `--print --output-format json` 返回一小段结果,不走流式、不带工具。默认选 `llm`;建议模型留空时用 `models.json` 第一条 `thinking.disabled` 变体(本机 `minimax-nothink`),显式模型则先按它自己跑、撑不住(拒绝 `reasoning_effort=none`)时降级到该变体 |
|
|
1187
|
+
| 慢引擎不拖界面 | `claude` / `codex` / `codebuddy` / `dsh` 等非 `llm` 引擎给 10s 主引擎预算;失败/超时才降级 `llm`,并清掉当前引擎的 model。**降级那一档最多 8s**,主引擎的预算要先给它留出来,否则主引擎把整个 timeout 吃光、降级永远轮不到。界面默认不传 engine,直接走快路径,20s 超时不会触发 |
|
|
1029
1188
|
| **允许空数组** | 模型判断「没有下一步」时回 `[]`,这是**合法结果**:客户端不画任何按钮,不拿模板凑 |
|
|
1030
1189
|
| 成功收尾后问 | 失败 / 被用户停止 / 还在跑的轮次都不问;本轮出现过提问卡(引擎在等作答)时也跳过,避免与作答卡抢注意力 |
|
|
1031
1190
|
| 历史最后一轮补一次 | 打开一条**已收尾、不在跑**的历史会话时,回放完对最后一轮补一次建议;更早的轮次不问(它的「下一步」早已被后续对话覆盖)。缓存按 `(sid, 最后一条 user 的 at/seq, 建议引擎, 建议模型)` 放 `localStorage`,30 分钟内重开不重复调模型;空数组也缓存 |
|
|
@@ -1084,13 +1243,14 @@ magic-test 桌面端那份内联 `app.js` 不加载这个组件,因此不会
|
|
|
1084
1243
|
| --- | --- |
|
|
1085
1244
|
| `--sessions` 给 `status` | **会话级唯一口径**,客户端状态判定一律先读它;`state` 降级为「这一轮的状态」,只在老版本 core 没有 `status` 时回落 |
|
|
1086
1245
|
| `alive` 与 `status` 同源 | `alive = (status == running)`。同一行不会出现 `status=idle` 却 `alive=true` |
|
|
1246
|
+
| `status=queued`(2026-10-24 只增取值) | 服务端作业队列**受理即登记**的排队占位:作业还没开跑(`pid=0`、`alive=false`、`has_log=false`),`run_id` = 客户端 `request_id`(缺省用 job id),`title` 回落 `prompt_head` 且 `title_summarized=true`(没有日志,别去归纳)。真开跑后这一行由 core 自己的 running 记录接手(占位被撤)。客户端把它画成「等待中」,**不要**当成「没发生过」或「已结束」 |
|
|
1087
1247
|
| 覆盖表路径 | `~/.magic-agent/session-status.json`(与 `session-titles.json` 同目录),进程内缓存;`MAGIC_AGENT_SESSIONS` 指向别处时取它的父目录 |
|
|
1088
1248
|
| 表形状 | `{ "<session_id>": {"state","run_id","run_started","updated_at"} }`;键优先 `session_id`,引擎还没吐 id 时落在 `run_id` 上,拿到后迁移并删旧键(与标题同一坑:留双键就会按不同 id 读到两个状态) |
|
|
1089
1249
|
| `run_started` 排序守卫 | 写入方是**各轮的进程**:旧一轮收尾(`Finish` / `Stop` / 进程已不在 → `gone`)可能晚于新一轮开跑。只在写入轮**不早于**表里那一轮时才覆盖,迟到的 `done` 不会盖掉新一轮的 `running` |
|
|
1090
1250
|
| 写入时机 | 与登记表同一步(`Handle.write` + 所有 `writeRecord` 路径,含 `Stop` / 同会话旧轮被 supersede / 读侧归一 `gone`);不落登记表(`--save-registry` 未开)时状态也不留痕 |
|
|
1091
1251
|
| 删除会话一并清 | `--delete-session` / `DELETE /desk/session/{id}` 会删掉状态行(返回里的 `statuses_removed`),避免同 id 再建会话时继承一条过期状态;删主会话还会沿父指针级联删掉子任务会话(`child_sessions_removed`) |
|
|
1092
1252
|
|
|
1093
|
-
状态词表与登记表完全一致:`running` / `waiting_approval` / `idle`(非终态),
|
|
1253
|
+
状态词表与登记表完全一致:`running` / `waiting_approval` / `idle` / `queued`(非终态),
|
|
1094
1254
|
`done` / `failed` / `cancelled` / `interrupted` / `gone`(终态)。
|
|
1095
1255
|
|
|
1096
1256
|
⚠️ 老版本 core 的 `--sessions` 没有 `status` —— 客户端**如实回落 `state`**(行为与旧版一致,
|
|
@@ -1172,8 +1332,8 @@ CLI)才按 `claude → codebuddy → codebuddy-ai → codex → trae → dsh`
|
|
|
1172
1332
|
```
|
|
1173
1333
|
|
|
1174
1334
|
`--consolidate-requirements` 的输出:`{ok, workspace, dir, module?, noop?, bootstrap?, bootstrap_engine?,
|
|
1175
|
-
scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pending, error?}`
|
|
1176
|
-
(`module` 只在模块级整理时出现;此时 `consumed` 恒为 0)。
|
|
1335
|
+
scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pending, merged?, merged_modules?, error?}`
|
|
1336
|
+
(`module` 只在模块级整理时出现;此时 `consumed` 恒为 0、`merged` 恒为 0)。
|
|
1177
1337
|
`noop=true` 是「没有未归纳记忆、也没 `--requirement-force`」的正常结果,不是错;
|
|
1178
1338
|
`bootstrap=true` 表示这一轮做了首次目录初始化(`bootstrap_engine` 是实际读目录的引擎,
|
|
1179
1339
|
`scanned` 是扫描到的条目数,排障用)。
|
|
@@ -1185,9 +1345,24 @@ scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pen
|
|
|
1185
1345
|
- **归纳是增量的、只增不减**:模型只给 `desc / points / conclusions / sources` 编号,
|
|
1186
1346
|
骨架(最终版 / 需求描述 / 需求点 / 已定结论 / 来源对话 / 更新记录)与日期由代码写;
|
|
1187
1347
|
已有文档的模块**不搬家**(模型每轮的 module 会漂,文档一旦归类就稳定)。
|
|
1348
|
+
- **模块去重(2026-10-25)**:空间级整理收尾时会把「表达同一件事」的已有模块合并
|
|
1349
|
+
(模型语义判定 + 确定性候选闸门;并入功能点更多的模块,同名文档内容合并不丢,
|
|
1350
|
+
各补一行「模块合并」更新记录)。`merged` 是合并掉的模块个数、`merged_modules` 是
|
|
1351
|
+
「被合并 → 并入」清单;带 `module` 的模块级整理**不做**模块合并(只动一个模块的纪律)。
|
|
1352
|
+
手动「整理」即使没有新记忆也会查一次(`noop` 与 `merged` 互斥)。
|
|
1188
1353
|
- **存储**在 `~/.magic-agent/requirements/`(`MAGIC_AGENT_REQUIREMENTS` 可覆盖),
|
|
1189
1354
|
与业务项目的 `~/.magic-test/requirements/` **互不相干、不自动迁移**:
|
|
1190
1355
|
`<ws>/features/<模块>/<名称>.md` 是给人直接打开的 markdown。
|
|
1356
|
+
- **会话↔需求关系(2026-10-25)**:归纳落账关系时**同步**写 `<ws>/relations.jsonl`
|
|
1357
|
+
(追加型,每行 `{ts, session, stitle?, module, feature, n?}`;去重键 = session+module+feature+n,
|
|
1358
|
+
每轮归纳收尾压缩一次;模块合并时 `module` 字段同步改写)。`stitle` 是写入当时的会话标题快照
|
|
1359
|
+
—— 登记表会被清理,图谱**不能**依赖事后回查。文档的「来源对话」段(30 条截断)只是展示,
|
|
1360
|
+
关系以此文件为准。读取口:CLI `--requirements-graph [all|<ws|目录>]` 与 HTTP
|
|
1361
|
+
`GET /desk/requirements/graph?ws=`(能力位同 `feature.requirements`,缺了回 501),
|
|
1362
|
+
输出 `{ok, root, workspaces:[{id, name, dir?, nodes, edges}]}`,node 形状
|
|
1363
|
+
`{id, type:"session"|"feature", title, module?}`(feature id = `f:<模块>/<名>`、
|
|
1364
|
+
session id = `s:<session>`),edge `{source, target}`(同对去重)。relations 里指向
|
|
1365
|
+
已不存在文档的功能点连同边**静默丢弃**(文档目录现算清单为准)。
|
|
1191
1366
|
|
|
1192
1367
|
**客户端要做的事**:列表页只调 `--requirements`(每刷一次都调,绝不顺手触发归纳);
|
|
1193
1368
|
「整理」按钮调 `--consolidate-requirements`,并**传它要整理的那个空间 + 当前配置的默认引擎**
|
|
@@ -1200,11 +1375,90 @@ scanned?, updated, created, points, conclusions, consumed, replayed?, calls, pen
|
|
|
1200
1375
|
按钮在首次初始化时要允许几分钟等待(文案「初始化中…」),完成后再 `--requirements` 刷新。
|
|
1201
1376
|
打开一篇文档读 `file` 绝对路径的正文即可(普通 markdown,走既有的阅读器)。插件在每轮 `/desk/ask` 收尾后**防抖 12s 自动归纳**
|
|
1202
1377
|
该轮所在工作区(`MAGIC_CLIENT_NO_REQ_AUTO=1` 关闭),所以客户端不必自己轮询触发。
|
|
1203
|
-
移动端把「需求」做成常驻页签(见 §1.3
|
|
1378
|
+
移动端把「需求」做成常驻页签(见 §1.3 的说明),列表页读失败要显示原因而不是空列表。
|
|
1204
1379
|
**没有功能点的工作区不渲染**(用户 2026-10-22):「全空 + 一条灰字说明」比「一列空工作区」
|
|
1205
1380
|
更像出了错;`feature_count == 0` 且模块里也没有行的那个 `workspaces[]` 项直接跳过,
|
|
1206
1381
|
整页都为空时再合成一句提示(有 `pending` 就引导点「整理」,否则说「聊几句就会有」)。
|
|
1207
1382
|
|
|
1383
|
+
#### 3.4.1.1 页面完成情况截图(2026-10-25)
|
|
1384
|
+
|
|
1385
|
+
**需求**:需求文档整理时,**涉及页面改造**的功能点要调用内置浏览器 MCP 对完成情况
|
|
1386
|
+
**截图标注并放进文档里**(用户原话)。
|
|
1387
|
+
|
|
1388
|
+
**能力位**:`feature.requirement.shots`(与 `feature.requirements` **分开** —— core 可能
|
|
1389
|
+
支持需求聚合但还不支持截图;界面要能分别判断,别把整个「需求」页签藏掉)。
|
|
1390
|
+
插件对 `POST /desk/requirements/shots` 先查这个位,缺了回 **501 + 中文原因**(点名缺哪个位)。
|
|
1391
|
+
|
|
1392
|
+
**文档形状**:功能点文档多出**第七段「页面完成情况」**(排在「已定结论」之后、
|
|
1393
|
+
「来源对话」之前):
|
|
1394
|
+
|
|
1395
|
+
```markdown
|
|
1396
|
+
## 页面完成情况
|
|
1397
|
+
|
|
1398
|
+
页面地址:https://example.com/app
|
|
1399
|
+
-  <!-- 2026-10-25T10:00:00Z -->
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
- 页面地址行前缀固定为 `页面地址:`;**地址只认 http/https**(占位符 / 本机路径 / `javascript:` 一律不收)。
|
|
1403
|
+
- 截图行是 ``;路径含空格或括号时用 `<>` 包(markdown 的标准转义)。
|
|
1404
|
+
- 行末的 `<!-- RFC3339 -->` 是**截图时刻**(渲染不可见、解析能取回)—— 清单上的「最后截图时间」靠它。
|
|
1405
|
+
- **文档格式由 core 拥有**:客户端不写文档、不碰 `~/.magic-agent/requirements/`。
|
|
1406
|
+
|
|
1407
|
+
**清单里的新字段**(`features[]` 里,只增不删):
|
|
1408
|
+
|
|
1409
|
+
| 字段 | 类型 | 含义 |
|
|
1410
|
+
| --- | --- | --- |
|
|
1411
|
+
| `page_url` | string | 该功能点对应的页面地址(归纳时从对话里提取;可空) |
|
|
1412
|
+
| `shots` | int | 已有的「页面完成情况」截图数(0 = 还没截) |
|
|
1413
|
+
| `shot_at` | int | 最后一张截图的时刻(unix 毫秒;0 = 还没截) |
|
|
1414
|
+
|
|
1415
|
+
**归纳侧的口径**:模型**只给增量**,其中 `page_url` 是**可选**字段(不在 `required` 里);
|
|
1416
|
+
提示词明确「只有当记忆里明确出现了这个页面(http/https 开头)才填,并原样照抄;没有就填空串,
|
|
1417
|
+
**绝不猜、绝不编**」。新值**合法时**才覆盖旧值(空 / 非法一律保留旧值)。
|
|
1418
|
+
归纳的**全量重写**必须**保住**已有的截图段与页面地址(否则每轮整理都把截图冲掉)。
|
|
1419
|
+
|
|
1420
|
+
**追加截图(CLI)**:
|
|
1421
|
+
|
|
1422
|
+
```bash
|
|
1423
|
+
magic-agent --requirement-shot <ws_id|目录> --requirement-name <功能点名> \
|
|
1424
|
+
[--requirement-module <模块>] [--requirement-caption <说明>] < shot.png
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
PNG 走 **stdin**(不进 argv:几百 KB 会撞命令行长度上限)。落盘到
|
|
1428
|
+
`<wsid>/shots/<模块或 unfiled>/<功能点slug>/<unix毫秒>[-n].png`(0600;校验 PNG magic;
|
|
1429
|
+
上限 8MB)。**找不到对应功能点时明确报错**(不新建同名空文档)。输出:
|
|
1430
|
+
`{ok, workspace, dir, module?, name?, file?, path?, shots, error?}`。
|
|
1431
|
+
|
|
1432
|
+
**追加截图(HTTP)**:`POST /desk/requirements/shots?ws=&module=&name=&caption=`
|
|
1433
|
+
(body = **PNG 原始字节**,不是 multipart)。插件 → core 走 `--requirement-shot`,PNG 经 stdin。
|
|
1434
|
+
`name` 必填;空 body 回 400;超 8MB 回 **413**(不是 400 —— 「图片太大」与「请求体坏了」
|
|
1435
|
+
要分得开,用户才看得懂);能力位缺了回 501。
|
|
1436
|
+
|
|
1437
|
+
**客户端要做的事**(移动端「需求」tab;2026-10-25 起截图改基 agent-browser):
|
|
1438
|
+
|
|
1439
|
+
- 功能点行上出**相机徽标**的条件 = `page_url` 非空 **且** core 有 `feature.requirement.shots`。
|
|
1440
|
+
文案分两档:还没截过 = 「待截图」,截过了 = 「N 图」。**没有 `page_url` 就不出徽标**
|
|
1441
|
+
(不给「点了必然失败」的死入口)。
|
|
1442
|
+
- 点徽标 → 抽屉(**页面地址可改** —— 归纳提错或漏提时人来定更准;**标注说明可写**)→
|
|
1443
|
+
「打开页面并截图」一步完成两件事:
|
|
1444
|
+
1. 手机端**强制走 `/b/` 中转**打开页面(给用户看、可触控 —— relay 只做远程鼠标);
|
|
1445
|
+
2. 同时 `POST /desk/requirements/shot-remote` 让**服务端 agent-browser** 对页面地址
|
|
1446
|
+
截一张全页图(真实 Chromium,不受跨域污染 / iframe 限制),回图片地址 → 标注浮层。
|
|
1447
|
+
- **服务端没装 agent-browser** → shot-remote 回 501 + 安装提示,界面如实 toast
|
|
1448
|
+
(`npm install -g agent-browser && agent-browser install`),不假装能截。
|
|
1449
|
+
- 标注浮层:矩形拖拽 / 文字 / 三色 / 撤销 / 清除 / 取消 / 完成。**文字不用 `window.prompt`**
|
|
1450
|
+
(Android WebView 与 Electron 小窗都没实现它,调用只会静默返回 null —— 表现是「点了没反应」)。
|
|
1451
|
+
- 「完成」→ canvas 导 PNG → `POST /desk/requirements/shots` → 刷新清单(徽标变「N 图」)。
|
|
1452
|
+
- **读文档里的截图**:`core` 写的是**本机绝对路径**(`C:/…` 或 `/…`),
|
|
1453
|
+
而 DOMPurify 的协议白名单会**摘掉**这种 `src`(默认只放行 http/https/mailto/tel/cid/xmpp)。
|
|
1454
|
+
所以渲染前要改写成 `<img data-reqshot="绝对路径">`(`data-*` 消毒默认放行),
|
|
1455
|
+
渲染后再把 `src` 补成回读地址(`/desk/media?path=…`,移动端带 `w=480`)。
|
|
1456
|
+
协议相对地址(`//host/a.png`)是**网页上的图**,不许当本机路径处理。
|
|
1457
|
+
|
|
1458
|
+
**已知边界**:标注只有矩形与文字(无箭头 / 马赛克 / 裁剪);服务端截的是 agent-browser
|
|
1459
|
+
独立 profile 里 page_url 加载完的初始状态(不共享中转 cookie jar,需登录的页面截出来
|
|
1460
|
+
可能是登录页;也不是手机 iframe 里滚到的位置)—— 与「手机上看到的」可能有差,如实记录。
|
|
1461
|
+
|
|
1208
1462
|
|
|
1209
1463
|
### 3.5 两条不变量
|
|
1210
1464
|
|