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.
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/cmd/magic-agent/main.go +12 -0
- package/cmd/magic-client/main.go +144 -0
- package/go.mod +11 -0
- package/go.sum +12 -0
- package/internal/agent/arkclaw.go +587 -0
- package/internal/agent/arkclaw_stream.go +312 -0
- package/internal/agent/arkclaw_stream_test.go +428 -0
- package/internal/agent/arkclaw_test.go +827 -0
- package/internal/agent/ask.go +694 -0
- package/internal/agent/ask_test.go +587 -0
- package/internal/agent/attachments.go +200 -0
- package/internal/agent/attachments_test.go +481 -0
- package/internal/agent/capability.go +154 -0
- package/internal/agent/capability_test.go +190 -0
- package/internal/agent/claude.go +406 -0
- package/internal/agent/codebuddy.go +1044 -0
- package/internal/agent/codebuddy_account_test.go +128 -0
- package/internal/agent/codebuddy_gateway.go +479 -0
- package/internal/agent/codebuddy_gateway_stream.go +358 -0
- package/internal/agent/codebuddy_gateway_test.go +673 -0
- package/internal/agent/codex.go +599 -0
- package/internal/agent/codex_home.go +206 -0
- package/internal/agent/codex_home_test.go +130 -0
- package/internal/agent/codex_stream_test.go +97 -0
- package/internal/agent/codex_test.go +280 -0
- package/internal/agent/custom_models.go +197 -0
- package/internal/agent/custom_models_test.go +283 -0
- package/internal/agent/dsh.go +754 -0
- package/internal/agent/dsh_sdk.go +679 -0
- package/internal/agent/dsh_sdk_test.go +726 -0
- package/internal/agent/dsh_test.go +759 -0
- package/internal/agent/engine.go +469 -0
- package/internal/agent/engine_base.go +497 -0
- package/internal/agent/engine_base_test.go +308 -0
- package/internal/agent/engine_test.go +858 -0
- package/internal/agent/env.go +88 -0
- package/internal/agent/env_test.go +106 -0
- package/internal/agent/jsonextract.go +128 -0
- package/internal/agent/jsonextract_test.go +110 -0
- package/internal/agent/keepalive_test.go +216 -0
- package/internal/agent/llmengine.go +470 -0
- package/internal/agent/llmengine_test.go +551 -0
- package/internal/agent/llmhttp.go +437 -0
- package/internal/agent/llmhttp_test.go +304 -0
- package/internal/agent/llmmodels.go +205 -0
- package/internal/agent/login.go +27 -0
- package/internal/agent/main_test.go +23 -0
- package/internal/agent/models.go +686 -0
- package/internal/agent/models_test.go +877 -0
- package/internal/agent/openclaw.go +543 -0
- package/internal/agent/openclaw_acp.go +949 -0
- package/internal/agent/openclaw_acp_test.go +406 -0
- package/internal/agent/openclaw_test.go +451 -0
- package/internal/agent/output.go +191 -0
- package/internal/agent/permission.go +362 -0
- package/internal/agent/permission_test.go +526 -0
- package/internal/agent/prompt.go +65 -0
- package/internal/agent/runcmd.go +259 -0
- package/internal/agent/runcmd_test.go +81 -0
- package/internal/agent/runcmd_unix.go +56 -0
- package/internal/agent/runcmd_windows.go +66 -0
- package/internal/agent/runner.go +180 -0
- package/internal/agent/runner_test.go +263 -0
- package/internal/agent/spawnhook_test.go +96 -0
- package/internal/agent/stream.go +882 -0
- package/internal/agent/stream_test.go +565 -0
- package/internal/agent/streamjson.go +185 -0
- package/internal/agent/tags.go +52 -0
- package/internal/agent/toolkind.go +96 -0
- package/internal/agent/toolkind_test.go +70 -0
- package/internal/agent/tools_suffix_test.go +261 -0
- package/internal/agent/trae.go +342 -0
- package/internal/agent/workspace_test.go +166 -0
- package/internal/boundary/boundary_test.go +165 -0
- package/internal/cli/ask.go +1113 -0
- package/internal/cli/ask_test.go +1088 -0
- package/internal/cli/attach_test.go +239 -0
- package/internal/cli/contract_test.go +157 -0
- package/internal/cli/control.go +149 -0
- package/internal/cli/control_test.go +192 -0
- package/internal/cli/engines_test.go +205 -0
- package/internal/cli/events.go +198 -0
- package/internal/cli/events_test.go +166 -0
- package/internal/cli/keepalive_test.go +391 -0
- package/internal/cli/login.go +78 -0
- package/internal/cli/permission_test.go +295 -0
- package/internal/cli/prompt_source_test.go +222 -0
- package/internal/cli/root.go +752 -0
- package/internal/cli/session.go +486 -0
- package/internal/cli/session_test.go +202 -0
- package/internal/cli/sessionlog.go +325 -0
- package/internal/cli/sessionlog_command_test.go +204 -0
- package/internal/cli/sessionlog_test.go +262 -0
- package/internal/cli/sessionlog_user_test.go +129 -0
- package/internal/cli/toolkind_wire_test.go +84 -0
- package/internal/cli/workspace_test.go +188 -0
- package/internal/client/cors_test.go +136 -0
- package/internal/client/lan.go +68 -0
- package/internal/client/lan_test.go +53 -0
- package/internal/client/pair.go +163 -0
- package/internal/client/pair_test.go +109 -0
- package/internal/client/profile.go +184 -0
- package/internal/client/service.go +888 -0
- package/internal/client/service_test.go +654 -0
- package/internal/client/session_events_test.go +186 -0
- package/internal/client/session_messages_test.go +150 -0
- package/internal/config/config.go +439 -0
- package/internal/config/config_test.go +448 -0
- package/internal/session/session.go +447 -0
- package/internal/session/session_append.go +241 -0
- package/internal/session/session_append_test.go +161 -0
- package/internal/session/session_kill_unix_test.go +90 -0
- package/internal/session/session_mark_test.go +95 -0
- package/internal/session/session_test.go +237 -0
- package/internal/session/sessionlog.go +128 -0
- package/internal/session/sessionlog_read_test.go +157 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/install.js +121 -0
- package/npm/lib/platform.js +78 -0
- package/package.json +36 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// Package agent - 引擎无关的 agent CLI 代理抽象层。
|
|
2
|
+
//
|
|
3
|
+
// magic-agent 是一个专业的 agent CLI 代理工具:把 claude / codebuddy /
|
|
4
|
+
// trae / llm / codex / openclaw / dsh 七家 CLI 与 arkclaw 网关的非交互调用
|
|
5
|
+
// 统一成一个 Engine 接口,对外提供一致的请求/响应结构、超时与重试语义、
|
|
6
|
+
// 固定的 text / json 输出格式。
|
|
7
|
+
//
|
|
8
|
+
// 各关注点分文件维护:
|
|
9
|
+
//
|
|
10
|
+
// engine.go — Engine 接口、Request/Response 类型、注册表
|
|
11
|
+
// engine_base.go — 公共基座:统一探测链(cliBase)+ 统一参数矩阵
|
|
12
|
+
// prompt.go — 多轮消息扁平化为单条 prompt 的公共逻辑
|
|
13
|
+
// claude.go — Claude Code CLI 引擎(--print --output-format json)
|
|
14
|
+
// codebuddy.go — CodeBuddy Code CLI 引擎(一个独立 CLI、两个账号)
|
|
15
|
+
// trae.go — Trae CLI 引擎(-p,无 --model flag,-c model.name= 覆盖)
|
|
16
|
+
// llmengine.go — simonw/LLM CLI 引擎(-m / -s / -o 透传)
|
|
17
|
+
// codex.go — Codex CLI 引擎(exec / exec resume + --output-schema)
|
|
18
|
+
// openclaw.go — OpenClaw CLI 引擎(agent --local --json)
|
|
19
|
+
// dsh.go — DeepSeek Harness 引擎(--profile headless "<任务>",纯文本 stdout)
|
|
20
|
+
// arkclaw.go — ArkClaw A2A 网关引擎(HTTP JSON-RPC message/send)
|
|
21
|
+
// arkclaw_stream.go — ArkClaw 流式通道(A2A message/stream 的 SSE 事件流)
|
|
22
|
+
// codebuddy_gateway.go — CodeBuddy Code HTTP 网关引擎(webhook 投递 + SSE 收流)
|
|
23
|
+
// codebuddy_gateway_stream.go — 上面那个的 SSE 通道(逐字增量)
|
|
24
|
+
// runner.go — 超时 + 重试编排(可重试错误分类、指数退避)
|
|
25
|
+
package agent
|
|
26
|
+
|
|
27
|
+
import (
|
|
28
|
+
"context"
|
|
29
|
+
"strings"
|
|
30
|
+
"time"
|
|
31
|
+
|
|
32
|
+
"github.com/darren/magic-agent/internal/config"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Message 是一轮对话消息。
|
|
36
|
+
type Message struct {
|
|
37
|
+
Role string // "system" | "user" | "assistant"
|
|
38
|
+
Content string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ToolsMode 工具开关模式。
|
|
42
|
+
//
|
|
43
|
+
// ToolsOff 禁用全部工具(默认):纯 chat 一次成型,输出可解析。
|
|
44
|
+
// ToolsOn 引擎默认工具集 + 权限旁路:真 agent 模式,模型可执行
|
|
45
|
+
// 工具调用(读文件、跑命令等),代价是输出可能是工具痕迹。
|
|
46
|
+
// ToolsAllowlist 白名单:仅允许列出的工具 + 权限旁路。
|
|
47
|
+
//
|
|
48
|
+
// 判定方法一律导出:Engine 实现与本包外的调用方(如 CLI 层测试)
|
|
49
|
+
// 都要能检视模式;非导出方法的接口无法被跨包断言。
|
|
50
|
+
type ToolsMode interface {
|
|
51
|
+
// IsOff 是否为「禁用全部工具」模式。
|
|
52
|
+
IsOff() bool
|
|
53
|
+
// IsOn 是否为「引擎默认工具集 + 权限旁路」模式。
|
|
54
|
+
IsOn() bool
|
|
55
|
+
// Allowlist 白名单模式下的工具名;非白名单模式返回 nil。
|
|
56
|
+
Allowlist() []string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// toolsModeImpl 统一实现载体。
|
|
60
|
+
type toolsModeImpl struct {
|
|
61
|
+
off bool
|
|
62
|
+
on bool
|
|
63
|
+
allowlist []string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ToolsOff / ToolsOn 预置模式。
|
|
67
|
+
var (
|
|
68
|
+
ToolsOff = toolsModeImpl{off: true}
|
|
69
|
+
ToolsOn = toolsModeImpl{on: true}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// ToolsAllowlist 构造白名单模式。
|
|
73
|
+
func ToolsAllowlist(names []string) ToolsMode {
|
|
74
|
+
return toolsModeImpl{allowlist: names}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// IsOff / IsOn / Allowlist 实现 ToolsMode。
|
|
78
|
+
func (t toolsModeImpl) IsOff() bool { return t.off }
|
|
79
|
+
func (t toolsModeImpl) IsOn() bool { return t.on }
|
|
80
|
+
func (t toolsModeImpl) Allowlist() []string { return t.allowlist }
|
|
81
|
+
|
|
82
|
+
// Request 是一次 agent 调用的完整输入。
|
|
83
|
+
type Request struct {
|
|
84
|
+
// Engine 引擎名("claude" | "codebuddy" | "trae"),由 Runner 填充,
|
|
85
|
+
// 引擎实现一般不需要读它。
|
|
86
|
+
Engine string
|
|
87
|
+
|
|
88
|
+
// Model 模型标识。空串 = 引擎自身配置的默认模型。
|
|
89
|
+
// 引擎实现负责剥掉可能的 "engine/model" 前缀。
|
|
90
|
+
Model string
|
|
91
|
+
|
|
92
|
+
// SystemPrompt 可选系统提示词。各引擎自行决定注入方式
|
|
93
|
+
//(claude/codebuddy用 --append-system-prompt,trae 拼进 prompt 头)。
|
|
94
|
+
SystemPrompt string
|
|
95
|
+
|
|
96
|
+
// Tools 工具开关模式(默认 ToolsOff)。
|
|
97
|
+
Tools ToolsMode
|
|
98
|
+
|
|
99
|
+
// Permission 四档权限档位(默认 DefaultPermissionTier = full,保持既有行为)。
|
|
100
|
+
//
|
|
101
|
+
// 四档语义与 Claude Code 参数映射见 permission.go。空串 = 用默认档。
|
|
102
|
+
// 只有 PermissionSupported 为真的引擎(claude / codebuddy)会真正落地;
|
|
103
|
+
// 其余引擎传非空值会在 CLI 层被**明确拒绝**(exit 2),不会静默忽略 ——
|
|
104
|
+
// 静默忽略一个安全设置,等于让用户以为自己被保护着。
|
|
105
|
+
Permission PermissionTier
|
|
106
|
+
|
|
107
|
+
// PermissionOptions 四档模型之上的可选项(沙箱排除命令、网络白名单、
|
|
108
|
+
// 第 3 档分类器的受信边界、deny/ask 规则)。零值即最严格。
|
|
109
|
+
// 只在 PermissionSupported 的引擎上生效。
|
|
110
|
+
PermissionOptions PermissionOptions
|
|
111
|
+
|
|
112
|
+
// Messages 对话消息;CLI 引擎会扁平化为单条 prompt。
|
|
113
|
+
Messages []Message
|
|
114
|
+
|
|
115
|
+
// SessionID 会话续接 id。空 = 新会话(默认,上下文从零开始);
|
|
116
|
+
// 非空 = 续接该会话(引擎透传各自的续接参数),上一轮上下文继续生效。
|
|
117
|
+
// 会话 id 通常取自上一次 Response.SessionID。各引擎映射(统一支持):
|
|
118
|
+
// claude/codebuddy --resume <id>;trae --resume=<id>;llm --cid <id>。
|
|
119
|
+
// 注意:新会话不主动禁用落盘(--no-session-persistence 的会话无法续接),
|
|
120
|
+
// 这样首轮返回的 session_id 才能用于下一轮 --session。
|
|
121
|
+
SessionID string
|
|
122
|
+
|
|
123
|
+
// Continue 续接"最近一次会话"(不需要 session id)。各引擎映射
|
|
124
|
+
//(统一支持):claude/codebuddy --continue;trae 裸 --resume
|
|
125
|
+
//(AUTO = 自动接最近一次会话,实测 -p 非交互模式可用);llm -c。
|
|
126
|
+
// 与 SessionID 同时设置时 SessionID 优先(显式 id 比"最近一次"更精确)。
|
|
127
|
+
Continue bool
|
|
128
|
+
|
|
129
|
+
// MaxTokens 输出 token 上限。0 = 不指定(交给引擎/模型默认)。
|
|
130
|
+
//
|
|
131
|
+
// 各引擎映射(统一支持,trae 除外):llm 经 `-o max_tokens` 透传;
|
|
132
|
+
// claude/codebuddy 经 `--settings` 注入 env CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
|
133
|
+
//(两者无原生 flag);trae 不支持 —— `-c <k>=<v>` 会整体覆盖模型配置块
|
|
134
|
+
//(实测注入后模型名漂移并触发配额错误),静默忽略。
|
|
135
|
+
// 推理模型(MiniMax-M3 等)的思考过程计入 output 配额,调用方
|
|
136
|
+
// 需要给足够大的值,否则正文会被思考吃光。
|
|
137
|
+
MaxTokens int
|
|
138
|
+
|
|
139
|
+
// Temperature 采样温度。nil = 不提(交给引擎/模型默认)。
|
|
140
|
+
// 仅 llm 支持(`-o temperature`);claude/codebuddy/trae 无原生参数,
|
|
141
|
+
// 忽略(不上报错误)。
|
|
142
|
+
Temperature *float64
|
|
143
|
+
|
|
144
|
+
// JSONSchema 是可选的结构化输出约束。
|
|
145
|
+
//
|
|
146
|
+
// 各引擎行为:
|
|
147
|
+
// llm 引擎:尝试把 Response.Text 解析回 JSON 对象。
|
|
148
|
+
// 严格匹配 schema 的顶层 Required 字段集 + Type=object,
|
|
149
|
+
// 解析成功则把 Text 替换为 json.Marshal 后的紧凑串。
|
|
150
|
+
// 失败则原样返回(不阻断,让上层解析器兜底)。
|
|
151
|
+
// 其他引擎:暂忽略;约束完全靠 system prompt 注入。
|
|
152
|
+
//
|
|
153
|
+
// 上游(magic-video)已经会把 schema 渲染进 system prompt 作为
|
|
154
|
+
// 提示词约束;本字段只是 llm 引擎做"输出后处理"以稳定 JSON。
|
|
155
|
+
JSONSchema *JSONSchema
|
|
156
|
+
|
|
157
|
+
// Attachments 随提示词一起发送的附件(截图 / 图片为主)。
|
|
158
|
+
//
|
|
159
|
+
// 用户需求原文(2026-09-17):「文件+提示词 比如截图加提示词」——
|
|
160
|
+
// 提示词与附件是**并列**关系,不是「提示词其实是个文件路径」。
|
|
161
|
+
//
|
|
162
|
+
// 各引擎落地方式(尽量用原生能力,见 AttachmentSupportOf):
|
|
163
|
+
//
|
|
164
|
+
// codex 原生 `-i/--image <FILE>...`(可重复)
|
|
165
|
+
// claude 原生 `--input-format stream-json`:附件作为 message content 的
|
|
166
|
+
// image block 走 stdin(CLI 要求此时 output-format 也是 stream-json)
|
|
167
|
+
// codebuddy 同上(同族 CLI,同样有 --input-format)
|
|
168
|
+
// llm 直连:OpenAI 兼容 content parts(image_url + data URL);
|
|
169
|
+
// 委托 llm CLI:原生 `-a/--attachment <path>`
|
|
170
|
+
// arkclaw 原生 A2A:message.parts 里追加 kind=file 的 file part(base64 inline)
|
|
171
|
+
// trae 无原生输入通道 → 只能把**绝对路径**写进提示词,靠它的读文件工具看
|
|
172
|
+
// openclaw 同上
|
|
173
|
+
//
|
|
174
|
+
// 路径由 CLI 层校验(存在 + 普通文件)后填成绝对路径;引擎不重复校验。
|
|
175
|
+
Attachments []Attachment
|
|
176
|
+
|
|
177
|
+
// Append 追加消息通道(常驻会话 / keep-alive)。
|
|
178
|
+
//
|
|
179
|
+
// 用户需求(2026-09-18):「会话中追加需求」。非 nil 时本次调用是**常驻会话**:
|
|
180
|
+
// 引擎先发提示词,之后每从通道收到一条文本就再追加一轮 user 消息(同一进程、同一会话),
|
|
181
|
+
// 通道关闭 → stdin EOF → 引擎收尾退出。
|
|
182
|
+
//
|
|
183
|
+
// claude / codebuddy / codebuddy-ai 走 stream-json 输入(`--input-format stream-json` 的
|
|
184
|
+
// stdin 可以持续喂 user 消息,官方文档明确「allows providing guidance to the model
|
|
185
|
+
// while it [is working]」,本项目 2026-09-18 实测同一进程内两轮均被处理);
|
|
186
|
+
// dsh 走 SDK 通道(同一进程内对同一 sessionId 继续 session/prompt,官方推荐的续接方式)。
|
|
187
|
+
// 其它引擎拿到非 nil 会在 CLI 层被挡下(见 AppendSupportOf)。
|
|
188
|
+
// 默认值是否开启按引擎区分,见 AppendDefaultOn(dsh 默认关)。
|
|
189
|
+
Append <-chan string
|
|
190
|
+
|
|
191
|
+
// OnSessionID 引擎发现 session_id 时回调(用于「看历史」持久化)。
|
|
192
|
+
//
|
|
193
|
+
// 谁需要它:CLI 在 runStreamAsk 里包一个 sessionWriter,每条事件落盘到
|
|
194
|
+
// `<dir>/<session_id>.jsonl`。但 session_id 出现得早(claude 的 init 行就有),
|
|
195
|
+
// 而引擎先发完初始帧 → 调用方再开文件 → 早期帧全丢;这个回调让 writer
|
|
196
|
+
// **在收到第一条 init 行时就开好文件**,避免丢前几条事件。
|
|
197
|
+
//
|
|
198
|
+
// 调用方保证:id 非空时回调;多次回调以最后一次为准;与 onEvent 并发调用,
|
|
199
|
+
// 调用方需要同步。引擎实现里一般一行 `req.OnSessionID(extract(line))` 即可。
|
|
200
|
+
OnSessionID func(id string)
|
|
201
|
+
|
|
202
|
+
// Timeout 单次尝试的超时(含引擎 CLI 自身执行时间)。
|
|
203
|
+
// 0 = 引擎默认。
|
|
204
|
+
Timeout time.Duration
|
|
205
|
+
|
|
206
|
+
// Workspace 工作目录(绝对路径)。空 = 用调用方的 cwd(默认行为)。
|
|
207
|
+
//
|
|
208
|
+
// 各引擎落地方式(尽量用原生能力,见 WorkspaceSupportOf):
|
|
209
|
+
//
|
|
210
|
+
// codex 原生 `-C/--cd <dir>`(working root)**并**子进程 cwd
|
|
211
|
+
// claude 子进程 cwd(CLI 无工作目录 flag;--add-dir 只用于追加额外目录)
|
|
212
|
+
// codebuddy 同上(有 --add-dir,语义同 claude)
|
|
213
|
+
// trae 同上(有 --add-dir)
|
|
214
|
+
// openclaw 不支持:workspace 与 agent 绑定(`openclaw agents`),无 per-call flag,
|
|
215
|
+
// 实测子进程 cwd 被忽略 → 参数被忽略(CLI 层提示)
|
|
216
|
+
// llm 不支持:模型不直接读写文件,无文件系统语义 → 参数被忽略(CLI 层提示)
|
|
217
|
+
// arkclaw 不支持:远端网关按 claw_id 绑定,客户端无法指定
|
|
218
|
+
//
|
|
219
|
+
// 上面的"生效/忽略"与 WorkspaceSupportOf 的取值一一对应(flag:-C / cwd / none)。
|
|
220
|
+
//
|
|
221
|
+
// 目录必须存在且是目录(CLI 层校验);不存在时 spawn 会以 "chdir ...: no such file
|
|
222
|
+
// or directory" 失败,这里提前拦掉更清楚。
|
|
223
|
+
Workspace string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// JSONSchema 是传给 Engine 的结构化输出契约(精简版)。
|
|
227
|
+
//
|
|
228
|
+
// 故意保持极简:只覆盖本项目实际用到(type=object + 顶层 Required)。
|
|
229
|
+
// 需要更多字段时再按需扩展。
|
|
230
|
+
type JSONSchema struct {
|
|
231
|
+
Type string // "object"
|
|
232
|
+
Properties map[string]map[string]any // field name -> 字段描述 {type, ...}
|
|
233
|
+
Required []string // 顶层必填字段名
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Response 是一次成功调用的结果。
|
|
237
|
+
type Response struct {
|
|
238
|
+
// Engine 产出该响应的引擎名。
|
|
239
|
+
Engine string
|
|
240
|
+
|
|
241
|
+
// Text 助手正文(不含思考过程)。
|
|
242
|
+
Text string
|
|
243
|
+
|
|
244
|
+
// Model 实际使用的模型标识(探测或透传,尽力而为)。
|
|
245
|
+
Model string
|
|
246
|
+
|
|
247
|
+
// SessionID 引擎返回的会话 id(可能为空)。
|
|
248
|
+
SessionID string
|
|
249
|
+
|
|
250
|
+
// 以下三个 token 计数尽力而为:引擎/CLI 不上报时为 0。
|
|
251
|
+
// 目前只有 llm 引擎能拿到(llm prompt --json 的 input_tokens /
|
|
252
|
+
// output_tokens);其余 CLI 引擎没有该信息。
|
|
253
|
+
InputTokens int
|
|
254
|
+
OutputTokens int
|
|
255
|
+
TotalTokens int
|
|
256
|
+
|
|
257
|
+
// Latency 一次成功尝试的耗时(不含重试等待)。
|
|
258
|
+
Latency time.Duration
|
|
259
|
+
|
|
260
|
+
// Attempts 本次调用总共尝试次数(含成功那次,>=1)。
|
|
261
|
+
Attempts int
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Engine 是一个 agent CLI 后端的统一抽象。
|
|
265
|
+
type Engine interface {
|
|
266
|
+
// Name 引擎标识("claude" / "codebuddy" / "trae")。
|
|
267
|
+
Name() string
|
|
268
|
+
|
|
269
|
+
// Detect 探测引擎 CLI 是否可用(可执行文件存在等)。
|
|
270
|
+
// 返回 (可用, 说明)。说明用于 `--engines` 展示。
|
|
271
|
+
Detect() (bool, string)
|
|
272
|
+
|
|
273
|
+
// Complete 执行一次调用。实现只负责单次尝试;
|
|
274
|
+
// 超时与重试由 Runner 在外层编排。
|
|
275
|
+
Complete(ctx context.Context, req Request) (Response, error)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* registry 引擎注册表(顺序即 `--engines` / `--contract` 的列表展示顺序)。
|
|
279
|
+
*
|
|
280
|
+
* ⚠️ 已知脆弱点(2026-09-27 记录,**行为未改**):内置引擎的注册挂在
|
|
281
|
+
* 「registry 为空才 initEngines」这个守卫上。任何在 Engines() 之前调 Register 的代码
|
|
282
|
+
* 都会让 registry 非空,于是**内置引擎一个都不注册、且没有任何报错** —— 现象是
|
|
283
|
+
* 「claude 的能力全是空的」「--engines 少了十个引擎」这类极难定位的问题。
|
|
284
|
+
* 生产路径不会触发(Register 只被 initEngines 与 registerConfiguredAgents 调用),
|
|
285
|
+
* 但用例里的探针引擎会触发,使**按名筛选**的测试结果随运行方式而变化
|
|
286
|
+
* (arkclaw_test.go 的 __capfam_probe__ 曾在跑 `-run 'Capabilit'` 时让内置引擎整体消失)。
|
|
287
|
+
* 要修就得连同一批「用假引擎遮住同名内置」的用例一起改:试过把内置改为无条件注册
|
|
288
|
+
*(前置或追加),都会打断 TestEnginesInstallCommandByAvailability 这类用例,故暂不动。
|
|
289
|
+
* 在那之前:**别在 Engines() 之前调 Register。** */
|
|
290
|
+
var registry []Engine
|
|
291
|
+
|
|
292
|
+
// Register 注册一个引擎(具名 A2A agent 走这里)。
|
|
293
|
+
func Register(e Engine) {
|
|
294
|
+
registry = append(registry, e)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Engines 返回全部已注册引擎。
|
|
298
|
+
func Engines() []Engine {
|
|
299
|
+
if len(registry) == 0 {
|
|
300
|
+
initEngines()
|
|
301
|
+
}
|
|
302
|
+
out := make([]Engine, len(registry))
|
|
303
|
+
copy(out, registry)
|
|
304
|
+
return out
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* CapabilityFamily 可选接口:引擎声明「我该走能力表的哪一行」。
|
|
308
|
+
*
|
|
309
|
+
* 为什么需要(2026-09-23):能力表(workspace / attachments / permission / ask / append)
|
|
310
|
+
* 全是**按引擎名写死的 switch**,而配置里的具名 agent 是**同一协议的新名字**
|
|
311
|
+
* (MagicAI = 另一个 ArkClawEngine)—— 新名字会落进 default 分支、拿到错的能力。
|
|
312
|
+
* 实测踩到:MagicAI 被报成 `workspace=cwd` / `attachments=prompt`,
|
|
313
|
+
* 而它实际是 `none` / `part:file`(后者会让界面白白警告「图贴了但模型看不到」)。
|
|
314
|
+
* 有了这个接口,具名实例只要声明家族,五张表一起就对。
|
|
315
|
+
* 未实现该接口的引擎 = 家族名就是自己的 Name()(内置引擎行为一字不变)。 */
|
|
316
|
+
type CapabilityFamily interface {
|
|
317
|
+
CapabilityFamily() string
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// CapabilityFamily 实现 CapabilityFamily:A2A JSON-RPC 网关这一族(含具名 agent)。
|
|
321
|
+
func (e *ArkClawEngine) CapabilityFamily() string { return "arkclaw" }
|
|
322
|
+
|
|
323
|
+
/* CapabilityFamilyOf 把引擎名解析成「能力家族名」。
|
|
324
|
+
*
|
|
325
|
+
* 先按注册表找到那个实例:它实现了 CapabilityFamily 就用它的答案;
|
|
326
|
+
* 否则(内置引擎 / 未注册的名字)返回原名 —— 于是内置引擎行为完全不变,
|
|
327
|
+
* 而具名 agent 自动继承其协议那一整行能力。
|
|
328
|
+
* ⚠️ 会(幂等地)触发注册表初始化:`MagicAI` 这种名字只有建完注册表才认得出来。 */
|
|
329
|
+
func CapabilityFamilyOf(engine string) string {
|
|
330
|
+
name := strings.TrimSpace(engine)
|
|
331
|
+
if name == "" {
|
|
332
|
+
return ""
|
|
333
|
+
}
|
|
334
|
+
if e := Lookup(name); e != nil {
|
|
335
|
+
if cf, ok := e.(CapabilityFamily); ok {
|
|
336
|
+
if fam := strings.TrimSpace(cf.CapabilityFamily()); fam != "" {
|
|
337
|
+
return fam
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return name
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Lookup 按名字查找引擎(大小写不敏感)。未找到返回 nil。
|
|
345
|
+
func Lookup(name string) Engine {
|
|
346
|
+
if name == "" {
|
|
347
|
+
return nil
|
|
348
|
+
}
|
|
349
|
+
for _, e := range Engines() {
|
|
350
|
+
if equalFold(e.Name(), name) {
|
|
351
|
+
return e
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return nil
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// initEngines 惰性初始化,避免 import 顺序影响注册(脆弱点见 registry 的注释)。
|
|
358
|
+
func initEngines() {
|
|
359
|
+
Register(&ClaudeEngine{})
|
|
360
|
+
Register(&CodeBuddyEngine{})
|
|
361
|
+
// codebuddy-ai 与 codebuddy 用**同一个**独立 CLI,只是账号不同
|
|
362
|
+
//(ACC_PRODUCT_CONFIG_V3 切 authentication.id,见 codebuddy.go)。
|
|
363
|
+
// 必须注册在 codebuddy 之后 —— Lookup 取第一个同名匹配,两者名字不同不冲突。
|
|
364
|
+
Register(&CodeBuddyAIEngine{})
|
|
365
|
+
// codebuddy-gateway = CodeBuddy 的**第三种接入方式**:不起本机 CLI,而是把 prompt
|
|
366
|
+
// 投给已在跑的 CodeBuddy Code HTTP 网关(`codebuddy --serve` / `/gateway`),
|
|
367
|
+
// 走 webhook 投递 + SSE 收流(见 codebuddy_gateway.go 文件头)。
|
|
368
|
+
Register(&CodeBuddyGatewayEngine{})
|
|
369
|
+
Register(&TraeEngine{})
|
|
370
|
+
Register(&LLMEngine{})
|
|
371
|
+
Register(&CodexEngine{})
|
|
372
|
+
Register(&OpenClawEngine{})
|
|
373
|
+
// dsh = DeepSeek Harness 的 headless profile(一次性任务入口)。
|
|
374
|
+
Register(&DshEngine{})
|
|
375
|
+
Register(&ArkClawEngine{})
|
|
376
|
+
// 配置里 agents 数组的**具名 A2A agent**(放在内置引擎之后,重名会被跳过)。
|
|
377
|
+
registerConfiguredAgents()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* registerConfiguredAgents 把配置里 `agents` 数组的每个条目注册成一个独立引擎
|
|
381
|
+
* (2026-09-23 加,用户:「新增 <url> 名字叫 MagicAI」)。
|
|
382
|
+
*
|
|
383
|
+
* 为什么在这里做、而不另起一个「引擎工厂」:注册表本来就是「有哪些引擎」的唯一真相,
|
|
384
|
+
* 而 agents 只是**同一协议(A2A JSON-RPC)的多个实例** —— 复用 ArkClawEngine、
|
|
385
|
+
* 只换 DisplayName / URL / Key / ClawID,于是 `--engines` 与 `-e <name>` 自动就能用,
|
|
386
|
+
* 上层(magic-test 的引擎下拉)**一行都不用改**(它读的就是 --engines)。
|
|
387
|
+
*
|
|
388
|
+
* 边界(三条都不致命 —— 宁可少一个引擎,也不要把 CLI 打死):
|
|
389
|
+
* · 读配置失败 / 没有 agents 节 → 什么都不注册(老行为一字不变);
|
|
390
|
+
* · 条目 name 为空 → 跳过(没有名字就没法 `-e` 它);
|
|
391
|
+
* · name 与已注册引擎重名 → 跳过(Lookup 取第一个匹配,注册了也只会被遮住,
|
|
392
|
+
* 却会在 `--engines` 里多出一条同名行 —— 那是纯粹的误导)。
|
|
393
|
+
*/
|
|
394
|
+
func registerConfiguredAgents() {
|
|
395
|
+
cfg, err := config.Load()
|
|
396
|
+
if err != nil {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
for _, e := range agentEnginesFrom(cfg.Agents, registry) {
|
|
400
|
+
Register(e)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// agentEnginesFrom 把配置里的 agents 条目转成待注册的引擎(纯函数,便于直接测)。
|
|
405
|
+
//
|
|
406
|
+
// 两条过滤规则见 registerConfiguredAgents 的注释:**空名跳过**、**与已有引擎重名跳过**。
|
|
407
|
+
// `existing` 传入而不是直接读包级 registry —— 注册表只在首次 `Engines()` 时建一次,
|
|
408
|
+
// 拿它当输入会让测试依赖「谁先跑」,那是假红/假绿的常见来源。
|
|
409
|
+
func agentEnginesFrom(agents []config.AgentConfig, existing []Engine) []Engine {
|
|
410
|
+
if len(agents) == 0 {
|
|
411
|
+
return nil
|
|
412
|
+
}
|
|
413
|
+
taken := make(map[string]bool, len(existing)+len(agents))
|
|
414
|
+
for _, e := range existing {
|
|
415
|
+
taken[strings.ToLower(e.Name())] = true
|
|
416
|
+
}
|
|
417
|
+
var out []Engine
|
|
418
|
+
for _, a := range agents {
|
|
419
|
+
name := strings.TrimSpace(a.Name)
|
|
420
|
+
if name == "" || taken[strings.ToLower(name)] {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
taken[strings.ToLower(name)] = true
|
|
424
|
+
out = append(out, &ArkClawEngine{DisplayName: name, URL: a.URL, Key: a.Key, ClawID: a.ClawID})
|
|
425
|
+
}
|
|
426
|
+
return out
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// toolsOrDefault nil ToolsMode 视为 ToolsOff。
|
|
430
|
+
// 返回接口(而非具体实现):判定一律走 ToolsMode 的导出方法。
|
|
431
|
+
func toolsOrDefault(t ToolsMode) ToolsMode {
|
|
432
|
+
if t == nil {
|
|
433
|
+
return ToolsOff
|
|
434
|
+
}
|
|
435
|
+
return t
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// toolsIsOff 是否「禁用工具」模式(nil 视为 off)。
|
|
439
|
+
// 各引擎用它决定是否注入 noToolSuffix —— 该后缀明文禁止工具调用,
|
|
440
|
+
// 只能在 off 模式出现,否则会和 on / 白名单模式互相打架。
|
|
441
|
+
func toolsIsOff(req Request) bool {
|
|
442
|
+
return toolsOrDefault(req.Tools).IsOff()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ToolsIsOff / ToolsIsOn / ToolsAllowlistOf 跨包检视 ToolsMode 的便捷包装。
|
|
446
|
+
// 与 ToolsMode 的导出方法等价,保留为语义化入口。
|
|
447
|
+
func ToolsIsOff(t ToolsMode) bool { return toolsOrDefault(t).IsOff() }
|
|
448
|
+
func ToolsIsOn(t ToolsMode) bool { return toolsOrDefault(t).IsOn() }
|
|
449
|
+
func ToolsAllowlistOf(t ToolsMode) []string { return toolsOrDefault(t).Allowlist() }
|
|
450
|
+
|
|
451
|
+
// equalFold 简易 ASCII 大小写不敏感比较。
|
|
452
|
+
func equalFold(a, b string) bool {
|
|
453
|
+
if len(a) != len(b) {
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
for i := 0; i < len(a); i++ {
|
|
457
|
+
ca, cb := a[i], b[i]
|
|
458
|
+
if 'A' <= ca && ca <= 'Z' {
|
|
459
|
+
ca += 'a' - 'A'
|
|
460
|
+
}
|
|
461
|
+
if 'A' <= cb && cb <= 'Z' {
|
|
462
|
+
cb += 'a' - 'A'
|
|
463
|
+
}
|
|
464
|
+
if ca != cb {
|
|
465
|
+
return false
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return true
|
|
469
|
+
}
|