# v4_patch.diff — PetWindow 跨线程调用修复 ## 变更文件 - `ui/pet_window.py` - `agent/memory.py` ## 变更内容 --- ### ui/pet_window.py #### 1. ChatBubbleWidget 添加 `_AddHelper` QObject(跨线程转发用) ```diff class ChatBubbleWidget: def __init__(self, parent, QtWidgets, QtCore): self._parent = parent self._QtWidgets = QtWidgets self._QtCore = QtCore self._messages: list[tuple[str, bool]] = [] # (text, is_self) + # 跨线程消息转发 helper(带 @Slot 的 QObject,用于 QMetaObject.invokeMethod) + class _AddHelper(QtCore.QObject): + @QtCore.Slot(str, bool) + def do_add(self, text, is_self): + self._cb._do_add_message(text, is_self) + self._add_helper = _AddHelper(parent) + self._add_helper._cb = self self._setup_ui(parent) ``` #### 2. `add_message` 中的线程检查改用 `_add_helper` ```diff def add_message(self, text: str, is_self: bool = False): """添加一条消息到列表(强制主线程执行)""" - # 所有 Qt 操作必须在主线程,先转发 - if self._QtWidgets.QApplication.instance().thread() != self._container.thread(): - self._QtCore.QMetaObject.invokeMethod( - self._container, - lambda: self._do_add_message(text, is_self), - self._QtCore.Qt.QConnectionType.QueuedConnection - ) - return + # 所有 Qt 操作必须在主线程,通过 helper QObject 转发 + if self._QtWidgets.QApplication.instance().thread() != self._container.thread(): + self._QtCore.QMetaObject.invokeMethod( + self._add_helper, + "do_add", + self._QtCore.Qt.ConnectionType.QueuedConnection, + self._QtCore.Q_ARG(str, text), + self._QtCore.Q_ARG(bool, is_self), + ) + return self._do_add_message(text, is_self) ``` #### 3. `_on_brain_tick` 中改用 `invokeMethod` ```diff def _on_brain_tick(self): if not self._brain or not self._running: return def _do_tick(): try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) result = loop.run_until_complete( self._brain.decide_action() ) loop.close() if result and result.get("action"): action = result["action"] - # show_reminder 操作必须在主线程,用 QTimer.singleShot 转发 + # show_reminder 操作必须在主线程,通过 helper QObject 的 QueuedConnection 转发 if hasattr(self, "_chat_bubble"): msg = action.get("message", "") - self._QtCore.QTimer.singleShot( - 0, - lambda m=msg: self._chat_bubble.add_message(m, False), - ) + self._QtCore.QMetaObject.invokeMethod( + self._chat_bubble._add_helper, + "do_add", + self._QtCore.Qt.ConnectionType.QueuedConnection, + self._QtCore.Q_ARG(str, msg), + self._QtCore.Q_ARG(bool, False), + ) except Exception: pass ``` #### 4. `_on_user_message` 中 reply 和 fallback 改用 `invokeMethod` ```diff def _on_user_message(self, text: str): if not text.strip(): return if hasattr(self, "_chat_bubble"): self._chat_bubble.add_message(text, is_self=True) def _do_think(): try: import asyncio loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) result = loop.run_until_complete( self._brain.think(text) if self._brain else None ) loop.close() if result and result.get("text"): reply = result["text"] - # 显示宠物回复(转发到主线程) + # 显示宠物回复(转发到主线程,通过 helper QObject 的 QueuedConnection) if hasattr(self, "_chat_bubble"): - self._QtCore.QTimer.singleShot( - 0, - lambda r=reply: self._chat_bubble.add_message(r, False), - ) + self._QtCore.QMetaObject.invokeMethod( + self._chat_bubble._add_helper, + "do_add", + self._QtCore.Qt.ConnectionType.QueuedConnection, + self._QtCore.Q_ARG(str, reply), + self._QtCore.Q_ARG(bool, False), + ) except Exception as exc: if hasattr(self, "_chat_bubble"): fallback = self._fallback_reply(text) - self._QtCore.QTimer.singleShot( - 0, - lambda f=fallback: self._chat_bubble.add_message(f, False), - ) + self._QtCore.QMetaObject.invokeMethod( + self._chat_bubble._add_helper, + "do_add", + self._QtCore.Qt.ConnectionType.QueuedConnection, + self._QtCore.Q_ARG(str, fallback), + self._QtCore.Q_ARG(bool, False), + ) t = threading.Thread(target=_do_think, daemon=True) t.start() ``` #### 5. PetWindowClass 字典补加 `_fallback_reply` ```diff "_on_tray_activated": _on_tray_activated, "_on_brain_tick": _on_brain_tick, "_on_user_message": _on_user_message, + "_fallback_reply": _fallback_reply, "mousePressEvent": mousePressEvent, ``` --- ### agent/memory.py #### `get_all()` 返回 `list[MemoryEntry]` 而非 `list[tuple]`,修复 `_embed_text` 中的下标访问 ```diff --- a/agent/memory.py +++ b/agent/memory.py @@ -767,7 +767,7 @@ class VectorMemory: embedder = self._embedder if hasattr(embedder, "_fitted") and not getattr(embedder, "_fitted", True): # 懒拟合:用现有语料库 fit TF-IDF - texts = [row[1] for row in self._store.get_all()] + texts = [entry.text for entry in self._store.get_all()] if texts: embedder.fit(texts + [text]) else: ``` --- ## 修复说明 ### 为什么用 `QMetaObject.invokeMethod(QueuedConnection)` 而不是 `QTimer.singleShot`? `QTimer.singleShot(0, callback)` 在 qasync 包装的事件循环中无法触发回调——qasync 将 Qt 事件循环包装为 asyncio 事件循环,但 `QTimer.singleShot` 的定时器事件无法被 qasync 的 `run_forever()` 检测到。 `QMetaObject.invokeMethod` + `Qt.ConnectionType.QueuedConnection` 始终工作,因为它是将调用排队到目标 QObject 所在线程的事件队列,qasync 能正确处理 Qt 的 queued 事件。 ### 为什么需要 `@QtCore.Slot` 装饰器? 只有 QObject 子类的方法才能通过 `QMetaObject.invokeMethod` 的 QueuedConnection 被调用。非 QObject 类的普通方法(如 `ChatBubbleWidget.add_message`)即使签名匹配也无法通过 `invokeMethod` 调用——因为 `ChatBubbleWidget` 不是 QObject,没有元对象系统。 `_AddHelper` 是 `QtCore.QObject` 子类,其 `do_add` 方法标有 `@QtCore.Slot(str, bool)` 装饰器,所以可以通过 `invokeMethod` + QueuedConnection 从任何线程安全调用。