Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
437e4a3213 | ||
|
|
3b75dfcda4 | ||
|
|
75da435f71 | ||
|
|
97e8545b50 |
+42
-7
@@ -214,7 +214,7 @@ class Wv2Session:
|
|||||||
self._js_pump = QTimer()
|
self._js_pump = QTimer()
|
||||||
self._js_pump.setInterval(25)
|
self._js_pump.setInterval(25)
|
||||||
self._js_pump.timeout.connect(self._js_pump_tick)
|
self._js_pump.timeout.connect(self._js_pump_tick)
|
||||||
self._js_pump.start()
|
# 无回调脚本不进入队列;没有待处理任务时不需要常驻唤醒 UI 线程。
|
||||||
# 🆕 预热:立即导航 about:blank,让 msedgewebview2 进程/GPU 在 UI 构建期间冷启动
|
# 🆕 预热:立即导航 about:blank,让 msedgewebview2 进程/GPU 在 UI 构建期间冷启动
|
||||||
# (实测本机首次真实页面导航需 12-15s,预热后降到 ~1s)
|
# (实测本机首次真实页面导航需 12-15s,预热后降到 ~1s)
|
||||||
try:
|
try:
|
||||||
@@ -339,8 +339,27 @@ class Wv2Session:
|
|||||||
print("[WV2] navigate error:", ex)
|
print("[WV2] navigate error:", ex)
|
||||||
|
|
||||||
def execute_js(self, script: str):
|
def execute_js(self, script: str):
|
||||||
"""fire-and-forget(ChatBridge.run_js 的替换,JS 文本完全同构)"""
|
"""执行无需返回值的脚本,不进入结果轮询队列。
|
||||||
self._js_run(script, None)
|
|
||||||
|
流式正文每个 token 都会走这里。旧实现统一使用
|
||||||
|
``ExecuteScriptWithResultAsync`` 并把任务放入 ``_js_pending``,高频
|
||||||
|
输出时会在渲染器和 Python 侧同时堆积大量无用结果;WebView2 原生
|
||||||
|
``ExecuteScriptAsync`` 已经提供了真正的 fire-and-forget 路径。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
execute_async = getattr(self.core, "ExecuteScriptAsync")
|
||||||
|
except AttributeError:
|
||||||
|
# 兼容旧版/测试替身未暴露 ExecuteScriptAsync 的情况。即使只能
|
||||||
|
# 使用带结果 API,也直接丢弃 Task,不把无用结果放进轮询队列。
|
||||||
|
try:
|
||||||
|
getattr(self.core, "ExecuteScriptWithResultAsync")(script)
|
||||||
|
except BaseException as ex:
|
||||||
|
print("[WV2] execute_js fallback error:", ex)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
execute_async(script)
|
||||||
|
except BaseException as ex:
|
||||||
|
print("[WV2] execute_js error:", ex)
|
||||||
|
|
||||||
def execute_js_async(self, script: str, cb):
|
def execute_js_async(self, script: str, cb):
|
||||||
"""带回调执行:真异步,回调在主线程定时器 tick 中发出(绝不阻塞)"""
|
"""带回调执行:真异步,回调在主线程定时器 tick 中发出(绝不阻塞)"""
|
||||||
@@ -358,13 +377,25 @@ class Wv2Session:
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
self._js_pending.append([task, cb, time.time()])
|
self._js_pending.append([task, cb, time.time()])
|
||||||
|
try:
|
||||||
|
if not self._js_pump.isActive():
|
||||||
|
self._js_pump.start()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _js_pump_tick(self):
|
def _js_pump_tick(self):
|
||||||
if not self._js_pending:
|
if not self._js_pending:
|
||||||
|
try:
|
||||||
|
self._js_pump.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
import json
|
import json
|
||||||
remaining = []
|
# 先摘下本批次;回调中再次入队的任务写入新的 _js_pending,不能被
|
||||||
for task, cb, t0 in self._js_pending:
|
# 本轮收尾赋值覆盖。
|
||||||
|
pending = self._js_pending
|
||||||
|
self._js_pending = []
|
||||||
|
for task, cb, t0 in pending:
|
||||||
done = False
|
done = False
|
||||||
try:
|
try:
|
||||||
done = task.IsCompleted
|
done = task.IsCompleted
|
||||||
@@ -373,7 +404,7 @@ class Wv2Session:
|
|||||||
if not done and time.time() - t0 > 10:
|
if not done and time.time() - t0 > 10:
|
||||||
done = True # 10s 安全超时(渲染器死亡时不永久卡队列)
|
done = True # 10s 安全超时(渲染器死亡时不永久卡队列)
|
||||||
if not done:
|
if not done:
|
||||||
remaining.append([task, cb, t0])
|
self._js_pending.append([task, cb, t0])
|
||||||
continue
|
continue
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
@@ -395,7 +426,11 @@ class Wv2Session:
|
|||||||
cb(result)
|
cb(result)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[WV2] js callback error:", ex)
|
print("[WV2] js callback error:", ex)
|
||||||
self._js_pending = remaining
|
if not self._js_pending:
|
||||||
|
try:
|
||||||
|
self._js_pump.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
+60
-21
@@ -9,12 +9,23 @@ import json
|
|||||||
from ui.views.custom_web_page import CustomWebPage
|
from ui.views.custom_web_page import CustomWebPage
|
||||||
from PyQt6.QtCore import QUrl # 🌟 新增:用于加载本地 HTML
|
from PyQt6.QtCore import QUrl # 🌟 新增:用于加载本地 HTML
|
||||||
|
|
||||||
# ========== 流式诊断日志(排查显示问题的根本手段,写 stream_diag.log) ==========
|
# ========== 流式诊断日志(仅显式开启时写 stream_diag.log) ==========
|
||||||
import time as _diag_time
|
import time as _diag_time
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_diagnostics_enabled() -> bool:
|
||||||
|
"""仅在显式要求时开启高开销流式诊断。"""
|
||||||
|
return (os.environ.get("HAOCODE_STREAM_DIAG") == "1"
|
||||||
|
or os.environ.get("HAOCODE_SHOT") == "1")
|
||||||
|
|
||||||
|
|
||||||
DIAG_LOG_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
DIAG_LOG_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
"stream_diag.log")
|
"stream_diag.log")
|
||||||
|
|
||||||
|
|
||||||
def diag_log(line: str):
|
def diag_log(line: str):
|
||||||
|
if not _stream_diagnostics_enabled():
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
t = _diag_time.time()
|
t = _diag_time.time()
|
||||||
with open(DIAG_LOG_PATH, "a", encoding="utf-8") as f:
|
with open(DIAG_LOG_PATH, "a", encoding="utf-8") as f:
|
||||||
@@ -1577,7 +1588,7 @@ class SessionContextPopup(QtWidgets.QWidget):
|
|||||||
|
|
||||||
self._anim_group.addAnimation(opacity_anim)
|
self._anim_group.addAnimation(opacity_anim)
|
||||||
self._anim_group.addAnimation(pos_anim)
|
self._anim_group.addAnimation(pos_anim)
|
||||||
self._anim_group.start()
|
self._anim_group.start(QtCore.QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
class RenameOverlay(QtWidgets.QWidget):
|
class RenameOverlay(QtWidgets.QWidget):
|
||||||
"""无边框重命名 — 独立顶层透明窗口(P2-03 修复)
|
"""无边框重命名 — 独立顶层透明窗口(P2-03 修复)
|
||||||
@@ -2077,7 +2088,7 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
|
|
||||||
def reload_config(self, config_data):
|
def reload_config(self, config_data):
|
||||||
"""按最新配置重建列表;配置缺失时仍保留可理解、可绘制的空状态。"""
|
"""按最新配置重建列表;配置缺失时仍保留可理解、可绘制的空状态。"""
|
||||||
self._finish_drawer()
|
self._finish_drawer(preserve_top=True)
|
||||||
self.config_data = config_data if isinstance(config_data, dict) else {}
|
self.config_data = config_data if isinstance(config_data, dict) else {}
|
||||||
self.list_widget.clear()
|
self.list_widget.clear()
|
||||||
self._groups.clear()
|
self._groups.clear()
|
||||||
@@ -2256,7 +2267,9 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
if g is None:
|
if g is None:
|
||||||
return
|
return
|
||||||
# 上一个抽屉动画进行中 → 先立即收敛到终态(允许快速连点切换)
|
# 上一个抽屉动画进行中 → 先立即收敛到终态(允许快速连点切换)
|
||||||
self._finish_drawer()
|
# 折叠/展开只改变高度时锁定当前顶部;否则按底边贴按钮会把
|
||||||
|
# 用户正在看的内容整体向下挤。
|
||||||
|
self._finish_drawer(preserve_top=True)
|
||||||
g["expanded"] = not g["expanded"]
|
g["expanded"] = not g["expanded"]
|
||||||
# SVG 箭头切换(展开=朝下 / 收起=朝右)
|
# SVG 箭头切换(展开=朝下 / 收起=朝右)
|
||||||
g["chevron"].setPixmap(
|
g["chevron"].setPixmap(
|
||||||
@@ -2264,7 +2277,7 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
self._clear_fx()
|
self._clear_fx()
|
||||||
self._apply_group_state(g)
|
self._apply_group_state(g)
|
||||||
self.list_widget.doItemsLayout()
|
self.list_widget.doItemsLayout()
|
||||||
self._apply_height_and_position()
|
self._apply_height_and_position(preserve_top=True)
|
||||||
self.list_widget.viewport().update()
|
self.list_widget.viewport().update()
|
||||||
|
|
||||||
def _apply_group_state(self, g):
|
def _apply_group_state(self, g):
|
||||||
@@ -2404,12 +2417,16 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
if d["t"] >= 1.0:
|
if d["t"] >= 1.0:
|
||||||
self._finish_drawer()
|
self._finish_drawer()
|
||||||
|
|
||||||
def _finish_drawer(self):
|
def _finish_drawer(self, preserve_top=False):
|
||||||
"""抽屉动画收敛到终态(动画完成 / 连点打断共用)"""
|
"""抽屉动画收敛到终态(动画完成 / 连点打断共用)"""
|
||||||
d = self._drawer
|
d = self._drawer
|
||||||
if d is None:
|
if d is None:
|
||||||
return
|
return
|
||||||
|
top_anchor = self.y() if preserve_top and self.isVisible() else None
|
||||||
d["timer"].stop()
|
d["timer"].stop()
|
||||||
|
# 每次动画都会创建一个 timer;停用后立即释放,避免反复折叠后
|
||||||
|
# 大量已停止的 QObject 留在弹窗树中。
|
||||||
|
d["timer"].deleteLater()
|
||||||
self._drawer = None
|
self._drawer = None
|
||||||
# 🆕 恢复滑移供应商头的透明背景(_start_drawer 里铺的白底)
|
# 🆕 恢复滑移供应商头的透明背景(_start_drawer 里铺的白底)
|
||||||
for it in d["after_items"]:
|
for it in d["after_items"]:
|
||||||
@@ -2425,7 +2442,9 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
return
|
return
|
||||||
parent = self.parentWidget()
|
parent = self.parentWidget()
|
||||||
btn = getattr(parent, "model_selector", None) if parent else None
|
btn = getattr(parent, "model_selector", None) if parent else None
|
||||||
if btn is not None:
|
if top_anchor is not None:
|
||||||
|
self.move(self.x(), top_anchor)
|
||||||
|
elif btn is not None:
|
||||||
# 真实 App:回到“按钮正上方右对齐”锚点
|
# 真实 App:回到“按钮正上方右对齐”锚点
|
||||||
btn_pos = btn.mapToGlobal(QtCore.QPoint(0, 0))
|
btn_pos = btn.mapToGlobal(QtCore.QPoint(0, 0))
|
||||||
self.move(btn_pos.x() + btn.width() - self.width(),
|
self.move(btn_pos.x() + btn.width() - self.width(),
|
||||||
@@ -2434,8 +2453,9 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
# 无锚定按钮(如调参工具宿主):保持动画起点底缘不漂
|
# 无锚定按钮(如调参工具宿主):保持动画起点底缘不漂
|
||||||
self.move(self.x(), d["bottom0"] - self.minimumHeight())
|
self.move(self.x(), d["bottom0"] - self.minimumHeight())
|
||||||
|
|
||||||
def _apply_height_and_position(self):
|
def _apply_height_and_position(self, preserve_top=False):
|
||||||
"""折叠/展开后:按可见项重算高度;若弹窗正显示中,保持“按钮正上方右对齐”锚点"""
|
"""重算高度;折叠时可锁定顶部,避免当前列表位置被挤走。"""
|
||||||
|
top_anchor = self.y() if preserve_top and self.isVisible() else None
|
||||||
self.adjust_popup_height()
|
self.adjust_popup_height()
|
||||||
if not self.isVisible():
|
if not self.isVisible():
|
||||||
return
|
return
|
||||||
@@ -2445,7 +2465,7 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
return
|
return
|
||||||
btn_pos = btn.mapToGlobal(QtCore.QPoint(0, 0))
|
btn_pos = btn.mapToGlobal(QtCore.QPoint(0, 0))
|
||||||
x = btn_pos.x() + btn.width() - self.width()
|
x = btn_pos.x() + btn.width() - self.width()
|
||||||
y = btn_pos.y() - self.height() - 5
|
y = top_anchor if top_anchor is not None else btn_pos.y() - self.height() - 5
|
||||||
self.move(x, y)
|
self.move(x, y)
|
||||||
|
|
||||||
def adjust_popup_height(self):
|
def adjust_popup_height(self):
|
||||||
@@ -2494,7 +2514,7 @@ class ModelSelectPopup(QtWidgets.QWidget):
|
|||||||
|
|
||||||
self.anim_group.addAnimation(self.opacity_anim)
|
self.anim_group.addAnimation(self.opacity_anim)
|
||||||
self.anim_group.addAnimation(self.pos_anim)
|
self.anim_group.addAnimation(self.pos_anim)
|
||||||
self.anim_group.start()
|
self.anim_group.start(QtCore.QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
def on_item_clicked(self, item):
|
def on_item_clicked(self, item):
|
||||||
data = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
data = item.data(QtCore.Qt.ItemDataRole.UserRole)
|
||||||
@@ -2680,7 +2700,7 @@ class SessionModePopup(QtWidgets.QWidget):
|
|||||||
self.pos_anim.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad)
|
self.pos_anim.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad)
|
||||||
self.anim_group.addAnimation(self.opacity_anim)
|
self.anim_group.addAnimation(self.opacity_anim)
|
||||||
self.anim_group.addAnimation(self.pos_anim)
|
self.anim_group.addAnimation(self.pos_anim)
|
||||||
self.anim_group.start()
|
self.anim_group.start(QtCore.QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
|
|
||||||
# ==================== 🌟 PDF 图片提取子线程工作者 ====================
|
# ==================== 🌟 PDF 图片提取子线程工作者 ====================
|
||||||
@@ -3064,7 +3084,7 @@ class PdfModePopup(QtWidgets.QWidget):
|
|||||||
ps.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad)
|
ps.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad)
|
||||||
self.anim_group.addAnimation(op)
|
self.anim_group.addAnimation(op)
|
||||||
self.anim_group.addAnimation(ps)
|
self.anim_group.addAnimation(ps)
|
||||||
self.anim_group.start()
|
self.anim_group.start(QtCore.QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
|
||||||
# 关闭方式:依赖 Qt.Popup 原生行为——点击弹窗以外任意区域即关闭(与 ModelSelectPopup 一致),
|
# 关闭方式:依赖 Qt.Popup 原生行为——点击弹窗以外任意区域即关闭(与 ModelSelectPopup 一致),
|
||||||
# 鼠标移出不再关闭。
|
# 鼠标移出不再关闭。
|
||||||
@@ -3292,8 +3312,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
# 🌟 提前初始化(init_model_popup 内部会触发 update_context_display 用到)
|
# 🌟 提前初始化(init_model_popup 内部会触发 update_context_display 用到)
|
||||||
self._active_streams = {}
|
self._active_streams = {}
|
||||||
|
self._stream_diagnostics = _stream_diagnostics_enabled()
|
||||||
self._diag_chunk_n = 0
|
self._diag_chunk_n = 0
|
||||||
self._diag_think_n = 0
|
self._diag_think_n = 0
|
||||||
|
if self._stream_diagnostics:
|
||||||
try:
|
try:
|
||||||
open(DIAG_LOG_PATH, "w").close()
|
open(DIAG_LOG_PATH, "w").close()
|
||||||
diag_log("APP_START")
|
diag_log("APP_START")
|
||||||
@@ -3305,7 +3327,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self._render_watchdog.setInterval(200)
|
self._render_watchdog.setInterval(200)
|
||||||
self._render_watchdog.timeout.connect(self._render_watchdog_tick)
|
self._render_watchdog.timeout.connect(self._render_watchdog_tick)
|
||||||
self._render_watchdog.start()
|
self._render_watchdog.start()
|
||||||
# 人肉 debug:JS console 桥抽取器(JS console.log → Python 控制台)
|
# 人肉 debug:JS console 桥抽取器(JS console.log → Python 控制台)。
|
||||||
|
# 这是诊断通道,不应在普通运行中每 500ms 跨进程执行一次 JS。
|
||||||
|
self._jslog_timer = None
|
||||||
|
if self._stream_diagnostics:
|
||||||
self._jslog_timer = QtCore.QTimer(self)
|
self._jslog_timer = QtCore.QTimer(self)
|
||||||
self._jslog_timer.setInterval(500)
|
self._jslog_timer.setInterval(500)
|
||||||
self._jslog_timer.timeout.connect(self._jslog_drain_tick)
|
self._jslog_timer.timeout.connect(self._jslog_drain_tick)
|
||||||
@@ -4977,6 +5002,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
if session_id == self.current_session_id:
|
if session_id == self.current_session_id:
|
||||||
self.chat_bridge.finish_message(msg_id)
|
self.chat_bridge.finish_message(msg_id)
|
||||||
|
if self._stream_diagnostics:
|
||||||
self.browser.page().runJavaScript("dumpDiag()", self._on_diag_dumped)
|
self.browser.page().runJavaScript("dumpDiag()", self._on_diag_dumped)
|
||||||
print("\n[系统]:✅ 回复完毕。")
|
print("\n[系统]:✅ 回复完毕。")
|
||||||
else:
|
else:
|
||||||
@@ -4993,7 +5019,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
f"usage_output={_du.get('output')} timeline={len(_tl0)}条")
|
f"usage_output={_du.get('output')} timeline={len(_tl0)}条")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 完成时刻屏幕快照 + DOM 体检
|
# 完成时刻屏幕快照 + DOM 体检均为诊断操作;尤其是 WebView2 grab()
|
||||||
|
# 会同步读回原生窗口,可能阻塞渲染器数百毫秒,普通运行绝不执行。
|
||||||
|
if self._stream_diagnostics:
|
||||||
|
if os.environ.get("HAOCODE_SHOT") == "1":
|
||||||
try:
|
try:
|
||||||
self._shot_n = getattr(self, "_shot_n", 0) + 1
|
self._shot_n = getattr(self, "_shot_n", 0) + 1
|
||||||
import os as _os
|
import os as _os
|
||||||
@@ -5004,10 +5033,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
print(f"[画面] 完成快照 -> {_os.path.basename(_shot_path)}", flush=True)
|
print(f"[画面] 完成快照 -> {_os.path.basename(_shot_path)}", flush=True)
|
||||||
except Exception as _e:
|
except Exception as _e:
|
||||||
print(f"[画面] 完成截图失败: {_e}", flush=True)
|
print(f"[画面] 完成截图失败: {_e}", flush=True)
|
||||||
|
|
||||||
def _finish_dom_probe(_res):
|
def _finish_dom_probe(_res):
|
||||||
diag_log(f"FINISH_DOM {_res}")
|
diag_log(f"FINISH_DOM {_res}")
|
||||||
print(f"[画面] 完成时刻 DOM 体检: {_res}", flush=True)
|
print(f"[画面] 完成时刻 DOM 体检: {_res}", flush=True)
|
||||||
self.browser.page().runJavaScript(f"probeStream('{msg_id}')", _finish_dom_probe)
|
|
||||||
|
self.browser.page().runJavaScript(
|
||||||
|
f"probeStream('{msg_id}')", _finish_dom_probe)
|
||||||
# 🚀 入库,自动成为时间线新叶子!
|
# 🚀 入库,自动成为时间线新叶子!
|
||||||
if (stream_state["content"] or stream_state["reasoning"]
|
if (stream_state["content"] or stream_state["reasoning"]
|
||||||
or stream_state.get("timeline")):
|
or stream_state.get("timeline")):
|
||||||
@@ -5389,7 +5421,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
now = _t.time()
|
now = _t.time()
|
||||||
if not hasattr(self, "_last_probe_at"):
|
if not hasattr(self, "_last_probe_at"):
|
||||||
self._last_probe_at = 0
|
self._last_probe_at = 0
|
||||||
if now - self._last_probe_at >= 2.0:
|
if self._stream_diagnostics and now - self._last_probe_at >= 2.0:
|
||||||
self._last_probe_at = now
|
self._last_probe_at = now
|
||||||
self.browser.page().runJavaScript(
|
self.browser.page().runJavaScript(
|
||||||
f"probeStream('{st['msg_id']}')", self._on_probe_result)
|
f"probeStream('{st['msg_id']}')", self._on_probe_result)
|
||||||
@@ -5438,7 +5470,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
"""接收到 Token"""
|
"""接收到 Token"""
|
||||||
self._diag_chunk_n += 1
|
self._diag_chunk_n += 1
|
||||||
_match = session_id == self.current_session_id
|
_match = session_id == self.current_session_id
|
||||||
if self._diag_chunk_n <= 3 or self._diag_chunk_n % 50 == 0 or not _match:
|
if self._stream_diagnostics and (
|
||||||
|
self._diag_chunk_n <= 3 or self._diag_chunk_n % 50 == 0 or not _match):
|
||||||
_st0 = self._active_streams.get(session_id)
|
_st0 = self._active_streams.get(session_id)
|
||||||
diag_log(f"CHUNK n={self._diag_chunk_n} match={_match} "
|
diag_log(f"CHUNK n={self._diag_chunk_n} match={_match} "
|
||||||
f"mid={(_st0 or {}).get('msg_id')} +{len(chunk)}c")
|
f"mid={(_st0 or {}).get('msg_id')} +{len(chunk)}c")
|
||||||
@@ -5458,7 +5491,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
else:
|
else:
|
||||||
st["timeline"][-1]["text"] += chunk
|
st["timeline"][-1]["text"] += chunk
|
||||||
|
|
||||||
# 人肉 debug:每个正文 token 控制台打印
|
# 人肉 debug:每个正文 token 控制台打印(仅显式诊断时开启)。
|
||||||
|
if self._stream_diagnostics:
|
||||||
try:
|
try:
|
||||||
_mid = self._active_streams[session_id]["msg_id"]
|
_mid = self._active_streams[session_id]["msg_id"]
|
||||||
_flat = " ".join(chunk.split())
|
_flat = " ".join(chunk.split())
|
||||||
@@ -5825,7 +5859,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
"""接收到思考过程"""
|
"""接收到思考过程"""
|
||||||
self._diag_think_n += 1
|
self._diag_think_n += 1
|
||||||
_match = session_id == self.current_session_id
|
_match = session_id == self.current_session_id
|
||||||
if self._diag_think_n <= 3 or self._diag_think_n % 50 == 0 or not _match:
|
if self._stream_diagnostics and (
|
||||||
|
self._diag_think_n <= 3 or self._diag_think_n % 50 == 0 or not _match):
|
||||||
_st0 = self._active_streams.get(session_id)
|
_st0 = self._active_streams.get(session_id)
|
||||||
diag_log(f"THINK n={self._diag_think_n} match={_match} "
|
diag_log(f"THINK n={self._diag_think_n} match={_match} "
|
||||||
f"mid={(_st0 or {}).get('msg_id')} +{len(chunk)}c")
|
f"mid={(_st0 or {}).get('msg_id')} +{len(chunk)}c")
|
||||||
@@ -5845,7 +5880,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
else:
|
else:
|
||||||
st["timeline"][-1]["text"] += chunk
|
st["timeline"][-1]["text"] += chunk
|
||||||
|
|
||||||
# 人肉 debug:每个思考 token 控制台打印
|
# 人肉 debug:每个思考 token 控制台打印(仅显式诊断时开启)。
|
||||||
|
if self._stream_diagnostics:
|
||||||
try:
|
try:
|
||||||
_mid = self._active_streams[session_id]["msg_id"]
|
_mid = self._active_streams[session_id]["msg_id"]
|
||||||
_flat = " ".join(chunk.split())
|
_flat = " ".join(chunk.split())
|
||||||
@@ -5898,6 +5934,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
"""JS 就绪检测的回调"""
|
"""JS 就绪检测的回调"""
|
||||||
if is_ready:
|
if is_ready:
|
||||||
print("[System]:✅ JS 引擎已就绪,正在加载历史记录...")
|
print("[System]:✅ JS 引擎已就绪,正在加载历史记录...")
|
||||||
|
if self._stream_diagnostics:
|
||||||
|
self.chat_bridge.run_js(
|
||||||
|
"if (window.setStreamDiagnostics) window.setStreamDiagnostics(true);")
|
||||||
# 🆕 P1-01:先注入渲染窗口配置,再开始窗口化加载
|
# 🆕 P1-01:先注入渲染窗口配置,再开始窗口化加载
|
||||||
if not self._rw_config_pushed:
|
if not self._rw_config_pushed:
|
||||||
self._rw_config_pushed = True
|
self._rw_config_pushed = True
|
||||||
|
|||||||
+67
-15
@@ -31,23 +31,40 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
window.__APP_VER = '20260721-v7';
|
window.__APP_VER = '20260721-v7';
|
||||||
|
// 流式逐 token 诊断默认关闭。逐 token 读取 DOM 高度/文本并写 console 会
|
||||||
|
// 触发强制布局和跨进程日志传输,长时间运行会明显拖慢渲染器;需要排查时
|
||||||
|
// 由 Python 显式调用 setStreamDiagnostics(true) 打开。
|
||||||
|
window.__STREAM_DIAG = false;
|
||||||
|
window.setStreamDiagnostics = function(enabled) {
|
||||||
|
window.__STREAM_DIAG = !!enabled;
|
||||||
|
if (window.__STREAM_DIAG && window.__startStreamHeartbeat) {
|
||||||
|
window.__startStreamHeartbeat();
|
||||||
|
}
|
||||||
|
};
|
||||||
console.log('[JS] ===== app.js 加载 ver=20260721-v7 =====');
|
console.log('[JS] ===== app.js 加载 ver=20260721-v7 =====');
|
||||||
// 公式渲染依赖本地 KaTeX(离线);此处确认资源加载结果,缺失时打印告警便于定位
|
// 公式渲染依赖本地 KaTeX(离线);此处确认资源加载结果,缺失时打印告警便于定位
|
||||||
console.log('[JS] KaTeX ' + (typeof katex !== 'undefined' ? katex.version + ' 就绪' : '缺失(公式将退化为纯文本)'));
|
console.log('[JS] KaTeX ' + (typeof katex !== 'undefined' ? katex.version + ' 就绪' : '缺失(公式将退化为纯文本)'));
|
||||||
// 渲染器主线程心跳:dt 异常大 = 渲染器被阻塞(截图/重绘/GPU 等)
|
// 渲染器主线程心跳:dt 异常大 = 渲染器被阻塞(截图/重绘/GPU 等)
|
||||||
(function() {
|
(function() {
|
||||||
var _hbLast = Date.now();
|
var _hbLast = Date.now();
|
||||||
setInterval(function() {
|
var _started = false;
|
||||||
|
function tick() {
|
||||||
var _now = Date.now();
|
var _now = Date.now();
|
||||||
var _dt = _now - _hbLast;
|
var _dt = _now - _hbLast;
|
||||||
_hbLast = _now;
|
_hbLast = _now;
|
||||||
if (_dt >= 1500) {
|
if (_dt >= 1500) {
|
||||||
console.log('[JS] 心跳 dt=' + _dt + 'ms (渲染器主线程曾卡顿)');
|
console.log('[JS] 心跳 dt=' + _dt + 'ms (渲染器主线程曾卡顿)');
|
||||||
}
|
}
|
||||||
}, 1000);
|
}
|
||||||
|
window.__startStreamHeartbeat = function() {
|
||||||
|
if (_started) return;
|
||||||
|
_started = true;
|
||||||
|
setInterval(tick, 1000);
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
window.__diag = { events: [], cap: 300, tokenN: 0, thinkN: 0 };
|
window.__diag = { events: [], cap: 300, tokenN: 0, thinkN: 0 };
|
||||||
function diagEvent(name, extra) {
|
function diagEvent(name, extra) {
|
||||||
|
if (!window.__STREAM_DIAG) return;
|
||||||
try {
|
try {
|
||||||
var d = window.__diag;
|
var d = window.__diag;
|
||||||
d.events.push({ t: Date.now() % 1000000, e: name, x: extra });
|
d.events.push({ t: Date.now() % 1000000, e: name, x: extra });
|
||||||
@@ -749,7 +766,8 @@ function createMessage(msgId, role, initialText, senderName, branchInfo) {
|
|||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
replyDiv.innerText = initialText;
|
replyDiv.innerText = initialText;
|
||||||
} else {
|
} else {
|
||||||
// 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment → 与时间线路径同样有浅灰气泡背景
|
// 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment,
|
||||||
|
// 与时间线路径保持相同的透明正文布局
|
||||||
var _tHtml = typeof marked !== 'undefined' ? safeHtml(marked.parse(initialText)) : safeHtml(initialText);
|
var _tHtml = typeof marked !== 'undefined' ? safeHtml(marked.parse(initialText)) : safeHtml(initialText);
|
||||||
replyDiv.innerHTML = '<div class="md-segment markdown-body">' + _tHtml + '</div>';
|
replyDiv.innerHTML = '<div class="md-segment markdown-body">' + _tHtml + '</div>';
|
||||||
}
|
}
|
||||||
@@ -1208,7 +1226,8 @@ function syncRenderThrottled(msgId, minGapMs) {
|
|||||||
// Qt 看门狗入口:强制渲染(幂等,尾部无变化时开销极小)
|
// Qt 看门狗入口:强制渲染(幂等,尾部无变化时开销极小)
|
||||||
function forceRenderNow(msgId) {
|
function forceRenderNow(msgId) {
|
||||||
var _frNow = Date.now();
|
var _frNow = Date.now();
|
||||||
if (!window.__frLast || _frNow - window.__frLast > 1000) {
|
if (window.__STREAM_DIAG &&
|
||||||
|
(!window.__frLast || _frNow - window.__frLast > 1000)) {
|
||||||
window.__frLast = _frNow;
|
window.__frLast = _frNow;
|
||||||
console.log('[JS] forceRenderNow id=' + msgId + ' (看门狗)');
|
console.log('[JS] forceRenderNow id=' + msgId + ' (看门狗)');
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1255,7 @@ function _fullRenderSegment(el, c, isThink) {
|
|||||||
}
|
}
|
||||||
function doStreamingRender(msgId) {
|
function doStreamingRender(msgId) {
|
||||||
window.__renderN = (window.__renderN || 0) + 1;
|
window.__renderN = (window.__renderN || 0) + 1;
|
||||||
if (window.__renderN % 10 === 1) {
|
if (window.__STREAM_DIAG && window.__renderN % 10 === 1) {
|
||||||
console.log('[JS] render#' + window.__renderN + ' 开始 id=' + msgId);
|
console.log('[JS] render#' + window.__renderN + ' 开始 id=' + msgId);
|
||||||
}
|
}
|
||||||
var buf = messageBuffer[msgId];
|
var buf = messageBuffer[msgId];
|
||||||
@@ -1288,10 +1307,12 @@ function appendReasoning(msgId, token) {
|
|||||||
buf.reasoning += token;
|
buf.reasoning += token;
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { diagEvent('appendReasoning', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) { diagEvent('appendReasoning', 'NO_WRAPPER:' + msgId); return; }
|
||||||
|
if (window.__STREAM_DIAG) {
|
||||||
window.__diag.thinkN++;
|
window.__diag.thinkN++;
|
||||||
if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) {
|
if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) {
|
||||||
diagEvent('appendReasoning', 'n=' + window.__diag.thinkN);
|
diagEvent('appendReasoning', 'n=' + window.__diag.thinkN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
wrapper.classList.add('streaming');
|
wrapper.classList.add('streaming');
|
||||||
var tl = ensureTimeline(wrapper, buf);
|
var tl = ensureTimeline(wrapper, buf);
|
||||||
if (!tl) return;
|
if (!tl) return;
|
||||||
@@ -1308,7 +1329,11 @@ function appendReasoning(msgId, token) {
|
|||||||
buf.thinkSegs.push(tc);
|
buf.thinkSegs.push(tc);
|
||||||
}
|
}
|
||||||
tc.__buf = (tc.__buf || '') + token;
|
tc.__buf = (tc.__buf || '') + token;
|
||||||
console.log('[JS] think#' + window.__diag.thinkN + ' 段buf=' + tc.__buf.length + 'c dom=' + (tc.textContent || '').length + 'c h=' + tc.offsetHeight);
|
if (window.__STREAM_DIAG) {
|
||||||
|
console.log('[JS] think#' + window.__diag.thinkN +
|
||||||
|
' 段buf=' + tc.__buf.length + 'c dom=' +
|
||||||
|
(tc.textContent || '').length + 'c h=' + tc.offsetHeight);
|
||||||
|
}
|
||||||
syncRenderThrottled(msgId); // ★ 同步通道
|
syncRenderThrottled(msgId); // ★ 同步通道
|
||||||
scheduleStreamingRender(msgId);
|
scheduleStreamingRender(msgId);
|
||||||
}
|
}
|
||||||
@@ -1319,10 +1344,12 @@ function appendToken(msgId, token) {
|
|||||||
buf.content += token;
|
buf.content += token;
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { diagEvent('appendToken', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) { diagEvent('appendToken', 'NO_WRAPPER:' + msgId); return; }
|
||||||
|
if (window.__STREAM_DIAG) {
|
||||||
window.__diag.tokenN++;
|
window.__diag.tokenN++;
|
||||||
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
||||||
diagEvent('appendToken', 'n=' + window.__diag.tokenN);
|
diagEvent('appendToken', 'n=' + window.__diag.tokenN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
wrapper.classList.add('streaming');
|
wrapper.classList.add('streaming');
|
||||||
var tl = ensureTimeline(wrapper, buf);
|
var tl = ensureTimeline(wrapper, buf);
|
||||||
if (!tl) return;
|
if (!tl) return;
|
||||||
@@ -1345,10 +1372,15 @@ function appendToken(msgId, token) {
|
|||||||
} else {
|
} else {
|
||||||
seg.__buf = (seg.__buf || '') + token;
|
seg.__buf = (seg.__buf || '') + token;
|
||||||
}
|
}
|
||||||
console.log('[JS] token#' + window.__diag.tokenN + ' 段buf=' + seg.__buf.length + 'c dom=' + (seg.textContent || '').length + 'c 段数=' + buf.textSegs.length + ' h=' + seg.offsetHeight);
|
if (window.__STREAM_DIAG) {
|
||||||
|
console.log('[JS] token#' + window.__diag.tokenN +
|
||||||
|
' 段buf=' + seg.__buf.length + 'c dom=' +
|
||||||
|
(seg.textContent || '').length + 'c 段数=' +
|
||||||
|
buf.textSegs.length + ' h=' + seg.offsetHeight);
|
||||||
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
||||||
diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length });
|
diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
syncRenderThrottled(msgId); // ★ 同步通道:token 到 → 内容必现
|
syncRenderThrottled(msgId); // ★ 同步通道:token 到 → 内容必现
|
||||||
scheduleStreamingRender(msgId); // rAF 通道:更平滑(环境允许时)
|
scheduleStreamingRender(msgId); // rAF 通道:更平滑(环境允许时)
|
||||||
}
|
}
|
||||||
@@ -1357,7 +1389,12 @@ function appendToken(msgId, token) {
|
|||||||
function finishMessage(msgId) {
|
function finishMessage(msgId) {
|
||||||
console.log('[JS] finishMessage id=' + msgId);
|
console.log('[JS] finishMessage id=' + msgId);
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { delete messageBuffer[msgId]; diagEvent('finish', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) {
|
||||||
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
diagEvent('finish', 'NO_WRAPPER:' + msgId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
diagEvent('finish', { id: msgId });
|
diagEvent('finish', { id: msgId });
|
||||||
var buf = messageBuffer[msgId];
|
var buf = messageBuffer[msgId];
|
||||||
cancelStreamingRender(msgId);
|
cancelStreamingRender(msgId);
|
||||||
@@ -1440,6 +1477,7 @@ function finishMessage(msgId) {
|
|||||||
if (buf.raf) { cancelAnimationFrame(buf.raf); buf.raf = 0; }
|
if (buf.raf) { cancelAnimationFrame(buf.raf); buf.raf = 0; }
|
||||||
}
|
}
|
||||||
delete messageBuffer[msgId];
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
|
||||||
// --- H. 刷新自定义滚动条 ---
|
// --- H. 刷新自定义滚动条 ---
|
||||||
reportWebScroll();
|
reportWebScroll();
|
||||||
@@ -1940,6 +1978,7 @@ function renderTimelineHistory(msgId, timelineJson) {
|
|||||||
// 删除 buffer → finishMessage 不再全量重渲(避免破坏时间线 DOM),
|
// 删除 buffer → finishMessage 不再全量重渲(避免破坏时间线 DOM),
|
||||||
// 但仍会执行代码高亮/滚动等收尾
|
// 但仍会执行代码高亮/滚动等收尾
|
||||||
delete messageBuffer[msgId];
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[JS]: renderTimelineHistory 解析失败', e);
|
console.error('[JS]: renderTimelineHistory 解析失败', e);
|
||||||
@@ -2063,6 +2102,23 @@ function hideLoadingOverlay() {
|
|||||||
setTimeout(function() { el.style.display = 'none'; }, 480);
|
setTimeout(function() { el.style.display = 'none'; }, 480);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 消息从当前窗口移除时一并释放附件缓存。附件使用 msgId-att-N 作为键,
|
||||||
|
// 只删除父消息键会让长文本和元数据跨会话持续留在内存中。
|
||||||
|
function clearMessageStores(msgId) {
|
||||||
|
if (!msgId) return;
|
||||||
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
delete longTextStore[msgId];
|
||||||
|
delete finalContentStore[msgId];
|
||||||
|
delete attachmentMetaStore[msgId];
|
||||||
|
var prefix = msgId + '-att-';
|
||||||
|
[longTextStore, attachmentMetaStore].forEach(function(store) {
|
||||||
|
Object.keys(store).forEach(function(key) {
|
||||||
|
if (key.indexOf(prefix) === 0) delete store[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 历史记录与视图控制 ====================
|
// ==================== 历史记录与视图控制 ====================
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
var bubbles = chatContainer.querySelectorAll('.message-wrapper, .system-note');
|
var bubbles = chatContainer.querySelectorAll('.message-wrapper, .system-note');
|
||||||
@@ -2073,6 +2129,7 @@ function clearChat() {
|
|||||||
longTextStore = {};
|
longTextStore = {};
|
||||||
finalContentStore = {};
|
finalContentStore = {};
|
||||||
attachmentMetaStore = {};
|
attachmentMetaStore = {};
|
||||||
|
__lastSyncRender = {};
|
||||||
|
|
||||||
// 🆕 P1-01:清渲染窗口状态机(游标/缓存/未决请求/代次;配置模式与大小保留)
|
// 🆕 P1-01:清渲染窗口状态机(游标/缓存/未决请求/代次;配置模式与大小保留)
|
||||||
if (typeof rwState !== 'undefined' && rwState && typeof RenderWindowState !== 'undefined') {
|
if (typeof rwState !== 'undefined' && rwState && typeof RenderWindowState !== 'undefined') {
|
||||||
@@ -2093,9 +2150,7 @@ function deleteMessage(msgId) {
|
|||||||
wrapper.style.transform = "translateY(-10px)";
|
wrapper.style.transform = "translateY(-10px)";
|
||||||
setTimeout(function() { wrapper.remove(); }, 300);
|
setTimeout(function() { wrapper.remove(); }, 300);
|
||||||
}
|
}
|
||||||
delete messageBuffer[msgId];
|
clearMessageStores(msgId);
|
||||||
delete longTextStore[msgId];
|
|
||||||
delete finalContentStore[msgId];
|
|
||||||
setTimeout(reportWebScroll, 350);
|
setTimeout(reportWebScroll, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2267,10 +2322,7 @@ function rwCaptureAnchor() {
|
|||||||
function rwRemoveMessageDom(msgId) {
|
function rwRemoveMessageDom(msgId) {
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (wrapper) wrapper.remove();
|
if (wrapper) wrapper.remove();
|
||||||
delete messageBuffer[msgId];
|
clearMessageStores(msgId);
|
||||||
delete longTextStore[msgId];
|
|
||||||
delete finalContentStore[msgId];
|
|
||||||
delete attachmentMetaStore[msgId];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载入口:固定在 chat-container 首/尾;无更多消息时隐藏。 */
|
/* 加载入口:固定在 chat-container 首/尾;无更多消息时隐藏。 */
|
||||||
|
|||||||
+6
-6
@@ -157,7 +157,7 @@ body, html {
|
|||||||
width: 85%;
|
width: 85%;
|
||||||
}
|
}
|
||||||
/* 🆕 内层同步撑满:.reply-content 基类是 fit-content(贴内容缩),
|
/* 🆕 内层同步撑满:.reply-content 基类是 fit-content(贴内容缩),
|
||||||
可见气泡(md-segment 背景/思考卡/工具卡)都在它里面 → 必须 100% 才真正恒定 */
|
可见内容块(正文段/思考卡/工具卡)都在它里面 → 必须 100% 才真正恒定 */
|
||||||
.assistant .reply-content { width: 100%; }
|
.assistant .reply-content { width: 100%; }
|
||||||
.user .message-content { align-items: flex-end; }
|
.user .message-content { align-items: flex-end; }
|
||||||
|
|
||||||
@@ -893,15 +893,15 @@ body, html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ========== 助手正文气泡(流式中/完成后一致) ========== */
|
/* ========== 助手正文(流式中/完成后一致,保持透明) ========== */
|
||||||
.message-wrapper.assistant .reply-content .md-segment {
|
.message-wrapper.assistant .reply-content .md-segment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px 14px;
|
padding: 0;
|
||||||
color: #30343b;
|
color: #30343b;
|
||||||
background-color: #f6f7f9;
|
background: transparent;
|
||||||
border: 1px solid #eceff3;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.message-wrapper.assistant .reply-content .md-segment + .md-segment {
|
.message-wrapper.assistant .reply-content .md-segment + .md-segment {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user