- Fix window drag: install eventFilter on live2d_container, central, and _live2d_widget; fix super() call in dynamic class - Fix text input: remove WA_TransparentForMouseEvents from _chat_container - Force QT_QPA_PLATFORM=xcb on Linux (wayland has mouse event issues) - Add HealthTracker module, update AgentBrain with health integration - Update scheduler and memory modules - Add v5_modify documentation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
192 lines
7.8 KiB
Diff
192 lines
7.8 KiB
Diff
# 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 从任何线程安全调用。
|