magic-agent 1.0.49 → 1.0.108
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 +514 -7
- package/README.md +31 -2
- package/client-ui/pc/app.css +3 -41
- package/client-ui/pc/js/04-task-list.js +6 -2
- package/client-ui/pc/js/90-glue.js +23 -4
- package/client-ui/pc/js/92-composer.js +11 -4
- package/client-ui/pc/js/93-rail.js +178 -19
- package/client-ui/shared/conv/03-chat-message.js +145 -25
- package/client-ui/shared/conv/09-stream-apply.js +55 -6
- package/client-ui/shared/conv/19-attachments.js +70 -6
- package/client-ui/shared/conv/27-conv-ledger.js +18 -0
- package/client-ui/shared/conv/29-msg-variants.js +10 -0
- package/client-ui/shared/conv/README.md +6 -0
- package/client-ui/shared/conv/conv.css +77 -0
- package/client-ui/shared/conversation.css +229 -43
- package/client-ui/shared/conversation.js +1316 -131
- package/client-ui/shared/transport-http.js +164 -12
- package/client-ui-mobile/index.html +2168 -143
- package/internal/agent/capability.go +16 -2
- package/internal/agent/claude.go +3 -2
- package/internal/agent/codebuddy.go +7 -1
- package/internal/agent/codex_test.go +3 -0
- package/internal/agent/custom_models_test.go +5 -0
- package/internal/agent/dsh.go +18 -2
- package/internal/agent/dsh_test.go +53 -2
- package/internal/agent/engine_test.go +34 -19
- package/internal/agent/env_test.go +14 -9
- package/internal/agent/models_test.go +9 -0
- package/internal/agent/runcmd_test.go +24 -3
- package/internal/agent/stream.go +33 -0
- package/internal/cli/ask.go +94 -4
- package/internal/cli/contract_test.go +26 -0
- package/internal/cli/events.go +18 -7
- package/internal/cli/keepalive_test.go +69 -0
- package/internal/cli/requirements.go +119 -0
- package/internal/cli/requirements_test.go +36 -0
- package/internal/cli/root.go +56 -4
- package/internal/cli/session.go +64 -25
- package/internal/cli/sessionlog.go +76 -7
- package/internal/cli/sessionlog_test.go +58 -0
- package/internal/cli/team.go +374 -0
- package/internal/cli/team_session.go +156 -0
- package/internal/cli/team_test.go +360 -0
- package/internal/client/plugins.go +7 -4
- package/internal/client/requirements.go +309 -0
- package/internal/client/requirements_test.go +239 -0
- package/internal/client/service.go +545 -19
- package/internal/client/service_test.go +47 -2
- package/internal/client/session_events_test.go +320 -0
- package/internal/client/team_flags.go +204 -0
- package/internal/client/team_flags_test.go +254 -0
- package/internal/requirements/bootstrap.go +423 -0
- package/internal/requirements/consolidate.go +908 -0
- package/internal/requirements/doc.go +473 -0
- package/internal/requirements/memory.go +324 -0
- package/internal/requirements/requirements.go +169 -0
- package/internal/requirements/requirements_test.go +530 -0
- package/internal/requirements/store.go +289 -0
- package/internal/session/delete.go +8 -4
- package/internal/session/session.go +194 -17
- package/internal/session/session_append.go +4 -1
- package/internal/session/session_append_test.go +19 -0
- package/internal/session/session_mark_test.go +42 -3
- package/internal/session/session_test.go +43 -0
- package/internal/session/sessionlog.go +50 -34
- package/internal/session/sessionlog_read_test.go +69 -11
- package/internal/session/status.go +205 -0
- package/internal/session/status_test.go +144 -0
- package/internal/team/adaptive.go +293 -0
- package/internal/team/board.go +216 -0
- package/internal/team/event.go +28 -0
- package/internal/team/execute.go +648 -0
- package/internal/team/plan.go +903 -0
- package/internal/team/planner.go +294 -0
- package/internal/team/reduce.go +192 -0
- package/internal/team/role.go +334 -0
- package/internal/team/route.go +206 -0
- package/internal/team/schedule.go +241 -0
- package/internal/team/schedule_test.go +286 -0
- package/internal/team/subsession.go +27 -0
- package/internal/team/team.go +401 -0
- package/internal/team/team_test.go +1997 -0
- package/npm/bin/magic-client.js +0 -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/package.json +1 -1
|
@@ -49,10 +49,16 @@ function qaHasContent(){
|
|
|
49
49
|
function qaSyncChatMode(){ qaSetChatMode(qaHasContent()); qaDotsSync(); }
|
|
50
50
|
/* 用户**离开底部**(点圆点跳转 / 自己滚上去看历史)→ 后续流式增量不再把他拽回底部。
|
|
51
51
|
⚠️ 2026-10-20 用户:「滚动不了」—— 旧口径只认圆点跳转,手动上滚仍被每条增量拽回底部,
|
|
52
|
-
长答复流式期间根本没法看上面。现在以「是否贴近底部」为准(见
|
|
53
|
-
回到底部 / 发新消息即复位。
|
|
52
|
+
长答复流式期间根本没法看上面。现在以「是否贴近底部」为准(见 qaScrollGateBind 的监听)。
|
|
53
|
+
回到底部 / 发新消息即复位。
|
|
54
|
+
⚠️ 这个标记**只有绑了 qaScrollGateBind 才会被改动** —— 宿主不绑,它就恒为 false。 */
|
|
54
55
|
let qaUserAway = false;
|
|
55
56
|
function qaNearBottom(s){ return !!s && (s.scrollHeight - s.scrollTop - s.clientHeight) < 24; }
|
|
57
|
+
/** 画布**装得下比一屏更多**的内容吗 —— 手势要不要当帧关闸就看它。
|
|
58
|
+
* ⚠️ 不能用「此刻是否已离开底部」当门槛:手指刚抬起时 scrollTop 还贴在底部(浏览器要到
|
|
59
|
+
* 下一帧才滚),那一刻判 nearBottom 恒为真 → 手势永远关不掉闸,手机上还是会被拽回。
|
|
60
|
+
* 反过来也不能不判:内容不足一屏时(没得滚)若把手势当真,一次误触就再也不跟随新内容了。 */
|
|
61
|
+
function qaCanScroll(s){ return !!s && s.scrollHeight > s.clientHeight + 4; }
|
|
56
62
|
function qaScrollIntoView(){
|
|
57
63
|
if(qaPaintBg) return; // 后台 run 的增量不许拽动前台画布(见 qaPaintBg)
|
|
58
64
|
if(qaUserAway) return; // 用户跳上去看历史了,别抢他的滚动位置
|
|
@@ -201,11 +207,37 @@ function qaDotsFlash(msg){
|
|
|
201
207
|
* ⚠️ MutationObserver 只盯 childList(不盯 subtree):圆点数只跟顶层消息有关,
|
|
202
208
|
* 正文/工具卡内部怎么长都不影响 —— 少一层回调,流式期间也不至于每笔都刷。 */
|
|
203
209
|
function qaDotsBind(){
|
|
210
|
+
qaScrollGateBind(); // 圆点轨与滚动闸共用同一批监听(见下)
|
|
204
211
|
const s = $("#qa-stream");
|
|
205
|
-
if(!s || s.dataset.
|
|
206
|
-
s.dataset.
|
|
212
|
+
if(!s || s.dataset.dotsObserved === "1") return;
|
|
213
|
+
s.dataset.dotsObserved = "1";
|
|
207
214
|
let raf = 0;
|
|
208
215
|
const kick = ()=>{ if(raf) return; raf = requestAnimationFrame(()=>{ raf = 0; qaDotsSync(); }); };
|
|
216
|
+
new MutationObserver(kick).observe(s, { childList: true });
|
|
217
|
+
window.addEventListener("resize", kick);
|
|
218
|
+
qaDotsSync();
|
|
219
|
+
}
|
|
220
|
+
/** 滚动闸:**流式增量能不能把你拽回底部**,全靠这里维护的 `qaUserAway`。
|
|
221
|
+
*
|
|
222
|
+
* ⚠️ 2026-10-21 用户:「移动端对话框向上滑动看历史时总又自动跳回聚焦到结尾」。
|
|
223
|
+
* 根因不在判断逻辑,而在**没人绑**:这套监听原来只写在 qaDotsBind 里(圆点轨的一部分),
|
|
224
|
+
* 而 qaDotsBind 只有 magic-test 工作台调(`src/js/17-bind.js`);两个组件宿主
|
|
225
|
+
* (PC / 移动)引的是 `shared/conversation.js`,它从来没调过 —— 于是
|
|
226
|
+
* `qaUserAway` 恒为 false,`qaScrollIntoView()` 每一笔增量都 `scrollTop = scrollHeight`。
|
|
227
|
+
* 桌面端还能靠触控板把内容顶住(scroll 后下一笔增量又拽回,表现为「抖」),
|
|
228
|
+
* 手机上手指一松就是「跳到结尾」。
|
|
229
|
+
* 所以把它**从圆点轨里拆出来**独立成函数,由画布自己绑(见 conversation.js 的 create),
|
|
230
|
+
* 圆点轨只是顺带复用(qaDotsBind 里也调一次,幂等)。
|
|
231
|
+
*
|
|
232
|
+
* 三条手势各自为什么要有:真机上 scroll 事件是**下一帧**才来的,而流式增量可能在那之前
|
|
233
|
+
* 就贴了底 —— 只靠 scroll 监听仍会「刚滚上去又被拽回」。所以手势**当帧**就置位:
|
|
234
|
+
* · wheel(桌面滚轮 / 触控板);
|
|
235
|
+
* · touchmove(手机拖动)—— 移动端根本没有 wheel 事件,缺这条等于没有闸;
|
|
236
|
+
* · scroll(惯性滑动 / 程序滚动兜底,回到底部时复位)。 */
|
|
237
|
+
function qaScrollGateBind(){
|
|
238
|
+
const s = $("#qa-stream");
|
|
239
|
+
if(!s || s.dataset.scrollGate === "1") return;
|
|
240
|
+
s.dataset.scrollGate = "1";
|
|
209
241
|
s.addEventListener("scroll", ()=>{
|
|
210
242
|
/* 贴近底部 → 恢复「贴底」跟随;离开底部(手动上滚、点圆点、翻历史)→ 停止跟随,
|
|
211
243
|
否则流式每个增量都把用户拽回底部(2026-10-20「滚动不了」)。 */
|
|
@@ -214,17 +246,30 @@ function qaDotsBind(){
|
|
|
214
246
|
}, { passive: true });
|
|
215
247
|
/* 真人滚动手势 → 放掉「刚点过的那一轮」,高亮重新跟着视口走
|
|
216
248
|
(只认滚轮 / 触摸:平滑滚动的 scroll 事件不算,否则跳转自己就把 pin 冲掉了)。
|
|
217
|
-
⚠️ 上滚手势在这里**立刻**置 `qaUserAway
|
|
249
|
+
⚠️ 上滚手势在这里**立刻**置 `qaUserAway`(见上面的函数头):scroll 事件要到下一帧才来,
|
|
218
250
|
流式增量却可能在这之前就贴底 —— 只靠 scroll 监听仍有「刚滚上去又被拽回」的竞态。 */
|
|
219
251
|
const unpin = ()=>{ const r = $("#qa-dots"); if(r) r.__pin = null; };
|
|
220
252
|
s.addEventListener("wheel", (ev)=>{
|
|
221
|
-
if(ev && ev.deltaY < 0 && !qaNearBottom(s)) qaUserAway = true;
|
|
253
|
+
if(ev && ev.deltaY < 0 && qaCanScroll(s) && !qaNearBottom(s)) qaUserAway = true;
|
|
222
254
|
unpin(); qaDotsMark();
|
|
223
255
|
}, { passive: true });
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
/* 手机:手指往下拖 = 内容往上翻(`touchmove` 里比较上一次 Y 才判得出方向)。
|
|
257
|
+
⚠️ 门槛是「**装得下**能滚的内容」而不是「此刻已经离开底部」:手指一落的瞬间 scrollTop
|
|
258
|
+
还贴在底部(浏览器下一帧才滚),拿 nearBottom 当门槛的话手势永远关不掉闸。
|
|
259
|
+
⚠️ passive 不能省:这个监听**不阻止滚动**,只读坐标。 */
|
|
260
|
+
let lastTouchY = 0;
|
|
261
|
+
s.addEventListener("touchstart", (ev)=>{
|
|
262
|
+
const t = ev && ev.touches && ev.touches[0];
|
|
263
|
+
if(t) lastTouchY = t.clientY;
|
|
264
|
+
unpin(); qaDotsMark();
|
|
265
|
+
}, { passive: true });
|
|
266
|
+
s.addEventListener("touchmove", (ev)=>{
|
|
267
|
+
const t = ev && ev.touches && ev.touches[0];
|
|
268
|
+
if(!t) return;
|
|
269
|
+
/* 手指**向下**移动(clientY 变大)= 内容往上翻 = 在看历史 → 立刻停止跟随。 */
|
|
270
|
+
if(t.clientY - lastTouchY > 2 && qaCanScroll(s)) qaUserAway = true;
|
|
271
|
+
lastTouchY = t.clientY;
|
|
272
|
+
}, { passive: true });
|
|
228
273
|
}
|
|
229
274
|
/* ── 头像的两个**可覆盖默认值**(2026-10-17)────────────────────────────
|
|
230
275
|
它们是「内核有默认、宿主可在加载后覆盖」的那种符号,**不是**必须实现品:
|
|
@@ -298,6 +343,40 @@ function qaBotAvatarHtml(){
|
|
|
298
343
|
if(src) return `<span class="qa-av"><img class="qa-av-img" src="${escHtml(src)}" alt="" draggable="false"></span>`;
|
|
299
344
|
return `<span class="qa-av qa-av-bot" aria-hidden="true">${QA_AV_BOT_SVG}</span>`;
|
|
300
345
|
}
|
|
346
|
+
/* 模型 id → **展示用** id:剥掉 codebuddy 系自定义模型的 `custom-local:` 前缀。
|
|
347
|
+
(2026-10-21 用户:「codebuddy的custom-local的模型前缀不用显示」)
|
|
348
|
+
前缀只是 App / CLI 内部区分自定义端点的约定,对用户是噪点;`--engines` 的清单早就
|
|
349
|
+
剥过了(agent.StripCustomLocalPrefix),但**会话登记表 / start 事件 / 引擎 result 行**
|
|
350
|
+
里仍是完整 id —— 那些是传给 `-m` 与落库的值,不能改,只能在渲染这一层剥。
|
|
351
|
+
⚠️ 只用于显示:`-m`、任务落库、payload 一律仍用完整 id(见 codebuddyCore.modelArg)。 */
|
|
352
|
+
function qaModelText(id){
|
|
353
|
+
return String(id == null ? "" : id).trim().replace(/^custom-local:/, "");
|
|
354
|
+
}
|
|
355
|
+
/* 模型 → **展示用倍率**(`"×0.34"`);没有倍率(引擎不给数据 / 该模型不计费)→ `""`。
|
|
356
|
+
数据来自 `--engines` 的 `model_credits`(见 internal/cli/root.go):键与 `models[]` 同形,
|
|
357
|
+
都已剥过 `custom-local:`,所以这里也按 qaModelText 归一后再查。
|
|
358
|
+
PC 宿主在 glue 层把 snake_case 归一成了 `modelCredits`,两种拼写这里都认 —— 内核不知道
|
|
359
|
+
宿主拿的是哪一份原样 JSON。**不给倍率时返回空串而不是 "×1"**:编一个默认倍率等于替
|
|
360
|
+
引擎宣称计费口径,宁可不显示。
|
|
361
|
+
`engine` 给了就只查那一个引擎(不同引擎的同名模型倍率可能不同);没给就取清单里第一个
|
|
362
|
+
有值的 —— 给「这条消息用的模型」这类宿主没记引擎的场景兜底。 */
|
|
363
|
+
function qaModelCredit(engines, engine, id){
|
|
364
|
+
const key = qaModelText(id);
|
|
365
|
+
if(!key) return "";
|
|
366
|
+
const list = Array.isArray(engines) ? engines : [];
|
|
367
|
+
const read = (e)=>{
|
|
368
|
+
const map = (e && (e.model_credits || e.modelCredits)) || null;
|
|
369
|
+
if(!map || typeof map !== "object") return "";
|
|
370
|
+
const v = String(map[key] == null ? "" : map[key]).trim();
|
|
371
|
+
return v ? "×" + v : "";
|
|
372
|
+
};
|
|
373
|
+
if(engine){
|
|
374
|
+
for(const e of list) if(e && e.engine === engine) return read(e);
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
for(const e of list){ const c = read(e); if(c) return c; }
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
301
380
|
/* **引擎名小标**(2026-09-22 用户:「头像旁边显示引擎名字」;同日追加「名字显示的大一点
|
|
302
381
|
再加上模型显示」→ 文案 = 「引擎 · 模型」,字号见 app.css M8):左侧消息头像独占一行(见文末 M7)
|
|
303
382
|
之后,头像那一行右侧空着 —— 把「这一条是哪个引擎、哪个模型说的」挂在头像右边。
|
|
@@ -305,7 +384,8 @@ function qaBotAvatarHtml(){
|
|
|
305
384
|
见 app.css 的 `.qa-who` 只认左侧形状;⚠️ 审核引擎那条 2026-09-30 起也在左侧,但它的小标挂在
|
|
306
385
|
**机器侧那条独立消息**上,报告本体不重复挂 —— 见 13-dispatch-stream 与 qaTraceToLeft)。
|
|
307
386
|
空引擎名 → 空串:宁可没有,也不写「未知」占位。
|
|
308
|
-
模型存的是 **id**,显示走 qaModelName
|
|
387
|
+
模型存的是 **id**,显示走 qaModelName(与「生成中」胶囊同一套分工:名字只活在展示层);
|
|
388
|
+
自定义模型的 `custom-local:` 前缀在显示前经 qaModelText 剥掉。
|
|
309
389
|
⚠️ `modelOnly`(2026-10-20 用户:「只显示模型即可 不然现在换行了」):头像那一行上并着
|
|
310
390
|
「运行中 · 耗时计时」,而这一行是 `flex-flow:row wrap` —— 「引擎 · 模型」整串
|
|
311
391
|
(引擎名 + 模型显示名,两段都能很长)会把余下宽度吃光,秒数被挤到**换行**,
|
|
@@ -314,7 +394,7 @@ function qaWhoLabel(engine, model, modelOnly){
|
|
|
314
394
|
const nm = String(engine == null ? "" : engine).trim();
|
|
315
395
|
if(!nm) return "";
|
|
316
396
|
const md = String(model == null ? "" : model).trim();
|
|
317
|
-
const mName = md ? qaModelName(md, nm) : "";
|
|
397
|
+
const mName = md ? qaModelName(qaModelText(md), nm) : "";
|
|
318
398
|
if(modelOnly) return mName || nm;
|
|
319
399
|
return mName ? `${nm} · ${mName}` : nm;
|
|
320
400
|
}
|
|
@@ -383,6 +463,13 @@ function qaMsgRetry(msg){
|
|
|
383
463
|
function qaRenderMine(text, atts){
|
|
384
464
|
const s = $("#qa-stream"); if(!s) return;
|
|
385
465
|
s.hidden = false;
|
|
466
|
+
/* 空态那句「还没有事件」得先让位:它只在画布一条都还没有时该在。
|
|
467
|
+
⚠️ 以前只有**事件流**那条路径会摘它(conversation.js 的 ingest),而用户气泡
|
|
468
|
+
是宿主直接调这里画的、不经过事件流 —— 于是发出第一句之后,画布上同时挂着
|
|
469
|
+
「还没有事件。写一句任务…」和用户那句,看着像消息没进去(2026-10-21 截图)。
|
|
470
|
+
放在这里摘是**唯一**不会漏的位置:实时发送 / 排队提交 / 历史回放三条路径都经过它。 */
|
|
471
|
+
const empty = s.querySelector(".conv__empty");
|
|
472
|
+
if(empty) empty.remove();
|
|
386
473
|
const div = document.createElement("div");
|
|
387
474
|
div.className = "qa-msg qa-mine";
|
|
388
475
|
div.innerHTML = `<div class="qa-ico">${qaAvatarHtml("mine")}</div><div class="qa-bd"><div class="qa-t md-body"></div></div>`;
|
|
@@ -436,6 +523,9 @@ function qaRenderMine(text, atts){
|
|
|
436
523
|
否则他跳上去看历史之后发的那条会落在折线以下、看不见自己刚问的话。 */
|
|
437
524
|
qaUserAway = false;
|
|
438
525
|
qaScrollIntoView();
|
|
526
|
+
/* 返回这条消息的根节点:调用方要给它挂状态(比如排队中的 `is-queued`)。
|
|
527
|
+
⚠️ 老调用方一律不看返回值,加上它不影响任何人。 */
|
|
528
|
+
return div;
|
|
439
529
|
}
|
|
440
530
|
/** 「引擎 · 模型 · 生成中」= 1 枚胶囊(用户 2026-09-17):
|
|
441
531
|
* 转圈 + 文字同装一枚圆角胶囊(.qa-run-pill),实时生成行与派发快照的「生成中」状态行
|
|
@@ -758,7 +848,7 @@ function qaRenderAnswer(host){
|
|
|
758
848
|
|
|
759
849
|
const run = document.createElement("div");
|
|
760
850
|
run.className = "qa-run";
|
|
761
|
-
run.innerHTML = qaRunPillHtml(`${qaEngine}${qaModel ? " · " + qaModelName(qaModel, qaEngine) : ""} · 生成中`);
|
|
851
|
+
run.innerHTML = qaRunPillHtml(`${qaEngine}${qaModel ? " · " + qaModelName(qaModelText(qaModel), qaEngine) : ""} · 生成中`);
|
|
762
852
|
s.appendChild(run);
|
|
763
853
|
/* ⚠️ 落笔即贴底(2026-09-23,与 qaRenderMine / qaRenderDone 同一约定)。
|
|
764
854
|
为什么必须在**这一行之后**:这条应答的立身有两块 —— 气泡节点(上面那个 div)+ 这枚
|
|
@@ -1033,8 +1123,9 @@ function qaRenderAnswer(host){
|
|
|
1033
1123
|
t.count.textContent = " · " + t.num + "个"; // 实时更新计数
|
|
1034
1124
|
return t;
|
|
1035
1125
|
}
|
|
1036
|
-
/** 落定即收进本段的折叠组:只认 toolCall
|
|
1037
|
-
* (子代理内部卡 subTool
|
|
1126
|
+
/** 落定即收进本段的折叠组:只认 toolCall 落在**流里**的那些卡
|
|
1127
|
+
* (子代理内部卡 subTool 挂在父卡体内,不在流里;委派卡 agentCall 刻意不设
|
|
1128
|
+
* `__qaFlow`,它是里程碑、始终留在正文里,见那里的说明)。幂等。
|
|
1038
1129
|
* ⚠️ 归属组记在**节点属性**上(`__qaGroup`):这一笔跑完时 `curGroup` 可能早已换段清空,
|
|
1039
1130
|
* 靠它才能回到**本段**那个组,而不是现建一个新组(那会把这张卡甩到整条流的末尾)。 */
|
|
1040
1131
|
function collapseIntoGroup(card){
|
|
@@ -1187,7 +1278,9 @@ function qaRenderAnswer(host){
|
|
|
1187
1278
|
生命周期由 ② 的 system 事件驱动(start/progress/notify 只改胶囊);
|
|
1188
1279
|
最终结果仍按 tool_use id 配对写进「输出」(done)—— 没等到 task_notification
|
|
1189
1280
|
就收账时,胶囊在 done 里一并落定,不会留下常驻转圈。 */
|
|
1190
|
-
|
|
1281
|
+
/* chipText:初始胶囊文案(可选)。团队模式预开的卡里,等上游的子任务写
|
|
1282
|
+
「等待上游 t1」—— 比一律「等待启动」更能说明它为什么不动。 */
|
|
1283
|
+
function agentCall(name, argsPreview, argsFull, promptText, chipText){
|
|
1191
1284
|
// 委派卡的摘要本来就是 description(见调用处的 desc 取值)→ 按人话渲染
|
|
1192
1285
|
const t = mkCard(name, argsPreview, argsFull, true);
|
|
1193
1286
|
const card = t.card;
|
|
@@ -1195,7 +1288,7 @@ function qaRenderAnswer(host){
|
|
|
1195
1288
|
card.querySelector(".qa-tool-ico").innerHTML = QA_ICONS.agent;
|
|
1196
1289
|
const chip = document.createElement("span");
|
|
1197
1290
|
chip.className = "qa-agent-chip";
|
|
1198
|
-
chip.textContent = "等待启动";
|
|
1291
|
+
chip.textContent = chipText || "等待启动";
|
|
1199
1292
|
card.querySelector(".qa-tool-meta").prepend(chip);
|
|
1200
1293
|
const bdEl = card.querySelector(".qa-tool-bd");
|
|
1201
1294
|
if(promptText != null && String(promptText).length){
|
|
@@ -1212,28 +1305,55 @@ function qaRenderAnswer(host){
|
|
|
1212
1305
|
sub.innerHTML = `<div class="qa-sub-h">子代理活动</div>`;
|
|
1213
1306
|
bdEl.appendChild(sub);
|
|
1214
1307
|
/* 委派卡是里程碑:正文/思考在这一笔处封口,但**不封工具段** —— 它仍属于这一段的
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1308
|
+
「内部活动」,与前后的工具卡同处一段。
|
|
1309
|
+
⚠️ **不设 `__qaFlow`**(2026-10-21 用户:「应该子任务也在对话中可见,类似于引擎的
|
|
1310
|
+
subagent」):委派的是「一段活」,它的结局就是用户要看的结果,落定后必须留在
|
|
1311
|
+
正文里。于是 `collapseIntoGroup` 的 `__qaFlow` 闸门直接跳过它 —— 不进
|
|
1312
|
+
「已调用工具 · N个」,也不被 `segFold` / `wrapTurn` 搬走(那两个只认
|
|
1313
|
+
`.qa-think` / `.qa-tools` / `.qa-turn`)。普通工具卡维持原口径不变。 */
|
|
1219
1314
|
sealThink(); sealText();
|
|
1220
|
-
card.__qaFlow = true;
|
|
1221
|
-
card.__qaGroup = g;
|
|
1222
1315
|
bd.appendChild(card);
|
|
1223
1316
|
qaScrollIntoView();
|
|
1224
1317
|
const api = {
|
|
1225
1318
|
done(resultText, ok = true){
|
|
1226
1319
|
t.done(resultText, ok);
|
|
1227
|
-
|
|
1320
|
+
/* 通知缺失时兜底落定(完成 → jade 绿)。
|
|
1321
|
+
⚠️ 判据不能是「胶囊还在 run」(2026-10-21):团队模式的子任务由 core 合成,
|
|
1322
|
+
不走引擎的 task_started / task_progress,胶囊到收卡时可能还停在初始的
|
|
1323
|
+
「等待启动」—— 只认 run 的话那条一直写着「等待启动」,看起来像卡死。
|
|
1324
|
+
真正该跳过的只有「已经被 task_notification 明确落定过」的(下面打 settled 标)。 */
|
|
1325
|
+
if(chip.dataset.settled !== "1"){
|
|
1228
1326
|
chip.classList.remove("run");
|
|
1229
1327
|
if(ok) chip.classList.add("ok");
|
|
1230
1328
|
chip.textContent = ok ? "完成" : "未完成";
|
|
1231
1329
|
}
|
|
1232
1330
|
},
|
|
1233
1331
|
get state(){ return t.state; },
|
|
1234
|
-
|
|
1332
|
+
/* 子会话徽标(2026-10-21,团队模式):子任务自己的引擎会话 id。
|
|
1333
|
+
它与「完成/运行中」胶囊并排,说明这段活跑在哪个会话 —— 会话列表里不单列,
|
|
1334
|
+
但在这里看得见,历史回放时同样能复原。幂等:同一个 id 只更新不重复插。 */
|
|
1335
|
+
subSession(sid){
|
|
1336
|
+
const s = String(sid || "");
|
|
1337
|
+
if(!s) return;
|
|
1338
|
+
let el = card.querySelector(".qa-agent-sess");
|
|
1339
|
+
if(!el){
|
|
1340
|
+
el = document.createElement("span");
|
|
1341
|
+
el.className = "qa-agent-sess";
|
|
1342
|
+
chip.insertAdjacentElement("afterend", el);
|
|
1343
|
+
}
|
|
1344
|
+
if(el.dataset.sid === s) return;
|
|
1345
|
+
el.dataset.sid = s;
|
|
1346
|
+
el.textContent = "子会话 " + s.slice(0, 8);
|
|
1347
|
+
el.title = s;
|
|
1348
|
+
},
|
|
1349
|
+
/* 开跑 = 卡里开始有子代理的活动。核心脏路径(claude 系)由 system/task_started 驱动,
|
|
1350
|
+
但**团队模式**的子任务是 core 自己合成的、没有那套 system 事件 —— 由渲染层在
|
|
1351
|
+
第一条子事件到达时自己点亮(见 09-stream-apply.js 的 agStart),否则胶囊会一直
|
|
1352
|
+
写着「等待启动」直到收卡。落定之后不再回退(notify 打过 settled)。 */
|
|
1353
|
+
start(){ if(chip.dataset.settled === "1") return; chip.textContent = "运行中"; chip.classList.add("run"); },
|
|
1235
1354
|
progress(label){ if(chip.classList.contains("run")) chip.textContent = String(label || "进行中"); },
|
|
1236
1355
|
notify(label, ok = true){
|
|
1356
|
+
chip.dataset.settled = "1"; // 明确落定过 → done 不再改写它
|
|
1237
1357
|
chip.classList.remove("run");
|
|
1238
1358
|
if(ok) chip.classList.add("ok");
|
|
1239
1359
|
chip.textContent = String(label || (ok ? "完成" : "已结束"));
|
|
@@ -61,6 +61,11 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
61
61
|
const evParent = ev.parent != null ? String(ev.parent)
|
|
62
62
|
: (ev.parent_tool_use_id != null ? String(ev.parent_tool_use_id) : null);
|
|
63
63
|
const agOf = (id)=> id != null ? agents.get(id) : null;
|
|
64
|
+
/* 委派卡里**第一条子事件**到达 = 这次委派真的开跑了。
|
|
65
|
+
claude 系原生流由 system/task_started 驱动胶囊;而**团队模式**的子事件是 core 自己
|
|
66
|
+
合成的、没有那套 system 事件 —— 不在这里点亮的话,那张卡从头到尾写着「等待启动」,
|
|
67
|
+
用户看到的是「子任务好像卡死了」(2026-10-21 实测)。start() 自带幂等:已落定就退出。 */
|
|
68
|
+
const agStart = (ag)=>{ if(ag && typeof ag.start === "function") ag.start(); return ag; };
|
|
64
69
|
/* 会话锚点:**任何**带 session_id 的事件都当场认(2026-10-15)。
|
|
65
70
|
⚠️ 以前只在 ask / result / error / turn_failed / end / turn_end 这六条分支里认 ——
|
|
66
71
|
等于「等到终点事件才绑锚点」。而 core 在 `--events` 下一拿到 id 就**每行都带**
|
|
@@ -71,6 +76,14 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
71
76
|
历史回放不受影响:那条路径的 ctx.onSession 是空函数(宿主侧只读回放)。 */
|
|
72
77
|
const sidAny = ev.session_id || ev.sessionId;
|
|
73
78
|
if(sidAny && typeof ctx.onSession === "function") ctx.onSession(sidAny);
|
|
79
|
+
/* 子会话徽标(2026-10-21,团队模式):团队事件带 sub_session_id = 这个子任务**自己的**
|
|
80
|
+
引擎会话 id。它**不**是这条对话的锚点(锚点只认 session_id,别拿它调 onSession),
|
|
81
|
+
只把「这段活跑在哪个会话」标到父委派卡头上 —— 子会话绑定在主会话里一起显示/存档,
|
|
82
|
+
会话列表不单列。子事件一条条来,subSession 自己幂等。 */
|
|
83
|
+
if(ev.sub_session_id && evParent){
|
|
84
|
+
const agSs = agOf(evParent);
|
|
85
|
+
if(agSs && typeof agSs.subSession === "function") agSs.subSession(String(ev.sub_session_id));
|
|
86
|
+
}
|
|
74
87
|
const finishTools = (ok)=>{ // 流结束/失败时兜底:还在转圈的工具卡一律收账
|
|
75
88
|
ctx.tools.forEach(tc=>{ if(tc.state === "running") tc.done("", ok); });
|
|
76
89
|
ctx.tools.clear();
|
|
@@ -95,7 +108,7 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
95
108
|
const mdl = String(ev.model || "");
|
|
96
109
|
const retry = Number(ev.attempt || 0) > 0;
|
|
97
110
|
if(typeof h.setRunLabel === "function"){
|
|
98
|
-
h.setRunLabel(`${eng}${mdl ? " · " + qaModelName(mdl, eng) : ""} · 生成中`);
|
|
111
|
+
h.setRunLabel(`${eng}${mdl ? " · " + qaModelName(qaModelText(mdl), eng) : ""} · 生成中`);
|
|
99
112
|
}
|
|
100
113
|
/* 头像右边的「引擎 · 模型」小标同一时机跟成实际值(与胶囊同一条口径:
|
|
101
114
|
界面说 A、实际跑 B 不行)。机器侧 `.qa-trace`(审查轮 / 数字分身的活动行)
|
|
@@ -126,7 +139,7 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
126
139
|
}
|
|
127
140
|
case "thinking":
|
|
128
141
|
// ③ 子代理思考 → 父卡嵌套区注脚;主会话思考 → 思考区
|
|
129
|
-
if(evParent && agOf(evParent)) agOf(evParent).subNote(ev.text || "");
|
|
142
|
+
if(evParent && agOf(evParent)) agStart(agOf(evParent)).subNote(ev.text || "");
|
|
130
143
|
else {
|
|
131
144
|
h.appendThinking(ev.text || "");
|
|
132
145
|
/* 过程步骤(2026-09-23):主会话思考也记进台账 —— 收尾时随消息一起落库,
|
|
@@ -137,7 +150,7 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
137
150
|
break;
|
|
138
151
|
case "text":
|
|
139
152
|
// ③ 子代理正文 → 父卡嵌套区;主会话正文 → 逐字正文
|
|
140
|
-
if(evParent && agOf(evParent)) agOf(evParent).subText(ev.text || "");
|
|
153
|
+
if(evParent && agOf(evParent)) agStart(agOf(evParent)).subText(ev.text || "");
|
|
141
154
|
else {
|
|
142
155
|
h.appendText(ev.text || "");
|
|
143
156
|
/* 消息台账(2026-09-22):主会话正文逐段累加进 run.msgText —— 收尾时它就是要落进
|
|
@@ -177,17 +190,24 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
177
190
|
「这一轮工具全带 description」时留下一个「已调用工具 · 0个」的空组。 */
|
|
178
191
|
const tid = ev.tid != null ? ev.tid : ev.id;
|
|
179
192
|
const argsText = ev.text || ev.args || "";
|
|
180
|
-
const ag = agOf(evParent);
|
|
193
|
+
const ag = evParent ? agStart(agOf(evParent)) : null;
|
|
181
194
|
/* 提问卡判定:qaAskParse 只跑一次(形状不稳、解析不便宜,下面还要用结果) */
|
|
182
195
|
const askQs = (typeof h.askCard === "function" && qaIsAskTool(ev.name)) ? qaAskParse(argsText) : null;
|
|
183
196
|
if(/^agent$|^task$/i.test(String(ev.name || "")) && !ag){
|
|
184
197
|
// ① 主会话委派:专属卡(任务词 + 状态胶囊 + 嵌套区),按 id 登记
|
|
185
198
|
let o = ev.inputObj || null;
|
|
186
199
|
if(!o && argsText){ try { o = JSON.parse(argsText); } catch(e2){ /* 非 JSON 参数只显示原文 */ } }
|
|
200
|
+
/* 团队模式的第二条同 id tool_use = **开跑信号**(core 在计划确定时就为全部子任务
|
|
201
|
+
预开了卡;等上游的那些先显示「等待上游 tN」)。命中已有卡只点亮它 ——
|
|
202
|
+
重画的话一个子任务会变成两张卡。 */
|
|
203
|
+
const prev = tid != null ? agents.get(String(tid)) : null;
|
|
204
|
+
if(prev){ agStart(prev); break; }
|
|
187
205
|
const desc = (o && (o.description || o.subagent_type)) || ev.preview || "";
|
|
188
206
|
const prompt = (o && (o.prompt || o.task || o.promptText)) || "";
|
|
207
|
+
const deps = (o && Array.isArray(o.deps)) ? o.deps.map(String).filter(Boolean) : [];
|
|
208
|
+
const chipText = deps.length ? ("等待上游 " + deps.join(" / ")) : "";
|
|
189
209
|
const card = typeof h.agentCall === "function"
|
|
190
|
-
? h.agentCall(ev.name || "Agent", String(desc || ""), argsText, String(prompt))
|
|
210
|
+
? h.agentCall(ev.name || "Agent", String(desc || ""), argsText, String(prompt), chipText)
|
|
191
211
|
: h.toolCall(ev.name || "Agent", ev.preview || "", argsText); // 旧手柄无 agentCall → 退普通卡
|
|
192
212
|
if(tid != null){ ctx.tools.set(tid, card); agents.set(String(tid), card); }
|
|
193
213
|
}else if(ag && typeof ag.subTool === "function"){
|
|
@@ -249,7 +269,11 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
249
269
|
// 期望载荷: { tid|id, text(结果串,可缺省), ok(布尔,缺省 true) }
|
|
250
270
|
const tid = ev.tid != null ? ev.tid : ev.id;
|
|
251
271
|
const tc = tid != null ? ctx.tools.get(tid) : null;
|
|
252
|
-
if(tc)
|
|
272
|
+
if(tc){
|
|
273
|
+
tc.done(qaToolOutForDisplay(ev.text || ""), ev.ok !== false);
|
|
274
|
+
/* 收卡兜底再补一次子会话徽标(渲染开始前的子事件可能没赶上,回放时尤其如此)。 */
|
|
275
|
+
if(ev.sub_session_id && typeof tc.subSession === "function") tc.subSession(String(ev.sub_session_id));
|
|
276
|
+
}
|
|
253
277
|
else if(evParent && agOf(evParent) && typeof agOf(evParent).subNote === "function")
|
|
254
278
|
agOf(evParent).subNote("结果回执:" + String(ev.text || "").slice(0, 400)); // ③ 无卡可填 → 父卡注脚
|
|
255
279
|
else if(typeof h.marker === "function"){
|
|
@@ -383,6 +407,31 @@ function qaStreamApplyOne(ev, ctx){
|
|
|
383
407
|
detail: Object.assign({}, ev, { kind: "notice" }), status: ev.status || "" });
|
|
384
408
|
else if(typeof h.note === "function") h.note(String(ev.text || ev.notice || ""));
|
|
385
409
|
break;
|
|
410
|
+
/* 团队模式的任务列表(KindTasks,见 internal/team/board.go):一条团队会话里**唯一**那张任务卡。
|
|
411
|
+
快照语义 —— core 每次任务状态变化都把整份清单重发一次(`text` = {"tasks":[…]}),
|
|
412
|
+
这里只做「整块换内容」:h.todoList 原地更新同一块面板(见 03-chat-message),
|
|
413
|
+
不增量拼接、也不会变成「一个子任务一张卡」。
|
|
414
|
+
⚠️ 状态词表是 core 侧的 pending / running / done / failed / skipped,
|
|
415
|
+
画面统一用任务列表的 is-* 类(running 就是任务列表里的 in_progress)。
|
|
416
|
+
⚠️ 以前没有这条分支 → 掉进 default 被画成「未识别事件 · tasks」。 */
|
|
417
|
+
case "tasks": {
|
|
418
|
+
let snap = null;
|
|
419
|
+
try { snap = JSON.parse(ev.text || ""); } catch(_) { snap = null; }
|
|
420
|
+
const arr = (snap && Array.isArray(snap.tasks)) ? snap.tasks : [];
|
|
421
|
+
const rows = arr.filter(t=>t && typeof t === "object").map(t=>{
|
|
422
|
+
const st = String(t.status || "pending").toLowerCase();
|
|
423
|
+
return {
|
|
424
|
+
text: String(t.title || t.id || ""),
|
|
425
|
+
status: st === "running" ? "in_progress"
|
|
426
|
+
: st === "done" ? "completed"
|
|
427
|
+
: (st === "failed" || st === "skipped") ? st : "pending",
|
|
428
|
+
/* 行尾标注「引擎 · 角色」:主会话的任务列表卡要能一眼看出这一行派给了谁 */
|
|
429
|
+
meta: [t.engine, t.role || t.kind].filter(Boolean).join(" · "),
|
|
430
|
+
};
|
|
431
|
+
}).filter(r=>r.text);
|
|
432
|
+
if(typeof h.todoList === "function") h.todoList(rows);
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
386
435
|
/* 模型产出的图片(KindImage,见 internal/agent/stream.go):`text` 就是图片本体
|
|
387
436
|
`data:<media_type>;base64,…`。图算**正文的一部分**(对齐 h.appendImage 的约定:
|
|
388
437
|
插在当前正文块的打字预览之前,图后到的文字自然排在图下)。
|
|
@@ -183,7 +183,9 @@ async function attSave(a){
|
|
|
183
183
|
let item = null;
|
|
184
184
|
if(window.desk && typeof window.desk.pasteImage === "function"){
|
|
185
185
|
const buf = await a.blob.arrayBuffer();
|
|
186
|
-
|
|
186
|
+
/* type 一并给桥:压缩后可能是 WebP/JPEG(见 attCompress),落盘端要靠它选扩展名。
|
|
187
|
+
老桥忽略多出来的字段,行为不变。 */
|
|
188
|
+
const r = await window.desk.pasteImage({ buf, type: a.blob.type || "image/png", label: "paste" });
|
|
187
189
|
if(!r || !r.ok) throw new Error((r && r.error) || "写入本机失败");
|
|
188
190
|
item = r.item;
|
|
189
191
|
}else{
|
|
@@ -412,6 +414,18 @@ function qaArtBase(){
|
|
|
412
414
|
}catch(e){ /* 拿不到就当没有基准 */ }
|
|
413
415
|
return "";
|
|
414
416
|
}
|
|
417
|
+
/** 取宿主桥函数。`window.desk` 可能是**冻结**的宿主桥(桌面壳 preload 注入的那份),
|
|
418
|
+
* 宿主补不进去的方法由 conversation.js 的 installDeskProbe 挪到 `window.__qaDeskExtra`
|
|
419
|
+
* —— 两处都问一遍,冻结与否都能拿到通道。 */
|
|
420
|
+
function qaDeskFn(name){
|
|
421
|
+
try{
|
|
422
|
+
if(typeof window !== "undefined" && window.desk && typeof window.desk[name] === "function") return window.desk[name];
|
|
423
|
+
const x = (typeof window !== "undefined") ? window.__qaDeskExtra : null;
|
|
424
|
+
if(x && typeof x[name] === "function") return x[name];
|
|
425
|
+
}catch(e){ /* 通道不通 -> null */ }
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
415
429
|
/** 落盘地址:宿主给了回读通道(`window.desk.mediaUrl`,组件宿主走 /desk/media)就用它;
|
|
416
430
|
* 桌面壳的页面是 `file://` → 拼 `file://`(与 qaShotSrc 同一招);
|
|
417
431
|
* 产物库里的(`~/.magic-test/images|pastes/…`)走内置后端,预览态也通。 */
|
|
@@ -419,9 +433,10 @@ function qaArtUrl(abs){
|
|
|
419
433
|
const p = String(abs || "");
|
|
420
434
|
if(!p) return "";
|
|
421
435
|
try{
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
const u = qaDeskFn("mediaUrl");
|
|
437
|
+
if(u){
|
|
438
|
+
const u2 = u(p);
|
|
439
|
+
if(u2) return u2;
|
|
425
440
|
}
|
|
426
441
|
}catch(e){ /* 宿主通道不通就退下面的 */ }
|
|
427
442
|
if(typeof qaShotSrc === "function"){
|
|
@@ -645,7 +660,7 @@ function qaArtNode(it){
|
|
|
645
660
|
* 返回画出来的张数(Promise):收尾那条链要**等它画完**再落 HTML 快照(见 qaTurnArtsMount)。 */
|
|
646
661
|
function qaArtMount(host, paths, opts){
|
|
647
662
|
const o = opts || {};
|
|
648
|
-
const api = (
|
|
663
|
+
const api = qaDeskFn("pathProbe");
|
|
649
664
|
if(!host || !api || !Array.isArray(paths) || !paths.length) return Promise.resolve(0);
|
|
650
665
|
const list = paths.slice(0, 12);
|
|
651
666
|
return Promise.resolve(api({ base: qaArtBase(), paths: list })).then((r)=>{
|
|
@@ -680,6 +695,46 @@ function qaArtMount(host, paths, opts){
|
|
|
680
695
|
}).catch(()=> 0); // 探针失败 = 当作「都不是产物」,正文照旧
|
|
681
696
|
}
|
|
682
697
|
|
|
698
|
+
/* ── 破图收尾(2026-10-20 用户:「是图片不存在吗 不存在的不显示」)────────────
|
|
699
|
+
探针只说「这会儿在盘上」,图却仍可能取不回来:手机连的是另一台/另一个实例、
|
|
700
|
+
快照里存的是换过的旧令牌、文件在渲染后被删。别留一张 alt + 碎图标的空卡:
|
|
701
|
+
① 先用**当下的**回读地址重试一次(快照里的 src 可能带的是旧令牌 / 旧 origin);
|
|
702
|
+
② 还失败就把这张卡摘掉;行空了连行一起摘,只剩「本轮产物」标题也把标题摘掉。
|
|
703
|
+
`error` 不冒泡,所以监听挂在 document 的**捕获阶段**(一次,页面级)。
|
|
704
|
+
快照恢复走 `innerHTML = snapshot`,内联 onerror 会随序列化丢掉,捕获监听不会。 */
|
|
705
|
+
function qaArtDrop(card){
|
|
706
|
+
const row = card && card.parentNode;
|
|
707
|
+
if(row) row.removeChild(card);
|
|
708
|
+
if(!row || !row.classList || !row.classList.contains("qa-arts") || row.children.length) return;
|
|
709
|
+
const cap = row.previousElementSibling;
|
|
710
|
+
const host = row.parentNode;
|
|
711
|
+
if(host) host.removeChild(row);
|
|
712
|
+
if(cap && cap.classList && cap.classList.contains("qa-arts-cap") && host && !host.querySelector(".qa-arts")){
|
|
713
|
+
host.removeChild(cap); // 标题下已经一张卡都不剩 → 标题也摘
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
(function(){ // 页面级一次性挂载(不占全局名)
|
|
718
|
+
if(typeof document === "undefined" || window.__qaArtErrSweep) return;
|
|
719
|
+
window.__qaArtErrSweep = true;
|
|
720
|
+
document.addEventListener("error", function(e){
|
|
721
|
+
const im = e && e.target;
|
|
722
|
+
if(!im || im.tagName !== "IMG" || !im.closest) return;
|
|
723
|
+
const card = im.closest(".qa-art-image"); // 只管产物卡;正文缩略图不在此列
|
|
724
|
+
if(!card) return;
|
|
725
|
+
const p = card.dataset.path;
|
|
726
|
+
if(p && !im.dataset.qaRetry && typeof qaArtUrl === "function"){
|
|
727
|
+
const u = qaArtUrl(p);
|
|
728
|
+
if(u && u !== im.getAttribute("src")){
|
|
729
|
+
im.dataset.qaRetry = "1";
|
|
730
|
+
im.src = u;
|
|
731
|
+
return; // 换新地址再试一次
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
qaArtDrop(card); // 真取不回来 → 不显示
|
|
735
|
+
}, true);
|
|
736
|
+
})();
|
|
737
|
+
|
|
683
738
|
/* ── 会话产物 · 消息末尾的「本轮产物」(2026-10-15)─────────────────────────
|
|
684
739
|
用户原话:「会话有新的图片、视频文件产生 应该在消息结束时附加在末尾作为会话产物显示预览」。
|
|
685
740
|
正文里写出来的产物路径早就有渲染(qaShotsMount / qaArtMount:图变缩略图、视频变就地播放器),
|
|
@@ -862,9 +917,14 @@ function qaShotsMount(host, text){
|
|
|
862
917
|
qaArtMount(host, qaArtCollect(rest));
|
|
863
918
|
}
|
|
864
919
|
|
|
865
|
-
/** 附件条 + 粘贴 / 拖拽接线(逐实例各绑一次,与 + 菜单同款)
|
|
920
|
+
/** 附件条 + 粘贴 / 拖拽接线(逐实例各绑一次,与 + 菜单同款)
|
|
921
|
+
* ⚠️ 幂等:`askBoxMountAll` 会调它,宿主也可能自己再调一次(magic-test 的 bindHomeEntry)。
|
|
922
|
+
* 重复绑定的症状不是报错,而是**一次粘贴挂两张图**(每份监听各跑一遍)——
|
|
923
|
+
* 用 DOM 上的标记挡住,比要求所有调用方都记得「只能调一次」可靠。 */
|
|
866
924
|
function askAttBind(p){
|
|
867
925
|
const box = $("#" + p + "-box"); if(!box) return;
|
|
926
|
+
if(box.dataset.attBound === "1") return;
|
|
927
|
+
box.dataset.attBound = "1";
|
|
868
928
|
const row = $("#" + p + "-atts");
|
|
869
929
|
if(row){
|
|
870
930
|
row.addEventListener("click", (e)=>{
|
|
@@ -998,6 +1058,10 @@ function askBoxMountAll(){
|
|
|
998
1058
|
host.innerHTML = askBoxMarkup(ASK_CFG[p]);
|
|
999
1059
|
askAddMenuBuild(p); // 先填一次,保证 DOM 里始终有菜单(含引擎提示行)
|
|
1000
1060
|
askChipsSync(p);
|
|
1061
|
+
/* 粘贴 / 拖拽的接线**必须在挂载时就绑上**(2026-10-21:`askAttBind` 此前没有任何调用点,
|
|
1062
|
+
于是这条 UI 的「粘贴截图」一直是死代码 —— 用户:「对话框中现在没支持粘贴图片」)。
|
|
1063
|
+
幂等,见函数自己的注释。 */
|
|
1064
|
+
askAttBind(p);
|
|
1001
1065
|
});
|
|
1002
1066
|
}
|
|
1003
1067
|
|
|
@@ -263,6 +263,18 @@ function qaSessionPlayer(){
|
|
|
263
263
|
}catch(e){ /* 收尾失败不影响回放 */ }
|
|
264
264
|
h = null;
|
|
265
265
|
},
|
|
266
|
+
/** 把这半轮**交出去**(2026-10-21 用户:「同一轮对话 显示成了两个」)。
|
|
267
|
+
* 场景:内存闸门把这一轮的 `user` 起点截掉了(`openCut`),窗口里整段都属于**还在长的一轮** ——
|
|
268
|
+
* 回放已经把它画在同一张还没封口的卡上。宿主如果照旧 `finish()` 封口、再让续传
|
|
269
|
+
* `qaRenderAnswer()` 另开一张,同一轮就会变成两张卡(过程组孤零零留在第一张)。
|
|
270
|
+
* 这里把在画的句柄连同配对表 / 产物收集器 / 起算时间一起交出,本 player 不再持有任何一轮;
|
|
271
|
+
* 续传接着往**同一张卡**上写,一轮只有一张。 */
|
|
272
|
+
handoff(){
|
|
273
|
+
if(!h) return null;
|
|
274
|
+
const keep = { h, tools, agents, arts, t0 };
|
|
275
|
+
h = null;
|
|
276
|
+
return keep;
|
|
277
|
+
},
|
|
266
278
|
};
|
|
267
279
|
/* 拆成内层函数:feed 里原来的 `return`(qaRenderAnswer 失败)只退出这一批,语义与改造前一致 */
|
|
268
280
|
function qaSessionPlayerFeed(list){
|
|
@@ -340,6 +352,12 @@ function qaSessionPlayer(){
|
|
|
340
352
|
null, turnMs || null);
|
|
341
353
|
if(arts) arts.flush(h); // 终态之后这一轮的产物补在消息末尾(与实时 onDone 同位)
|
|
342
354
|
t0 = 0; // 这一轮收尾了:下一轮(若没有 user 事件)重新起算
|
|
355
|
+
/* ⚠️ 句柄必须一起置空(2026-10-21 用户:「同一轮对话 显示成了两个」):
|
|
356
|
+
收尾后它已经是**封口**的卡(`closed = true`、过程块已折进组里),再往它上面喂事件
|
|
357
|
+
`ensureText()` 只会在队尾新开一块正文 —— 同一轮的后续正文就落在收尾行下面,与续传
|
|
358
|
+
另起的那张卡并排,看起来就是「一轮两卡 + 正文重复」。置空后下一批事件按新一轮开卡,
|
|
359
|
+
与宿主续传(qaTailFeed 的终态分支)同口径。 */
|
|
360
|
+
h = null;
|
|
343
361
|
}
|
|
344
362
|
}
|
|
345
363
|
}
|
|
@@ -510,11 +510,21 @@ function qaTodoFill(host, rows, opts){
|
|
|
510
510
|
ico.className = "qa-todo-ico";
|
|
511
511
|
if(t.status === "completed") ico.innerHTML = QA_V_ICONS.ok;
|
|
512
512
|
else if(t.status === "in_progress") ico.innerHTML = QA_V_ICONS.clock;
|
|
513
|
+
/* 团队模式的任务列表(KindTasks)多两档终态:失败要说出来、跳过要看得出来
|
|
514
|
+
(以前只认 completed / in_progress,这两个状态会退化成「空圈 = 没开始」)。 */
|
|
515
|
+
else if(t.status === "failed") ico.innerHTML = QA_V_ICONS.alert;
|
|
513
516
|
const txt = document.createElement("span");
|
|
514
517
|
txt.className = "qa-todo-t";
|
|
515
518
|
txt.textContent = (t.status === "in_progress" && t.active) ? t.active : t.text;
|
|
516
519
|
r.appendChild(ico);
|
|
517
520
|
r.appendChild(txt);
|
|
521
|
+
/* 行尾标注(可选,`引擎 · 角色`):只有团队任务列表给这一档,待办清单没有 */
|
|
522
|
+
if(t.meta){
|
|
523
|
+
const mt = document.createElement("span");
|
|
524
|
+
mt.className = "qa-todo-meta";
|
|
525
|
+
mt.textContent = String(t.meta);
|
|
526
|
+
r.appendChild(mt);
|
|
527
|
+
}
|
|
518
528
|
list.appendChild(r);
|
|
519
529
|
});
|
|
520
530
|
host.appendChild(list);
|
|
@@ -37,6 +37,12 @@ const convShards = JSON.parse(fs.readFileSync(path.join(convDir, 'manifest.json'
|
|
|
37
37
|
`wsDirOf` / `qaWorkspace` / `reqHas` …)。另有两个**可选**宿主能力不在清单里(缺了只是不画产物卡):
|
|
38
38
|
`window.desk.pathProbe` 与 `window.desk.mediaUrl`。
|
|
39
39
|
|
|
40
|
+
**画布挂好之后必须调一次 `qaScrollGateBind()`**(幂等)。它维护「用户正在上翻历史」的闸
|
|
41
|
+
(`qaUserAway`)—— 流式增量能不能把你拽回底部全靠它。组件宿主(`shared/conversation.js`)
|
|
42
|
+
已在 `create()` 里调过;工作台是在 `qaDotsBind()` 里顺带调。**不调的表现**:手机上一上滑就
|
|
43
|
+
被下一笔增量弹回结尾(2026-10-21 就是这么坏了一个版本 —— 那段监听原本只写在圆点轨里,
|
|
44
|
+
而圆点轨只有工作台用)。
|
|
45
|
+
|
|
40
46
|
### 命名与「半个文件」的规矩
|
|
41
47
|
|
|
42
48
|
- 被整份搬走的(`03` `06` `08` `19` `29`):内核分片沿用原文件名。
|