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,283 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
// custom_models_test.go - 自定义模型端点同步(见 custom_models.go)。
|
|
4
|
+
//
|
|
5
|
+
// 背景回归:桌面 App 配好的 custom-local 模型,在独立 CLI 下会报
|
|
6
|
+
// "Custom model <id> has no endpoint url configured" —— 端点必须落在
|
|
7
|
+
// <配置目录>/models.json。这里锁住四条性质:
|
|
8
|
+
// 1. 首次同步:建文件、含 url/apiKey、availableModels 与 CLI 自己的行为对齐、权限 600(含密钥)
|
|
9
|
+
// 2. 幂等:无新增时**不落盘**(内容逐字节不变)
|
|
10
|
+
// 3. 只增不改:既有条目与未识别字段原样保留;availableModels 追加不丢
|
|
11
|
+
// 4. 安全兜底:App 缓存缺失 / 条目 disabled → 不建文件、不报错
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"encoding/json"
|
|
15
|
+
"os"
|
|
16
|
+
"path/filepath"
|
|
17
|
+
"testing"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// writeJSON 写一个 JSON 文件(测试夹具)。
|
|
21
|
+
func writeJSON(t *testing.T, path string, v any) {
|
|
22
|
+
t.Helper()
|
|
23
|
+
b, err := json.Marshal(v)
|
|
24
|
+
if err != nil {
|
|
25
|
+
t.Fatalf("marshal fixture: %v", err)
|
|
26
|
+
}
|
|
27
|
+
if err := os.WriteFile(path, b, 0o644); err != nil {
|
|
28
|
+
t.Fatalf("write fixture: %v", err)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// appCacheFixture 造一份 App 合并配置缓存的等价结构(嵌套 models + disabled 条目 + 非模型字段)。
|
|
33
|
+
func appCacheFixture() map[string]any {
|
|
34
|
+
return map[string]any{
|
|
35
|
+
"authentication": map[string]any{"id": "codebuddy"},
|
|
36
|
+
"models": []any{
|
|
37
|
+
map[string]any{"id": "hy3", "credits": "x0.00"},
|
|
38
|
+
map[string]any{
|
|
39
|
+
"id": "custom-local:MiniMax-M3", "name": "MiniMax-M3",
|
|
40
|
+
"url": "https://api.minimaxi.com/v1", "apiKey": "sk-test-key",
|
|
41
|
+
"tags": []any{"custom"}, "supportsImages": true,
|
|
42
|
+
},
|
|
43
|
+
map[string]any{
|
|
44
|
+
"id": "custom-local:MiniMax-M3.1-Flash-Preview", "name": "MiniMax-M3.1-Flash-Preview",
|
|
45
|
+
"url": "https://api.minimaxi.com/v1", "apiKey": "sk-test-key-2",
|
|
46
|
+
},
|
|
47
|
+
// disabled 条目不得同步
|
|
48
|
+
map[string]any{
|
|
49
|
+
"id": "custom-local:disabled-one", "url": "https://x/v1",
|
|
50
|
+
"apiKey": "sk-dead", "disabled": true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func TestSyncCustomModelsFromApp_FirstSync(t *testing.T) {
|
|
57
|
+
dir := t.TempDir()
|
|
58
|
+
acc := filepath.Join(dir, "acc-product-config-v3.json")
|
|
59
|
+
writeJSON(t, acc, appCacheFixture())
|
|
60
|
+
cfg := filepath.Join(dir, "config")
|
|
61
|
+
if err := os.MkdirAll(cfg, 0o700); err != nil {
|
|
62
|
+
t.Fatal(err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
added, err := syncCustomModelsFromApp(acc, cfg)
|
|
66
|
+
if err != nil {
|
|
67
|
+
t.Fatalf("sync: %v", err)
|
|
68
|
+
}
|
|
69
|
+
if added != 2 {
|
|
70
|
+
t.Errorf("added = %d, want 2(两个 enabled 的 custom-local)", added)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
path := filepath.Join(cfg, customModelsFileName)
|
|
74
|
+
b, err := os.ReadFile(path)
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatalf("models.json 应被创建: %v", err)
|
|
77
|
+
}
|
|
78
|
+
var doc struct {
|
|
79
|
+
Models []map[string]any `json:"models"`
|
|
80
|
+
AvailableModels []string `json:"availableModels"`
|
|
81
|
+
}
|
|
82
|
+
if err := json.Unmarshal(b, &doc); err != nil {
|
|
83
|
+
t.Fatalf("parse models.json: %v", err)
|
|
84
|
+
}
|
|
85
|
+
if len(doc.Models) != 2 {
|
|
86
|
+
t.Fatalf("models = %d, want 2", len(doc.Models))
|
|
87
|
+
}
|
|
88
|
+
// 端点与密钥字段必须原样带过去(CLI 就是靠 url 才能路由)
|
|
89
|
+
var found bool
|
|
90
|
+
for _, m := range doc.Models {
|
|
91
|
+
if m["id"] == "custom-local:MiniMax-M3" {
|
|
92
|
+
found = true
|
|
93
|
+
if m["url"] != "https://api.minimaxi.com/v1" || m["apiKey"] != "sk-test-key" {
|
|
94
|
+
t.Errorf("端点/密钥没带过去: %v", m)
|
|
95
|
+
}
|
|
96
|
+
if m["supportsImages"] != true {
|
|
97
|
+
t.Errorf("其它字段应原样保留: %v", m)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if !found {
|
|
102
|
+
t.Errorf("缺条目 custom-local:MiniMax-M3: %v", doc.Models)
|
|
103
|
+
}
|
|
104
|
+
// availableModels 与 CLI 自己的 saveCustomModel 行为对齐
|
|
105
|
+
for _, id := range []string{"custom-local:MiniMax-M3", "custom-local:MiniMax-M3.1-Flash-Preview"} {
|
|
106
|
+
if !containsStr(doc.AvailableModels, id) {
|
|
107
|
+
t.Errorf("availableModels 缺 %q: %v", id, doc.AvailableModels)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 含第三方密钥 → 仅属主可读写
|
|
111
|
+
if fi, err := os.Stat(path); err != nil {
|
|
112
|
+
t.Fatal(err)
|
|
113
|
+
} else if fi.Mode().Perm() != 0o600 {
|
|
114
|
+
t.Errorf("权限 = %v, want 0600(文件含 apiKey)", fi.Mode().Perm())
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func TestSyncCustomModelsFromApp_IdempotentAndMergeOnly(t *testing.T) {
|
|
119
|
+
dir := t.TempDir()
|
|
120
|
+
acc := filepath.Join(dir, "acc.json")
|
|
121
|
+
writeJSON(t, acc, appCacheFixture())
|
|
122
|
+
cfg := filepath.Join(dir, "config")
|
|
123
|
+
if err := os.MkdirAll(cfg, 0o700); err != nil {
|
|
124
|
+
t.Fatal(err)
|
|
125
|
+
}
|
|
126
|
+
if _, err := syncCustomModelsFromApp(acc, cfg); err != nil {
|
|
127
|
+
t.Fatal(err)
|
|
128
|
+
}
|
|
129
|
+
path := filepath.Join(cfg, customModelsFileName)
|
|
130
|
+
before, err := os.ReadFile(path)
|
|
131
|
+
if err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 第二次:无新增 → 不落盘(内容逐字节不变)
|
|
136
|
+
added, err := syncCustomModelsFromApp(acc, cfg)
|
|
137
|
+
if err != nil {
|
|
138
|
+
t.Fatalf("second sync: %v", err)
|
|
139
|
+
}
|
|
140
|
+
if added != 0 {
|
|
141
|
+
t.Errorf("第二次应新增 0,得到 %d", added)
|
|
142
|
+
}
|
|
143
|
+
after, err := os.ReadFile(path)
|
|
144
|
+
if err != nil {
|
|
145
|
+
t.Fatal(err)
|
|
146
|
+
}
|
|
147
|
+
if string(before) != string(after) {
|
|
148
|
+
t.Error("无新增时不应改写 models.json")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 用户手工加过条目 / 别的字段 / availableModels → 必须保留,只追加
|
|
152
|
+
user := map[string]any{
|
|
153
|
+
"models": []any{map[string]any{"id": "custom-local:user-added", "url": "https://u/v1", "apiKey": "sk-u"}},
|
|
154
|
+
"availableModels": []string{"custom-local:user-added", "hy3"},
|
|
155
|
+
"unknownField": "keep-me",
|
|
156
|
+
}
|
|
157
|
+
writeJSON(t, path, user)
|
|
158
|
+
if _, err := syncCustomModelsFromApp(acc, cfg); err != nil {
|
|
159
|
+
t.Fatal(err)
|
|
160
|
+
}
|
|
161
|
+
b, err := os.ReadFile(path)
|
|
162
|
+
if err != nil {
|
|
163
|
+
t.Fatal(err)
|
|
164
|
+
}
|
|
165
|
+
var doc map[string]any
|
|
166
|
+
if err := json.Unmarshal(b, &doc); err != nil {
|
|
167
|
+
t.Fatal(err)
|
|
168
|
+
}
|
|
169
|
+
if doc["unknownField"] != "keep-me" {
|
|
170
|
+
t.Errorf("未识别字段被改动了: %v", doc["unknownField"])
|
|
171
|
+
}
|
|
172
|
+
ids := map[string]bool{}
|
|
173
|
+
for _, m := range doc["models"].([]any) {
|
|
174
|
+
ids[m.(map[string]any)["id"].(string)] = true
|
|
175
|
+
}
|
|
176
|
+
if !ids["custom-local:user-added"] || !ids["custom-local:MiniMax-M3"] {
|
|
177
|
+
t.Errorf("应同时保留用户条目与同步条目: %v", ids)
|
|
178
|
+
}
|
|
179
|
+
avail, _ := doc["availableModels"].([]any)
|
|
180
|
+
if !hasStringInList(avail, "custom-local:user-added") || !hasStringInList(avail, "hy3") {
|
|
181
|
+
t.Errorf("既有 availableModels 不得丢失: %v", avail)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestSyncCustomModelsFromApp_NoSourceNoFile(t *testing.T) {
|
|
186
|
+
dir := t.TempDir()
|
|
187
|
+
cfg := filepath.Join(dir, "config")
|
|
188
|
+
path := filepath.Join(cfg, customModelsFileName)
|
|
189
|
+
|
|
190
|
+
cases := []struct{ name, acc string }{
|
|
191
|
+
{"acc 路径为空", ""},
|
|
192
|
+
{"acc 文件不存在", filepath.Join(dir, "nope.json")},
|
|
193
|
+
{"acc 是坏 JSON", filepath.Join(dir, "broken.json")},
|
|
194
|
+
{"acc 里没有自定义模型", filepath.Join(dir, "plain.json")},
|
|
195
|
+
}
|
|
196
|
+
if err := os.WriteFile(filepath.Join(dir, "broken.json"), []byte("{oops"), 0o644); err != nil {
|
|
197
|
+
t.Fatal(err)
|
|
198
|
+
}
|
|
199
|
+
writeJSON(t, filepath.Join(dir, "plain.json"), map[string]any{
|
|
200
|
+
"models": []any{map[string]any{"id": "hy3"}},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
for _, tc := range cases {
|
|
204
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
205
|
+
added, err := syncCustomModelsFromApp(tc.acc, cfg)
|
|
206
|
+
if err != nil {
|
|
207
|
+
t.Errorf("应静默返回,不该报错: %v", err)
|
|
208
|
+
}
|
|
209
|
+
if added != 0 {
|
|
210
|
+
t.Errorf("added = %d, want 0", added)
|
|
211
|
+
}
|
|
212
|
+
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
213
|
+
t.Errorf("没有可同步的内容时不该建文件")
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// configDir 为空同样静默
|
|
219
|
+
if added, err := syncCustomModelsFromApp(filepath.Join(dir, "plain.json"), ""); added != 0 || err != nil {
|
|
220
|
+
t.Errorf("configDir 为空应静默, got (%d, %v)", added, err)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func TestSyncCustomModelsFromApp_CorruptExistingFileRebuilt(t *testing.T) {
|
|
225
|
+
dir := t.TempDir()
|
|
226
|
+
acc := filepath.Join(dir, "acc.json")
|
|
227
|
+
writeJSON(t, acc, appCacheFixture())
|
|
228
|
+
cfg := filepath.Join(dir, "config")
|
|
229
|
+
if err := os.MkdirAll(cfg, 0o700); err != nil {
|
|
230
|
+
t.Fatal(err)
|
|
231
|
+
}
|
|
232
|
+
// 既有 models.json 坏了:应重建(CLI 自己也这么兜:读失败当空),且不崩
|
|
233
|
+
if err := os.WriteFile(filepath.Join(cfg, customModelsFileName), []byte("{oops"), 0o600); err != nil {
|
|
234
|
+
t.Fatal(err)
|
|
235
|
+
}
|
|
236
|
+
added, err := syncCustomModelsFromApp(acc, cfg)
|
|
237
|
+
if err != nil {
|
|
238
|
+
t.Fatalf("坏文件应可恢复: %v", err)
|
|
239
|
+
}
|
|
240
|
+
if added != 2 {
|
|
241
|
+
t.Errorf("added = %d, want 2", added)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func TestEnsureCustomModelEndpoint_OnlyCustomLocal(t *testing.T) {
|
|
246
|
+
dir := t.TempDir()
|
|
247
|
+
acc := filepath.Join(dir, "acc.json")
|
|
248
|
+
writeJSON(t, acc, appCacheFixture())
|
|
249
|
+
cfg := filepath.Join(dir, "config")
|
|
250
|
+
|
|
251
|
+
c := codebuddyCore{name: "codebuddy", accConfigPath: acc, configDir: cfg}
|
|
252
|
+
path := filepath.Join(cfg, customModelsFileName)
|
|
253
|
+
|
|
254
|
+
// 官方模型:不该碰 models.json(避免给每条请求加无谓的文件读)
|
|
255
|
+
c.ensureCustomModelEndpoint("hy3")
|
|
256
|
+
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
257
|
+
t.Errorf("官方模型不该触发同步")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 自定义模型:补端点
|
|
261
|
+
c.ensureCustomModelEndpoint("custom-local:MiniMax-M3")
|
|
262
|
+
if _, err := os.Stat(path); err != nil {
|
|
263
|
+
t.Errorf("自定义模型应触发同步: %v", err)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 用户显式指定 CODEBUDDY_CONFIG_DIR 时,落点跟着走(与 CLI 的实际读取一致)
|
|
267
|
+
alt := filepath.Join(dir, "alt-config")
|
|
268
|
+
t.Setenv("CODEBUDDY_CONFIG_DIR", alt)
|
|
269
|
+
c.ensureCustomModelEndpoint("custom-local:MiniMax-M3")
|
|
270
|
+
if _, err := os.Stat(filepath.Join(alt, customModelsFileName)); err != nil {
|
|
271
|
+
t.Errorf("应写到 CODEBUDDY_CONFIG_DIR: %v", err)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// hasStringInList 判断 []any 里是否有该字符串(availableModels 反序列化成 []any)。
|
|
276
|
+
func hasStringInList(list []any, want string) bool {
|
|
277
|
+
for _, v := range list {
|
|
278
|
+
if s, ok := v.(string); ok && s == want {
|
|
279
|
+
return true
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return false
|
|
283
|
+
}
|