magic-agent 2.4.2 → 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 +290 -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 +7 -10
- 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 +193 -7
- package/internal/client/lan_test.go +110 -0
- 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/session/sessionlog.go +23 -0
- package/internal/session/sessionlog_read_test.go +39 -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
|
@@ -10,12 +10,15 @@ package agent
|
|
|
10
10
|
// codebuddyBase 注释。
|
|
11
11
|
//
|
|
12
12
|
// 账号怎么分开(一个 CLI 挂两个账号):
|
|
13
|
-
// CLI 的票据路径是
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// CLI 的票据路径是 <os.homedir()>\AppData\Local\CodeBuddyExtension\Data\Public\
|
|
14
|
+
// auth\<authentication.id>.info。key 由 authentication.id 决定,而它的取值链里
|
|
15
|
+
// **ACC_PRODUCT_CONFIG_V3(环境变量)优先于 CLI 包内的 product.json**。
|
|
16
|
+
// ⚠️ CLI 2.174.0 起该优先级不再可靠(getAuthSavePath 先读合并后的产品配置),
|
|
17
|
+
// 故 codebuddy-ai 额外用独立 homedir 物理隔离 auth 目录(见 codebuddyAIHomeEnv);
|
|
18
|
+
// codebuddy 继续用真实 homedir + ~/.codebuddy。
|
|
16
19
|
//
|
|
17
|
-
// codebuddy 账号 id codebuddy → auth/codebuddy.info
|
|
18
|
-
// codebuddy-ai 账号 id codebuddy-ai →
|
|
20
|
+
// codebuddy 账号 id codebuddy → auth/codebuddy.info(真实 homedir)
|
|
21
|
+
// codebuddy-ai 账号 id codebuddy-ai → ~/.codebuddy-ai/AppData/.../auth/*.info
|
|
19
22
|
//
|
|
20
23
|
// 非交互协议与 claude 同源(CodeBuddy Code v2.x)、flag 面一致;模型清单与积分倍率
|
|
21
24
|
// 走同一条扩展链(见 listModels)。
|
|
@@ -58,9 +61,9 @@ import (
|
|
|
58
61
|
"context"
|
|
59
62
|
"encoding/json"
|
|
60
63
|
"fmt"
|
|
61
|
-
"io"
|
|
62
64
|
"os"
|
|
63
65
|
"path/filepath"
|
|
66
|
+
"runtime"
|
|
64
67
|
"strings"
|
|
65
68
|
"time"
|
|
66
69
|
)
|
|
@@ -94,6 +97,12 @@ type codebuddyCore struct {
|
|
|
94
97
|
// extendedModelSources=true 时是扩展清单链 ①;一律用于 modelCredits 的首选 ①;
|
|
95
98
|
// 以及**自定义模型端点同步**的来源(见 custom_models.go)。
|
|
96
99
|
accConfigPath string
|
|
100
|
+
// accountCatalogDir 非空时,清单/倍率的 ① 改用**本账号 CLI 自己下发**的远程
|
|
101
|
+
// 配置缓存(<config_dir>/local_storage),并用同目录的选择器清单过滤;不再读桌面
|
|
102
|
+
// App 的 acc 缓存。codebuddy-ai 用:账号已与 App 物理隔离(独立 homedir),App 的
|
|
103
|
+
// acc 缓存属于另一个账号,只有 5 条模型,会把清单钉死在 2 个 custom-local。
|
|
104
|
+
// 空 = 维持「acc 缓存 → 远程缓存 ∪ product.json」原链(codebuddy 主引擎)。
|
|
105
|
+
accountCatalogDir string
|
|
97
106
|
// configDir 该引擎自己的配置目录(codebuddyDir / codebuddyAIDir),
|
|
98
107
|
// 即 CLI 读 models.json 的地方 —— 自定义模型端点同步的落点。
|
|
99
108
|
// 用户显式设了 CODEBUDDY_CONFIG_DIR 时以那个为准(见 effectiveConfigDir)。
|
|
@@ -137,7 +146,16 @@ func (c codebuddyCore) selectorModels() []string {
|
|
|
137
146
|
// ② 远程配置缓存 ∪ App 包 product.json(超集近似,含国内后端条目)
|
|
138
147
|
//
|
|
139
148
|
// 命中 ① 时 ② 不再叠加 —— ① 就是客户端所见,别无二义。倍率表与清单同源同链。
|
|
149
|
+
//
|
|
150
|
+
// ① 的来源按引擎区分:accountCatalogDir 非空(codebuddy-ai)时用**本账号 CLI 自己
|
|
151
|
+
// 下发**的远程配置缓存 + 同目录选择器清单(见 remoteCacheCatalogWithSelector),
|
|
152
|
+
// 不再读桌面 App 的 acc 缓存 —— 账号隔离后那份属于另一个账号。
|
|
140
153
|
func (c codebuddyCore) extendedModelCatalog(bin string) ([]string, map[string]string, bool) {
|
|
154
|
+
if c.accountCatalogDir != "" {
|
|
155
|
+
if ids, credits := remoteCacheCatalogWithSelector(c.accountCatalogDir); len(ids) > 0 {
|
|
156
|
+
return ids, credits, true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
141
159
|
if ids, credits := readAccCatalog(c.accConfigPath, c.selectorModels()); len(ids) > 0 {
|
|
142
160
|
return ids, credits, true
|
|
143
161
|
}
|
|
@@ -172,6 +190,15 @@ func (c codebuddyCore) extendedModelCatalog(bin string) ([]string, map[string]st
|
|
|
172
190
|
// 不改变模型清单来源**:两个引擎的清单都走同一条扩展链(见 listModels),
|
|
173
191
|
// 倍率表与清单同源同链、按 model id 对上。
|
|
174
192
|
func (c codebuddyCore) modelCredits(bin string) map[string]string {
|
|
193
|
+
// ① 同 extendedModelCatalog:codebuddy-ai 用本账号 CLI 下发的配置(有模型即钉死)
|
|
194
|
+
if c.accountCatalogDir != "" {
|
|
195
|
+
if ids, credits := remoteCacheCatalogWithSelector(c.accountCatalogDir); len(ids) > 0 {
|
|
196
|
+
if len(credits) == 0 {
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
return credits
|
|
200
|
+
}
|
|
201
|
+
}
|
|
175
202
|
// ① 客户端合并配置缓存(与 extendedModelCatalog 同源同语义:**有模型即钉死**,
|
|
176
203
|
// 哪怕一条倍率都没有也不穿透 ②③ 去混别处的倍率 —— 客户端不给就是不显示)
|
|
177
204
|
if ids, credits := readAccCatalog(c.accConfigPath, c.selectorModels()); len(ids) > 0 {
|
|
@@ -238,7 +265,7 @@ func (c codebuddyCore) listModels(ctx context.Context) ([]string, error) {
|
|
|
238
265
|
// ① 客户端合并配置缓存(客户端模型选择器同源)→ ② 远程配置缓存 ∪
|
|
239
266
|
// App 包 product.json(超集近似)→ 都没有时落 ③ --help(见函数头)。
|
|
240
267
|
if ids, _, ok := c.extendedModelCatalog(bin); ok {
|
|
241
|
-
return ids, nil
|
|
268
|
+
return c.withCLICustomModels(ids), nil
|
|
242
269
|
}
|
|
243
270
|
}
|
|
244
271
|
stdout, stderr, err := runCLIEnvIn(ctx, "", c.extraEnv, bin, "--help")
|
|
@@ -249,7 +276,13 @@ func (c codebuddyCore) listModels(ctx context.Context) ([]string, error) {
|
|
|
249
276
|
if len(models) == 0 {
|
|
250
277
|
return nil, fmt.Errorf("%w: --help has no \"Currently supported\" list", ErrNoModelSource)
|
|
251
278
|
}
|
|
252
|
-
return models, nil
|
|
279
|
+
return c.withCLICustomModels(models), nil
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// withCLICustomModels 把 CLI 自己 models.json 里的本地自定义模型并入清单
|
|
283
|
+
// (并剔除已从 models.json 删除的旧条目,见 MergeCLICustomModels)。
|
|
284
|
+
func (c codebuddyCore) withCLICustomModels(ids []string) []string {
|
|
285
|
+
return MergeCLICustomModels(c.effectiveConfigDir(), ids)
|
|
253
286
|
}
|
|
254
287
|
|
|
255
288
|
// syncCustomModelEndpoints 顺带把 App 里配好的自定义模型端点补进 CLI 的 models.json。
|
|
@@ -407,32 +440,34 @@ func (c codebuddyCore) stream(ctx context.Context, req Request, onEvent func(Str
|
|
|
407
440
|
// 与 claude 完全同构、同理由(2026-10-01 改,Windows 实测):
|
|
408
441
|
// 位置参数在 Windows 上会被 .cmd 批处理拆坏,含换行时整段丢失。
|
|
409
442
|
// 常驻会话(Request.Append 非 nil)的区别只在 stdin 是个**流**。
|
|
443
|
+
// Reply 非 nil 时同样保持打开 —— 计划审批等 control_request 要写回 control_response
|
|
444
|
+
//(2026-10-25 实测:不回就整轮挂住,见 Request.Reply)。
|
|
410
445
|
prompt := FlattenPrompt(req.SystemPrompt, req.Messages, false)
|
|
411
446
|
if prompt == "" {
|
|
412
447
|
return StreamResult{}, fmt.Errorf("%s: empty prompt", c.name)
|
|
413
448
|
}
|
|
414
|
-
|
|
415
|
-
if
|
|
416
|
-
|
|
417
|
-
if perr != nil {
|
|
418
|
-
return StreamResult{}, perr
|
|
419
|
-
}
|
|
420
|
-
stdin = pipe
|
|
421
|
-
} else {
|
|
422
|
-
line, serr := streamJSONUserLine(prompt, req.Attachments)
|
|
423
|
-
if serr != nil {
|
|
424
|
-
return StreamResult{}, serr
|
|
425
|
-
}
|
|
426
|
-
stdin = strings.NewReader(line)
|
|
449
|
+
stdin, closeStdin, serr := streamJSONStdinFor(req, prompt)
|
|
450
|
+
if serr != nil {
|
|
451
|
+
return StreamResult{}, serr
|
|
427
452
|
}
|
|
453
|
+
defer closeStdin()
|
|
428
454
|
args := streamJSONArgs(c.buildArgsBase(req), prompt, true)
|
|
429
455
|
acc := &streamAccumulator{
|
|
430
456
|
Engine: c.name,
|
|
431
|
-
OnEvent: onEvent,
|
|
432
457
|
OnSessionID: req.OnSessionID,
|
|
433
458
|
AggregateOnly: true,
|
|
434
459
|
SanitizeTurnText: stripThinkBlock,
|
|
435
460
|
}
|
|
461
|
+
if onEvent != nil || req.Reply != nil {
|
|
462
|
+
acc.OnEvent = func(ev StreamEvent) {
|
|
463
|
+
if streamTerminalKind(ev.Kind) {
|
|
464
|
+
closeStdin()
|
|
465
|
+
}
|
|
466
|
+
if onEvent != nil {
|
|
467
|
+
onEvent(ev)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
436
471
|
var fin streamJSONResult
|
|
437
472
|
// 同上:stderr 尾巴是「为什么没走完协议」的唯一线索。
|
|
438
473
|
var cliStderr streamStderrBuf
|
|
@@ -536,6 +571,19 @@ func (e *CodeBuddyEngine) ModelCredits(ctx context.Context) map[string]string {
|
|
|
536
571
|
return e.core().modelCredits(bin)
|
|
537
572
|
}
|
|
538
573
|
|
|
574
|
+
// ModelVendors 实现 ModelVendorLister:各模型的供应商展示名(选择器第二行用)。
|
|
575
|
+
// 只取 CLI 自己 models.json 里 `vendor` 有意义的条目(App 写的 "Custom" 会被过滤),
|
|
576
|
+
// 拿不到返回 nil。
|
|
577
|
+
func (e *CodeBuddyEngine) ModelVendors(ctx context.Context) map[string]string {
|
|
578
|
+
return CLIModelVendors(e.core().effectiveConfigDir())
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ModelQuota 实现 ModelQuotaLister:自定义 MiniMax 端点的 token plan 额度
|
|
582
|
+
// (与 llm 下 MiniMax 模型同一口径,按 key 查;官方 minimax-* 没有用户 key,不显示)。
|
|
583
|
+
func (e *CodeBuddyEngine) ModelQuota(ctx context.Context) map[string]QuotaWindows {
|
|
584
|
+
return CLIModelQuotas(ctx, e.core().effectiveConfigDir())
|
|
585
|
+
}
|
|
586
|
+
|
|
539
587
|
// Complete 实现 Engine:单次调用 codebuddy CLI。
|
|
540
588
|
func (e *CodeBuddyEngine) Complete(ctx context.Context, req Request) (Response, error) {
|
|
541
589
|
return e.core().complete(ctx, req)
|
|
@@ -583,6 +631,7 @@ func (e *CodeBuddyAIEngine) core() codebuddyCore {
|
|
|
583
631
|
extendedModelSources: true,
|
|
584
632
|
modelCacheDirs: codebuddyAIModelCacheDirs(),
|
|
585
633
|
accConfigPath: codebuddyAIAccConfigPath(),
|
|
634
|
+
accountCatalogDir: filepath.Join(codebuddyAIDir(), "local_storage"),
|
|
586
635
|
configDir: codebuddyAIDir(),
|
|
587
636
|
}
|
|
588
637
|
}
|
|
@@ -684,12 +733,13 @@ func codebuddyModelCacheDirs() []string {
|
|
|
684
733
|
return []string{own}
|
|
685
734
|
}
|
|
686
735
|
|
|
687
|
-
// codebuddyAIDir codebuddy-ai
|
|
736
|
+
// codebuddyAIDir codebuddy-ai 引擎的专用目录(~/.codebuddy-ai)。
|
|
688
737
|
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
738
|
+
// 这个目录同时承担三件事:CLI 配置目录(settings / 会话 / 插件 / models.json)、
|
|
739
|
+
// 以及 codebuddy-ai 的 **user home**(票据落在它下面的 AppData/Local/
|
|
740
|
+
// CodeBuddyExtension/Data/Public/auth/,见 codebuddyAIHomeEnv)。
|
|
741
|
+
// 账号(authentication.id)与目录各管一层:id 决定票据文件名,homedir 决定
|
|
742
|
+
// 票据落在哪个 auth 目录 —— 只有两层都分开,两个引擎才不会共用登录态。
|
|
693
743
|
func codebuddyAIDir() string {
|
|
694
744
|
home, err := os.UserHomeDir()
|
|
695
745
|
if err != nil || home == "" {
|
|
@@ -700,17 +750,21 @@ func codebuddyAIDir() string {
|
|
|
700
750
|
|
|
701
751
|
// ── 账号隔离:一个 CLI,两个账号 ─────────────────────────────
|
|
702
752
|
//
|
|
703
|
-
// CLI 的票据路径由 authentication.id
|
|
753
|
+
// CLI 的票据路径由 homedir + authentication.id 共同决定:
|
|
704
754
|
//
|
|
705
|
-
//
|
|
755
|
+
// <os.homedir()>/AppData/Local/CodeBuddyExtension/Data/Public/auth/<id>.info
|
|
706
756
|
//
|
|
707
|
-
// 而
|
|
757
|
+
// 而 id 的取值链是:
|
|
708
758
|
//
|
|
709
759
|
// ① 合并后的产品配置 → ② ACC_PRODUCT_CONFIG_PATH 指向的文件
|
|
710
760
|
// → ③ ACC_PRODUCT_CONFIG_V3 / _V2(环境变量里的 JSON)→ ④ CLI 包内 product.json
|
|
711
761
|
//
|
|
712
|
-
//
|
|
713
|
-
// CLI
|
|
762
|
+
// 2026-09-28 实测:独立运行时 ①② 为空,③ 生效 —— 注入不同 id 就能各读各的票据。
|
|
763
|
+
// 但 CLI 2.174.0 起这条不再可靠:getAuthSavePath() 先读合并后的产品配置,
|
|
764
|
+
// 还会经 resolveCustomAuthId() 给文件名加 `-custom-<host>` 后缀,登录/刷新时
|
|
765
|
+
// 可能落回 codebuddy.info / Tencent-Cloud.coding-copilot.info(两个引擎同一账号)。
|
|
766
|
+
// 因此 2026-10-24 起 codebuddy-ai 再叠一层 **独立 homedir**(见 codebuddyAIHomeEnv),
|
|
767
|
+
// 把 auth 目录物理隔开;codebuddy 引擎维持原样(真 homedir + ~/.codebuddy)。
|
|
714
768
|
//
|
|
715
769
|
// ⚠️ ACC_PRODUCT_CONFIG_PATH 必须**清空**:父会话(WorkBuddy App / 内嵌会话)会把它
|
|
716
770
|
// 指到自己的 acc-product-config,而它优先级**高于** ③,会把账号选回 App 的默认账号。
|
|
@@ -761,9 +815,36 @@ func codebuddyDir() string {
|
|
|
761
815
|
}
|
|
762
816
|
|
|
763
817
|
// codebuddyAIExtraEnv 返回 codebuddy-ai 子进程的账号环境变量。
|
|
764
|
-
// 与 codebuddy
|
|
818
|
+
// 与 codebuddy 的差异在账号标识、配置目录,以及**独立的 user home**
|
|
819
|
+
// (见 codebuddyAIHomeEnv:票据按 homedir 落盘,必须物理隔开)。
|
|
765
820
|
func codebuddyAIExtraEnv() []string {
|
|
766
|
-
|
|
821
|
+
env := codebuddyAccountEnv(codebuddyAIDir(), codebuddyAIAuthID, codebuddyAIAuthIDEnv)
|
|
822
|
+
return append(env, codebuddyAIHomeEnv()...)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// codebuddyAIHomeEnv 把 codebuddy-ai 的 sharedData(票据目录)关进它自己的目录。
|
|
826
|
+
//
|
|
827
|
+
// 为什么光靠 ACC_PRODUCT_CONFIG_V3 不够(CLI 2.174.0 实测):
|
|
828
|
+
// CLI 的票据路径是
|
|
829
|
+
//
|
|
830
|
+
// <os.homedir()>\AppData\Local\CodeBuddyExtension\Data\Public\auth\<id>.info
|
|
831
|
+
//
|
|
832
|
+
// 而 id 由 getAuthSavePath() 决定,顺序是「合并后的产品配置 → spilled 配置 →
|
|
833
|
+
// 环境变量 V3 → 包内 product.json」,且还会经 resolveCustomAuthId() 追加
|
|
834
|
+
// `-custom-<host>` 后缀。env 只是在某些时机生效,登录/刷新时可能落回
|
|
835
|
+
// `codebuddy.info` 或 `Tencent-Cloud.coding-copilot.info`,于是两个引擎共用
|
|
836
|
+
// 同一份票据(2026-10-24 实测:auth 目录里根本没有 codebuddy-ai.info)。
|
|
837
|
+
// 把 homedir 指到 ~/.codebuddy-ai 后,两边的 auth/ 目录物理隔离,id 怎么解析
|
|
838
|
+
// 都不会撞;codebuddy 引擎保持现状(真 homedir + ~/.codebuddy,票据不动)。
|
|
839
|
+
//
|
|
840
|
+
// Windows 上 Node 的 os.homedir() 读 USERPROFILE;HOME 在本机为空,git 仍按
|
|
841
|
+
// HOMEDRIVE/HOMEPATH 找到真实用户配置,不破坏工具链。其它平台读 HOME。
|
|
842
|
+
func codebuddyAIHomeEnv() []string {
|
|
843
|
+
home := codebuddyAIDir()
|
|
844
|
+
if runtime.GOOS == "windows" {
|
|
845
|
+
return []string{"USERPROFILE=" + home}
|
|
846
|
+
}
|
|
847
|
+
return []string{"HOME=" + home}
|
|
767
848
|
}
|
|
768
849
|
|
|
769
850
|
// bin 探测 codebuddy-ai CLI 路径。
|
|
@@ -799,6 +880,16 @@ func (e *CodeBuddyAIEngine) ModelCredits(ctx context.Context) map[string]string
|
|
|
799
880
|
return e.core().modelCredits(bin)
|
|
800
881
|
}
|
|
801
882
|
|
|
883
|
+
// ModelVendors 实现 ModelVendorLister(同 CodeBuddyEngine,见上)。
|
|
884
|
+
func (e *CodeBuddyAIEngine) ModelVendors(ctx context.Context) map[string]string {
|
|
885
|
+
return CLIModelVendors(e.core().effectiveConfigDir())
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ModelQuota 实现 ModelQuotaLister(同 CodeBuddyEngine,见上)。
|
|
889
|
+
func (e *CodeBuddyAIEngine) ModelQuota(ctx context.Context) map[string]QuotaWindows {
|
|
890
|
+
return CLIModelQuotas(ctx, e.core().effectiveConfigDir())
|
|
891
|
+
}
|
|
892
|
+
|
|
802
893
|
// Complete 实现 Engine:单次调用 codebuddy-ai CLI。
|
|
803
894
|
func (e *CodeBuddyAIEngine) Complete(ctx context.Context, req Request) (Response, error) {
|
|
804
895
|
return e.core().complete(ctx, req)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
package agent
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"runtime"
|
|
4
7
|
"strings"
|
|
5
8
|
"testing"
|
|
6
9
|
)
|
|
@@ -126,3 +129,54 @@ func TestCodeBuddyLoginCommandMissingBin(t *testing.T) {
|
|
|
126
129
|
t.Fatal("CLI 不存在时应返回错误")
|
|
127
130
|
}
|
|
128
131
|
}
|
|
132
|
+
|
|
133
|
+
// codebuddy-ai 必须连 homedir 一起隔离:CLI 2.174.0 的票据目录由 os.homedir()
|
|
134
|
+
// 决定,只靠 authentication.id 已不能保证两个引擎各用各的票据(见 codebuddyAIHomeEnv)。
|
|
135
|
+
// codebuddy 主引擎保持原样 —— 不注入 HOME/USERPROFILE,仍用真实 home 和现有票据。
|
|
136
|
+
func TestCodeBuddyAIHomeIsolation(t *testing.T) {
|
|
137
|
+
t.Setenv("CODEBUDDY_CONFIG_DIR", "")
|
|
138
|
+
t.Setenv(codebuddyAIAuthIDEnv, "")
|
|
139
|
+
|
|
140
|
+
want := codebuddyAIDir()
|
|
141
|
+
ai := envMap(codebuddyAIExtraEnv())
|
|
142
|
+
if runtime.GOOS == "windows" {
|
|
143
|
+
if ai["USERPROFILE"] != want {
|
|
144
|
+
t.Errorf("USERPROFILE = %q, want %q", ai["USERPROFILE"], want)
|
|
145
|
+
}
|
|
146
|
+
if _, ok := ai["HOME"]; ok {
|
|
147
|
+
t.Errorf("Windows 上不应改写 HOME(git 依赖真实用户配置),实际 %q", ai["HOME"])
|
|
148
|
+
}
|
|
149
|
+
} else if ai["HOME"] != want {
|
|
150
|
+
t.Errorf("HOME = %q, want %q", ai["HOME"], want)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main := envMap(codebuddyAccountEnv(codebuddyDir(), codebuddyAuthID, codebuddyAuthIDEnv))
|
|
154
|
+
for _, k := range []string{"HOME", "USERPROFILE"} {
|
|
155
|
+
if v, ok := main[k]; ok {
|
|
156
|
+
t.Errorf("codebuddy 引擎不应改写 %s(实际 %q),否则会动到现有票据", k, v)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// codebuddy-ai 隔离后,清单来源改用本账号 CLI 自己下发的远程配置缓存:
|
|
162
|
+
// 按同目录选择器清单过滤具名模型、custom-local 一律保留(与 readAccCatalog 同语义)。
|
|
163
|
+
// 回归目标:桌面 App 的 acc 缓存(另一账号、只有 5 条)不再把清单钉死在 2 个 custom。
|
|
164
|
+
func TestRemoteCacheCatalogWithSelector(t *testing.T) {
|
|
165
|
+
dir := t.TempDir()
|
|
166
|
+
entry := `[{"userId":"u1","data":{` +
|
|
167
|
+
`"models":[{"id":"hy3","credits":"x0.00"},{"id":"hy3-b","credits":"x0.10"},` +
|
|
168
|
+
`{"id":"custom-local:MiniMax-M3"}],` +
|
|
169
|
+
`"agents":[{"name":"cli","models":["hy3"]}]}}]`
|
|
170
|
+
if err := os.WriteFile(filepath.Join(dir, "entry_a.info"), []byte(entry), 0o600); err != nil {
|
|
171
|
+
t.Fatal(err)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ids, credits := remoteCacheCatalogWithSelector(dir)
|
|
175
|
+
want := "hy3,custom-local:MiniMax-M3"
|
|
176
|
+
if got := strings.Join(ids, ","); got != want {
|
|
177
|
+
t.Errorf("清单 = %q, want %q(选择器外的具名模型应被过滤)", got, want)
|
|
178
|
+
}
|
|
179
|
+
if credits["hy3"] != "0.00" {
|
|
180
|
+
t.Errorf("倍率 hy3 = %q, want 0.00", credits["hy3"])
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -16,6 +16,7 @@ package agent
|
|
|
16
16
|
// 两个开关都是"多发/少发"级别的错,所以必须有断言钉住。
|
|
17
17
|
|
|
18
18
|
import (
|
|
19
|
+
"strconv"
|
|
19
20
|
"strings"
|
|
20
21
|
"testing"
|
|
21
22
|
)
|
|
@@ -260,3 +261,105 @@ func TestStripThinkBlock(t *testing.T) {
|
|
|
260
261
|
})
|
|
261
262
|
}
|
|
262
263
|
}
|
|
264
|
+
|
|
265
|
+
// TestStripToolMissingLines 「工具不存在」错误行的摘除边界。
|
|
266
|
+
func TestStripToolMissingLines(t *testing.T) {
|
|
267
|
+
const line = `Tool "AskUserQuestion" does not exist in the current tool set.`
|
|
268
|
+
cases := []struct {
|
|
269
|
+
name string
|
|
270
|
+
in string
|
|
271
|
+
want string
|
|
272
|
+
wantTools []string
|
|
273
|
+
}{
|
|
274
|
+
{"单独一行", line, "", []string{"AskUserQuestion"}},
|
|
275
|
+
{"夹在正文中间", "前半\n" + line + "\n后半", "前半\n后半", []string{"AskUserQuestion"}},
|
|
276
|
+
{"两次调用", line + line, "", []string{"AskUserQuestion", "AskUserQuestion"}},
|
|
277
|
+
{"别的工具报错不动", `Tool "Bash" is not available`, `Tool "Bash" is not available`, nil},
|
|
278
|
+
{"只有开头相同不动", `提及 Tool "x" 但没说不在工具集`, `提及 Tool "x" 但没说不在工具集`, nil},
|
|
279
|
+
{"没有关键字", "普通正文", "普通正文", nil},
|
|
280
|
+
}
|
|
281
|
+
for _, c := range cases {
|
|
282
|
+
t.Run(c.name, func(t *testing.T) {
|
|
283
|
+
got, tools := stripToolMissingLines(c.in)
|
|
284
|
+
if got != c.want {
|
|
285
|
+
t.Errorf("stripToolMissingLines(%q) = %q,want %q", c.in, got, c.want)
|
|
286
|
+
}
|
|
287
|
+
if strings.Join(tools, ",") != strings.Join(c.wantTools, ",") {
|
|
288
|
+
t.Errorf("工具名 = %v,want %v", tools, c.wantTools)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// TestToolMissingLineBecomesNotice 回归(2026-10-24 真机,codebuddy + hy3):
|
|
295
|
+
// 模型调 AskUserQuestion,CLI 把它当成**正文**推回来 ——
|
|
296
|
+
//
|
|
297
|
+
// Tool "AskUserQuestion" does not exist in the current tool set.
|
|
298
|
+
//
|
|
299
|
+
// 这句英文错误不能进正文(UI 里会夹在回答中间,回放同样脏),要改成一条
|
|
300
|
+
// KindNotice 过程提示;正文只留模型真正说的话。
|
|
301
|
+
func TestToolMissingLineBecomesNotice(t *testing.T) {
|
|
302
|
+
const errLine = `Tool "AskUserQuestion" does not exist in the current tool set.`
|
|
303
|
+
lines := []string{
|
|
304
|
+
`{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text"}}}`,
|
|
305
|
+
`{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"我需要先和你对齐。"}}}`,
|
|
306
|
+
`{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":` + strconv.Quote(errLine) + `}}}`,
|
|
307
|
+
// 新版 CLI 的流尾聚合行:同一份全文(含错误行)再给一次。
|
|
308
|
+
`{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":` +
|
|
309
|
+
strconv.Quote("我需要先和你对齐。"+errLine) + `}]}}`,
|
|
310
|
+
}
|
|
311
|
+
acc := &streamAccumulator{Engine: "codebuddy", AggregateOnly: true}
|
|
312
|
+
evs := feedNDJSON(t, acc, lines)
|
|
313
|
+
|
|
314
|
+
var joined string
|
|
315
|
+
notices := 0
|
|
316
|
+
for _, ev := range evs {
|
|
317
|
+
switch ev.Kind {
|
|
318
|
+
case KindText:
|
|
319
|
+
joined += ev.Text
|
|
320
|
+
if strings.Contains(ev.Text, "does not exist") {
|
|
321
|
+
t.Fatalf("错误行进正文了:%q", ev.Text)
|
|
322
|
+
}
|
|
323
|
+
case KindNotice:
|
|
324
|
+
notices++
|
|
325
|
+
if !strings.Contains(ev.Text, "AskUserQuestion") {
|
|
326
|
+
t.Errorf("提示文案没提工具名:%q", ev.Text)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if joined != "我需要先和你对齐。" {
|
|
331
|
+
t.Fatalf("正文 = %q,want 只留模型说的话", joined)
|
|
332
|
+
}
|
|
333
|
+
if acc.Text.String() != joined {
|
|
334
|
+
t.Fatalf("acc.Text = %q,want 与正文一致", acc.Text.String())
|
|
335
|
+
}
|
|
336
|
+
// 增量与聚合行各出现一次,提示只该有一条(toolMissingSeen 去重)。
|
|
337
|
+
if notices != 1 {
|
|
338
|
+
t.Fatalf("KindNotice 条数 = %d,want 1", notices)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// TestTurnEndStripsToolMissingLine result 行带同一句错误时也不能漏进 turn_end。
|
|
343
|
+
func TestTurnEndStripsToolMissingLine(t *testing.T) {
|
|
344
|
+
const errLine = `Tool "AskUserQuestion" does not exist in the current tool set.`
|
|
345
|
+
acc := &streamAccumulator{Engine: "codebuddy"}
|
|
346
|
+
var kinds []StreamEventKind
|
|
347
|
+
got := ""
|
|
348
|
+
acc.OnEvent = func(ev StreamEvent) {
|
|
349
|
+
kinds = append(kinds, ev.Kind)
|
|
350
|
+
if ev.Kind == KindTurnEnd {
|
|
351
|
+
got = ev.Text
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
acc.emitTurnEnd("好,"+errLine+"\n继续", "s-1")
|
|
355
|
+
if strings.Contains(got, "does not exist") {
|
|
356
|
+
t.Fatalf("turn_end 仍带错误行:%q", got)
|
|
357
|
+
}
|
|
358
|
+
if got != "好,\n继续" {
|
|
359
|
+
t.Fatalf("turn_end = %q", got)
|
|
360
|
+
}
|
|
361
|
+
// 提示要在 turn_end 之前发出去(turn_end 是轮次边界,之后不该再有事件)。
|
|
362
|
+
if len(kinds) < 2 || kinds[len(kinds)-1] != KindTurnEnd || kinds[0] != KindNotice {
|
|
363
|
+
t.Fatalf("事件顺序 = %v,want [notice … turn_end]", kinds)
|
|
364
|
+
}
|
|
365
|
+
}
|