统一会话展示与专家路由行为,修复图片异步显示与气泡宽度问题。

本次移除 image 专家角色并同步更新路由/绑定/测试,同时增强 session-bootstrap 的共享与角色分层记忆加载,减少刷新依赖并提升默认可用性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-29 22:16:49 +08:00
parent 1ebfa47dd3
commit d5e30542aa
19 changed files with 199 additions and 62 deletions

View file

@ -3445,6 +3445,7 @@ async function renderChatUi() {
let hasRealStreamText = false;
let sawWsChatEvent = false;
let sawWsTerminalEvent = false;
let sawStreamToolRefAttachments = false;
let markerState = { p: 0, e: false, ep: 0, t: 0, s: 0, k: 0, reclaimed: 0 };
let turnAcceptedAtMs = null;
let phaseRunningAtMs = null;
@ -3796,6 +3797,31 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
const body = formatToolPanelText(name, rawPayload, { streamMode: true });
const sqlAudit = extractSqlAuditPayload(rawPayload);
const images = extractToolImageItems(rawPayload);
const _containsRefAttachments = (obj, depth = 0) => {
if (!obj || typeof obj !== "object" || depth > 4) return false;
// Common case: { attachments: [...] }
if (Array.isArray(obj.attachments)) {
for (const att of obj.attachments) {
if (!att || typeof att !== "object") continue;
const typ = String(att.type || "").toLowerCase();
if (!typ) continue;
if (typ.endsWith("_ref") || typ === "relay_pointer") return true;
}
}
// Recurse a few common container keys first.
const keys = ["result", "payload", "message", "data", "output"];
for (const k of keys) {
if (k in obj && _containsRefAttachments(obj[k], depth + 1)) return true;
}
// Shallow scan other object values (bounded).
let scanned = 0;
for (const v of Object.values(obj)) {
if (scanned++ > 12) break;
if (_containsRefAttachments(v, depth + 1)) return true;
}
return false;
};
if (_containsRefAttachments(rawPayload)) sawStreamToolRefAttachments = true;
chatStreamSegments.push({ type: "tool", key, title: `${name} ${liveTag}`, body, sqlAudit, images });
};
const appendFinalAssistant = async (message, fallbackText) => {
@ -3954,6 +3980,15 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
renderStreamComposite();
_markStreamTerminal("end", t("chat.status.end"));
ok = true;
} else if (sawStreamToolRefAttachments) {
// Streaming UI cannot render image_ref/relay_pointer; recover from persisted history so images appear
// without requiring a manual refresh.
try {
if (streamRow && streamRow.parentNode) streamRow.remove();
} catch (_) {}
await loadMessagesForActive();
scrollMessagesToBottom(true);
ok = true;
} else {
ok = await appendFinalAssistant(payload.message, chatStream || extractWsAssistantText(payload.message || {}));
}
@ -3961,6 +3996,7 @@ ${autoLimit ? `<div style="margin-top:8px;"><span class="muted">auto-added claus
chatStream = "";
streamTextBuffer = "";
chatRunId = null;
sawStreamToolRefAttachments = false;
const streamedEnough = hasRealStreamText || chatStreamSegments.length > 0 || sawWsChatEvent;
turnFinalized = true;
turnStreamedEnough = streamedEnough;

View file

@ -471,7 +471,9 @@ body.theme-ds-body .card {
/* 与 .chat-avatar-slot 48px + .chat-row gap 10px 一致 */
.chat-msg-col--assistant {
width: min(max(0px, calc(50% + 2cm - 58px)), 100%);
/* 宽度随容器自适应,但最宽限制为 50% + 2cm(扣除头像轨 58px)。 */
flex: 1 1 auto;
width: auto;
max-width: min(max(0px, calc(50% + 2cm - 58px)), 100%);
}