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
|
@@ -92,7 +92,7 @@ func shortSessionsDir(t *testing.T) string {
|
|
|
92
92
|
|
|
93
93
|
func TestKeepAliveRequiresStream(t *testing.T) {
|
|
94
94
|
kaEngineForTest(t)
|
|
95
|
-
_, _, err := runAskCmd(t, "", "-e", "codebuddy", "--keep-alive", "-p", "hi")
|
|
95
|
+
_, _, err := runAskCmd(t, "", "--save-registry", "-e", "codebuddy", "--keep-alive", "-p", "hi")
|
|
96
96
|
if err == nil {
|
|
97
97
|
t.Fatal("--keep-alive 不配 --stream 应报错")
|
|
98
98
|
}
|
|
@@ -143,7 +143,7 @@ func TestKeepAliveIsDefaultForStreaming(t *testing.T) {
|
|
|
143
143
|
|
|
144
144
|
done := make(chan error, 1)
|
|
145
145
|
go func() {
|
|
146
|
-
_, _, err := runAskCmd(t, "", "-e", "codebuddy", "--stream", "--idle", "1s", "-p", "首轮")
|
|
146
|
+
_, _, err := runAskCmd(t, "", "--save-registry", "-e", "codebuddy", "--stream", "--idle", "1s", "-p", "首轮")
|
|
147
147
|
done <- err
|
|
148
148
|
}()
|
|
149
149
|
|
|
@@ -183,7 +183,7 @@ func TestKeepAliveCanBeDisabled(t *testing.T) {
|
|
|
183
183
|
t.Setenv(session.EnvDir, dir)
|
|
184
184
|
registerFake(&streamingEngine{name: "fake-ka-off", text: "ok"})
|
|
185
185
|
|
|
186
|
-
if _, _, err := runAskCmd(t, "", "-e", "fake-ka-off", "--stream", "--keep-alive=false", "hi"); err != nil {
|
|
186
|
+
if _, _, err := runAskCmd(t, "", "--save-registry", "-e", "fake-ka-off", "--stream", "--keep-alive=false", "hi"); err != nil {
|
|
187
187
|
t.Fatalf("ask: %v", err)
|
|
188
188
|
}
|
|
189
189
|
recs := mustListSessions(t, dir)
|
|
@@ -201,7 +201,7 @@ func TestKeepAliveNotAppliedToNonStream(t *testing.T) {
|
|
|
201
201
|
t.Setenv(session.EnvDir, dir)
|
|
202
202
|
registerFake(&stringEngine{name: "fake-ka-nostream", text: "ok"})
|
|
203
203
|
|
|
204
|
-
if _, _, err := runAskCmd(t, "", "-e", "fake-ka-nostream", "hi"); err != nil {
|
|
204
|
+
if _, _, err := runAskCmd(t, "", "--save-registry", "-e", "fake-ka-nostream", "hi"); err != nil {
|
|
205
205
|
t.Fatalf("ask: %v", err)
|
|
206
206
|
}
|
|
207
207
|
recs := mustListSessions(t, dir)
|
|
@@ -255,7 +255,7 @@ func TestAppendToNonKeepAliveSession(t *testing.T) {
|
|
|
255
255
|
t.Setenv(session.EnvDir, shortSessionsDir(t))
|
|
256
256
|
registerFake(&stringEngine{name: "fake-ka-none", text: "ok"})
|
|
257
257
|
|
|
258
|
-
if _, _, err := runAskCmd(t, "", "-e", "fake-ka-none", "普通调用"); err != nil {
|
|
258
|
+
if _, _, err := runAskCmd(t, "", "--save-registry", "-e", "fake-ka-none", "普通调用"); err != nil {
|
|
259
259
|
t.Fatalf("ask: %v", err)
|
|
260
260
|
}
|
|
261
261
|
rows := parseSessions(t, mustSessions(t))
|
|
@@ -283,7 +283,7 @@ func TestKeepAliveAppendEndToEnd(t *testing.T) {
|
|
|
283
283
|
// ① 起常驻会话(后台跑,空闲 1s 自动收工)
|
|
284
284
|
done := make(chan error, 1)
|
|
285
285
|
go func() {
|
|
286
|
-
_, _, err := runAskCmd(t, "", "-e", "codebuddy", "--stream", "--keep-alive",
|
|
286
|
+
_, _, err := runAskCmd(t, "", "--save-registry", "-e", "codebuddy", "--stream", "--keep-alive",
|
|
287
287
|
"--idle", "1s", "-p", "首轮任务")
|
|
288
288
|
done <- err
|
|
289
289
|
}()
|
|
@@ -350,6 +350,101 @@ func TestKeepAliveAppendEndToEnd(t *testing.T) {
|
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// TestKeepAliveAppendWithoutRegistry 默认不留档(不开 --save-registry)时,
|
|
354
|
+
// --append 仍要能用 —— 走的是「按约定找 socket」那条路(session.AppendSocketPath)。
|
|
355
|
+
//
|
|
356
|
+
// 为什么这一条重要:常驻会话对 claude/codebuddy 是**默认开**的,而登记表默认不落。
|
|
357
|
+
// 若 --append 只认登记表,用户会撞上「默认能常驻、却追加不了」这个自相矛盾的行为。
|
|
358
|
+
// 代价是这条路径只认 run_id(session_id 要靠记录映射,没有记录就无从查起)。
|
|
359
|
+
func TestKeepAliveAppendWithoutRegistry(t *testing.T) {
|
|
360
|
+
dir := shortSessionsDir(t)
|
|
361
|
+
t.Setenv(session.EnvDir, dir)
|
|
362
|
+
|
|
363
|
+
eng := kaEngineForTest(t)
|
|
364
|
+
|
|
365
|
+
// ① 起常驻会话(**不开** --save-registry)
|
|
366
|
+
done := make(chan error, 1)
|
|
367
|
+
go func() {
|
|
368
|
+
_, _, err := runAskCmd(t, "", "-e", "codebuddy", "--stream", "--keep-alive",
|
|
369
|
+
"--idle", "5s", "-p", "首轮任务")
|
|
370
|
+
done <- err
|
|
371
|
+
}()
|
|
372
|
+
|
|
373
|
+
// ② 登记表里什么都没有(这正是前提),只能等 socket 文件出现
|
|
374
|
+
if recs := mustListSessions(t, dir); len(recs) != 0 {
|
|
375
|
+
t.Fatalf("默认不该有登记记录,实际 %+v", recs)
|
|
376
|
+
}
|
|
377
|
+
var runID string
|
|
378
|
+
deadline := time.Now().Add(10 * time.Second)
|
|
379
|
+
for time.Now().Before(deadline) && runID == "" {
|
|
380
|
+
ents, _ := os.ReadDir(dir)
|
|
381
|
+
for _, e := range ents {
|
|
382
|
+
if strings.HasSuffix(e.Name(), ".sock") {
|
|
383
|
+
runID = strings.TrimSuffix(e.Name(), ".sock")
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if runID == "" {
|
|
387
|
+
time.Sleep(50 * time.Millisecond)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if runID == "" {
|
|
391
|
+
t.Fatal("常驻会话没开出追加入口 socket")
|
|
392
|
+
}
|
|
393
|
+
// 按约定推出来的路径必须与实际建出来的同一个(两端算法唯一的交叉点)
|
|
394
|
+
if got, want := session.AppendSocketPath(runID), filepath.Join(dir, runID+".sock"); got != want {
|
|
395
|
+
t.Fatalf("AppendSocketPath = %q want %q", got, want)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ③ 按 run_id 追加(记录不存在的路径)
|
|
399
|
+
stdout, _, err := runAskCmd(t, "", "--append", runID, "-p", "不留档也能追加")
|
|
400
|
+
if err != nil {
|
|
401
|
+
t.Fatalf("--append(无登记记录): %v", err)
|
|
402
|
+
}
|
|
403
|
+
var out struct {
|
|
404
|
+
Type string `json:"type"`
|
|
405
|
+
Queued bool `json:"queued"`
|
|
406
|
+
}
|
|
407
|
+
if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &out); err != nil {
|
|
408
|
+
t.Fatalf("--append 输出不是 JSON: %v (%s)", err, stdout)
|
|
409
|
+
}
|
|
410
|
+
if out.Type != "append" || !out.Queued {
|
|
411
|
+
t.Errorf("--append 结果不对: %+v", out)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ④ 引擎确实收到了;收工后 socket 照旧清掉
|
|
415
|
+
if got := waitAppend(t, eng, "不留档也能追加", 10*time.Second); got != "不留档也能追加" {
|
|
416
|
+
t.Errorf("引擎收到的追加 = %q", got)
|
|
417
|
+
}
|
|
418
|
+
// ⑤ --stop 依赖登记表(pid 只存在那里),所以不留档的会话**停不掉** ——
|
|
419
|
+
// 这是「默认不留痕」的已知代价,明写出来免得当成 bug 报回来。
|
|
420
|
+
if _, _, err := runAskCmd(t, "", "--stop", runID); err == nil {
|
|
421
|
+
t.Error("--stop 对不留档的会话应当报「找不到」(它靠登记表寻址)")
|
|
422
|
+
}
|
|
423
|
+
select {
|
|
424
|
+
case err := <-done:
|
|
425
|
+
if err != nil {
|
|
426
|
+
t.Fatalf("常驻会话收尾报错: %v", err)
|
|
427
|
+
}
|
|
428
|
+
case <-time.After(20 * time.Second):
|
|
429
|
+
t.Fatal("常驻会话没收工")
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// waitAppend 等引擎收到追加内容(追加是异步投递的,socket 收到 ≠ 引擎已读)。
|
|
434
|
+
func waitAppend(t *testing.T, eng *kaEngine, want string, d time.Duration) string {
|
|
435
|
+
t.Helper()
|
|
436
|
+
deadline := time.Now().Add(d)
|
|
437
|
+
for time.Now().Before(deadline) {
|
|
438
|
+
for _, got := range eng.received() {
|
|
439
|
+
if got == want {
|
|
440
|
+
return got
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
time.Sleep(50 * time.Millisecond)
|
|
444
|
+
}
|
|
445
|
+
return ""
|
|
446
|
+
}
|
|
447
|
+
|
|
353
448
|
// mustListSessions 直接读登记表(比 --sessions 的 JSON 更直接,且能看到 append 字段)。
|
|
354
449
|
func mustListSessions(t *testing.T, dir string) []session.Record {
|
|
355
450
|
t.Helper()
|
|
@@ -139,7 +139,11 @@ func TestEnginesReportsPermission(t *testing.T) {
|
|
|
139
139
|
if seen["codebuddy-ai"] != "flag:--permission-mode" {
|
|
140
140
|
t.Errorf("codebuddy-ai 的 permission 字段 = %q, want flag:--permission-mode", seen["codebuddy-ai"])
|
|
141
141
|
}
|
|
142
|
-
|
|
142
|
+
// dsh 的档位不走 flag,走子进程环境变量 —— 能力串是 env:<VAR>,客户端据此放行。
|
|
143
|
+
if seen["dsh"] != "env:DSH_PERMISSION_MODE" {
|
|
144
|
+
t.Errorf("dsh 的 permission 字段 = %q, want env:DSH_PERMISSION_MODE", seen["dsh"])
|
|
145
|
+
}
|
|
146
|
+
for _, e := range []string{"trae", "llm", "codex", "openclaw", "arkclaw"} {
|
|
143
147
|
if v, ok := seen[e]; !ok {
|
|
144
148
|
t.Errorf("--engines 缺 %s 行", e)
|
|
145
149
|
} else if v != "none" {
|
package/internal/cli/root.go
CHANGED
|
@@ -17,7 +17,7 @@ package cli
|
|
|
17
17
|
// -f, --file <path> 从文件读 prompt(- 读 stdin)
|
|
18
18
|
// -a, --attach <path> 附件(截图/图片),可重复/逗号分隔;与提示词一起发给引擎
|
|
19
19
|
// -w, --workspace <dir> 工作目录:在该目录里执行引擎(详见下方「工作目录」)
|
|
20
|
-
// -t, --timeout <dur> 单次尝试超时(如 3m;默认
|
|
20
|
+
// -t, --timeout <dur> 单次尝试超时(如 3m;默认 1h)
|
|
21
21
|
// -r, --retries <n> 失败重试次数(默认 0)
|
|
22
22
|
// --backoff <dur> 首次重试退避(默认 2s,指数翻倍,上限 30s)
|
|
23
23
|
// -o, --output <format> 输出:json(默认)| text
|
|
@@ -34,10 +34,19 @@ package cli
|
|
|
34
34
|
// --engines 列出引擎、可用性与支持的模型(JSON 数组;--json 兼容保留)
|
|
35
35
|
// --no-models 配合 --engines:跳过模型探测(只列引擎,快)
|
|
36
36
|
// --stop <id> 停止指定会话/运行(session_id 或 run_id),杀掉它的引擎进程组
|
|
37
|
-
// --
|
|
37
|
+
// --delete-session <id> 删除一条会话:登记记录 + 事件历史 + 标题覆盖一起删,**全机生效**
|
|
38
|
+
// (各端读同一份登记表,一端删了处处都看不到)。幂等
|
|
39
|
+
// --sessions 列出会话登记表(JSON 数组:run_id / session_id / pid / engine /
|
|
40
|
+
// source / client / state;全机视图,不按 client 过滤)。
|
|
41
|
+
// ⚠️ 只见过**已存档**的会话(--save-registry,默认关)
|
|
42
|
+
// --save-registry 把会话登记落盘(<run_id>.json),让 --sessions / --stop 事后能按 id
|
|
43
|
+
// 寻址。**默认关**:不留痕;不留档的会话停不掉
|
|
38
44
|
// --keep-alive 常驻会话(claude/codebuddy 默认开,dsh 需显式传;需配合 --stream):
|
|
39
45
|
// 首轮结束后不退出、等 --append 追加;--keep-alive=false 关闭
|
|
40
46
|
// --append <id> 向常驻会话追加一条消息(内容用 -p/位置参数给)
|
|
47
|
+
// --session-log <id> 读一段会话的事件历史(--after <seq> 可增量续读)
|
|
48
|
+
// --save-history 把流式会话的事件落盘存档(<session_id>.jsonl,供 --session-log
|
|
49
|
+
// 与客户端回放历史)。**默认关**:一次性调用不留痕
|
|
41
50
|
// --idle <dur> 常驻会话空闲收工时长(默认 5m;0 = 本轮结束就收工)
|
|
42
51
|
// -v, --verbose 重试过程打到 stderr
|
|
43
52
|
//
|
|
@@ -64,6 +73,7 @@ import (
|
|
|
64
73
|
"os"
|
|
65
74
|
"strings"
|
|
66
75
|
"sync"
|
|
76
|
+
"time"
|
|
67
77
|
|
|
68
78
|
"github.com/spf13/cobra"
|
|
69
79
|
|
|
@@ -179,8 +189,9 @@ llm 引擎(包装 simonw/LLM CLI,模型/密钥由它自管):
|
|
|
179
189
|
codex codex debug models
|
|
180
190
|
openclaw openclaw models list --json
|
|
181
191
|
arkclaw 无清单(模型由网关按 claw_id 绑定)→ 输出 models_note 说明
|
|
182
|
-
dsh
|
|
183
|
-
|
|
192
|
+
dsh 配置文件里已配置的模型(0.1.7+ 读 profile 的 cordis.patch.yml,
|
|
193
|
+
更早读 $DSH_HOME/settings.yaml;输出 route/model);
|
|
194
|
+
headless 无 --model,换模型要改配置
|
|
184
195
|
探测会真的启动这些 CLI 的只读子命令(最慢 openclaw ~2.5s),故并发执行、
|
|
185
196
|
单引擎上限 30s;拿不到清单不报错,原因写进该行的 models_note。
|
|
186
197
|
--no-models 整段跳过探测(只列引擎与可用性,不启动任何 CLI)。
|
|
@@ -232,19 +243,20 @@ dsh 引擎(DeepSeek Harness,本机 dsh CLI):
|
|
|
232
243
|
说明:
|
|
233
244
|
· 模型:SDK 通道下 -m 生效(转成 initialize 的 provider/model,取 route/model 形态,
|
|
234
245
|
可用值见 --engines 的 models 字段);回退 headless 后 -m 不生效(无 --model 参数),
|
|
235
|
-
模型由 dsh
|
|
236
|
-
llm-pi-ai.providers
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
magic-agent -e dsh --stream --keep-alive "记住 42"
|
|
244
|
-
magic-agent --append <run_id|session_id> -p "
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
模型由 dsh 自己的配置决定(0.1.7+ 在 $DSH_HOME/profiles/<profile>/cordis.patch.yml
|
|
247
|
+
的 llm-pi-ai.providers + agent-default-model,更早在 $DSH_HOME/settings.yaml;
|
|
248
|
+
密钥放 $DSH_HOME/.credentials.yaml 或 .env),此时会打一行告警。
|
|
249
|
+
· 工具调用与流式:SDK 通道(普通轮)与 headless + --json(续接轮 / 回退)都能上报
|
|
250
|
+
thinking / text / tool_use / tool_result;老版 dsh(headless 没有 --json)只能流
|
|
251
|
+
stderr 的 "dsh: reasoning:" 推理增量、正文收尾一次给出,**没有工具事件**。
|
|
252
|
+
· 多轮上下文(三种,按需选):
|
|
253
|
+
magic-agent -e dsh -s <session_id> -p "追问" # 真续接:dsh 自己恢复上下文
|
|
254
|
+
magic-agent -e dsh --stream --keep-alive "记住 42" # 常驻会话首轮(同进程内续接)
|
|
255
|
+
magic-agent --append <run_id|session_id> -p "刚才那个数" # 往常驻会话追加
|
|
256
|
+
-s/--session <id> 走 headless 的 --session-id:会话由 dsh 持久化在
|
|
257
|
+
$DSH_HOME/sessions/<cwd>/,上下文由 dsh 自己接回来(SDK 通道没有 resume 方法,
|
|
258
|
+
所以续接轮由 headless + --json 承载)。⚠️ 会话按**工作区**分桶:换了 -w 就找不到
|
|
259
|
+
那条会话,dsh 会明确报 does not exist。
|
|
248
260
|
· --permission 未接线(dsh 的权限预设是配置层概念,无法表达四档里的「沙箱关闭」档)。
|
|
249
261
|
· --tools 由 dsh 自己的工具循环决定(无命令行级逐工具开关),传入即忽略。
|
|
250
262
|
· --json-schema 支持(走与 llm/openclaw 相同的输出后处理抽 JSON)。
|
|
@@ -324,7 +336,7 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
324
336
|
追加进来的轮次照常以事件流输出(每轮收尾发一个 turn_end 事件,便于调用方知道「这轮完了」)。
|
|
325
337
|
收工:最后一轮结束后 --idle(默认 5m)内没人追加 → 优雅退出;--idle 0 = 本轮结束就收工
|
|
326
338
|
(追加窗口只在任务运行期间);也可随时 --stop。
|
|
327
|
-
常驻生效期间不套 -t 默认超时(长任务+追加不受
|
|
339
|
+
常驻生效期间不套 -t 默认超时(长任务+追加不受 1h 限制),显式给 -t 才套。
|
|
328
340
|
不生效的形态会自动忽略(非 --stream 调用、不支持追加的引擎);显式传 --keep-alive 却
|
|
329
341
|
不满足条件则明确报错(exit 2)。Windows 无 unix socket,暂不支持。
|
|
330
342
|
注意:常驻进程会在最后一轮结束后继续存活 --idle 那么久 —— 调用方若要「一轮完成」
|
|
@@ -333,7 +345,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
333
345
|
--stream 调用以为命令卡住了;要开就显式 --keep-alive。
|
|
334
346
|
|
|
335
347
|
停止指定会话(--stop / --sessions):
|
|
336
|
-
|
|
348
|
+
开了 --save-registry 时,每次调用都会往会话登记表落一条记录
|
|
349
|
+
(~/.magic-agent/sessions/<run_id>.json,MAGIC_AGENT_SESSIONS 可改目录),
|
|
337
350
|
里面记着**引擎子进程的 pid** 与引擎/模型/工作目录/会话 id。为什么需要它:
|
|
338
351
|
· 引擎 CLI 在独立进程组里跑(Setpgid),调用方 kill 掉 magic-agent 的进程组**带不走它**;
|
|
339
352
|
· 进程句柄只活在调用方内存里,应用重启后之前 detach 出去的会话就再也停不掉。
|
|
@@ -344,6 +357,34 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
344
357
|
行为:先 SIGTERM,2s 内不退再 SIGKILL(连它的 node worker 一起带走),然后回写 state=stopped。
|
|
345
358
|
幂等:进程已不在 / 会话已结束 → stopped=false + reason,退出码仍是 0;id 不存在 → 退出码 1(多半写错了)。
|
|
346
359
|
另:magic-agent 自己收到 SIGINT/SIGTERM 时也会先带走自己的引擎子进程再退出,不留孤儿。
|
|
360
|
+
⚠️ **默认不落盘**(用户 2026-10-12:「会话登记也不需要」):不带 --save-registry 时上面这些
|
|
361
|
+
命令看不见本次调用 —— 默认不留痕是有意的(手工跑一轮 / CI / 脚本编排事后没人寻址)。
|
|
362
|
+
代价:不留档的会话**停不掉**(pid 只存在记录里),--append 也只能按 run_id 走约定路径。
|
|
363
|
+
|
|
364
|
+
删除会话(--delete-session <session_id|run_id>):
|
|
365
|
+
登记记录 + 事件日志(<id>.jsonl)+ 标题覆盖一起删掉,**全机生效**:
|
|
366
|
+
各端(PC 的 --sessions、移动端 H5 的 /desk/sessions)读的都是同一份登记表,
|
|
367
|
+
一端删了处处都看不到。与插件的 DELETE /desk/session/{id} 是同一份实现
|
|
368
|
+
(internal/session/delete.go),两条路可互换。
|
|
369
|
+
magic-agent --delete-session <session_id> # 删掉这条会话(含历史)
|
|
370
|
+
幂等:id 不认识 / 文件本来就不在 → 退出码 0,JSON 里 records_removed=0。
|
|
371
|
+
⚠️ 是**物理删除**:删完历史就回放不了了。客户端把它当成「删除」的唯一实现,
|
|
372
|
+
只在本地藏起来(记墓碑)的话,别的客户端照样列着(2026-10-19 用户报的
|
|
373
|
+
「从pc端删除的会话移动端还在显示」就是这么来的)。
|
|
374
|
+
|
|
375
|
+
会话历史存档(--save-history,默认关):
|
|
376
|
+
会话的**内容**(每条事件)另存一份 ~/.magic-agent/sessions/<session_id>.jsonl,
|
|
377
|
+
--session-log / 客户端「看历史」读的就是它;只有 --stream 才写。**默认不写**:
|
|
378
|
+
没人会回看,写下来却会一直堆在磁盘上(登记表有 24h 保留期在清,jsonl 没有)。
|
|
379
|
+
magic-agent -e claude --stream -p "跑一遍测试" # 不存档(默认)
|
|
380
|
+
magic-agent -e claude --stream --save-history -p "跑一遍测试" # 存档,事后可 --session-log 回看
|
|
381
|
+
magic-agent --session-log <session_id|run_id> # 读存档(没存档就是空数组)
|
|
382
|
+
⚠️ 没存档的会话在客户端列表里 has_log=false —— 能看到、能续接,但点开没有历史内容。
|
|
383
|
+
magic-client 恒传 --save-registry 与 --save-history(客户端本来就要列会话 + 回放历史),
|
|
384
|
+
所以界面里的会话既在列表里、也有历史。
|
|
385
|
+
⚠️ **不等终点事件就落盘**(2026-10-15):一开跑就按 <run_id>.jsonl 写,引擎的
|
|
386
|
+
session_id 到手后改名为 <session_id>.jsonl(读侧两个名字都认)——
|
|
387
|
+
于是「正在跑的会话」在别的客户端里当场可读,引擎整轮不给 id 的形态也不白开存档。
|
|
347
388
|
|
|
348
389
|
附件(-a/--attach:文件+提示词,比如截图加提示词):
|
|
349
390
|
每个引擎尽量用**自己的原生方式**收附件,能力见 --engines 的 attachments 字段:
|
|
@@ -355,7 +396,8 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
355
396
|
arkclaw part:file A2A 原生 file part(base64 inline;远端看不到本机路径)
|
|
356
397
|
trae prompt 无原生通道 → 把**绝对路径**写进提示词(需要 --tools 非 off)
|
|
357
398
|
openclaw prompt 同上
|
|
358
|
-
dsh
|
|
399
|
+
dsh sdk:content-blocks SDK 通道把图片作为原生 image 块发出(base64 inline,
|
|
400
|
+
真进模型上下文);非图片附件 / headless 回退 → 路径写进提示词
|
|
359
401
|
codebuddy-gateway
|
|
360
402
|
prompt 走 payload.attachments 原生字段,但网关把它渲染成路径清单
|
|
361
403
|
塞进 prompt(图不会以图片形式进模型上下文)
|
|
@@ -409,14 +451,29 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
409
451
|
// 与 --stop 同理:用「传过没有」判断,`--session-log ""` 要走参数校验(报要 id),
|
|
410
452
|
// 而不是掉进下面的「提问」分支报 empty prompt。
|
|
411
453
|
return runSessionLog(cmd, opts)
|
|
454
|
+
case flagChanged(cmd, "summarize-title"):
|
|
455
|
+
// 同上:空 id 走参数校验,不要掉进「提问」分支。
|
|
456
|
+
return runSummarizeTitle(cmd, opts)
|
|
457
|
+
case flagChanged(cmd, "session-title"):
|
|
458
|
+
return runSessionTitle(cmd, opts)
|
|
459
|
+
case flagChanged(cmd, "title-force"):
|
|
460
|
+
// --title-force 只在 --summarize-title 下成立:单给它必然是调用方误解了
|
|
461
|
+
// (想要「强制重新归纳」却没给 id)。明说,不静默忽略。
|
|
462
|
+
return &usageError{fmt.Errorf("--title-force 只在 --summarize-title 下成立:请配合 --summarize-title <id> 使用")}
|
|
412
463
|
case flagChanged(cmd, "after"):
|
|
413
464
|
// --after 只在 --session-log 下成立:给了却没给 --session-log 就**明说**,
|
|
414
465
|
// 不静默忽略(静默忽略是本项目反复踩的坑)。
|
|
415
466
|
return &usageError{fmt.Errorf("--after 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
|
|
467
|
+
case flagChanged(cmd, "before"), flagChanged(cmd, "limit"):
|
|
468
|
+
// 同上:--before / --limit 是 --session-log 的翻页参数,单给必然是误解。
|
|
469
|
+
return &usageError{fmt.Errorf("--before / --limit 只在 --session-log 下成立:请配合 --session-log <id> 使用")}
|
|
416
470
|
case flagChanged(cmd, "stop"):
|
|
417
471
|
// 用「传过没有」而不是「值非空」判断:`--stop ""` 也要走参数校验
|
|
418
472
|
//(exit 2 提示要 id),而不是掉进「提问」分支报 empty prompt。
|
|
419
473
|
return runStop(cmd, opts)
|
|
474
|
+
case flagChanged(cmd, "delete-session"):
|
|
475
|
+
// 同上:空 id 走参数校验(`--delete-session ""` 不该被当成提问)。
|
|
476
|
+
return runDeleteSession(cmd, opts)
|
|
420
477
|
case flagChanged(cmd, "append"):
|
|
421
478
|
return runAppend(cmd, args, opts)
|
|
422
479
|
case opts.contract:
|
|
@@ -430,6 +487,11 @@ codebuddy-gateway 引擎(CodeBuddy Code HTTP 网关,走 webhook + SSE;凭
|
|
|
430
487
|
},
|
|
431
488
|
}
|
|
432
489
|
bindAskFlags(root, opts)
|
|
490
|
+
/* 唯一的子命令:`client start|stop|status`。
|
|
491
|
+
为什么破例留一个子命令:它管的是**另一个常驻进程**(magic-client),
|
|
492
|
+
生命周期与 core 的一次性调用完全不同,硬塞成一排 --client-* flags 会让
|
|
493
|
+
「这些 flag 和提问无关」这件事看不出来(用户 2026-10-20:「提供启动停止 client 的子命令」)。 */
|
|
494
|
+
root.AddCommand(clientSubcommand())
|
|
433
495
|
/* `--login` / `--repair-models` 忘带引擎名时,cobra 在解析阶段就报错,**走不到**
|
|
434
496
|
runLogin / runRepairModels 的校验分支,默认文案是英文的
|
|
435
497
|
"flag needs an argument: ..."(且退出码 1)。这里翻成与传空串一致的中文
|
|
@@ -540,6 +602,12 @@ func rejectRemovedSubcommands(cmd *cobra.Command, args []string) error {
|
|
|
540
602
|
|
|
541
603
|
// exitCodeOf 从错误推断退出码。
|
|
542
604
|
func exitCodeOf(err error) int {
|
|
605
|
+
// 转发型子命令(`client`)自带退出码:2 = 参数错这种约定要原样带给调用方,
|
|
606
|
+
// 否则脚本拿到一律为 1 就分不出「用错了」和「跑失败了」。
|
|
607
|
+
var ec *exitCodeError
|
|
608
|
+
if errors.As(err, &ec) && ec.code > 0 {
|
|
609
|
+
return ec.code
|
|
610
|
+
}
|
|
543
611
|
if _, ok := err.(*usageError); ok {
|
|
544
612
|
return exitUsage
|
|
545
613
|
}
|
|
@@ -560,6 +628,67 @@ var probeEngineCredits = func(ctx context.Context, l agent.ModelCreditLister) ma
|
|
|
560
628
|
return l.ModelCredits(ctx)
|
|
561
629
|
}
|
|
562
630
|
|
|
631
|
+
// modelListCacheTTL 模型清单 / 积分倍率探测结果的缓存有效期(30 分钟)。
|
|
632
|
+
//
|
|
633
|
+
// 模型清单来自 CLI 只读子命令或本地缓存文件,30 分钟内几乎不会变;缓存让重复跑
|
|
634
|
+
// --engines / --contract 不必每次重新启动 CLI 进程或重读文件。瞬时失败(CLI 没装 /
|
|
635
|
+
// 探测超时)与空清单**不进缓存**,避免把错误钉死 30 分钟 —— 只有真正拿到模型清单
|
|
636
|
+
// 才落缓存。缓存是进程级的:单次 magic-agent 进程只会探测一次,跨进程的重复调用
|
|
637
|
+
// (如客户端反复拉 --contract)也能省掉 CLI 启动开销。
|
|
638
|
+
const modelListCacheTTL = 30 * time.Minute
|
|
639
|
+
|
|
640
|
+
// modelListCacheEntry 一条缓存:模型清单 + 积分倍率 + 过期时间。
|
|
641
|
+
type modelListCacheEntry struct {
|
|
642
|
+
models []string
|
|
643
|
+
credits map[string]string
|
|
644
|
+
expiresAt time.Time
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
var (
|
|
648
|
+
modelListCacheMu sync.Mutex
|
|
649
|
+
modelListCache = map[string]*modelListCacheEntry{}
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
// cachedModelList 带 30 分钟缓存地探测某引擎的模型清单与积分倍率。
|
|
653
|
+
//
|
|
654
|
+
// key 一般用引擎名(codebuddy / codebuddy-ai / trae …);命中且未过期的缓存直接返回,
|
|
655
|
+
// 不触碰 CLI / 文件。底层走 probeEngineModels / probeEngineCredits(可被测试替换)。
|
|
656
|
+
// ListModels 出错或返回空清单时**不缓存**(视为瞬时态,下次重探)。
|
|
657
|
+
func cachedModelList(ctx context.Context, key string, l agent.ModelLister) (models []string, credits map[string]string, note string) {
|
|
658
|
+
modelListCacheMu.Lock()
|
|
659
|
+
if it, ok := modelListCache[key]; ok && time.Now().Before(it.expiresAt) {
|
|
660
|
+
modelListCacheMu.Unlock()
|
|
661
|
+
return it.models, it.credits, ""
|
|
662
|
+
}
|
|
663
|
+
modelListCacheMu.Unlock()
|
|
664
|
+
|
|
665
|
+
models, err := probeEngineModels(ctx, l)
|
|
666
|
+
if err != nil {
|
|
667
|
+
return nil, nil, err.Error()
|
|
668
|
+
}
|
|
669
|
+
if len(models) == 0 {
|
|
670
|
+
return nil, nil, "no models reported"
|
|
671
|
+
}
|
|
672
|
+
if cl, ok := l.(agent.ModelCreditLister); ok {
|
|
673
|
+
credits = probeEngineCredits(ctx, cl)
|
|
674
|
+
}
|
|
675
|
+
modelListCacheMu.Lock()
|
|
676
|
+
modelListCache[key] = &modelListCacheEntry{
|
|
677
|
+
models: models,
|
|
678
|
+
credits: credits,
|
|
679
|
+
expiresAt: time.Now().Add(modelListCacheTTL),
|
|
680
|
+
}
|
|
681
|
+
modelListCacheMu.Unlock()
|
|
682
|
+
return models, credits, ""
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// clearModelListCache 清空模型清单缓存(测试用,避免用例之间互相命中缓存)。
|
|
686
|
+
func clearModelListCache() {
|
|
687
|
+
modelListCacheMu.Lock()
|
|
688
|
+
modelListCache = map[string]*modelListCacheEntry{}
|
|
689
|
+
modelListCacheMu.Unlock()
|
|
690
|
+
}
|
|
691
|
+
|
|
563
692
|
// flagChanged 报告某个 flag 是否被显式传过(用于「传了就走这条分支」的开关型参数)。
|
|
564
693
|
func flagChanged(cmd *cobra.Command, name string) bool {
|
|
565
694
|
f := cmd.Flags().Lookup(name)
|
|
@@ -666,8 +795,10 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
666
795
|
// stdin:stream-json claude / codebuddy 原生 stream-json input(图片走 content block)
|
|
667
796
|
// flag:-a llm CLI 原生 --attachment(直连模型走 image_url content parts)
|
|
668
797
|
// part:file arkclaw 走 A2A 原生 file part(base64 inline)
|
|
798
|
+
// sdk:content-blocks dsh 的 SDK 通道把图片作为原生 image content block 发出去
|
|
799
|
+
// (base64 inline,真进模型上下文);显式切 headless 后为 prompt
|
|
669
800
|
// prompt 无原生通道:把绝对路径写进提示词,靠引擎读文件
|
|
670
|
-
// (trae/openclaw/
|
|
801
|
+
// (trae/openclaw/codebuddy-gateway)
|
|
671
802
|
Attachments string `json:"attachments,omitempty"`
|
|
672
803
|
// Append 是否支持「常驻会话 + 追加消息」(`--stream --keep-alive` 与 `--append`)。
|
|
673
804
|
// claude / codebuddy 为 true(stream-json 输入可持续喂 user 消息);
|
|
@@ -686,6 +817,12 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
686
817
|
// 且网关会把远程任务的权限强制切到 bypassPermissions)
|
|
687
818
|
// 调用方据此决定是否下发 --permission 与那几个沙箱/规则类 flags。
|
|
688
819
|
Permission string `json:"permission,omitempty"`
|
|
820
|
+
// Reasoning 该引擎是否接受 `--reasoning <档>`(machine-readable,见
|
|
821
|
+
// agent.ReasoningSupportOf):claude / codebuddy / codebuddy-ai / codex 为 true;
|
|
822
|
+
// 其余引擎未接线 —— core 对它们**静默忽略**该 flag(不报错、不下传)。
|
|
823
|
+
// 新增(2026-10-20,只增字段不升 contractVersion):老版 CLI 没有这个字段,
|
|
824
|
+
// 客户端据此判断「能不能安全下发 --reasoning」(缺失 = 不下发,避免 exit 2)。
|
|
825
|
+
Reasoning bool `json:"reasoning"`
|
|
689
826
|
// Capabilities 该引擎**静态声明**的能力 id 集合(字典序,稳定、无重复)。
|
|
690
827
|
// 与运行态分开:ok / streaming / models / version 说的是「此刻如何」,
|
|
691
828
|
// capabilities 说的是「能做什么」,二者的推导见 internal/agent/capability.go。
|
|
@@ -707,7 +844,8 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
707
844
|
Attachments: agent.AttachmentSupportOf(e.Name()),
|
|
708
845
|
Append: agent.AppendSupportOf(e.Name()),
|
|
709
846
|
Ask: agent.AskSupportOf(e.Name()),
|
|
710
|
-
Permission: agent.PermissionSupportOf(e.Name())
|
|
847
|
+
Permission: agent.PermissionSupportOf(e.Name()),
|
|
848
|
+
Reasoning: agent.ReasoningSupportOf(e.Name())}
|
|
711
849
|
if ok {
|
|
712
850
|
r.Bin, r.Note = note, ""
|
|
713
851
|
}
|
|
@@ -738,26 +876,30 @@ func emitEngineRows(cmd *cobra.Command, opts *askOptions, envelope bool) error {
|
|
|
738
876
|
continue
|
|
739
877
|
}
|
|
740
878
|
wg.Add(1)
|
|
741
|
-
go func(i int, l agent.ModelLister) {
|
|
879
|
+
go func(i int, name string, l agent.ModelLister) {
|
|
742
880
|
defer wg.Done()
|
|
743
881
|
ctx, cancel := context.WithTimeout(cmd.Context(), agent.DefaultModelProbeTimeout)
|
|
744
882
|
defer cancel()
|
|
745
|
-
models,
|
|
883
|
+
models, credits, note := cachedModelList(ctx, name, l)
|
|
884
|
+
// codebuddy 系:展示时剥掉自定义模型 id 的 "custom-local:" 前缀(噪点);
|
|
885
|
+
// -m 实际传值时由引擎按裸名补回前缀(见 codebuddyCore.modelArg),
|
|
886
|
+
// 自定义模型依旧可用。
|
|
887
|
+
if name == "codebuddy" || name == "codebuddy-ai" {
|
|
888
|
+
models = agent.StripCustomLocalPrefix(models)
|
|
889
|
+
credits = agent.StripCustomLocalPrefixCredits(credits)
|
|
890
|
+
}
|
|
746
891
|
switch {
|
|
747
|
-
case
|
|
748
|
-
|
|
749
|
-
rows[i].ModelsNote = err.Error()
|
|
892
|
+
case note != "":
|
|
893
|
+
rows[i].ModelsNote = note
|
|
750
894
|
case len(models) == 0:
|
|
751
895
|
rows[i].ModelsNote = "no models reported"
|
|
752
896
|
default:
|
|
753
897
|
rows[i].Models = models
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if cl, ok := l.(agent.ModelCreditLister); ok {
|
|
757
|
-
rows[i].ModelCredits = probeEngineCredits(ctx, cl)
|
|
898
|
+
if credits != nil {
|
|
899
|
+
rows[i].ModelCredits = credits
|
|
758
900
|
}
|
|
759
901
|
}
|
|
760
|
-
}(i, lister)
|
|
902
|
+
}(i, e.Name(), lister)
|
|
761
903
|
}
|
|
762
904
|
wg.Wait()
|
|
763
905
|
if !envelope {
|