magic-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/cmd/magic-agent/main.go +12 -0
- package/cmd/magic-client/main.go +144 -0
- package/go.mod +11 -0
- package/go.sum +12 -0
- package/internal/agent/arkclaw.go +587 -0
- package/internal/agent/arkclaw_stream.go +312 -0
- package/internal/agent/arkclaw_stream_test.go +428 -0
- package/internal/agent/arkclaw_test.go +827 -0
- package/internal/agent/ask.go +694 -0
- package/internal/agent/ask_test.go +587 -0
- package/internal/agent/attachments.go +200 -0
- package/internal/agent/attachments_test.go +481 -0
- package/internal/agent/capability.go +154 -0
- package/internal/agent/capability_test.go +190 -0
- package/internal/agent/claude.go +406 -0
- package/internal/agent/codebuddy.go +1044 -0
- package/internal/agent/codebuddy_account_test.go +128 -0
- package/internal/agent/codebuddy_gateway.go +479 -0
- package/internal/agent/codebuddy_gateway_stream.go +358 -0
- package/internal/agent/codebuddy_gateway_test.go +673 -0
- package/internal/agent/codex.go +599 -0
- package/internal/agent/codex_home.go +206 -0
- package/internal/agent/codex_home_test.go +130 -0
- package/internal/agent/codex_stream_test.go +97 -0
- package/internal/agent/codex_test.go +280 -0
- package/internal/agent/custom_models.go +197 -0
- package/internal/agent/custom_models_test.go +283 -0
- package/internal/agent/dsh.go +754 -0
- package/internal/agent/dsh_sdk.go +679 -0
- package/internal/agent/dsh_sdk_test.go +726 -0
- package/internal/agent/dsh_test.go +759 -0
- package/internal/agent/engine.go +469 -0
- package/internal/agent/engine_base.go +497 -0
- package/internal/agent/engine_base_test.go +308 -0
- package/internal/agent/engine_test.go +858 -0
- package/internal/agent/env.go +88 -0
- package/internal/agent/env_test.go +106 -0
- package/internal/agent/jsonextract.go +128 -0
- package/internal/agent/jsonextract_test.go +110 -0
- package/internal/agent/keepalive_test.go +216 -0
- package/internal/agent/llmengine.go +470 -0
- package/internal/agent/llmengine_test.go +551 -0
- package/internal/agent/llmhttp.go +437 -0
- package/internal/agent/llmhttp_test.go +304 -0
- package/internal/agent/llmmodels.go +205 -0
- package/internal/agent/login.go +27 -0
- package/internal/agent/main_test.go +23 -0
- package/internal/agent/models.go +686 -0
- package/internal/agent/models_test.go +877 -0
- package/internal/agent/openclaw.go +543 -0
- package/internal/agent/openclaw_acp.go +949 -0
- package/internal/agent/openclaw_acp_test.go +406 -0
- package/internal/agent/openclaw_test.go +451 -0
- package/internal/agent/output.go +191 -0
- package/internal/agent/permission.go +362 -0
- package/internal/agent/permission_test.go +526 -0
- package/internal/agent/prompt.go +65 -0
- package/internal/agent/runcmd.go +259 -0
- package/internal/agent/runcmd_test.go +81 -0
- package/internal/agent/runcmd_unix.go +56 -0
- package/internal/agent/runcmd_windows.go +66 -0
- package/internal/agent/runner.go +180 -0
- package/internal/agent/runner_test.go +263 -0
- package/internal/agent/spawnhook_test.go +96 -0
- package/internal/agent/stream.go +882 -0
- package/internal/agent/stream_test.go +565 -0
- package/internal/agent/streamjson.go +185 -0
- package/internal/agent/tags.go +52 -0
- package/internal/agent/toolkind.go +96 -0
- package/internal/agent/toolkind_test.go +70 -0
- package/internal/agent/tools_suffix_test.go +261 -0
- package/internal/agent/trae.go +342 -0
- package/internal/agent/workspace_test.go +166 -0
- package/internal/boundary/boundary_test.go +165 -0
- package/internal/cli/ask.go +1113 -0
- package/internal/cli/ask_test.go +1088 -0
- package/internal/cli/attach_test.go +239 -0
- package/internal/cli/contract_test.go +157 -0
- package/internal/cli/control.go +149 -0
- package/internal/cli/control_test.go +192 -0
- package/internal/cli/engines_test.go +205 -0
- package/internal/cli/events.go +198 -0
- package/internal/cli/events_test.go +166 -0
- package/internal/cli/keepalive_test.go +391 -0
- package/internal/cli/login.go +78 -0
- package/internal/cli/permission_test.go +295 -0
- package/internal/cli/prompt_source_test.go +222 -0
- package/internal/cli/root.go +752 -0
- package/internal/cli/session.go +486 -0
- package/internal/cli/session_test.go +202 -0
- package/internal/cli/sessionlog.go +325 -0
- package/internal/cli/sessionlog_command_test.go +204 -0
- package/internal/cli/sessionlog_test.go +262 -0
- package/internal/cli/sessionlog_user_test.go +129 -0
- package/internal/cli/toolkind_wire_test.go +84 -0
- package/internal/cli/workspace_test.go +188 -0
- package/internal/client/cors_test.go +136 -0
- package/internal/client/lan.go +68 -0
- package/internal/client/lan_test.go +53 -0
- package/internal/client/pair.go +163 -0
- package/internal/client/pair_test.go +109 -0
- package/internal/client/profile.go +184 -0
- package/internal/client/service.go +888 -0
- package/internal/client/service_test.go +654 -0
- package/internal/client/session_events_test.go +186 -0
- package/internal/client/session_messages_test.go +150 -0
- package/internal/config/config.go +439 -0
- package/internal/config/config_test.go +448 -0
- package/internal/session/session.go +447 -0
- package/internal/session/session_append.go +241 -0
- package/internal/session/session_append_test.go +161 -0
- package/internal/session/session_kill_unix_test.go +90 -0
- package/internal/session/session_mark_test.go +95 -0
- package/internal/session/session_test.go +237 -0
- package/internal/session/sessionlog.go +128 -0
- package/internal/session/sessionlog_read_test.go +157 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/install.js +121 -0
- package/npm/lib/platform.js +78 -0
- package/package.json +36 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
import "sort"
|
|
4
|
+
|
|
5
|
+
/* 契约版本与能力标识 —— magic-client(桌面 / 移动客户端插件)契约面 v1。
|
|
6
|
+
*
|
|
7
|
+
* 为什么需要(2026-09-27):客户端只能通过「唯一来源」拿到引擎能力,否则每个
|
|
8
|
+
* 接入方都会自己写一遍 switch,最后各自漂移。实测已经踩过的同类事故:
|
|
9
|
+
* 客户端拿不到能力字段 → 界面空白;桌面上层按版本号挑二进制 → 挑到旧构建。
|
|
10
|
+
*
|
|
11
|
+
* 与既有 `*SupportOf` 表的关系:那些表给的是「落地方式」(flag:-i /
|
|
12
|
+
* stdin:stream-json / part:file / prompt / none…),属于实现细节;能力 id 是
|
|
13
|
+
* 「结论」(这个引擎能不能收附件、能不能续接)。两者由同一批表推导,实现
|
|
14
|
+
* 细节变化时能力 id 不必跟着变 —— 客户端只认结论。
|
|
15
|
+
*
|
|
16
|
+
* 静态与运行态的边界(重要):
|
|
17
|
+
* capabilities 静态声明,回答「能做什么」,不启动任何 CLI 就能算出来;
|
|
18
|
+
* ok / streaming / models / version 运行态,回答「此刻如何」,探测才拿得到。
|
|
19
|
+
* 客户端按 capabilities 决定界面形态,按运行态决定按钮可用性与降级提示。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ContractVersion 契约面版本。客户端拿它与自己的期望范围比对,超出范围应**明确
|
|
23
|
+
// 报错**并提示升级,而不是带着不匹配的能力继续跑。
|
|
24
|
+
//
|
|
25
|
+
// 递增规则:只增字段(向后兼容)不升版本;一旦有字段改名、语义变化或删除,
|
|
26
|
+
// 必须升版本 —— 老客户端会因版本不匹配而拒绝启动,这正是我们想要的结果。
|
|
27
|
+
const ContractVersion = 1
|
|
28
|
+
|
|
29
|
+
// 能力 id。命名规则:`域.能力`,域只有 session / workspace / attachment /
|
|
30
|
+
// model / engine 五个,客户端按域分组渲染即可。
|
|
31
|
+
const (
|
|
32
|
+
// CapSessionStream 支持 `--stream`:正文或思考增量实时到达(成色见各引擎注释)。
|
|
33
|
+
CapSessionStream = "session.stream"
|
|
34
|
+
// CapSessionAppend 支持常驻会话 + `--append` 追加消息。
|
|
35
|
+
CapSessionAppend = "session.append"
|
|
36
|
+
// CapSessionAsk 支持「需要用户选择」的往返(`{"type":"ask"}` 事件)。
|
|
37
|
+
CapSessionAsk = "session.ask"
|
|
38
|
+
// CapSessionPermission 支持四档权限模型(manual / accept-edits / auto / full)。
|
|
39
|
+
CapSessionPermission = "session.permission"
|
|
40
|
+
// CapWorkspaceSelect 能指定工作目录(原生 flag 或子进程 cwd)。
|
|
41
|
+
CapWorkspaceSelect = "workspace.select"
|
|
42
|
+
// CapAttachmentNative 附件走引擎的原生通道(图片真的进模型上下文)。
|
|
43
|
+
CapAttachmentNative = "attachment.native"
|
|
44
|
+
// CapAttachmentPrompt 附件只有降级通道:把绝对路径写进提示词,靠引擎自己读文件。
|
|
45
|
+
CapAttachmentPrompt = "attachment.prompt"
|
|
46
|
+
// CapModelList 有动态模型清单来源(`--engines` 的 models 字段)。
|
|
47
|
+
CapModelList = "model.list"
|
|
48
|
+
// CapModelCredits 能给出各模型的积分倍率(`model_credits` 字段)。
|
|
49
|
+
CapModelCredits = "model.credits"
|
|
50
|
+
// CapEngineInstall 有一键安装 / 升级命令(`install` 字段)。
|
|
51
|
+
CapEngineInstall = "engine.install"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// CapabilityIDs 返回全部已知能力 id(字典序,稳定)。用于校验与文档生成。
|
|
55
|
+
func CapabilityIDs() []string {
|
|
56
|
+
out := []string{
|
|
57
|
+
CapAttachmentNative,
|
|
58
|
+
CapAttachmentPrompt,
|
|
59
|
+
CapEngineInstall,
|
|
60
|
+
CapModelCredits,
|
|
61
|
+
CapModelList,
|
|
62
|
+
CapSessionAppend,
|
|
63
|
+
CapSessionAsk,
|
|
64
|
+
CapSessionPermission,
|
|
65
|
+
CapSessionStream,
|
|
66
|
+
CapWorkspaceSelect,
|
|
67
|
+
}
|
|
68
|
+
sort.Strings(out)
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// CapabilitiesOf 返回某个引擎**静态声明**的能力 id 集合(字典序,稳定、无重复)。
|
|
73
|
+
//
|
|
74
|
+
// 未注册的引擎名返回 nil —— 调用方据此区分「没这个引擎」与「这个引擎什么都不会」
|
|
75
|
+
// (后者在当前实现里不可能出现:所有内置引擎至少支持 workspace 或附件降级)。
|
|
76
|
+
func CapabilitiesOf(engine string) []string {
|
|
77
|
+
e := Lookup(engine)
|
|
78
|
+
if e == nil {
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
return capabilitiesOfEngine(e)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// capabilitiesOfEngine 按引擎实例推导能力(CapabilitiesOf 的实现)。
|
|
85
|
+
//
|
|
86
|
+
// 单独拆出来是为了让用例能直接拿引擎实例断言,**不必依赖包级注册表的全局状态** ——
|
|
87
|
+
// 注册表有个已知脆弱点(Engines() 之前被 Register 过就不注册内置引擎,见 engine.go),
|
|
88
|
+
// 而按名筛选测试时它会让「按名字查表」的结果凭空消失。
|
|
89
|
+
func capabilitiesOfEngine(e Engine) []string {
|
|
90
|
+
name := e.Name()
|
|
91
|
+
var caps []string
|
|
92
|
+
add := func(id string) { caps = append(caps, id) }
|
|
93
|
+
|
|
94
|
+
if AsStreamer(e) != nil {
|
|
95
|
+
add(CapSessionStream)
|
|
96
|
+
}
|
|
97
|
+
if AppendSupportOf(name) {
|
|
98
|
+
add(CapSessionAppend)
|
|
99
|
+
}
|
|
100
|
+
if !unsupported(AskSupportOf(name)) {
|
|
101
|
+
add(CapSessionAsk)
|
|
102
|
+
}
|
|
103
|
+
if !unsupported(PermissionSupportOf(name)) {
|
|
104
|
+
add(CapSessionPermission)
|
|
105
|
+
}
|
|
106
|
+
if !unsupported(WorkspaceSupportOf(name)) {
|
|
107
|
+
add(CapWorkspaceSelect)
|
|
108
|
+
}
|
|
109
|
+
switch AttachmentSupportOf(name) {
|
|
110
|
+
case "prompt":
|
|
111
|
+
add(CapAttachmentPrompt)
|
|
112
|
+
case "", "none":
|
|
113
|
+
// 无附件通道:不加任何附件能力。
|
|
114
|
+
default:
|
|
115
|
+
add(CapAttachmentNative)
|
|
116
|
+
}
|
|
117
|
+
if ModelListerOf(e) != nil {
|
|
118
|
+
add(CapModelList)
|
|
119
|
+
}
|
|
120
|
+
if _, ok := e.(ModelCreditLister); ok {
|
|
121
|
+
add(CapModelCredits)
|
|
122
|
+
}
|
|
123
|
+
if InstallCommandOf(name) != "" {
|
|
124
|
+
add(CapEngineInstall)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
sort.Strings(caps)
|
|
128
|
+
return caps
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// unsupported 判断一张能力表的取值是否表示「没有这条通道」。
|
|
132
|
+
// 空串与 "none" 都算没有 —— 两张表历史上两种写法都出现过,这里统一兜住。
|
|
133
|
+
func unsupported(v string) bool {
|
|
134
|
+
return v == "" || v == "none"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* 核心级能力 id:不属于任何单个引擎,而是 core 自己提供的**通道**。
|
|
138
|
+
*
|
|
139
|
+
* 与引擎能力分开命名(`feature.` 前缀)与分开返回:客户端要回答的是两个不同问题 ——
|
|
140
|
+
* 「这个引擎能做什么」(engines[].capabilities)与「这台机器上的 core 支不支持这条通道」
|
|
141
|
+
* (features)。混在一个集合里会让前者随 core 升级而无故变化。
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
const (
|
|
145
|
+
// FeatureSessionEvents `--stream --events`:事件流带契约版本与行号,并先发一行 ready。
|
|
146
|
+
FeatureSessionEvents = "feature.session.events"
|
|
147
|
+
// FeatureSessionControl `--stream --control`:stdin NDJSON 控制通道(打断 / 收工 / 回审批)。
|
|
148
|
+
FeatureSessionControl = "feature.session.control"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// CoreFeatures 返回 core 自身提供的通道能力(字典序,稳定)。
|
|
152
|
+
func CoreFeatures() []string {
|
|
153
|
+
return []string{FeatureSessionControl, FeatureSessionEvents}
|
|
154
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sort"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// hasCap 判断能力列表里有没有某个 id。
|
|
10
|
+
func hasCap(caps []string, id string) bool {
|
|
11
|
+
for _, c := range caps {
|
|
12
|
+
if c == id {
|
|
13
|
+
return true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// TestCapabilitiesOfAllEngines 注册表里每个引擎的能力列表都要**结构合法**:
|
|
20
|
+
// 有序、无重复、id 都在已知集合里。
|
|
21
|
+
//
|
|
22
|
+
// 刻意不要求「非空」,也不检查注册表里有多少个引擎:注册表里可能出现用例注册的
|
|
23
|
+
// 探针引擎(它什么都没声明),而且它有个已知脆弱点会让内置引擎在某些运行方式下
|
|
24
|
+
// 整体缺席(见 engine.go 的 registry 注释)。「真实引擎至少有一项能力」由
|
|
25
|
+
// TestCapabilitiesDocumentedEngines 用**直接构造的实例**断言,与注册表状态无关。
|
|
26
|
+
func TestCapabilitiesOfAllEngines(t *testing.T) {
|
|
27
|
+
known := map[string]bool{}
|
|
28
|
+
for _, id := range CapabilityIDs() {
|
|
29
|
+
known[id] = true
|
|
30
|
+
}
|
|
31
|
+
engines := Engines()
|
|
32
|
+
if len(engines) == 0 {
|
|
33
|
+
t.Fatal("引擎注册表为空,能力推导无从校验")
|
|
34
|
+
}
|
|
35
|
+
for _, e := range engines {
|
|
36
|
+
caps := CapabilitiesOf(e.Name())
|
|
37
|
+
if !sort.StringsAreSorted(caps) {
|
|
38
|
+
t.Errorf("%s: 能力列表未排序: %v", e.Name(), caps)
|
|
39
|
+
}
|
|
40
|
+
seen := map[string]bool{}
|
|
41
|
+
for _, id := range caps {
|
|
42
|
+
if seen[id] {
|
|
43
|
+
t.Errorf("%s: 能力重复: %s", e.Name(), id)
|
|
44
|
+
}
|
|
45
|
+
seen[id] = true
|
|
46
|
+
if !known[id] {
|
|
47
|
+
t.Errorf("%s: 未知能力 id %q(新增能力要同时进 CapabilityIDs)", e.Name(), id)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TestCapabilitiesOfUnknownEngine 未注册的引擎名返回 nil —— 调用方据此区分
|
|
54
|
+
// 「没这个引擎」与「这个引擎什么都不会」。
|
|
55
|
+
func TestCapabilitiesOfUnknownEngine(t *testing.T) {
|
|
56
|
+
if caps := CapabilitiesOf("no-such-engine-xyz"); caps != nil {
|
|
57
|
+
t.Errorf("未注册引擎应返回 nil,得到 %v", caps)
|
|
58
|
+
}
|
|
59
|
+
if caps := CapabilitiesOf(""); caps != nil {
|
|
60
|
+
t.Errorf("空名字应返回 nil,得到 %v", caps)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// TestCapabilitiesStable 多次调用结果逐项一致:capabilities 是**静态声明**,
|
|
65
|
+
// 不应受运行态(是否冷却、模型探测成功与否)影响。
|
|
66
|
+
func TestCapabilitiesStable(t *testing.T) {
|
|
67
|
+
for _, e := range Engines() {
|
|
68
|
+
a := CapabilitiesOf(e.Name())
|
|
69
|
+
b := CapabilitiesOf(e.Name())
|
|
70
|
+
if len(a) != len(b) {
|
|
71
|
+
t.Errorf("%s: 两次调用长度不同 %d vs %d", e.Name(), len(a), len(b))
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
for i := range a {
|
|
75
|
+
if a[i] != b[i] {
|
|
76
|
+
t.Errorf("%s: 第 %d 项不稳定 %q vs %q", e.Name(), i, a[i], b[i])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// TestCapabilitiesDocumentedEngines 钉住 README 里已经写明的几条推导,防止把
|
|
83
|
+
// 「原生通道」与「降级通道」写反。
|
|
84
|
+
//
|
|
85
|
+
// 用**直接构造的引擎实例**(capabilitiesOfEngine),不走按名字查表 —— 注册表有全局状态,
|
|
86
|
+
// 会随测试筛选方式变化(见 engine.go 的 registry 注释),而这几条事实与注册表无关。
|
|
87
|
+
func TestCapabilitiesDocumentedEngines(t *testing.T) {
|
|
88
|
+
// claude:stream / ask(AskUserQuestion + can_use_tool)/ 四档权限 / 原生附件 / npm 安装命令
|
|
89
|
+
claude := capabilitiesOfEngine(&ClaudeEngine{})
|
|
90
|
+
if len(claude) == 0 {
|
|
91
|
+
t.Fatal("claude 的能力列表为空(真实引擎至少应有一项)")
|
|
92
|
+
}
|
|
93
|
+
for _, id := range []string{
|
|
94
|
+
CapSessionStream, CapSessionAsk, CapSessionPermission,
|
|
95
|
+
CapAttachmentNative, CapEngineInstall,
|
|
96
|
+
} {
|
|
97
|
+
if !hasCap(claude, id) {
|
|
98
|
+
t.Errorf("claude 应有能力 %s,实际 %v", id, claude)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// llm:README 明确「其余引擎」没有 ask / permission 通道
|
|
103
|
+
llm := capabilitiesOfEngine(&LLMEngine{})
|
|
104
|
+
for _, id := range []string{CapSessionAsk, CapSessionPermission} {
|
|
105
|
+
if hasCap(llm, id) {
|
|
106
|
+
t.Errorf("llm 不应有能力 %s,实际 %v", id, llm)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// codebuddy 系 2026-09-28 起改成**独立安装**的 CodeBuddy Code CLI
|
|
111
|
+
//(不再是桌面 GUI 应用)→ 有可执行安装路径,因此有 engine.install。
|
|
112
|
+
for _, e := range []Engine{&CodeBuddyEngine{}, &CodeBuddyAIEngine{}} {
|
|
113
|
+
if cb := capabilitiesOfEngine(e); !hasCap(cb, CapEngineInstall) {
|
|
114
|
+
t.Errorf("%s 应有能力 %s(npm 一键安装命令),实际 %v", e.Name(), CapEngineInstall, cb)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 反例:没有可执行安装路径的引擎不给 engine.install
|
|
118
|
+
//(codebuddy-gateway 要的不是安装,而是「把网关跑起来」)。
|
|
119
|
+
if gw := capabilitiesOfEngine(&CodeBuddyGatewayEngine{}); hasCap(gw, CapEngineInstall) {
|
|
120
|
+
t.Errorf("codebuddy-gateway 不应有能力 %s(无安装命令),实际 %v", CapEngineInstall, gw)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 走路径降级的引擎:给 attachment.prompt 而不是 attachment.native
|
|
124
|
+
for _, tc := range []struct {
|
|
125
|
+
name string
|
|
126
|
+
e Engine
|
|
127
|
+
}{
|
|
128
|
+
{"trae", &TraeEngine{}},
|
|
129
|
+
{"openclaw", &OpenClawEngine{}},
|
|
130
|
+
{"dsh", &DshEngine{}},
|
|
131
|
+
} {
|
|
132
|
+
caps := capabilitiesOfEngine(tc.e)
|
|
133
|
+
if !hasCap(caps, CapAttachmentPrompt) {
|
|
134
|
+
t.Errorf("%s 应有能力 %s(附件走提示词路径),实际 %v", tc.name, CapAttachmentPrompt, caps)
|
|
135
|
+
}
|
|
136
|
+
if hasCap(caps, CapAttachmentNative) {
|
|
137
|
+
t.Errorf("%s 不应有能力 %s,实际 %v", tc.name, CapAttachmentNative, caps)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// arkclaw 的附件走 A2A 原生 file part → 应给出 attachment.native。
|
|
142
|
+
// 这条直接查表:ArkClawEngine 的名字来自配置(不是常量),不适合直接构造。
|
|
143
|
+
if got := AttachmentSupportOf("arkclaw"); got != "part:file" {
|
|
144
|
+
t.Errorf("arkclaw attachments = %q want part:file(原生 file part → attachment.native)", got)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// TestCoreFeaturesSorted 核心级通道能力要干净可用(客户端拿它判断 core 支不支持某条通道)。
|
|
149
|
+
func TestCoreFeaturesSorted(t *testing.T) {
|
|
150
|
+
f := CoreFeatures()
|
|
151
|
+
if len(f) == 0 {
|
|
152
|
+
t.Fatal("CoreFeatures 为空")
|
|
153
|
+
}
|
|
154
|
+
if !sort.StringsAreSorted(f) {
|
|
155
|
+
t.Errorf("CoreFeatures 未排序: %v", f)
|
|
156
|
+
}
|
|
157
|
+
for i := 1; i < len(f); i++ {
|
|
158
|
+
if f[i] == f[i-1] {
|
|
159
|
+
t.Errorf("CoreFeatures 有重复项: %s", f[i])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for _, want := range []string{FeatureSessionEvents, FeatureSessionControl} {
|
|
163
|
+
if !hasCap(f, want) {
|
|
164
|
+
t.Errorf("CoreFeatures 缺 %s,实际 %v", want, f)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 核心级能力不能混进引擎能力白名单(两者是两个问题)。
|
|
168
|
+
for _, id := range CapabilityIDs() {
|
|
169
|
+
if strings.HasPrefix(id, "feature.") {
|
|
170
|
+
t.Errorf("CapabilityIDs 不应含核心级能力 %q", id)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// TestCapabilityIDsDedupedAndSorted 能力全集本身要干净:无重复、已排序
|
|
176
|
+
// (客户端与服务端都可能拿它做校验白名单)。
|
|
177
|
+
func TestCapabilityIDsDedupedAndSorted(t *testing.T) {
|
|
178
|
+
ids := CapabilityIDs()
|
|
179
|
+
if len(ids) == 0 {
|
|
180
|
+
t.Fatal("CapabilityIDs 为空")
|
|
181
|
+
}
|
|
182
|
+
if !sort.StringsAreSorted(ids) {
|
|
183
|
+
t.Errorf("CapabilityIDs 未排序: %v", ids)
|
|
184
|
+
}
|
|
185
|
+
for i := 1; i < len(ids); i++ {
|
|
186
|
+
if ids[i] == ids[i-1] {
|
|
187
|
+
t.Errorf("CapabilityIDs 有重复项: %s", ids[i])
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|