fix: persist elapsed badge on refresh via message ts

- normalize() now preserves ts field from API response
- restoreElapsedBadges() moved to pollSession finally block
  (messages are empty during renderAllMessages)
- sendPrompt() adds ts to local user messages
- restoreElapsedBadges() resets lastUserTs when user msg has no ts
  (prevents stale accumulation from previous rounds)
This commit is contained in:
wjl2023 committed 2026-05-26 00:03:59 +08:00
1 parent 537f465b93
commit bc1c9f58dd
2 files changed
+48 -8

No files matched your search

+8 -6
View File
@@ -217,9 +217,11 @@ CSS: `.task-elapsed { color: var(--accent); font-size: 12px; font-weight: 600; }
**问题:计时"已运行x"刷新后丢失**
- 根因:`r.taskStartedAt` 仅存于内存 runtime Map,刷新后 runtime 重建;pollSession 重新检测 running → setBusy → startTaskTimer 设新 Date.now(),真正开始时间丢失
- 修复方案(仅改 2 个函数,4 行新增):
1. `startTaskTimer`:尝试从 `sessionStorage.getItem('ga_task_started_' + sess.id)` 恢复时间戳,若无则用 Date.now();设置后写入 sessionStorage
2. `stopTaskTimer`:任务结束时 `sessionStorage.removeItem('ga_task_started_' + sess.id)`
- 为什么用 sessionStorage:与标签页生命周期绑定,关闭标签自动清除,不残留脏数据
- 为什么不需要 onBridgeReady 额外恢复:pollSession 检测到 running 后自动调 setBusy → startTaskTimer,此时 sessionStorage 中已有值会被读取恢复
- 改动文件:app.js(startTaskTimer L1198-1205 / stopTaskTimer L1219-1225)
- ❌ sessionStorage 方案失败(刷新后仍丢失)
- ✅ 最终方案:基于消息 ts 时间戳恢复(2026-05-25 成功验证)
- 根因:消息对象本身有 `ts` 字段(API 返回),但 `normalize()` 函数丢弃了它
- 修复 3 处(仅 app.js):
1. `normalize()` 增加 `if (m.ts) o.ts = m.ts;` — 保留时间戳
2. `restoreElapsedBadges(sess, box)` 从 `renderAllMessages` 移到 `pollSession` finally 块 — 确保消息已加载
3. `startTaskTimer(sess)` 从最后一条 user 消息的 ts 恢复 taskStartedAt — 运行中任务继续计时
- 验证结果:刷新后 badge 正确显示"已运行 17s"/"已运行 14s",已完成任务静态显示,运行中任务实时计时
+40 -2
View File
@@ -1073,8 +1073,35 @@ function msgNode(msg) {
function renderAllMessages(sess) {
const box = ensureMsgs(); box.innerHTML = '';
for (const m of sess.messages) box.appendChild(msgNode(m));
// badge 恢复在 pollSession finally 中执行(此时 messages 已通过异步加载填充)
refreshEmptyState(sess); scrollBottom(true);
}
// 遍历消息对,用 ts 差值恢复 badge;对运行中任务恢复 taskStartedAt
function restoreElapsedBadges(sess, box) {
const msgs = sess.messages;
if (!msgs || !msgs.length) return;
const nodes = box.querySelectorAll('.msg');
let lastUserTs = null;
for (let i = 0; i < msgs.length; i++) {
if (msgs[i].role === 'user') {
lastUserTs = msgs[i].ts ? msgs[i].ts * 1000 : null; // 无 ts 则重置
} else if (msgs[i].role === 'assistant') {
if (lastUserTs && msgs[i].ts) {
const elapsed = msgs[i].ts * 1000 - lastUserTs;
if (elapsed > 0 && nodes[i]) {
ensureTaskElapsedBadge(nodes[i], lastUserTs, msgs[i].ts * 1000);
}
}
lastUserTs = null;
}
}
// 运行中任务:最后一条是 user 且 session busy,恢复实时计时
if (lastUserTs && rt(sess).busy) {
const r = rt(sess);
r.taskStartedAt = lastUserTs;
r.taskEndedAt = null;
}
}
function appendMessage(sess, msg) {
if (!isActive(sess)) return;
const el = msgNode(msg);
@@ -1198,7 +1225,15 @@ function ensureTaskElapsedBadge(wrap, startedAt, endedAt) {
function startTaskTimer(sess) {
const r = rt(sess);
if (r.taskStartedAt) return; // 已在计时,不重置
r.taskStartedAt = Date.now();
// 优先从消息时间戳恢复(刷新后持久化)
const msgs = sess.messages;
let restored = 0;
if (msgs && msgs.length) {
for (let i = msgs.length - 1; i >= 0; i--) {
if (msgs[i].role === 'user' && msgs[i].ts) { restored = msgs[i].ts * 1000; break; }
}
}
r.taskStartedAt = restored || Date.now();
r.taskEndedAt = null;
if (r.taskTimerId) clearInterval(r.taskTimerId);
r.taskTimerId = setInterval(() => {
@@ -1393,6 +1428,7 @@ function normalize(m) {
if (m.stopped) o.stopped = true;
if (m.images) o.images = m.images;
if (m.files) o.files = m.files;
if (m.ts) o.ts = m.ts;
return o;
}
function upsert(sess, raw, partial) {
@@ -1428,6 +1464,8 @@ async function pollSession(sess) {
setBusy(sess, false);
} finally {
r.polling = false; renderSessionList();
// 历史消息已全部加载,恢复已完成任务的耗时 badge
if (isActive(sess)) restoreElapsedBadges(sess, ensureMsgs());
tokPollBridge();
}
}
@@ -1519,7 +1557,7 @@ async function sendPrompt(text) {
.filter(Boolean)
.join('\n\n');
const usedFiles = collectUsedFiles(text);
const userMsg = { role: 'user', content: text };
const userMsg = { role: 'user', content: text, ts: Date.now() / 1000 };
const previewImgs = usedFiles.filter(f => f.isImage).map(f => ({ id: 'f-' + f.sid, name: f.name, path: f.path, dataUrl: f.dataUrl || '' }));
if (previewImgs.length) userMsg.images = previewImgs;
const previewFiles = usedFiles.filter(f => !f.isImage).map(f => ({ id: 'f-' + f.sid, name: f.name, path: f.path }));