- 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>
303 lines
8.2 KiB
Markdown
303 lines
8.2 KiB
Markdown
# EzVibe v2 演进修改记录
|
||
|
||
> 本文档记录从 EVOLUTION_ROADMAP.md 出发的所有实际代码改动。
|
||
> 对应 Phase 1 和 Phase 2 的已完成项。
|
||
|
||
---
|
||
|
||
## Phase 1:底层重构与稳定性
|
||
|
||
### Phase 1.1 — qasync 替换 QTimer 方案
|
||
|
||
**改动文件**:[main.py](main.py#L320-L360)
|
||
|
||
**现状(旧)**:
|
||
```python
|
||
timer = QTimer(app)
|
||
timer.timeout.connect(_run_async)
|
||
timer.setInterval(20) # 50 Hz
|
||
timer.start()
|
||
app.exec()
|
||
```
|
||
|
||
**目标**:引入 qasync,实现 Qt 事件循环与 asyncio 的原生融合。
|
||
|
||
**改动后**:
|
||
```python
|
||
import qasync
|
||
|
||
qasync_loop = qasync.QEventLoop(app)
|
||
asyncio.set_event_loop(qasync_loop)
|
||
|
||
async def _run_scheduler_async():
|
||
while getattr(self, "_running", False):
|
||
# 异步调度循环,每 10 秒检查一次
|
||
triggered = await self._scheduler.check_and_trigger(user_activity_level=activity)
|
||
for action in triggered:
|
||
self._window.show_reminder(...)
|
||
await asyncio.sleep(10)
|
||
|
||
scheduler_task = qasync_loop.create_task(_run_scheduler_async())
|
||
|
||
with qasync_loop:
|
||
qasync_loop.run_forever()
|
||
```
|
||
|
||
**依赖**:`requirements.txt` 新增 `qasync>=0.6.0`
|
||
|
||
---
|
||
|
||
### Phase 1.3 — 新增 HealthTracker 模块
|
||
|
||
**新增文件**:[agent/health_tracker.py](agent/health_tracker.py)
|
||
|
||
**数据库 Schema**:
|
||
```sql
|
||
CREATE TABLE health_events (
|
||
id TEXT PRIMARY KEY,
|
||
event_type TEXT NOT NULL, -- 'water' | 'stand' | 'stretch' | 'screen_break'
|
||
timestamp REAL NOT NULL,
|
||
source TEXT NOT NULL, -- 'user_action' | 'reminder_confirmed' | 'implicit_detected'
|
||
metadata TEXT NOT NULL, -- JSON
|
||
created_at REAL NOT NULL
|
||
);
|
||
|
||
CREATE TABLE daily_stats (
|
||
date TEXT PRIMARY KEY, -- 'YYYY-MM-DD'
|
||
water_count INTEGER DEFAULT 0,
|
||
stand_count INTEGER DEFAULT 0,
|
||
stretch_count INTEGER DEFAULT 0,
|
||
sedentary_minutes INTEGER DEFAULT 0,
|
||
screen_time_minutes INTEGER DEFAULT 0,
|
||
last_stand_at REAL,
|
||
last_water_at REAL,
|
||
updated_at REAL NOT NULL
|
||
);
|
||
|
||
CREATE TABLE sedentary_alerts (
|
||
id TEXT PRIMARY KEY,
|
||
triggered_at REAL NOT NULL,
|
||
acknowledged INTEGER DEFAULT 0,
|
||
acknowledged_at REAL,
|
||
message TEXT
|
||
);
|
||
```
|
||
|
||
**核心接口**:
|
||
- `record_water()` / `record_stand()` / `record_stretch()` — 记录健康事件
|
||
- `get_today_stats()` — 返回今日统计 dict
|
||
- `get_consecutive_sedentary_minutes()` — 当前连续静坐分钟数
|
||
- `get_health_context_for_llm()` — 生成注入 LLM System Prompt 的健康上下文
|
||
- `trigger_sedentary_alert()` / `acknowledge_alert()` — 警报管理
|
||
- `get_weekly_report()` / `get_streak()` — 统计聚合
|
||
|
||
**与 main.py 集成**:
|
||
```python
|
||
# main.py _init_components()
|
||
from agent.health_tracker import HealthTracker
|
||
self._health_tracker = HealthTracker(db_path="data/health.db")
|
||
self._brain = AgentBrain(
|
||
...,
|
||
health_tracker=self._health_tracker,
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 2:感知与决策升级
|
||
|
||
### Phase 2.1 — DisplayMode 柔性降级
|
||
|
||
**改动文件**:[agent/scheduler.py](agent/scheduler.py#L35-L52)
|
||
|
||
**新增枚举**:
|
||
```python
|
||
class DisplayMode(Enum):
|
||
NORMAL = "normal" # 正常打扰(气泡通知)
|
||
QUIET = "quiet" # 安静模式(缩角落举牌子)
|
||
AGGRESSIVE = "aggressive" # 强制弹窗(高优先级警告)
|
||
```
|
||
|
||
**Behavior 数据结构变更**:
|
||
```python
|
||
@dataclass
|
||
class Behavior:
|
||
...
|
||
display_mode: DisplayMode = DisplayMode.NORMAL # 新增字段
|
||
```
|
||
|
||
**逻辑变更**(`_is_activity_restricted`):
|
||
- 原设计:`annoyed` 时完全屏蔽 P0 打扰
|
||
- 新设计:`annoyed` 时 P0 降级为 QUIET 模式,不完全屏蔽
|
||
|
||
```python
|
||
# 烦躁时 P0 提醒降级为 QUIET(不屏蔽,改展示模式)
|
||
if emotion == "annoyed" and behavior.priority == 0:
|
||
behavior.display_mode = DisplayMode.QUIET
|
||
|
||
return False # 不再直接屏蔽,而是降级
|
||
```
|
||
|
||
**行为触发返回时附加 display_mode**:
|
||
```python
|
||
result["display_mode"] = behavior.display_mode.value
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 2.2 — LLM Prompt 工程升级(HealthTracker 上下文注入)
|
||
|
||
**改动文件**:[agent/brain.py](agent/brain.py#L36-L60)
|
||
|
||
**System Prompt 变更**:
|
||
```python
|
||
DEFAULT_SYSTEM_PROMPT = """...
|
||
【情绪驱动行为规则】
|
||
...
|
||
|
||
{health_context} <!-- 新增占位符 -->
|
||
|
||
【主动行为能力】
|
||
...
|
||
"""
|
||
```
|
||
|
||
**AgentBrain 初始化变更**:
|
||
```python
|
||
def __init__(
|
||
self,
|
||
...,
|
||
health_tracker: Any = None, # 新增参数
|
||
) -> None:
|
||
...
|
||
self._health_tracker = health_tracker
|
||
```
|
||
|
||
**think() 方法中注入健康上下文**:
|
||
```python
|
||
async def think(self, user_input: str, ...):
|
||
# 2. 获取健康上下文(Phase 2.2 — HealthTracker 数据注入)
|
||
health_context = ""
|
||
if self._health_tracker:
|
||
try:
|
||
health_context = await self._health_tracker.get_health_context_for_llm()
|
||
except Exception as exc:
|
||
logger.warning("[Brain] 健康数据获取失败: %s", exc)
|
||
health_context = "【用户健康状态】暂无数据"
|
||
|
||
# 3. 构建系统提示词(注入情绪 + 健康上下文)
|
||
system_prompt = self._system_prompt.format(
|
||
emotion_display=emotion_display,
|
||
emotion_state=emotion,
|
||
health_context=health_context, # 注入
|
||
)
|
||
```
|
||
|
||
**main.py 集成**:
|
||
```python
|
||
self._health_tracker = HealthTracker(db_path="data/health.db")
|
||
self._brain = AgentBrain(
|
||
llm_backend=self._llm_backend,
|
||
llm_config=self._llm_config,
|
||
emotion_engine=self._emotion,
|
||
memory=self._memory,
|
||
health_tracker=self._health_tracker, # 传入
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 2.3 — 解耦"动作触发"与"文案生成"(FallbackMessageCache)
|
||
|
||
**改动文件**:[agent/brain.py](agent/brain.py#L196-L248)
|
||
|
||
**新增类**:
|
||
```python
|
||
class FallbackMessageCache:
|
||
"""
|
||
P0 健康提醒的 Fallback 文案缓存。
|
||
|
||
设计文档:decide_action() 等待 LLM 生成文案后才执行 UI 动作。
|
||
改进:P0 健康提醒立即执行 UI 动作,LLM 文案异步加载或使用 Fallback 缓存。
|
||
"""
|
||
|
||
FALLBACK_MESSAGES: dict[str, list[str]] = {
|
||
"remind_water": [
|
||
"记得喝水哦~",
|
||
"该补充水分了!",
|
||
"喝水时间到!",
|
||
"滴—— 喝水提醒!💧",
|
||
"身体需要水份~来一杯吧!",
|
||
],
|
||
"remind_stretch": [
|
||
"起来伸展一下吧!",
|
||
"坐了好久啦,站起来动动!",
|
||
"伸个懒腰吧~身体需要活动!",
|
||
"⚠️ 久坐提醒:起身动一动!",
|
||
"站起来伸展一下,缓解疲劳~",
|
||
],
|
||
"nudge_idle": [...],
|
||
"nudge_happy": [...],
|
||
}
|
||
|
||
def get(self, action_type: str) -> str:
|
||
messages = self.FALLBACK_MESSAGES.get(action_type, [])
|
||
if not messages:
|
||
return f"[{action_type}]"
|
||
return self._rng.choice(messages)
|
||
```
|
||
|
||
**AgentBrain 集成**:
|
||
```python
|
||
def __init__(...):
|
||
...
|
||
self._fallback_cache = FallbackMessageCache()
|
||
```
|
||
|
||
**decide_action 变更**(`_decide_proactive_action`):
|
||
```python
|
||
# P0 规则:高频工作 + 非烦躁状态 → 强制健康提醒
|
||
# Phase 2.3:使用 Fallback 文案缓存,立即返回不等待 LLM
|
||
if activity < 0.15 and emotion != "annoyed":
|
||
if self._check_cooldown("remind_health"):
|
||
return {
|
||
"type": "remind_stretch",
|
||
"message": self._fallback_cache.get("remind_stretch"),
|
||
"priority": 0,
|
||
}
|
||
|
||
# 喝水提醒(更低优先级)
|
||
if activity < 0.4 and self._check_cooldown("remind_water"):
|
||
return {
|
||
"type": "remind_water",
|
||
"message": self._fallback_cache.get("remind_water"),
|
||
"priority": 1,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 文件变更清单
|
||
|
||
| 文件 | 变更类型 | 对应 Phase |
|
||
|------|----------|------------|
|
||
| [requirements.txt](requirements.txt) | 修改 | 1.1 |
|
||
| [main.py](main.py) | 修改 | 1.1, 1.3, 2.2 |
|
||
| [agent/health_tracker.py](agent/health_tracker.py) | 新增 | 1.3 |
|
||
| [agent/scheduler.py](agent/scheduler.py) | 修改 | 2.1 |
|
||
| [agent/brain.py](agent/brain.py) | 修改 | 2.2, 2.3 |
|
||
|
||
---
|
||
|
||
## 运行命令
|
||
|
||
```bash
|
||
# 使用 conda ai 环境
|
||
conda activate ai
|
||
|
||
# Dummy 模式(无 GUI)
|
||
/home/e2hang/miniforge3/envs/ai/bin/python main.py --dummy
|
||
|
||
# 完整 GUI 模式
|
||
/home/e2hang/miniforge3/envs/ai/bin/python main.py
|
||
``` |