Files
cultivation-world-simulator/docs/specs/sqlite-event-manager.md
2026-01-07 00:46:27 -08:00

331 lines
8.1 KiB
Markdown
Raw 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.
# SQLite EventManager 重构规范
## 概述
将 EventManager 从内存存储迁移到 SQLite实现事件持久化、分页加载和更好的查询性能。
## 决策摘要
| 项目 | 决定 |
|------|------|
| 写入策略 | 实时写入(每个事件立即写入 SQLite |
| 前端加载 | 分页加载,向上滚动加载更旧事件 |
| 每页数量 | 100 条 |
| 数据库位置 | 每个存档一个,格式 `{save_name}_events.db` |
| 索引策略 | SQLite 原生索引 |
| 数据清理 | 用户控制(提供"清理历史"按钮) |
| 旧数据迁移 | 自动迁移(加载时从 JSON 迁移到 SQLite |
| 实时推送 | 保持现有 WebSocket 方式 |
| 错误处理 | 静默失败,记录日志但不影响游戏运行 |
| API 分页 | Cursor 分页(复合 cursor: month_stamp + rowid |
| Pair 查询 | 在事件筛选器中增加"筛选两人"选项 |
| 多存档 | 支持,使用时间戳命名(如 `save_20260105_1423` |
| 优先级 | **高** |
---
## 数据库设计
### 表结构
```sql
-- 事件主表
CREATE TABLE events (
id TEXT PRIMARY KEY, -- UUID
month_stamp INTEGER NOT NULL, -- 月份时间戳
content TEXT NOT NULL, -- 事件内容
is_major BOOLEAN DEFAULT FALSE, -- 是否为大事(长期记忆)
is_story BOOLEAN DEFAULT FALSE, -- 是否为故事事件
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 事件-角色关联表
CREATE TABLE event_avatars (
event_id TEXT NOT NULL,
avatar_id TEXT NOT NULL,
PRIMARY KEY (event_id, avatar_id),
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
);
-- 索引
CREATE INDEX idx_events_month_stamp ON events(month_stamp DESC);
CREATE INDEX idx_events_is_major ON events(is_major);
CREATE INDEX idx_event_avatars_avatar_id ON event_avatars(avatar_id);
CREATE INDEX idx_event_avatars_event_id ON event_avatars(event_id);
```
### 复合 Cursor 格式
```
{month_stamp}_{rowid}
```
使用 SQLite rowid 而非 UUID保证同一 month_stamp 内的确定性排序UUID 排序不稳定)。
示例: `1764_12345`
---
## API 设计
### GET /api/events
获取事件列表(分页)。
**Query Parameters:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| avatar_id | string | 否 | 按单个角色筛选 |
| avatar_id_1 | string | 否 | Pair 查询:角色 1 |
| avatar_id_2 | string | 否 | Pair 查询:角色 2需同时提供 avatar_id_1 |
| cursor | string | 否 | 分页 cursor获取该位置之前的事件 |
| limit | int | 否 | 每页数量,默认 100 |
**Response:**
```json
{
"events": [
{
"id": "uuid",
"text": "147年5月: 张三开始修炼",
"content": "张三开始修炼",
"year": 147,
"month": 5,
"month_stamp": 1764,
"related_avatar_ids": ["avatar_id_1"],
"is_major": false,
"is_story": false
}
],
"next_cursor": "1764_12345", // null 表示没有更多
"has_more": true
}
```
### DELETE /api/events/cleanup
清理历史事件(用户触发)。
**Query Parameters:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| keep_major | bool | 否 | 是否保留大事,默认 true |
| before_month_stamp | int | 否 | 删除此时间之前的事件 |
---
## 后端实现
### 新增文件
```
src/classes/event_storage.py # SQLite 存储层
```
### 修改文件
```
src/classes/event_manager.py # 重构为使用 SQLite
src/server/main.py # 新增分页 API
src/sim/save/save_game.py # 关联数据库文件
src/sim/load/load_game.py # 加载时连接数据库
```
### EventStorage 类
```python
class EventStorage:
"""SQLite 事件存储层。"""
def __init__(self, db_path: Path):
"""初始化数据库连接,创建表(如不存在)。"""
def add_event(self, event: Event) -> bool:
"""
写入单个事件。
失败时记录日志并返回 False不抛异常。
"""
def get_events(
self,
avatar_id: str | None = None,
avatar_id_pair: tuple[str, str] | None = None,
cursor: str | None = None,
limit: int = 100,
) -> tuple[list[Event], str | None]:
"""
分页查询事件。
返回 (events, next_cursor)。
"""
def get_events_by_avatar(self, avatar_id: str, limit: int = 50) -> list[Event]:
"""后端用:获取角色相关事件(供 LLM prompt 使用)。"""
def get_events_between(self, id1: str, id2: str, limit: int = 50) -> list[Event]:
"""后端用:获取两角色之间的事件。"""
def cleanup(self, keep_major: bool = True, before: int | None = None) -> int:
"""清理事件,返回删除数量。"""
def close(self):
"""关闭数据库连接。"""
```
### EventManager 重构
```python
class EventManager:
"""重构后的 EventManager使用 SQLite 存储。"""
def __init__(self, storage: EventStorage):
self._storage = storage
# 移除所有内存索引_events, _by_avatar, _by_pair 等)
def add_event(self, event: Event) -> None:
"""实时写入 SQLite。"""
self._storage.add_event(event)
# 其他查询方法委托给 storage
```
---
## 前端实现
### 修改文件
```
web/src/stores/world.ts # 事件状态管理
web/src/components/panels/EventPanel.vue # 分页 UI
web/src/api/game.ts # 新增 API
```
### 事件状态管理
```typescript
// world.ts
interface EventState {
events: GameEvent[];
cursor: string | null;
hasMore: boolean;
isLoading: boolean;
}
// 新增 actions
async function loadMoreEvents(): Promise<void>;
async function resetEvents(avatarId?: string, avatarId2?: string): Promise<void>;
```
### EventPanel 改动
1. **向上滚动加载**:监听滚动事件,当接近顶部时触发 `loadMoreEvents()`
2. **筛选器扩展**:增加"筛选两人"选项
3. **加载指示器**:显示加载状态
4. **切换筛选重置**:切换角色时调用 `resetEvents()` 重新加载
### 筛选器 UI
```
[所有人 ▼] [+ 添加第二人]
// 选择第二人后变为:
[张三 ▼] [李四 ▼] [×]
```
---
## 存档系统整合
### 文件结构
```
assets/saves/
├── save_20260105_1423.json # 游戏状态
├── save_20260105_1423_events.db # 事件数据库
├── save_20260105_1500.json
└── save_20260105_1500_events.db
```
### 保存流程
1. 保存 JSON 存档(现有逻辑)
2. 确保数据库文件存在(实时写入已处理)
3. 可选:执行 `VACUUM` 优化数据库大小
### 加载流程
1. 读取 JSON 存档
2. 连接对应的 `_events.db` 文件
3. 如果数据库不存在,创建空数据库
### 新建游戏
1. 生成时间戳名称:`save_YYYYMMDD_HHMM`
2. 创建空数据库文件
3. 初始化游戏状态
---
## 错误处理
### SQLite 写入失败
```python
def add_event(self, event: Event) -> bool:
try:
# 写入逻辑
return True
except Exception as e:
logger.error(f"Failed to write event: {e}")
return False # 不抛异常,游戏继续运行
```
### 数据库文件丢失
加载存档时,如果 `_events.db` 不存在:
- 记录警告日志
- 创建新的空数据库
- 游戏正常运行(但没有历史事件)
---
## TODO未来扩展
```python
# TODO(xzhseh): 事件搜索功能
# - 支持按关键词搜索事件内容
# - 可能需要 SQLite FTS5 扩展
# TODO(xzhseh): 事件分类/标签系统
# - 添加 event_type 字段combat/cultivation/social/death 等)
# - 支持按类型筛选
```
---
## 实现顺序
1. **Phase 1: 后端核心**
- [x] 创建 `EventStorage`
- [x] 重构 `EventManager`
- [x] 修复 `event_id``id` bug
- [x] 实现分页 API
2. **Phase 2: 存档整合**
- [x] 修改保存/加载逻辑
- [x] 支持多存档槽位
- [x] 时间戳命名
3. **Phase 3: 前端**
- [x] 分页加载 UI
- [x] 向上滚动加载
- [x] 双人筛选器
4. **Phase 4: 清理**
- [x] 用户清理历史 API`/api/events/cleanup`
- [ ] 清理历史 UI 按钮
- [x] 移除旧的内存索引代码EventManager 已重构)