Files
EzVibe/docs/v4_modify/v4_patch.diff
e2hang 96cb28fe08 feat: multi-layer eventFilter for window drag + text input fix + Qt platform xcb
- 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>
2026-05-19 23:36:58 +08:00

192 lines
7.8 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 从任何线程安全调用。