feat(web): incremental render-window batch rendering engine

Add render_window.js: Python-driven incremental pagination with id+chainIndex state, anchor restore, batch guard and streaming auto-detect; wired through chat_bridge and the page shell.
This commit is contained in:
2026-09-17 16:40:04 +08:00
parent 156f94e840
commit ef00f64435
6 changed files with 932 additions and 8 deletions
+42
View File
@@ -9,6 +9,9 @@ class ChatBridge(QObject):
delete_message_requested = pyqtSignal(str) # 删除消息信号 (msg_id)
attachment_clicked = pyqtSignal(str)
scroll_changed = pyqtSignal(float, float, float) # 页面 scrollY / scrollHeight / innerHeight
# 🆕 P1-01 渲染窗口:前端请求换页(fire-and-forget;响应经 run_js 推送)
window_page_requested = pyqtSignal(str, str, str, int) # (session_id, direction, boundary_id, generation)
def __init__(self, page, channel, js_runner=None):
"""
:param page: QWebEnginePage 实例(QtWebEngine 路径)
@@ -18,6 +21,7 @@ class ChatBridge(QObject):
super().__init__()
self.page = page
self._js_runner = js_runner
self.window = None # 🆕 P1-01MainWindow 引用(分页请求需要访问 DB/代次)
if channel is not None:
# 将自身注册到 channel 中,前端可通过 bridge 对象调用
channel.registerObject("bridge", self)
@@ -157,8 +161,46 @@ class ChatBridge(QObject):
"""隐藏加载界面(带渐变退场)"""
self.run_js("hideLoadingOverlay();")
# ---------- 🆕 P1-01 渲染窗口 (Python -> JS) ----------
def rw_config(self, mode: str, size: int):
"""注入渲染窗口配置(JS 就绪后、首次加载前调用一次)"""
self.run_js("rwApplyConfig(" + json.dumps(
{"render_window_mode": mode, "render_window_size": size}) + ");")
def rw_begin(self, session_id: str, generation: int, total: int):
"""开始一轮窗口化加载:清屏 + 重同步 (session, generation)"""
self.run_js(f"rwBegin({json.dumps(session_id)}, {int(generation)}, {int(total)});")
def rw_init_window(self, session_id: str, generation: int, chain_len: int, items):
"""初始窗口就绪:items = [(msg_id, chain_index), ...](旧→新)"""
payload = json.dumps({
"sessionId": session_id,
"generation": int(generation),
"chainLen": int(chain_len),
"items": [{"id": mid, "chainIndex": ix} for mid, ix in items],
}, ensure_ascii=False)
self.run_js(f"rwInitWindow({payload});")
def rw_note_live(self, session_id: str, generation: int, msg_id: str,
chain_index: int, chain_len: int = -1):
"""新消息追加进窗口(发送用户消息 / 助手占位 / 切回续流)。
未持久化/未知 → chain_index=-1;未知链长 → chain_len=-1JS 保持旧值)"""
self.run_js(f"rwNoteLive({json.dumps(session_id)}, {int(generation)}, "
f"{json.dumps(msg_id)}, {int(chain_index)}, {int(chain_len)});")
def rw_page_response(self, payload: dict):
"""分页响应推送:payload 由 MainWindow._on_window_page_request 构造"""
self.run_js("rwPageResponse(" + json.dumps(payload, ensure_ascii=False) + ");")
# ---------- 交互 Slot (JS -> Python) ----------
@pyqtSlot(str, str, str, int)
def onRequestWindowPage(self, session_id, direction, boundary_msg_id, generation):
"""🆕 P1-01:前端请求换页(direction: older/newer)。
fire-and-forget:结果由 Python 经 rwPageResponse 推送(双引擎同构)。"""
self.window_page_requested.emit(session_id, direction, boundary_msg_id, int(generation))
@pyqtSlot(str)
def onRegenerateClicked(self, msg_id):
"""前端点击“重新生成”按钮时触发"""