magic-agent 2.4.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT-CONTRACT.md +284 -30
- package/README.md +15 -14
- package/client-ui/pc/app.css +32 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +37 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +241 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +63 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +81 -0
- package/client-ui/shared/conv/conv.css +79 -19
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +336 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/index.html +2932 -333
- package/client-ui-mobile/reqs.css +102 -0
- package/client-ui-mobile/reqs.js +485 -70
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +21 -0
- package/client-ui-mobile/scripts.js +539 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +33 -9
- package/cmd/magic-client/main.go +57 -26
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_resume.go +13 -2
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +136 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +108 -6
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +160 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/events.go +4 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +122 -5
- package/internal/cli/session.go +6 -0
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/daemon.go +90 -17
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/jobs.go +294 -21
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +198 -1
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/restart.go +134 -0
- package/internal/client/restart_test.go +151 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_other.go +27 -0
- package/internal/client/scripts_decode_test.go +125 -0
- package/internal/client/scripts_decode_windows.go +206 -0
- package/internal/client/scripts_test.go +726 -0
- package/internal/client/service.go +322 -18
- package/internal/client/service_test.go +51 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/session.go +92 -0
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +1 -5
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +8 -2
|
@@ -19,6 +19,7 @@ import (
|
|
|
19
19
|
"fmt"
|
|
20
20
|
"io"
|
|
21
21
|
"strings"
|
|
22
|
+
"sync"
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
// streamJSONResult 两家 CLI 收尾行(type=result)的公共形状。
|
|
@@ -81,9 +82,13 @@ func runStreamJSONIn(
|
|
|
81
82
|
// streamJSONInputPipe 常驻会话的 stdin:先写首条 user 消息(提示词 + 附件),
|
|
82
83
|
// 之后每从 appendCh 收到一条就再写一轮;appendCh 关闭 → 关掉写端 → 子进程收尾。
|
|
83
84
|
//
|
|
85
|
+
// replyCh 非 nil 时同时接收**原生控制应答行**(control_response,见 Request.Reply):
|
|
86
|
+
// 它和 appendCh 是两条独立的通道 —— 追加是「下一轮用户消息」,应答是「这一轮正等着的裁决」,
|
|
87
|
+
// 混在一条通道里会让 CLI 把应答当 user 消息读(实测后果:整轮卡死)。
|
|
88
|
+
//
|
|
84
89
|
// 返回的 reader 交给子进程当 stdin(不 EOF 就一直活着)。写失败(子进程已退)时
|
|
85
90
|
// 直接收摊,不让 goroutine 泄漏。
|
|
86
|
-
func streamJSONInputPipe(prompt string, atts []Attachment, appendCh <-chan string) (io.Reader, error) {
|
|
91
|
+
func streamJSONInputPipe(prompt string, atts []Attachment, appendCh <-chan string, replyCh <-chan string) (io.Reader, error) {
|
|
87
92
|
first, err := streamJSONUserLine(prompt, atts)
|
|
88
93
|
if err != nil {
|
|
89
94
|
return nil, err
|
|
@@ -94,19 +99,116 @@ func streamJSONInputPipe(prompt string, atts []Attachment, appendCh <-chan strin
|
|
|
94
99
|
if _, err := pw.Write([]byte(first)); err != nil {
|
|
95
100
|
return
|
|
96
101
|
}
|
|
97
|
-
for
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
for {
|
|
103
|
+
select {
|
|
104
|
+
case msg, ok := <-appendCh:
|
|
105
|
+
if !ok {
|
|
106
|
+
return // 追加入口关闭 = 收工
|
|
107
|
+
}
|
|
108
|
+
line, lerr := streamJSONUserLine(msg, nil)
|
|
109
|
+
if lerr != nil {
|
|
110
|
+
continue // 追加内容构造失败(不该发生):跳过这一条,别把整个会话带崩
|
|
111
|
+
}
|
|
112
|
+
if _, err := pw.Write([]byte(line)); err != nil {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
case raw, ok := <-replyCh:
|
|
116
|
+
if !ok {
|
|
117
|
+
replyCh = nil // 应答通道关闭不等于收工:继续等 append
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
if _, err := pw.Write(ensureTrailingNewline(raw)); err != nil {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
101
123
|
}
|
|
102
|
-
|
|
124
|
+
}
|
|
125
|
+
}()
|
|
126
|
+
return pr, nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// streamJSONLiveInput 一轮式调用的「活 stdin」:写完首条 user 消息后**保持写端打开**,
|
|
130
|
+
// 只用来把原生控制应答行(control_response)喂回去;done 关闭即 EOF。
|
|
131
|
+
//
|
|
132
|
+
// 为什么不能继续用 strings.Reader(2026-10-25 实测):那个 reader 一读完就 EOF,
|
|
133
|
+
// CLI 随后发出 control_request(计划审批)时 stdin 已经关掉 —— 应答没有落点,
|
|
134
|
+
// 整轮只能挂到超时。而只要把 stdin 留着,CLI 在被批准后就会继续跑完这一轮。
|
|
135
|
+
// ⚠️ 代价:stdin 一直开着时 CLI 在吐完 result 后**不会自己退出**(实测),
|
|
136
|
+
// 所以调用方必须在收到 turn_end / result / error 时立刻关掉 done —— 不能等 Stream 返回。
|
|
137
|
+
func streamJSONLiveInput(prompt string, atts []Attachment, replyCh <-chan string, done <-chan struct{}) (io.Reader, error) {
|
|
138
|
+
first, err := streamJSONUserLine(prompt, atts)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, err
|
|
141
|
+
}
|
|
142
|
+
pr, pw := io.Pipe()
|
|
143
|
+
go func() {
|
|
144
|
+
defer pw.Close()
|
|
145
|
+
if _, err := pw.Write([]byte(first)); err != nil {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
for {
|
|
149
|
+
select {
|
|
150
|
+
case <-done:
|
|
103
151
|
return
|
|
152
|
+
case raw, ok := <-replyCh:
|
|
153
|
+
if !ok {
|
|
154
|
+
replyCh = nil
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if _, err := pw.Write(ensureTrailingNewline(raw)); err != nil {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
104
160
|
}
|
|
105
161
|
}
|
|
106
162
|
}()
|
|
107
163
|
return pr, nil
|
|
108
164
|
}
|
|
109
165
|
|
|
166
|
+
// streamJSONStdinFor 按 Request 的通道形态挑 stdin 实现(claude / codebuddy 共用)。
|
|
167
|
+
//
|
|
168
|
+
// Append 非 nil → 常驻会话(续轮)流;Reply 一并接上
|
|
169
|
+
// Reply 非 nil → 一轮式「活 stdin」:只为准许回 control_response 而保持打开
|
|
170
|
+
// 都没有 → 老行为:一行写完即 EOF
|
|
171
|
+
//
|
|
172
|
+
// 返回的 close 由调用方 defer —— 活 stdin 下它负责 EOF,让 CLI 退出。
|
|
173
|
+
func streamJSONStdinFor(req Request, prompt string) (io.Reader, func(), error) {
|
|
174
|
+
if req.Append != nil {
|
|
175
|
+
pipe, err := streamJSONInputPipe(prompt, req.Attachments, req.Append, req.Reply)
|
|
176
|
+
return pipe, func() {}, err
|
|
177
|
+
}
|
|
178
|
+
if req.Reply != nil {
|
|
179
|
+
done := make(chan struct{})
|
|
180
|
+
var once sync.Once
|
|
181
|
+
closeFn := func() { once.Do(func() { close(done) }) }
|
|
182
|
+
pipe, err := streamJSONLiveInput(prompt, req.Attachments, req.Reply, done)
|
|
183
|
+
if err != nil {
|
|
184
|
+
return nil, func() {}, err
|
|
185
|
+
}
|
|
186
|
+
return pipe, closeFn, nil
|
|
187
|
+
}
|
|
188
|
+
line, err := streamJSONUserLine(prompt, req.Attachments)
|
|
189
|
+
if err != nil {
|
|
190
|
+
return nil, func() {}, err
|
|
191
|
+
}
|
|
192
|
+
return strings.NewReader(line), func() {}, nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// streamTerminalKind 这一条事件是否代表「本轮结束」(活 stdin 据此立刻 EOF)。
|
|
196
|
+
func streamTerminalKind(k StreamEventKind) bool {
|
|
197
|
+
switch k {
|
|
198
|
+
case KindTurnEnd, KindTurnFailed:
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ensureTrailingNewline 保证写进 stdin 的每一行都以换行结尾(NDJSON 一行一条)。
|
|
205
|
+
func ensureTrailingNewline(s string) []byte {
|
|
206
|
+
if strings.HasSuffix(s, "\n") {
|
|
207
|
+
return []byte(s)
|
|
208
|
+
}
|
|
209
|
+
return []byte(s + "\n")
|
|
210
|
+
}
|
|
211
|
+
|
|
110
212
|
// streamJSONUserLine 把「提示词 + 附件」拼成一行 stream-json 输入(含结尾换行)。
|
|
111
213
|
//
|
|
112
214
|
// content 块顺序:图片附件(image block)在前、提示词文本在后 —— 与官方
|
|
@@ -24,7 +24,10 @@ func argLogger(t *testing.T) (string, func() string) {
|
|
|
24
24
|
log := filepath.Join(dir, "args.log")
|
|
25
25
|
cli := filepath.Join(dir, "fake")
|
|
26
26
|
script := "#!/bin/sh\nprintf '%s\\n' \"$*\" > " + log +
|
|
27
|
-
"\necho '{\"type\":\"result\",\"result\":\"ok\"}'\n"
|
|
27
|
+
"\necho '{\"type\":\"result\",\"result\":\"ok\"}'\n" +
|
|
28
|
+
// trae 2.0 的 JSONL 事件(同一个假 CLI 要同时被 claude/codebuddy/trae 复用)
|
|
29
|
+
"\necho '{\"type\":\"item.completed\",\"item\":{\"id\":\"i\",\"type\":\"agent_message\",\"text\":\"ok\"}}'\n" +
|
|
30
|
+
"\necho '{\"type\":\"turn.completed\"}'\n"
|
|
28
31
|
if err := os.WriteFile(cli, []byte(script), 0o755); err != nil {
|
|
29
32
|
t.Fatal(err)
|
|
30
33
|
}
|
|
@@ -131,9 +134,9 @@ func TestTraeToolModeSuffix(t *testing.T) {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
// TestTraeToolModeMapping 固化 trae 的工具模式契约(2026-09-16
|
|
137
|
+
// TestTraeToolModeMapping 固化 trae 的工具模式契约(2026-09-16 真机验证,2.0 同构)。
|
|
135
138
|
//
|
|
136
|
-
//
|
|
139
|
+
// TRAE CLI 的权限开关语义:
|
|
137
140
|
// - --allowed-tool 只做「自动批准」,不裁剪工具集(--allowed-tool WebFetch
|
|
138
141
|
// 仍下发全部 18 个工具);
|
|
139
142
|
// - --disallowed-tool 才真正做减法;
|
package/internal/agent/trae.go
CHANGED
|
@@ -1,65 +1,58 @@
|
|
|
1
1
|
package agent
|
|
2
2
|
|
|
3
|
-
// trae.go -
|
|
3
|
+
// trae.go - TRAE CLI 引擎(2.0,命令名 traex)。
|
|
4
|
+
//
|
|
5
|
+
// 后端:TRAE CLI 2.0(0.2xx,Rust 版),非交互入口为
|
|
6
|
+
// `traex exec --json`,输出 Codex 同源的 JSONL 事件流。
|
|
7
|
+
// 参数构造与事件解析见 trae_exec.go。
|
|
4
8
|
//
|
|
5
|
-
// 后端:trae-cli(~/.local/bin/trae-cli)。非交互模式:-p,纯文本输出。
|
|
6
9
|
// 与 claude/codebuddy 的关键差异:
|
|
7
10
|
//
|
|
8
|
-
// -
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
// -
|
|
13
|
-
// -
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// -
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// - MaxTokens / Temperature 不支持:`-c <k>=<v>` 会整体覆盖模型配置块
|
|
20
|
-
// (实测注入 model.max_tokens 后模型名漂移并触发配额错误),因此这两个
|
|
21
|
-
// 参数构造时静默忽略(见 engine_base.go 矩阵②)。
|
|
11
|
+
// - 没有独立 system prompt 通道;system / 多轮消息由 FlattenPrompt
|
|
12
|
+
// 拼进提示词(off 模式再追加 noToolSuffix)。
|
|
13
|
+
// - 模型用 -m/--model(不是 1.x 的 -c model.name=);默认取
|
|
14
|
+
// ~/.trae/traecli.toml 的 model。
|
|
15
|
+
// - 续接:`exec resume <id>` / `exec resume --last`(没有 --continue)。
|
|
16
|
+
// - 工具:off 靠 --disallowed-tool 逐个减法;on / 白名单用 -y 全放行
|
|
17
|
+
// (trae 没有「只允许白名单」的表达能力)。
|
|
18
|
+
// - 没有 --query-timeout:超时完全由外层 runner 杀进程组。
|
|
19
|
+
// - MaxTokens / Temperature 不支持,静默忽略(见 engine_base.go 矩阵②)。
|
|
20
|
+
// - 附件没有原生图片通道(exec 的 -i 只收图片且与 1.x 行为不一致),
|
|
21
|
+
// 统一把绝对路径写进提示词,靠 CLI 的读文件工具。
|
|
22
22
|
//
|
|
23
|
-
// CLI 路径解析顺序:显式 BinPath → MAGIC_AGENT_TRAE_BIN →
|
|
24
|
-
//
|
|
23
|
+
// CLI 路径解析顺序:显式 BinPath → MAGIC_AGENT_TRAE_BIN → 安装器默认位 →
|
|
24
|
+
// PATH(探测链统一收敛在 engine_base.go 的 cliBase)。
|
|
25
25
|
|
|
26
26
|
import (
|
|
27
27
|
"context"
|
|
28
|
-
"encoding/json"
|
|
29
28
|
"fmt"
|
|
30
29
|
"os"
|
|
31
30
|
"path/filepath"
|
|
32
|
-
"strings"
|
|
33
31
|
"time"
|
|
34
32
|
)
|
|
35
33
|
|
|
36
|
-
// TraeEngine 通过
|
|
34
|
+
// TraeEngine 通过 traex 实现 Engine。
|
|
37
35
|
type TraeEngine struct {
|
|
38
36
|
// BinPath 显式指定 CLI 路径(空 = 自动探测)。测试注入用。
|
|
39
37
|
BinPath string
|
|
40
38
|
|
|
41
|
-
// Model 显式模型(非空时 -
|
|
39
|
+
// Model 显式模型(非空时 -m <model>)。
|
|
42
40
|
Model string
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
// Name 实现 Engine。
|
|
46
44
|
func (e *TraeEngine) Name() string { return "trae" }
|
|
47
45
|
|
|
48
|
-
// DefaultTraeTimeout 单次尝试默认超时(
|
|
46
|
+
// DefaultTraeTimeout 单次尝试默认超时(1 小时,与其余 CLI 引擎对齐)。
|
|
49
47
|
const DefaultTraeTimeout = time.Hour
|
|
50
48
|
|
|
51
49
|
// traeOffDisableTools off 模式下逐个 --disallowed-tool 禁用的实体工具。
|
|
52
50
|
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// 摘不掉,补集禁用后仍保留),Skill、TaskCreate/Get/List/Update、
|
|
56
|
-
// BashOutput、KillShell 属会话内辅助、无外部副作用,故不在禁用之列。
|
|
57
|
-
//
|
|
58
|
-
// 注意工具名必须是真实存在的:编辑与写入在 trae 里叫 Edit / Write ——
|
|
59
|
-
// 此前这里写的是并不存在的 `Replace`,等于 off 模式漏禁 Write。
|
|
51
|
+
// 取「会读文件 / 会改机器」的那批;Skill / 任务管理等会话内辅助工具
|
|
52
|
+
// 无外部副作用,不在禁用之列。
|
|
60
53
|
var traeOffDisableTools = []string{"Bash", "Edit", "Write", "Glob", "Grep", "Read"}
|
|
61
54
|
|
|
62
|
-
// bin 探测
|
|
55
|
+
// bin 探测 traex 路径(委托 cliBase 统一探测链)。
|
|
63
56
|
func (e *TraeEngine) bin() string {
|
|
64
57
|
return traeBase.resolve(e.BinPath)
|
|
65
58
|
}
|
|
@@ -73,42 +66,18 @@ func (e *TraeEngine) Detect() (bool, string) {
|
|
|
73
66
|
return true, p
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
// TraeDefaultModel
|
|
77
|
-
//
|
|
78
|
-
// 轻量扫描解析(不引入 yaml 依赖);文件缺失/格式变化返回空串。
|
|
69
|
+
// TraeDefaultModel 返回 trae CLI 当前配置的默认模型(如 "MiniMax-M3.1-Flash")。
|
|
70
|
+
// 2.0 读 ~/.trae/traecli.toml 的顶层 `model = "..."`;文件缺失/格式变化返回空串。
|
|
79
71
|
func TraeDefaultModel() string {
|
|
80
72
|
home, err := os.UserHomeDir()
|
|
81
73
|
if err != nil {
|
|
82
74
|
return ""
|
|
83
75
|
}
|
|
84
|
-
|
|
85
|
-
if err != nil {
|
|
86
|
-
return ""
|
|
87
|
-
}
|
|
88
|
-
inModelBlock := false
|
|
89
|
-
for _, ln := range strings.Split(string(data), "\n") {
|
|
90
|
-
trimmed := strings.TrimSpace(ln)
|
|
91
|
-
if !inModelBlock {
|
|
92
|
-
inModelBlock = ln == trimmed && strings.HasPrefix(trimmed, "model:")
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
if strings.HasPrefix(trimmed, "name:") {
|
|
96
|
-
v := strings.TrimSpace(strings.TrimPrefix(trimmed, "name:"))
|
|
97
|
-
v = strings.Trim(v, `"'`)
|
|
98
|
-
if v != "" {
|
|
99
|
-
return v
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if ln == trimmed && trimmed != "" {
|
|
103
|
-
inModelBlock = false
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return ""
|
|
76
|
+
return traeDefaultModelFromTOML(filepath.Join(home, ".trae", "traecli.toml"))
|
|
107
77
|
}
|
|
108
78
|
|
|
109
|
-
// ListModels 实现 ModelLister:`
|
|
110
|
-
//
|
|
111
|
-
// 取 name(= -c model.name=<name> 接受的那个标识),缺失时退回 real_name。
|
|
79
|
+
// ListModels 实现 ModelLister:`traex models --json` 是 CLI 自己的权威清单。
|
|
80
|
+
// 取 name(= -m 接受的标识),缺失时退回 real_name。
|
|
112
81
|
func (e *TraeEngine) ListModels(ctx context.Context) ([]string, error) {
|
|
113
82
|
stdout, err := probeCLI(ctx, "trae", e.bin(), "models", "--json")
|
|
114
83
|
if err != nil {
|
|
@@ -120,48 +89,26 @@ func (e *TraeEngine) ListModels(ctx context.Context) ([]string, error) {
|
|
|
120
89
|
}
|
|
121
90
|
models := modelNamesFromObjects(objs, "name", "real_name")
|
|
122
91
|
if len(models) == 0 {
|
|
123
|
-
return nil, fmt.Errorf("%w:
|
|
92
|
+
return nil, fmt.Errorf("%w: traecli models --json returned no entries", ErrNoModelSource)
|
|
124
93
|
}
|
|
125
94
|
return models, nil
|
|
126
95
|
}
|
|
127
96
|
|
|
128
|
-
// Complete 实现 Engine
|
|
97
|
+
// Complete 实现 Engine:单次 `exec --json`,把 JSONL 聚合成最终正文。
|
|
129
98
|
func (e *TraeEngine) Complete(ctx context.Context, req Request) (Response, error) {
|
|
130
99
|
start := time.Now()
|
|
131
100
|
bin := e.bin()
|
|
132
101
|
if bin == "" {
|
|
133
102
|
return Response{}, fmt.Errorf("trae CLI not found; set MAGIC_AGENT_TRAE_BIN")
|
|
134
103
|
}
|
|
104
|
+
return e.completeExec(ctx, bin, req, start)
|
|
105
|
+
}
|
|
135
106
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return Response{}, fmt.Errorf("trae: empty prompt")
|
|
139
|
-
}
|
|
140
|
-
// trae 没有独立 system 注入 flag;无 system prompt 时约束也要生效。
|
|
141
|
-
// 仅 off 模式追加:on/白名单下 noToolSuffix 会与「允许调用工具」冲突。
|
|
142
|
-
if req.SystemPrompt == "" && toolsIsOff(req) {
|
|
143
|
-
prompt += noToolSuffix
|
|
144
|
-
}
|
|
145
|
-
// 附件:trae-cli 没有图片/附件输入通道(--help 无 image/attach/input-format),
|
|
146
|
-
// 只能把绝对路径写进提示词,靠 trae 的读文件工具看 —— 因此需要工具可用。
|
|
147
|
-
prompt = appendAttachmentSection(prompt, req.Attachments)
|
|
148
|
-
|
|
149
|
-
args := e.buildArgs(req, prompt)
|
|
150
|
-
|
|
151
|
-
// workspace:trae 无工作目录 flag(有 --add-dir 但那是追加额外目录),子进程 cwd 即原生方式
|
|
152
|
-
stdout, stderr, err := runCLIIn(ctx, req.Workspace, bin, args...)
|
|
153
|
-
if err != nil {
|
|
154
|
-
return Response{}, wrapCliError("trae", stdout, stderr, err)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
text := strings.TrimSpace(stdout)
|
|
158
|
-
if text == "" {
|
|
159
|
-
return Response{}, fmt.Errorf("trae CLI returned empty output")
|
|
160
|
-
}
|
|
161
|
-
|
|
107
|
+
// modelLabel 本轮展示用模型名:请求显式 > 引擎注入 > CLI 配置默认。
|
|
108
|
+
func (e *TraeEngine) modelLabel(req Request) string {
|
|
162
109
|
modelLabel := stripModelPrefix(req.Model)
|
|
163
110
|
if modelLabel == "" {
|
|
164
|
-
modelLabel = e.Model
|
|
111
|
+
modelLabel = stripModelPrefix(e.Model)
|
|
165
112
|
}
|
|
166
113
|
if modelLabel == "" {
|
|
167
114
|
if m := TraeDefaultModel(); m != "" {
|
|
@@ -170,175 +117,35 @@ func (e *TraeEngine) Complete(ctx context.Context, req Request) (Response, error
|
|
|
170
117
|
modelLabel = "cli-default"
|
|
171
118
|
}
|
|
172
119
|
}
|
|
173
|
-
return
|
|
174
|
-
Text: text,
|
|
175
|
-
Model: modelLabel,
|
|
176
|
-
// 纯文本模式下 CLI 不回 session_id;续接请求时把传入的 id 原样
|
|
177
|
-
// 回传,让调用方可以逐轮透传(新会话仍为空)。
|
|
178
|
-
SessionID: req.SessionID,
|
|
179
|
-
Latency: time.Since(start),
|
|
180
|
-
}, nil
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// buildArgs 构造 trae-cli 参数(Complete 与 Stream 共用)。
|
|
184
|
-
func (e *TraeEngine) buildArgs(req Request, prompt string) []string {
|
|
185
|
-
args := []string{"-p"}
|
|
186
|
-
switch {
|
|
187
|
-
case req.SessionID != "":
|
|
188
|
-
// 续接指定会话:pflag 可选值风格(--resume string[="AUTO"]),
|
|
189
|
-
// 必须用等号形式 --resume=<id>;空格形式下裸 id 会被吞成 AUTO 默认值。
|
|
190
|
-
args = append(args, "--resume="+req.SessionID)
|
|
191
|
-
case req.Continue:
|
|
192
|
-
// 续接最近一次会话:裸 --resume 触发 AUTO 模式(实测 -p 可用)。
|
|
193
|
-
args = append(args, "--resume")
|
|
194
|
-
}
|
|
195
|
-
// 模型:显式值 > 引擎注入值;都为空则用 CLI 自身默认。
|
|
196
|
-
model := e.Model
|
|
197
|
-
if model == "" {
|
|
198
|
-
model = stripModelPrefix(req.Model)
|
|
199
|
-
}
|
|
200
|
-
if model != "" {
|
|
201
|
-
args = append(args, "-c", "model.name="+model)
|
|
202
|
-
}
|
|
203
|
-
// 统一参数矩阵:req.MaxTokens / req.Temperature 在 trae 上明确不支持、
|
|
204
|
-
// 静默忽略 —— `-c <k>=<v>` 会整体覆盖模型配置块,实测注入
|
|
205
|
-
// model.max_tokens 后模型名漂移并触发配额错误(见 engine_base.go 矩阵②)。
|
|
206
|
-
_ = req.MaxTokens
|
|
207
|
-
_ = req.Temperature
|
|
208
|
-
// 工具模式映射。trae-cli 的权限开关实测语义(2026-09-16 真机验证):
|
|
209
|
-
//
|
|
210
|
-
// --allowed-tool 只做「自动批准该工具」,**不裁剪**工具集
|
|
211
|
-
// (实测 --allowed-tool WebFetch 仍下发全部 18 个工具)
|
|
212
|
-
// --disallowed-tool 真正做减法,从工具集里移除该工具(实测 18 → 4)
|
|
213
|
-
// -y / --yolo 跳过全部权限检查 ⇒ 全工具可用
|
|
214
|
-
//
|
|
215
|
-
// 由此得出映射:
|
|
216
|
-
//
|
|
217
|
-
// off 逐个 --disallowed-tool 关掉内置工具(Bash/Edit/Write/Glob/Grep/Read)
|
|
218
|
-
// on -y(全放行;非 -p 交互场景下的权限旁路)
|
|
219
|
-
// allowlist 默认开启所有工具 ⇒ 与 on 同一条全放行路径(-y)
|
|
220
|
-
//
|
|
221
|
-
// ⚠️ 白名单在 trae 上「降级为全放行」是刻意的默认语义,不是实现漏做:
|
|
222
|
-
// trae 无法表达「只允许这几个工具」——想收窄只能对补集逐个 --disallowed-tool,
|
|
223
|
-
// 而补集随 CLI 版本漂移(今天 18 个工具、明天可能加),漏一条就等于白名单失效,
|
|
224
|
-
// 维护成本高于收益。故认定「默认开启所有工具」为白名单模式在 trae 上的契约,
|
|
225
|
-
// 与 codex 的白名单降级(engine_base.go 矩阵④:白名单 → 全放行)保持一致。
|
|
226
|
-
//
|
|
227
|
-
// 返回值里不再出现 --allowed-tool:既然 -y 已全量放行,逐条声明只是
|
|
228
|
-
// 「看起来像收窄」的噪音(这正是此前 allowlist 被 -y 架空的观感来源)。
|
|
229
|
-
// 需要收窄时请用 off 模式(真减法),而不要指望白名单。
|
|
230
|
-
tools := toolsOrDefault(req.Tools)
|
|
231
|
-
switch {
|
|
232
|
-
case tools.IsOff():
|
|
233
|
-
// 真实工具名,逐个减法;`Replace` 不是 trae 的工具(编辑/写入是
|
|
234
|
-
// Edit / Write),此前写成 Replace 等于漏禁 Write。
|
|
235
|
-
for _, t := range traeOffDisableTools {
|
|
236
|
-
args = append(args, "--disallowed-tool", t)
|
|
237
|
-
}
|
|
238
|
-
case tools.IsOn():
|
|
239
|
-
args = append(args, "-y")
|
|
240
|
-
default:
|
|
241
|
-
// 默认开启所有工具:全放行开关是唯一有效机制。
|
|
242
|
-
args = append(args, "-y")
|
|
243
|
-
}
|
|
244
|
-
// 把外层超时透传给 CLI 的 query-timeout(取上限 600s,CLI 单查询上限)。
|
|
245
|
-
if req.Timeout > 0 {
|
|
246
|
-
qt := req.Timeout
|
|
247
|
-
if qt > 600*time.Second {
|
|
248
|
-
qt = 600 * time.Second
|
|
249
|
-
}
|
|
250
|
-
args = append(args, "--query-timeout", qt.String())
|
|
251
|
-
} else {
|
|
252
|
-
args = append(args, "--query-timeout", "600s")
|
|
253
|
-
}
|
|
254
|
-
args = append(args, prompt)
|
|
255
|
-
return args
|
|
120
|
+
return modelLabel
|
|
256
121
|
}
|
|
257
122
|
|
|
258
|
-
// Stream 实现 Streamer
|
|
259
|
-
// trae 的 stream-json 用 delta.content 直出正文增量;
|
|
260
|
-
// 模型侧不开 reasoning,无 thinking 通道(Thinking 恒为空)。
|
|
123
|
+
// Stream 实现 Streamer:逐行消费 `exec --json`,正文 / 思考 / 工具实时转发。
|
|
261
124
|
func (e *TraeEngine) Stream(ctx context.Context, req Request, onEvent func(StreamEvent)) (StreamResult, error) {
|
|
262
125
|
start := time.Now()
|
|
263
126
|
bin := e.bin()
|
|
264
127
|
if bin == "" {
|
|
265
128
|
return StreamResult{}, fmt.Errorf("trae CLI not found; set MAGIC_AGENT_TRAE_BIN")
|
|
266
129
|
}
|
|
130
|
+
return e.streamExec(ctx, bin, req, start, onEvent)
|
|
131
|
+
}
|
|
267
132
|
|
|
133
|
+
// traePrompt 组装 2.0 的单条提示词(system + 多轮 + off 约束 + 附件路径)。
|
|
134
|
+
func traePrompt(req Request) (string, error) {
|
|
268
135
|
prompt := FlattenPrompt(req.SystemPrompt, req.Messages, toolsIsOff(req))
|
|
269
136
|
if prompt == "" {
|
|
270
|
-
return
|
|
137
|
+
return "", fmt.Errorf("trae: empty prompt")
|
|
271
138
|
}
|
|
272
139
|
if req.SystemPrompt == "" && toolsIsOff(req) {
|
|
273
140
|
prompt += noToolSuffix
|
|
274
141
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
args := e.buildArgs(req, prompt)
|
|
278
|
-
args = append(args, "--output-format", "stream-json", "--include-partial-messages")
|
|
279
|
-
|
|
280
|
-
acc := &streamAccumulator{Engine: e.Name(), OnEvent: onEvent, OnSessionID: req.OnSessionID}
|
|
281
|
-
var fin struct {
|
|
282
|
-
Type string `json:"type"`
|
|
283
|
-
Subtype string `json:"subtype"`
|
|
284
|
-
IsError bool `json:"is_error"`
|
|
285
|
-
Result string `json:"result"`
|
|
286
|
-
SessionID string `json:"session_id"`
|
|
287
|
-
}
|
|
288
|
-
seenResult := false
|
|
289
|
-
|
|
290
|
-
err := runStreamCLIIn(ctx, req.Workspace, bin, args, func(line string) error {
|
|
291
|
-
isResult, perr := acc.handleNDJSONLine(line)
|
|
292
|
-
if perr != nil {
|
|
293
|
-
return perr
|
|
294
|
-
}
|
|
295
|
-
if isResult {
|
|
296
|
-
_ = json.Unmarshal([]byte(line), &fin)
|
|
297
|
-
seenResult = true
|
|
298
|
-
}
|
|
299
|
-
return nil
|
|
300
|
-
})
|
|
301
|
-
if err != nil {
|
|
302
|
-
return StreamResult{}, err
|
|
303
|
-
}
|
|
304
|
-
if !seenResult {
|
|
305
|
-
err := fmt.Errorf("trae CLI stream ended without result line")
|
|
306
|
-
acc.emitTurnFailed(acc.SessionID, err)
|
|
307
|
-
return StreamResult{}, err
|
|
308
|
-
}
|
|
309
|
-
if fin.IsError {
|
|
310
|
-
return StreamResult{}, fmt.Errorf("trae CLI error (subtype=%s): %s", fin.Subtype, truncateStr(fin.Result, 500))
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
text := fin.Result
|
|
314
|
-
if text == "" {
|
|
315
|
-
text = strings.TrimSpace(acc.Text.String())
|
|
316
|
-
}
|
|
317
|
-
if text == "" {
|
|
318
|
-
return StreamResult{}, fmt.Errorf("trae CLI returned empty result")
|
|
319
|
-
}
|
|
142
|
+
return appendAttachmentSection(prompt, req.Attachments), nil
|
|
143
|
+
}
|
|
320
144
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if modelLabel == "" {
|
|
326
|
-
if m := TraeDefaultModel(); m != "" {
|
|
327
|
-
modelLabel = m
|
|
328
|
-
} else {
|
|
329
|
-
modelLabel = "cli-default"
|
|
330
|
-
}
|
|
145
|
+
// traeModelFor 模型取值:请求显式 > 引擎注入(都去掉 `trae/` 前缀)。
|
|
146
|
+
func traeModelFor(req Request, e *TraeEngine) string {
|
|
147
|
+
if m := stripModelPrefix(req.Model); m != "" {
|
|
148
|
+
return m
|
|
331
149
|
}
|
|
332
|
-
return
|
|
333
|
-
Response: Response{
|
|
334
|
-
Engine: e.Name(),
|
|
335
|
-
Text: text,
|
|
336
|
-
Model: modelLabel,
|
|
337
|
-
SessionID: fin.SessionID,
|
|
338
|
-
Latency: time.Since(start),
|
|
339
|
-
},
|
|
340
|
-
Thinking: acc.Thinking.String(),
|
|
341
|
-
// trae 协议不暴露独立工具事件通道;Tools 恒为 nil。
|
|
342
|
-
// 见 stream.go 顶部协议说明。
|
|
343
|
-
}, nil
|
|
150
|
+
return stripModelPrefix(e.Model)
|
|
344
151
|
}
|