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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -0
  3. package/cmd/magic-agent/main.go +12 -0
  4. package/cmd/magic-client/main.go +144 -0
  5. package/go.mod +11 -0
  6. package/go.sum +12 -0
  7. package/internal/agent/arkclaw.go +587 -0
  8. package/internal/agent/arkclaw_stream.go +312 -0
  9. package/internal/agent/arkclaw_stream_test.go +428 -0
  10. package/internal/agent/arkclaw_test.go +827 -0
  11. package/internal/agent/ask.go +694 -0
  12. package/internal/agent/ask_test.go +587 -0
  13. package/internal/agent/attachments.go +200 -0
  14. package/internal/agent/attachments_test.go +481 -0
  15. package/internal/agent/capability.go +154 -0
  16. package/internal/agent/capability_test.go +190 -0
  17. package/internal/agent/claude.go +406 -0
  18. package/internal/agent/codebuddy.go +1044 -0
  19. package/internal/agent/codebuddy_account_test.go +128 -0
  20. package/internal/agent/codebuddy_gateway.go +479 -0
  21. package/internal/agent/codebuddy_gateway_stream.go +358 -0
  22. package/internal/agent/codebuddy_gateway_test.go +673 -0
  23. package/internal/agent/codex.go +599 -0
  24. package/internal/agent/codex_home.go +206 -0
  25. package/internal/agent/codex_home_test.go +130 -0
  26. package/internal/agent/codex_stream_test.go +97 -0
  27. package/internal/agent/codex_test.go +280 -0
  28. package/internal/agent/custom_models.go +197 -0
  29. package/internal/agent/custom_models_test.go +283 -0
  30. package/internal/agent/dsh.go +754 -0
  31. package/internal/agent/dsh_sdk.go +679 -0
  32. package/internal/agent/dsh_sdk_test.go +726 -0
  33. package/internal/agent/dsh_test.go +759 -0
  34. package/internal/agent/engine.go +469 -0
  35. package/internal/agent/engine_base.go +497 -0
  36. package/internal/agent/engine_base_test.go +308 -0
  37. package/internal/agent/engine_test.go +858 -0
  38. package/internal/agent/env.go +88 -0
  39. package/internal/agent/env_test.go +106 -0
  40. package/internal/agent/jsonextract.go +128 -0
  41. package/internal/agent/jsonextract_test.go +110 -0
  42. package/internal/agent/keepalive_test.go +216 -0
  43. package/internal/agent/llmengine.go +470 -0
  44. package/internal/agent/llmengine_test.go +551 -0
  45. package/internal/agent/llmhttp.go +437 -0
  46. package/internal/agent/llmhttp_test.go +304 -0
  47. package/internal/agent/llmmodels.go +205 -0
  48. package/internal/agent/login.go +27 -0
  49. package/internal/agent/main_test.go +23 -0
  50. package/internal/agent/models.go +686 -0
  51. package/internal/agent/models_test.go +877 -0
  52. package/internal/agent/openclaw.go +543 -0
  53. package/internal/agent/openclaw_acp.go +949 -0
  54. package/internal/agent/openclaw_acp_test.go +406 -0
  55. package/internal/agent/openclaw_test.go +451 -0
  56. package/internal/agent/output.go +191 -0
  57. package/internal/agent/permission.go +362 -0
  58. package/internal/agent/permission_test.go +526 -0
  59. package/internal/agent/prompt.go +65 -0
  60. package/internal/agent/runcmd.go +259 -0
  61. package/internal/agent/runcmd_test.go +81 -0
  62. package/internal/agent/runcmd_unix.go +56 -0
  63. package/internal/agent/runcmd_windows.go +66 -0
  64. package/internal/agent/runner.go +180 -0
  65. package/internal/agent/runner_test.go +263 -0
  66. package/internal/agent/spawnhook_test.go +96 -0
  67. package/internal/agent/stream.go +882 -0
  68. package/internal/agent/stream_test.go +565 -0
  69. package/internal/agent/streamjson.go +185 -0
  70. package/internal/agent/tags.go +52 -0
  71. package/internal/agent/toolkind.go +96 -0
  72. package/internal/agent/toolkind_test.go +70 -0
  73. package/internal/agent/tools_suffix_test.go +261 -0
  74. package/internal/agent/trae.go +342 -0
  75. package/internal/agent/workspace_test.go +166 -0
  76. package/internal/boundary/boundary_test.go +165 -0
  77. package/internal/cli/ask.go +1113 -0
  78. package/internal/cli/ask_test.go +1088 -0
  79. package/internal/cli/attach_test.go +239 -0
  80. package/internal/cli/contract_test.go +157 -0
  81. package/internal/cli/control.go +149 -0
  82. package/internal/cli/control_test.go +192 -0
  83. package/internal/cli/engines_test.go +205 -0
  84. package/internal/cli/events.go +198 -0
  85. package/internal/cli/events_test.go +166 -0
  86. package/internal/cli/keepalive_test.go +391 -0
  87. package/internal/cli/login.go +78 -0
  88. package/internal/cli/permission_test.go +295 -0
  89. package/internal/cli/prompt_source_test.go +222 -0
  90. package/internal/cli/root.go +752 -0
  91. package/internal/cli/session.go +486 -0
  92. package/internal/cli/session_test.go +202 -0
  93. package/internal/cli/sessionlog.go +325 -0
  94. package/internal/cli/sessionlog_command_test.go +204 -0
  95. package/internal/cli/sessionlog_test.go +262 -0
  96. package/internal/cli/sessionlog_user_test.go +129 -0
  97. package/internal/cli/toolkind_wire_test.go +84 -0
  98. package/internal/cli/workspace_test.go +188 -0
  99. package/internal/client/cors_test.go +136 -0
  100. package/internal/client/lan.go +68 -0
  101. package/internal/client/lan_test.go +53 -0
  102. package/internal/client/pair.go +163 -0
  103. package/internal/client/pair_test.go +109 -0
  104. package/internal/client/profile.go +184 -0
  105. package/internal/client/service.go +888 -0
  106. package/internal/client/service_test.go +654 -0
  107. package/internal/client/session_events_test.go +186 -0
  108. package/internal/client/session_messages_test.go +150 -0
  109. package/internal/config/config.go +439 -0
  110. package/internal/config/config_test.go +448 -0
  111. package/internal/session/session.go +447 -0
  112. package/internal/session/session_append.go +241 -0
  113. package/internal/session/session_append_test.go +161 -0
  114. package/internal/session/session_kill_unix_test.go +90 -0
  115. package/internal/session/session_mark_test.go +95 -0
  116. package/internal/session/session_test.go +237 -0
  117. package/internal/session/sessionlog.go +128 -0
  118. package/internal/session/sessionlog_read_test.go +157 -0
  119. package/npm/dist/darwin-arm64/magic-agent +0 -0
  120. package/npm/dist/darwin-x64/magic-agent +0 -0
  121. package/npm/dist/linux-arm64/magic-agent +0 -0
  122. package/npm/dist/linux-x64/magic-agent +0 -0
  123. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  124. package/npm/install.js +121 -0
  125. package/npm/lib/platform.js +78 -0
  126. 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
+ }