diff --git a/PROGRESS.md b/PROGRESS.md index 902aacf..4cc3508 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,6 +21,13 @@ ## 已完成关键能力 +### 2026-06-15 / 消息分页:尾部窗口 + 向上滚动加载更早(切 task 提速) + +- 痛点:切 task 卡顿 —— `/v1/tasks/{id}/messages` 无分页一次拉全量,前端 `renderMessages` 又对每条跑 markdown+highlight+media 全量渲 DOM,消息多时两段成本都线性涨。 +- 后端 `web/app.py` `list_messages`:加可选 query `limit`、`before_idx`。不传 → 旧行为(升序全量,仅多返 `has_more:false`,向后兼容);传 `limit` → 取尾部最近 N 条(`idx desc + limit` 再 reverse);传 `before_idx` → 取该 idx 之前更早一批。响应恒含 `has_more`。 +- 前端 `chat.js`:① `selectTask` 进来立即把 chat-stream 换「加载中…」(治感知,切换瞬时跟手);② `loadMessages` 默认 `limit=60`,结果存 `state.loadedMessages/msgHasMore`;③ 新增 `loadEarlierMessages` + `_msgScrollObserver`(复用 task list 的 sentinel 范式),顶部 sentinel 进视口自动 prepend 更早一批后整窗重渲(renderMessages 仍是对 loadedMessages 的纯函数,时序累积逻辑不动),重渲后锚回滚动位不跳视口。 +- `state.js` 加 `loadedMessages/msgHasMore/msgLoadingEarlier`;`dev.html` 加 `.msg-top-sentinel` 样式。取舍:只载尾部时进度 dock 仅反映窗口内 task_progress,补满更早后一致。bump 0.12.13 → 0.12.14。 + ### 2026-06-15 / 图片预览:左键拖动平移 + 光标语义改正 - 光标:100% 时改回普通箭头(原 `zoom-in` 放大镜误导 —— 左键不缩放,缩放是 Ctrl+滚轮);放大后改 `grab`、拖动中 `grabbing`,贴合"可拖"语义。 diff --git a/core/__init__.py b/core/__init__.py index bec3845..c22a2c9 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.12.13" +__version__ = "0.12.14" diff --git a/web/app.py b/web/app.py index 7e62178..020d786 100644 --- a/web/app.py +++ b/web/app.py @@ -1412,21 +1412,53 @@ def create_app() -> FastAPI: raise HTTPException(404, f"task not found: {tid}") @app.get("/v1/tasks/{task_id}/messages", tags=["messages"]) - def list_messages(task_id: str, user_id: UUID = Depends(require_user)): - """task 历史消息(idx 升序);LiteLLM 原 payload 透传给前端,自行渲染。""" + def list_messages( + task_id: str, + limit: int = None, + before_idx: int = None, + user_id: UUID = Depends(require_user), + ): + """task 历史消息(idx 升序);LiteLLM 原 payload 透传给前端,自行渲染。 + + 分页(尾部窗口): + - 不传 limit → 升序全量返回(向后兼容旧前端),has_more=false。 + - 传 limit → 取**尾部**最近 limit 条(idx desc + limit 再 reverse 回升序)。 + - 传 before_idx → 只取 idx < before_idx 的更早部分(配合 limit 向上翻页)。 + 响应恒含 has_more:本次返回窗口之前是否还有更早消息。 + """ try: tid = UUID(task_id) except ValueError: raise HTTPException(404, f"invalid task id: {task_id!r}") with session_scope() as s: _assert_owns_task(s, tid, user_id) - rows = s.execute( - select( - Message.idx, Message.payload, Message.tokens_in, - Message.tokens_out, Message.model_profile, Message.created_at, - ).where(Message.task_id == tid).order_by(Message.idx) - ).all() + cols = ( + Message.idx, Message.payload, Message.tokens_in, + Message.tokens_out, Message.model_profile, Message.created_at, + ) + if limit is None: + # 旧行为:升序全量 + rows = s.execute( + select(*cols).where(Message.task_id == tid).order_by(Message.idx) + ).all() + else: + # 尾部窗口:倒序取 limit 条,再翻回升序 + q = select(*cols).where(Message.task_id == tid) + if before_idx is not None: + q = q.where(Message.idx < before_idx) + rows = list(s.execute(q.order_by(Message.idx.desc()).limit(limit)).all()) + rows.reverse() + # has_more:本窗口最早一条之前是否还有 + has_more = False + if rows: + first_idx = rows[0].idx + has_more = s.execute( + select(Message.idx) + .where(Message.task_id == tid, Message.idx < first_idx) + .limit(1) + ).first() is not None return { + "has_more": has_more, "messages": [ { "idx": r.idx, diff --git a/web/static/dev.html b/web/static/dev.html index cff4434..ea525d1 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -514,6 +514,8 @@ display: flex; flex-direction: column; gap: 8px; min-height: 0; /* 允许在 flex 容器里收缩 + 触发自身滚动 */ } + /* 顶部"上滑加载更早"占位:居中淡色小字,自身不占气泡样式 */ + .msg-top-sentinel { align-self: center; font-size: 12px; padding: 4px 0; opacity: 0.6; } /* 阅读宽度:assistant/system/tool 限到 ~48rem(约 60-80 字/行,长文不至于满屏铺开难回扫); user 气泡更窄(36rem)。宽屏下提升可读性,窄屏 92% 仍生效(min 取小者) */ .msg { border: 1px solid var(--border); border-radius: var(--r-md); padding: 8px 12px; max-width: min(92%, 48rem); animation: msg-in .22s cubic-bezier(.2,.7,.2,1); } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 823cda8..e4df2f4 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -237,6 +237,9 @@ export async function selectTask(tid) { }); // 手机视图:选中任务自动切到对话面板(桌面 mqPhone 不命中 → no-op) if (mqPhone.matches) setMobileView("mv-mid"); + // 立即清空 + 显示加载占位:切 task 体感瞬时跟手,不等 meta/messages 两个 await + $("chat-stream").innerHTML = `