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
|
@@ -32,6 +32,13 @@ const ATT_EDGE = 1920; // 压缩后最长边(px)
|
|
|
32
32
|
const ATT_TARGET = 1.5 * 1024 * 1024; // 压缩目标(字节)
|
|
33
33
|
const ATT_RE = /^image\/(png|jpeg|jpg|gif|webp|bmp)$/;
|
|
34
34
|
|
|
35
|
+
/** 截图/产物路径扫描的单行长度上限 —— 超长行直接不扫。
|
|
36
|
+
* ⚠️ 实测:一行 0.87MB 的 `data:image/png;base64,…` 在 qaShotInline 里单行要扫 78 秒
|
|
37
|
+
* (每次正则命中都要 slice 前缀,超长行上退化成 O(n²)),主线程一冻就是几分钟,
|
|
38
|
+
* 表现就是「左栏 tab 点不动」。base64 流本来就摘不出可显示路径(紧贴在字母/数字后
|
|
39
|
+
* 一律不算,见 qaShotInline),放过它不影响观感;正常正文不会有 4KB 以上的单行。 */
|
|
40
|
+
const QA_SCAN_MAX_LINE = 4096;
|
|
41
|
+
|
|
35
42
|
const askAtts = {}; // p → [att, ...](按实例分开,与胶囊同款)
|
|
36
43
|
let attSeq = 0;
|
|
37
44
|
|
|
@@ -360,10 +367,69 @@ function askAttBody(p, text){
|
|
|
360
367
|
摘掉的只是屏幕上那几行,别把 askAttBody / 飞书桥写进去的东西改了。
|
|
361
368
|
⚠️ 摘不干净宁可**不摘**:标记后面没有能识别的图片路径时(历史数据 / 人手写的),
|
|
362
369
|
原样保留 —— 静默吃掉用户写的字比显示得难看严重得多。 */
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
|
|
371
|
+
/* ── 对话产物(2026-10-12)────────────────────────────────────────────────
|
|
372
|
+
用户原话:「现在对话ui上还不能显示对话的产物 图片视频等各自文件」。
|
|
373
|
+
图这一档早就有(上面那套 `.qa-atts` 缩略图),但引擎**产出的东西**不止图:成片
|
|
374
|
+
(mp4/mov)、音频、文档(md/pdf/xlsx)、模型(glb)……它们在正文里都只是一串路径,
|
|
375
|
+
一眼看不出是什么、也点不开。这里把**确实存在**的那些渲染成对应的卡:
|
|
376
|
+
· 图 → 缩略图(复用既有那一排 `.qa-atts`,点开看原图)
|
|
377
|
+
· 视频 / 音频 → 就地播放器(`<video>` / `<audio controls>`)
|
|
378
|
+
· 其它文件 → 一枚文件条(后缀徽标 + 名字 + 大小),点开在**右侧栏就地预览**、
|
|
379
|
+
⌥ 点(或右键)在访达里高亮
|
|
380
|
+
⚠️ **存在性由主进程判**(`desk:path-probe`,一次批量问完,见 main.js 那三个口):
|
|
381
|
+
只有真的落在盘上的文件才画 —— 正文里提到的 `app.js`、远端 URL、示例路径都不会
|
|
382
|
+
被误画成卡(宁可少画,不可画错)。
|
|
383
|
+
⚠️ 相对路径按**这条会话的 workspace** 解析(引擎就是在那个目录里干活的,见 qaArtBase);
|
|
384
|
+
相对路径**只认「独占一行」那一档**(行式误判面最小),行内的仍只认绝对路径。
|
|
385
|
+
⚠️ 没有桥(浏览器预览)→ 一个新卡都不画,正文原样 —— 与既有「拼不出地址就不动」同一口径。 */
|
|
386
|
+
|
|
387
|
+
/** 后缀 → 产物类型(图 / 视频 / 音频 / 文件)。**后缀表只有这一份**:主进程只回
|
|
388
|
+
* 「存在 / 绝对路径 / 大小」,类型由渲染层判(见 main.js 的 desk:path-probe 说明)。 */
|
|
389
|
+
function qaArtKind(p){
|
|
390
|
+
const s = String(p || "").split(/[?#]/)[0];
|
|
391
|
+
if(QA_IMG_EXT.test(s)) return "image";
|
|
392
|
+
if(/\.(mp4|mov|m4v|webm|mkv|avi)$/i.test(s)) return "video";
|
|
393
|
+
if(/\.(mp3|wav|m4a|aac|ogg|flac)$/i.test(s)) return "audio";
|
|
394
|
+
if(QA_ART_EXT.test(s)) return "file";
|
|
395
|
+
return "";
|
|
396
|
+
}
|
|
397
|
+
/** 这条会话在哪干活(相对路径的基准):任务的 workspace 优先,否则当前工作目录。
|
|
398
|
+
* 两者都拿不到就回空串 —— 那档只认绝对路径的候选(probe 里相对路径解析不出来自然不画)。 */
|
|
399
|
+
function qaArtBase(){
|
|
400
|
+
try{
|
|
401
|
+
const arr = (typeof qaTasks !== "undefined" && Array.isArray(qaTasks)) ? qaTasks : [];
|
|
402
|
+
const t = arr.find(x=>x.id === qaActiveId);
|
|
403
|
+
if(t && t.workspace) return String(t.workspace);
|
|
404
|
+
if(typeof wsDirOf === "function"){
|
|
405
|
+
const d = String(wsDirOf() || "");
|
|
406
|
+
if(d) return d;
|
|
407
|
+
}
|
|
408
|
+
/* 组件宿主(PC 网页 / 移动端)没有任务表,wsDirOf 恒为空串 —— 但组件会把当前会话的
|
|
409
|
+
工作目录写回 `window.qaWorkspace`(conversation.js 的 syncHostState),拿它当基准。
|
|
410
|
+
没有这一档,回放里的**相对路径**产物一律探不到(用户报「会话产物不显示」)。 */
|
|
411
|
+
if(typeof window !== "undefined" && window.qaWorkspace) return String(window.qaWorkspace);
|
|
412
|
+
}catch(e){ /* 拿不到就当没有基准 */ }
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
/** 落盘地址:宿主给了回读通道(`window.desk.mediaUrl`,组件宿主走 /desk/media)就用它;
|
|
416
|
+
* 桌面壳的页面是 `file://` → 拼 `file://`(与 qaShotSrc 同一招);
|
|
417
|
+
* 产物库里的(`~/.magic-test/images|pastes/…`)走内置后端,预览态也通。 */
|
|
418
|
+
function qaArtUrl(abs){
|
|
419
|
+
const p = String(abs || "");
|
|
420
|
+
if(!p) return "";
|
|
421
|
+
try{
|
|
422
|
+
if(typeof window !== "undefined" && window.desk && typeof window.desk.mediaUrl === "function"){
|
|
423
|
+
const u = window.desk.mediaUrl(p);
|
|
424
|
+
if(u) return u;
|
|
425
|
+
}
|
|
426
|
+
}catch(e){ /* 宿主通道不通就退下面的 */ }
|
|
427
|
+
if(typeof qaShotSrc === "function"){
|
|
428
|
+
const u = qaShotSrc(p);
|
|
429
|
+
if(u) return u;
|
|
430
|
+
}
|
|
431
|
+
return "file://" + encodeURI(p).replace(/#/g, "%23");
|
|
432
|
+
}
|
|
367
433
|
|
|
368
434
|
/* ── 行内形态(2026-09-20 二次)─────────────────────────────────────────────
|
|
369
435
|
用户原话:「用户的截图显示对了 bot回复中显示图片还不对」。
|
|
@@ -379,13 +445,13 @@ const QA_IMG_EXT = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
|
379
445
|
markdown 图片/链接的地址(`![]()` / `[]()` —— 那条管线自己有渲染与尺寸口径)。
|
|
380
446
|
⚠️ 拼不出可显示地址的路径(预览态拿不到本机文件)**一个字都不动** —— 与 qaShotsMount
|
|
381
447
|
末尾那条「摘不干净宁可不摘」同一口径。 */
|
|
382
|
-
const QA_SHOT_PATH_RE = /(?:file:\/\/|\/|~\/)[^\s"'`<>()\[\],。;:、)】]+?\.(?:png|jpe?g|gif|webp|bmp)(?![A-Za-z0-9])/gi;
|
|
383
448
|
|
|
384
449
|
/** 一行里的「标记 + 路径」/「光秃秃的路径」→ 摘成缩略图。
|
|
385
450
|
* 返回剩下的文字(shots 里收集被摘掉的路径,顺序与正文一致)。 */
|
|
386
451
|
function qaShotInline(line, shots){
|
|
387
452
|
const raw = String(line || "");
|
|
388
453
|
if(!raw || raw.indexOf("/") < 0) return raw; // 一行里连 `/` 都没有 → 没什么可摘的
|
|
454
|
+
if(raw.length > QA_SCAN_MAX_LINE) return raw; // 超长行(base64 图流)扫它是 O(n²),直接放过
|
|
389
455
|
const codes = []; // 行内代码先占位(里面的路径不是截图)
|
|
390
456
|
let s = raw.replace(/`[^`]*`/g, (m)=>{ codes.push(m); return "\u0000" + (codes.length - 1) + "\u0000"; });
|
|
391
457
|
const hits = [];
|
|
@@ -486,6 +552,287 @@ function qaThumbNode(src, title, alt){
|
|
|
486
552
|
return im;
|
|
487
553
|
}
|
|
488
554
|
|
|
555
|
+
/* ── 对话产物:从正文里挑出「可能是产物」的路径(2026-10-12)───────────────
|
|
556
|
+
三条来源(都是模型/工具真会写出来的样子):
|
|
557
|
+
① 整行就是一个路径(可带 `- ` 列表前缀);
|
|
558
|
+
② markdown 图片 / 链接的地址(`` / `[成片](out/x.mp4)`);
|
|
559
|
+
③ 行内的路径(「成片在 out/x.mp4」「见 docs/campus/onboarding.md」这种半句话里的)。
|
|
560
|
+
三条都**认相对路径** —— 会不会误判成产物不由这里管:**存不存在由主进程探针判**
|
|
561
|
+
(所以「见 src/js/app.js」这类提法不会画成卡)。
|
|
562
|
+
⚠️ 代码块整段跳过(那里面的路径是给人看的字,不是产物)。
|
|
563
|
+
⚠️ 只**挑**不摘:正文一个字都不动,卡片画在正文下面(见 qaArtMount)。
|
|
564
|
+
与图片那一路(摘掉路径、只留缩略图)不同 —— 图是「一眼要看的东西」,
|
|
565
|
+
而视频/文档的路径本身也常是正文的一部分(「成片在 out/x.mp4」),摘掉反而读不通。
|
|
566
|
+
⚠️ 挑多了不要紧:**是否真的存在由主进程判**(desk:path-probe,见 qaArtMount)。 */
|
|
567
|
+
function qaArtCollect(text){
|
|
568
|
+
const out = [], seen = new Set();
|
|
569
|
+
const push = (p)=>{ const s = String(p || "").trim(); if(!s || seen.has(s) || !qaArtKind(s)) return; seen.add(s); out.push(s); };
|
|
570
|
+
let fence = false;
|
|
571
|
+
for(const raw of String(text || "").split("\n")){
|
|
572
|
+
if(raw.length > QA_SCAN_MAX_LINE) continue; // 超长行(base64 图流)不扫,理由见 QA_SCAN_MAX_LINE
|
|
573
|
+
const ln = raw.trim();
|
|
574
|
+
if(/^(?:```|~~~)/.test(ln)){ fence = !fence; continue; }
|
|
575
|
+
if(fence) continue;
|
|
576
|
+
const one = QA_ART_LINE.exec(ln); // ① 整行一个路径(含相对路径)
|
|
577
|
+
if(one){ push(one[1]); continue; }
|
|
578
|
+
let m;
|
|
579
|
+
const md = /!?\[[^\]]*\]\(([^)\s]+)\)/g; // ② markdown 地址
|
|
580
|
+
while((m = md.exec(raw))){
|
|
581
|
+
const href = m[1];
|
|
582
|
+
if(/^(?:https?|data|blob|mailto|#)/i.test(href)) continue; // 远端 / 锚点不归这里管
|
|
583
|
+
push(href);
|
|
584
|
+
}
|
|
585
|
+
const abs = new RegExp(QA_ART_PATH_SRC + "[^\\s\"'`<>()\\[\\],。;:、)】]+?\\.(?:" + QA_ART_EXT_SRC + ")(?![A-Za-z0-9])", "gi");
|
|
586
|
+
while((m = abs.exec(raw))){ // ③ 行内路径(含相对路径 —— 存不存在由探针判)
|
|
587
|
+
const p = m[0];
|
|
588
|
+
if(/[(!]\s*$/.test(raw.slice(0, m.index))) continue; // markdown 地址已在②处理
|
|
589
|
+
if(/[A-Za-z0-9_+/=:.-]$/.test(raw.slice(0, m.index))) continue; // 紧贴字母/数字 → URL / base64
|
|
590
|
+
push(p);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return out.slice(0, 12); // 一次最多画 12 张(多了就不是「产物」了)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** 一张产物卡(按类型分流:图 / 视频 / 音频 / 其它文件)。 */
|
|
597
|
+
function qaArtNode(it){
|
|
598
|
+
const kind = qaArtKind(it.abs) || "file";
|
|
599
|
+
const name = String((it.abs || "").split("/").pop() || "文件");
|
|
600
|
+
const card = document.createElement("div");
|
|
601
|
+
card.className = "qa-art qa-art-" + kind;
|
|
602
|
+
card.dataset.path = it.abs || "";
|
|
603
|
+
card.title = (it.abs || "") + "\n点击在右侧栏预览 · ⌥ 点击在访达中显示";
|
|
604
|
+
const sz = (typeof attSize === "function" && it.size) ? attSize(it.size) : "";
|
|
605
|
+
if(kind === "image"){ // 图:走既有缩略图形制(点开看原图那条链)
|
|
606
|
+
card.appendChild(qaThumbNode(qaArtUrl(it.abs), name + " · 点开看原图", name));
|
|
607
|
+
return card;
|
|
608
|
+
}
|
|
609
|
+
if(kind === "video" || kind === "audio"){ // 视频 / 音频:就地播放
|
|
610
|
+
const m = document.createElement(kind);
|
|
611
|
+
m.controls = true; m.preload = "metadata"; m.src = qaArtUrl(it.abs);
|
|
612
|
+
if(kind === "video") m.playsInline = true;
|
|
613
|
+
card.appendChild(m);
|
|
614
|
+
}
|
|
615
|
+
const cap = document.createElement("div");
|
|
616
|
+
cap.className = "qa-art-cap";
|
|
617
|
+
if(kind === "file"){ // 其它文件:后缀徽标 + 名字 + 大小
|
|
618
|
+
const ext = document.createElement("span");
|
|
619
|
+
ext.className = "qa-art-ext";
|
|
620
|
+
ext.textContent = (name.split(".").pop() || "?").toUpperCase().slice(0, 4);
|
|
621
|
+
cap.appendChild(ext);
|
|
622
|
+
}
|
|
623
|
+
const nm = document.createElement("span");
|
|
624
|
+
nm.className = "qa-art-nm"; nm.textContent = name;
|
|
625
|
+
cap.appendChild(nm);
|
|
626
|
+
if(sz){
|
|
627
|
+
const s = document.createElement("span");
|
|
628
|
+
s.className = "qa-art-sz"; s.textContent = sz;
|
|
629
|
+
cap.appendChild(s);
|
|
630
|
+
}
|
|
631
|
+
const go = document.createElement("span");
|
|
632
|
+
go.className = "qa-art-go"; go.textContent = "查看";
|
|
633
|
+
cap.appendChild(go);
|
|
634
|
+
card.appendChild(cap);
|
|
635
|
+
return card;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** 把挑出的产物路径画成一排卡(**只画真的落在盘上的**)。
|
|
639
|
+
* ⚠️ 存在性问主进程(`desk:path-probe`,一次批量)——渲染层拿不到 fs。
|
|
640
|
+
* 正文里提到的 `app.js`、远端 URL、示例路径都不会被误画成卡。
|
|
641
|
+
* ⚠️ 相对路径按**这条会话的 workspace** 解析(qaArtBase);没有桥(浏览器预览)→ 一张不画。
|
|
642
|
+
* `opts.label`(2026-10-15)= 在这一排卡上面先落一行小标题(「本轮产物」用,见 qaTurnArtsMount);
|
|
643
|
+
* `opts.detached` = 允许画进**不在文档树上**的宿主:后台那一轮的画布被整份摘进 holder,
|
|
644
|
+
* 节点还在、只是不在树上(`isConnected` 为假)—— 收尾时往它末尾补产物正是这种情形。
|
|
645
|
+
* 返回画出来的张数(Promise):收尾那条链要**等它画完**再落 HTML 快照(见 qaTurnArtsMount)。 */
|
|
646
|
+
function qaArtMount(host, paths, opts){
|
|
647
|
+
const o = opts || {};
|
|
648
|
+
const api = (window.desk && typeof window.desk.pathProbe === "function") ? window.desk.pathProbe : null;
|
|
649
|
+
if(!host || !api || !Array.isArray(paths) || !paths.length) return Promise.resolve(0);
|
|
650
|
+
const list = paths.slice(0, 12);
|
|
651
|
+
return Promise.resolve(api({ base: qaArtBase(), paths: list })).then((r)=>{
|
|
652
|
+
const items = (r && r.ok && Array.isArray(r.items)) ? r.items : [];
|
|
653
|
+
const hit = items.filter((x)=> x && x.exists && x.abs);
|
|
654
|
+
if(!hit.length) return 0; // 一个都不存在 → 不画
|
|
655
|
+
if(!o.detached && !host.isConnected) return 0; // 这条消息已经换掉了 → 不画
|
|
656
|
+
if(o.label){
|
|
657
|
+
const cap = document.createElement("div");
|
|
658
|
+
cap.className = "qa-arts-cap";
|
|
659
|
+
cap.textContent = String(o.label);
|
|
660
|
+
host.appendChild(cap);
|
|
661
|
+
}
|
|
662
|
+
/* 按类型分行(2026-10-20 用户:「按类型分类显示 要么就宽高一致」)——
|
|
663
|
+
同一排里图片是缩略图、视频是播放器、文档是胶囊卡,宽高天然不同,混在一起参差不齐。
|
|
664
|
+
这里按 图 / 视频 / 音频 / 文件 各起一行,行内格子尺寸再由 app.css 统一。 */
|
|
665
|
+
const buckets = new Map();
|
|
666
|
+
hit.forEach((x)=>{
|
|
667
|
+
const k = qaArtKind(x.abs) || "file";
|
|
668
|
+
if(!buckets.has(k)) buckets.set(k, []);
|
|
669
|
+
buckets.get(k).push(x);
|
|
670
|
+
});
|
|
671
|
+
["image", "video", "audio", "file"].forEach((k)=>{
|
|
672
|
+
const arr = buckets.get(k);
|
|
673
|
+
if(!arr || !arr.length) return;
|
|
674
|
+
const row = document.createElement("div");
|
|
675
|
+
row.className = "qa-arts qa-arts-" + k;
|
|
676
|
+
arr.forEach((x)=> row.appendChild(qaArtNode(x)));
|
|
677
|
+
host.appendChild(row);
|
|
678
|
+
});
|
|
679
|
+
return hit.length;
|
|
680
|
+
}).catch(()=> 0); // 探针失败 = 当作「都不是产物」,正文照旧
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/* ── 会话产物 · 消息末尾的「本轮产物」(2026-10-15)─────────────────────────
|
|
684
|
+
用户原话:「会话有新的图片、视频文件产生 应该在消息结束时附加在末尾作为会话产物显示预览」。
|
|
685
|
+
正文里写出来的产物路径早就有渲染(qaShotsMount / qaArtMount:图变缩略图、视频变就地播放器),
|
|
686
|
+
但**没写进正文**的那些没人画:
|
|
687
|
+
· 工具在输出里报的落盘产物(`MEDIA: /…/x.png` 这类)只落在工具卡里,而工具卡默认收在
|
|
688
|
+
「任务耗时 · N 项过程」折叠组内 —— 跑完一眼看不见;
|
|
689
|
+
· 写类工具**参数 JSON** 里的 `file_path`(Write / Edit 改的是哪个文件,参数里最准);
|
|
690
|
+
· 壳在 notice 里报的录屏 / 截屏(`recFiles` + `recDir`)与短视频成片
|
|
691
|
+
(`shortsFiles` + `shortsDir`)此前只有一句 toast。
|
|
692
|
+
于是这里补两件事:**收**(这一轮进来的产物路径记在 run 上)与**画**(收尾时补在消息末尾)。
|
|
693
|
+
⚠️ 认图 / 视频 / 文件三档(2026-10-20 扩档:原先只认图与视频,用户反馈 md / pdf / glb 等文档与
|
|
694
|
+
模型文件在正文里只有路径、"本轮产物"里也不显示 —— 一起放开)。音频(mp3/wav)暂不搬,
|
|
695
|
+
那类大多是纯过程产物,不像文档那样需要点进去看。
|
|
696
|
+
⚠️ 与正文里已经画过的**不重复**(按绝对路径与文件名双判):用户要的是「正文没提的也看得见」,
|
|
697
|
+
正文里已经有的再画一遍就成了同一张图出现两次。
|
|
698
|
+
⚠️ 只**收**不摘、只**补**不改:正文与工具输出原文一个字都不动。 */
|
|
699
|
+
const QA_TURN_ART_KIND = /^(?:image|video|file)$/;
|
|
700
|
+
/** 记一条候选路径进**这一轮**的产物集(去重;留图 / 视频 / 文件三档)。 */
|
|
701
|
+
function qaTurnArtsNote(run, p){
|
|
702
|
+
const s = String(p == null ? "" : p).trim();
|
|
703
|
+
if(!run || !s) return;
|
|
704
|
+
if(!QA_TURN_ART_KIND.test(qaArtKind(s))) return;
|
|
705
|
+
if(!run.turnArts) run.turnArts = new Set();
|
|
706
|
+
run.turnArts.add(s);
|
|
707
|
+
}
|
|
708
|
+
/** 从一段**工具结果正文**里挑产物 —— 与正文那条同一个挑法(qaArtCollect):
|
|
709
|
+
* 一次调用最多 12 条,代码块整段跳过,存不存在由探针判。 */
|
|
710
|
+
function qaTurnArtsNoteText(run, text){
|
|
711
|
+
if(!run || !text) return;
|
|
712
|
+
try{ qaArtCollect(String(text)).forEach((p)=> qaTurnArtsNote(run, p)); }
|
|
713
|
+
catch(e){ /* 挑不出来不影响这一轮 */ }
|
|
714
|
+
}
|
|
715
|
+
/* 工具调用的**参数**(JSON)里本来就写着产物路径(2026-10-20 用户:「工具调用是json格式的啊
|
|
716
|
+
里面有文件路径」)。写类工具的 `file_path` / `path` / `output_path` 这一步落盘到哪儿,
|
|
717
|
+
参数里是**确定的**,比事后再从结果正文里猜可靠 —— 结果常常只有一句
|
|
718
|
+
「has been updated successfully」,路径根本不在结果里。
|
|
719
|
+
⚠️ 只认**写文件**的工具:Read / Glob / Grep 的参数里也有路径,那是输入不是产物。 */
|
|
720
|
+
const QA_TOOL_WRITE_NAME = /^(?:write|write_file|edit|multiedit|multi_edit|notebook_edit|notebookedit|create_file|str_replace_editor|filesystem_write_file|filesystem_edit_file)$/i;
|
|
721
|
+
const QA_TOOL_PATH_KEY = /^(?:file_path|filepath|path|filename|file_name|file|output_path|output|target_file|target_path|dest|dst|destination|save_path|out)$/i;
|
|
722
|
+
/** 从写类工具的**参数 JSON** 里挑产物路径(键名看着像路径 + 后缀认得出来才算)。
|
|
723
|
+
* ⚠️ 参数不是 JSON(命令行文本)就不猜:那是结果正文那条链路的事。 */
|
|
724
|
+
function qaTurnArtsNoteTool(run, name, args){
|
|
725
|
+
if(!run) return;
|
|
726
|
+
if(!QA_TOOL_WRITE_NAME.test(String(name || "").trim())) return;
|
|
727
|
+
let o = args;
|
|
728
|
+
if(typeof o === "string"){
|
|
729
|
+
const s = o.trim();
|
|
730
|
+
if(!s) return;
|
|
731
|
+
try{ o = JSON.parse(s); }catch(e){ return; }
|
|
732
|
+
}
|
|
733
|
+
if(!o || typeof o !== "object") return;
|
|
734
|
+
const walk = (v, depth)=>{
|
|
735
|
+
if(!v || depth > 2) return;
|
|
736
|
+
if(Array.isArray(v)){ v.forEach((x)=> walk(x, depth + 1)); return; }
|
|
737
|
+
if(typeof v !== "object") return;
|
|
738
|
+
Object.keys(v).forEach((k)=>{
|
|
739
|
+
const val = v[k];
|
|
740
|
+
if(typeof val === "string"){
|
|
741
|
+
if(QA_TOOL_PATH_KEY.test(k) && qaArtKind(val)) qaTurnArtsNote(run, val);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
walk(val, depth + 1);
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
walk(o, 0);
|
|
748
|
+
}
|
|
749
|
+
/** 从壳的 notice 里挑产物:录屏 / 截屏(`recDir` + `recFiles`)、短视频成片(`shortsDir` + `shortsFiles`)。
|
|
750
|
+
* ⚠️ 壳给的是**目录 + 文件名**两截(绝对路径由这里拼):拼法必须与主进程落盘的位置一致,
|
|
751
|
+
* 所以目录一律取 notice 上带的那一份,不自己按 kind 猜(见 main.js 的 recDir / shortsDir)。 */
|
|
752
|
+
function qaTurnArtsNoteNotice(run, ev){
|
|
753
|
+
if(!run || !ev) return;
|
|
754
|
+
const dir = String(ev.shortsDir || ev.recDir || "").trim().replace(/\/+$/, "");
|
|
755
|
+
const files = Array.isArray(ev.shortsFiles) ? ev.shortsFiles
|
|
756
|
+
: (Array.isArray(ev.recFiles) ? ev.recFiles : []);
|
|
757
|
+
if(!dir || !files.length) return;
|
|
758
|
+
files.forEach((n)=>{ const b = String(n == null ? "" : n).trim(); if(b) qaTurnArtsNote(run, dir + "/" + b); });
|
|
759
|
+
}
|
|
760
|
+
/** 收尾时把这一轮的产物补在这条消息**末尾**(`.qa-bd` 的尾巴 = 正文之后、操作行之前)。
|
|
761
|
+
* ⚠️ 调用方要**等这个 Promise**(收尾那条链用它落 HTML 快照)—— 补晚了产物卡就进不了历史。
|
|
762
|
+
* ⚠️ 正文里已经画过的跳过:两条链(`.qa-atts` 缩略图 / `.qa-arts` 产物卡)都落在 `.qa-t` 里,
|
|
763
|
+
* 按**绝对路径**与**文件名**双判(正文那条链画的是探针回来的 abs,这里手上可能还是相对路径)。 */
|
|
764
|
+
function qaTurnArtsMount(run, h){
|
|
765
|
+
const paths = (run && run.turnArts) ? [...run.turnArts] : [];
|
|
766
|
+
const el = h && h.el;
|
|
767
|
+
const host = (el && el.querySelector) ? el.querySelector(".qa-bd") : null;
|
|
768
|
+
if(!host || !paths.length) return Promise.resolve(0);
|
|
769
|
+
const seen = new Set();
|
|
770
|
+
const mark = (s)=>{ const t = String(s == null ? "" : s); if(t){ seen.add(t); seen.add(t.split("/").pop()); } };
|
|
771
|
+
try{
|
|
772
|
+
el.querySelectorAll(".qa-t .qa-art[data-path]").forEach((n)=> mark(n.dataset.path));
|
|
773
|
+
el.querySelectorAll(".qa-t .qa-atts img, .qa-t img").forEach((n)=>{ mark(n.getAttribute("src")); mark(n.getAttribute("alt")); });
|
|
774
|
+
}catch(e){ /* 取不到就当没画过 —— 顶多多画一张,不会漏 */ }
|
|
775
|
+
const want = paths.filter((p)=>{ const b = String(p).split("/").pop(); return !seen.has(p) && !seen.has(b); });
|
|
776
|
+
if(!want.length) return Promise.resolve(0);
|
|
777
|
+
return qaArtMount(host, want, { label: "本轮产物", detached: true });
|
|
778
|
+
}
|
|
779
|
+
/** 回放 / 续传专用(2026-10-20 方案 A):按**轮**收产物,封口时补挂到这轮消息末尾。
|
|
780
|
+
*
|
|
781
|
+
* 为什么要有它:历史重画(`qaSessionPlayer` / 续传 `qaTailFeed`)把 `ctx.onStep` 桩成空函数,
|
|
782
|
+
* 实时那条链接在 `onStep` 上的 `qaTurnArtsNoteTool` / `qaTurnArtsNoteText` 一次也不会被调 →
|
|
783
|
+
* 重画出来的轮次末尾永远没有「本轮产物」(用户报的「开发壳不显示产物」就是这条路)。
|
|
784
|
+
* 这里直接吃事件本身,用**与实时完全同一套挑法**(写类工具参数 / 工具输出正文 / notice),
|
|
785
|
+
* 迟到的那一枚 `qaTurnArtsMount` 只补挂、不改正文、不影响回放速度。
|
|
786
|
+
* @returns {{ note(ev):void, flush(h):void, settled(ms):Promise }}
|
|
787
|
+
* note(ev) 喂一条回放事件(只认 tool_use / tool_result / notice)
|
|
788
|
+
* flush(h) 一轮封口:把这一轮收到的产物挂到 `h.el` 的消息末尾(容器已不在树上也照挂)
|
|
789
|
+
* settled() 等已排队的补挂画完(要取 innerHTML 存 `__convReplay` 时才需要) */
|
|
790
|
+
function qaTurnArtsTrack(){
|
|
791
|
+
let run = null;
|
|
792
|
+
const pending = [];
|
|
793
|
+
const ensure = ()=> (run || (run = { turnArts: new Set() }));
|
|
794
|
+
const flush = (h)=>{
|
|
795
|
+
const r = run; run = null; // 无论挂不挂得上都换轮
|
|
796
|
+
if(!r || !r.turnArts || !r.turnArts.size) return;
|
|
797
|
+
const el = h && h.el;
|
|
798
|
+
if(!el) return;
|
|
799
|
+
/* 同一张卡可能被分页拼接过(qaConvJoinTurn 会搬子节点):路径按卡累积,不漏也不重 */
|
|
800
|
+
const merged = new Set(Array.isArray(el.__qaTurnArts) ? el.__qaTurnArts : []);
|
|
801
|
+
r.turnArts.forEach((p)=> merged.add(p));
|
|
802
|
+
el.__qaTurnArts = [...merged];
|
|
803
|
+
if(typeof qaTurnArtsMount !== "function") return;
|
|
804
|
+
try{
|
|
805
|
+
pending.push(Promise.resolve(qaTurnArtsMount({ turnArts: new Set(el.__qaTurnArts) }, { el })).catch(()=> 0));
|
|
806
|
+
}catch(e){ /* 挂不上不影响回放 */ }
|
|
807
|
+
};
|
|
808
|
+
return {
|
|
809
|
+
note(ev){
|
|
810
|
+
const k = String((ev && ev.kind) || "");
|
|
811
|
+
if(k === "tool_use"){
|
|
812
|
+
if(typeof qaTurnArtsNoteTool === "function") qaTurnArtsNoteTool(ensure(), ev.name, ev.text);
|
|
813
|
+
}else if(k === "tool_result"){
|
|
814
|
+
if(typeof qaTurnArtsNoteText === "function") qaTurnArtsNoteText(ensure(), ev.text);
|
|
815
|
+
}else if(k === "notice"){
|
|
816
|
+
if(typeof qaTurnArtsNoteNotice === "function") qaTurnArtsNoteNotice(ensure(), ev);
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
flush,
|
|
820
|
+
settled(ms){
|
|
821
|
+
const p = Promise.all(pending.splice(0).map((x)=> Promise.resolve(x).catch(()=> 0)));
|
|
822
|
+
return (typeof qaTurnArtsSettled === "function") ? qaTurnArtsSettled(p, ms) : p;
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** 探针那一跑的**时间上限**(收尾链要等它画完再落快照,但绝不能被它拖住:
|
|
828
|
+
* 解锁撰写区、自动提交排队的那条都在同一个 settle 里)。到时按「没补上」处理,界面照常往下走。 */
|
|
829
|
+
function qaTurnArtsSettled(p, ms){
|
|
830
|
+
return Promise.race([
|
|
831
|
+
Promise.resolve(p).catch(()=> 0),
|
|
832
|
+
new Promise((res)=> setTimeout(()=> res(0), ms || 1500)),
|
|
833
|
+
]);
|
|
834
|
+
}
|
|
835
|
+
|
|
489
836
|
/** 气泡正文的统一落笔处:markdown(摘掉截图路径那几行/那几段)+ 下面一排截图缩略图。
|
|
490
837
|
* 三条渲染器都走它 —— 实时那条用户气泡(qaRenderMine)、实时那条 bot 答复
|
|
491
838
|
* (qaRenderAnswer 的 flushClosed / finish / setResult)与派发快照那条(qaReqConvMsg):
|
|
@@ -493,7 +840,9 @@ function qaThumbNode(src, title, alt){
|
|
|
493
840
|
* ⚠️ 2026-09-20 二次:bot / 审核引擎的答复也走这里了(用户:「用户的截图显示对了
|
|
494
841
|
* bot回复中显示图片还不对」)—— 引擎回话里带的是**它看到的引用**,同一串路径
|
|
495
842
|
* 在用户气泡里是缩略图、在 bot 气泡里是文字,读起来就像「bot 没看见图」。
|
|
496
|
-
* 摘的仍然只是**屏幕上那几个字符**:库里的正文 / 下发给引擎的 prompt 一个字不动。
|
|
843
|
+
* 摘的仍然只是**屏幕上那几个字符**:库里的正文 / 下发给引擎的 prompt 一个字不动。
|
|
844
|
+
* ⚠️ 2026-10-12:出处再加一档 —— **对话产物**(视频 / 音频 / 文档 / 相对路径的图)
|
|
845
|
+
* 画成卡(见 qaArtMount),挑与画都在这一处,三条渲染器同样一起受益。 */
|
|
497
846
|
function qaShotsMount(host, text){
|
|
498
847
|
if(!host) return;
|
|
499
848
|
const sp = qaShotSplit(text);
|
|
@@ -505,11 +854,12 @@ function qaShotsMount(host, text){
|
|
|
505
854
|
: sp.text;
|
|
506
855
|
try{ host.innerHTML = mdToHtmlOnly(rest); highlightIn(host); }
|
|
507
856
|
catch(e){ host.textContent = rest; }
|
|
508
|
-
if(!srcs.length) return;
|
|
509
857
|
const row = document.createElement("div");
|
|
510
858
|
row.className = "qa-atts";
|
|
511
859
|
srcs.forEach((x)=> row.appendChild(qaThumbNode(x.u, x.p.split("/").pop() + " · 点开看原图")));
|
|
512
|
-
host.appendChild(row);
|
|
860
|
+
if(srcs.length) host.appendChild(row);
|
|
861
|
+
/* 产物卡:在**摘完之后**的正文上挑(已经被摘走的图片路径不会再被挑一遍)。 */
|
|
862
|
+
qaArtMount(host, qaArtCollect(rest));
|
|
513
863
|
}
|
|
514
864
|
|
|
515
865
|
/** 附件条 + 粘贴 / 拖拽接线(逐实例各绑一次,与 + 菜单同款) */
|
|
@@ -524,22 +874,18 @@ function askAttBind(p){
|
|
|
524
874
|
else askAttDel(p, host.dataset.id);
|
|
525
875
|
});
|
|
526
876
|
}
|
|
527
|
-
/* 生成中(.is-busy
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
toast("正在生成中 · 等这一轮结束再加截图");
|
|
534
|
-
return true;
|
|
535
|
-
};
|
|
877
|
+
/* ⚠️ 生成中(.is-busy)**不再拦贴图**(2026-10-19 用户:「加截图没啥冲突 不应该限制」)。
|
|
878
|
+
改前这里有一道 busyGate:处理中粘贴 / 拖入一律挡下,提示「等这一轮结束再加截图」。
|
|
879
|
+
它与既有语义自相矛盾 —— 撰写区在生成中本来就**照常可写**,回车即排队(qaMsgQueue);
|
|
880
|
+
图的落盘既不占这一轮的资源、也不改这一轮的 prompt,挂上去等着就行:
|
|
881
|
+
附件存在**实例**上(askAtts[p]),排队那条收尾后由 qaMsgFlush 走 qaSend 取走,
|
|
882
|
+
贴图与那条一起发出。纯文本粘贴的默认行为照旧不碰。 */
|
|
536
883
|
/* 粘贴:只认「确实带图片文件」的剪贴板。没有图片时一行不拦 ——
|
|
537
884
|
纯文本 / 富文本粘贴继续走浏览器默认行为(无差别 preventDefault 会吃掉文字粘贴,
|
|
538
885
|
这是这类实现最常见的一处自伤)。 */
|
|
539
886
|
box.addEventListener("paste", (e)=>{
|
|
540
887
|
const files = attFromDataTransfer(e.clipboardData);
|
|
541
888
|
if(!files.length) return;
|
|
542
|
-
if(busyGate(e)) return;
|
|
543
889
|
e.preventDefault();
|
|
544
890
|
askAttAdd(p, files);
|
|
545
891
|
});
|
|
@@ -549,8 +895,7 @@ function askAttBind(p){
|
|
|
549
895
|
["dragenter", "dragover"].forEach((t)=>box.addEventListener(t, (e)=>{
|
|
550
896
|
if(!attFromDataTransfer(e.dataTransfer).length) return;
|
|
551
897
|
e.preventDefault(); e.stopPropagation();
|
|
552
|
-
|
|
553
|
-
box.classList.add("is-drop");
|
|
898
|
+
box.classList.add("is-drop"); // 生成中同样给落点反馈(贴图不再受限)
|
|
554
899
|
}));
|
|
555
900
|
box.addEventListener("dragleave", (e)=>{
|
|
556
901
|
if(!box.contains(e.relatedTarget)) box.classList.remove("is-drop");
|
|
@@ -563,7 +908,6 @@ function askAttBind(p){
|
|
|
563
908
|
if(anyFile){ e.preventDefault(); e.stopPropagation(); toast("只支持图片(PNG / JPEG / WebP / GIF / BMP)"); }
|
|
564
909
|
return;
|
|
565
910
|
}
|
|
566
|
-
if(busyGate(e)) return;
|
|
567
911
|
e.preventDefault(); e.stopPropagation();
|
|
568
912
|
askAttAdd(p, files);
|
|
569
913
|
});
|