magic-agent 1.0.2 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CLIENT-CONTRACT.md +254 -8
  2. package/README.md +48 -5
  3. package/client-ui/index.html +32 -0
  4. package/client-ui/pc/app.css +89 -1
  5. package/client-ui/pc/index.html +18 -8
  6. package/client-ui/pc/js/00-base.js +8 -30
  7. package/client-ui/pc/js/05-task-list-render.js +4 -1
  8. package/client-ui/pc/js/07-chat-stream-render.js +2 -85
  9. package/client-ui/pc/js/10-markdown.js +3 -171
  10. package/client-ui/pc/js/18-askbox.js +2 -176
  11. package/client-ui/pc/js/90-glue.js +1 -1
  12. package/client-ui/pc/js/91-workbench-stubs.js +4 -10
  13. package/client-ui/pc/js/93-rail.js +10 -3
  14. package/client-ui/shared/conv/01-dom.js +52 -0
  15. package/client-ui/shared/conv/02-media-kinds.js +34 -0
  16. package/client-ui/shared/conv/03-chat-message.js +1561 -0
  17. package/client-ui/shared/conv/06-ask-answer.js +377 -0
  18. package/client-ui/shared/conv/07-stream-delegation.js +158 -0
  19. package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
  20. package/client-ui/shared/conv/10-markdown-render.js +252 -0
  21. package/client-ui/shared/conv/18-askbox-markup.js +185 -0
  22. package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
  23. package/client-ui/shared/conv/27-conv-ledger.js +381 -0
  24. package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
  25. package/client-ui/shared/conv/README.md +257 -0
  26. package/client-ui/shared/conv/conv.css +1638 -0
  27. package/client-ui/shared/conv/dom-map.md +170 -0
  28. package/client-ui/shared/conv/manifest.json +44 -0
  29. package/client-ui/shared/conversation.css +368 -190
  30. package/client-ui/shared/conversation.js +1722 -788
  31. package/client-ui/shared/host-glue.js +143 -0
  32. package/client-ui/shared/transport-http.js +130 -6
  33. package/client-ui-mobile/index.html +2417 -103
  34. package/cmd/magic-client/clientctl.go +273 -0
  35. package/cmd/magic-client/main.go +71 -5
  36. package/go.mod +6 -3
  37. package/go.sum +4 -1
  38. package/internal/agent/arkclaw.go +3 -2
  39. package/internal/agent/arkclaw_stream.go +126 -8
  40. package/internal/agent/arkclaw_stream_test.go +97 -15
  41. package/internal/agent/arkclaw_test.go +3 -3
  42. package/internal/agent/attachments.go +18 -5
  43. package/internal/agent/attachments_test.go +7 -1
  44. package/internal/agent/capability_test.go +11 -1
  45. package/internal/agent/claude.go +96 -158
  46. package/internal/agent/codebuddy.go +108 -133
  47. package/internal/agent/codebuddy_gateway.go +8 -4
  48. package/internal/agent/codebuddy_gateway_stream.go +82 -2
  49. package/internal/agent/codebuddy_gateway_test.go +236 -1
  50. package/internal/agent/codebuddy_stream_test.go +262 -0
  51. package/internal/agent/codex.go +50 -9
  52. package/internal/agent/codex_stream_test.go +30 -0
  53. package/internal/agent/codex_test.go +24 -0
  54. package/internal/agent/custom_models.go +260 -8
  55. package/internal/agent/custom_models_test.go +204 -7
  56. package/internal/agent/dsh.go +464 -130
  57. package/internal/agent/dsh_resume.go +258 -0
  58. package/internal/agent/dsh_sdk.go +68 -17
  59. package/internal/agent/dsh_sdk_test.go +102 -2
  60. package/internal/agent/dsh_test.go +409 -36
  61. package/internal/agent/engine.go +26 -6
  62. package/internal/agent/engine_base.go +46 -2
  63. package/internal/agent/engine_base_test.go +29 -0
  64. package/internal/agent/engine_test.go +101 -0
  65. package/internal/agent/env.go +78 -2
  66. package/internal/agent/env_test.go +37 -0
  67. package/internal/agent/itemtracker.go +271 -0
  68. package/internal/agent/launch.go +192 -0
  69. package/internal/agent/launch_test.go +206 -0
  70. package/internal/agent/launch_unix.go +15 -0
  71. package/internal/agent/launch_windows.go +24 -0
  72. package/internal/agent/limitnotice.go +103 -0
  73. package/internal/agent/limitnotice_test.go +211 -0
  74. package/internal/agent/llmengine.go +2 -2
  75. package/internal/agent/models.go +3 -2
  76. package/internal/agent/openclaw.go +4 -3
  77. package/internal/agent/openclaw_acp.go +7 -0
  78. package/internal/agent/openclaw_test.go +2 -2
  79. package/internal/agent/permission.go +100 -11
  80. package/internal/agent/permission_test.go +100 -36
  81. package/internal/agent/repair_models_test.go +194 -0
  82. package/internal/agent/runcmd.go +13 -3
  83. package/internal/agent/runner.go +7 -1
  84. package/internal/agent/runner_test.go +1 -1
  85. package/internal/agent/stream.go +777 -43
  86. package/internal/agent/stream_test.go +3 -2
  87. package/internal/agent/stream_timeline_test.go +905 -0
  88. package/internal/agent/streamjson.go +36 -2
  89. package/internal/agent/toolkind.go +20 -1
  90. package/internal/agent/toolkind_test.go +7 -1
  91. package/internal/agent/tools_suffix_test.go +10 -3
  92. package/internal/agent/trae.go +5 -3
  93. package/internal/agent/workspace_test.go +52 -0
  94. package/internal/cli/ask.go +282 -51
  95. package/internal/cli/ask_test.go +15 -11
  96. package/internal/cli/attach_test.go +32 -20
  97. package/internal/cli/clientcmd.go +156 -0
  98. package/internal/cli/events.go +53 -8
  99. package/internal/cli/events_test.go +230 -0
  100. package/internal/cli/keepalive_test.go +101 -6
  101. package/internal/cli/login.go +61 -4
  102. package/internal/cli/login_test.go +99 -0
  103. package/internal/cli/permission_test.go +5 -1
  104. package/internal/cli/repair.go +60 -0
  105. package/internal/cli/repair_test.go +51 -0
  106. package/internal/cli/root.go +195 -32
  107. package/internal/cli/session.go +183 -29
  108. package/internal/cli/session_test.go +144 -5
  109. package/internal/cli/sessionlog.go +385 -38
  110. package/internal/cli/sessionlog_test.go +708 -0
  111. package/internal/cli/sessionlog_user_test.go +142 -1
  112. package/internal/cli/sessiontitle.go +141 -0
  113. package/internal/client/browse.go +456 -0
  114. package/internal/client/browse_test.go +199 -0
  115. package/internal/client/cors_test.go +35 -0
  116. package/internal/client/daemon.go +525 -0
  117. package/internal/client/daemon_test.go +212 -0
  118. package/internal/client/detach_unix.go +19 -0
  119. package/internal/client/detach_windows.go +27 -0
  120. package/internal/client/pair.go +3 -3
  121. package/internal/client/plugins.go +376 -0
  122. package/internal/client/plugins_test.go +223 -0
  123. package/internal/client/proxy.go +374 -0
  124. package/internal/client/proxy_test.go +212 -0
  125. package/internal/client/service.go +1464 -48
  126. package/internal/client/service_test.go +649 -206
  127. package/internal/client/session_events_test.go +130 -0
  128. package/internal/client/wsproxy.go +113 -0
  129. package/internal/client/wsproxy_test.go +102 -0
  130. package/internal/session/delete.go +151 -0
  131. package/internal/session/delete_test.go +103 -0
  132. package/internal/session/session.go +155 -20
  133. package/internal/session/session_append.go +79 -21
  134. package/internal/session/session_test.go +124 -10
  135. package/internal/session/sessionlog.go +572 -6
  136. package/internal/session/sessionlog_read_test.go +410 -0
  137. package/internal/session/title.go +486 -0
  138. package/internal/session/title_test.go +298 -0
  139. package/npm/bin/magic-agent.js +78 -10
  140. package/npm/bin/magic-client.js +100 -0
  141. package/npm/dist/darwin-arm64/magic-agent +0 -0
  142. package/npm/dist/darwin-arm64/magic-client +0 -0
  143. package/npm/dist/darwin-x64/magic-agent +0 -0
  144. package/npm/dist/darwin-x64/magic-client +0 -0
  145. package/npm/dist/linux-arm64/magic-agent +0 -0
  146. package/npm/dist/linux-arm64/magic-client +0 -0
  147. package/npm/dist/linux-x64/magic-agent +0 -0
  148. package/npm/dist/linux-x64/magic-client +0 -0
  149. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  150. package/npm/dist/win32-x64/magic-client.exe +0 -0
  151. package/npm/install.js +36 -13
  152. package/npm/lib/platform.js +27 -5
  153. package/package.json +8 -3
  154. package/client-ui/pc/js/03-chat-message.js +0 -880
  155. package/client-ui/pc/js/06-ask-answer.js +0 -204
  156. /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
  157. /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
  158. /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
  159. /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
@@ -17,7 +17,7 @@ package cli
17
17
  // -f, --file <path> 从文件读 prompt(- 读 stdin)
18
18
  // -a, --attach <path> 附件(截图/图片),可重复/逗号分隔;与提示词一起发给引擎
19
19
  // -w, --workspace <dir> 工作目录:在该目录里执行引擎(详见下方「工作目录」)
20
- // -t, --timeout <dur> 单次尝试超时(如 3m;默认 600s)
20
+ // -t, --timeout <dur> 单次尝试超时(如 3m;默认 1h)
21
21
  // -r, --retries <n> 失败重试次数(默认 0)
22
22
  // --backoff <dur> 首次重试退避(默认 2s,指数翻倍,上限 30s)
23
23
  // -o, --output <format> 输出:json(默认)| text
@@ -34,10 +34,19 @@ package cli
34
34
  // --engines 列出引擎、可用性与支持的模型(JSON 数组;--json 兼容保留)
35
35
  // --no-models 配合 --engines:跳过模型探测(只列引擎,快)
36
36
  // --stop <id> 停止指定会话/运行(session_id 或 run_id),杀掉它的引擎进程组
37
- // --sessions 列出会话登记表(JSON 数组:run_id / session_id / pid / engine / state)
37
+ // --delete-session <id> 删除一条会话:登记记录 + 事件历史 + 标题覆盖一起删,**全机生效**
38
+ // (各端读同一份登记表,一端删了处处都看不到)。幂等
39
+ // --sessions 列出会话登记表(JSON 数组:run_id / session_id / pid / engine /
40
+ // source / client / state;全机视图,不按 client 过滤)。
41
+ // ⚠️ 只见过**已存档**的会话(--save-registry,默认关)
42
+ // --save-registry 把会话登记落盘(<run_id>.json),让 --sessions / --stop 事后能按 id
43
+ // 寻址。**默认关**:不留痕;不留档的会话停不掉
38
44
  // --keep-alive 常驻会话(claude/codebuddy 默认开,dsh 需显式传;需配合 --stream):
39
45
  // 首轮结束后不退出、等 --append 追加;--keep-alive=false 关闭
40
46
  // --append <id> 向常驻会话追加一条消息(内容用 -p/位置参数给)
47
+ // --session-log <id> 读一段会话的事件历史(--after <seq> 可增量续读)
48
+ // --save-history 把流式会话的事件落盘存档(<session_id>.jsonl,供 --session-log
49
+ // 与客户端回放历史)。**默认关**:一次性调用不留痕
41
50
  // --idle <dur> 常驻会话空闲收工时长(默认 5m;0 = 本轮结束就收工)
42
51
  // -v, --verbose 重试过程打到 stderr
43
52
  //
@@ -64,6 +73,7 @@ import (
64
73
  "os"
65
74
  "strings"
66
75
  "sync"
76
+ "time"
67
77
 
68
78
  "github.com/spf13/cobra"
69
79
 
@@ -129,6 +139,7 @@ func NewRootCommand() *cobra.Command {
129
139
  magic-agent --engines # 列出引擎、可用性与各引擎支持的模型(JSON 数组)
130
140
  magic-agent --engines --no-models # 只列引擎与可用性(跳过模型探测,不启动 CLI)
131
141
  magic-agent --engines --json # 等价(--json 为兼容保留)
142
+ magic-agent --repair-models codebuddy # 还原被覆盖写坏的桌面端 models.json(事故收尾,先留 .broken.bak)
132
143
 
133
144
  四档权限模型(--permission,仅 claude / codebuddy / codebuddy-ai 生效):
134
145
  把「哪些动作自动放行 + 放行不了时由谁裁决」收敛成四档,映射到 Claude Code 的参数:
@@ -178,8 +189,9 @@ llm 引擎(包装 simonw/LLM CLI,模型/密钥由它自管):
178
189
  codex codex debug models
179
190
  openclaw openclaw models list --json
180
191
  arkclaw 无清单(模型由网关按 claw_id 绑定)→ 输出 models_note 说明
181
- dsh $DSH_HOME/settings.yaml 里已配置的模型(llm-pi-ai.providers.<route>.models,
182
- 输出 route/model);headless 无 --model,换模型要改该文件
192
+ dsh 配置文件里已配置的模型(0.1.7+ 读 profile 的 cordis.patch.yml,
193
+ 更早读 $DSH_HOME/settings.yaml;输出 route/model);
194
+ headless 无 --model,换模型要改配置
183
195
  探测会真的启动这些 CLI 的只读子命令(最慢 openclaw ~2.5s),故并发执行、
184
196
  单引擎上限 30s;拿不到清单不报错,原因写进该行的 models_note。
185
197
  --no-models 整段跳过探测(只列引擎与可用性,不启动任何 CLI)。
@@ -231,19 +243,20 @@ dsh 引擎(DeepSeek Harness,本机 dsh CLI):
231
243
  说明:
232
244
  · 模型:SDK 通道下 -m 生效(转成 initialize 的 provider/model,取 route/model 形态,
233
245
  可用值见 --engines 的 models 字段);回退 headless 后 -m 不生效(无 --model 参数),
234
- 模型由 dsh 自己的配置决定($DSH_HOME/settings.yaml 的 agent-default-model 段 +
235
- llm-pi-ai.providers,密钥放 $DSH_HOME/.credentials.yaml 或 .env),此时会打一行告警。
236
- · 工具调用只在 SDK 通道上有:headless 的 stdout/stderr 里都没有工具事件(官方定位就是
237
- 「推理走 stderr、正文走 stdout、然后退出」)→ 要工具事件请确保走 SDK 通道。
238
- · --stream:SDK 通道下推理 → thinking、正文 → 逐 step text、工具 → tool_use/tool_result、
239
- 收尾一条 turn_end;回退 headless 后只流推理(stderr 的 "dsh: reasoning:" 增量),
240
- 正文在 turn 结束时一次性给出。
241
- · 多轮上下文:用**常驻会话**(SDK 通道下同一进程内对同一会话继续 prompt,官方推荐的续接方式):
242
- magic-agent -e dsh --stream --keep-alive "记住 42" # 首轮(dsh 的常驻默认关,要显式传)
243
- magic-agent --append <run_id|session_id> -p "刚才那个数是多少" # 追问,上下文接得上
244
- 不支持按 id 续接(-s/--session):headless 每次调用都是全新会话;SDK 的 sessionId
245
- 只在**同一个 runtime 进程内**可续,换进程拿旧 id 会被 -32603 "session already exists"
246
- 拒掉(协议只有 initialize / session/prompt / shutdown,没有 resume 方法)。
246
+ 模型由 dsh 自己的配置决定(0.1.7+ 在 $DSH_HOME/profiles/<profile>/cordis.patch.yml
247
+ 的 llm-pi-ai.providers + agent-default-model,更早在 $DSH_HOME/settings.yaml;
248
+ 密钥放 $DSH_HOME/.credentials.yaml 或 .env),此时会打一行告警。
249
+ · 工具调用与流式:SDK 通道(普通轮)与 headless + --json(续接轮 / 回退)都能上报
250
+ thinking / text / tool_use / tool_result;老版 dsh(headless 没有 --json)只能流
251
+ stderr 的 "dsh: reasoning:" 推理增量、正文收尾一次给出,**没有工具事件**。
252
+ · 多轮上下文(三种,按需选):
253
+ magic-agent -e dsh -s <session_id> -p "追问" # 真续接:dsh 自己恢复上下文
254
+ magic-agent -e dsh --stream --keep-alive "记住 42" # 常驻会话首轮(同进程内续接)
255
+ magic-agent --append <run_id|session_id> -p "刚才那个数" # 往常驻会话追加
256
+ -s/--session <id> 走 headless 的 --session-id:会话由 dsh 持久化在
257
+ $DSH_HOME/sessions/<cwd>/,上下文由 dsh 自己接回来(SDK 通道没有 resume 方法,
258
+ 所以续接轮由 headless + --json 承载)。⚠️ 会话按**工作区**分桶:换了 -w 就找不到
259
+ 那条会话,dsh 会明确报 does not exist。
247
260
  · --permission 未接线(dsh 的权限预设是配置层概念,无法表达四档里的「沙箱关闭」档)。
248
261
  · --tools 由 dsh 自己的工具循环决定(无命令行级逐工具开关),传入即忽略。
249
262
  · --json-schema 支持(走与 llm/openclaw 相同的输出后处理抽 JSON)。
@@ -323,7 +336,7 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
323
336
  追加进来的轮次照常以事件流输出(每轮收尾发一个 turn_end 事件,便于调用方知道「这轮完了」)。
324
337
  收工:最后一轮结束后 --idle(默认 5m)内没人追加 → 优雅退出;--idle 0 = 本轮结束就收工
325
338
  (追加窗口只在任务运行期间);也可随时 --stop。
326
- 常驻生效期间不套 -t 默认超时(长任务+追加不受 600s 限制),显式给 -t 才套。
339
+ 常驻生效期间不套 -t 默认超时(长任务+追加不受 1h 限制),显式给 -t 才套。
327
340
  不生效的形态会自动忽略(非 --stream 调用、不支持追加的引擎);显式传 --keep-alive 却
328
341
  不满足条件则明确报错(exit 2)。Windows 无 unix socket,暂不支持。
329
342
  注意:常驻进程会在最后一轮结束后继续存活 --idle 那么久 —— 调用方若要「一轮完成」
@@ -332,7 +345,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
332
345
  --stream 调用以为命令卡住了;要开就显式 --keep-alive。
333
346
 
334
347
  停止指定会话(--stop / --sessions):
335
- 每次调用都会往会话登记表落一条记录(~/.magic-agent/sessions/<run_id>.json,MAGIC_AGENT_SESSIONS 可改目录),
348
+ 开了 --save-registry 时,每次调用都会往会话登记表落一条记录
349
+ (~/.magic-agent/sessions/<run_id>.json,MAGIC_AGENT_SESSIONS 可改目录),
336
350
  里面记着**引擎子进程的 pid** 与引擎/模型/工作目录/会话 id。为什么需要它:
337
351
  · 引擎 CLI 在独立进程组里跑(Setpgid),调用方 kill 掉 magic-agent 的进程组**带不走它**;
338
352
  · 进程句柄只活在调用方内存里,应用重启后之前 detach 出去的会话就再也停不掉。
@@ -343,6 +357,34 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
343
357
  行为:先 SIGTERM,2s 内不退再 SIGKILL(连它的 node worker 一起带走),然后回写 state=stopped。
344
358
  幂等:进程已不在 / 会话已结束 → stopped=false + reason,退出码仍是 0;id 不存在 → 退出码 1(多半写错了)。
345
359
  另:magic-agent 自己收到 SIGINT/SIGTERM 时也会先带走自己的引擎子进程再退出,不留孤儿。
360
+ ⚠️ **默认不落盘**(用户 2026-10-12:「会话登记也不需要」):不带 --save-registry 时上面这些
361
+ 命令看不见本次调用 —— 默认不留痕是有意的(手工跑一轮 / CI / 脚本编排事后没人寻址)。
362
+ 代价:不留档的会话**停不掉**(pid 只存在记录里),--append 也只能按 run_id 走约定路径。
363
+
364
+ 删除会话(--delete-session <session_id|run_id>):
365
+ 登记记录 + 事件日志(<id>.jsonl)+ 标题覆盖一起删掉,**全机生效**:
366
+ 各端(PC 的 --sessions、移动端 H5 的 /desk/sessions)读的都是同一份登记表,
367
+ 一端删了处处都看不到。与插件的 DELETE /desk/session/{id} 是同一份实现
368
+ (internal/session/delete.go),两条路可互换。
369
+ magic-agent --delete-session <session_id> # 删掉这条会话(含历史)
370
+ 幂等:id 不认识 / 文件本来就不在 → 退出码 0,JSON 里 records_removed=0。
371
+ ⚠️ 是**物理删除**:删完历史就回放不了了。客户端把它当成「删除」的唯一实现,
372
+ 只在本地藏起来(记墓碑)的话,别的客户端照样列着(2026-10-19 用户报的
373
+ 「从pc端删除的会话移动端还在显示」就是这么来的)。
374
+
375
+ 会话历史存档(--save-history,默认关):
376
+ 会话的**内容**(每条事件)另存一份 ~/.magic-agent/sessions/<session_id>.jsonl,
377
+ --session-log / 客户端「看历史」读的就是它;只有 --stream 才写。**默认不写**:
378
+ 没人会回看,写下来却会一直堆在磁盘上(登记表有 24h 保留期在清,jsonl 没有)。
379
+ magic-agent -e claude --stream -p "跑一遍测试" # 不存档(默认)
380
+ magic-agent -e claude --stream --save-history -p "跑一遍测试" # 存档,事后可 --session-log 回看
381
+ magic-agent --session-log <session_id|run_id> # 读存档(没存档就是空数组)
382
+ ⚠️ 没存档的会话在客户端列表里 has_log=false —— 能看到、能续接,但点开没有历史内容。
383
+ magic-client 恒传 --save-registry 与 --save-history(客户端本来就要列会话 + 回放历史),
384
+ 所以界面里的会话既在列表里、也有历史。
385
+ ⚠️ **不等终点事件就落盘**(2026-10-15):一开跑就按 <run_id>.jsonl 写,引擎的
386
+ session_id 到手后改名为 <session_id>.jsonl(读侧两个名字都认)——
387
+ 于是「正在跑的会话」在别的客户端里当场可读,引擎整轮不给 id 的形态也不白开存档。
346
388
 
347
389
  附件(-a/--attach:文件+提示词,比如截图加提示词):
348
390
  每个引擎尽量用**自己的原生方式**收附件,能力见 --engines 的 attachments 字段:
@@ -354,7 +396,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
354
396
  arkclaw part:file A2A 原生 file part(base64 inline;远端看不到本机路径)
355
397
  trae prompt 无原生通道 → 把**绝对路径**写进提示词(需要 --tools 非 off)
356
398
  openclaw prompt 同上
357
- dsh prompt 同上(headless 无附件参数;dsh 自己的工具能读文件)
399
+ dsh sdk:content-blocks SDK 通道把图片作为原生 image 块发出(base64 inline,
400
+ 真进模型上下文);非图片附件 / headless 回退 → 路径写进提示词
358
401
  codebuddy-gateway
359
402
  prompt 走 payload.attachments 原生字段,但网关把它渲染成路径清单
360
403
  塞进 prompt(图不会以图片形式进模型上下文)
@@ -399,20 +442,38 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
399
442
  case flagChanged(cmd, "login"):
400
443
  // 用「传过没有」而不是「值非空」判断:`--login ""` 也要走参数校验(提示要引擎名)
401
444
  return runLogin(cmd, opts)
445
+ case flagChanged(cmd, "repair-models"):
446
+ // 同上:`--repair-models ""` 要走参数校验,而不是掉进「提问」分支报 empty prompt。
447
+ return runRepairModels(cmd, opts)
402
448
  case opts.listSessions:
403
449
  return runSessions(cmd, opts)
404
450
  case flagChanged(cmd, "session-log"):
405
451
  // 与 --stop 同理:用「传过没有」判断,`--session-log ""` 要走参数校验(报要 id),
406
452
  // 而不是掉进下面的「提问」分支报 empty prompt。
407
453
  return runSessionLog(cmd, opts)
454
+ case flagChanged(cmd, "summarize-title"):
455
+ // 同上:空 id 走参数校验,不要掉进「提问」分支。
456
+ return runSummarizeTitle(cmd, opts)
457
+ case flagChanged(cmd, "session-title"):
458
+ return runSessionTitle(cmd, opts)
459
+ case flagChanged(cmd, "title-force"):
460
+ // --title-force 只在 --summarize-title 下成立:单给它必然是调用方误解了
461
+ // (想要「强制重新归纳」却没给 id)。明说,不静默忽略。
462
+ return &usageError{fmt.Errorf("--title-force 只在 --summarize-title 下成立:请配合 --summarize-title <id> 使用")}
408
463
  case flagChanged(cmd, "after"):
409
464
  // --after 只在 --session-log 下成立:给了却没给 --session-log 就**明说**,
410
465
  // 不静默忽略(静默忽略是本项目反复踩的坑)。
411
466
  return &usageError{fmt.Errorf("--after 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
467
+ case flagChanged(cmd, "before"), flagChanged(cmd, "limit"):
468
+ // 同上:--before / --limit 是 --session-log 的翻页参数,单给必然是误解。
469
+ return &usageError{fmt.Errorf("--before / --limit 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
412
470
  case flagChanged(cmd, "stop"):
413
471
  // 用「传过没有」而不是「值非空」判断:`--stop ""` 也要走参数校验
414
472
  //(exit 2 提示要 id),而不是掉进「提问」分支报 empty prompt。
415
473
  return runStop(cmd, opts)
474
+ case flagChanged(cmd, "delete-session"):
475
+ // 同上:空 id 走参数校验(`--delete-session ""` 不该被当成提问)。
476
+ return runDeleteSession(cmd, opts)
416
477
  case flagChanged(cmd, "append"):
417
478
  return runAppend(cmd, args, opts)
418
479
  case opts.contract:
@@ -426,6 +487,28 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
426
487
  },
427
488
  }
428
489
  bindAskFlags(root, opts)
490
+ /* 唯一的子命令:`client start|stop|status`。
491
+ 为什么破例留一个子命令:它管的是**另一个常驻进程**(magic-client),
492
+ 生命周期与 core 的一次性调用完全不同,硬塞成一排 --client-* flags 会让
493
+ 「这些 flag 和提问无关」这件事看不出来(用户 2026-10-20:「提供启动停止 client 的子命令」)。 */
494
+ root.AddCommand(clientSubcommand())
495
+ /* `--login` / `--repair-models` 忘带引擎名时,cobra 在解析阶段就报错,**走不到**
496
+ runLogin / runRepairModels 的校验分支,默认文案是英文的
497
+ "flag needs an argument: ..."(且退出码 1)。这里翻成与传空串一致的中文
498
+ usage(exit 2),免得用户只看到一句看不懂的报错。 */
499
+ root.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {
500
+ if err == nil || !strings.Contains(err.Error(), "flag needs an argument") {
501
+ return err
502
+ }
503
+ switch {
504
+ case strings.Contains(err.Error(), "login"):
505
+ return &usageError{fmt.Errorf("--login 需要引擎名:magic-agent --login <engine>(可用:%s)", loginCapableEngines())}
506
+ case strings.Contains(err.Error(), "repair-models"):
507
+ return &usageError{fmt.Errorf("--repair-models 需要引擎名:magic-agent --repair-models <engine>(可用:%s)",
508
+ strings.Join(agent.DesktopEngines(), " / "))}
509
+ }
510
+ return err
511
+ })
429
512
  // 无子命令设计:禁用 cobra 自动注入的 completion 命令,
430
513
  // 保证 "magic-agent completion" 也只是被当作 prompt 而非隐藏子命令。
431
514
  root.CompletionOptions.DisableDefaultCmd = true
@@ -519,6 +602,12 @@ func rejectRemovedSubcommands(cmd *cobra.Command, args []string) error {
519
602
 
520
603
  // exitCodeOf 从错误推断退出码。
521
604
  func exitCodeOf(err error) int {
605
+ // 转发型子命令(`client`)自带退出码:2 = 参数错这种约定要原样带给调用方,
606
+ // 否则脚本拿到一律为 1 就分不出「用错了」和「跑失败了」。
607
+ var ec *exitCodeError
608
+ if errors.As(err, &ec) && ec.code > 0 {
609
+ return ec.code
610
+ }
522
611
  if _, ok := err.(*usageError); ok {
523
612
  return exitUsage
524
613
  }
@@ -539,6 +628,67 @@ var probeEngineCredits = func(ctx context.Context, l agent.ModelCreditLister) ma
539
628
  return l.ModelCredits(ctx)
540
629
  }
541
630
 
631
+ // modelListCacheTTL 模型清单 / 积分倍率探测结果的缓存有效期(30 分钟)。
632
+ //
633
+ // 模型清单来自 CLI 只读子命令或本地缓存文件,30 分钟内几乎不会变;缓存让重复跑
634
+ // --engines / --contract 不必每次重新启动 CLI 进程或重读文件。瞬时失败(CLI 没装 /
635
+ // 探测超时)与空清单**不进缓存**,避免把错误钉死 30 分钟 —— 只有真正拿到模型清单
636
+ // 才落缓存。缓存是进程级的:单次 magic-agent 进程只会探测一次,跨进程的重复调用
637
+ // (如客户端反复拉 --contract)也能省掉 CLI 启动开销。
638
+ const modelListCacheTTL = 30 * time.Minute
639
+
640
+ // modelListCacheEntry 一条缓存:模型清单 + 积分倍率 + 过期时间。
641
+ type modelListCacheEntry struct {
642
+ models []string
643
+ credits map[string]string
644
+ expiresAt time.Time
645
+ }
646
+
647
+ var (
648
+ modelListCacheMu sync.Mutex
649
+ modelListCache = map[string]*modelListCacheEntry{}
650
+ )
651
+
652
+ // cachedModelList 带 30 分钟缓存地探测某引擎的模型清单与积分倍率。
653
+ //
654
+ // key 一般用引擎名(codebuddy / codebuddy-ai / trae …);命中且未过期的缓存直接返回,
655
+ // 不触碰 CLI / 文件。底层走 probeEngineModels / probeEngineCredits(可被测试替换)。
656
+ // ListModels 出错或返回空清单时**不缓存**(视为瞬时态,下次重探)。
657
+ func cachedModelList(ctx context.Context, key string, l agent.ModelLister) (models []string, credits map[string]string, note string) {
658
+ modelListCacheMu.Lock()
659
+ if it, ok := modelListCache[key]; ok && time.Now().Before(it.expiresAt) {
660
+ modelListCacheMu.Unlock()
661
+ return it.models, it.credits, ""
662
+ }
663
+ modelListCacheMu.Unlock()
664
+
665
+ models, err := probeEngineModels(ctx, l)
666
+ if err != nil {
667
+ return nil, nil, err.Error()
668
+ }
669
+ if len(models) == 0 {
670
+ return nil, nil, "no models reported"
671
+ }
672
+ if cl, ok := l.(agent.ModelCreditLister); ok {
673
+ credits = probeEngineCredits(ctx, cl)
674
+ }
675
+ modelListCacheMu.Lock()
676
+ modelListCache[key] = &modelListCacheEntry{
677
+ models: models,
678
+ credits: credits,
679
+ expiresAt: time.Now().Add(modelListCacheTTL),
680
+ }
681
+ modelListCacheMu.Unlock()
682
+ return models, credits, ""
683
+ }
684
+
685
+ // clearModelListCache 清空模型清单缓存(测试用,避免用例之间互相命中缓存)。
686
+ func clearModelListCache() {
687
+ modelListCacheMu.Lock()
688
+ modelListCache = map[string]*modelListCacheEntry{}
689
+ modelListCacheMu.Unlock()
690
+ }
691
+
542
692
  // flagChanged 报告某个 flag 是否被显式传过(用于「传了就走这条分支」的开关型参数)。
543
693
  func flagChanged(cmd *cobra.Command, name string) bool {
544
694
  f := cmd.Flags().Lookup(name)
@@ -645,8 +795,10 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
645
795
  // stdin:stream-json claude / codebuddy 原生 stream-json input(图片走 content block)
646
796
  // flag:-a llm CLI 原生 --attachment(直连模型走 image_url content parts)
647
797
  // part:file arkclaw 走 A2A 原生 file part(base64 inline)
798
+ // sdk:content-blocks dsh 的 SDK 通道把图片作为原生 image content block 发出去
799
+ // (base64 inline,真进模型上下文);显式切 headless 后为 prompt
648
800
  // prompt 无原生通道:把绝对路径写进提示词,靠引擎读文件
649
- // (trae/openclaw/dsh/codebuddy-gateway)
801
+ // (trae/openclaw/codebuddy-gateway)
650
802
  Attachments string `json:"attachments,omitempty"`
651
803
  // Append 是否支持「常驻会话 + 追加消息」(`--stream --keep-alive` 与 `--append`)。
652
804
  // claude / codebuddy 为 true(stream-json 输入可持续喂 user 消息);
@@ -665,6 +817,12 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
665
817
  // 且网关会把远程任务的权限强制切到 bypassPermissions)
666
818
  // 调用方据此决定是否下发 --permission 与那几个沙箱/规则类 flags。
667
819
  Permission string `json:"permission,omitempty"`
820
+ // Reasoning 该引擎是否接受 `--reasoning <档>`(machine-readable,见
821
+ // agent.ReasoningSupportOf):claude / codebuddy / codebuddy-ai / codex 为 true;
822
+ // 其余引擎未接线 —— core 对它们**静默忽略**该 flag(不报错、不下传)。
823
+ // 新增(2026-10-20,只增字段不升 contractVersion):老版 CLI 没有这个字段,
824
+ // 客户端据此判断「能不能安全下发 --reasoning」(缺失 = 不下发,避免 exit 2)。
825
+ Reasoning bool `json:"reasoning"`
668
826
  // Capabilities 该引擎**静态声明**的能力 id 集合(字典序,稳定、无重复)。
669
827
  // 与运行态分开:ok / streaming / models / version 说的是「此刻如何」,
670
828
  // capabilities 说的是「能做什么」,二者的推导见 internal/agent/capability.go。
@@ -686,7 +844,8 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
686
844
  Attachments: agent.AttachmentSupportOf(e.Name()),
687
845
  Append: agent.AppendSupportOf(e.Name()),
688
846
  Ask: agent.AskSupportOf(e.Name()),
689
- Permission: agent.PermissionSupportOf(e.Name())}
847
+ Permission: agent.PermissionSupportOf(e.Name()),
848
+ Reasoning: agent.ReasoningSupportOf(e.Name())}
690
849
  if ok {
691
850
  r.Bin, r.Note = note, ""
692
851
  }
@@ -717,26 +876,30 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
717
876
  continue
718
877
  }
719
878
  wg.Add(1)
720
- go func(i int, l agent.ModelLister) {
879
+ go func(i int, name string, l agent.ModelLister) {
721
880
  defer wg.Done()
722
881
  ctx, cancel := context.WithTimeout(cmd.Context(), agent.DefaultModelProbeTimeout)
723
882
  defer cancel()
724
- models, err := probeEngineModels(ctx, lister)
883
+ models, credits, note := cachedModelList(ctx, name, l)
884
+ // codebuddy 系:展示时剥掉自定义模型 id 的 "custom-local:" 前缀(噪点);
885
+ // -m 实际传值时由引擎按裸名补回前缀(见 codebuddyCore.modelArg),
886
+ // 自定义模型依旧可用。
887
+ if name == "codebuddy" || name == "codebuddy-ai" {
888
+ models = agent.StripCustomLocalPrefix(models)
889
+ credits = agent.StripCustomLocalPrefixCredits(credits)
890
+ }
725
891
  switch {
726
- case err != nil:
727
- // 失败不致命:原因进 models_note,models 留空。
728
- rows[i].ModelsNote = err.Error()
892
+ case note != "":
893
+ rows[i].ModelsNote = note
729
894
  case len(models) == 0:
730
895
  rows[i].ModelsNote = "no models reported"
731
896
  default:
732
897
  rows[i].Models = models
733
- // 积分倍率与 models 同链同源(同一批缓存文件),探测失败/缺省
734
- // 不致命:该字段整体省略(omitempty),不影响 models。
735
- if cl, ok := l.(agent.ModelCreditLister); ok {
736
- rows[i].ModelCredits = probeEngineCredits(ctx, cl)
898
+ if credits != nil {
899
+ rows[i].ModelCredits = credits
737
900
  }
738
901
  }
739
- }(i, lister)
902
+ }(i, e.Name(), lister)
740
903
  }
741
904
  wg.Wait()
742
905
  if !envelope {