magic-agent 1.0.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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -0
  3. package/cmd/magic-agent/main.go +12 -0
  4. package/cmd/magic-client/main.go +144 -0
  5. package/go.mod +11 -0
  6. package/go.sum +12 -0
  7. package/internal/agent/arkclaw.go +587 -0
  8. package/internal/agent/arkclaw_stream.go +312 -0
  9. package/internal/agent/arkclaw_stream_test.go +428 -0
  10. package/internal/agent/arkclaw_test.go +827 -0
  11. package/internal/agent/ask.go +694 -0
  12. package/internal/agent/ask_test.go +587 -0
  13. package/internal/agent/attachments.go +200 -0
  14. package/internal/agent/attachments_test.go +481 -0
  15. package/internal/agent/capability.go +154 -0
  16. package/internal/agent/capability_test.go +190 -0
  17. package/internal/agent/claude.go +406 -0
  18. package/internal/agent/codebuddy.go +1044 -0
  19. package/internal/agent/codebuddy_account_test.go +128 -0
  20. package/internal/agent/codebuddy_gateway.go +479 -0
  21. package/internal/agent/codebuddy_gateway_stream.go +358 -0
  22. package/internal/agent/codebuddy_gateway_test.go +673 -0
  23. package/internal/agent/codex.go +599 -0
  24. package/internal/agent/codex_home.go +206 -0
  25. package/internal/agent/codex_home_test.go +130 -0
  26. package/internal/agent/codex_stream_test.go +97 -0
  27. package/internal/agent/codex_test.go +280 -0
  28. package/internal/agent/custom_models.go +197 -0
  29. package/internal/agent/custom_models_test.go +283 -0
  30. package/internal/agent/dsh.go +754 -0
  31. package/internal/agent/dsh_sdk.go +679 -0
  32. package/internal/agent/dsh_sdk_test.go +726 -0
  33. package/internal/agent/dsh_test.go +759 -0
  34. package/internal/agent/engine.go +469 -0
  35. package/internal/agent/engine_base.go +497 -0
  36. package/internal/agent/engine_base_test.go +308 -0
  37. package/internal/agent/engine_test.go +858 -0
  38. package/internal/agent/env.go +88 -0
  39. package/internal/agent/env_test.go +106 -0
  40. package/internal/agent/jsonextract.go +128 -0
  41. package/internal/agent/jsonextract_test.go +110 -0
  42. package/internal/agent/keepalive_test.go +216 -0
  43. package/internal/agent/llmengine.go +470 -0
  44. package/internal/agent/llmengine_test.go +551 -0
  45. package/internal/agent/llmhttp.go +437 -0
  46. package/internal/agent/llmhttp_test.go +304 -0
  47. package/internal/agent/llmmodels.go +205 -0
  48. package/internal/agent/login.go +27 -0
  49. package/internal/agent/main_test.go +23 -0
  50. package/internal/agent/models.go +686 -0
  51. package/internal/agent/models_test.go +877 -0
  52. package/internal/agent/openclaw.go +543 -0
  53. package/internal/agent/openclaw_acp.go +949 -0
  54. package/internal/agent/openclaw_acp_test.go +406 -0
  55. package/internal/agent/openclaw_test.go +451 -0
  56. package/internal/agent/output.go +191 -0
  57. package/internal/agent/permission.go +362 -0
  58. package/internal/agent/permission_test.go +526 -0
  59. package/internal/agent/prompt.go +65 -0
  60. package/internal/agent/runcmd.go +259 -0
  61. package/internal/agent/runcmd_test.go +81 -0
  62. package/internal/agent/runcmd_unix.go +56 -0
  63. package/internal/agent/runcmd_windows.go +66 -0
  64. package/internal/agent/runner.go +180 -0
  65. package/internal/agent/runner_test.go +263 -0
  66. package/internal/agent/spawnhook_test.go +96 -0
  67. package/internal/agent/stream.go +882 -0
  68. package/internal/agent/stream_test.go +565 -0
  69. package/internal/agent/streamjson.go +185 -0
  70. package/internal/agent/tags.go +52 -0
  71. package/internal/agent/toolkind.go +96 -0
  72. package/internal/agent/toolkind_test.go +70 -0
  73. package/internal/agent/tools_suffix_test.go +261 -0
  74. package/internal/agent/trae.go +342 -0
  75. package/internal/agent/workspace_test.go +166 -0
  76. package/internal/boundary/boundary_test.go +165 -0
  77. package/internal/cli/ask.go +1113 -0
  78. package/internal/cli/ask_test.go +1088 -0
  79. package/internal/cli/attach_test.go +239 -0
  80. package/internal/cli/contract_test.go +157 -0
  81. package/internal/cli/control.go +149 -0
  82. package/internal/cli/control_test.go +192 -0
  83. package/internal/cli/engines_test.go +205 -0
  84. package/internal/cli/events.go +198 -0
  85. package/internal/cli/events_test.go +166 -0
  86. package/internal/cli/keepalive_test.go +391 -0
  87. package/internal/cli/login.go +78 -0
  88. package/internal/cli/permission_test.go +295 -0
  89. package/internal/cli/prompt_source_test.go +222 -0
  90. package/internal/cli/root.go +752 -0
  91. package/internal/cli/session.go +486 -0
  92. package/internal/cli/session_test.go +202 -0
  93. package/internal/cli/sessionlog.go +325 -0
  94. package/internal/cli/sessionlog_command_test.go +204 -0
  95. package/internal/cli/sessionlog_test.go +262 -0
  96. package/internal/cli/sessionlog_user_test.go +129 -0
  97. package/internal/cli/toolkind_wire_test.go +84 -0
  98. package/internal/cli/workspace_test.go +188 -0
  99. package/internal/client/cors_test.go +136 -0
  100. package/internal/client/lan.go +68 -0
  101. package/internal/client/lan_test.go +53 -0
  102. package/internal/client/pair.go +163 -0
  103. package/internal/client/pair_test.go +109 -0
  104. package/internal/client/profile.go +184 -0
  105. package/internal/client/service.go +888 -0
  106. package/internal/client/service_test.go +654 -0
  107. package/internal/client/session_events_test.go +186 -0
  108. package/internal/client/session_messages_test.go +150 -0
  109. package/internal/config/config.go +439 -0
  110. package/internal/config/config_test.go +448 -0
  111. package/internal/session/session.go +447 -0
  112. package/internal/session/session_append.go +241 -0
  113. package/internal/session/session_append_test.go +161 -0
  114. package/internal/session/session_kill_unix_test.go +90 -0
  115. package/internal/session/session_mark_test.go +95 -0
  116. package/internal/session/session_test.go +237 -0
  117. package/internal/session/sessionlog.go +128 -0
  118. package/internal/session/sessionlog_read_test.go +157 -0
  119. package/npm/dist/darwin-arm64/magic-agent +0 -0
  120. package/npm/dist/darwin-x64/magic-agent +0 -0
  121. package/npm/dist/linux-arm64/magic-agent +0 -0
  122. package/npm/dist/linux-x64/magic-agent +0 -0
  123. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  124. package/npm/install.js +121 -0
  125. package/npm/lib/platform.js +78 -0
  126. package/package.json +36 -0
@@ -0,0 +1,752 @@
1
+ package cli
2
+
3
+ // root.go - magic-agent CLI 入口与全局 flags。
4
+ //
5
+ // 命令结构(无子命令,统一根命令直用):
6
+ //
7
+ // magic-agent [flags] [prompt...] 提问(唯一主流程)
8
+ // magic-agent --engines 列出引擎、可用性与各引擎支持的模型(JSON 数组)
9
+ // magic-agent --version 版本信息
10
+ //
11
+ // 关键 flags:
12
+ //
13
+ // -e, --engine <name> 引擎:claude | codebuddy | codebuddy-ai | codebuddy-gateway | trae | llm | codex | openclaw | dsh | arkclaw(默认 codebuddy)
14
+ // -m, --model <name> 模型(空 = 引擎默认;llm 引擎读 models.json 的 id)
15
+ // -s, --system <prompt> 系统提示词(空 = 用配置文件的 systemPrompt 默认值;值可为文件路径 / @文件)
16
+ // -p, --prompt <text> 提示词(值可为文件路径 / @文件 → 按文件内容用)
17
+ // -f, --file <path> 从文件读 prompt(- 读 stdin)
18
+ // -a, --attach <path> 附件(截图/图片),可重复/逗号分隔;与提示词一起发给引擎
19
+ // -w, --workspace <dir> 工作目录:在该目录里执行引擎(详见下方「工作目录」)
20
+ // -t, --timeout <dur> 单次尝试超时(如 3m;默认 600s)
21
+ // -r, --retries <n> 失败重试次数(默认 0)
22
+ // --backoff <dur> 首次重试退避(默认 2s,指数翻倍,上限 30s)
23
+ // -o, --output <format> 输出:json(默认)| text
24
+ // --tools <mode> off(默认)| on | 逗号分隔白名单
25
+ // --permission <tier> 四档权限档位:manual | accept-edits | auto | full
26
+ // (默认 full = 保持既有行为;仅 claude/codebuddy 生效)
27
+ // --sandbox-exclude 始终在沙箱外执行的命令(docker,watchman…)
28
+ // --sandbox-domain 沙箱网络白名单域名
29
+ // --auto-mode-env 第 3 档分类器的受信边界(自然语言)
30
+ // --permission-deny 追加 deny 规则(所有档位含 full 都生效)
31
+ // --permission-ask 追加 ask 规则(命中即强制人工审批)
32
+ // --max-tokens <n> 输出 token 上限(claude/codebuddy 经 --settings 注入,llm 透传 -o,trae 忽略)
33
+ // --temperature <t> 采样温度(仅 llm 引擎透传 -o temperature,其余忽略)
34
+ // --engines 列出引擎、可用性与支持的模型(JSON 数组;--json 兼容保留)
35
+ // --no-models 配合 --engines:跳过模型探测(只列引擎,快)
36
+ // --stop <id> 停止指定会话/运行(session_id 或 run_id),杀掉它的引擎进程组
37
+ // --sessions 列出会话登记表(JSON 数组:run_id / session_id / pid / engine / state)
38
+ // --keep-alive 常驻会话(claude/codebuddy 默认开,dsh 需显式传;需配合 --stream):
39
+ // 首轮结束后不退出、等 --append 追加;--keep-alive=false 关闭
40
+ // --append <id> 向常驻会话追加一条消息(内容用 -p/位置参数给)
41
+ // --idle <dur> 常驻会话空闲收工时长(默认 5m;0 = 本轮结束就收工)
42
+ // -v, --verbose 重试过程打到 stderr
43
+ //
44
+ // 退出码:0 成功;1 调用失败;2 参数/输入错误。
45
+ //
46
+ // 工作目录(-w/--workspace <dir>):让引擎在指定目录里干活。**尽量用各 CLI 的原生能力**:
47
+ //
48
+ // codex 原生 `-C/--cd <dir>`(working root)+ 子进程 cwd
49
+ // claude/codebuddy 无工作目录 flag(--add-dir 只追加额外可访问目录)→ 子进程 cwd
50
+ // trae 同上(有 --add-dir)
51
+ // dsh 同上(dsh 官方语义就是「调用时所在目录即默认 workspace 根」)
52
+ // openclaw **不支持**(workspace 与 agent 绑定;实测子进程 cwd 被忽略)
53
+ // llm / arkclaw **不支持**(无文件系统语义:llm 是 chat CLI;arkclaw 由网关按 claw_id 绑定)
54
+ // codebuddy-gateway **不支持**(网关在它自己的 cwd 里跑 agent,协议里没有工作目录字段)
55
+ // → 参数被忽略,且会打一行 stderr 提示,不静默
56
+ //
57
+ // 目录必须存在且是目录(否则 exit 2 报错)。机器可读的能力表见 `--engines` 的 workspace 字段
58
+ //(flag:-C / cwd / none)。
59
+
60
+ import (
61
+ "context"
62
+ "errors"
63
+ "fmt"
64
+ "os"
65
+ "strings"
66
+ "sync"
67
+
68
+ "github.com/spf13/cobra"
69
+
70
+ "github.com/darren/magic-agent/internal/agent"
71
+ )
72
+
73
+ // Version 版本号。
74
+ var Version = "0.1.0"
75
+
76
+ // NewRootCommand 构建 CLI 根命令(无子命令,全部走根命令 flags)。
77
+ //
78
+ // .magic-agent -p "问题" (-p/--prompt 提示词)
79
+ // magic-agent "问题" (位置参数同样有效)
80
+ // magic-agent -e codebuddy -m hy3 "问题"
81
+ // magic-agent --engines (列引擎,JSON 数组输出;--json 兼容)
82
+ //
83
+ // 注意 -p 在 claude 语境里是 --print,这里统一让位给 --prompt。
84
+ func NewRootCommand() *cobra.Command {
85
+ opts := newAskOptions()
86
+ root := &cobra.Command{
87
+ Use: "magic-agent [flags] [prompt...]",
88
+ Short: "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw / dsh / arkclaw 引擎",
89
+ Version: Version,
90
+ Long: `magic-agent - agent CLI 代理工具
91
+
92
+ 把 claude / codebuddy / trae 等 CLI 与 openclaw / dsh / arkclaw
93
+ 等 agent 后端的非交互调用统一成一条命令:支持切换引擎与模型、超时与
94
+ 重试、固定 text/json 输出格式、稳定退出码。适合脚本化编排与上层工具集成。
95
+
96
+ 引擎:
97
+ claude Claude Code CLI(-p --output-format json)
98
+ codebuddy CodeBuddy Code CLI(**独立安装**:npm i -g @tencent-ai/codebuddy-code;默认 hy3,可切 glm-5.3 等)
99
+ codebuddy-ai 同一个 CodeBuddy Code CLI 的**第二个账号**(默认不传 --model,交给 CLI 自选)
100
+ 两个引擎跑同一个二进制,靠 authentication.id 分开账号,互不顶号;
101
+ 登录:magic-agent --login codebuddy / --login codebuddy-ai
102
+ trae Trae CLI(使用 trae 自身配置的默认模型)
103
+ llm simonw/LLM CLI(模型与密钥由 llm models / llm keys 自管)
104
+ codex Codex CLI(使用 codex 自身配置的模型与凭据)
105
+ openclaw OpenClaw CLI(本机 openclaw 可执行文件)
106
+ dsh DeepSeek Harness(dsh --profile headless "<任务>";模型在 dsh 自己的配置里)
107
+ arkclaw ArkClaw A2A 网关(HTTP JSON-RPC message/send;凭据读配置文件)
108
+ codebuddy-gateway
109
+ CodeBuddy Code HTTP 网关(把 prompt 投给已在跑的 codebuddy --serve /
110
+ /gateway 远程控制,走 webhook 投递 + SSE 收流;凭据读配置文件,见下)
111
+
112
+ 示例:
113
+ magic-agent -p "用一句话解释什么是熵" # 直接提问(默认 json 输出)
114
+ magic-agent "问题" # 位置参数等价
115
+ magic-agent -e codebuddy "写一首俳句" # codebuddy 默认 hy3
116
+ magic-agent -e codebuddy -m glm-5.3 "写一首俳句"
117
+ magic-agent -e trae -t 10m "总结这篇文档" # 默认关工具;--tools on 可开
118
+ magic-agent -e claude --tools Bash,Read "看看这个目录" # 工具白名单
119
+ cat doc.md | magic-agent -e claude -f - "总结上文"
120
+ magic-agent -e claude -p ./prompt.md "补充一句" # 提示词给文件路径 → 读文件内容
121
+ magic-agent -e claude -p @notes/task.md # @ 强制按文件读(读不到直接报错)
122
+ magic-agent -e claude -s ./reviewer.md "审一下这段" # 系统提示词也可给文件
123
+ magic-agent -e claude -r 2 "1+1=?" # 失败重试 2 次
124
+ magic-agent --session <id> "接着刚才继续" # 会话续接(id 取上次输出 session_id)
125
+ magic-agent -c "接着刚才继续" # 续接最近一次会话(免传 id)
126
+ magic-agent -e llm -m minimax-m3 --max-tokens 32000 "写一集剧本"
127
+ magic-agent -e dsh "把 tests 跑一遍并总结失败原因" # DeepSeek Harness(headless)
128
+ magic-agent -e dsh -w ~/proj "重构这个模块" # dsh 的 workspace = 子进程 cwd
129
+ magic-agent --engines # 列出引擎、可用性与各引擎支持的模型(JSON 数组)
130
+ magic-agent --engines --no-models # 只列引擎与可用性(跳过模型探测,不启动 CLI)
131
+ magic-agent --engines --json # 等价(--json 为兼容保留)
132
+
133
+ 四档权限模型(--permission,仅 claude / codebuddy / codebuddy-ai 生效):
134
+ 把「哪些动作自动放行 + 放行不了时由谁裁决」收敛成四档,映射到 Claude Code 的参数:
135
+
136
+ manual 第1档 沙箱开启,只读放行,其余逐项由「用户」确认
137
+ → --permission-mode default
138
+ accept-edits 第2档 沙箱开启,工作区内编辑放行,命令仍由「用户」确认
139
+ → --permission-mode acceptEdits
140
+ auto 第3档 沙箱开启,沙箱内放行,越界交「LLM Guardian」判定
141
+ → --permission-mode auto
142
+ full 第4档 沙箱关闭,命令直接在宿主机执行,无审批
143
+ → --permission-mode bypassPermissions
144
+
145
+ **默认是 full**(保持既有行为:改造前 claude/codebuddy 开工具时恒传
146
+ --dangerously-skip-permissions / -y,语义正是第 4 档)。做 agent 任务时
147
+ 推荐显式传 --permission auto —— 沙箱 + LLM 判定,安全与效率兼顾。
148
+ 沙箱配置没有 CLI flag(claude 无 --sandbox),统一经 --settings 注入;
149
+ MaxTokens 的 env 注入与它在同一份 JSON 里合并(--settings 不是可重复 flag)。
150
+ 第 1~3 档默认写 failIfUnavailable=true(沙箱起不来就报错,不静默降级)与
151
+ allowUnsandboxedCommands=false(关闭逃逸舱口);需要放行的命令用 --sandbox-exclude 列出。
152
+
153
+ magic-agent -e claude --tools on --permission auto -p "重构这个函数"
154
+ magic-agent -e claude --tools on --permission accept-edits -p "改注释"
155
+ magic-agent -e claude --tools on --permission manual -p "先看看再动手"
156
+ magic-agent -e claude --tools on --permission auto \
157
+ --auto-mode-env "Source control: github.example.com/acme-corp" \
158
+ --permission-ask 'Bash(git push *)' -p "提交并推送" # 推送前强制人工检查点
159
+ magic-agent -e claude --tools on --permission full \
160
+ --permission-deny 'Bash(rm -rf *)' -p "清理临时文件" # 全权但保留硬红线
161
+ 说明:--permission 传给未接线的引擎(trae / llm / codex / openclaw / dsh / arkclaw)会
162
+ exit 2 明确报错,不会静默忽略 —— 静默忽略一个安全设置,等于让用户以为自己被保护着。
163
+ 机器可读的能力表见 --engines 每行的 permission 字段。
164
+
165
+ llm 引擎(包装 simonw/LLM CLI,模型/密钥由它自管):
166
+ magic-agent -e llm -m minimax-m3 "问题" # 按 llm CLI 注册名指定
167
+ magic-agent -e llm --max-tokens 32000 "问题" # 透传 -o max_tokens
168
+ magic-agent -e llm --session <id> "接着说" # --session → --cid 续接(id 取上轮输出 session_id)
169
+ magic-agent -e llm "问题" # 用 llm 的默认模型
170
+ magic-agent --engines # 看 llm 已注册的模型清单
171
+
172
+ 模型清单(--engines 的 models 字段,一律不硬编码):
173
+ 每个可用引擎的模型清单都从它自己的权威入口现取现算,CLI 升级/新注册模型后自动跟随:
174
+ claude ~/.claude/settings.json(顶层 model + env 里 ANTHROPIC_*MODEL[*_NAME])
175
+ codebuddy codebuddy --help 里 --model 自带的 "Currently supported: (...)" 清单
176
+ trae trae-cli models --json
177
+ llm llm models
178
+ codex codex debug models
179
+ openclaw openclaw models list --json
180
+ arkclaw 无清单(模型由网关按 claw_id 绑定)→ 输出 models_note 说明
181
+ dsh $DSH_HOME/settings.yaml 里已配置的模型(llm-pi-ai.providers.<route>.models,
182
+ 输出 route/model);headless 无 --model,换模型要改该文件
183
+ 探测会真的启动这些 CLI 的只读子命令(最慢 openclaw ~2.5s),故并发执行、
184
+ 单引擎上限 30s;拿不到清单不报错,原因写进该行的 models_note。
185
+ --no-models 整段跳过探测(只列引擎与可用性,不启动任何 CLI)。
186
+
187
+ 引擎不可用时给「一键安装命令」(--engines 的 install 字段,shell 一行,可直接执行):
188
+ claude npm install -g @anthropic-ai/claude-code
189
+ codex npm install -g @openai/codex
190
+ openclaw npm install -g openclaw@latest
191
+ dsh npm i -g @deepseek-ai/dsh
192
+ trae sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"
193
+ llm python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm
194
+ 命令一律取自各 CLI 的官方安装方式;**没有可执行安装路径的引擎不给该字段**
195
+ (codebuddy / codebuddy-ai 是桌面端 GUI 应用,arkclaw 靠配置文件),
196
+ 它们的「怎么才能用」写在同行的 note 里。
197
+ magic-agent --engines --no-models | jq -r '.[] | select(.ok|not) | .install'
198
+
199
+ 桌面 / 移动客户端(magic-client 插件)的契约面(定义见 internal/agent/capability.go):
200
+ --contract 输出带版本的 envelope:{"contractVersion":N,"engines":[...]},
201
+ engines 与 --engines 的行**完全同构**,且每行多一个 capabilities 字段
202
+ (静态能力 id,字典序)。capabilities 回答「能做什么」,ok / streaming /
203
+ models 回答「此刻如何」:客户端按前者定界面形态,按后者决定按钮可用与降级提示。
204
+ ⚠️ --contract 默认**不探测模型**(快);要连模型一起拿写 --no-models=false。
205
+ magic-agent --contract # 契约版本 + 各引擎能力(快)
206
+ magic-agent --contract | jq '.engines[] | {engine, capabilities}'
207
+ magic-agent --contract | jq -r '.contractVersion' # 客户端启动时先校验这个
208
+
209
+ 流式事件与控制通道(桌面 / 移动客户端据此驱动界面与回传动作):
210
+ --stream 正文 / 思考增量逐行 NDJSON(老形状,逐字节不变,老消费者继续用)
211
+ --stream --events 同上,但每行多 v / seq,并先发一行 ready —— **客户端消费形态**
212
+ --stream --control 从 stdin 读 NDJSON 控制命令,让调用方能打断与回审批:
213
+ {"op":"ping"} 探活 → pong
214
+ {"op":"interrupt"} 打断当前轮 → 本轮真的结束再发 interrupted
215
+ {"op":"stop"} 收工(关常驻会话)→ stopped
216
+ {"op":"answer","text":"<答案>"} 把答案作为下一轮 user 消息追进常驻会话 → answered
217
+ ⚠️ headless 下原地作答模型拿不到答案(见 agent.KindAsk)
218
+ ⚠️ 依赖常驻会话生效;不可用时回一条 control_error
219
+ magic-agent --stream --events --control -p "跑一遍测试" < cmds.ndjson
220
+ echo '{"op":"interrupt"}' | magic-agent --stream --events --control -p "长任务"
221
+
222
+ dsh 引擎(DeepSeek Harness,本机 dsh CLI):
223
+ 默认走 SDK 通道:dsh --profile sdk(stdio + 换行分帧 JSON-RPC,实时收会话事件);
224
+ 该 profile 不可用时自动回退 headless(dsh --profile headless "<任务>",stdout 只给最终正文)。
225
+ magic-agent -e dsh "把测试跑一遍并总结失败原因"
226
+ magic-agent -e dsh -w ~/proj "重构这个模块" # workspace = 子进程 cwd(dsh 原生语义)
227
+ magic-agent -e dsh -a shot.png "看截图报错" # 附件无原生通道 → 路径写进提示词
228
+ magic-agent -e dsh --stream "看看这个仓库" # 推理 / 正文 / 工具调用都实时出
229
+ MAGIC_AGENT_DSH_PROFILE=headless magic-agent -e dsh "..." # 强制回退通道(排障用)
230
+ magic-agent --engines # 看 dsh 是否已装(bin 探测)
231
+ 说明:
232
+ · 模型:SDK 通道下 -m 生效(转成 initialize 的 provider/model,取 route/model 形态,
233
+ 可用值见 --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 方法)。
247
+ · --permission 未接线(dsh 的权限预设是配置层概念,无法表达四档里的「沙箱关闭」档)。
248
+ · --tools 由 dsh 自己的工具循环决定(无命令行级逐工具开关),传入即忽略。
249
+ · --json-schema 支持(走与 llm/openclaw 相同的输出后处理抽 JSON)。
250
+ · CLI 路径解析:MAGIC_AGENT_DSH_BIN → 常见安装位 → PATH → npm 全局前缀
251
+ (npm i -g @deepseek-ai/dsh;最后一跳读 NPM_CONFIG_PREFIX,专治「装好了却报 not found」)。
252
+
253
+ arkclaw 引擎(A2A JSON-RPC 网关,不走本机 CLI;凭据放配置文件):
254
+ 配置文件 ~/.config/magic-agent/config.json(路径可用 MAGIC_AGENT_CONFIG 覆盖):
255
+ {
256
+ "systemPrompt": "你是一个中文助手,始终用中文回答所有问题。",
257
+ "arkclaw": {
258
+ "url": "https://<host>/a2a/jsonrpc",
259
+ "key": "<apikey>",
260
+ "claw_id": "ci-xxxxxxxx"
261
+ }
262
+ }
263
+ 环境变量可覆盖文件值:MAGIC_AGENT_ARKCLAW_URL / MAGIC_AGENT_ARKCLAW_KEY / MAGIC_AGENT_ARKCLAW_CLAW_ID
264
+ magic-agent -e arkclaw "问题" # 单轮 message/send
265
+ magic-agent -e arkclaw --session <ctx> "接着说" # 按 contextId 续接同一上下文
266
+ magic-agent -e arkclaw --stream "问题" # 流式(A2A SSE;见下)
267
+ magic-agent --engines # 看 arkclaw 是否已配置齐备
268
+
269
+ 说明:--stream 走 A2A 官方的 message/stream(SSE),但**网关不吐逐字增量** —— 实测只有
270
+ 「受理帧 + 整段正文」两帧,正文仍一次性到达(要逐字得 claw 侧改);网关没按 SSE 回时
271
+ 自动退回一次性响应。不要改成 WebSocket:实测网关不回 101 升级响应,且 A2A 规范里
272
+ WS 属自定义绑定、火山网关也没有 WS API 类型。
273
+ 不支持 -c/--continue(A2A 无「查询最近上下文」接口,请显式传 --session);
274
+ --tools / --max-tokens / --temperature 由网关侧决定,静默忽略;
275
+ --json-schema 支持(走与 llm 相同的输出后处理抽 JSON)。
276
+
277
+ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭据放配置文件):
278
+ 接的是**已经在跑的** CodeBuddy Code 网关 —— codebuddy --serve(或交互会话里的 /gateway
279
+ 远程控制)暴露的那套 HTTP 服务。magic-agent 把 prompt 以 webhook 投递进去,再收 SSE 增量。
280
+ 为什么值得单开一个引擎:复用那个**已经登录、已经配好模型与 MCP** 的网关进程(不必每次
281
+ spawn 一个 CLI),并且执行发生在网关那台机器上(Tunnel 形态下网关可以在另一台机器/容器)。
282
+ 配置文件(同 arkclaw 那节所在文件):
283
+ {
284
+ "codebuddyGateway": {
285
+ "url": "http://127.0.0.1:8399"
286
+ }
287
+ }
288
+ url = 网关根地址(不带 /api/v1);--auth password 的网关再给 password(启动日志里打印的
289
+ 那个口令)。节名别名:codebuddyGateway | codebuddy_gateway | codebuddy-gateway | cbgw;
290
+ 字段别名:url|endpoint、password|token|key|secret。环境变量覆盖:
291
+ MAGIC_AGENT_CBGW_URL / MAGIC_AGENT_CBGW_PASSWORD。
292
+ magic-agent -e codebuddy-gateway "问题" # 单轮
293
+ magic-agent -e codebuddy-gateway --session conv-1 "接着说" # 按 conversation id 续接
294
+ magic-agent -e codebuddy-gateway --stream -o text "问题" # 流式(见下方「成色」说明)
295
+ magic-agent --engines | jq '.[] | select(.engine=="codebuddy-gateway")'
296
+
297
+ 说明:网关是「受理即返回」的异步模型 —— 投递只回 202 + runId,**正文只在 SSE 上出现**,
298
+ 所以非流式(不带 --stream)也读 SSE,只是不往外发增量。
299
+ --session <conversation_id> 就是网关的会话锚点,多轮上下文接得上;不传则每次新会话
300
+ (输出里的 session_id 给的就是这个锚点,不是网关内部的 session UUID)。
301
+ 不支持 -c/--continue(网关无「查询最近会话」接口,显式报错)。
302
+ -m/--model 不生效(模型由网关进程自己的配置决定,会打一行告警);
303
+ --tools / --permission / -w 同样没有落地通道(由网关进程的 --agent /
304
+ --permission-mode / 启动目录决定)→ 能力表报 none,传了会在 CLI 层提示或报错。
305
+ -a/--attach 走 payload.attachments 原生字段,但网关把它渲染成「附件清单 + 路径」
306
+ 塞进 prompt(图不会以图片形式进模型上下文)→ 能力表如实报 prompt。
307
+ ⚠️ --stream 的成色:实测(2026-09-24)该网关只推「终帧(整段正文)」,**不发增量帧**
308
+ —— 所以它和 arkclaw 一样是「整段到达」,不是打字机。增量帧的解析已按协议实现,
309
+ 网关将来吐 content.chunk 时客户端零改动即可接住。
310
+ 网关没按 SSE 回(老版本)时如实报错,不假装有增量。
311
+
312
+ 常驻会话与追加需求(--append;claude/codebuddy 默认开,dsh 需显式 --keep-alive):
313
+ 引擎能在**同一个进程**里接着收 user 消息,所以:
314
+ magic-agent -e claude --stream -p "先把仓库跑一遍测试" -o text # 默认就是常驻会话
315
+ magic-agent -e dsh --stream --keep-alive -p "记住 42" -o text # dsh 要显式开
316
+ magic-agent --append <run_id|session_id> -p "追加:顺便把 lint 也跑了" # 任务跑着时随时追加
317
+ magic-agent -e claude --stream --keep-alive=false -p "..." # 关掉常驻(turn 结束即退出)
318
+ 两条落地通道(CLI 侧同一套 --append 入口):
319
+ claude/codebuddy --input-format stream-json 的 stdin 持续喂 user 消息;
320
+ dsh SDK 通道下对同一 sessionId 继续 session/prompt(官方推荐的续接方式)
321
+ 常驻会话会开一个 unix socket 追加入口(路径写在会话登记表的 append 字段里,权限 0600),
322
+ --append 连上去把消息交给它,再由它写进引擎 stdin(dsh 则是排进同一会话)成为下一轮。
323
+ 追加进来的轮次照常以事件流输出(每轮收尾发一个 turn_end 事件,便于调用方知道「这轮完了」)。
324
+ 收工:最后一轮结束后 --idle(默认 5m)内没人追加 → 优雅退出;--idle 0 = 本轮结束就收工
325
+ (追加窗口只在任务运行期间);也可随时 --stop。
326
+ 常驻生效期间不套 -t 默认超时(长任务+追加不受 600s 限制),显式给 -t 才套。
327
+ 不生效的形态会自动忽略(非 --stream 调用、不支持追加的引擎);显式传 --keep-alive 却
328
+ 不满足条件则明确报错(exit 2)。Windows 无 unix socket,暂不支持。
329
+ 注意:常驻进程会在最后一轮结束后继续存活 --idle 那么久 —— 调用方若要「一轮完成」
330
+ 的信号,请读事件流里的 turn_end(而不是等进程退出),或传 --keep-alive=false。
331
+ dsh 的常驻**默认关**:它的 SDK 通道本来是一次一轮的形态,默认挂 5 分钟空闲窗口会让既有
332
+ --stream 调用以为命令卡住了;要开就显式 --keep-alive。
333
+
334
+ 停止指定会话(--stop / --sessions):
335
+ 每次调用都会往会话登记表落一条记录(~/.magic-agent/sessions/<run_id>.json,MAGIC_AGENT_SESSIONS 可改目录),
336
+ 里面记着**引擎子进程的 pid** 与引擎/模型/工作目录/会话 id。为什么需要它:
337
+ · 引擎 CLI 在独立进程组里跑(Setpgid),调用方 kill 掉 magic-agent 的进程组**带不走它**;
338
+ · 进程句柄只活在调用方内存里,应用重启后之前 detach 出去的会话就再也停不掉。
339
+ 有了记录,任何进程都能按会话停:
340
+ magic-agent --sessions # 列出全部(run_id / session_id / pid / engine / state)
341
+ magic-agent --stop <session_id> # 按会话 id 停(续接场景最常用)
342
+ magic-agent --stop <run_id> # 按运行 id 停(新会话还没拿到 session_id 时)
343
+ 行为:先 SIGTERM,2s 内不退再 SIGKILL(连它的 node worker 一起带走),然后回写 state=stopped。
344
+ 幂等:进程已不在 / 会话已结束 → stopped=false + reason,退出码仍是 0;id 不存在 → 退出码 1(多半写错了)。
345
+ 另:magic-agent 自己收到 SIGINT/SIGTERM 时也会先带走自己的引擎子进程再退出,不留孤儿。
346
+
347
+ 附件(-a/--attach:文件+提示词,比如截图加提示词):
348
+ 每个引擎尽量用**自己的原生方式**收附件,能力见 --engines 的 attachments 字段:
349
+ codex flag:-i 原生 -i/--image(图片;resume 轮退化为提示词里的路径)
350
+ claude stdin:stream-json 原生 --input-format stream-json,图片作为 content block 走 stdin
351
+ codebuddy stdin:stream-json 同上(同族 CLI)
352
+ llm flag:-a 直连走 OpenAI content parts(image_url data URL);
353
+ 委托 llm CLI 时走原生 -a/--attachment
354
+ arkclaw part:file A2A 原生 file part(base64 inline;远端看不到本机路径)
355
+ trae prompt 无原生通道 → 把**绝对路径**写进提示词(需要 --tools 非 off)
356
+ openclaw prompt 同上
357
+ dsh prompt 同上(headless 无附件参数;dsh 自己的工具能读文件)
358
+ codebuddy-gateway
359
+ prompt 走 payload.attachments 原生字段,但网关把它渲染成路径清单
360
+ 塞进 prompt(图不会以图片形式进模型上下文)
361
+ 路径在 CLI 层校验(存在 + 普通文件 + 32MB 上限),出错 exit 2;附件与提示词是并列关系,
362
+ 提示词本身照旧(不会因为给了附件就被改写)。图片按**魔数**识别(不信扩展名),
363
+ 非图片附件在没有原生通道的引擎上同样退化为路径。
364
+ magic-agent -p "这张图什么颜色" -a ~/Desktop/shot.png
365
+ magic-agent -p "对比这两张图" -a a.png -a b.png
366
+ magic-agent -p "看截图报错" --tools on -a shot.png -o text
367
+
368
+ 提示词可以是文件路径(-p / 位置参数 / -s 都适用):
369
+ 值恰好命中一个**已存在的普通文件** → 按文件内容作为提示词,并打一行 stderr 提示(不静默);
370
+ 写 "@<path>" 则**强制**按文件读(读不到 / 不是普通文件 → exit 2 报错)。
371
+ 自动识别很保守:单行、长度 ≤ 4096 字节、stat 出来是普通文件(目录不算),
372
+ 所以 -p "解释一下 README.md"、多行提示词都不受影响;-f/--file 仍是不加猜测的显式写法。
373
+ magic-agent -p ./prompt.md "补充一句" # 文件内容在前,追加内容在后
374
+ magic-agent ./tasks/task.md # 位置参数同样支持
375
+ magic-agent -p @notes/task.md # 强制按文件读(不存在就报错)
376
+ magic-agent -s ./reviewer.md "审一下" # 系统提示词也一样(含配置里的 systemPrompt)
377
+
378
+ 默认系统提示词(同一份配置文件,对所有引擎生效):
379
+ "systemPrompt" 是**全局默认**:调用时没给 -s/--system 就用它,给了则 -s 优先。
380
+ 键名宽松兼容(systemPrompt / system_prompt / system 任选),取值可用环境变量
381
+ MAGIC_AGENT_SYSTEM_PROMPT 临时覆盖;未配置 → 不注入,行为与以前完全一致。
382
+ magic-agent "1+1=?" # 用配置里的默认 systemPrompt
383
+ magic-agent -s "只输出译文" "Hello" # -s 优先,默认值本轮不生效`,
384
+ SilenceUsage: true,
385
+ SilenceErrors: true,
386
+ Args: rejectRemovedSubcommands,
387
+ // 禁用 cobra 内置 completion 子命令:本 CLI 无子命令形态。
388
+ CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
389
+ RunE: func(cmd *cobra.Command, args []string) error {
390
+ /* 只在流式下成立的开关:给了却不在流式路径上就报错,**不静默忽略**
391
+ —— 静默忽略是本项目反复踩过的坑(见 README「-t 600s 被静默忽略」那条)。 */
392
+ if !opts.stream && (opts.events || opts.control) {
393
+ return &usageError{fmt.Errorf("--events / --control 只在流式下成立:请配合 --stream 使用")}
394
+ }
395
+ if opts.control && opts.file == "-" {
396
+ return &usageError{fmt.Errorf("--control 与「从 stdin 读提示词(-f -)」冲突:stdin 已被控制通道占用")}
397
+ }
398
+ switch {
399
+ case flagChanged(cmd, "login"):
400
+ // 用「传过没有」而不是「值非空」判断:`--login ""` 也要走参数校验(提示要引擎名)
401
+ return runLogin(cmd, opts)
402
+ case opts.listSessions:
403
+ return runSessions(cmd, opts)
404
+ case flagChanged(cmd, "session-log"):
405
+ // 与 --stop 同理:用「传过没有」判断,`--session-log ""` 要走参数校验(报要 id),
406
+ // 而不是掉进下面的「提问」分支报 empty prompt。
407
+ return runSessionLog(cmd, opts)
408
+ case flagChanged(cmd, "after"):
409
+ // --after 只在 --session-log 下成立:给了却没给 --session-log 就**明说**,
410
+ // 不静默忽略(静默忽略是本项目反复踩的坑)。
411
+ return &usageError{fmt.Errorf("--after 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
412
+ case flagChanged(cmd, "stop"):
413
+ // 用「传过没有」而不是「值非空」判断:`--stop ""` 也要走参数校验
414
+ //(exit 2 提示要 id),而不是掉进「提问」分支报 empty prompt。
415
+ return runStop(cmd, opts)
416
+ case flagChanged(cmd, "append"):
417
+ return runAppend(cmd, args, opts)
418
+ case opts.contract:
419
+ return runContract(cmd, opts)
420
+ case opts.engines:
421
+ return runEngines(cmd, opts)
422
+ case opts.stream:
423
+ return runStreamAsk(cmd, args, opts)
424
+ }
425
+ return runAsk(cmd, args, opts)
426
+ },
427
+ }
428
+ bindAskFlags(root, opts)
429
+ // 无子命令设计:禁用 cobra 自动注入的 completion 命令,
430
+ // 保证 "magic-agent completion" 也只是被当作 prompt 而非隐藏子命令。
431
+ root.CompletionOptions.DisableDefaultCmd = true
432
+ // --version 输出固定 "magic-agent <ver>",不带 cobra 默认前缀。
433
+ root.SetVersionTemplate("{{.Name}} {{.Version}}\n")
434
+ return root
435
+ }
436
+
437
+ // Execute 运行 CLI,返回进程退出码。
438
+ //
439
+ // 错误输出约定(stderr,与 -o 联动):
440
+ // - 引擎执行失败:runAsk 已用 WriteError 输出(reportedError 标记),此处跳过
441
+ // - 其余错误(参数错 / 引擎预检失败 / 子命令形态):此处统一输出 ——
442
+ // -o json 时输出同结构 envelope(attempts=0,engine 为 -e 原值),
443
+ // text 时一行 "magic-agent: <err>"。stdout 恒不产生半截内容。
444
+ func Execute() int {
445
+ // 被 SIGINT/SIGTERM 终止时先带走引擎子进程(它在自己的进程组里,调用方杀不到)。
446
+ installSignalStop()
447
+ root := NewRootCommand()
448
+ err := root.Execute()
449
+ if err == nil {
450
+ return exitOK
451
+ }
452
+ // 已由 WriteError 格式化输出过(json envelope / text 行)。
453
+ if errors.Is(err, errReportedSentinel) {
454
+ return exitCodeOf(err)
455
+ }
456
+
457
+ // 按 -o 的实际生效值(含默认)决定 stderr 格式。
458
+ format := agent.FormatJSON
459
+ if f := root.PersistentFlags().Lookup("output"); f != nil && f.Value.String() != "" {
460
+ if v, perr := agent.ParseFormat(f.Value.String()); perr == nil {
461
+ format = v
462
+ }
463
+ }
464
+ engineName := ""
465
+ if f := root.PersistentFlags().Lookup("engine"); f != nil {
466
+ engineName = f.Value.String()
467
+ }
468
+ // 参数类错误尚未执行任何尝试;attempts=0 便于调用方区分阶段。
469
+ _ = agent.WriteError(os.Stderr, format, engineName, 0, err)
470
+ return exitCodeOf(err)
471
+ }
472
+
473
+ // errReportedSentinel reportedError 的内部标记(errors.Is 用)。
474
+ var errReportedSentinel = errors.New("already reported")
475
+
476
+ // reportedError 包装"已输出过失败信息"的错误:
477
+ // WriteError 已把 envelope/文本写到 stderr,Execute 不再打印第二遍。
478
+ type reportedError struct{ err error }
479
+
480
+ func (r *reportedError) Error() string { return r.err.Error() }
481
+
482
+ // Is 让 errors.Is(err, errReportedSentinel) 命中。
483
+ func (r *reportedError) Is(target error) bool { return target == errReportedSentinel }
484
+
485
+ // Unwrap 保留退出码推断(usageError 判定)沿链下钻。
486
+ func (r *reportedError) Unwrap() error { return r.err }
487
+
488
+ // 退出码约定。
489
+ const (
490
+ exitOK = 0
491
+ exitFail = 1
492
+ exitUsage = 2
493
+ )
494
+
495
+ // usageError 标记参数/输入错误(exit 2)。
496
+ type usageError struct{ err error }
497
+
498
+ func (u *usageError) Error() string { return u.err.Error() }
499
+
500
+ // removedSubcommands 已删除的子命令名(v0.1 曾有 ask/engines/version
501
+ // 子命令形态;现统一为根命令 flags)。出现在首位参数时直接拒绝,
502
+ // 避免被误当成 prompt 发给引擎。
503
+ var removedSubcommands = map[string]string{
504
+ "ask": "--prompt / 位置参数",
505
+ "engines": "--engines",
506
+ "version": "--version",
507
+ "completion": "(no shell completion)",
508
+ }
509
+
510
+ // rejectRemovedSubcommands 首个位置参数命中已删子命令名时报 usage 错误。
511
+ func rejectRemovedSubcommands(cmd *cobra.Command, args []string) error {
512
+ if len(args) > 0 {
513
+ if hint, ok := removedSubcommands[strings.ToLower(args[0])]; ok {
514
+ return &usageError{fmt.Errorf("unknown command %q: subcommands were removed, use %s instead", args[0], hint)}
515
+ }
516
+ }
517
+ return cobra.ArbitraryArgs(cmd, args)
518
+ }
519
+
520
+ // exitCodeOf 从错误推断退出码。
521
+ func exitCodeOf(err error) int {
522
+ if _, ok := err.(*usageError); ok {
523
+ return exitUsage
524
+ }
525
+ return exitFail
526
+ }
527
+
528
+ // probeEngineModels 单引擎模型探测的入口。
529
+ //
530
+ // 提为包级变量只为测试替换:单元测试不应该真的去启动用户机器上的 CLI
531
+ // (各引擎探测命令的正确性在 internal/agent 用假 CLI 覆盖)。
532
+ // 生产路径就是 lister.ListModels(ctx)。
533
+ var probeEngineModels = func(ctx context.Context, l agent.ModelLister) ([]string, error) {
534
+ return l.ListModels(ctx)
535
+ }
536
+
537
+ // probeEngineCredits 单引擎积分倍率探测的入口(与 probeEngineModels 同理,可测替换)。
538
+ var probeEngineCredits = func(ctx context.Context, l agent.ModelCreditLister) map[string]string {
539
+ return l.ModelCredits(ctx)
540
+ }
541
+
542
+ // flagChanged 报告某个 flag 是否被显式传过(用于「传了就走这条分支」的开关型参数)。
543
+ func flagChanged(cmd *cobra.Command, name string) bool {
544
+ f := cmd.Flags().Lookup(name)
545
+ return f != nil && f.Changed
546
+ }
547
+
548
+ // runEngines 列出引擎、CLI 探测结果与各引擎当前支持的模型(原 engines 子命令的 flag 形态)。
549
+ //
550
+ // 固定输出单行 JSON 数组(jq 友好),每项:
551
+ //
552
+ // engine 引擎名
553
+ // ok 引擎是否可用
554
+ // bin 可用时给二进制路径(无本机 CLI 的引擎给端点 URL)
555
+ // note 不可用原因
556
+ // install 不可用时给「一键安装命令」(shell 一行,可直接执行);没有可执行安装
557
+ // 路径的引擎(codebuddy/codebuddy-ai 是 GUI 应用、arkclaw 靠配置文件)不给
558
+ // models 该引擎当前支持的模型标识(动态探测,见 agent.ModelLister)
559
+ // models_note 拿不到 models 时的原因(无动态来源 / 探测失败)
560
+ // model_credits 各模型积分倍率(model → "0.34";仅实现 agent.ModelCreditLister
561
+ // 的引擎给出,与 models 同源同链;缺省省略该字段)
562
+ // workspace 该引擎「指定工作目录」的落地方式:flag:-C(codex 原生)/ cwd(子进程 cwd)/
563
+ // none(llm、arkclaw、openclaw 不支持,-w 会被忽略)
564
+ // streaming 该引擎是否支持 `--stream`(false = 调用方应改用非流式)。9 个引擎都是 true,
565
+ // 但成色不同:dsh 只流**推理增量**(正文仍一次性给出);
566
+ // openclaw 走 `openclaw acp`(逐字正文增量 + 工具事件,⚠️ 多一个外部依赖
567
+ // 「Gateway 在跑」——桥起不来时该轮回退非流式、且该字段会在冷却期内变 false,
568
+ // 见 internal/agent/openclaw_acp.go);
569
+ // arkclaw 走 A2A 官方 SSE(`message/stream`),⚠️ 但网关不吐逐字增量,
570
+ // 实测只有「受理帧 + 整段正文」,见 internal/agent/arkclaw_stream.go
571
+ //
572
+ // 模型**一律现取现算、不硬编码**:各引擎用自己 CLI/配置文件里的权威入口
573
+ // (trae-cli models / llm models / codex debug models / openclaw models list /
574
+ // codebuddy --help / claude settings.json)。探测要真的启动 CLI(只读子命令),
575
+ // 故并发执行并套 agent.DefaultModelProbeTimeout;--no-models 可整段跳过(只列引擎,快)。
576
+ // --json 为兼容旧调用保留(本函数不再区分,恒为 JSON)。
577
+ func runEngines(cmd *cobra.Command, opts *askOptions) error {
578
+ return emitEngineRows(cmd, opts, false)
579
+ }
580
+
581
+ // runContract 输出带版本的契约 envelope(契约版本 + 引擎数组)。
582
+ //
583
+ // 客户端启动时读它做契约校验:版本超出自己支持的范围就明确报错,而不是带着
584
+ // 不匹配的能力继续跑 —— 「core 加了字段、客户端按旧字段解析」这类漂移在界面上
585
+ // 表现为空白,是这类架构最常见的事故(本项目已踩过两次同类问题)。
586
+ //
587
+ // ⚠️ 默认**不探测模型**:契约只描述「引擎能做什么」,模型清单属于运行态,且每个
588
+ // 引擎要起一次真 CLI 进程(10 个引擎约 30s 级)。要连模型一起拿,显式写
589
+ // `--contract --no-models=false`。
590
+ func runContract(cmd *cobra.Command, opts *askOptions) error {
591
+ o := *opts
592
+ if !flagChanged(cmd, "no-models") {
593
+ o.noModels = true
594
+ }
595
+ return emitEngineRows(cmd, &o, true)
596
+ }
597
+
598
+ // emitEngineRows 构建并输出引擎行 —— `--engines` 与 `--contract` 的唯一实现。
599
+ // envelope=true 时外层套一层带 contractVersion 的结构。
600
+ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
601
+ engines := agent.Engines()
602
+ type row struct {
603
+ Engine string `json:"engine"`
604
+ OK bool `json:"ok"`
605
+ Bin string `json:"bin"`
606
+ Note string `json:"note,omitempty"`
607
+ // Install 该引擎的「一键安装 / 升级命令」(shell 一行,可直接执行)。
608
+ // ⚠️ 2026-09-23 起**不再只给不可用的行**(用户:「引擎检测除了 a2a 的 其他也要支持有升级」):
609
+ // 各引擎的安装命令本身就是「装最新版」(`npm i -g <pkg>` / 官方 curl 脚本 / pip),
610
+ // 对**已装好的引擎重跑一遍 = 升级到最新版**,所以可用时也照给。
611
+ // 调用方据此决定:ok:false → 按钮叫「安装」;ok:true → 按钮叫「升级」。
612
+ // 没有可执行安装路径的引擎(codebuddy/codebuddy-ai 是桌面端 GUI 应用、
613
+ // arkclaw / 具名 A2A agent 靠配置文件)**不给**该字段 —— 原因在 note 里,
614
+ // 不编一条跑不通的命令(见 agent.InstallCommandOf)。
615
+ Install string `json:"install,omitempty"`
616
+ // Version 该引擎 CLI 的**当前版本**(尽力而为;读不到就省略)。
617
+ // 用途:让「升级」可验证 —— 调用方跑完升级命令重探一次,比对新旧版本号,
618
+ // 如实说「已升级:旧 → 新」或「版本未变(多半已是最新)」。
619
+ // 读法见 agent.EngineVersionOf(跑 `<bin> --version`,取第一个 x.y.z)。
620
+ // 两类引擎天然没有:A2A 网关(本机没二进制)与桌面端 GUI 应用(CLI 无 --version)。
621
+ Version string `json:"version,omitempty"`
622
+ Models []string `json:"models,omitempty"`
623
+ ModelsNote string `json:"models_note,omitempty"`
624
+ // ModelCredits 各模型的积分倍率(machine-readable,model → 倍率数字字符串,
625
+ // 如 "fast-model":"0.34",源于客户端 "x0.34 credits";与 models 同源同链探测,
626
+ // 仅实现了 agent.ModelCreditLister 的引擎给出 —— 目前 codebuddy / codebuddy-ai)。
627
+ // 客户端不给倍率的模型(custom-local)不出现在 map 里。
628
+ ModelCredits map[string]string `json:"model_credits,omitempty"`
629
+ // Workspace 该引擎「指定工作目录」的落地方式(machine-readable,供调用方决定是否下发):
630
+ // flag:-C 原生 flag(codex);cwd 子进程 cwd(claude/codebuddy/trae);
631
+ // none 无落地通道(llm/arkclaw/openclaw/codebuddy-gateway,参数会被忽略)
632
+ Workspace string `json:"workspace,omitempty"`
633
+ // Streaming 是否实现 Streamer(即 `--stream` 可用)。false 时调用方应改用非流式
634
+ //(观物台的 desk:ask 据此决定要不要加 --stream)。当前 10 个引擎都实现了 Streamer,
635
+ // 但「流式」的成色不同:dsh 只有 thinking 增量、正文收尾一次性给出;
636
+ // arkclaw 走 A2A 官方 SSE,实测网关也只发「受理帧 + 整段正文」(见 agent/arkclaw_stream.go);
637
+ // codebuddy-gateway 同样走 SSE,但它同时是**非流式路径的唯一取文通道**
638
+ //(投递响应里没有正文)。⚠️ 实测该网关目前只推终帧、不发增量帧 ——
639
+ // 成色与 arkclaw 一样是「整段到达」(见 agent/codebuddy_gateway_stream.go);
640
+ // openclaw 的 ACP 桥不可用(Gateway 没起 / scope 未批)时这里会临时变 false(冷却期内)。
641
+ Streaming bool `json:"streaming"`
642
+ // Attachments 该引擎「收附件(截图等)」的落地方式(machine-readable,供调用方决定
643
+ // 是否下发 -a/--attach,以及要不要提示用户降级):
644
+ // flag:-i codex 原生 --image
645
+ // stdin:stream-json claude / codebuddy 原生 stream-json input(图片走 content block)
646
+ // flag:-a llm CLI 原生 --attachment(直连模型走 image_url content parts)
647
+ // part:file arkclaw 走 A2A 原生 file part(base64 inline)
648
+ // prompt 无原生通道:把绝对路径写进提示词,靠引擎读文件
649
+ // (trae/openclaw/dsh/codebuddy-gateway)
650
+ Attachments string `json:"attachments,omitempty"`
651
+ // Append 是否支持「常驻会话 + 追加消息」(`--stream --keep-alive` 与 `--append`)。
652
+ // claude / codebuddy 为 true(stream-json 输入可持续喂 user 消息);
653
+ // dsh 也为 true,但**默认关**(需显式 --keep-alive,见 agent.AppendDefaultOn)。
654
+ Append bool `json:"append"`
655
+ // Ask 该引擎「需要用户选择」的落地方式(machine-readable,见 agent.AskSupportOf):
656
+ // tool:AskUserQuestion 有该工具 + can_use_tool 协议(claude / codebuddy)
657
+ // none 无该能力(其余引擎,含 codebuddy-gateway —— 协议里没有作答通道)
658
+ // 调用方据此决定要不要处理 `--stream` 输出里的 `{"type":"ask"}` 事件。
659
+ Ask string `json:"ask,omitempty"`
660
+ // Permission 该引擎对四档权限模型(manual / accept-edits / auto / full)的
661
+ // 落地方式(machine-readable,见 agent.PermissionSupportOf):
662
+ // flag:--permission-mode claude / codebuddy 有原生 --permission-mode
663
+ // none 其余引擎未接线 —— 传 --permission 会 exit 2 报错
664
+ // (codebuddy-gateway 的档位在网关进程启动参数上,
665
+ // 且网关会把远程任务的权限强制切到 bypassPermissions)
666
+ // 调用方据此决定是否下发 --permission 与那几个沙箱/规则类 flags。
667
+ Permission string `json:"permission,omitempty"`
668
+ // Capabilities 该引擎**静态声明**的能力 id 集合(字典序,稳定、无重复)。
669
+ // 与运行态分开:ok / streaming / models / version 说的是「此刻如何」,
670
+ // capabilities 说的是「能做什么」,二者的推导见 internal/agent/capability.go。
671
+ // 客户端按 capabilities 决定界面形态,按运行态决定按钮可用与降级提示。
672
+ Capabilities []string `json:"capabilities"`
673
+ }
674
+ rows := make([]row, len(engines))
675
+ var wg sync.WaitGroup
676
+ for i, e := range engines {
677
+ ok, note := e.Detect()
678
+ r := row{Engine: e.Name(), OK: ok, Note: note,
679
+ Capabilities: agent.CapabilitiesOf(e.Name()),
680
+ Workspace: agent.WorkspaceSupportOf(e.Name()),
681
+ // 能力(实现了 Streamer)× 可用性(流式通道此刻没在冷却中):
682
+ // 调用方据此决定要不要传 `--stream`,界面据此决定要不要画流式标记。
683
+ // 典型场景:openclaw 的 ACP 桥未授权 → 冷却中 → 这里报 false → 调用方直接走非流式,
684
+ // 界面也不显示流式(用户 2026-09-21:「没批的时候要能检测不显示流式即可」)。
685
+ Streaming: agent.AsStreamer(e) != nil && agent.StreamAvailable(e.Name()),
686
+ Attachments: agent.AttachmentSupportOf(e.Name()),
687
+ Append: agent.AppendSupportOf(e.Name()),
688
+ Ask: agent.AskSupportOf(e.Name()),
689
+ Permission: agent.PermissionSupportOf(e.Name())}
690
+ if ok {
691
+ r.Bin, r.Note = note, ""
692
+ }
693
+ /* 安装 / 升级命令:**可用时也给**(同一条命令重跑一遍 = 升级到最新版)。
694
+ 没有可执行安装路径的引擎留空 —— 见 InstallCommandOf 的说明,不编命令。 */
695
+ r.Install = agent.InstallCommandOf(e.Name())
696
+ rows[i] = r
697
+ /* ⚠️ `--no-models` 是「能力探测」的快路径(调用方只问有哪些能力),
698
+ 所以**版本与模型一起跳过** —— 不为它多起 N 个进程。 */
699
+ if opts.noModels {
700
+ continue
701
+ }
702
+ /* 当前版本(可读才有;A2A 网关 / GUI 应用天然没有,见 EngineVersionOf)。
703
+ 与模型探测同一档并发跑:它只是 `<bin> --version` 一行输出。 */
704
+ if ok && r.Bin != "" {
705
+ wg.Add(1)
706
+ go func(i int, bin string) {
707
+ defer wg.Done()
708
+ rows[i].Version = agent.EngineVersionOf(cmd.Context(), bin)
709
+ }(i, r.Bin)
710
+ }
711
+ if !ok {
712
+ continue
713
+ }
714
+ lister := agent.ModelListerOf(e)
715
+ if lister == nil {
716
+ rows[i].ModelsNote = "no dynamic model source"
717
+ continue
718
+ }
719
+ wg.Add(1)
720
+ go func(i int, l agent.ModelLister) {
721
+ defer wg.Done()
722
+ ctx, cancel := context.WithTimeout(cmd.Context(), agent.DefaultModelProbeTimeout)
723
+ defer cancel()
724
+ models, err := probeEngineModels(ctx, lister)
725
+ switch {
726
+ case err != nil:
727
+ // 失败不致命:原因进 models_note,models 留空。
728
+ rows[i].ModelsNote = err.Error()
729
+ case len(models) == 0:
730
+ rows[i].ModelsNote = "no models reported"
731
+ default:
732
+ rows[i].Models = models
733
+ // 积分倍率与 models 同链同源(同一批缓存文件),探测失败/缺省
734
+ // 不致命:该字段整体省略(omitempty),不影响 models。
735
+ if cl, ok := l.(agent.ModelCreditLister); ok {
736
+ rows[i].ModelCredits = probeEngineCredits(ctx, cl)
737
+ }
738
+ }
739
+ }(i, lister)
740
+ }
741
+ wg.Wait()
742
+ if !envelope {
743
+ return printJSON(cmd.OutOrStdout(), rows)
744
+ }
745
+ /* 匿名结构体就地引用函数内的 row 类型:不必把 row 提到包级,
746
+ 也就不会把上面那张字段注释表搬来搬去。 */
747
+ return printJSON(cmd.OutOrStdout(), struct {
748
+ ContractVersion int `json:"contractVersion"`
749
+ Features []string `json:"features"`
750
+ Engines []row `json:"engines"`
751
+ }{ContractVersion: agent.ContractVersion, Features: agent.CoreFeatures(), Engines: rows})
752
+ }