magic-agent 1.0.3 → 1.0.49
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 +254 -8
- package/README.md +48 -5
- package/client-ui/index.html +32 -0
- package/client-ui/pc/app.css +89 -1
- package/client-ui/pc/index.html +18 -8
- package/client-ui/pc/js/00-base.js +8 -30
- package/client-ui/pc/js/05-task-list-render.js +4 -1
- package/client-ui/pc/js/07-chat-stream-render.js +2 -85
- package/client-ui/pc/js/10-markdown.js +3 -171
- package/client-ui/pc/js/18-askbox.js +2 -176
- package/client-ui/pc/js/90-glue.js +1 -1
- package/client-ui/pc/js/91-workbench-stubs.js +4 -10
- package/client-ui/pc/js/93-rail.js +10 -3
- package/client-ui/shared/conv/01-dom.js +52 -0
- package/client-ui/shared/conv/02-media-kinds.js +34 -0
- package/client-ui/shared/conv/03-chat-message.js +1561 -0
- package/client-ui/shared/conv/06-ask-answer.js +377 -0
- package/client-ui/shared/conv/07-stream-delegation.js +158 -0
- package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
- package/client-ui/shared/conv/10-markdown-render.js +252 -0
- package/client-ui/shared/conv/18-askbox-markup.js +185 -0
- package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
- package/client-ui/shared/conv/27-conv-ledger.js +381 -0
- package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
- package/client-ui/shared/conv/README.md +257 -0
- package/client-ui/shared/conv/conv.css +1638 -0
- package/client-ui/shared/conv/dom-map.md +170 -0
- package/client-ui/shared/conv/manifest.json +44 -0
- package/client-ui/shared/conversation.css +368 -190
- package/client-ui/shared/conversation.js +1722 -788
- package/client-ui/shared/host-glue.js +143 -0
- package/client-ui/shared/transport-http.js +130 -6
- package/client-ui-mobile/index.html +2417 -103
- package/cmd/magic-client/clientctl.go +273 -0
- package/cmd/magic-client/main.go +71 -5
- package/go.mod +6 -3
- package/go.sum +4 -1
- package/internal/agent/arkclaw.go +3 -2
- package/internal/agent/arkclaw_stream.go +126 -8
- package/internal/agent/arkclaw_stream_test.go +97 -15
- package/internal/agent/arkclaw_test.go +3 -3
- package/internal/agent/attachments.go +18 -5
- package/internal/agent/attachments_test.go +7 -1
- package/internal/agent/capability_test.go +11 -1
- package/internal/agent/claude.go +96 -158
- package/internal/agent/codebuddy.go +108 -133
- package/internal/agent/codebuddy_gateway.go +8 -4
- package/internal/agent/codebuddy_gateway_stream.go +82 -2
- package/internal/agent/codebuddy_gateway_test.go +236 -1
- package/internal/agent/codebuddy_stream_test.go +262 -0
- package/internal/agent/codex.go +50 -9
- package/internal/agent/codex_stream_test.go +30 -0
- package/internal/agent/codex_test.go +24 -0
- package/internal/agent/custom_models.go +109 -3
- package/internal/agent/custom_models_test.go +74 -0
- package/internal/agent/dsh.go +464 -130
- package/internal/agent/dsh_resume.go +258 -0
- package/internal/agent/dsh_sdk.go +68 -17
- package/internal/agent/dsh_sdk_test.go +102 -2
- package/internal/agent/dsh_test.go +409 -36
- package/internal/agent/engine.go +26 -6
- package/internal/agent/engine_base.go +46 -2
- package/internal/agent/engine_base_test.go +29 -0
- package/internal/agent/engine_test.go +101 -0
- package/internal/agent/env.go +69 -1
- package/internal/agent/itemtracker.go +271 -0
- package/internal/agent/launch.go +192 -0
- package/internal/agent/launch_test.go +206 -0
- package/internal/agent/launch_unix.go +15 -0
- package/internal/agent/launch_windows.go +24 -0
- package/internal/agent/limitnotice.go +103 -0
- package/internal/agent/limitnotice_test.go +211 -0
- package/internal/agent/llmengine.go +2 -2
- package/internal/agent/models.go +3 -2
- package/internal/agent/openclaw.go +4 -3
- package/internal/agent/openclaw_acp.go +7 -0
- package/internal/agent/openclaw_test.go +2 -2
- package/internal/agent/permission.go +100 -11
- package/internal/agent/permission_test.go +100 -36
- package/internal/agent/runcmd.go +13 -3
- package/internal/agent/runner.go +7 -1
- package/internal/agent/runner_test.go +1 -1
- package/internal/agent/stream.go +777 -43
- package/internal/agent/stream_test.go +3 -2
- package/internal/agent/stream_timeline_test.go +905 -0
- package/internal/agent/streamjson.go +36 -2
- package/internal/agent/toolkind.go +20 -1
- package/internal/agent/toolkind_test.go +7 -1
- package/internal/agent/tools_suffix_test.go +10 -3
- package/internal/agent/trae.go +5 -3
- package/internal/agent/workspace_test.go +52 -0
- package/internal/cli/ask.go +260 -33
- package/internal/cli/ask_test.go +15 -11
- package/internal/cli/attach_test.go +32 -20
- package/internal/cli/clientcmd.go +156 -0
- package/internal/cli/events.go +53 -8
- package/internal/cli/events_test.go +230 -0
- package/internal/cli/keepalive_test.go +101 -6
- package/internal/cli/permission_test.go +5 -1
- package/internal/cli/root.go +174 -32
- package/internal/cli/session.go +175 -28
- package/internal/cli/session_test.go +144 -5
- package/internal/cli/sessionlog.go +385 -38
- package/internal/cli/sessionlog_test.go +708 -0
- package/internal/cli/sessionlog_user_test.go +142 -1
- package/internal/cli/sessiontitle.go +141 -0
- package/internal/client/browse.go +456 -0
- package/internal/client/browse_test.go +199 -0
- package/internal/client/cors_test.go +35 -0
- package/internal/client/daemon.go +525 -0
- package/internal/client/daemon_test.go +212 -0
- package/internal/client/detach_unix.go +19 -0
- package/internal/client/detach_windows.go +27 -0
- package/internal/client/pair.go +3 -3
- package/internal/client/plugins.go +376 -0
- package/internal/client/plugins_test.go +223 -0
- package/internal/client/proxy.go +374 -0
- package/internal/client/proxy_test.go +212 -0
- package/internal/client/service.go +1464 -48
- package/internal/client/service_test.go +649 -206
- package/internal/client/session_events_test.go +130 -0
- package/internal/client/wsproxy.go +113 -0
- package/internal/client/wsproxy_test.go +102 -0
- package/internal/session/delete.go +151 -0
- package/internal/session/delete_test.go +103 -0
- package/internal/session/session.go +155 -20
- package/internal/session/session_append.go +79 -21
- package/internal/session/session_test.go +124 -10
- package/internal/session/sessionlog.go +572 -6
- package/internal/session/sessionlog_read_test.go +410 -0
- package/internal/session/title.go +486 -0
- package/internal/session/title_test.go +298 -0
- package/npm/bin/magic-agent.js +101 -10
- package/npm/bin/magic-client.js +118 -0
- 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 +36 -13
- package/npm/lib/platform.js +27 -5
- package/package.json +8 -3
- package/client-ui/pc/js/03-chat-message.js +0 -880
- package/client-ui/pc/js/06-ask-answer.js +0 -204
- /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
- /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
- /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
- /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
|
@@ -271,47 +271,10 @@ func (c codebuddyCore) complete(ctx context.Context, req Request) (Response, err
|
|
|
271
271
|
// 自定义模型才补端点(用户可能刚在 App 里配好就直接 -m 调用,没打开过选择器)。
|
|
272
272
|
c.ensureCustomModelEndpoint(req.Model)
|
|
273
273
|
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
args := c.buildArgs(req)
|
|
281
|
-
|
|
282
|
-
// workspace:codebuddy 与 claude 同族(无工作目录 flag,--add-dir 只加额外目录)
|
|
283
|
-
stdout, stderr, err := runCLIEnvIn(ctx, req.Workspace, c.extraEnv, bin, args...)
|
|
284
|
-
if err != nil {
|
|
285
|
-
return Response{}, wrapCliError(c.name, stdout, stderr, err)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
raw := strings.TrimSpace(stdout)
|
|
289
|
-
if raw == "" {
|
|
290
|
-
return Response{}, fmt.Errorf("%s CLI returned empty output", c.name)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
text := extractCodeBuddyResult(raw)
|
|
294
|
-
|
|
295
|
-
// 兜底:个别版本会把 user 请求回显(<user_query>…</user_query>)当正文。
|
|
296
|
-
if cleaned := stripUserQueryEcho(text); cleaned != text {
|
|
297
|
-
text = cleaned
|
|
298
|
-
}
|
|
299
|
-
if strings.TrimSpace(text) == "" {
|
|
300
|
-
return Response{}, fmt.Errorf("%s CLI 返回内容仅为请求回显(无模型正文)", c.name)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return Response{
|
|
304
|
-
Text: text,
|
|
305
|
-
Model: req.Model,
|
|
306
|
-
SessionID: extractCodeBuddySession(raw),
|
|
307
|
-
Latency: time.Since(start),
|
|
308
|
-
}, nil
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// completeWithAttachments 走 stream-json 输入通道跑一次带附件的调用
|
|
312
|
-
// (codebuddy 与 claude 同族协议),把事件归约成单次 Response。
|
|
313
|
-
func (c codebuddyCore) completeWithAttachments(ctx context.Context, req Request, start time.Time) (Response, error) {
|
|
314
|
-
bin := c.bin()
|
|
274
|
+
// 提示词走 stdin 的 stream-json 通道(**所有**调用,含无附件的纯文本)。
|
|
275
|
+
// 与 claude 完全同构、同理由:位置参数在 Windows 上会被 .cmd 批处理拆坏
|
|
276
|
+
// (含换行时整段丢失),详见 claude.go Complete 的注释与 launch.go。
|
|
277
|
+
// 2026-10-01 起这不再是「附件专用」路径。
|
|
315
278
|
prompt := FlattenPrompt(req.SystemPrompt, req.Messages, false)
|
|
316
279
|
if prompt == "" {
|
|
317
280
|
return Response{}, fmt.Errorf("%s: empty prompt", c.name)
|
|
@@ -321,19 +284,26 @@ func (c codebuddyCore) completeWithAttachments(ctx context.Context, req Request,
|
|
|
321
284
|
if err != nil {
|
|
322
285
|
return Response{}, err
|
|
323
286
|
}
|
|
324
|
-
acc := &streamAccumulator{
|
|
287
|
+
acc := &streamAccumulator{
|
|
288
|
+
Engine: c.name,
|
|
289
|
+
OnSessionID: req.OnSessionID,
|
|
290
|
+
SanitizeTurnText: stripThinkBlock,
|
|
291
|
+
}
|
|
325
292
|
var fin streamJSONResult
|
|
326
|
-
|
|
293
|
+
// CLI 的 stderr 尾巴留一份:非零退出与「没有 result 行」都要拿它拼可读原因
|
|
294
|
+
//(见 streamNoResultErr;不带的话只剩一句「stream ended without result line」)。
|
|
295
|
+
var cliStderr streamStderrBuf
|
|
296
|
+
seen, err := runStreamJSONIn(ctx, req.Workspace, c.extraEnv, bin, args, strings.NewReader(stdin), acc, &fin, &cliStderr)
|
|
327
297
|
if err != nil {
|
|
328
|
-
return Response{}, wrapCliError(c.name, "",
|
|
298
|
+
return Response{}, wrapCliError(c.name, "", cliStderr.String(), err)
|
|
329
299
|
}
|
|
330
300
|
if !seen {
|
|
331
|
-
return Response{},
|
|
301
|
+
return Response{}, streamNoResultErr(c.name, cliStderr.String())
|
|
332
302
|
}
|
|
333
303
|
if fin.IsError {
|
|
334
304
|
return Response{}, fmt.Errorf("%s CLI error (subtype=%s): %s", c.name, fin.Subtype, truncateStr(fin.Result, 500))
|
|
335
305
|
}
|
|
336
|
-
text := stripUserQueryEcho(finalizeStreamText(fin, acc))
|
|
306
|
+
text := stripThinkBlock(stripUserQueryEcho(finalizeStreamText(fin, acc)))
|
|
337
307
|
if strings.TrimSpace(text) == "" {
|
|
338
308
|
return Response{}, fmt.Errorf("%s CLI 返回内容仅为请求回显(无模型正文)", c.name)
|
|
339
309
|
}
|
|
@@ -345,13 +315,15 @@ func (c codebuddyCore) completeWithAttachments(ctx context.Context, req Request,
|
|
|
345
315
|
}, nil
|
|
346
316
|
}
|
|
347
317
|
|
|
348
|
-
// buildArgs 构造 codebuddy CLI
|
|
318
|
+
// buildArgs 构造 codebuddy CLI 参数(含末尾的位置参数 prompt)。
|
|
319
|
+
//
|
|
320
|
+
// ⚠️ 2026-10-01 起**生产路径已不再用它**(提示词全走 stdin),
|
|
321
|
+
// 保留是因为 buildArgsBase 的语义仍被 streamJSONArgs 依赖。
|
|
349
322
|
func (c codebuddyCore) buildArgs(req Request) []string {
|
|
350
323
|
return append(c.buildArgsBase(req), FlattenPrompt(req.SystemPrompt, req.Messages, false))
|
|
351
324
|
}
|
|
352
325
|
|
|
353
|
-
// buildArgsBase 构造参数(不含末尾的位置参数 prompt
|
|
354
|
-
// 那条路提示词走 stdin,不能再作为命令行参数传。
|
|
326
|
+
// buildArgsBase 构造参数(不含末尾的位置参数 prompt)——提示词走 stdin 的路径复用它。
|
|
355
327
|
func (c codebuddyCore) buildArgsBase(req Request) []string {
|
|
356
328
|
args := []string{"--print", "--output-format", "json"}
|
|
357
329
|
switch {
|
|
@@ -381,18 +353,25 @@ func (c codebuddyCore) buildArgsBase(req Request) []string {
|
|
|
381
353
|
// 四档权限档位:codebuddy 与 claude 同族,--permission-mode 取值完全一致
|
|
382
354
|
//(default / acceptEdits / auto / bypassPermissions),故复用同一映射。
|
|
383
355
|
// 改造前这里恒传 -y(= --dangerously-skip-permissions,第 4 档)。
|
|
384
|
-
|
|
356
|
+
// ⚠️ 2026-10-01:与 claude 同坑 —— 只传 --permission-mode 不传 --tools 时,
|
|
357
|
+
// codebuddy CLI 在 stream-json 模式下也不发 result 行;必须显式 --tools default。
|
|
358
|
+
args = append(args, "--tools", "default",
|
|
359
|
+
"--permission-mode", claudePermissionMode(req.Permission))
|
|
385
360
|
default:
|
|
386
361
|
args = append(args, "--tools", strings.Join(tools.Allowlist(), ","),
|
|
387
362
|
"--permission-mode", claudePermissionMode(req.Permission))
|
|
388
363
|
}
|
|
389
|
-
if m :=
|
|
364
|
+
if m := c.modelArg(req.Model); m != "" {
|
|
390
365
|
args = append(args, "--model", m)
|
|
391
366
|
} else if c.model != "" {
|
|
392
367
|
// 默认模型(codebuddy=hy3;codebuddy-ai 故意留空 = CLI 自身默认,
|
|
393
368
|
// 见文件头的模型注册表漂移说明);空串 = 不传 --model。
|
|
394
369
|
args = append(args, "--model", c.model)
|
|
395
370
|
}
|
|
371
|
+
// 推理档位:codebuddy 的 --effort 认 minimal/low/medium/high/xhigh/max。
|
|
372
|
+
if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
373
|
+
args = append(args, "--effort", lv)
|
|
374
|
+
}
|
|
396
375
|
// 统一参数矩阵:`--settings` 只接受一份载荷,故 MaxTokens 的 env 注入与
|
|
397
376
|
// 四档模型的 sandbox / autoMode / permissions 合并进同一个 JSON。
|
|
398
377
|
// 注意 codebuddy 的沙箱键集与 claude 略有差异(文档未列 failIfUnavailable),
|
|
@@ -414,97 +393,52 @@ func (c codebuddyCore) stream(ctx context.Context, req Request, onEvent func(Str
|
|
|
414
393
|
// 自定义模型才补端点(同 complete,见 custom_models.go)。
|
|
415
394
|
c.ensureCustomModelEndpoint(req.Model)
|
|
416
395
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if perr != nil {
|
|
425
|
-
return StreamResult{}, perr
|
|
426
|
-
}
|
|
427
|
-
stdin = pipe
|
|
428
|
-
} else {
|
|
429
|
-
line, serr := streamJSONUserLine(prompt, req.Attachments)
|
|
430
|
-
if serr != nil {
|
|
431
|
-
return StreamResult{}, serr
|
|
432
|
-
}
|
|
433
|
-
stdin = strings.NewReader(line)
|
|
434
|
-
}
|
|
435
|
-
args = streamJSONArgs(c.buildArgsBase(req), prompt, true)
|
|
436
|
-
acc := &streamAccumulator{Engine: c.name, OnEvent: onEvent, OnSessionID: req.OnSessionID}
|
|
437
|
-
var fin streamJSONResult
|
|
438
|
-
seen, err := runStreamJSONIn(ctx, req.Workspace, c.extraEnv, bin, args, stdin, acc, &fin)
|
|
439
|
-
if err != nil {
|
|
440
|
-
return StreamResult{}, err
|
|
441
|
-
}
|
|
442
|
-
if !seen {
|
|
443
|
-
return StreamResult{}, fmt.Errorf("%s CLI stream ended without result line", c.name)
|
|
444
|
-
}
|
|
445
|
-
if fin.IsError {
|
|
446
|
-
return StreamResult{}, fmt.Errorf("%s CLI error (subtype=%s): %s", c.name, fin.Subtype, truncateStr(fin.Result, 500))
|
|
447
|
-
}
|
|
448
|
-
text := stripUserQueryEcho(finalizeStreamText(fin, acc))
|
|
449
|
-
if strings.TrimSpace(text) == "" {
|
|
450
|
-
return StreamResult{}, fmt.Errorf("%s CLI 返回内容仅为请求回显(无模型正文)", c.name)
|
|
451
|
-
}
|
|
452
|
-
return StreamResult{
|
|
453
|
-
Response: Response{
|
|
454
|
-
Engine: c.name,
|
|
455
|
-
Text: text,
|
|
456
|
-
Model: fin.Model,
|
|
457
|
-
SessionID: fin.SessionID,
|
|
458
|
-
Latency: time.Since(start),
|
|
459
|
-
},
|
|
460
|
-
Thinking: acc.Thinking.String(),
|
|
461
|
-
Tools: acc.Tools,
|
|
462
|
-
}, nil
|
|
463
|
-
}
|
|
464
|
-
for i := range args {
|
|
465
|
-
if args[i] == "--output-format" {
|
|
466
|
-
args[i+1] = "stream-json"
|
|
467
|
-
break
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
args = append(args, "--include-partial-messages", "--verbose")
|
|
471
|
-
|
|
472
|
-
acc := &streamAccumulator{Engine: c.name, OnEvent: onEvent}
|
|
473
|
-
var fin struct {
|
|
474
|
-
Type string `json:"type"`
|
|
475
|
-
Subtype string `json:"subtype"`
|
|
476
|
-
IsError bool `json:"is_error"`
|
|
477
|
-
Result string `json:"result"`
|
|
478
|
-
SessionID string `json:"session_id"`
|
|
479
|
-
Model string `json:"model"`
|
|
396
|
+
// 提示词一律走 stdin 的 stream-json 输入通道(不再当位置参数)。
|
|
397
|
+
// 与 claude 完全同构、同理由(2026-10-01 改,Windows 实测):
|
|
398
|
+
// 位置参数在 Windows 上会被 .cmd 批处理拆坏,含换行时整段丢失。
|
|
399
|
+
// 常驻会话(Request.Append 非 nil)的区别只在 stdin 是个**流**。
|
|
400
|
+
prompt := FlattenPrompt(req.SystemPrompt, req.Messages, false)
|
|
401
|
+
if prompt == "" {
|
|
402
|
+
return StreamResult{}, fmt.Errorf("%s: empty prompt", c.name)
|
|
480
403
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
isResult, perr := acc.handleNDJSONLine(line)
|
|
404
|
+
var stdin io.Reader
|
|
405
|
+
if req.Append != nil {
|
|
406
|
+
pipe, perr := streamJSONInputPipe(prompt, req.Attachments, req.Append)
|
|
485
407
|
if perr != nil {
|
|
486
|
-
return perr
|
|
408
|
+
return StreamResult{}, perr
|
|
487
409
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
410
|
+
stdin = pipe
|
|
411
|
+
} else {
|
|
412
|
+
line, serr := streamJSONUserLine(prompt, req.Attachments)
|
|
413
|
+
if serr != nil {
|
|
414
|
+
return StreamResult{}, serr
|
|
491
415
|
}
|
|
492
|
-
|
|
493
|
-
}
|
|
416
|
+
stdin = strings.NewReader(line)
|
|
417
|
+
}
|
|
418
|
+
args := streamJSONArgs(c.buildArgsBase(req), prompt, true)
|
|
419
|
+
acc := &streamAccumulator{
|
|
420
|
+
Engine: c.name,
|
|
421
|
+
OnEvent: onEvent,
|
|
422
|
+
OnSessionID: req.OnSessionID,
|
|
423
|
+
AggregateOnly: true,
|
|
424
|
+
SanitizeTurnText: stripThinkBlock,
|
|
425
|
+
}
|
|
426
|
+
var fin streamJSONResult
|
|
427
|
+
// 同上:stderr 尾巴是「为什么没走完协议」的唯一线索。
|
|
428
|
+
var cliStderr streamStderrBuf
|
|
429
|
+
seen, err := runStreamJSONIn(ctx, req.Workspace, c.extraEnv, bin, args, stdin, acc, &fin, &cliStderr)
|
|
494
430
|
if err != nil {
|
|
495
431
|
return StreamResult{}, err
|
|
496
432
|
}
|
|
497
|
-
if !
|
|
498
|
-
|
|
433
|
+
if !seen {
|
|
434
|
+
err := streamNoResultErr(c.name, cliStderr.String())
|
|
435
|
+
acc.emitTurnFailed(acc.SessionID, err)
|
|
436
|
+
return StreamResult{}, err
|
|
499
437
|
}
|
|
500
438
|
if fin.IsError {
|
|
501
439
|
return StreamResult{}, fmt.Errorf("%s CLI error (subtype=%s): %s", c.name, fin.Subtype, truncateStr(fin.Result, 500))
|
|
502
440
|
}
|
|
503
|
-
|
|
504
|
-
text := stripUserQueryEcho(fin.Result)
|
|
505
|
-
if strings.TrimSpace(text) == "" {
|
|
506
|
-
text = strings.TrimSpace(acc.Text.String())
|
|
507
|
-
}
|
|
441
|
+
text := stripThinkBlock(stripUserQueryEcho(finalizeStreamText(fin, acc)))
|
|
508
442
|
if strings.TrimSpace(text) == "" {
|
|
509
443
|
return StreamResult{}, fmt.Errorf("%s CLI 返回内容仅为请求回显(无模型正文)", c.name)
|
|
510
444
|
}
|
|
@@ -533,8 +467,8 @@ type CodeBuddyEngine struct {
|
|
|
533
467
|
// Name 实现 Engine。
|
|
534
468
|
func (e *CodeBuddyEngine) Name() string { return "codebuddy" }
|
|
535
469
|
|
|
536
|
-
// DefaultCodeBuddyTimeout 单次尝试默认超时(codebuddy / codebuddy-ai
|
|
537
|
-
const DefaultCodeBuddyTimeout =
|
|
470
|
+
// DefaultCodeBuddyTimeout 单次尝试默认超时(codebuddy / codebuddy-ai 共用;2026-10-10 由 5 分钟统一上调到 1 小时)。
|
|
471
|
+
const DefaultCodeBuddyTimeout = time.Hour
|
|
538
472
|
|
|
539
473
|
// DefaultCodeBuddyModel codebuddy 引擎的默认模型。
|
|
540
474
|
// 未显式 -m 指定时使用 hy3(与 magic-video 的 DefaultCreativeModel 一致)。
|
|
@@ -873,6 +807,47 @@ func (e *CodeBuddyAIEngine) buildArgsBase(req Request) []string { return e.core(
|
|
|
873
807
|
|
|
874
808
|
// ── 输出解析(两个后端共用)──────────────────────────────────
|
|
875
809
|
|
|
810
|
+
// stripThinkBlock 剥离整段 <think>…</think> 思维链块。
|
|
811
|
+
//
|
|
812
|
+
// 为什么需要(2026-10-01 实测):codebuddy 在 --tools off + 「直接输出纯文本
|
|
813
|
+
// markdown」的约束下,仍会把推理写进 result 字段,形如
|
|
814
|
+
//
|
|
815
|
+
// <think>The user is asking me to…</think>\n\n1
|
|
816
|
+
//
|
|
817
|
+
// 也就是**推理与正文同处一个字符串**。不剥掉的话:
|
|
818
|
+
// - `-o text` 的正文里混着英文推理,用户看到的是一段莫名其妙的话;
|
|
819
|
+
// - 更糟的是 --tools off 注入的约束里有「第一行必须是答案正文」,
|
|
820
|
+
// 模型为遵守它而把答案挪到 think 块之后,客户端按行渲染会全乱。
|
|
821
|
+
//
|
|
822
|
+
// 与 stripUserQueryEcho 的关系:两者都是「剥掉不该当正文的前缀」,
|
|
823
|
+
// 串联使用(先剥回显、再剥思维链)。llm 引擎的 thinkSplitter 是**流式**版本
|
|
824
|
+
// (逐行喂、边喂边分流),这里是整段版本,用于 result 行一次性给全的场景。
|
|
825
|
+
//
|
|
826
|
+
// 找不到闭合标签时原样返回(宁可把标签露出来,也不要吞掉真内容)。
|
|
827
|
+
//
|
|
828
|
+
// ⚠️ 必须**循环**剥而不是只剥第一段:codebuddy 在多轮工具调用后会把每一轮的
|
|
829
|
+
// 推理各写一个 think 块(<think>a</think>正文<think>b</think>正文…)。
|
|
830
|
+
// 只剥第一段的话,第二段推理会原样漏进用户看到的正文 —— 症状是
|
|
831
|
+
// 「回答中间突然冒出一段英文」,比整段漏出来更难定位。
|
|
832
|
+
func stripThinkBlock(s string) string {
|
|
833
|
+
const (
|
|
834
|
+
open = "<think>"
|
|
835
|
+
close = "</think>"
|
|
836
|
+
)
|
|
837
|
+
for {
|
|
838
|
+
i := strings.Index(s, open)
|
|
839
|
+
if i < 0 {
|
|
840
|
+
return s
|
|
841
|
+
}
|
|
842
|
+
j := strings.Index(s[i:], close)
|
|
843
|
+
if j < 0 {
|
|
844
|
+
// 未闭合 → 整段原样(不吞真内容)
|
|
845
|
+
return s
|
|
846
|
+
}
|
|
847
|
+
s = strings.TrimSpace(s[i+j+len(close):])
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
876
851
|
// stripUserQueryEcho 剥离 <user_query>…</user_query> 请求回显。
|
|
877
852
|
func stripUserQueryEcho(s string) string {
|
|
878
853
|
t := strings.TrimSpace(s)
|
|
@@ -79,10 +79,10 @@ import (
|
|
|
79
79
|
|
|
80
80
|
// DefaultCBGatewayTimeout 单次调用的默认超时。
|
|
81
81
|
//
|
|
82
|
-
//
|
|
83
|
-
// DEFAULT_RUN_TIMEOUT_MS
|
|
84
|
-
//
|
|
85
|
-
const DefaultCBGatewayTimeout =
|
|
82
|
+
// 网关侧的 runTimeoutMs 默认就是 1_800_000(30 分钟,实测值来自网关内核
|
|
83
|
+
// DEFAULT_RUN_TIMEOUT_MS),本引擎默认取 1 小时与 CLI 的 -t 默认一致(2026-10-10 由 10 分钟
|
|
84
|
+
// 上调),避免「CLI 说 1h、引擎内部却更短」这种自相矛盾。要跑更久的任务显式给 -t。
|
|
85
|
+
const DefaultCBGatewayTimeout = time.Hour
|
|
86
86
|
|
|
87
87
|
// cbGatewayMaxBody 单次响应体读取上限(webhook 受理响应很小,留足余量即可)。
|
|
88
88
|
const cbGatewayMaxBody = 1 << 20 // 1 MiB
|
|
@@ -337,6 +337,10 @@ func (e *CodeBuddyGatewayEngine) run(ctx context.Context, req Request, onEvent f
|
|
|
337
337
|
sessionID: convID,
|
|
338
338
|
}
|
|
339
339
|
if err := e.consumeStream(httpCtx, client, r, runID, st, req.JSONSchema); err != nil {
|
|
340
|
+
// 流被截断 / 任务 failed / HTTP 拒绝都走这一条:已发出的正文 item
|
|
341
|
+
// 必须收成 failed,不能让它永远停在 running(消费方只能靠超时猜,
|
|
342
|
+
// 历史回放里那一条会永远显示「生成中」)。语义见 cbGatewayStreamState.fail。
|
|
343
|
+
st.fail(err)
|
|
340
344
|
return st.result(e, req, start), err
|
|
341
345
|
}
|
|
342
346
|
res := st.result(e, req, start)
|
|
@@ -115,6 +115,21 @@ type cbGatewayStreamState struct {
|
|
|
115
115
|
status string // 最近一次 status(错误信息用)
|
|
116
116
|
sessionID string // 会话锚点(网关回 sessionId 时以它为准)
|
|
117
117
|
tools []ToolCall
|
|
118
|
+
|
|
119
|
+
// tracker 状态收敛的身份与版本(见 itemtracker.go)。
|
|
120
|
+
// 原生 id = 网关分配的 runId(一个 run 恒定,见 codebuddy_gateway.go 的 deliver)。
|
|
121
|
+
textTrack *itemTracker
|
|
122
|
+
// textTrack 是否已经发过事件(发过就不能再换 item id)。
|
|
123
|
+
textStarted bool
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// trackers 惰性建立跟踪器。
|
|
127
|
+
func (st *cbGatewayStreamState) trackers() *itemTracker {
|
|
128
|
+
if st.textTrack == nil {
|
|
129
|
+
st.textTrack = newItemTracker(cbGatewayEngineName, laneText)
|
|
130
|
+
st.textTrack.Begin(st.runID)
|
|
131
|
+
}
|
|
132
|
+
return st.textTrack
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
// Stream 实现 Streamer:走 webhook 投递 + SSE 收流(见文件头)。
|
|
@@ -241,6 +256,8 @@ func (st *cbGatewayStreamState) handleFrame(raw []byte, schema *JSONSchema) (boo
|
|
|
241
256
|
st.final = text
|
|
242
257
|
st.completed = true
|
|
243
258
|
st.emitText(text)
|
|
259
|
+
// completed = 轮次终点:发 turn_end 把 item 收成 final(见 finish 的注释)。
|
|
260
|
+
st.finish(firstNonEmpty(st.convID, st.sessionID))
|
|
244
261
|
return true, nil
|
|
245
262
|
|
|
246
263
|
case "streaming":
|
|
@@ -279,9 +296,72 @@ func (st *cbGatewayStreamState) emitText(full string) {
|
|
|
279
296
|
return
|
|
280
297
|
}
|
|
281
298
|
st.emitted.WriteString(full)
|
|
282
|
-
if st.onEvent
|
|
283
|
-
|
|
299
|
+
if st.onEvent == nil {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
// ⚠️ snapshot 直接取 st.emitted(累积到此刻的全文),**不能**喂给
|
|
303
|
+
// tracker 的 Append —— Append 的语义是「把这段**增量**拼到已有 snapshot
|
|
304
|
+
// 后面」,而这里传进去的是「累积到此刻的全文」,两者混用会逐帧翻倍
|
|
305
|
+
// (第一帧"你好"、第二帧"你好世界",累加后得到"你好你好世界")。
|
|
306
|
+
//
|
|
307
|
+
// 正确分工见 itemtracker.go 文件头:tracker 管 id / revision / status,
|
|
308
|
+
// 内容累积由调用方独家负责。nextWith 就是为此准备的入口。
|
|
309
|
+
ev := StreamEvent{Kind: KindText, Text: full}
|
|
310
|
+
st.trackers().nextWith(StatusRunning, st.emitted.String()).apply(&ev)
|
|
311
|
+
st.textStarted = true
|
|
312
|
+
st.onEvent(ev)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// fail 异常收尾(status=failed):流被截断 / 任务 failed / HTTP 拒绝 / 读出错。
|
|
316
|
+
//
|
|
317
|
+
// 与 finish 的分工:turn_end = 网关给了 completed 帧(成功收尾),
|
|
318
|
+
// turn_failed = 没有(异常收尾),带 Error / Reason。
|
|
319
|
+
// 挂到**已有**正文 item 上:partial 的内容仍然存在,只是这条 item 不再增长。
|
|
320
|
+
// 从没建立过 item 时直接返回 —— 不该为「一句正文都没流出来」的轮次硬造 item。
|
|
321
|
+
//
|
|
322
|
+
// 只有一个调用点(run 里的 consumeStream 错误分支):它已经覆盖了
|
|
323
|
+
// handleFrame 报错、读流中断、无终帧、非 SSE 缺终帧等全部异常出口,
|
|
324
|
+
// 在这里收口比在每个 return err 处各写一遍可靠。
|
|
325
|
+
func (st *cbGatewayStreamState) fail(err error) {
|
|
326
|
+
if st.onEvent == nil || err == nil || !st.textStarted {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
ev := StreamEvent{
|
|
330
|
+
Kind: KindTurnFailed,
|
|
331
|
+
SessionID: firstNonEmpty(st.convID, st.sessionID),
|
|
332
|
+
Error: err.Error(),
|
|
333
|
+
Reason: ReasonOf(err),
|
|
334
|
+
Status: StatusFailed,
|
|
335
|
+
}
|
|
336
|
+
if attrs, ok := st.trackers().Failed(st.emitted.String()); ok {
|
|
337
|
+
attrs.apply(&ev)
|
|
338
|
+
}
|
|
339
|
+
st.onEvent(ev)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// finish 收尾:把 item 标成 final(completed 帧已到,正文不会再变)。
|
|
343
|
+
//
|
|
344
|
+
// 发的是 **KindTurnEnd** 而不是又一条 KindText,原因有三个:
|
|
345
|
+
// 1. 语义对:completed 帧是**轮次边界**,不是「又来了一段正文」。
|
|
346
|
+
// 之前试过补一条空 Text 的 KindText 帧,能过测试但语义是错的 ——
|
|
347
|
+
// 消费方按 Text 拼正文的实现会收到一个空增量。
|
|
348
|
+
// 2. 不破坏既有收集器:现有测试与调用方都按「KindText 就是正文增量」收集,
|
|
349
|
+
// 多一条空帧会让它们数错条数。
|
|
350
|
+
// 3. 轮次边界有独立信号后,思考 lane(将来若有)也能靠它收尾 ——
|
|
351
|
+
// 与 claude / dsh 引擎的处理方式一致。
|
|
352
|
+
//
|
|
353
|
+
// ⚠️ keep-alive 的空闲收工挂在 KindTurnEnd 上(见 keepAlive 的调用点),
|
|
354
|
+
// 但 codebuddy-gateway 不在 AppendSupportOf 名单里,不会启用常驻会话,
|
|
355
|
+
// 所以这里发 turn_end 是安全的。
|
|
356
|
+
func (st *cbGatewayStreamState) finish(sessionID string) {
|
|
357
|
+
if st.onEvent == nil || !st.textStarted {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
ev := StreamEvent{Kind: KindTurnEnd, Text: st.final, SessionID: sessionID}
|
|
361
|
+
if attrs, ok := st.trackers().Finalize(st.emitted.String()); ok {
|
|
362
|
+
attrs.apply(&ev)
|
|
284
363
|
}
|
|
364
|
+
st.onEvent(ev)
|
|
285
365
|
}
|
|
286
366
|
|
|
287
367
|
// result 把累积状态收成 StreamResult(终态与错误路径共用)。
|