Persist chat loading state across task switches
This commit is contained in:
parent
7a0d03fb29
commit
ea89c5f209
|
|
@ -350,7 +350,22 @@
|
||||||
.cancelled-badge { margin-top: 8px; padding: 4px 10px; font-size: 12px; color: var(--accent); background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--r-md); display: inline-block; }
|
.cancelled-badge { margin-top: 8px; padding: 4px 10px; font-size: 12px; color: var(--accent); background: var(--accent-soft); border: 1px dashed var(--accent); border-radius: var(--r-md); display: inline-block; }
|
||||||
.msg .role { font-size: 11px; color: var(--muted); margin-bottom: 2px; font-family: var(--mono); }
|
.msg .role { font-size: 11px; color: var(--muted); margin-bottom: 2px; font-family: var(--mono); }
|
||||||
.msg .body { word-wrap: break-word; font-size: 14px; line-height: 1.55; }
|
.msg .body { word-wrap: break-word; font-size: 14px; line-height: 1.55; }
|
||||||
.msg .body.streaming::after { content: "▌"; color: var(--accent); animation: blink 1s infinite; }
|
.msg.assistant.live-run { border-color: rgba(220, 38, 38, 0.28); box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.08), 0 8px 24px rgba(220, 38, 38, 0.08); }
|
||||||
|
.msg .body.streaming { min-width: 96px; min-height: 22px; }
|
||||||
|
.msg .body.streaming:empty::before { content: "思考中"; color: var(--muted); }
|
||||||
|
.msg .body.streaming::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.15em;
|
||||||
|
height: 1.15em;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: -0.18em;
|
||||||
|
border: 2px solid rgba(220, 38, 38, 0.18);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
@keyframes blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } }
|
@keyframes blink { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } }
|
||||||
/* markdown 输出:.msg .body 与 file-preview .md-render 共用一组规则 */
|
/* markdown 输出:.msg .body 与 file-preview .md-render 共用一组规则 */
|
||||||
.msg .body > :first-child, .md-render > :first-child { margin-top: 0; }
|
.msg .body > :first-child, .md-render > :first-child { margin-top: 0; }
|
||||||
|
|
@ -1039,7 +1054,8 @@ const state = {
|
||||||
// 同 wd 内除自己外其他活跃 task(run_status in running/cancelling),供 banner 显示
|
// 同 wd 内除自己外其他活跃 task(run_status in running/cancelling),供 banner 显示
|
||||||
concurrentWarnings: [],
|
concurrentWarnings: [],
|
||||||
evtSrc: null,
|
evtSrc: null,
|
||||||
streaming: false, // 当前是否在流式中;true 时显示 stop 按钮
|
streaming: false, // 兼容旧判断:任一 task 是否在流式中
|
||||||
|
liveRuns: new Map(), // task_id -> 当前浏览器会话内运行中的回复卡/累计文本
|
||||||
// task list 滚动加载 + 筛选
|
// task list 滚动加载 + 筛选
|
||||||
taskPage: 0, // 已加载到的最后一页(0 = 未加载)
|
taskPage: 0, // 已加载到的最后一页(0 = 未加载)
|
||||||
taskPageSize: 20,
|
taskPageSize: 20,
|
||||||
|
|
@ -1703,6 +1719,11 @@ async function selectTask(tid) {
|
||||||
state.taskMeta = meta;
|
state.taskMeta = meta;
|
||||||
renderChatMeta();
|
renderChatMeta();
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
|
if (meta.run_status === "running" || meta.run_status === "cancelling") {
|
||||||
|
ensureRunningTaskSubscribed(tid, `/v1/tasks/${tid}/events`);
|
||||||
|
} else {
|
||||||
|
renderLiveRunIfVisible();
|
||||||
|
}
|
||||||
// 文件面板自动跳到该 task 的 working_dir(user_root 下一级子目录),
|
// 文件面板自动跳到该 task 的 working_dir(user_root 下一级子目录),
|
||||||
// 不强绑定 — 用户可点 crumb 回上层看 user_root 其他目录
|
// 不强绑定 — 用户可点 crumb 回上层看 user_root 其他目录
|
||||||
const wdName = meta.working_dir ? meta.working_dir.split("/").filter(Boolean).pop() : "";
|
const wdName = meta.working_dir ? meta.working_dir.split("/").filter(Boolean).pop() : "";
|
||||||
|
|
@ -1859,11 +1880,68 @@ async function loadMessages() {
|
||||||
renderMessages(data.messages);
|
renderMessages(data.messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLiveRun(taskId) {
|
||||||
|
return taskId ? state.liveRuns.get(taskId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCurrentTaskStreaming() {
|
||||||
|
return !!getLiveRun(state.taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLiveAssistantCard(run) {
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "msg assistant live-run";
|
||||||
|
card.innerHTML = `<div class="role">助手</div><div class="body streaming">${run.acc ? renderMd(run.acc) : ""}</div>`;
|
||||||
|
run.card = card;
|
||||||
|
run.body = card.querySelector(".body");
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLiveRunIfVisible() {
|
||||||
|
const run = getLiveRun(state.taskId);
|
||||||
|
if (!run) {
|
||||||
|
setActionMode("idle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wrap = $("chat-stream");
|
||||||
|
const card = run.card || createLiveAssistantCard(run);
|
||||||
|
if (run.body && run.acc) run.body.innerHTML = renderMd(run.acc);
|
||||||
|
if (card.parentElement !== wrap) wrap.appendChild(card);
|
||||||
|
wrap.scrollTop = wrap.scrollHeight;
|
||||||
|
setActionMode(run.cancelling ? "cancelling" : "streaming");
|
||||||
|
$("chat-hint").textContent = run.cancelling ? "停止中…" : "接收中…";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRunningTaskSubscribed(taskId, url) {
|
||||||
|
if (!taskId || getLiveRun(taskId)) return;
|
||||||
|
const run = {
|
||||||
|
taskId,
|
||||||
|
url,
|
||||||
|
acc: "",
|
||||||
|
pending: false,
|
||||||
|
seenRels: new Set(),
|
||||||
|
terminal: false,
|
||||||
|
card: null,
|
||||||
|
body: null,
|
||||||
|
cancelling: state.taskMeta && state.taskMeta.run_status === "cancelling",
|
||||||
|
workingDir: state.taskMeta && state.taskMeta.working_dir,
|
||||||
|
};
|
||||||
|
state.liveRuns.set(taskId, run);
|
||||||
|
state.streaming = true;
|
||||||
|
renderLiveRunIfVisible();
|
||||||
|
streamSse(url, run);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRunHint(run, text) {
|
||||||
|
if (state.taskId === run.taskId) $("chat-hint").textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMessages(msgs) {
|
function renderMessages(msgs) {
|
||||||
const wrap = $("chat-stream");
|
const wrap = $("chat-stream");
|
||||||
wrap.innerHTML = "";
|
wrap.innerHTML = "";
|
||||||
if (!msgs.length) {
|
if (!msgs.length) {
|
||||||
wrap.innerHTML = `<div class="empty">(暂无消息 · 在下方输入开始对话)</div>`;
|
wrap.innerHTML = `<div class="empty">(暂无消息 · 在下方输入开始对话)</div>`;
|
||||||
|
renderLiveRunIfVisible();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 模型切换点小标:assistant 行的 model_profile 与上一个 assistant 不同就插一行分隔
|
// 模型切换点小标:assistant 行的 model_profile 与上一个 assistant 不同就插一行分隔
|
||||||
|
|
@ -1949,6 +2027,7 @@ function renderMessages(msgs) {
|
||||||
}
|
}
|
||||||
wrap.scrollTop = wrap.scrollHeight;
|
wrap.scrollTop = wrap.scrollHeight;
|
||||||
upgradeMediaArtifacts(wrap);
|
upgradeMediaArtifacts(wrap);
|
||||||
|
renderLiveRunIfVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───── send + SSE ─────
|
// ───── send + SSE ─────
|
||||||
|
|
@ -1975,7 +2054,7 @@ function setActionMode(mode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function chatAction() {
|
function chatAction() {
|
||||||
if (state.streaming) cancelCurrentTask();
|
if (isCurrentTaskStreaming()) cancelCurrentTask();
|
||||||
else sendMessage();
|
else sendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1984,7 +2063,7 @@ $("chat-input").addEventListener("keydown", (e) => {
|
||||||
// streaming 期间 Enter 不触发停止 —— 用户可能正在编辑下一条草稿,误触发风险高
|
// streaming 期间 Enter 不触发停止 —— 用户可能正在编辑下一条草稿,误触发风险高
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!state.streaming) sendMessage();
|
if (!isCurrentTaskStreaming()) sendMessage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("chat-input").addEventListener("input", syncOptimizeBtn);
|
$("chat-input").addEventListener("input", syncOptimizeBtn);
|
||||||
|
|
@ -2127,11 +2206,12 @@ $("chat-stream").addEventListener("click", (e) => {
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
if (!state.taskId) return;
|
if (!state.taskId) return;
|
||||||
if (state.streaming) return;
|
if (isCurrentTaskStreaming()) return;
|
||||||
const content = $("chat-input").value.trim();
|
const content = $("chat-input").value.trim();
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
setActionMode("cancelling"); // 临时锁住,等 events_url 拿到再切 streaming
|
setActionMode("cancelling"); // 临时锁住,等 events_url 拿到再切 streaming
|
||||||
$("chat-hint").textContent = "发送中…";
|
$("chat-hint").textContent = "发送中…";
|
||||||
|
const taskId = state.taskId;
|
||||||
try {
|
try {
|
||||||
// 立刻渲染 user 消息卡(乐观)
|
// 立刻渲染 user 消息卡(乐观)
|
||||||
const wrap = $("chat-stream");
|
const wrap = $("chat-stream");
|
||||||
|
|
@ -2142,21 +2222,34 @@ async function sendMessage() {
|
||||||
|
|
||||||
// assistant 流式占位卡
|
// assistant 流式占位卡
|
||||||
const asstCard = document.createElement("div");
|
const asstCard = document.createElement("div");
|
||||||
asstCard.className = "msg assistant";
|
asstCard.className = "msg assistant live-run";
|
||||||
asstCard.innerHTML = `<div class="role">助手</div><div class="body streaming"></div>`;
|
asstCard.innerHTML = `<div class="role">助手</div><div class="body streaming"></div>`;
|
||||||
wrap.appendChild(asstCard);
|
wrap.appendChild(asstCard);
|
||||||
wrap.scrollTop = wrap.scrollHeight;
|
wrap.scrollTop = wrap.scrollHeight;
|
||||||
|
|
||||||
const r = await api("POST", `/v1/tasks/${state.taskId}/messages`, {
|
const r = await api("POST", `/v1/tasks/${taskId}/messages`, {
|
||||||
content,
|
content,
|
||||||
image_model: state.imageModel || "",
|
image_model: state.imageModel || "",
|
||||||
video_model: state.videoModel || "",
|
video_model: state.videoModel || "",
|
||||||
});
|
});
|
||||||
$("chat-input").value = "";
|
$("chat-input").value = "";
|
||||||
syncOptimizeBtn();
|
syncOptimizeBtn();
|
||||||
|
const run = {
|
||||||
|
taskId,
|
||||||
|
url: r.events_url,
|
||||||
|
acc: "",
|
||||||
|
pending: false,
|
||||||
|
seenRels: new Set(),
|
||||||
|
terminal: false,
|
||||||
|
card: asstCard,
|
||||||
|
body: asstCard.querySelector(".body"),
|
||||||
|
cancelling: false,
|
||||||
|
workingDir: state.taskMeta && state.taskMeta.working_dir,
|
||||||
|
};
|
||||||
|
state.liveRuns.set(taskId, run);
|
||||||
state.streaming = true;
|
state.streaming = true;
|
||||||
setActionMode("streaming");
|
setActionMode("streaming");
|
||||||
streamSse(r.events_url, asstCard);
|
streamSse(r.events_url, run);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
appendErrorCard(e.message);
|
appendErrorCard(e.message);
|
||||||
|
|
@ -2166,7 +2259,9 @@ async function sendMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelCurrentTask() {
|
async function cancelCurrentTask() {
|
||||||
if (!state.taskId || !state.streaming) return;
|
const run = getLiveRun(state.taskId);
|
||||||
|
if (!state.taskId || !run) return;
|
||||||
|
run.cancelling = true;
|
||||||
setActionMode("cancelling");
|
setActionMode("cancelling");
|
||||||
$("chat-hint").textContent = "停止中…";
|
$("chat-hint").textContent = "停止中…";
|
||||||
try {
|
try {
|
||||||
|
|
@ -2176,21 +2271,37 @@ async function cancelCurrentTask() {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
// 409 = 已结束 / 已 cancelling,不算错;其他贴 toast
|
// 409 = 已结束 / 已 cancelling,不算错;其他贴 toast
|
||||||
if (e.status !== 409) appendErrorCard("cancel: " + e.message);
|
if (e.status !== 409) appendErrorCard("cancel: " + e.message);
|
||||||
|
run.cancelling = false;
|
||||||
setActionMode("streaming");
|
setActionMode("streaming");
|
||||||
$("chat-hint").textContent = "接收中…";
|
$("chat-hint").textContent = "接收中…";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function streamSse(url, asstCard) {
|
function streamSse(url, run) {
|
||||||
// EventSource 不支持自定义 header,token 走 query string(?token=...)
|
// EventSource 不支持自定义 header,token 走 query string(?token=...)
|
||||||
// 这里 SSE 走 same-origin,token 经 URL 传给后端 — 但当前后端只读 Authorization 头
|
// 这里 SSE 走 same-origin,token 经 URL 传给后端 — 但当前后端只读 Authorization 头
|
||||||
// 简单做法:走带 token 的 fetch + ReadableStream 替代 EventSource
|
// 简单做法:走带 token 的 fetch + ReadableStream 替代 EventSource
|
||||||
fetchSse(url, asstCard).catch((e) => appendErrorCard("sse: " + e.message));
|
fetchSse(url, run).catch((e) => {
|
||||||
|
appendRunError(run, "sse: " + e.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSse(url, asstCard) {
|
function appendRunError(run, msg) {
|
||||||
const body = asstCard.querySelector(".body");
|
if (state.taskId === run.taskId) {
|
||||||
const ctx = { acc: "", body, pending: false, seenRels: new Set(), terminal: false };
|
appendErrorCard(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const host = run.card || createLiveAssistantCard(run);
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "msg error";
|
||||||
|
card.innerHTML = `<div class="role">错误</div><div class="body">${escapeHtml(msg)}</div>`;
|
||||||
|
host.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSse(url, run) {
|
||||||
|
const body = run.body || (run.card && run.card.querySelector(".body"));
|
||||||
|
run.body = body;
|
||||||
|
const ctx = run;
|
||||||
const hint = $("chat-hint");
|
const hint = $("chat-hint");
|
||||||
// 重连:reader 异常 / 自然 EOF 但未收到 done/error 时,GET events 重订阅。
|
// 重连:reader 异常 / 自然 EOF 但未收到 done/error 时,GET events 重订阅。
|
||||||
// 后端 stream_events 重连入口会校验 run_status,task 已不活跃直接吐 done 关流;
|
// 后端 stream_events 重连入口会校验 run_status,task 已不活跃直接吐 done 关流;
|
||||||
|
|
@ -2200,14 +2311,14 @@ async function fetchSse(url, asstCard) {
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
await consumeSseStream(url, asstCard, ctx);
|
await consumeSseStream(url, run.card, ctx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (ctx.terminal) break; // 已收到 done/error,不重连
|
if (ctx.terminal) break; // 已收到 done/error,不重连
|
||||||
if (attempt >= backoffs.length) {
|
if (attempt >= backoffs.length) {
|
||||||
appendErrorCard("连接已断开,刚才的回复可能未完成,请重发消息。");
|
appendRunError(ctx, "连接已断开,刚才的回复可能未完成,请重发消息。");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hint.textContent = `连接断开,重连中…(${attempt + 1}/${backoffs.length})`;
|
setRunHint(ctx, `连接断开,重连中…(${attempt + 1}/${backoffs.length})`);
|
||||||
await new Promise(r => setTimeout(r, backoffs[attempt]));
|
await new Promise(r => setTimeout(r, backoffs[attempt]));
|
||||||
attempt++;
|
attempt++;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -2216,28 +2327,35 @@ async function fetchSse(url, asstCard) {
|
||||||
if (ctx.terminal) break; // 正常收尾(看到 done/error)
|
if (ctx.terminal) break; // 正常收尾(看到 done/error)
|
||||||
// 未见 done/error 就 EOF → 服务端中途关流(进程被杀 / nginx 切),重连
|
// 未见 done/error 就 EOF → 服务端中途关流(进程被杀 / nginx 切),重连
|
||||||
if (attempt >= backoffs.length) {
|
if (attempt >= backoffs.length) {
|
||||||
appendErrorCard("连接已断开,刚才的回复可能未完成,请重发消息。");
|
appendRunError(ctx, "连接已断开,刚才的回复可能未完成,请重发消息。");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hint.textContent = `连接断开,重连中…(${attempt + 1}/${backoffs.length})`;
|
setRunHint(ctx, `连接断开,重连中…(${attempt + 1}/${backoffs.length})`);
|
||||||
await new Promise(r => setTimeout(r, backoffs[attempt]));
|
await new Promise(r => setTimeout(r, backoffs[attempt]));
|
||||||
attempt++;
|
attempt++;
|
||||||
}
|
}
|
||||||
// 最终定稿 + 代码高亮(流式中不 highlight,省 CPU)
|
// 最终定稿 + 代码高亮(流式中不 highlight,省 CPU)
|
||||||
body.innerHTML = renderMd(ctx.acc);
|
if (ctx.body) {
|
||||||
highlightIn(asstCard);
|
ctx.body.innerHTML = renderMd(ctx.acc);
|
||||||
|
if (ctx.card) highlightIn(ctx.card);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
body.classList.remove("streaming");
|
if (ctx.body) ctx.body.classList.remove("streaming");
|
||||||
|
state.liveRuns.delete(ctx.taskId);
|
||||||
|
state.streaming = state.liveRuns.size > 0;
|
||||||
|
if (state.taskId === ctx.taskId) {
|
||||||
hint.textContent = "就绪";
|
hint.textContent = "就绪";
|
||||||
state.streaming = false;
|
|
||||||
setActionMode("idle");
|
setActionMode("idle");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 刷新 task meta + messages(拿真实持久化的);失败路径已退出,这里不再跑
|
// 刷新 task meta + messages(拿真实持久化的);失败路径已退出,这里不再跑
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
|
if (state.taskId === ctx.taskId) {
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
loadFiles(); // 回复结束后右侧文件面板同步刷新(可能有新写入 / 修改的产物)
|
loadFiles(); // 回复结束后右侧文件面板同步刷新(可能有新写入 / 修改的产物)
|
||||||
refreshConcurrentWarnings(); // 自己 task 收尾,顺便清/更新 banner(同 wd 邻居可能也变了)
|
refreshConcurrentWarnings(); // 自己 task 收尾,顺便清/更新 banner(同 wd 邻居可能也变了)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function consumeSseStream(url, asstCard, ctx) {
|
async function consumeSseStream(url, asstCard, ctx) {
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
|
|
@ -2247,7 +2365,7 @@ async function consumeSseStream(url, asstCard, ctx) {
|
||||||
const reader = r.body.getReader();
|
const reader = r.body.getReader();
|
||||||
const dec = new TextDecoder();
|
const dec = new TextDecoder();
|
||||||
let buf = "";
|
let buf = "";
|
||||||
$("chat-hint").textContent = "接收中…";
|
setRunHint(ctx, "接收中…");
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
if (done) return;
|
if (done) return;
|
||||||
|
|
@ -2285,9 +2403,13 @@ function parseSseFrame(frame) {
|
||||||
|
|
||||||
function handleSseEvent(ev, asstCard, ctx) {
|
function handleSseEvent(ev, asstCard, ctx) {
|
||||||
const t = ev.event;
|
const t = ev.event;
|
||||||
|
asstCard = asstCard || ctx.card || createLiveAssistantCard(ctx);
|
||||||
|
ctx.card = asstCard;
|
||||||
|
ctx.body = ctx.body || asstCard.querySelector(".body");
|
||||||
const stream = $("chat-stream");
|
const stream = $("chat-stream");
|
||||||
|
const visible = state.taskId === ctx.taskId;
|
||||||
// 用户拖到上面看历史时不抢滚动,只在贴底时跟流
|
// 用户拖到上面看历史时不抢滚动,只在贴底时跟流
|
||||||
const nearBottom = stream.scrollHeight - stream.scrollTop - stream.clientHeight < 120;
|
const nearBottom = visible && (stream.scrollHeight - stream.scrollTop - stream.clientHeight < 120);
|
||||||
if (t === "text" && ev.data && ev.data.delta) {
|
if (t === "text" && ev.data && ev.data.delta) {
|
||||||
ctx.acc += ev.data.delta;
|
ctx.acc += ev.data.delta;
|
||||||
// rAF 节流:每帧最多 1 次重渲染,流式 token 高频时不抖
|
// rAF 节流:每帧最多 1 次重渲染,流式 token 高频时不抖
|
||||||
|
|
@ -2307,7 +2429,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
det.className = "tool-call";
|
det.className = "tool-call";
|
||||||
det.innerHTML = `<summary>工具调用:${escapeHtml(fn)}</summary><pre>${escapeHtml(argsStr)}</pre>`;
|
det.innerHTML = `<summary>工具调用:${escapeHtml(fn)}</summary><pre>${escapeHtml(argsStr)}</pre>`;
|
||||||
asstCard.appendChild(det);
|
asstCard.appendChild(det);
|
||||||
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
|
const wd = _workingDirName(ctx.workingDir);
|
||||||
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(fn);
|
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(fn);
|
||||||
const fresh = isProducer
|
const fresh = isProducer
|
||||||
? extractArtifactRels(argsStr, wd).filter(r => !ctx.seenRels.has(r))
|
? extractArtifactRels(argsStr, wd).filter(r => !ctx.seenRels.has(r))
|
||||||
|
|
@ -2327,7 +2449,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
det.className = "tool-call";
|
det.className = "tool-call";
|
||||||
det.innerHTML = `<summary>工具结果${banner}</summary><pre>${escapeHtml(txtStr)}</pre>`;
|
det.innerHTML = `<summary>工具结果${banner}</summary><pre>${escapeHtml(txtStr)}</pre>`;
|
||||||
asstCard.appendChild(det);
|
asstCard.appendChild(det);
|
||||||
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
|
const wd = _workingDirName(ctx.workingDir);
|
||||||
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(toolName);
|
const isProducer = ARTIFACT_PRODUCING_TOOLS.has(toolName);
|
||||||
const fresh = isProducer
|
const fresh = isProducer
|
||||||
? extractArtifactRels(txtStr, wd).filter(r => !ctx.seenRels.has(r))
|
? extractArtifactRels(txtStr, wd).filter(r => !ctx.seenRels.has(r))
|
||||||
|
|
@ -2338,7 +2460,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
asstCard.insertAdjacentHTML("beforeend", barHtml);
|
asstCard.insertAdjacentHTML("beforeend", barHtml);
|
||||||
if (isProducer) upgradeMediaArtifacts(asstCard);
|
if (isProducer) upgradeMediaArtifacts(asstCard);
|
||||||
}
|
}
|
||||||
scheduleFilesRefresh(); // 工具调用结果回来,FS 可能被改了,debounce 刷新右侧
|
if (visible) scheduleFilesRefresh(); // 工具调用结果回来,FS 可能被改了,debounce 刷新右侧
|
||||||
} else if (t === "cancelled") {
|
} else if (t === "cancelled") {
|
||||||
const badge = document.createElement("div");
|
const badge = document.createElement("div");
|
||||||
badge.className = "cancelled-badge";
|
badge.className = "cancelled-badge";
|
||||||
|
|
@ -2346,7 +2468,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
||||||
asstCard.appendChild(badge);
|
asstCard.appendChild(badge);
|
||||||
} else if (t === "error") {
|
} else if (t === "error") {
|
||||||
const msg = (ev.data && (ev.data.msg || ev.data.error)) || JSON.stringify(ev.data);
|
const msg = (ev.data && (ev.data.msg || ev.data.error)) || JSON.stringify(ev.data);
|
||||||
appendErrorCard(msg);
|
appendRunError(ctx, msg);
|
||||||
}
|
}
|
||||||
if (nearBottom) stream.scrollTop = stream.scrollHeight;
|
if (nearBottom) stream.scrollTop = stream.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue