magic-agent 2.4.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT-CONTRACT.md +284 -30
- package/README.md +15 -14
- package/client-ui/pc/app.css +32 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +37 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +241 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +63 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +81 -0
- package/client-ui/shared/conv/conv.css +79 -19
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +336 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/index.html +2932 -333
- package/client-ui-mobile/reqs.css +102 -0
- package/client-ui-mobile/reqs.js +485 -70
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +21 -0
- package/client-ui-mobile/scripts.js +539 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +33 -9
- package/cmd/magic-client/main.go +57 -26
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_resume.go +13 -2
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +136 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +108 -6
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +160 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/events.go +4 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +122 -5
- package/internal/cli/session.go +6 -0
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/daemon.go +90 -17
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/jobs.go +294 -21
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +198 -1
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/restart.go +134 -0
- package/internal/client/restart_test.go +151 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_other.go +27 -0
- package/internal/client/scripts_decode_test.go +125 -0
- package/internal/client/scripts_decode_windows.go +206 -0
- package/internal/client/scripts_test.go +726 -0
- package/internal/client/service.go +322 -18
- package/internal/client/service_test.go +51 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/session.go +92 -0
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +1 -5
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +8 -2
package/internal/agent/dsh.go
CHANGED
|
@@ -84,6 +84,7 @@ import (
|
|
|
84
84
|
"fmt"
|
|
85
85
|
"os"
|
|
86
86
|
"path/filepath"
|
|
87
|
+
"strconv"
|
|
87
88
|
"strings"
|
|
88
89
|
"time"
|
|
89
90
|
)
|
|
@@ -170,16 +171,17 @@ func (e *DshEngine) Detect() (bool, string) {
|
|
|
170
171
|
// 什么的东西」—— 网关列出的上百个模型并不都能用,只有配置里声明过的才会被路由接受。
|
|
171
172
|
// 故**配置层才是权威**,也顺带避免了每次探测都发网络请求 + 需要解密凭据。
|
|
172
173
|
//
|
|
173
|
-
// 配置位置随 dsh 版本变过(见 dshConfigDocs):0.1.7
|
|
174
|
-
//
|
|
174
|
+
// 配置位置随 dsh 版本变过(见 dshConfigDocs):0.1.7 起是 cordis.patch.yml
|
|
175
|
+
// (profile 级,0.2.1 起也会写在 home 级 $DSH_HOME/cordis.patch.yml),之前是
|
|
176
|
+
// $DSH_HOME/settings.yaml。各处都读、取并集 ——
|
|
175
177
|
// 前者是 SDK 通道,后者是 headless 回退通道,magic-agent 两条都会用到。
|
|
176
178
|
//
|
|
177
179
|
// 输出形态 `provider/model`(与 openclaw 的 `provider/model` 同构):dsh 的模型只在
|
|
178
180
|
// 所属路由内有意义,同名模型可能出现在两条路由上,裸 id 会歧义。
|
|
179
181
|
//
|
|
180
|
-
// 这份清单**就是** `-m`
|
|
181
|
-
//
|
|
182
|
-
//
|
|
182
|
+
// 这份清单**就是** `-m` 的可取值:SDK 通道转成 initialize 的 provider/model
|
|
183
|
+
// (见 dshResolveRoute);headless 通道没有 --model,改由每轮临时 `--patch` 覆盖配置层的
|
|
184
|
+
// agent-default-model(见 dshHeadlessPatchFor)—— 两条通道都能按 -m 换模型。
|
|
183
185
|
func (e *DshEngine) ListModels(_ context.Context) ([]string, error) {
|
|
184
186
|
docs := dshConfigDocs()
|
|
185
187
|
var models []string
|
|
@@ -202,7 +204,7 @@ func (e *DshEngine) ListModels(_ context.Context) ([]string, error) {
|
|
|
202
204
|
tried = strings.Join(paths, "、")
|
|
203
205
|
}
|
|
204
206
|
return nil, fmt.Errorf("%w: 没有读到已配置的模型;找过 %s。"+
|
|
205
|
-
"dsh 0.1.7 起 provider 声明在
|
|
207
|
+
"dsh 0.1.7 起 provider 声明在 cordis.patch.yml(profile 级或 home 级,可用 dsh 的「设置 → 模型」写入),"+
|
|
206
208
|
"更早的版本在 settings.yaml 的 llm-pi-ai.providers.<id>.models 段",
|
|
207
209
|
ErrNoModelSource, tried)
|
|
208
210
|
}
|
|
@@ -343,27 +345,24 @@ func dshNativeImageMIME(mime string) bool {
|
|
|
343
345
|
// 形态:`dsh --profile headless [--session-id <id>] "<prompt>"`,stdout 即最终正文。
|
|
344
346
|
// --session-id 是 dsh 自己的续接入口(见 dsh_resume.go),只在续接轮带上。
|
|
345
347
|
func (e *DshEngine) completeHeadless(ctx context.Context, req Request, bin, sessionID, prompt string, start time.Time) (Response, error) {
|
|
346
|
-
// headless 没有 --model 参数:-m
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if req.Model != "" && sessionID == "" {
|
|
351
|
-
fmt.Fprintf(stderr, " ⚠ dsh: headless 通道下 --model %s 不生效(无 --model 参数);"+
|
|
352
|
-
"换模型请改 $DSH_HOME/settings.yaml 的 agent-default-model 段,或用 dsh 的 Settings → Models\n", req.Model)
|
|
353
|
-
}
|
|
348
|
+
// headless 没有 --model 参数:-m 只能经 `--patch` 覆盖配置层的 agent-default-model
|
|
349
|
+
//(见 dshHeadlessPatchFor)。拿不到 --patch 才退回「配置默认模型」,并告警一次。
|
|
350
|
+
patchPath, cleanup := dshHeadlessPatchFor(ctx, bin, req.Model)
|
|
351
|
+
defer cleanup()
|
|
354
352
|
|
|
355
|
-
|
|
353
|
+
promptArg, stdinText := dshHeadlessPromptArg(prompt)
|
|
354
|
+
args := dshHeadlessArgs(e.profile(), sessionID, patchPath, promptArg, false)
|
|
356
355
|
|
|
357
356
|
// workspace:dsh 的「调用时所在目录即默认 workspace 根」正是子进程 cwd 的语义。
|
|
358
357
|
// 档位同 SDK 通道:走 DSH_PERMISSION_MODE(dsh 没有逐调用的权限 flag)。
|
|
359
|
-
stdout, stderrText, runErr :=
|
|
358
|
+
stdout, stderrText, runErr := runCLIEnvInStdin(ctx, req.Workspace, dshPermissionEnv(req.Permission), stdinText, bin, args...)
|
|
360
359
|
|
|
361
360
|
resp := Response{
|
|
362
361
|
Engine: e.Name(),
|
|
363
362
|
// headless 的 stdout 就是最终助手正文(推理增量走 stderr,不混入)。
|
|
364
363
|
Text: strings.TrimSpace(stdout),
|
|
365
|
-
//
|
|
366
|
-
Model:
|
|
364
|
+
// 模型标签:本轮真的用哪个(有 -m 且 patch 生效 → -m;否则配置默认)。
|
|
365
|
+
Model: dshHeadlessModelLabel(req, patchPath),
|
|
367
366
|
Latency: time.Since(start),
|
|
368
367
|
}
|
|
369
368
|
if resp.Model == "" {
|
|
@@ -386,18 +385,19 @@ func (e *DshEngine) completeHeadless(ctx context.Context, req Request, bin, sess
|
|
|
386
385
|
return resp, nil
|
|
387
386
|
}
|
|
388
387
|
|
|
389
|
-
//
|
|
388
|
+
// dshHeadlessModelLabel headless 轮次对外报的模型标签(= 本轮真正用的那个)。
|
|
390
389
|
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
390
|
+
// patch 生效(有 -m 且 dsh 支持 --patch)→ 就是 -m 解析出的 `provider/model`
|
|
391
|
+
// 没有 -m → 读 $DSH_HOME 的 agent-default-model
|
|
392
|
+
// (读不到返回空串,调用方回落 "config-default")
|
|
393
|
+
//
|
|
394
|
+
// 旧实现里续接轮直接回 `req.Model` 当「最佳近似」,但实际跑的是配置默认模型 ——
|
|
395
|
+
// 2026-10-25 用户报障就是被这个假标签误导(界面写 volcengine/…,真实是 minimax/…)。
|
|
396
|
+
// 现在模型真的经 --patch 落到配置层,标签才跟事实一致。
|
|
397
|
+
func dshHeadlessModelLabel(req Request, patchPath string) string {
|
|
398
|
+
if patchPath != "" {
|
|
399
|
+
if p, m := dshResolveRoute(req.Model); p != "" && m != "" {
|
|
400
|
+
return p + "/" + m
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
return DshConfiguredModel()
|
|
@@ -405,17 +405,26 @@ func dshHeadlessModel(req Request, sessionID string) string {
|
|
|
405
405
|
|
|
406
406
|
// dshHeadlessArgs 构造 headless 的参数:**启动器 flag 在前,app 参数在后**。
|
|
407
407
|
//
|
|
408
|
-
// ["--profile", <profile>] [+ "--session-id", <id>] [+ "--json"] + ["<prompt>"]
|
|
408
|
+
// [--patch <file>] ["--profile", <profile>] [+ "--session-id", <id>] [+ "--json"] + ["<prompt>"]
|
|
409
|
+
//
|
|
410
|
+
// ⚠️ 三类 flag 的归属不同(顺序不能错):
|
|
411
|
+
// - `--patch` 是**启动器 flag**(LauncherOptions),必须在 --profile **之前** —— 放在
|
|
412
|
+
// 后面会被 headless 当成未知 app 参数(实测 `dsh --profile headless --patch` → unknown option)。
|
|
413
|
+
// - --session-id / --json 是 **headless 自己的 flag**,排在 --profile 之后、任务文本之前。
|
|
414
|
+
// - 启动器在第一个它不认识的 token 处结束解析,剩下的全交给 app。
|
|
409
415
|
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// 超长 prompt 会撞 ARG_MAX:需要喂超长文本时先落成文件,让 dsh 用读文件工具看。
|
|
416
|
+
// prompt 默认走位置参数(headless 唯一的 app 参数,无 --prompt)。Windows 上改走 stdin
|
|
417
|
+
// 的 `-`(见 dshHeadlessPromptArg);位置参数形态下超长 prompt 会撞 ARG_MAX:需要喂超长
|
|
418
|
+
// 文本时先落成文件,让 dsh 用读文件工具看。
|
|
414
419
|
//
|
|
415
420
|
// ⚠️ 只有 headless 用得到它:SDK 通道的 prompt 走 session/prompt 的 contentBlocks
|
|
416
421
|
// (见 dsh_sdk.go),既不占命令行也不受 ARG_MAX 限制。
|
|
417
|
-
func dshHeadlessArgs(profile, sessionID, prompt string, jsonEvents bool) []string {
|
|
418
|
-
args := []string
|
|
422
|
+
func dshHeadlessArgs(profile, sessionID, patchPath, prompt string, jsonEvents bool) []string {
|
|
423
|
+
args := make([]string, 0, 8)
|
|
424
|
+
if patchPath != "" {
|
|
425
|
+
args = append(args, "--patch", patchPath)
|
|
426
|
+
}
|
|
427
|
+
args = append(args, "--profile", profile)
|
|
419
428
|
if sessionID != "" {
|
|
420
429
|
args = append(args, "--session-id", sessionID)
|
|
421
430
|
}
|
|
@@ -425,6 +434,67 @@ func dshHeadlessArgs(profile, sessionID, prompt string, jsonEvents bool) []strin
|
|
|
425
434
|
return append(args, prompt)
|
|
426
435
|
}
|
|
427
436
|
|
|
437
|
+
// dshHeadlessPromptArg 决定 headless 的提示词怎么进 dsh:位置参数,还是 stdin("-")。
|
|
438
|
+
//
|
|
439
|
+
// Windows 上必须走 stdin:dsh 在 PATH 上解析到 npm 的 `dsh.cmd`,launchWrap 会以
|
|
440
|
+
// `powershell -Command "& 'dsh.cmd' …"` 启动,参数还要再过一层 cmd.exe 的 `%*` 重解析 ——
|
|
441
|
+
// **多行位置参数会被整段丢掉**(2026-10-25 实测:单行正常,带换行的提示词模型收到的是空消息)。
|
|
442
|
+
// dsh 的 headless 原生支持 `-` 读 stdin(help 原文:task `-` reads stdin),走 stdin 既绕开
|
|
443
|
+
// cmd 解析层,也顺带避开 ARG_MAX。unix 无此问题,保持原来的位置参数形态。
|
|
444
|
+
func dshHeadlessPromptArg(prompt string) (arg, stdinText string) {
|
|
445
|
+
if platformIsWindows() {
|
|
446
|
+
return "-", prompt
|
|
447
|
+
}
|
|
448
|
+
return prompt, ""
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// dshHeadlessPatchFor 把本轮 `-m` 落成一份临时 `--patch`(只覆盖 agent-default-model)。
|
|
452
|
+
//
|
|
453
|
+
// 为什么需要:headless 通道**没有 --model**(`dsh --profile headless --help` 只有
|
|
454
|
+
// --json / --session-id),模型只由配置层 agent-default-model 决定;而续接轮又**必须**
|
|
455
|
+
// 走 headless(SDK 没有 resume,见 dsh_resume.go)。2026-10-25 用户报障就是这条缝:
|
|
456
|
+
// 界面选 volcengine/deepseek-v4.1-flash-1017,续接轮实际跑配置默认的 minimax/MiniMax-M3.1-Flash,
|
|
457
|
+
// 报的是 MiniMax 的「Token Plan 用量上限」,看上去像模型选错了。
|
|
458
|
+
//
|
|
459
|
+
// 顶层启动器支持 `dsh --patch <path>`(profile 之上再叠一层),所以每轮按 -m 写一份临时
|
|
460
|
+
// patch,进程结束后删掉 —— 模型选择在续接轮 / 回退轮同样生效。
|
|
461
|
+
//
|
|
462
|
+
// 返回 (路径, 清理函数);没有 -m / 拆不出 provider/model / dsh 不认 --patch 时返回 "",
|
|
463
|
+
// 此时沿用配置默认模型并告警一次(旧行为,但不再谎报模型)。
|
|
464
|
+
func dshHeadlessPatchFor(ctx context.Context, bin, reqModel string) (string, func()) {
|
|
465
|
+
model := strings.TrimSpace(reqModel)
|
|
466
|
+
if model == "" {
|
|
467
|
+
return "", func() {}
|
|
468
|
+
}
|
|
469
|
+
if !dshHeadlessCapabilities(ctx, bin).Patch {
|
|
470
|
+
fmt.Fprintf(stderr, " ⚠ dsh: 这台 dsh 不认 --patch,headless 通道无法按 -m %s 换模型"+
|
|
471
|
+
"(实跑配置默认模型);请升级 dsh,或改 $DSH_HOME 的 agent-default-model\n", model)
|
|
472
|
+
return "", func() {}
|
|
473
|
+
}
|
|
474
|
+
provider, id := dshResolveRoute(model)
|
|
475
|
+
if provider == "" || id == "" {
|
|
476
|
+
fmt.Fprintf(stderr, " ⚠ dsh: 无法从 -m %s 拆出 provider/model,本轮沿用配置默认模型\n", model)
|
|
477
|
+
return "", func() {}
|
|
478
|
+
}
|
|
479
|
+
f, err := os.CreateTemp("", "magic-agent-dsh-model-*.yml")
|
|
480
|
+
if err != nil {
|
|
481
|
+
fmt.Fprintf(stderr, " ⚠ dsh: 写临时 patch 失败(%v),本轮沿用配置默认模型\n", err)
|
|
482
|
+
return "", func() {}
|
|
483
|
+
}
|
|
484
|
+
// YAML 双引号标量 = JSON 字符串,用 strconv.Quote 免得模型 id 里的特殊字符破坏结构。
|
|
485
|
+
content := "- id: agent-default-model\n config:\n provider: " + strconv.Quote(provider) +
|
|
486
|
+
"\n model: " + strconv.Quote(id) + "\n"
|
|
487
|
+
_, werr := f.WriteString(content)
|
|
488
|
+
cerr := f.Close()
|
|
489
|
+
if werr != nil || cerr != nil {
|
|
490
|
+
_ = os.Remove(f.Name())
|
|
491
|
+
fmt.Fprintf(stderr, " ⚠ dsh: 写临时 patch 失败(%v),本轮沿用配置默认模型\n", errors.Join(werr, cerr))
|
|
492
|
+
return "", func() {}
|
|
493
|
+
}
|
|
494
|
+
path := f.Name()
|
|
495
|
+
return path, func() { _ = os.Remove(path) }
|
|
496
|
+
}
|
|
497
|
+
|
|
428
498
|
// ── 流式(--stream)────────────────────────────────────────────
|
|
429
499
|
//
|
|
430
500
|
// dsh 的流式形态与通道有关,如实接线:
|
|
@@ -532,14 +602,12 @@ func (e *DshEngine) Stream(ctx context.Context, req Request, onEvent func(Stream
|
|
|
532
602
|
// 有 --json 时按事件流逐条上报(推理 / 正文 / 工具调用,能力与 SDK 通道同构);
|
|
533
603
|
// 老版本没有 --json 就退回「stderr 推理增量 + stdout 收尾正文」,工具调用拿不到 —— 如实说明。
|
|
534
604
|
func (e *DshEngine) streamHeadless(ctx context.Context, req Request, bin, sessionID, prompt string, onEvent func(StreamEvent), start time.Time) (StreamResult, error) {
|
|
535
|
-
// 同 completeHeadless
|
|
536
|
-
if req.Model != "" && sessionID == "" {
|
|
537
|
-
fmt.Fprintf(stderr, " ⚠ dsh: headless 通道下 --model %s 不生效(无 --model 参数);"+
|
|
538
|
-
"换模型请改 $DSH_HOME/settings.yaml 的 agent-default-model 段,或用 dsh 的 Settings → Models\n", req.Model)
|
|
539
|
-
}
|
|
540
|
-
|
|
605
|
+
// 同 completeHeadless:-m 经 --patch 覆盖配置层(headless 没有 --model)。
|
|
541
606
|
caps := dshHeadlessCapabilities(ctx, bin)
|
|
542
|
-
|
|
607
|
+
patchPath, cleanup := dshHeadlessPatchFor(ctx, bin, req.Model)
|
|
608
|
+
defer cleanup()
|
|
609
|
+
promptArg, stdinText := dshHeadlessPromptArg(prompt)
|
|
610
|
+
args := dshHeadlessArgs(e.profile(), sessionID, patchPath, promptArg, caps.JSON)
|
|
543
611
|
|
|
544
612
|
emit := func(ev StreamEvent) {
|
|
545
613
|
if onEvent != nil {
|
|
@@ -607,11 +675,11 @@ func (e *DshEngine) streamHeadless(ctx context.Context, req Request, bin, sessio
|
|
|
607
675
|
runErr error
|
|
608
676
|
)
|
|
609
677
|
if caps.JSON {
|
|
610
|
-
stdout, stderrText, runErr =
|
|
678
|
+
stdout, stderrText, runErr = runStreamLinesInStdin(ctx, req.Workspace, dshPermissionEnv(req.Permission), stdinText, bin, args, true, onLine)
|
|
611
679
|
} else {
|
|
612
680
|
// 老版本:推理增量在 stderr("dsh: reasoning:" 起头),正文在 stdout 收尾时一次给出。
|
|
613
681
|
inReasoning := false
|
|
614
|
-
stdout, stderrText, runErr =
|
|
682
|
+
stdout, stderrText, runErr = runStreamLinesInStdin(ctx, req.Workspace, dshPermissionEnv(req.Permission), stdinText, bin, args, false, func(line string) error {
|
|
615
683
|
// 标题行本身不是内容;它出现即进入推理区段。
|
|
616
684
|
if strings.HasPrefix(line, dshReasoningPrefix) {
|
|
617
685
|
inReasoning = true
|
|
@@ -650,7 +718,7 @@ func (e *DshEngine) streamHeadless(ctx context.Context, req Request, bin, sessio
|
|
|
650
718
|
resp := Response{
|
|
651
719
|
Engine: e.Name(),
|
|
652
720
|
Text: text,
|
|
653
|
-
Model:
|
|
721
|
+
Model: dshHeadlessModelLabel(req, patchPath),
|
|
654
722
|
SessionID: sid,
|
|
655
723
|
Latency: time.Since(start),
|
|
656
724
|
}
|
|
@@ -711,6 +779,9 @@ func DshHome() string {
|
|
|
711
779
|
// ≤ 0.1.6 $DSH_HOME/settings.yaml(顶层段 agent-default-model / llm-pi-ai)
|
|
712
780
|
// ≥ 0.1.7 各 profile 的 $DSH_HOME/profiles/<name>/cordis.patch.yml
|
|
713
781
|
// (settings 体系重构后,providers 由「设置 → 模型」页面写进这份 patch)
|
|
782
|
+
// ≥ 0.2.1 也会写在 home 级 $DSH_HOME/cordis.patch.yml(对所有 profile 生效;
|
|
783
|
+
// 2026-10-23 实测 0.2.1-rc.2 重写配置后 profile 级 patch 清成空 [],
|
|
784
|
+
// provider 只剩 home 级这一份 —— 不读它就整条链空手而归)
|
|
714
785
|
//
|
|
715
786
|
// ⚠️ 0.1.7 首次启动会把旧的 settings.yaml 改名为 settings.yaml.imported
|
|
716
787
|
//(官方 dsh-settings:改名先于首次写入,被组合拒绝的 section 只留在改名后的文件里)。
|
|
@@ -740,7 +811,8 @@ func (d dshConfigDoc) legacyText() string {
|
|
|
740
811
|
// (MAGIC_AGENT_DSH_PROFILE 可改;headless 回退通道见 ②)
|
|
741
812
|
// ② headless profile 的 cordis.patch.yml —— SDK 不可用时的回退通道,
|
|
742
813
|
// 列模型时不能漏(两条通道 magic-agent 都会用到)
|
|
743
|
-
// ③ $DSH_HOME/
|
|
814
|
+
// ③ $DSH_HOME/cordis.patch.yml —— home 级 patch,对所有 profile 生效
|
|
815
|
+
// ④ $DSH_HOME/settings.yaml —— 0.1.7 之前的位置,兼容未升级的装机
|
|
744
816
|
func dshConfigDocs() []dshConfigDoc {
|
|
745
817
|
home := DshHome()
|
|
746
818
|
if home == "" {
|
|
@@ -750,12 +822,14 @@ func dshConfigDocs() []dshConfigDoc {
|
|
|
750
822
|
if names[0] != dshDefaultProfile {
|
|
751
823
|
names = append(names, dshDefaultProfile)
|
|
752
824
|
}
|
|
753
|
-
docs := make([]dshConfigDoc, 0, len(names)+
|
|
825
|
+
docs := make([]dshConfigDoc, 0, len(names)+2)
|
|
754
826
|
for _, name := range names {
|
|
755
827
|
p := filepath.Join(home, "profiles", name, "cordis.patch.yml")
|
|
756
828
|
docs = append(docs, dshConfigDoc{Path: p, Patch: true, Text: readFileOrEmpty(p)})
|
|
757
829
|
}
|
|
758
|
-
p := filepath.Join(home, "
|
|
830
|
+
p := filepath.Join(home, "cordis.patch.yml")
|
|
831
|
+
docs = append(docs, dshConfigDoc{Path: p, Patch: true, Text: readFileOrEmpty(p)})
|
|
832
|
+
p = filepath.Join(home, "settings.yaml")
|
|
759
833
|
return append(docs, dshConfigDoc{Path: p, Text: readFileOrEmpty(p)})
|
|
760
834
|
}
|
|
761
835
|
|
|
@@ -155,10 +155,13 @@ func dshSessionDir() string {
|
|
|
155
155
|
|
|
156
156
|
// ── 能力探测:这台 dsh 的 headless 认不认 --session-id / --json ──
|
|
157
157
|
|
|
158
|
-
// dshHeadlessCaps headless 支持的两个 flag
|
|
158
|
+
// dshHeadlessCaps headless 支持的两个 flag(外加顶层启动器的 --patch)。
|
|
159
159
|
type dshHeadlessCaps struct {
|
|
160
160
|
SessionID bool // --session-id(0.1.7+ 才有;没有就当不了真续接)
|
|
161
161
|
JSON bool // --json(事件流;没有就走旧版 stderr 推理增量)
|
|
162
|
+
// Patch 顶层启动器的 `--patch <path>`(在 profile 之上再叠一层配置)。
|
|
163
|
+
// headless 没有 --model,续接轮要按 -m 换模型只能靠它(见 dshHeadlessModelPatch)。
|
|
164
|
+
Patch bool
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
var dshCapsCache sync.Map // bin → dshHeadlessCaps
|
|
@@ -176,7 +179,7 @@ func dshHeadlessCapabilities(ctx context.Context, bin string) dshHeadlessCaps {
|
|
|
176
179
|
if v, ok := dshCapsCache.Load(bin); ok {
|
|
177
180
|
return v.(dshHeadlessCaps)
|
|
178
181
|
}
|
|
179
|
-
caps := dshHeadlessCaps{SessionID: true, JSON: true}
|
|
182
|
+
caps := dshHeadlessCaps{SessionID: true, JSON: true, Patch: true}
|
|
180
183
|
if stdout, stderr, err := runCLI(ctx, bin, "--profile", "headless", "--help"); err == nil {
|
|
181
184
|
help := stdout + stderr
|
|
182
185
|
if strings.Contains(help, "Usage:") && strings.Contains(help, "--profile") {
|
|
@@ -184,6 +187,14 @@ func dshHeadlessCapabilities(ctx context.Context, bin string) dshHeadlessCaps {
|
|
|
184
187
|
caps.JSON = strings.Contains(help, "--json")
|
|
185
188
|
}
|
|
186
189
|
}
|
|
190
|
+
// --patch 是**顶层启动器**的 flag,不在 headless 的 app help 里 —— 单独探一次。
|
|
191
|
+
// 同样只在输出确实像 help 时才据它判断(否则保留乐观默认 true)。
|
|
192
|
+
if stdout, stderr, err := runCLI(ctx, bin, "--help"); err == nil {
|
|
193
|
+
help := stdout + stderr
|
|
194
|
+
if strings.Contains(help, "Usage:") {
|
|
195
|
+
caps.Patch = strings.Contains(help, "--patch")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
187
198
|
dshCapsCache.Store(bin, caps)
|
|
188
199
|
return caps
|
|
189
200
|
}
|
|
@@ -382,22 +382,102 @@ func TestDshHeadlessArgs(t *testing.T) {
|
|
|
382
382
|
cases := []struct {
|
|
383
383
|
name string
|
|
384
384
|
sessionID string
|
|
385
|
+
patch string
|
|
385
386
|
json bool
|
|
386
387
|
want []string
|
|
387
388
|
}{
|
|
388
|
-
{"纯 headless", "", false, []string{"--profile", "headless", "任务"}},
|
|
389
|
-
{"续接", "s1", false, []string{"--profile", "headless", "--session-id", "s1", "任务"}},
|
|
390
|
-
{"事件流", "", true, []string{"--profile", "headless", "--json", "任务"}},
|
|
391
|
-
{"续接 + 事件流", "s1", true, []string{"--profile", "headless", "--session-id", "s1", "--json", "任务"}},
|
|
389
|
+
{"纯 headless", "", "", false, []string{"--profile", "headless", "任务"}},
|
|
390
|
+
{"续接", "s1", "", false, []string{"--profile", "headless", "--session-id", "s1", "任务"}},
|
|
391
|
+
{"事件流", "", "", true, []string{"--profile", "headless", "--json", "任务"}},
|
|
392
|
+
{"续接 + 事件流", "s1", "", true, []string{"--profile", "headless", "--session-id", "s1", "--json", "任务"}},
|
|
393
|
+
// --patch 是**启动器** flag,必须在 --profile 之前(放到后面 headless 会报 unknown option)。
|
|
394
|
+
{"模型 patch", "s1", "/tmp/p.yml", false,
|
|
395
|
+
[]string{"--patch", "/tmp/p.yml", "--profile", "headless", "--session-id", "s1", "任务"}},
|
|
392
396
|
}
|
|
393
397
|
for _, c := range cases {
|
|
394
|
-
got := dshHeadlessArgs("headless", c.sessionID, "任务", c.json)
|
|
398
|
+
got := dshHeadlessArgs("headless", c.sessionID, c.patch, "任务", c.json)
|
|
395
399
|
if fmt.Sprint(got) != fmt.Sprint(c.want) {
|
|
396
400
|
t.Errorf("%s: %v want %v", c.name, got, c.want)
|
|
397
401
|
}
|
|
398
402
|
}
|
|
399
403
|
}
|
|
400
404
|
|
|
405
|
+
// headless 提示词传输:Windows 走 stdin("-"),其余平台保持位置参数。
|
|
406
|
+
func TestDshHeadlessPromptArg(t *testing.T) {
|
|
407
|
+
arg, stdin := dshHeadlessPromptArg("【用户】\n只回复 OK")
|
|
408
|
+
if platformIsWindows() {
|
|
409
|
+
if arg != "-" || stdin != "【用户】\n只回复 OK" {
|
|
410
|
+
t.Errorf("Windows 应走 stdin:arg=%q stdin=%q", arg, stdin)
|
|
411
|
+
}
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
if arg != "【用户】\n只回复 OK" || stdin != "" {
|
|
415
|
+
t.Errorf("非 Windows 应走位置参数:arg=%q stdin=%q", arg, stdin)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// dshHeadlessPatchFor:把 -m 落成 agent-default-model 覆盖;没 -m 不落文件。
|
|
420
|
+
func TestDshHeadlessPatchFor(t *testing.T) {
|
|
421
|
+
dir := t.TempDir()
|
|
422
|
+
w, _ := argsCaptureCLI(t, dir, "dsh", `echo 'ok'`)
|
|
423
|
+
|
|
424
|
+
if path, cleanup := dshHeadlessPatchFor(context.Background(), w.bin, ""); path != "" {
|
|
425
|
+
t.Fatalf("没有 -m 不该写 patch,got %q", path)
|
|
426
|
+
} else {
|
|
427
|
+
cleanup()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
path, cleanup := dshHeadlessPatchFor(context.Background(), w.bin, "volcengine/deepseek-v4.1-flash-1017")
|
|
431
|
+
if path == "" {
|
|
432
|
+
t.Fatal("有 -m 应写出临时 patch")
|
|
433
|
+
}
|
|
434
|
+
data, err := os.ReadFile(path)
|
|
435
|
+
if err != nil {
|
|
436
|
+
t.Fatalf("读 patch: %v", err)
|
|
437
|
+
}
|
|
438
|
+
body := string(data)
|
|
439
|
+
for _, want := range []string{"id: agent-default-model", `provider: "volcengine"`, `model: "deepseek-v4.1-flash-1017"`} {
|
|
440
|
+
if !strings.Contains(body, want) {
|
|
441
|
+
t.Errorf("patch 内容缺 %q:%q", want, body)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
cleanup()
|
|
445
|
+
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
446
|
+
t.Errorf("cleanup 应删掉临时 patch,stat err = %v", err)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 续接轮把 -m 透成 --patch:模型选择在 headless 通道同样生效(2026-10-25 报障)。
|
|
451
|
+
func TestDshResumePassesModelPatch(t *testing.T) {
|
|
452
|
+
dir := t.TempDir()
|
|
453
|
+
w, log := argsCaptureCLI(t, dir, "dsh", `echo 'ok'`)
|
|
454
|
+
|
|
455
|
+
resp, err := (&DshEngine{BinPath: w.bin}).Complete(context.Background(), Request{
|
|
456
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
457
|
+
SessionID: "sess-1",
|
|
458
|
+
Model: "volcengine/deepseek-v4.1-flash-1017",
|
|
459
|
+
})
|
|
460
|
+
if err != nil {
|
|
461
|
+
t.Fatalf("Complete: %v", err)
|
|
462
|
+
}
|
|
463
|
+
args := readFileString(t, log)
|
|
464
|
+
var headlessLine string
|
|
465
|
+
for _, ln := range strings.Split(args, "\n") {
|
|
466
|
+
if strings.Contains(ln, "--patch") {
|
|
467
|
+
headlessLine = ln
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if headlessLine == "" {
|
|
471
|
+
t.Fatalf("续接轮应带 --patch(headless 没有 --model):%q", args)
|
|
472
|
+
}
|
|
473
|
+
if strings.Index(headlessLine, "--patch") > strings.Index(headlessLine, "--profile") {
|
|
474
|
+
t.Errorf("--patch 必须排在 --profile 之前:%q", headlessLine)
|
|
475
|
+
}
|
|
476
|
+
if resp.Model != "volcengine/deepseek-v4.1-flash-1017" {
|
|
477
|
+
t.Errorf("Model = %q want 本轮 -m 指定的那条路由", resp.Model)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
401
481
|
// -c/--continue:按会话登记表找**同一 workspace** 下最近一条 dsh 会话(其它引擎不算)。
|
|
402
482
|
func TestDshLatestSessionIDPrefersWorkspace(t *testing.T) {
|
|
403
483
|
sess := t.TempDir()
|
|
@@ -870,6 +950,28 @@ func TestDshListModelsFromProfilePatch(t *testing.T) {
|
|
|
870
950
|
}
|
|
871
951
|
}
|
|
872
952
|
|
|
953
|
+
// home 级 $DSH_HOME/cordis.patch.yml(对所有 profile 生效)也要进探测链:
|
|
954
|
+
// 2026-10-23 实测 dsh 0.2.1-rc.2 重写配置后 profile 级 patch 清成空 [],
|
|
955
|
+
// provider 只剩 home 级这一份,不读它就整条链空手而归。
|
|
956
|
+
func TestDshListModelsFromHomePatch(t *testing.T) {
|
|
957
|
+
t.Setenv("MAGIC_AGENT_DSH_PROFILE", "")
|
|
958
|
+
home := writeDshPatch(t, "sdk", "[]\n")
|
|
959
|
+
if err := os.WriteFile(filepath.Join(home, "cordis.patch.yml"),
|
|
960
|
+
[]byte("- id: llm-pi-ai\n config:\n providers:\n minimax:\n models:\n - id: MiniMax-M3.1-Flash\n"), 0o644); err != nil {
|
|
961
|
+
t.Fatal(err)
|
|
962
|
+
}
|
|
963
|
+
t.Setenv("DSH_HOME", home)
|
|
964
|
+
|
|
965
|
+
got, err := (&DshEngine{}).ListModels(context.Background())
|
|
966
|
+
if err != nil {
|
|
967
|
+
t.Fatalf("ListModels: %v", err)
|
|
968
|
+
}
|
|
969
|
+
want := []string{"minimax/MiniMax-M3.1-Flash"}
|
|
970
|
+
if !equalStrings(got, want) {
|
|
971
|
+
t.Errorf("ListModels() = %v\n want %v", got, want)
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
873
975
|
// 两条通道(sdk + headless 回退)的 patch 取并集:magic-agent 两条都会用到。
|
|
874
976
|
func TestDshListModelsUnionAcrossProfiles(t *testing.T) {
|
|
875
977
|
t.Setenv("MAGIC_AGENT_DSH_PROFILE", "")
|
|
@@ -1175,7 +1277,7 @@ func TestDshIgnoredParamsProduceNoFlags(t *testing.T) {
|
|
|
1175
1277
|
_ = req.Temperature
|
|
1176
1278
|
_ = req.Tools
|
|
1177
1279
|
_ = req.Timeout
|
|
1178
|
-
args := dshHeadlessArgs(e.profile(), "", "【用户】\nhi", false)
|
|
1280
|
+
args := dshHeadlessArgs(e.profile(), "", "", "【用户】\nhi", false)
|
|
1179
1281
|
want := []string{"--profile", "headless", "【用户】\nhi"}
|
|
1180
1282
|
if fmt.Sprint(args) != fmt.Sprint(want) {
|
|
1181
1283
|
t.Errorf("headless args = %v want %v", args, want)
|
package/internal/agent/engine.go
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// prompt.go — 多轮消息扁平化为单条 prompt 的公共逻辑
|
|
13
13
|
// claude.go — Claude Code CLI 引擎(--print --output-format json)
|
|
14
14
|
// codebuddy.go — CodeBuddy Code CLI 引擎(一个独立 CLI、两个账号)
|
|
15
|
-
// trae.go —
|
|
15
|
+
// trae.go — TRAE CLI 2.0 引擎(exec --json,-m 指定模型)
|
|
16
16
|
// llmengine.go — simonw/LLM CLI 引擎(-m / -s / -o 透传)
|
|
17
17
|
// codex.go — Codex CLI 引擎(exec / exec resume + --output-schema)
|
|
18
18
|
// openclaw.go — OpenClaw CLI 引擎(agent --local --json)
|
|
@@ -92,6 +92,24 @@ type Request struct {
|
|
|
92
92
|
// 引擎实现负责剥掉可能的 "engine/model" 前缀。
|
|
93
93
|
Model string
|
|
94
94
|
|
|
95
|
+
// FallbackEngine / FallbackModel 主引擎失败时的**备用引擎**(2026-10-24)。
|
|
96
|
+
//
|
|
97
|
+
// 只在「主引擎只有过程、没有正文」时降级(见 fallback.go):已经流出正文再换
|
|
98
|
+
// 模型重来,用户会看到两段互相矛盾的答案。降级时 SessionID / Continue 一律清空 ——
|
|
99
|
+
// 备用引擎没有主引擎那条会话,拿旧 id 去 resume 只会报「会话不存在」。
|
|
100
|
+
// 空引擎 = 没配备用,行为与以前完全一致。引擎实现不读这两个字段。
|
|
101
|
+
FallbackEngine string
|
|
102
|
+
FallbackModel string
|
|
103
|
+
|
|
104
|
+
// FallbackDown 降级标记:为 true 时本轮请求**本身就是备用引擎在跑**(见 fallback.go)。
|
|
105
|
+
//
|
|
106
|
+
// 为什么要有它(2026-10-26 修「主备没真的发挥作用」):模型清单只属于它自己的引擎。
|
|
107
|
+
// 用户配的备用模型(如 codebuddy 的 `hy3`)拿到别的引擎上只会吃一句
|
|
108
|
+
// `--model hy3 未知`,于是备用「跑了但没用」。fallbackTarget 因此清空 Model,
|
|
109
|
+
// 这个字段让引擎侧在 Model 为空时**不要回落到自己的默认模型** —— 配置里的备用模型
|
|
110
|
+
// 作废时「用备用引擎自己的默认模型重跑」比「带着一个必然失败的模型再失败一次」有用。
|
|
111
|
+
FallbackDown bool
|
|
112
|
+
|
|
95
113
|
// SystemPrompt 可选系统提示词。各引擎自行决定注入方式
|
|
96
114
|
//(claude/codebuddy用 --append-system-prompt,trae 拼进 prompt 头)。
|
|
97
115
|
SystemPrompt string
|
|
@@ -211,6 +229,24 @@ type Request struct {
|
|
|
211
229
|
// 默认值是否开启按引擎区分,见 AppendDefaultOn(dsh 默认关)。
|
|
212
230
|
Append <-chan string
|
|
213
231
|
|
|
232
|
+
// Reply 原生控制应答通道(可写回引擎自己的控制协议行)。
|
|
233
|
+
//
|
|
234
|
+
// 为什么需要(2026-10-25 实测):codebuddy / claude 在 headless 下遇到需要宿主裁决的动作
|
|
235
|
+
// (计划审批 ExitPlanMode、manual 档下的工具授权)时会发一行
|
|
236
|
+
//
|
|
237
|
+
// {"type":"control_request","request_id":"perm_…","request":{"subtype":"can_use_tool",…}}
|
|
238
|
+
//
|
|
239
|
+
// 然后**原地等 stdin 上的 control_response**。不回就整轮挂住(用户侧看到的是
|
|
240
|
+
// 「转圈、没有审批卡」)。回的内容必须走子进程 stdin —— 不能当成下一条 user 消息,
|
|
241
|
+
// 那时 CLI 正阻塞在控制请求上,user 消息要等到这一轮结束才会被读。
|
|
242
|
+
//
|
|
243
|
+
// 语义:非 nil 时本次调用的 stdin 会**保持打开**(首条 user 消息之后不 EOF),
|
|
244
|
+
// 通道里每收到一行就原样写进子进程 stdin;这一轮收尾(turn_end / result / error)
|
|
245
|
+
// 或 Stream 返回时关掉写端,让 CLI 正常退出。
|
|
246
|
+
// 只有 stream-json 输入通道的引擎(claude / codebuddy / codebuddy-ai)支持;其余引擎
|
|
247
|
+
// 拿到非 nil 会在 CLI 层被挡下(见 AppendSupportOf)。
|
|
248
|
+
Reply <-chan string
|
|
249
|
+
|
|
214
250
|
// OnSessionID 引擎发现 session_id 时回调(用于「看历史」持久化)。
|
|
215
251
|
//
|
|
216
252
|
// 谁需要它:CLI 在 runStreamAsk 里包一个 sessionWriter,每条事件落盘到
|
|
@@ -22,16 +22,16 @@ import (
|
|
|
22
22
|
// ┌──────────────┬──────────────────────────┬───────────────────────────┬────────────────────────────┬──────────────────┬───────────────────────┬─────────────────────┬──────────────────────┐
|
|
23
23
|
// │ Request 字段 │ claude │ codebuddy │ trae │ llm │ codex │ openclaw │ dsh │
|
|
24
24
|
// ├──────────────┼──────────────────────────┼───────────────────────────┼────────────────────────────┼──────────────────┼───────────────────────┼─────────────────────┼──────────────────────┤
|
|
25
|
-
// │ Model │ --model <m> │ --model <m> │ -
|
|
26
|
-
// │ SessionID │ --resume <id> │ --resume <id> │
|
|
27
|
-
// │ Continue │ --continue │ --continue │
|
|
25
|
+
// │ Model │ --model <m> │ --model <m> │ -m <m> │ -m <m> │ -m <m> │ --model <m> │ 不支持(配置层)⑨ │
|
|
26
|
+
// │ SessionID │ --resume <id> │ --resume <id> │ exec resume <id> │ --cid <id> │ exec resume <id> │ --session-id <id> │ 不支持(显式报错)⑨ │
|
|
27
|
+
// │ Continue │ --continue │ --continue │ exec resume --last │ -c │ resume --last │ sessions 查最新 id │ 不支持(显式报错)⑨ │
|
|
28
28
|
// │ MaxTokens │ --settings(env 注入)① │ --settings(env 注入)① │ 不支持(静默忽略)② │ -o max_tokens │ 不支持(静默忽略)② │ 静默忽略② │ 静默忽略② │
|
|
29
29
|
// │ Temperature │ 不支持 │ 不支持 │ 不支持(静默忽略)② │ -o temperature │ 不支持(静默忽略)② │ 静默忽略② │ 静默忽略② │
|
|
30
30
|
// │ Reasoning │ --effort <l> │ --effort <l> │ 不支持(忽略) │ 不支持 │ -c reasoning_effort │ 不支持 │ 不支持 │
|
|
31
31
|
// │ Tools │ --tools / 档位③ │ --tools / 档位③ │ --disallowed-tool / 全放行③│ 不支持 │ -s 沙箱档位④ │ 内嵌 agent(忽略) │ 内嵌 agent(忽略)⑨ │
|
|
32
|
-
// │ Permission │ --permission-mode⑧ │ --permission-mode⑧ │
|
|
32
|
+
// │ Permission │ --permission-mode⑧ │ --permission-mode⑧ │ 未接线(仅 -y) │ 不支持 │ 未接线(沙箱×审批两轴)│ 未接线(exec.mode) │ 未接线(配置层预设)⑨│
|
|
33
33
|
// │ SystemPrompt │ --append-system-prompt │ --append-system-prompt │ 展平进 prompt 头部 │ -s │ -c developer_instr.⑤ │ 展平进 prompt 头部 │ 展平进 prompt 头部 │
|
|
34
|
-
// │ Timeout │ 进程级超时 │ 进程级超时 │
|
|
34
|
+
// │ Timeout │ 进程级超时 │ 进程级超时 │ 进程级超时 │ 进程级超时 │ 进程级超时 │ + --timeout <sec> │ 进程级超时 │
|
|
35
35
|
// │ JSONSchema │ 不支持 │ 不支持 │ 不支持 │ 输出后处理抽 JSON│ --output-schema 原生 │ 输出后处理抽 JSON │ 输出后处理抽 JSON │
|
|
36
36
|
// │ Workspace │ 子进程 cwd⑥ │ 子进程 cwd⑥ │ 子进程 cwd⑥ │ 不支持⑦ │ -C/--cd + cwd⑥ │ 不支持⑦ │ 子进程 cwd⑥ │
|
|
37
37
|
// └──────────────┴──────────────────────────┴───────────────────────────┴────────────────────────────┴──────────────────┴───────────────────────┴─────────────────────┴──────────────────────┘
|
|
@@ -41,11 +41,10 @@ import (
|
|
|
41
41
|
//
|
|
42
42
|
// 注入输出上限(已用真实 CLI --help 验证);两者无 temperature 原生参数,也无对应 settings 键。
|
|
43
43
|
//
|
|
44
|
-
// ② trae 的
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// 三者构造参数时统一静默忽略。
|
|
44
|
+
// ② trae 2.0 的 exec 没有 max-tokens / temperature 原生参数(1.x 的 `-c` 覆盖
|
|
45
|
+
// 会整体替换模型配置块,曾实测导致模型名漂移),因此这两项在 trae 上明确不支持;
|
|
46
|
+
// codex 实测 `-c model_max_output_tokens=` / `-c temperature=` 会被 CLI 静默忽略
|
|
47
|
+
// (无对应生效键);openclaw agent 无相关 flag —— 三者构造参数时统一静默忽略。
|
|
49
48
|
//
|
|
50
49
|
// ③ Tools 语义:关闭 = claude/codebuddy `--tools ""`;trae `--disallowed-tool`
|
|
51
50
|
//
|
|
@@ -53,7 +52,7 @@ import (
|
|
|
53
52
|
// 开启 = claude/codebuddy `--permission-mode <档位>`(四档模型,见 ⑧ 与 permission.go;
|
|
54
53
|
// 改造前恒传 `--dangerously-skip-permissions` / `-y` = 第 4 档);trae `-y`。
|
|
55
54
|
// 白名单 = claude/codebuddy `--tools a,b` + `--permission-mode <档位>`;trae **无收窄能力**
|
|
56
|
-
// (`--allowed-tool`
|
|
55
|
+
// (`--allowed-tool` 只做自动批准、不裁剪工具集,实测仍下发全部工具),
|
|
57
56
|
// 故白名单降级为「默认开启所有工具」= 全放行 `-y`,与 codex 的白名单降级同构。
|
|
58
57
|
// llm 无工具概念。
|
|
59
58
|
//
|
|
@@ -170,10 +169,15 @@ var (
|
|
|
170
169
|
binName: "trae-cli",
|
|
171
170
|
envVar: "MAGIC_AGENT_TRAE_BIN",
|
|
172
171
|
candidates: []string{
|
|
172
|
+
// TRAE CLI 2.0 的官方 v2 安装器落点:主程序 traex,
|
|
173
|
+
// 同目录还有 trae-cli / traecli 兼容入口(.cmd / 符号链接)。
|
|
174
|
+
"~/AppData/Local/Programs/TraeCLI/bin/traex.exe",
|
|
175
|
+
"~/.local/bin/traex",
|
|
176
|
+
"~/.trae/cli/current/traex",
|
|
177
|
+
// 兼容入口(PATH 不干净时的兜底)
|
|
173
178
|
"~/.local/bin/trae-cli",
|
|
174
|
-
"~/bin/trae-cli",
|
|
175
179
|
},
|
|
176
|
-
notFound: "
|
|
180
|
+
notFound: "TRAE CLI not found (install TRAE CLI or set MAGIC_AGENT_TRAE_BIN)",
|
|
177
181
|
}
|
|
178
182
|
llmBase = cliBase{
|
|
179
183
|
binName: "llm",
|
|
@@ -370,9 +374,9 @@ func WorkspaceSupportOf(engine string) string {
|
|
|
370
374
|
// codex npm install -g @openai/codex
|
|
371
375
|
// openclaw npm install -g openclaw@latest(官方文档;装完通常还要 openclaw onboard --install-daemon)
|
|
372
376
|
// dsh npm i -g @deepseek-ai/dsh
|
|
373
|
-
// trae sh -c "$(curl -
|
|
374
|
-
// (官方
|
|
375
|
-
//
|
|
377
|
+
// trae sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"
|
|
378
|
+
// (官方 TRAE CLI 2.0 安装脚本;Windows 用
|
|
379
|
+
// `irm https://trae.cn/trae-cli/install_v2.ps1 | iex`。1.x 已不再支持)
|
|
376
380
|
// llm python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm
|
|
377
381
|
// (与 npm postinstall 同一条隔离安装路径,避开系统 Python;另有 pip / pipx / brew)
|
|
378
382
|
//
|
|
@@ -400,7 +404,7 @@ func InstallCommandOf(engine string) string {
|
|
|
400
404
|
case "dsh":
|
|
401
405
|
return "npm i -g @deepseek-ai/dsh"
|
|
402
406
|
case "trae":
|
|
403
|
-
return `sh -c "$(curl -
|
|
407
|
+
return `sh -c "$(curl -fsSL https://trae.cn/trae-cli/install_v2.sh)"`
|
|
404
408
|
case "llm":
|
|
405
409
|
return "python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm"
|
|
406
410
|
}
|