magic-agent 2.4.3 → 2.6.3
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 +467 -41
- package/README.md +15 -14
- package/client-ui/pc/app.css +74 -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 +40 -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 +247 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +72 -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 +217 -0
- package/client-ui/shared/conv/conv.css +329 -174
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +373 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +5554 -639
- package/client-ui-mobile/reqs.css +110 -1
- package/client-ui-mobile/reqs.js +533 -74
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +67 -0
- package/client-ui-mobile/scripts.js +599 -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 +69 -10
- package/cmd/magic-client/main.go +210 -32
- 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_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +157 -7
- 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/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- 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/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- 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 +166 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +119 -7
- 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 +168 -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/engines_test.go +103 -0
- package/internal/cli/events.go +15 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +157 -11
- package/internal/cli/session.go +75 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- 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/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -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/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +297 -33
- 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/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +518 -24
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- 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/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +207 -2
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +288 -0
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +290 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +31 -0
- package/internal/client/scripts_decode_test.go +130 -0
- package/internal/client/scripts_decode_windows.go +182 -0
- package/internal/client/scripts_test.go +739 -0
- package/internal/client/service.go +717 -97
- package/internal/client/service_test.go +217 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -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/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -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/multiclient_test.go +368 -0
- package/internal/session/session.go +175 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- 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 +6 -3
- 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/npm/install.js +11 -0
- package/package.json +14 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
/* wraplaunch_test.go - 钉住两个对外导出的行为。
|
|
4
|
+
*
|
|
5
|
+
* 这两个导出是「发现 → 脚本」功能的地基:
|
|
6
|
+
* · WrapLaunch —— Windows 上 .cmd/.bat/.ps1 必须包 PowerShell,否则参数被拆坏;
|
|
7
|
+
* 且**退出码要透传**(否则「脚本失败」只显示退出码 1)。
|
|
8
|
+
* · ConfigureProcGroup —— 停止脚本要能整组杀,不留孤儿 worker。
|
|
9
|
+
*
|
|
10
|
+
* 放在这里而不是 internal/client:这两个是 agent 层的能力,
|
|
11
|
+
* 行为边界属于 agent 层(平台差异 + 退出码语义)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import (
|
|
15
|
+
"os/exec"
|
|
16
|
+
"strings"
|
|
17
|
+
"testing"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// TestWrapLaunchKeepsArgsOnUnix unix 上:原样返回,不包装、不改写。
|
|
21
|
+
//
|
|
22
|
+
// 脚本参数是用户自己填的定长参数 —— 套上引擎的 emptyStringFlags 白名单
|
|
23
|
+
// 会莫名其妙地改写用户的参数。
|
|
24
|
+
func TestWrapLaunchKeepsArgsOnUnix(t *testing.T) {
|
|
25
|
+
if platformIsWindows() {
|
|
26
|
+
t.Skip("Windows 上 .cmd 会被包装,本断言只对 unix 成立")
|
|
27
|
+
}
|
|
28
|
+
bin, args := WrapLaunch("/opt/bin/run.sh", []string{"--port", "8080", "a b"})
|
|
29
|
+
if bin != "/opt/bin/run.sh" {
|
|
30
|
+
t.Fatalf("bin = %q,应原样保留", bin)
|
|
31
|
+
}
|
|
32
|
+
if len(args) != 3 || args[0] != "--port" || args[1] != "8080" || args[2] != "a b" {
|
|
33
|
+
t.Fatalf("args = %q,应原样保留", args)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TestWrapLaunchNativeExeUntouched Windows 上:原生 .exe 不包装。
|
|
38
|
+
//
|
|
39
|
+
// 包一层 PowerShell 会多一个常驻进程,进程组杀 / 超时 / 信号转发都多一跳。
|
|
40
|
+
func TestWrapLaunchNativeExeUntouched(t *testing.T) {
|
|
41
|
+
if !platformIsWindows() {
|
|
42
|
+
t.Skip("本断言只对 Windows 成立")
|
|
43
|
+
}
|
|
44
|
+
bin, args := WrapLaunch(`C:\tools\run.exe`, []string{"--x"})
|
|
45
|
+
if bin != `C:\tools\run.exe` {
|
|
46
|
+
t.Fatalf("bin = %q,.exe 不该被包装", bin)
|
|
47
|
+
}
|
|
48
|
+
if len(args) != 1 || args[0] != "--x" {
|
|
49
|
+
t.Fatalf("args = %q,应原样保留", args)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TestWrapLaunchWindowsScriptShape Windows 上:脚本被包成 PowerShell,且参数是单引号字面量。
|
|
54
|
+
//
|
|
55
|
+
// 精确断言整个 argv —— 参数顺序/个数错了 PowerShell 会静默执行错的命令。
|
|
56
|
+
func TestWrapLaunchWindowsScriptShape(t *testing.T) {
|
|
57
|
+
if !platformIsWindows() {
|
|
58
|
+
t.Skip("本断言只对 Windows 成立")
|
|
59
|
+
}
|
|
60
|
+
bin, args := WrapLaunch(`D:\tools\build.cmd`, []string{"--port", "8080"})
|
|
61
|
+
if bin != powershellBin() {
|
|
62
|
+
t.Fatalf("bin = %q,want %q", bin, powershellBin())
|
|
63
|
+
}
|
|
64
|
+
// argv 形状:-NoProfile / -ExecutionPolicy / Bypass / -Command / <脚本>
|
|
65
|
+
want := []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"}
|
|
66
|
+
if len(args) != len(want)+1 {
|
|
67
|
+
t.Fatalf("args = %q\nwant 前缀 %q + 一个脚本", args, want)
|
|
68
|
+
}
|
|
69
|
+
for i := range want {
|
|
70
|
+
if args[i] != want[i] {
|
|
71
|
+
t.Fatalf("第 %d 个 = %q,want %q", i, args[i], want[i])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
script := args[len(args)-1]
|
|
75
|
+
// 路径与参数都必须是**单引号字面量**(不是双引号 —— 双引号里的 $ 会被展开)。
|
|
76
|
+
if !strings.Contains(script, `& 'D:\tools\build.cmd'`) {
|
|
77
|
+
t.Fatalf("脚本里没有单引号字面量调用:%q", script)
|
|
78
|
+
}
|
|
79
|
+
if !strings.Contains(script, `'--port' '8080'`) {
|
|
80
|
+
t.Fatalf("参数没有逐个单引号包裹:%q", script)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// TestWrapLaunchPropagatesExitCode Windows 上:包装后必须带 `exit $LASTEXITCODE`。
|
|
85
|
+
//
|
|
86
|
+
// 实测:PowerShell 用 `& 'script.cmd'` 调 `exit /b 3` 的批处理时,
|
|
87
|
+
// **PowerShell 自己的退出码是 1,不是 3** —— 退出码被吞了。
|
|
88
|
+
// 少这句,界面上「脚本失败」只显示退出码 1,用户少一条关键排查信息。
|
|
89
|
+
func TestWrapLaunchPropagatesExitCode(t *testing.T) {
|
|
90
|
+
if !platformIsWindows() {
|
|
91
|
+
t.Skip("本断言只对 Windows 成立")
|
|
92
|
+
}
|
|
93
|
+
_, args := WrapLaunch(`D:\tools\build.cmd`, nil)
|
|
94
|
+
script := args[len(args)-1]
|
|
95
|
+
if !strings.Contains(script, "exit $LASTEXITCODE") {
|
|
96
|
+
t.Fatalf("包装后没有透传退出码:%q", script)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// TestConfigureProcGroupSetsAttr 配置后进程组要真的建起来。
|
|
101
|
+
//
|
|
102
|
+
// 这条钉的是「停止脚本能整组杀」的前提 —— 没建进程组,
|
|
103
|
+
// unix 上 kill(-pid) 会杀掉调用方自己那一组(灾难性),Windows 上 taskkill /T 无树可杀。
|
|
104
|
+
func TestConfigureProcGroupSetsAttr(t *testing.T) {
|
|
105
|
+
cmd := exec.Command("cmd", "/c", "exit", "0")
|
|
106
|
+
before := cmd.SysProcAttr
|
|
107
|
+
ConfigureProcGroup(cmd)
|
|
108
|
+
if cmd.SysProcAttr == before {
|
|
109
|
+
t.Fatal("配置后 SysProcAttr 没变(进程组没建起来)")
|
|
110
|
+
}
|
|
111
|
+
// nil 不能 panic(调用方可能拿到空 cmd)。
|
|
112
|
+
ConfigureProcGroup(nil)
|
|
113
|
+
}
|
package/internal/cli/ask.go
CHANGED
|
@@ -44,25 +44,32 @@ import (
|
|
|
44
44
|
|
|
45
45
|
// askOptions 根命令的全部参数。
|
|
46
46
|
type askOptions struct {
|
|
47
|
-
engine
|
|
48
|
-
model
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
engine string
|
|
48
|
+
model string
|
|
49
|
+
// 备用引擎(2026-10-24):主引擎失败且零产出时自动降级(见 agent/fallback.go)。
|
|
50
|
+
// 空 = 不降级,行为与以前一致。
|
|
51
|
+
fallbackEngine string
|
|
52
|
+
fallbackModel string
|
|
53
|
+
system string
|
|
54
|
+
prompt string
|
|
55
|
+
file string
|
|
56
|
+
attach []string // 附件(截图/图片)路径;与提示词一起发给引擎
|
|
57
|
+
tools string
|
|
58
|
+
timeout time.Duration
|
|
59
|
+
retries int
|
|
60
|
+
backoff time.Duration
|
|
61
|
+
output string
|
|
62
|
+
verbose bool
|
|
63
|
+
engines bool
|
|
64
|
+
contract bool // 输出带版本的契约 envelope(--contract;客户端启动校验用)
|
|
65
|
+
noModels bool
|
|
66
|
+
quotas bool // 只查额度 / 签到(--quotas;不探测引擎、不启动 CLI)
|
|
67
|
+
login string // 拉起某引擎自己的交互式登录会话(--login <engine>)
|
|
63
68
|
// repairModels 还原被反向同步写坏的桌面端 models.json(--repair-models <engine>):
|
|
64
69
|
// 2026-09-30 Windows 事故的收尾手段,见 internal/agent/custom_models.go 的事故还原一节。
|
|
65
|
-
repairModels
|
|
70
|
+
repairModels string
|
|
71
|
+
// checkin 领取指定引擎账号的每日积分(--checkin <engine>):只读本机票据调账单接口。
|
|
72
|
+
checkin string
|
|
66
73
|
events bool // --stream 的事件流带契约版本与行号(--events;客户端消费用)
|
|
67
74
|
control bool // 从 stdin 读 NDJSON 控制命令(--control;打断 / 收工 / 回审批)
|
|
68
75
|
jsonOut bool
|
|
@@ -136,6 +143,13 @@ type askOptions struct {
|
|
|
136
143
|
// 一轮结束后生成 0-3 条「用户下一句」的快捷回复;一次性旁路调用,不续接会话。
|
|
137
144
|
// 值 = 请求 JSON 的文件路径("-" = stdin)。
|
|
138
145
|
suggestInput string // --suggest-followups <file|->:{"user":"…","reply":"…"}
|
|
146
|
+
// 建议引擎的主备(2026-10-24):主建议引擎失败时先试这个,再落到内置 llm 兜底。
|
|
147
|
+
suggestFallbackEngine string // --suggest-fallback-engine
|
|
148
|
+
suggestFallbackModel string // --suggest-fallback-model
|
|
149
|
+
|
|
150
|
+
// 自动任务模式的冲突判定(2026-10-26):一次性旁路调用,不续接会话。
|
|
151
|
+
// 值 = 请求 JSON 的文件路径("-" = stdin)。
|
|
152
|
+
judgeInput string // --judge-conflict <file|->:{"prompt":"…","candidates":[…]}
|
|
139
153
|
|
|
140
154
|
// 四档权限模型(见 internal/agent/permission.go)。仅 claude / codebuddy 接线。
|
|
141
155
|
permission string // 档位:manual | accept-edits | auto | full
|
|
@@ -157,8 +171,9 @@ type askOptions struct {
|
|
|
157
171
|
teamParallel int // --team-parallel:并发度(默认 4)
|
|
158
172
|
teamTaskTO time.Duration // --team-task-timeout:单子任务超时(0 = 按总超时推算)
|
|
159
173
|
teamRoutes []string // --team-route <kind>=<engine>[/<model>],可重复
|
|
174
|
+
teamFallbacks []string // --team-fallback <kind>=<engine>[/<model>],可重复(主备里的「备」)
|
|
160
175
|
teamRunDir string // --team-run-dir:覆盖运行目录父级
|
|
161
|
-
teamPerm string // --team-permission
|
|
176
|
+
teamPerm string // --team-permission:子任务权限上限(空 = 继承主会话 --permission)
|
|
162
177
|
teamAdaptive bool // --team-adaptive:执行后按失败 / open 项再规划一轮
|
|
163
178
|
teamSplit string // --team-split:拆解粒度 coarse | standard | fine
|
|
164
179
|
teamSplitCode bool // --team-split-code:简单代码也拆成独立子任务
|
|
@@ -173,6 +188,7 @@ type askOptions struct {
|
|
|
173
188
|
* 所以派发一律用 flagChanged 判断(看「传过没有」而不是「值非空」)。 */
|
|
174
189
|
requirements string // --requirements [<ws|dir>]:读清单
|
|
175
190
|
consolidateReqs string // --consolidate-requirements [<ws|dir>]:跑一轮归纳
|
|
191
|
+
reqGraph string // --requirements-graph [<ws|dir>]:会话↔需求关系图谱(只读)
|
|
176
192
|
reqForce bool // --requirement-force
|
|
177
193
|
reqAll bool // --requirement-all:全量重放
|
|
178
194
|
reqEngine string // --requirement-engine(默认 llm)
|
|
@@ -180,6 +196,10 @@ type askOptions struct {
|
|
|
180
196
|
reqModule string // --requirement-module:只整理这个模块
|
|
181
197
|
reqTimeout time.Duration // --requirement-timeout
|
|
182
198
|
reqAuto bool // --requirement-auto:自动归纳,不触发首次初始化
|
|
199
|
+
// 截图相关(--requirement-shot):PNG 走 stdin,不走命令行参数。
|
|
200
|
+
reqShot string // --requirement-shot [<ws|dir>]:给功能点追加一张完成情况截图
|
|
201
|
+
reqShotName string // --requirement-shot-name:功能点名
|
|
202
|
+
reqShotCap string // --requirement-shot-caption:截图标注说明
|
|
183
203
|
}
|
|
184
204
|
|
|
185
205
|
// newAskOptions 返回带默认值的选项集。
|
|
@@ -203,6 +223,10 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
203
223
|
f := cmd.PersistentFlags()
|
|
204
224
|
f.StringVarP(&opts.engine, "engine", "e", "codebuddy", "引擎: codebuddy | codebuddy-ai | claude | trae | llm | codex | openclaw | dsh | arkclaw(默认 codebuddy)")
|
|
205
225
|
f.StringVarP(&opts.model, "model", "m", "", "模型(空 = 引擎默认:codebuddy=hy3;codebuddy-ai=CLI 自选默认;llm 引擎传 llm CLI 注册名,如 minimax-m3;dsh/arkclaw 不透传)")
|
|
226
|
+
/* 主备(2026-10-24):主引擎失败且**零产出**时自动换备用重跑(额度用尽 / 单模型故障)。
|
|
227
|
+
只降一级;已经流出正文则不降级(避免两段互相矛盾的答案)。 */
|
|
228
|
+
f.StringVar(&opts.fallbackEngine, "fallback-engine", "", "备用引擎:主引擎失败且零产出时自动降级(缺省 = 不降级;可用值同 --engine)")
|
|
229
|
+
f.StringVar(&opts.fallbackModel, "fallback-model", "", "备用引擎的模型(空 = 该引擎默认;只在 --fallback-engine 非空时生效)")
|
|
206
230
|
f.StringVarP(&opts.system, "system", "s", "", "系统提示词(空 = 用配置文件 ~/.config/magic-agent/config.json 的 systemPrompt;两者都为空则不注入)。值若是文件路径或 @文件 → 读该文件内容")
|
|
207
231
|
f.StringVarP(&opts.prompt, "prompt", "p", "", "提示词(值若是文件路径或 @文件 → 读该文件内容作为提示词)")
|
|
208
232
|
f.StringVarP(&opts.workspace, "workspace", "w", "", "工作目录(workspace):在该目录里执行引擎;codex 走原生 -C,claude/codebuddy/trae/dsh 用子进程 cwd,llm/arkclaw/openclaw 不支持(忽略并提示)")
|
|
@@ -222,6 +246,9 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
222
246
|
f.BoolVar(&opts.titleForce, "title-force", false, "配合 --summarize-title:已有标题也重新归纳(默认命中缓存就直接返回,省一次 LLM 调用)")
|
|
223
247
|
f.StringVar(&opts.titleEngine, "title-engine", "llm", "--summarize-title 用哪个引擎归纳(默认 llm:纯文本、不带工具)")
|
|
224
248
|
f.StringVar(&opts.suggestInput, "suggest-followups", "", "生成下一轮快捷回复:传请求 JSON 的文件路径(\"-\" = stdin):{\"user\":\"…\",\"reply\":\"…\"}。旁路调用,不续接当前会话;引擎走 -e(默认 llm)")
|
|
249
|
+
f.StringVar(&opts.suggestFallbackEngine, "suggest-fallback-engine", "", "配合 --suggest-followups:建议引擎的备用(主建议引擎失败时先试它,再落到内置 llm 兜底)")
|
|
250
|
+
f.StringVar(&opts.suggestFallbackModel, "suggest-fallback-model", "", "配合 --suggest-followups:建议备用引擎的模型(空 = 该引擎默认)")
|
|
251
|
+
f.StringVar(&opts.judgeInput, "judge-conflict", "", "判断本轮任务与同工作空间运行中任务是否冲突(自动任务模式用):传请求 JSON 的文件路径(\"-\" = stdin):{\"prompt\":\"…\",\"candidates\":[{title,recent,files}]}。旁路调用,不续接会话;引擎走 -e(默认 llm)")
|
|
225
252
|
f.Uint64Var(&opts.logAfter, "after", 0, "配合 --session-log:增量续读,只回 seq > N 的事件(另附 count/after/lastSeq/snapshotRequired)")
|
|
226
253
|
f.Uint64Var(&opts.logBefore, "before", 0, "配合 --session-log:向上翻更早的历史,从文件尾跳过 N 条已画事件再取一页(0 = 最新一页;下一页把返回的 nextBefore 传回来。另附 count/before/nextBefore/firstSeq/more;与 --after 互斥)")
|
|
227
254
|
f.IntVar(&opts.logLimit, "limit", 200, "配合 --before:每页最多返回多少条事件(默认 200)")
|
|
@@ -244,8 +271,10 @@ func bindAskFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
244
271
|
f.BoolVar(&opts.engines, "engines", false, "列出支持的引擎、本机 CLI 可用性与各引擎当前支持的模型(JSON 数组;每行含 capabilities / models / workspace / streaming 能力字段;不可用的引擎带 install 一键安装命令)")
|
|
245
272
|
f.BoolVar(&opts.contract, "contract", false, "输出桌面/移动客户端契约:{\"contractVersion\":N,\"engines\":[...]}(engines 与 --engines 同构,另含 capabilities 静态能力字段)。默认不探测模型(快);要模型写 --no-models=false")
|
|
246
273
|
f.BoolVar(&opts.noModels, "no-models", false, "配合 --engines / --contract:跳过各引擎的模型探测(只列引擎与可用性,不启动 CLI)")
|
|
274
|
+
f.BoolVar(&opts.quotas, "quotas", false, "只查各引擎账号的额度与每日签到(JSON 数组,字段与 --engines 同名;不探测引擎、不启动任何 CLI,秒级返回)")
|
|
247
275
|
f.StringVar(&opts.login, "login", "", "拉起指定引擎自己的交互式登录会话(如 codebuddy / codebuddy-ai):自动带上该引擎的账号环境,进去执行 /login 即可;登录态落在该引擎自己的票据上,两个账号互不顶号")
|
|
248
276
|
f.StringVar(&opts.repairModels, "repair-models", "", "还原被覆盖写坏的桌面端 models.json(如 codebuddy / codebuddy-ai):把对象格式改回桌面端的数组格式,官方模型清单不动;只在形状确实是坏的时候才改,且先留 .broken.bak 备份")
|
|
277
|
+
f.StringVar(&opts.checkin, "checkin", "", "领取指定引擎账号的每日积分(如 codebuddy / codebuddy-ai):用该引擎自己的登录票据调账单接口,输出本次到账积分与连续天数;每天只能领一次")
|
|
249
278
|
f.BoolVar(&opts.jsonOut, "json", false, "兼容保留:--engines 已默认 JSON,本 flag 不再需要")
|
|
250
279
|
f.BoolVar(&opts.stream, "stream", false, "流式输出:正文/思考增量实时打到 stdout(text 模式思考走 stderr)")
|
|
251
280
|
f.BoolVar(&opts.events, "events", false, "配合 --stream:事件流带契约版本与行号(每行加 v / seq,并先发一行 ready),供客户端消费;老消费者不要开(形状与 --stream 不同)")
|
|
@@ -271,9 +300,11 @@ func bindTeamFlags(cmd *cobra.Command, opts *askOptions) {
|
|
|
271
300
|
f.DurationVar(&opts.teamTaskTO, "team-task-timeout", 0, "配合 --team:单个子任务超时(0 = 总超时的 1/2,上限 20m)")
|
|
272
301
|
f.StringArrayVar(&opts.teamRoutes, "team-route", nil,
|
|
273
302
|
"配合 --team:给某类子任务或某个角色指定引擎/模型,形如 image=llm/hunyuan-image-3.0 或 role:coder=claude/sonnet(可重复;类别:plan|text|code|image|video|game;角色用 role:<name>)")
|
|
303
|
+
f.StringArrayVar(&opts.teamFallbacks, "team-fallback", nil,
|
|
304
|
+
"配合 --team:给某类/某个角色指定**备用**引擎/模型,主引擎失败且零产出时自动降级,形如 image=dsh/v3 或 role:coder=codebuddy(可重复;键同 --team-route)")
|
|
274
305
|
f.StringVar(&opts.teamRunDir, "team-run-dir", "", "配合 --team:覆盖运行目录的父目录(空 = 选了空间落 <空间>/.magic/team,否则落 ~/.magic-agent/team)")
|
|
275
|
-
f.StringVar(&opts.teamPerm, "team-permission",
|
|
276
|
-
"配合 --team
|
|
306
|
+
f.StringVar(&opts.teamPerm, "team-permission", "",
|
|
307
|
+
"配合 --team:子任务的权限上限(空 = 继承主会话 --permission;显式给档位可压低子任务权限,如 accept-edits)")
|
|
277
308
|
f.BoolVar(&opts.teamAdaptive, "team-adaptive", true,
|
|
278
309
|
"配合 --team:第一轮执行后按失败与未解决项再规划一轮补充任务(默认开;只补一轮、最多 3 个)")
|
|
279
310
|
f.StringVar(&opts.teamSplit, "team-split", "",
|
|
@@ -555,18 +586,29 @@ func runAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
555
586
|
|
|
556
587
|
// 会话登记 + spawn 钩子:把引擎子进程 pid 记下来,之后任何进程都能
|
|
557
588
|
// `magic-agent --stop <session_id>` 精确停掉它(见 internal/session)。
|
|
558
|
-
h := beginSession(engine.Name(), req, opts.saveRegistry)
|
|
589
|
+
h, berr := beginSession(engine.Name(), req, opts.saveRegistry)
|
|
590
|
+
if berr != nil {
|
|
591
|
+
/* 登记失败 = 这一轮跑不起来(最典型:工作空间锁等超时)。**必须当失败返回**:
|
|
592
|
+
继续跑的话,引擎会在没有登记的情况下开跑,客户端那边看到的是「在跑」,
|
|
593
|
+
实际这一轮早该失败 —— 而失败原因只剩 stderr 里一行字。
|
|
594
|
+
⚠️ 用原 error 返回(不是 reportedError):reportedError 的语义是
|
|
595
|
+
「已经输出过了」,包上它 Execute 就不再打印,原因会被吞掉。 */
|
|
596
|
+
return berr
|
|
597
|
+
}
|
|
559
598
|
ctx := withSessionHook(cmd.Context(), h)
|
|
560
599
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
Retries: opts.retries,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
600
|
+
/* 非流式路径同样支持主备降级:主引擎失败且没拿到正文时换备用再跑一次
|
|
601
|
+
(提示写到 stderr —— 这条路径没有事件流可挂 notice)。 */
|
|
602
|
+
resp, fbInfo, err := agent.RunWithFallback(ctx, engine, req, agent.RunOptions{
|
|
603
|
+
Timeout: opts.timeout, Retries: opts.retries, Backoff: opts.backoff, Verbose: opts.verbose,
|
|
604
|
+
}, func(fe agent.FallbackEvent) {
|
|
605
|
+
note := agent.FallbackNoticeText(fe)
|
|
606
|
+
fmt.Fprintf(cmd.ErrOrStderr(), "magic-agent: %s\n", note)
|
|
607
|
+
})
|
|
569
608
|
if err != nil {
|
|
609
|
+
if fbInfo.Reason != "" {
|
|
610
|
+
fmt.Fprintf(cmd.ErrOrStderr(), "magic-agent: 本轮没有降级:%s\n", fbInfo.Reason)
|
|
611
|
+
}
|
|
570
612
|
finishSession(h, "", session.StateFailed)
|
|
571
613
|
attempts := 1 + opts.retries
|
|
572
614
|
// WriteError 已把失败信息(json envelope / text 行)写到 stderr,
|
|
@@ -587,7 +629,10 @@ func runAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
587
629
|
// ⚠️ saveRegistry=false 时**不落盘**(用户 2026-10-12:「会话登记也不需要」):
|
|
588
630
|
// 句柄照常分配(run_id 要给常驻会话的追加入口命名),只是磁盘上不留记录 ——
|
|
589
631
|
// 代价是 --sessions / --stop 看不见它,--append 只能按 run_id 走约定路径。
|
|
590
|
-
|
|
632
|
+
//
|
|
633
|
+
// 返回的 err 是登记失败的原因(最典型的是**工作空间锁等超时**):
|
|
634
|
+
// 吞掉它的话,调用方只知道「没有会话句柄」,真实原因查不出来。
|
|
635
|
+
func beginSession(engine string, req agent.Request, saveRegistry bool) (*session.Handle, error) {
|
|
591
636
|
h, err := session.Begin(session.BeginOptions{
|
|
592
637
|
Engine: engine,
|
|
593
638
|
Model: req.Model,
|
|
@@ -600,10 +645,10 @@ func beginSession(engine string, req agent.Request, saveRegistry bool) *session.
|
|
|
600
645
|
NoPersist: !saveRegistry,
|
|
601
646
|
})
|
|
602
647
|
if err != nil {
|
|
603
|
-
return nil
|
|
648
|
+
return nil, err
|
|
604
649
|
}
|
|
605
650
|
setActiveSession(h)
|
|
606
|
-
return h
|
|
651
|
+
return h, nil
|
|
607
652
|
}
|
|
608
653
|
|
|
609
654
|
// withSessionHook 给 ctx 挂上「子进程已启动」回调(h 为空时原样返回)。
|
|
@@ -655,8 +700,7 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
655
700
|
if err != nil {
|
|
656
701
|
return err
|
|
657
702
|
}
|
|
658
|
-
|
|
659
|
-
if streamer == nil {
|
|
703
|
+
if agent.AsStreamer(engine) == nil {
|
|
660
704
|
return &usageError{fmt.Errorf("engine %q does not support streaming", engine.Name())}
|
|
661
705
|
}
|
|
662
706
|
|
|
@@ -759,6 +803,9 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
759
803
|
Status: string(ev.Status), Parent: ev.Parent,
|
|
760
804
|
SubSessionID: ev.SubSessionID,
|
|
761
805
|
Error: ev.Error, Reason: ev.Reason,
|
|
806
|
+
/* 失败分类(2026-10-25):落盘它,历史回放才画得出**同一张**额度卡 ——
|
|
807
|
+
只在实时流里带,用户翻历史时那一轮会退回普通失败卡(两张脸)。 */
|
|
808
|
+
ErrorKind: ev.ErrorKind, ErrorCode: ev.ErrorCode, ResetAt: ev.ResetAt,
|
|
762
809
|
})
|
|
763
810
|
}
|
|
764
811
|
if format == agent.FormatJSON {
|
|
@@ -800,6 +847,10 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
800
847
|
// runStreamAsk 的 error 分支写(stdout 一条 error 事件),这里只
|
|
801
848
|
// 保证 text 模式的调用方也**看得见**这一轮没有正常结束。
|
|
802
849
|
fmt.Fprint(stderr, "✗ 本轮异常收尾:"+ev.Reason+"\n")
|
|
850
|
+
case agent.KindNotice:
|
|
851
|
+
/* 过程提示(max_tokens 截断 / 「工具不在当前工具集」等)。
|
|
852
|
+
与 thinking、工具事件同走 stderr,stdout 正文保持干净。 */
|
|
853
|
+
fmt.Fprint(stderr, "· "+ev.Text+"\n")
|
|
803
854
|
}
|
|
804
855
|
wireLogger(ev)
|
|
805
856
|
}
|
|
@@ -822,7 +873,12 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
822
873
|
}
|
|
823
874
|
|
|
824
875
|
// 会话登记 + spawn 钩子(与 runAsk 同一套:--stop 靠它拿到引擎 pid)。
|
|
825
|
-
h := beginSession(engine.Name(), req, opts.saveRegistry)
|
|
876
|
+
h, berr := beginSession(engine.Name(), req, opts.saveRegistry)
|
|
877
|
+
if berr != nil {
|
|
878
|
+
/* 同 runAsk:登记失败就别开跑。真实原因(等锁超时等)直接报出去,
|
|
879
|
+
免得整轮以「跑起来了但列表里没有」的形式收场。 */
|
|
880
|
+
return berr
|
|
881
|
+
}
|
|
826
882
|
ctx = withSessionHook(ctx, h)
|
|
827
883
|
|
|
828
884
|
/* 把「本次运行 id」也交给存档 writer(2026-10-15):引擎还没吐 session_id 时,
|
|
@@ -832,7 +888,11 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
832
888
|
· 引擎**整轮都不给 id** 的形态(dsh 的 headless 回退轮)不再白开 --save-history
|
|
833
889
|
(此前那些事件一直躺在内存 buffer 里,收工时被丢光)。
|
|
834
890
|
拿到 id 后 writer 自己改名为 <session_id>.jsonl(见 sessionlog.go 的 renameTo)。 */
|
|
835
|
-
|
|
891
|
+
/* ⚠️ h 可能为 nil:session.Begin 失败(例如工作空间锁等超时)时 beginSession 返回 nil,
|
|
892
|
+
而 h.RunID() 是值接收者 → nil 解引用直接 panic,整轮以一堆栈回溯收场,
|
|
893
|
+
真实原因(等锁超时)反而被淹没。beginSession 的错误本来就该由调用方当失败处理,
|
|
894
|
+
这里只保证不崩:没有 run id 就不设,存档 writer 继续用会话 id 命名。 */
|
|
895
|
+
if sessionLog != nil && h != nil {
|
|
836
896
|
sessionLog.setRunID(h.RunID())
|
|
837
897
|
}
|
|
838
898
|
|
|
@@ -870,6 +930,16 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
870
930
|
onEvent = ka.wrapOnEvent(onEvent)
|
|
871
931
|
}
|
|
872
932
|
|
|
933
|
+
/* 控制应答通道(原地回审批,2026-10-25):计划审批 ExitPlanMode / manual 档的工具授权
|
|
934
|
+
都是引擎发 control_request **原地等 stdin 上的 control_response** —— 不写回去整轮就挂着
|
|
935
|
+
(用户报障:「没看到审批」+ 一直转圈)。这条路与常驻会话无关:Windows 没有追加入口,
|
|
936
|
+
但审批照样要能回,所以独立建通道(引擎侧会因此把 stdin 保持到本轮收尾,见 Request.Reply)。 */
|
|
937
|
+
var controlReplies chan string
|
|
938
|
+
if opts.control && agent.AskAnswerControlOf(engine.Name()) {
|
|
939
|
+
controlReplies = make(chan string, 8)
|
|
940
|
+
req.Reply = controlReplies
|
|
941
|
+
}
|
|
942
|
+
|
|
873
943
|
/* 控制通道:给调用方一条回传通道(打断 / 收工 / 回审批)。
|
|
874
944
|
起在常驻会话之后 —— answer 依赖 ka。 */
|
|
875
945
|
var ctl *controlSession
|
|
@@ -884,6 +954,27 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
884
954
|
cancelAll()
|
|
885
955
|
},
|
|
886
956
|
answer: func(text string) bool { return ka != nil && ka.Push(text) },
|
|
957
|
+
answerAsk: func(requestID string, allowed bool, message, toolName string) bool {
|
|
958
|
+
if controlReplies == nil {
|
|
959
|
+
return false
|
|
960
|
+
}
|
|
961
|
+
raw, eerr := agent.EncodeAskControlResponse(engine.Name(), &agent.AskRequest{
|
|
962
|
+
Engine: engine.Name(),
|
|
963
|
+
Kind: agent.AskKindPermission,
|
|
964
|
+
Source: agent.AskSourceControlRequest,
|
|
965
|
+
RequestID: requestID,
|
|
966
|
+
ToolName: toolName,
|
|
967
|
+
}, agent.AskAnswer{Denied: !allowed, Message: message})
|
|
968
|
+
if eerr != nil {
|
|
969
|
+
return false
|
|
970
|
+
}
|
|
971
|
+
select {
|
|
972
|
+
case controlReplies <- string(raw):
|
|
973
|
+
return true
|
|
974
|
+
default:
|
|
975
|
+
return false // 缓冲区满 = 引擎侧没在读,如实失败
|
|
976
|
+
}
|
|
977
|
+
},
|
|
887
978
|
})
|
|
888
979
|
defer ctl.Close()
|
|
889
980
|
}
|
|
@@ -961,6 +1052,18 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
961
1052
|
}
|
|
962
1053
|
}
|
|
963
1054
|
isCommand := isSlashCommand(lastUserMsg)
|
|
1055
|
+
/* core 自己合成的帧(command / fallback)统一走这里:JSON 形态落盘 + 外发,
|
|
1056
|
+
text 形态只落 sessionlog(stdout 是正文通道)。 */
|
|
1057
|
+
emitCoreFrame := func(ev agent.StreamEvent, textOnly bool) {
|
|
1058
|
+
if format == agent.FormatJSON {
|
|
1059
|
+
_ = sink.stream(ev)
|
|
1060
|
+
wireLogger(ev)
|
|
1061
|
+
return
|
|
1062
|
+
}
|
|
1063
|
+
if textOnly && sessionLog != nil {
|
|
1064
|
+
sessionLog.push(sessionEvent{Kind: string(ev.Kind), Text: ev.Text})
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
964
1067
|
emitCommand := func(status agent.StreamStatus) {
|
|
965
1068
|
if !isCommand {
|
|
966
1069
|
return
|
|
@@ -977,8 +1080,26 @@ func runStreamAsk(cmd *cobra.Command, args []string, opts *askOptions) error {
|
|
|
977
1080
|
}
|
|
978
1081
|
emitCommand(agent.StatusRunning)
|
|
979
1082
|
|
|
980
|
-
|
|
1083
|
+
/* 降级那一帧在 agent 层发出(走 onEvent),这里同时落 sessionlog —— 历史回放要能
|
|
1084
|
+
看到「这条回答是备用给的」。两个动作都放 onEvent 里:text 形态只落盘、JSON 形态
|
|
1085
|
+
落盘 + 外发(emitCoreFrame 的分支)。 */
|
|
1086
|
+
if sessionLog != nil {
|
|
1087
|
+
inner := onEvent
|
|
1088
|
+
onEvent = func(ev agent.StreamEvent) {
|
|
1089
|
+
if ev.Kind == agent.KindFallback {
|
|
1090
|
+
sessionLog.push(sessionEvent{Kind: string(agent.KindFallback), Text: ev.Text})
|
|
1091
|
+
}
|
|
1092
|
+
inner(ev)
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
res, fbInfo, err := agent.StreamWithFallback(ctx, engine, req, onEvent)
|
|
1096
|
+
/* 主备结论(2026-10-26):**没降级的原因**补一条 notice —— 用户配过备用却看到一个
|
|
1097
|
+
普通的失败时,得能分辨「配错 / 能力不支持 / 主引擎已经出过正文」。
|
|
1098
|
+
只在失败路径报:成功轮不打扰(配了备用、主引擎也跑好了,这是常态)。 */
|
|
981
1099
|
interrupted := ctl != nil && ctl.WasInterrupted()
|
|
1100
|
+
if err != nil && !interrupted && fbInfo.Reason != "" {
|
|
1101
|
+
emitCoreFrame(agent.StreamEvent{Kind: agent.KindNotice, Text: "本轮没有降级:" + fbInfo.Reason}, true)
|
|
1102
|
+
}
|
|
982
1103
|
if interrupted {
|
|
983
1104
|
/* 打断的应答放在这里、而不是命令到达的那一刻:interrupt 的真实含义是
|
|
984
1105
|
「这一轮真的结束了」。失败链本身就是 context canceled,不必再包一层。 */
|
|
@@ -1360,6 +1481,16 @@ func prepareAsk(cmd *cobra.Command, args []string, opts *askOptions) (agent.Engi
|
|
|
1360
1481
|
t := opts.temperature
|
|
1361
1482
|
req.Temperature = &t
|
|
1362
1483
|
}
|
|
1484
|
+
/* 备用引擎(2026-10-24):指向不存在的引擎属于**配置错**,开跑前就报,
|
|
1485
|
+
别等主引擎失败后才发现降级目标也没有。与主引擎同名 = 没配(不重复跑一次)。 */
|
|
1486
|
+
if fb := strings.TrimSpace(opts.fallbackEngine); fb != "" && !strings.EqualFold(fb, engine.Name()) {
|
|
1487
|
+
if agent.Lookup(fb) == nil {
|
|
1488
|
+
return nil, "", agent.Request{}, &usageError{fmt.Errorf(
|
|
1489
|
+
"--fallback-engine %q 不存在(可用引擎见 magic-agent --engines)", fb)}
|
|
1490
|
+
}
|
|
1491
|
+
req.FallbackEngine = fb
|
|
1492
|
+
req.FallbackModel = strings.TrimSpace(opts.fallbackModel)
|
|
1493
|
+
}
|
|
1363
1494
|
if s := strings.TrimSpace(opts.jsonSchema); s != "" {
|
|
1364
1495
|
schema, err := parseJSONSchemaInline(s)
|
|
1365
1496
|
if err != nil {
|
package/internal/cli/ask_test.go
CHANGED
|
@@ -641,7 +641,7 @@ func unregisterLast() {}
|
|
|
641
641
|
// ── --engines flag ───────────────────────────────────────────
|
|
642
642
|
//
|
|
643
643
|
// 注意:以下用例一律带 --no-models —— 模型探测会真的启动本机 CLI
|
|
644
|
-
//(
|
|
644
|
+
//(traex / llm / codex / openclaw / codebuddy),单元测试不该依赖它们。
|
|
645
645
|
// models 字段本身的行为由 TestEnginesFlagIncludesModels 用假引擎 + 假探测覆盖。
|
|
646
646
|
|
|
647
647
|
// --engines 默认即输出 JSON 数组(含引擎名与是否可用),无需 --json。
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
// checkin.go - `--checkin <engine>`:领取该引擎账号的每日积分。
|
|
4
|
+
//
|
|
5
|
+
// 与 --login 同源:读该引擎自己的登录票据(codebuddy-ai 读隔离目录),
|
|
6
|
+
// 调 CodeBuddy 账单接口 POST /billing/meter/daily-checkin。每天只能领一次,
|
|
7
|
+
// 重复领取返回服务端业务错误(ok:false + message),不算命令失败(exit 0)。
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"context"
|
|
11
|
+
"fmt"
|
|
12
|
+
"strings"
|
|
13
|
+
"time"
|
|
14
|
+
|
|
15
|
+
"github.com/darren/magic-agent/internal/agent"
|
|
16
|
+
"github.com/spf13/cobra"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// checkinOutput `--checkin` 的输出形状(客户端 / 脚本都消费它)。
|
|
20
|
+
type checkinOutput struct {
|
|
21
|
+
Engine string `json:"engine"`
|
|
22
|
+
// OK 本次是否真的领到(false 时看 message:已领 / 活动未开 / 未登录)。
|
|
23
|
+
OK bool `json:"ok"`
|
|
24
|
+
// Credit 本次到账积分(字符串,可能为空)。
|
|
25
|
+
Credit string `json:"credit,omitempty"`
|
|
26
|
+
// StreakDays 连续签到天数。
|
|
27
|
+
StreakDays int `json:"streak_days,omitempty"`
|
|
28
|
+
// CheckedToday 领取后恒为 true;失败时保留服务端语义(此处为 false)。
|
|
29
|
+
CheckedToday bool `json:"checked_today"`
|
|
30
|
+
// Message 业务失败原因。
|
|
31
|
+
Message string `json:"message,omitempty"`
|
|
32
|
+
// CreditLeft 领取后的账号剩余积分(能查到才给)。
|
|
33
|
+
CreditLeft string `json:"credit_left,omitempty"`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// runCheckin 处理 `--checkin <engine>`。
|
|
37
|
+
func runCheckin(cmd *cobra.Command, opts *askOptions) error {
|
|
38
|
+
name := strings.TrimSpace(opts.checkin)
|
|
39
|
+
if name == "" {
|
|
40
|
+
return &usageError{fmt.Errorf("--checkin 需要引擎名:magic-agent --checkin <engine>(当前支持 codebuddy / codebuddy-ai)")}
|
|
41
|
+
}
|
|
42
|
+
eng := agent.Lookup(name)
|
|
43
|
+
if eng == nil {
|
|
44
|
+
return &usageError{fmt.Errorf("未知引擎 %q(当前支持 codebuddy / codebuddy-ai)", name)}
|
|
45
|
+
}
|
|
46
|
+
c, ok := eng.(agent.Checkiner)
|
|
47
|
+
if !ok {
|
|
48
|
+
return &usageError{fmt.Errorf("引擎 %s 不支持 --checkin:每日积分只有 codebuddy / codebuddy-ai 有", eng.Name())}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ctx, cancel := context.WithTimeout(cmd.Context(), 20*time.Second)
|
|
52
|
+
defer cancel()
|
|
53
|
+
|
|
54
|
+
out := checkinOutput{Engine: eng.Name()}
|
|
55
|
+
info, err := c.ClaimCheckin(ctx)
|
|
56
|
+
if err != nil {
|
|
57
|
+
out.OK = false
|
|
58
|
+
out.Message = err.Error()
|
|
59
|
+
if info.Message != "" {
|
|
60
|
+
out.Message = info.Message
|
|
61
|
+
}
|
|
62
|
+
// 「今天已签到」这类业务失败也带上真实状态,客户端能把「领取」翻成「今日已领」。
|
|
63
|
+
out.CheckedToday = info.CheckedToday
|
|
64
|
+
out.StreakDays = info.StreakDays
|
|
65
|
+
return printJSON(cmd.OutOrStdout(), out)
|
|
66
|
+
}
|
|
67
|
+
out.OK = true
|
|
68
|
+
out.Credit = info.Credit
|
|
69
|
+
out.StreakDays = info.StreakDays
|
|
70
|
+
out.CheckedToday = true
|
|
71
|
+
|
|
72
|
+
// 领取后顺手刷新剩余额度(查不到就省略;不让它影响领取结果)。
|
|
73
|
+
if cl, ok := eng.(agent.CreditLister); ok {
|
|
74
|
+
out.CreditLeft = cl.CreditLeft(ctx)
|
|
75
|
+
}
|
|
76
|
+
return printJSON(cmd.OutOrStdout(), out)
|
|
77
|
+
}
|
|
@@ -36,7 +36,8 @@ func clientSubcommand() *cobra.Command {
|
|
|
36
36
|
Short: "客户端插件(magic-client):后台启动 / 停止 / 看状态,手机据此连接",
|
|
37
37
|
Long: `把参数原样交给 magic-client(客户端插件,随 npm 包一起安装)。
|
|
38
38
|
|
|
39
|
-
magic-agent client start
|
|
39
|
+
magic-agent client start # 后台常驻(默认绑 0.0.0.0:9100),打印手机地址与配对二维码
|
|
40
|
+
magic-agent client start --local # 只绑 127.0.0.1,手机连不上
|
|
40
41
|
magic-agent client status # 在不在、端口、手机地址、健康检查
|
|
41
42
|
magic-agent client stop # 停掉(本来没跑也算成功)
|
|
42
43
|
|
|
@@ -45,7 +46,7 @@ func clientSubcommand() *cobra.Command {
|
|
|
45
46
|
状态文件(~/.magic-client/client.json),三个动作围绕它工作,令牌跨重启稳定
|
|
46
47
|
(存 ~/.magic-client/token,删掉它才会换)。
|
|
47
48
|
|
|
48
|
-
⚠️
|
|
49
|
+
⚠️ 默认绑局域网(0.0.0.0:9100):手机要和这台电脑在**同一个 Wi-Fi**。跨网访问不在本命令范围内。
|
|
49
50
|
|
|
50
51
|
本命令不在 core 里实现,只负责转发(桌面 / 移动端是可选插件,core 不依赖它)。
|
|
51
52
|
详细 flag 见 magic-client start --help。`,
|