refactor: 移除 crewai agent 架构相关代码并更新 LLM 配置

This commit is contained in:
qzl
2026-03-04 11:37:09 +08:00
parent 87399f74c8
commit b02a322bf3
71 changed files with 1045 additions and 7499 deletions
+116
View File
@@ -0,0 +1,116 @@
# 前后端 API 对比分析
**Date:** 2026-03-04
**Status:** Open
**Type:** 架构分析
---
## 一、后端已有、前端缺失的 API
### 1. Friendships API (`/api/v1/friends`)
| 方法 | 路径 | 功能 | 前端状态 |
|------|------|------|----------|
| POST | `/requests` | 发送好友请求 | **缺失** |
| GET | `/requests/inbox` | 获取收件箱 | **缺失** |
| GET | `/requests/outgoing` | 获取发出的请求 | **缺失** |
| POST | `/requests/{id}/accept` | 接受好友请求 | **缺失** |
| POST | `/requests/{id}/decline` | 拒绝好友请求 | **缺失** |
| DELETE | `/requests/{id}` | 取消好友请求 | **缺失** |
| GET | `` | 获取好友列表 | **缺失** |
| DELETE | `/{id}` | 删除好友 | **缺失** |
### 2. Inbox Messages API (`/api/v1/inbox/messages`)
| 方法 | 路径 | 功能 | 前端状态 |
|------|------|------|----------|
| GET | `` | 获取消息列表 | **缺失** |
| POST | `/{id}/accept` | 接受邀请 | **缺失** |
| POST | `/{id}/dismiss` | 忽略消息 | **缺失** |
### 3. Chat/AgUi 流式 API
| 功能 | 前端状态 |
|------|----------|
| 发送消息 SSE 流式 | **仅有 Mock** |
| 加载历史记录 | **仅有 Mock** |
> 前端 `AgUiService` 只有本地 mock (`throw UnimplementedError`),未实现真实 API 调用。
### 4. Infra API
| 方法 | 路径 | 功能 | 前端状态 |
|------|------|------|----------|
| GET | `/infra/health` | 基础设施健康检查 | **未使用** |
---
## 二、前端已有、后端已实现的 API
### Auth API (`/api/v1/auth`)
| 方法 | 路径 | 后端 | 前端 |
|------|------|------|------|
| POST | `/verifications` | ✅ | ✅ |
| POST | `/verifications/verify` | ✅ | ✅ |
| POST | `/verifications/resend` | ✅ | ✅ |
| POST | `/sessions` | ✅ | ✅ |
| POST | `/sessions/refresh` | ✅ | ✅ |
| DELETE | `/sessions` | ✅ | ✅ |
| POST | `/password-reset` | ✅ | ✅ |
| POST | `/password-reset/confirm` | ✅ | ✅ |
| GET | `/users` | ✅ | **未使用** |
### Users API (`/api/v1/users`)
| 方法 | 路径 | 后端 | 前端 |
|------|------|------|------|
| GET | `/me` | ✅ | ✅ |
| PATCH | `/me` | ✅ | ✅ |
| POST | `/search` | ✅ | ✅ |
### Schedule Items API (`/api/v1/schedule-items`)
| 方法 | 路径 | 后端 | 前端 |
|------|------|------|------|
| POST | `` | ✅ | **仅有 Mock** |
| GET | `` (range query) | ✅ | **仅有 Mock** |
| GET | `/{id}` | ✅ | **仅有 Mock** |
| PATCH | `/{id}` | ✅ | **仅有 Mock** |
| DELETE | `/{id}` | ✅ | **仅有 Mock** |
| POST | `/{id}/share` | ✅ | **缺失** |
---
## 三、待实现功能清单
| 优先级 | 功能 | 说明 |
|--------|------|------|
| **P0** | FriendsApi | 前端无 Friendships API 客户端 |
| **P0** | InboxMessagesApi | 前端无 Inbox Messages API 客户端 |
| **P0** | Chat/AgUi 后端连接 | 前端 AgUiService 未实现真实 API |
| **P1** | CalendarService 真实 API | MockCalendarService → 真实 API 调用 |
| **P1** | Schedule Share 接口 | 前端未调用 `POST /{id}/share` |
| **P2** | Infra Health 集成 | 可用于前端健康检查 |
---
## 四、相关文件位置
### 前端 API 客户端
- `apps/lib/features/auth/data/auth_api.dart` - Auth API
- `apps/lib/features/users/data/users_api.dart` - Users API
- `apps/lib/features/calendar/data/services/mock_calendar_service.dart` - Calendar Mock
- `apps/lib/features/chat/data/services/ag_ui_service.dart` - Chat/AgUi Mock
- `apps/lib/features/chat/data/services/mock_history_service.dart` - History Mock
### 后端 Router
- `backend/src/v1/auth/router.py` - Auth 路由
- `backend/src/v1/users/router.py` - Users 路由
- `backend/src/v1/friendships/router.py` - Friendships 路由
- `backend/src/v1/inbox_messages/router.py` - Inbox Messages 路由
- `backend/src/v1/schedule_items/router.py` - Schedule Items 路由
- `backend/src/v1/infra/router.py` - Infra 路由
-92
View File
@@ -1,92 +0,0 @@
# Backlog - Known Issues & Improvements
## Database Triggers
### [TRIGGER-001] user_agents 自动创建
**Status**: ✅ Resolved
**Priority**: Medium
**Created**: 2026-02-27
**Resolved**: 2026-03-02
**Description**:
当新用户注册时,`user_agents` 表未自动创建默认 Agent 配置记录。
**Current Behavior**:
- `auth.users``profiles` 已有 trigger 自动创建
- `user_agents` 无自动创建机制
**Expected Behavior**:
新用户注册后,应有默认的 Agent 配置(如 INTENT_RECOGNITION、TASK_EXECUTION、RESULT_REPORTING 三种类型)。
**Impact**:
- 用户首次使用 Agent Chat 功能时可能失败
- 需要应用层手动初始化或前端引导配置
**Solution**:
1. 创建 `user_agent_catalog.yaml` 配置文件定义 3 种 agent 类型
2. 创建 `user_agent_catalog` 表存储配置(持久化)
3. 修改 `user_agents` 表唯一约束:`user_id``(user_id, agent_type)` 允许每个用户有多个 agents
4. 扩展 `create_profile_for_new_user()` trigger 从 catalog 批量插入 user_agents
5. 实现配置动态更新:修改 YAML → 重启应用 → 自动同步到数据库
6. 为已存在用户补充 user_agents 记录
**Verification**:
- ✅ 新用户注册自动创建 3 个 agents
- ✅ Catalog 表已填充(INTENT_RECOGNITION, TASK_EXECUTION, RESULT_REPORTING
- ✅ 已为 1 个存在用户补充 3 个 agents
- ✅ Backend tests: 247 passed
**Related Files**:
- `backend/src/core/config/static/database/user_agent_catalog.yaml`
- `backend/src/models/user_agent_catalog.py`
- `backend/alembic/versions/50ae013ce530_add_user_agent_catalog.py`
- `backend/src/core/config/initial/init_data.py`
---
## Flutter Design Tokens
### [TOKEN-001] 大量硬编码颜色违反 AGENTS.md 规则
**Status**: ✅ Resolved (Partial)
**Priority**: Medium
**Created**: 2026-03-02
**Resolved**: 2026-03-02
**Description**:
`apps/AGENTS.md` 规则要求禁止硬编码颜色,必须使用 `design_tokens.dart` 中的 `AppColors`。但实际代码中存在大量硬编码。
**Current Behavior**:
- `apps/AGENTS.md` 规定:"NEVER hardcode colors, sizes, or spacing values"
- 代码中有 **126 处**硬编码 `Color(0xFF...)`(扫描发现比预期多 17 处)
**Expected Behavior**:
所有颜色应使用 `AppColors` 中定义的值。
**Impact**:
- 与项目规范不一致
- 后续 theme 统一修改困难
- 代码审查难以发现
**Solution** (渐进式迁移):
1. **扫描分析**: 发现 126 个硬编码颜色,Top 3: `FFF8FAFC` (11), `FFF8FAFF` (9), `FFE2E8F0` (5)
2. **扩展 AppColors**: 添加 11 个语义化 tokensurfaceSecondary, borderSecondary, success, warning 等)
3. **优先迁移**: 8 个高频文件(settings, contacts, calendar_event_detail 等)
4. **批量替换**: 37 个硬编码颜色已替换为 tokens
5. **测试验证**: 140/140 Flutter tests 通过
**Migration Results**:
- ✅ 新增 token: 11 个(覆盖 57 次使用)
- ✅ 已迁移: 37 个硬编码颜色
- ⚠️ 剩余: 90 个(建议后续迭代继续迁移)
**Recommendations**:
1. 继续迁移剩余 90 个硬编码颜色
2. 优先处理 `contacts_screen.dart`, `settings_screen.dart`, `features_screen.dart` (46% of remaining)
3. 考虑添加 lint rule 防止新增硬编码颜色
**Related Files**:
- `apps/lib/core/theme/design_tokens.dart`
- `apps/lib/features/*/ui/screens/*.dart` (8 files migrated)
- `apps/AGENTS.md`
-127
View File
@@ -1,127 +0,0 @@
# 日历功能新增 Bug 记录
**Created**: 2026-03-02
**Feature**: 日历事件创建功能
**Status**: Partial (Bug 5, 7 pending)
---
## Bug 1: 日视图时间范围不完整
**Severity**: High
**Status**: Fixed
**修复内容**:
- 修改循环范围从 `for (var hour = 7; hour <= 22; hour++)` 改为 `for (var hour = 0; hour <= 23; hour++)`
- 现在日视图显示完整的 00:00-24:00 时间线
---
## Bug 2: 日历事件保存后视图不刷新
**Severity**: High
**Status**: Fixed
**修复内容**:
-`CreateEventSheet` 添加 `onSaved` 回调参数
- 在月视图和日视图中使用 `setState` + `UniqueKey` 强制刷新事件列表
- 保存后自动刷新,无需手动切换页面
---
## Bug 3: 日视图事件高度未按时间范围渲染
**Severity**: Medium
**Status**: Fixed
**修复内容**:
- 使用 `Positioned` + `Stack` 替代原来的 `Wrap` 布局
- 根据事件实际持续时间计算高度:`持续分钟数 / 60 * _hourHeight`
- 事件垂直位置根据开始时间计算:`开始分钟数 / 60 * _hourHeight`
---
## Bug 4: 月视图事件超过3个后无法显示
**Severity**: Medium
**Status**: Fixed
**修复内容**:
- 显示前 2 个事件
- 超过 2 个时显示 `+N` 按钮
- 点击 `+N` 跳转到日视图
---
## Bug 5: 日视图点击事件跳转到错误页面
**Severity**: High
**Status**: Pending
**现象描述**:
- 在日视图中点击日历事件
- 跳转到的页面不是日历详情页,而是显示了类似首页的聊天输入框
- 页面显示"输入消息..."和麦克风图标
- 路由应该是 `/calendar/events/evt_xxx` 但显示的是首页布局
**已尝试的修复**:
-`/calendar/events/:id` 路由移到最前面优先匹配
- 调试日志显示 eventId 正确传递
- 添加了 `clipBehavior: Clip.none``Material + InkWell`
**可能原因**:
- 路由配置问题,可能被其他路由或 Shell 组件影响
- 页面布局问题导致显示异常
**修复建议**:
- 检查路由配置,确认 CalendarEventDetailScreen 正确加载
- 检查页面布局是否正确渲染
---
## Bug 6: 日视图多事件重叠时布局错乱
**Severity**: Medium
**Status**: Fixed
**修复内容**:
- 使用 `_calculateEventColumns` 方法计算事件列位置
- 最早开始的事件放在最左边
- 其余事件依次向右排列,每个事件宽度 = 总宽度 / 列数
- 使用 `Positioned` 绝对定位控制事件位置
---
## Bug 7: 日历详情页布局错误
**Severity**: High
**Status**: Pending
**现象描述**:
- 点击跳转到的页面显示异常
- 页面顶部显示聊天输入框("输入消息..."和麦克风图标)
- 应该是日历详情页但显示了类似首页的布局
- 渲染错误:RenderFlex children have non-zero flex but incoming width constraints are unbounded
**已尝试的修复**:
-`_buildInputContainer` 中的 Container 添加 `width: double.infinity`
- 修改内部的 Row 使用 const
**修复建议**:
- 需要重新设计详情页布局
- 确认底部输入框组件是否需要(可能复制自其他页面)
---
## 相关文件清单
### 新增文件
- `apps/lib/features/calendar/data/models/schedule_item_model.dart` - 日历事件数据模型
- `apps/lib/features/calendar/data/services/mock_calendar_service.dart` - Mock 服务
- `apps/lib/features/calendar/ui/widgets/create_event_sheet.dart` - 创建事件底部弹窗
### 修改文件
- `apps/lib/features/calendar/ui/screens/calendar_month_screen.dart` - 月视图
- `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart` - 日视图
- `apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart` - 事件详情页
- `apps/lib/core/router/app_router.dart` - 路由配置
+145
View File
@@ -0,0 +1,145 @@
# 前后端测试分析报告
**Date:** 2026-03-04
**Status:** Completed
---
## 测试统计
### 后端测试
| 类型 | 数量 | 状态 |
|------|------|------|
| Unit Tests | ~100+ | 可运行 |
| Integration Tests | ~70+ | 可运行 |
| E2E Tests | 5 | **无法运行** (缺少 playwright 依赖) |
### 前端测试
| 类型 | 数量 | 状态 |
|------|------|------|
| Flutter Tests | 140 | ✅ 全部通过 |
---
## 问题发现
### 1. 后端 E2E 测试无法运行 (HIGH)
**问题**: 5 个 E2E 测试文件需要 `playwright` 模块,但依赖未安装。
**影响文件**:
- `tests/e2e/test_auth_flow.py`
- `tests/e2e/test_infra_health_e2e.py`
- `tests/e2e/test_logging_e2e.py`
- `tests/e2e/test_mobile_health_e2e.py`
- `tests/e2e/test_profile_flow.py`
**错误**:
```
ModuleNotFoundError: No module named 'playwright'
```
**建议**:
- 安装 playwright: `uv add playwright && uv run playwright install`
- 或者移除这些无法运行的 E2E 测试文件
---
### 2. 测试文件命名冲突导致收集警告 (LOW)
**问题**: 存在多个同名 `test_schemas.py` 文件在不同目录,导致 pytest 收集时显示警告。
**影响文件**:
- `tests/unit/v1/schedule_items/test_schemas.py`
- `tests/unit/v1/profile/test_schemas.py`
- `tests/unit/v1/inbox_messages/test_schemas.py`
- `tests/unit/v1/friendships/test_schemas.py`
**状态**: 测试实际可以正常运行,只是有警告提示。
**建议**: 可保持现状(这是合理的代码组织方式),或重命名为 `test_*.py` 以消除警告。
---
### 3. 遗留测试验证旧字段 (INFO)
**文件**: `tests/unit/v1/profile/test_schemas.py`
**测试**: `test_profile_update_rejects_display_name_field`
**说明**: 此测试验证旧的 `display_name` 字段被正确拒绝。字段已在之前的重构中删除。
**状态**: **有效** - 这是一个回归测试,确保旧字段不被使用。
---
## 未发现的问题
### 冗余测试
经过检查,未发现明显冗余的测试:
- 每个模块的测试覆盖不同的功能
- Unit tests、Integration tests、E2E tests 有清晰的职责划分
### 死代码
未发现测试文件中有未使用的:
- imports
- mock 类
- helper 函数
### 缺失测试
未发现对应已实现功能但缺少测试的情况。
---
## 测试覆盖模块
### 后端
| 模块 | Unit | Integration | E2E |
|------|------|-------------|-----|
| Auth | ✅ | ✅ | ❌ |
| Users | - | ✅ | - |
| Profile | ✅ | - | ❌ |
| Friendships | ✅ | ✅ | - |
| Inbox Messages | ✅ | ✅ | - |
| Schedule Items | ✅ | ✅ | - |
| Logging | ✅ | ✅ | ✅ |
| Settings | ✅ | - | - |
### 前端
| 模块 | 测试数 |
|------|--------|
| Auth | ~20 |
| Chat | ~70 |
| Home | ~15 |
| Calendar | ~5 |
| Core (API, Storage) | ~30 |
---
## 建议
1. **立即**: 解决 E2E 测试依赖问题或移除无法运行的测试文件
2. **可选**: 清理 test_schemas.py 重名警告(低优先级)
3. **保持**: 现有的测试结构良好,无需重大重构
---
## 附: 测试代码质量问题
### 测试类未完全实现 Protocol (LSP 警告)
**文件**: `tests/unit/v1/auth/test_auth_service.py`
**问题**: `FakeGateway``LogoutAssertingGateway` 类没有实现 `AuthServiceGateway` Protocol 的全部方法:
- `request_password_reset`
- `confirm_password_reset`
**影响**: LSP 类型检查器报告错误,但运行时不受影响(因为这些方法在测试中不会被调用)。
**建议**: 可选择补充缺失的方法实现,或使用 `@pytest.mark.skip` 标记不需要的协议方法。
---
*报告生成时间: 2026-03-04*
-458
View File
@@ -1,458 +0,0 @@
# Bug Fix Design: TRIGGER-001 & TOKEN-001
**Date:** 2026-03-02
**Author:** AI Assistant
**Status:** Approved
## Overview
解决 `docs/bugs/backlog.md` 中的两个 bug
1. TRIGGER-001: user_agents 自动创建
2. TOKEN-001: Flutter 硬编码颜色迁移
## TRIGGER-001: user_agents 自动创建
### Problem
新用户注册时,`user_agents` 表未自动创建默认 Agent 配置记录,导致首次使用 Agent Chat 功能失败。
### Solution
创建配置驱动的自动初始化机制:
- YAML 配置文件定义默认 agent 配置
- 数据库 catalog 表存储配置(持久化)
- Trigger 从 catalog 表读取并批量插入
- 应用启动时自动同步 YAML → catalog 表
### Architecture
```
user_agent_catalog.yaml
↓ (应用启动时)
initialize_user_agent_catalog()
↓ (upsert)
user_agent_catalog 表
↓ (Trigger 查询)
新用户注册 → create_profile_for_new_user()
├─→ 插入 profiles
└─→ 批量插入 user_agents (3条)
```
### Data Model
#### 1. user_agent_catalog.yaml
```yaml
agents:
- agent_type: INTENT_RECOGNITION
llm_model_code: qwen3.5-flash
status: active
config:
temperature: 0.7
- agent_type: TASK_EXECUTION
llm_model_code: deepseek-v3.2
status: active
config:
temperature: 0.7
- agent_type: RESULT_REPORTING
llm_model_code: deepseek-v3.2
status: active
config:
temperature: 0.7
```
#### 2. user_agent_catalog 表
```sql
CREATE TABLE user_agent_catalog (
agent_type VARCHAR(20) PRIMARY KEY,
llm_id UUID NOT NULL REFERENCES llms(id) ON DELETE RESTRICT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
CONSTRAINT chk_status CHECK (status IN ('active', 'paused', 'migrating'))
);
```
**字段说明:**
- `agent_type`: 主键,agent 类型(INTENT_RECOGNITION / TASK_EXECUTION / RESULT_REPORTING
- `llm_id`: 外键,关联 llms 表
- `status`: 默认状态(active / paused / migrating
- `config`: 默认配置(JSONB,包含 temperature 等)
#### 3. Trigger 函数
```sql
CREATE OR REPLACE FUNCTION create_profile_for_new_user()
RETURNS trigger AS $$
BEGIN
-- 插入 profile(现有逻辑)
INSERT INTO profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data ->> 'username',
split_part(NEW.email, '@', 1),
'user_' || substring(NEW.id::text, 1, 8)
),
NULL, NULL, '{}'::jsonb, now(), now()
)
ON CONFLICT (id) DO NOTHING;
-- 从 user_agent_catalog 批量插入 user_agents
INSERT INTO user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by)
SELECT
gen_random_uuid(),
NEW.id,
uac.llm_id,
uac.agent_type,
uac.config,
uac.status,
NEW.id,
NEW.id
FROM user_agent_catalog uac;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
```
### Implementation Components
#### 1. Python Model
```python
# backend/src/models/user_agent_catalog.py
class UserAgentCatalog(TimestampMixin, Base):
__tablename__ = "user_agent_catalog"
agent_type: Mapped[str] = mapped_column(String(20), primary_key=True)
llm_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("llms.id"), nullable=False)
status: Mapped[str] = mapped_column(String(20), nullable=False)
config: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default="{}")
```
#### 2. 初始化函数
```python
# backend/src/core/config/initial/init_data.py
class UserAgentCatalogSeed(BaseModel):
agent_type: str
llm_model_code: str
status: str
config: dict[str, Any]
def load_user_agent_catalog(catalog_path: Path | None = None) -> dict[str, Any]:
path = catalog_path or _default_user_agent_catalog_path()
with path.open("r", encoding="utf-8") as file:
loaded = yaml.safe_load(file) or {}
# ... validation logic
return parsed.model_dump()
async def initialize_user_agent_catalog() -> None:
catalog = load_user_agent_catalog()
async with AsyncSessionLocal() as session:
async with session.begin():
for agent in catalog["agents"]:
# 查找 llm_id
llm = await session.execute(
select(Llm).where(Llm.model_code == agent["llm_model_code"])
)
llm_id = llm.scalar_one().id
# Upsert
existing = await session.execute(
select(UserAgentCatalog).where(
UserAgentCatalog.agent_type == agent["agent_type"]
)
)
catalog_entry = existing.scalar_one_or_none()
if catalog_entry:
catalog_entry.llm_id = llm_id
catalog_entry.status = agent["status"]
catalog_entry.config = agent["config"]
else:
session.add(UserAgentCatalog(
agent_type=agent["agent_type"],
llm_id=llm_id,
status=agent["status"],
config=agent["config"]
))
logger.info("Initialized user agent catalog")
async def initialize_data() -> bool:
await initialize_llm_catalog()
await initialize_user_agent_catalog() # 新增
return True
```
### Migration Strategy
#### 1. Alembic 迁移
```python
# backend/alembic/versions/20260302_add_user_agent_catalog.py
def upgrade() -> None:
# 创建表
op.create_table(
"user_agent_catalog",
sa.Column("agent_type", sa.String(20), nullable=False),
sa.Column("llm_id", sa.UUID(), nullable=False),
sa.Column("status", sa.String(20), nullable=False),
sa.Column("config", postgresql.JSONB(), nullable=False, server_default="{}"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
sa.PrimaryKeyConstraint("agent_type"),
sa.ForeignKeyConstraint(["llm_id"], ["llms.id"], ondelete="RESTRICT"),
)
op.execute(
"ALTER TABLE user_agent_catalog "
"ADD CONSTRAINT chk_status CHECK (status IN ('active', 'paused', 'migrating'))"
)
# 替换 trigger 函数
op.execute("""
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
-- ... 新的 trigger 代码
""")
# 为已存在用户补充 user_agents
op.execute("""
INSERT INTO user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by)
SELECT
gen_random_uuid(),
p.id,
uac.llm_id,
uac.agent_type,
uac.config,
uac.status,
p.id,
p.id
FROM profiles p
CROSS JOIN user_agent_catalog uac
WHERE NOT EXISTS (
SELECT 1 FROM user_agents ua WHERE ua.user_id = p.id
);
""")
_enable_rls("user_agent_catalog")
def downgrade() -> None:
# 恢复旧 trigger
op.execute("""
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
-- ... 旧的 trigger 代码(只有 profile
""")
_drop_rls("user_agent_catalog")
op.drop_table("user_agent_catalog")
```
### Configuration Update Flow
**用户修改配置的步骤:**
1. 编辑 `backend/src/core/config/static/database/user_agent_catalog.yaml`
2. 重启应用(或调用 `uv run python -m core.runtime.cli bootstrap`
3. `initialize_user_agent_catalog()` 自动执行
4. user_agent_catalog 表更新(upsert
5. 新用户注册时使用新配置
**示例:**
```yaml
# 修改 temperature
agents:
- agent_type: INTENT_RECOGNITION
llm_model_code: qwen3.5-flash
status: active
config:
temperature: 0.5 # 从 0.7 改为 0.5
```
重启应用后,新注册的用户 INTENT_RECOGNITION agent 会使用 temperature=0.5。
### Testing Strategy
1. **单元测试:** `load_user_agent_catalog()` 函数
2. **集成测试:** `initialize_user_agent_catalog()` upsert 逻辑
3. **E2E 测试:** 新用户注册后自动创建 3 个 user_agents 记录
4. **回归测试:** 已存在用户不受影响
### File Changes
```
backend/src/core/config/static/database/user_agent_catalog.yaml (填充)
backend/src/models/user_agent_catalog.py (新建)
backend/src/core/config/initial/init_data.py (扩展)
backend/alembic/versions/20260302_add_user_agent_catalog.py (新建)
backend/tests/unit/core/test_agent_init_data.py (扩展)
```
---
## TOKEN-001: Flutter 硬编码颜色迁移
### Problem
109 处硬编码 `Color(0xFF...)` 违反 `apps/AGENTS.md` 规则:"NEVER hardcode colors, sizes, or spacing values"。
分散在 11 个页面文件:
- register_screen.dart, register_verification_screen.dart
- settings_screen.dart, account_screen.dart
- contacts_screen.dart, calendar_event_detail_screen.dart
- add_contact_screen.dart, features_screen.dart
- memory_screen.dart, home_screen.dart
- todo_detail_screen.dart
### Solution
**渐进式迁移:**
1. 扫描所有硬编码颜色,统计频率
2. 将高频颜色添加到 `design_tokens.dart``AppColors`
3. 逐页面替换硬编码为 `AppColors.xxx`
4. 保留合理的动态颜色场景
### Migration Steps
#### Phase 1: 扫描和统计
```bash
# 扫描所有硬编码颜色
grep -r "Color(0x" apps/lib --include="*.dart" | \
sed 's/.*Color(0x\([0-9A-Fa-f]*\)).*/\1/' | \
sort | uniq -c | sort -rn
```
**预期输出:**
```
45 2196F3 # 蓝色(链接/主色)
23 D32F2F # 红色(错误)
18 4CAF50 # 绿色(成功)
...
```
#### Phase 2: 扩展 AppColors
```dart
// apps/lib/core/theme/design_tokens.dart
class AppColors {
// 现有颜色...
// 从硬编码迁移的颜色
static const Color linkBlue = Color(0xFF2196F3);
static const Color errorRed = Color(0xFFD32F2F);
static const Color successGreen = Color(0xFF4CAF50);
// ... 根据扫描结果添加
}
```
#### Phase 3: 逐页面迁移
```dart
// Before
Container(
color: Color(0xFF2196F3),
child: Text('Link'),
)
// After
Container(
color: AppColors.linkBlue,
child: Text('Link'),
)
```
**迁移顺序(按频率):**
1. register_screen.dart (最高频)
2. register_verification_screen.dart
3. settings_screen.dart
4. ... (其他页面)
#### Phase 4: 测试和验证
1. **视觉测试:** 逐页面检查 UI 是否正常
2. **Widget 测试:** 更新测试使用 AppColors
3. **设计审查:** 确认颜色一致性
### Allowed Exceptions
以下场景可以保留硬编码(需在 AGENTS.md 中明确):
- 动态计算的渐变色
- 图片处理相关的颜色
- 第三方库要求的颜色格式
### Testing Strategy
1. **Widget 测试:** 确保组件仍然正常渲染
2. **视觉回归测试:** 对比迁移前后的截图
3. **代码审查:** 确保没有遗漏的硬编码
### File Changes
```
apps/lib/core/theme/design_tokens.dart (扩展)
apps/lib/features/*/ui/screens/*.dart (迁移)
apps/AGENTS.md (明确允许的场景)
```
---
## Implementation Order
1. **TRIGGER-001** (优先级更高,影响功能)
- 创建 catalog YAML
- 创建 model 和迁移
- 扩展初始化逻辑
- 修改 trigger
- 测试
2. **TOKEN-001** (次要,不影响功能)
- 扫描硬编码
- 扩展 AppColors
- 逐页面迁移
- 测试
## Success Criteria
### TRIGGER-001
- [ ] 新用户注册后自动创建 3 个 user_agents 记录
- [ ] 已存在用户不受影响
- [ ] 配置可通过 YAML + 重启更新
- [ ] 所有测试通过
### TOKEN-001
- [ ] 硬编码颜色数量从 109 减少到 < 10
- [ ] UI 视觉无回归
- [ ] 所有测试通过
## Risks and Mitigations
### TRIGGER-001
- **风险:** llms 表中缺少对应的 model_code
- **缓解:** 初始化前检查 llms 表是否存在,提供清晰的错误信息
- **风险:** Trigger 性能影响
- **缓解:** catalog 表只有 3 行,查询性能可忽略
### TOKEN-001
- **风险:** 迁移后颜色不一致
- **缓解:** 视觉回归测试 + 设计审查
- **风险:** 遗漏某些硬编码
- **缓解:** 自动化扫描 + 代码审查
## Related Documents
- `docs/bugs/backlog.md` - Bug 列表
- `backend/AGENTS.md` - Backend 开发规则
- `apps/AGENTS.md` - Flutter 开发规则
- `backend/src/core/config/initial/init_data.py` - 现有初始化逻辑
- `backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py` - Trigger 修改参考
-806
View File
@@ -1,806 +0,0 @@
# Bug Fix Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 修复 TRIGGER-001 (user_agents 自动创建) 和 TOKEN-001 (Flutter 硬编码颜色迁移)
**Architecture:**
- TRIGGER-001: YAML 配置 → catalog 表 → Trigger 自动创建
- TOKEN-001: 渐进式迁移硬编码颜色到 AppColors
**Tech Stack:** Python, PostgreSQL, Alembic, Dart, Flutter
---
## TRIGGER-001: user_agents 自动创建
### Task 1: 创建 user_agent_catalog.yaml 配置文件
**Files:**
- Modify: `backend/src/core/config/static/database/user_agent_catalog.yaml`
**Step 1: 填充 YAML 配置**
```yaml
agents:
- agent_type: INTENT_RECOGNITION
llm_model_code: qwen3.5-flash
status: active
config:
temperature: 0.7
- agent_type: TASK_EXECUTION
llm_model_code: deepseek-v3.2
status: active
config:
temperature: 0.7
- agent_type: RESULT_REPORTING
llm_model_code: deepseek-v3.2
status: active
config:
temperature: 0.7
```
**Step 2: 验证 YAML 语法**
Run: `cat backend/src/core/config/static/database/user_agent_catalog.yaml`
Expected: YAML 格式正确,包含 3 个 agent 配置
**Step 3: Commit**
```bash
git add backend/src/core/config/static/database/user_agent_catalog.yaml
git commit -m "feat(config): add user_agent_catalog.yaml with default agent configs"
```
---
### Task 2: 创建 UserAgentCatalog 模型
**Files:**
- Create: `backend/src/models/user_agent_catalog.py`
- Modify: `backend/src/models/__init__.py`
**Step 1: 创建模型文件**
```python
# backend/src/models/user_agent_catalog.py
from __future__ import annotations
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
class UserAgentCatalog(TimestampMixin, Base):
__tablename__ = "user_agent_catalog"
agent_type: Mapped[str] = mapped_column(
String(20),
primary_key=True,
)
llm_id: Mapped[str] = mapped_column(
UUID(as_uuid=True),
ForeignKey("llms.id", ondelete="RESTRICT"),
nullable=False,
)
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
)
config: Mapped[dict] = mapped_column(
JSONB,
nullable=False,
server_default="{}",
)
```
**Step 2: 在 __init__.py 中导出**
```python
# backend/src/models/__init__.py (添加)
from models.user_agent_catalog import UserAgentCatalog
__all__ = [
# ... 现有导出
"UserAgentCatalog",
]
```
**Step 3: 运行类型检查**
Run: `cd backend && uv run basedpyright src/models/user_agent_catalog.py`
Expected: No errors
**Step 4: Commit**
```bash
git add backend/src/models/user_agent_catalog.py backend/src/models/__init__.py
git commit -m "feat(models): add UserAgentCatalog model"
```
---
### Task 3: 创建 Alembic 迁移
**Files:**
- Create: `backend/alembic/versions/20260302_add_user_agent_catalog.py`
**Step 1: 生成迁移文件**
Run: `cd backend && uv run alembic revision -m "add_user_agent_catalog"`
**Step 2: 编写迁移逻辑**
```python
# backend/alembic/versions/20260302_add_user_agent_catalog.py
"""add user_agent_catalog table
Revision ID: 202603020001
Revises:
Create Date: 2026-03-02
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "202603020001"
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# 创建 user_agent_catalog 表
op.create_table(
"user_agent_catalog",
sa.Column("agent_type", sa.String(20), nullable=False),
sa.Column("llm_id", sa.UUID(), nullable=False),
sa.Column("status", sa.String(20), nullable=False),
sa.Column(
"config",
postgresql.JSONB(astext_type=sa.Text()),
server_default="{}",
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("agent_type"),
sa.ForeignKeyConstraint(
["llm_id"], ["llms.id"], name="fk_user_agent_catalog_llm_id", ondelete="RESTRICT"
),
)
op.execute(
"ALTER TABLE user_agent_catalog "
"ADD CONSTRAINT chk_user_agent_catalog_status "
"CHECK (status IN ('active', 'paused', 'migrating'))"
)
_enable_rls("user_agent_catalog")
# 替换 trigger 函数
op.execute("""
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data ->> 'username',
split_part(NEW.email, '@', 1),
'user_' || substring(NEW.id::text, 1, 8)
),
NULL,
NULL,
'{}'::jsonb,
now(),
now()
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO public.user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by)
SELECT
gen_random_uuid(),
NEW.id,
uac.llm_id,
uac.agent_type,
uac.config,
uac.status,
NEW.id,
NEW.id
FROM public.user_agent_catalog uac;
RETURN NEW;
END;
$$;
""")
def downgrade() -> None:
# 恢复旧 trigger 函数
op.execute("""
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data ->> 'username',
split_part(NEW.email, '@', 1),
'user_' || substring(NEW.id::text, 1, 8)
),
NULL,
NULL,
'{}'::jsonb,
now(),
now()
)
ON CONFLICT (id) DO NOTHING;
RETURN NEW;
END;
$$;
""")
_drop_rls("user_agent_catalog")
op.drop_constraint("chk_user_agent_catalog_status", "user_agent_catalog", type_="check")
op.drop_constraint("fk_user_agent_catalog_llm_id", "user_agent_catalog", type_="foreignkey")
op.drop_table("user_agent_catalog")
def _enable_rls(table_name: str) -> None:
for role in ["anon", "authenticated"]:
for action in ["select", "insert", "update", "delete"]:
op.execute(
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
)
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
for role in ["anon", "authenticated"]:
op.execute(
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
)
op.execute(
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
)
op.execute(
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
)
op.execute(
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
)
def _drop_rls(table_name: str) -> None:
for role in ["anon", "authenticated"]:
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
```
**Step 3: 运行迁移检查**
Run: `cd backend && uv run alembic check`
Expected: No issues
**Step 4: Commit**
```bash
git add backend/alembic/versions/20260302_add_user_agent_catalog.py
git commit -m "feat(migration): add user_agent_catalog table and update trigger"
```
---
### Task 4: 扩展初始化函数
**Files:**
- Modify: `backend/src/core/config/initial/init_data.py`
**Step 1: 添加 Pydantic 模型**
```python
# backend/src/core/config/initial/init_data.py (添加到文件顶部)
from models.user_agent_catalog import UserAgentCatalog
class UserAgentCatalogSeed(BaseModel):
agent_type: str
llm_model_code: str
status: str
config: dict[str, Any]
class UserAgentCatalogYaml(BaseModel):
agents: list[UserAgentCatalogSeed]
```
**Step 2: 添加 load 函数**
```python
# backend/src/core/config/initial/init_data.py (添加)
def _default_user_agent_catalog_path() -> Path:
return (
Path(__file__).resolve().parents[1] / "static" / "database" / "user_agent_catalog.yaml"
)
def load_user_agent_catalog(catalog_path: Path | None = None) -> dict[str, Any]:
path = catalog_path or _default_user_agent_catalog_path()
with path.open("r", encoding="utf-8") as file:
loaded = yaml.safe_load(file) or {}
if not isinstance(loaded, dict):
raise ValueError(f"Invalid user agent catalog format: {path}")
raw_agents = loaded.get("agents", [])
if not isinstance(raw_agents, list):
raise ValueError(f"Invalid user agent catalog agents section: {path}")
try:
parsed = UserAgentCatalogYaml.model_validate({"agents": list(raw_agents)})
except ValidationError as exc:
raise ValueError(f"Invalid user agent catalog data: {path}") from exc
return parsed.model_dump()
```
**Step 3: 添加初始化函数**
```python
# backend/src/core/config/initial/init_data.py (添加)
async def _upsert_user_agent_catalog(
session: AsyncSession,
*,
agent_type: str,
llm_id: uuid.UUID,
status: str,
config: dict[str, Any],
) -> None:
result = await session.execute(
select(UserAgentCatalog).where(UserAgentCatalog.agent_type == agent_type)
)
catalog_entry = result.scalar_one_or_none()
if catalog_entry is None:
session.add(UserAgentCatalog(
agent_type=agent_type,
llm_id=llm_id,
status=status,
config=config,
))
else:
catalog_entry.llm_id = llm_id
catalog_entry.status = status
catalog_entry.config = config
async def initialize_user_agent_catalog() -> None:
"""Initialize user agent catalog from YAML."""
catalog = load_user_agent_catalog()
async with AsyncSessionLocal() as session:
async with session.begin():
for agent in catalog["agents"]:
# 查找 llm_id
result = await session.execute(
select(Llm).where(Llm.model_code == agent["llm_model_code"])
)
llm = result.scalar_one_or_none()
if llm is None:
raise RuntimeError(
f"LLM model '{agent['llm_model_code']}' not found for agent type '{agent['agent_type']}'"
)
await _upsert_user_agent_catalog(
session,
agent_type=agent["agent_type"],
llm_id=llm.id,
status=agent["status"],
config=agent["config"],
)
logger.info("Initialized user agent catalog")
```
**Step 4: 更新 initialize_data 函数**
```python
# backend/src/core/config/initial/init_data.py (修改)
async def initialize_data() -> bool:
"""Initialize bootstrap data."""
await initialize_llm_catalog()
await initialize_user_agent_catalog() # 新增
return True
```
**Step 5: 运行类型检查**
Run: `cd backend && uv run basedpyright src/core/config/initial/init_data.py`
Expected: No errors
**Step 6: Commit**
```bash
git add backend/src/core/config/initial/init_data.py
git commit -m "feat(init): add user_agent_catalog initialization"
```
---
### Task 5: 编写单元测试
**Files:**
- Modify: `backend/tests/unit/core/test_agent_init_data.py`
**Step 1: 添加测试**
```python
# backend/tests/unit/core/test_agent_init_data.py (添加到文件末尾)
def test_user_agent_catalog_file_exists_and_has_required_fields() -> None:
catalog_path = (
Path(__file__).resolve().parents[4]
/ "src"
/ "core"
/ "config"
/ "static"
/ "database"
/ "user_agent_catalog.yaml"
)
assert catalog_path.exists(), f"Catalog file not found: {catalog_path}"
catalog = init_data.load_user_agent_catalog(catalog_path)
assert "agents" in catalog
assert isinstance(catalog["agents"], list)
assert len(catalog["agents"]) == 3
for agent in catalog["agents"]:
assert "agent_type" in agent
assert "llm_model_code" in agent
assert "status" in agent
assert "config" in agent
assert isinstance(agent["config"], dict)
def test_load_user_agent_catalog_raises_on_invalid_structure(tmp_path: Path) -> None:
catalog_path = tmp_path / "user_agent_catalog.yaml"
catalog_path.write_text("invalid: structure\n")
with pytest.raises(ValueError, match="Invalid user agent catalog"):
init_data.load_user_agent_catalog(catalog_path)
```
**Step 2: 运行测试**
Run: `cd backend && uv run pytest tests/unit/core/test_agent_init_data.py -v`
Expected: All tests pass
**Step 3: Commit**
```bash
git add backend/tests/unit/core/test_agent_init_data.py
git commit -m "test(init): add user_agent_catalog validation tests"
```
---
### Task 6: 运行迁移并验证
**Files:**
- None (database operations)
**Step 1: 运行迁移**
Run: `cd backend && uv run alembic upgrade head`
Expected: Migration succeeds
**Step 2: 运行初始化**
Run: `cd backend && uv run python -m core.runtime.cli init-data`
Expected: "Initialized user agent catalog" in output
**Step 3: 验证数据库**
Run: `docker exec -it social-supabase-db psql -U postgres -d postgres -c "SELECT * FROM user_agent_catalog;"`
Expected: 3 rows (INTENT_RECOGNITION, TASK_EXECUTION, RESULT_REPORTING)
**Step 4: 测试 Trigger(手动)**
```sql
-- 创建测试用户
INSERT INTO auth.users (id, email, raw_user_meta_data)
VALUES (gen_random_uuid(), 'test@example.com', '{"username": "testuser"}');
-- 验证 profiles 和 user_agents 自动创建
SELECT * FROM profiles WHERE username = 'testuser';
SELECT * FROM user_agents WHERE user_id = (SELECT id FROM profiles WHERE username = 'testuser');
-- 清理测试数据
DELETE FROM user_agents WHERE user_id = (SELECT id FROM profiles WHERE username = 'testuser');
DELETE FROM profiles WHERE username = 'testuser';
DELETE FROM auth.users WHERE email = 'test@example.com';
```
Expected: profiles 有 1 条,user_agents 有 3 条
---
### Task 7: 为已存在用户补充 user_agents
**Files:**
- None (database operations)
**Step 1: 执行补充脚本**
```sql
-- 为已存在但没有 user_agents 的用户补充记录
INSERT INTO user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by)
SELECT
gen_random_uuid(),
p.id,
uac.llm_id,
uac.agent_type,
uac.config,
uac.status,
p.id,
p.id
FROM profiles p
CROSS JOIN user_agent_catalog uac
WHERE NOT EXISTS (
SELECT 1 FROM user_agents ua WHERE ua.user_id = p.id
);
```
**Step 2: 验证结果**
Run: `docker exec -it social-supabase-db psql -U postgres -d postgres -c "SELECT user_id, COUNT(*) FROM user_agents GROUP BY user_id;"`
Expected: 每个用户有 3 条 user_agents 记录
---
## TOKEN-001: Flutter 硬编码颜色迁移
### Task 8: 扫描硬编码颜色
**Files:**
- None (analysis only)
**Step 1: 扫描所有硬编码颜色**
Run:
```bash
cd apps && grep -r "Color(0x" lib --include="*.dart" | \
sed 's/.*Color(0x\([0-9A-Fa-f]*\)).*/\1/' | \
sort | uniq -c | sort -rn > /tmp/color_stats.txt && \
cat /tmp/color_stats.txt
```
Expected: 统计结果输出,显示每个颜色的使用频率
**Step 2: 记录统计结果**
将统计结果保存到临时文件供后续使用。
---
### Task 9: 扩展 AppColors
**Files:**
- Modify: `apps/lib/core/theme/design_tokens.dart`
**Step 1: 添加新颜色常量**
```dart
// apps/lib/core/theme/design_tokens.dart (在 AppColors 类中添加)
class AppColors {
// 现有颜色...
// 从硬编码迁移的颜色(根据 Task 8 的统计结果)
static const Color linkBlue = Color(0xFF2196F3);
static const Color errorRed = Color(0xFFD32F2F);
static const Color successGreen = Color(0xFF4CAF50);
static const Color warningOrange = Color(0xFFFF9800);
// ... 根据统计结果添加其他高频颜色
}
```
**Step 2: 运行 Flutter 分析**
Run: `cd apps && flutter analyze lib/core/theme/design_tokens.dart`
Expected: No issues
**Step 3: Commit**
```bash
git add apps/lib/core/theme/design_tokens.dart
git commit -m "feat(theme): add migrated colors to AppColors"
```
---
### Task 10: 迁移 register_screen.dart
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart`
**Step 1: 替换硬编码颜色**
```dart
// Before
Container(color: Color(0xFF2196F3))
// After
Container(color: AppColors.linkBlue)
```
**Step 2: 检查所有实例**
Run: `cd apps && grep "Color(0x" lib/features/auth/ui/screens/register_screen.dart`
Expected: No matches (或只剩下合理的动态颜色)
**Step 3: 运行 Flutter 分析**
Run: `cd apps && flutter analyze lib/features/auth/ui/screens/register_screen.dart`
Expected: No issues
**Step 4: Commit**
```bash
git add apps/lib/features/auth/ui/screens/register_screen.dart
git commit -m "refactor(auth): migrate hardcoded colors in register_screen"
```
---
### Task 11: 迁移其他页面(批量)
**Files:**
- Modify: 所有包含硬编码颜色的页面文件
**Step 1: 逐页面迁移**
按照频率顺序迁移:
1. `register_verification_screen.dart`
2. `settings_screen.dart`
3. `account_screen.dart`
4. `contacts_screen.dart`
5. `calendar_event_detail_screen.dart`
6. `add_contact_screen.dart`
7. `features_screen.dart`
8. `memory_screen.dart`
9. `home_screen.dart`
10. `todo_detail_screen.dart`
每个文件重复 Task 10 的步骤。
**Step 2: 批量提交**
```bash
git add apps/lib/features/
git commit -m "refactor(ui): migrate hardcoded colors to AppColors"
```
---
### Task 12: 验证和测试
**Files:**
- None (validation only)
**Step 1: 检查剩余硬编码**
Run: `cd apps && grep -r "Color(0x" lib --include="*.dart" | wc -l`
Expected: < 10 (只保留合理的动态颜色)
**Step 2: 运行 Flutter 测试**
Run: `cd apps && flutter test`
Expected: All tests pass
**Step 3: 视觉检查**
手动运行应用,检查页面显示是否正常。
---
## Final Verification
### Task 13: 全量测试
**Files:**
- None (testing only)
**Step 1: 运行后端测试**
Run: `cd backend && uv run pytest`
Expected: All tests pass
**Step 2: 运行 Flutter 测试**
Run: `cd apps && flutter test`
Expected: All tests pass
**Step 3: 运行迁移检查**
Run: `cd backend && uv run alembic check`
Expected: No issues
**Step 4: 类型检查**
Run: `cd backend && uv run basedpyright src/`
Expected: No errors
---
## Summary
**Total Tasks:** 13
**Estimated Time:** 3-4 hours
**Key Deliverables:**
- ✅ user_agent_catalog.yaml 配置文件
- ✅ UserAgentCatalog 模型
- ✅ 数据库迁移(表 + trigger)
- ✅ 初始化函数扩展
- ✅ 单元测试
- ✅ 硬编码颜色迁移(109 → <10)
**Success Criteria:**
- [ ] 新用户注册后自动创建 3 个 user_agents 记录
- [ ] 已存在用户不受影响
- [ ] 配置可通过 YAML + 重启更新
- [ ] 硬编码颜色数量 < 10
- [ ] 所有测试通过
- [ ] UI 视觉无回归
@@ -1,100 +0,0 @@
# 日历事件创建功能设计
**Date:** 2026-03-02
**Status:** Approved
---
## 1. UI 架构
### 入口
- 位置:月视图和日视图右上角
- 图标:LucideIcons.plus
- 点击后从底部弹出创建表单(showModalBottomSheet
### 底部弹窗表单
- 高度:约占屏幕 80%
- 顶部有关闭按钮和"新建日程"标题
- 内含两个可切换的 TabBar(类似苹果日历)
### 两个 Tab
| Tab | 字段 |
|-----|------|
| 基础 | 标题、开始日期/时间、结束日期/时间 |
| 进阶 | 描述、地点、颜色、备注 |
---
## 2. 数据模型
```dart
class ScheduleItemModel {
String id; // UUID
String title; // 标题(必填)
String? description; // 描述
DateTime startAt; // 开始时间(必填)
DateTime? endAt; // 结束时间
String timezone; // 时区,默认 "Asia/Shanghai"
ScheduleMetadata? metadata; // 扩展字段
String sourceType; // 来源,默认 "manual"
String status; // 状态,默认 "active"
}
class ScheduleMetadata {
String? color; // 颜色,如 "#3B82F6"
String? location; // 地点
String? notes; // 备注
List<Attachment>? attachments;
}
```
---
## 3. Mock 服务设计
参考 `mock_history_service.dart` 模式,创建 `mock_calendar_service.dart`
- 使用 `Env.isMockApi` 开关
- 内存中存储事件列表
- 支持 CRUD 操作
---
## 4. 日历视图集成
### 月视图
- `_buildWeekEvents` 遍历当天事件,显示最多 2-3 个事件标题
- 点击日期跳转到日视图
### 日视图
- `_buildTimelineBoard` 在对应时间位置显示事件块
- 点击事件进入详情页
---
## 5. 路由
现有路由已支持:
- `/calendar/month` - 月视图
- `/calendar/dayweek` - 日视图
- `/calendar/events/:id` - 事件详情页
底部弹窗使用 showModalBottomSheet,无需新路由。
---
## 6. 实现步骤
1. 创建数据模型 `schedule_item_model.dart`
2. 创建 Mock Calendar Service
3. 创建底部弹窗创建表单组件
4. 在月视图添加 + 号图标
5. 在日视图添加 + 号图标
6. 集成事件显示到月视图
7. 集成事件显示到日视图
8. 更新事件详情页支持编辑
File diff suppressed because it is too large Load Diff
@@ -1,184 +0,0 @@
# Config 目录重构计划
**日期**: 2026-03-02
**目标**: 重新整理 backend/src/core/config 下的配置文件,按领域分类
---
## 1. 需求背景
当前 `backend/src/core/config` 结构存在以下问题:
1. **职责不清**: `initialization` 模块放在 `core/` 根目录,但实际是配置初始化,应归属 `config/`
2. **分类粗放**: `static/agent/` 混入了不同领域的配置(LLM 目录、工具白名单、Agent 模板)
3. **配置不符规范**: CrewAI 模板缺少必要字段(backstory、expected_output),且 prompts 目录是冗余的
---
## 2. 目标结构
```
backend/src/core/
├── config/
│ ├── settings.py # 运行时配置(不变)
│ ├── initial/
│ │ └── init_data.py # 种子数据初始化(移动)
│ └── static/
│ ├── database/
│ │ ├── llm_catalog.yaml # LLM 目录(移动)
│ │ └── user_agent_catalog.yaml # 用户 Agent 种子(新增,置空)
│ └── crewai/
│ ├── agents.yaml # Agent 定义(移动 + 补充字段)
│ ├── tasks.yaml # Task 定义(移动 + 补充 expected_output
│ ├── workflow.yaml # 工作流(移动)
│ └── tools.yaml # 工具白名单(移动)
└── agent/ # 代码实现(不变)
└── crewai/
└── template_loader.py # 需更新路径引用
```
---
## 3. 涉及代码清单
### 3.1 需要移动/删除的文件
| 操作 | 源路径 | 目标路径 |
|------|--------|----------|
| 移动 | `core/initialization/init_data.py` | `core/config/initial/init_data.py` |
| 移动 | `core/config/static/agent/llm_catalog.yaml` | `core/config/static/database/llm_catalog.yaml` |
| 新建 | - | `core/config/static/database/user_agent_catalog.yaml` |
| 移动 | `core/config/static/agent/crewai/agents.yaml` | `core/config/static/crewai/agents.yaml` |
| 移动 | `core/config/static/agent/crewai/tasks.yaml` | `core/config/static/crewai/tasks.yaml` |
| 移动 | `core/config/static/agent/crewai/workflow.yaml` | `core/config/static/crewai/workflow.yaml` |
| 移动 | `core/config/static/agent/tools.yaml` | `core/config/static/crewai/tools.yaml` |
| 删除 | `core/config/static/agent/crewai/prompts/` | - |
### 3.2 需要修改的代码文件
| 文件 | 修改内容 |
|------|----------|
| `core/config/initial/init_data.py` | 更新 `_default_catalog_path()` 指向 `static/database/` |
| `core/agent/crewai/template_loader.py` | 1. 更新 `_default_static_root()` 指向 `static/crewai/`<br>2. 移除 `CrewAITemplate.prompts` 字段<br>3. 移除 `_read_prompt()` 和 prompts 加载逻辑 |
| `core/runtime/cli.py` | 更新 import 路径 |
| `tests/unit/core/test_agent_init_data.py` | 更新 import 路径和路径断言 |
| `tests/unit/core/config/test_crewai_template_loader.py` | 1. 更新路径构造<br>2. 移除 prompts 目录创建<br>3. 移除 prompts 相关断言 |
### 3.3 需要更新的配置文件内容
#### agents.yaml - 补充 backstory
```yaml
intent:
role: Intent Agent
goal: Classify user intent and decide execution strategy
backstory: >
You are an expert intent classifier with deep understanding
of user query patterns and dialogue acts. Your role is to
analyze user input and determine the appropriate action.
execution:
role: Execution Agent
goal: Execute tasks with available tools
backstory: >
You are a skilled task executor with expertise in tool calling,
API interactions, and result verification. You work systematically
to complete user requests.
organization:
role: Organization Agent
goal: Format final response and references
backstory: >
You specialize in presenting results in a clear, user-friendly manner.
You ensure responses are well-structured and actionable.
```
#### tasks.yaml - 补充 expected_output
```yaml
intent:
description: Identify user intent and required capabilities
expected_output: >
Structured intent classification with intent type, confidence score,
and recommended action plan
execution:
description: Execute intent with tools and model calls
expected_output: >
Verified execution results with tool outputs, status, and any errors
organization:
description: Format final response and references
expected_output: >
User-friendly response with structured output, citations, and
clear next steps if applicable
```
#### user_agent_catalog.yaml - 置空
```yaml
agents: []
```
---
## 4. 实施步骤
### Phase 1: 移动配置文件
1. 创建 `core/config/static/database/` 目录
2. 创建 `core/config/static/crewai/` 目录
3. 移动并合并 LLM 目录配置
4. 创建空的 user_agent_catalog.yaml
### Phase 2: 更新代码引用
1. 移动 `initialization/init_data.py``config/initial/init_data.py`
2. 更新 `init_data.py` 中的路径函数 `_default_catalog_path()``static/database/`
3. 更新 `template_loader.py`:
- 路径函数 `_default_static_root()``static/crewai/`
- 移除 `_read_prompt()` 函数
- 移除 `CrewAITemplate.prompts` 字段
- 移除 prompts 加载逻辑
4. 更新 `cli.py` 的 import 路径
5. 删除旧的 `core/initialization/` 目录(如为空)
### Phase 4: 更新测试
1. 更新 `test_agent_init_data.py`:
- import 路径: `core.initialization``core.config.initial`
- 路径断言: `static/agent/``static/database/`
2. 更新 `test_crewai_template_loader.py`:
- 路径构造: `agent/``crewai/`
- 移除 prompts 目录创建代码
- 移除 `template.prompts` 相关断言
3. 运行测试验证
---
## 5. 风险与回滚
### 风险
- 路径变更可能导致运行时找不到配置文件
- 测试路径断言需要同步更新
### 回滚方案
- 保留 git 分支,验证通过后再合并
- 如有问题,可通过 git revert 快速回滚
---
## 6. 验证方式
```bash
# 1. 运行单元测试
cd backend && uv run pytest tests/unit/core/test_agent_init_data.py tests/unit/core/config/test_crewai_template_loader.py -v
# 2. 运行 lint
cd backend && uv run ruff check src/core/config/ src/core/agent/crewai/ src/core/runtime/cli.py
# 3. 运行 typecheck
cd backend && uv run basedpyright src/core/config/ src/core/agent/crewai/ src/core/runtime/cli.py
```
@@ -1,95 +0,0 @@
# Agent Interrupt/Resume 遗留问题修复设计
## 1. 目标
本次修复一次性完成以下三项遗留问题:
1. `state_snapshot` 并发一致性问题(并发 resume 竞争)
2. `expires_at` 过期未强校验问题
3. `state_snapshot` 缺少强类型与版本化问题
## 2. 设计决策
采用方案 2(严格重构):
- `state_snapshot` 仅接受新结构,不再兼容旧结构
- 统一快照版本为 `version = 2`
- 使用强类型模型约束快照结构与状态迁移
- resume 入口引入行级锁语义,避免并发双写
## 3. 状态快照模型
`state_snapshot` 顶层结构:
```json
{
"version": 2,
"pending_tool_call": {
"interrupt_id": "int-1",
"tool_name": "srv.transfer_funds",
"tool_args": {"to": "u2", "amount": 100},
"status": "PENDING_APPROVAL",
"expires_at": "2026-03-03T12:00:00Z",
"decision": null,
"result": null,
"updated_at": "2026-03-03T11:59:00Z"
},
"run_context": {
"thread_id": "t1",
"run_id": "r1"
}
}
```
说明:
- `version` 必须为 2,否则拒绝处理
- `pending_tool_call` 字段缺失或类型错误,按无效快照处理
- `run_context` 仅保留 interrupt/resume 必需字段
## 4. 状态机约束
仅允许以下迁移:
- `PENDING_APPROVAL -> APPROVED_EXECUTING -> EXECUTED`
- `PENDING_APPROVAL -> REJECTED`
- `PENDING_APPROVAL -> EXPIRED`
非法状态迁移必须返回错误,不做隐式修复。
## 5. 并发与过期语义
- resume 前先对目标 session 加锁再读取快照
- 同一 `interrupt_id` 并发 resume 只能有一个请求成功
-`expires_at < now(UTC)`,先迁移为 `EXPIRED`,再返回 410
## 6. 错误语义(RFC7807
- `409 Conflict`: run/interrupt 不匹配,或并发冲突导致状态已消费
- `410 Gone`: 挂起调用已过期
- `422 Unprocessable Entity`: `state_snapshot` 非法或版本不匹配
- `404 Not Found`: 目标 session/run 不存在
## 7. 测试策略
采用 TDD,先写失败测试后实现:
- 快照版本校验(`version != 2`
- 快照结构校验(必填字段/类型)
- 并发 resume 幂等竞争(仅一个成功)
- 过期校验(返回 410 + 状态置 EXPIRED
- 合法状态迁移路径覆盖
## 8. 验证命令
- `uv run pytest backend/tests/unit/v1/agent -v`
- `uv run pytest backend/tests/integration/v1/agent/test_chat_routes.py -v`
- `uv run pytest backend/tests/integration/v1/agent/test_interrupt_resume_flow.py -v`
- `cd backend && uv run ruff check src/v1/agent`
- `cd backend && uv run basedpyright src/v1/agent`
## 9. 风险与回滚
- 风险:旧快照不再兼容,可能触发运行时拒绝
- 处置:通过明确 422 错误暴露不合规数据,结合日志定位并人工修复数据
- 回滚:回退本次变更并恢复旧快照解析逻辑(仅在紧急故障时)
@@ -1,377 +0,0 @@
# Agent Interrupt/Resume Strict Refactor Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 通过严格重构一次性修复 interrupt/resume 的并发安全、过期校验和 state_snapshot 强类型版本化问题。
**Architecture:**`state_snapshot v2` 为唯一合法结构,服务层使用强类型模型解析与状态迁移,resume 路径在读取会话时加行锁保证并发一致性。路由层维持现有 run/resume 入口,错误通过 HTTPException 输出,测试覆盖版本校验、过期语义、并发幂等和状态机迁移。
**Tech Stack:** FastAPI, SQLAlchemy AsyncSession, Pydantic v2, pytest
---
### Task 1: 新增 state_snapshot v2 强类型模型
**Files:**
- Modify: `backend/src/v1/agent/schemas.py`
- Test: `backend/tests/unit/v1/agent/test_schemas.py`
**Step 1: Write the failing test**
```python
def test_state_snapshot_v2_model_accepts_valid_payload():
payload = {
"version": 2,
"pending_tool_call": {
"interrupt_id": "int-1",
"tool_name": "srv.transfer_funds",
"tool_args": {"to": "u2", "amount": 100},
"status": "PENDING_APPROVAL",
"expires_at": "2026-03-03T12:00:00Z",
"decision": None,
"result": None,
"updated_at": "2026-03-03T11:59:00Z",
},
"run_context": {"thread_id": "t1", "run_id": "r1"},
}
model = AgentSessionSnapshot.model_validate(payload)
assert model.version == 2
def test_state_snapshot_v2_rejects_wrong_version():
payload = {
"version": 1,
"pending_tool_call": None,
"run_context": {"thread_id": "t1", "run_id": "r1"},
}
with pytest.raises(ValueError):
AgentSessionSnapshot.model_validate(payload)
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/unit/v1/agent/test_schemas.py -v`
Expected: FAIL`AgentSessionSnapshot` 未定义或校验不符合预期)
**Step 3: Write minimal implementation**
```python
class PendingToolStatus(str, Enum):
PENDING_APPROVAL = "PENDING_APPROVAL"
APPROVED_EXECUTING = "APPROVED_EXECUTING"
EXECUTED = "EXECUTED"
REJECTED = "REJECTED"
EXPIRED = "EXPIRED"
class PendingToolCall(BaseModel):
interrupt_id: str
tool_name: str
tool_args: dict[str, Any]
status: PendingToolStatus
expires_at: datetime
decision: dict[str, Any] | None = None
result: dict[str, Any] | None = None
updated_at: datetime
class SnapshotRunContext(BaseModel):
thread_id: str
run_id: str
class AgentSessionSnapshot(BaseModel):
version: Literal[2]
pending_tool_call: PendingToolCall | None = None
run_context: SnapshotRunContext
```
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/unit/v1/agent/test_schemas.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent/schemas.py backend/tests/unit/v1/agent/test_schemas.py
git commit -m "refactor(agent): add strict v2 session snapshot schema"
```
---
### Task 2: service 层改为 v2 快照读写(严格拒绝旧结构)
**Files:**
- Modify: `backend/src/v1/agent/service.py`
- Test: `backend/tests/unit/v1/agent/test_service_pending_tool_call.py`
**Step 1: Write the failing test**
```python
@pytest.mark.asyncio
async def test_set_pending_tool_call_writes_v2_snapshot(service, session):
await service.set_pending_tool_call(
session_id=session.id,
interrupt_id="int-1",
tool_name="srv.transfer_funds",
tool_args={"to": "u2", "amount": 100},
expires_at=datetime.now(timezone.utc) + timedelta(minutes=5),
thread_id="t1",
run_id="r1",
)
snapshot = await service.get_state_snapshot(session.id)
assert snapshot["version"] == 2
assert snapshot["run_context"]["run_id"] == "r1"
@pytest.mark.asyncio
async def test_invalid_legacy_snapshot_is_rejected(service, session):
session.state_snapshot = {"pending_tool_call": {"status": "PENDING_APPROVAL"}}
with pytest.raises(ValueError):
await service.apply_resume_decision(
session_id=session.id,
interrupt_id="int-1",
decision={"decision": "approved"},
)
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/unit/v1/agent/test_service_pending_tool_call.py -v`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def _build_snapshot_v2(...):
return AgentSessionSnapshot(...).model_dump(mode="json")
def _load_snapshot_v2(raw: dict[str, Any] | None) -> AgentSessionSnapshot:
if raw is None:
raise ValueError("state_snapshot missing")
return AgentSessionSnapshot.model_validate(raw)
```
并将 `set_pending_tool_call/get_state_snapshot/update_pending_tool_call_status` 全部改成 v2 模型读写。
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/unit/v1/agent/test_service_pending_tool_call.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent/service.py backend/tests/unit/v1/agent/test_service_pending_tool_call.py
git commit -m "refactor(agent): enforce v2 snapshot read write in service"
```
---
### Task 3: 增加 resume 行锁与并发幂等
**Files:**
- Modify: `backend/src/v1/agent/service.py`
- Test: `backend/tests/unit/v1/agent/test_resume_idempotency.py`
**Step 1: Write the failing test**
```python
@pytest.mark.asyncio
async def test_apply_resume_decision_uses_locked_session_fetch(service, fake_db, session):
await service.apply_resume_decision(
session_id=session.id,
interrupt_id="int-1",
decision={"decision": "approved"},
)
assert fake_db.last_fetch_with_lock is True
@pytest.mark.asyncio
async def test_resume_is_idempotent(service, session):
first = await service.apply_resume_decision(...)
second = await service.apply_resume_decision(...)
assert first.applied is True
assert second.applied is False
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/unit/v1/agent/test_resume_idempotency.py -v`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
async def _get_session_for_update(self, session_id: UUID) -> AgentChatSession | None:
stmt = (
select(AgentChatSession)
.where(AgentChatSession.id == session_id)
.with_for_update()
.limit(1)
)
result = await self._session.execute(stmt)
return result.scalar_one_or_none()
```
`apply_resume_decision` 改为锁内读取、校验、状态迁移,保证并发下单次生效。
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/unit/v1/agent/test_resume_idempotency.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent/service.py backend/tests/unit/v1/agent/test_resume_idempotency.py
git commit -m "fix(agent): add row lock for resume state transition"
```
---
### Task 4: 增加 expires_at 过期校验(含 EXPIRED 迁移)
**Files:**
- Modify: `backend/src/v1/agent/service.py`
- Test: `backend/tests/unit/v1/agent/test_resume_idempotency.py`
**Step 1: Write the failing test**
```python
@pytest.mark.asyncio
async def test_resume_expired_pending_returns_not_applied_and_marks_expired(service, session):
await service.set_pending_tool_call(..., expires_at=datetime.now(timezone.utc) - timedelta(seconds=1), thread_id="t1", run_id="r1")
result = await service.apply_resume_decision(
session_id=session.id,
interrupt_id="int-1",
decision={"decision": "approved"},
)
assert result.applied is False
snapshot = await service.get_state_snapshot(session.id)
assert snapshot["pending_tool_call"]["status"] == "EXPIRED"
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/unit/v1/agent/test_resume_idempotency.py -v`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
if pending.expires_at < datetime.now(timezone.utc):
pending.status = PendingToolStatus.EXPIRED
pending.updated_at = datetime.now(timezone.utc)
session.state_snapshot = snapshot.model_dump(mode="json")
return ResumeDecisionResult(applied=False, expired=True)
```
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/unit/v1/agent/test_resume_idempotency.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent/service.py backend/tests/unit/v1/agent/test_resume_idempotency.py
git commit -m "fix(agent): enforce expires_at when applying resume decision"
```
---
### Task 5: 路由层补齐 v2 快照与过期/冲突错误映射
**Files:**
- Modify: `backend/src/v1/agent/router.py`
- Modify: `backend/src/v1/agent/service.py`
- Test: `backend/tests/integration/v1/agent/test_chat_routes.py`
- Test: `backend/tests/integration/v1/agent/test_interrupt_resume_flow.py`
**Step 1: Write the failing test**
```python
def test_resume_route_returns_409_on_run_id_mismatch(client):
...
def test_resume_route_returns_410_when_pending_expired(client):
...
def test_resume_route_returns_422_for_legacy_snapshot(client):
...
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/integration/v1/agent/test_chat_routes.py backend/tests/integration/v1/agent/test_interrupt_resume_flow.py -v`
Expected: FAIL
**Step 3: Write minimal implementation**
`stream_resume` 或路由调用链里将领域错误映射为:
- 过期 -> `HTTPException(410)`
- 旧快照/结构错误 -> `HTTPException(422)`
- 状态冲突/重复消费 -> `HTTPException(409)`
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/integration/v1/agent/test_chat_routes.py backend/tests/integration/v1/agent/test_interrupt_resume_flow.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent/router.py backend/src/v1/agent/service.py backend/tests/integration/v1/agent/test_chat_routes.py backend/tests/integration/v1/agent/test_interrupt_resume_flow.py
git commit -m "fix(agent): map resume snapshot errors to 409 410 422"
```
---
### Task 6: 更新文档并完成验证
**Files:**
- Modify: `docs/plans/2026-03-03-agent-chat-design.md`
- Modify: `docs/runtime/runtime-route.md`
**Step 1: Update docs**
- 明确 `state_snapshot version=2` 为唯一支持结构
- 明确 resume 过期与并发冲突语义(410/409)
- 明确旧快照拒绝策略(422
**Step 2: Run unit tests**
Run: `uv run pytest backend/tests/unit/v1/agent -v`
Expected: PASS
**Step 3: Run integration tests**
Run: `uv run pytest backend/tests/integration/v1/agent/test_chat_routes.py backend/tests/integration/v1/agent/test_interrupt_resume_flow.py -v`
Expected: PASS
**Step 4: Run static checks**
Run: `cd backend && uv run ruff check src/v1/agent`
Expected: PASS
Run: `cd backend && uv run basedpyright src/v1/agent`
Expected: PASS
**Step 5: Commit**
```bash
git add docs/plans/2026-03-03-agent-chat-design.md docs/runtime/runtime-route.md
git commit -m "docs(agent): document strict snapshot v2 and resume error semantics"
```
---
Plan complete and saved to `docs/plans/2026-03-03-interrupt-resume-fixes-implementation-plan.md`.
Execution mode selected by user request: Subagent-Driven (this session), proceed task-by-task immediately.
@@ -0,0 +1,201 @@
# Agent 后端硬切重构设计
## 目标
- 一次性移除现有 Agent 运行时代码、测试和旧文档契约,避免新旧方案并存。
- 仅从后端重新设计 Agent 体系,不依赖前端实现细节。
- 新方案必须满足以下六项要求:
1. 配置层可通过 `.env` 驱动 LLM API Key。
2. 对话与 resume 通过 Celery 队列处理,不阻塞 Web 主线程。
3. `v1/agent` 仅负责路由组织与服务调用,核心逻辑在 `core/agent`
4. 按 CrewAI 官方模型组织 Agent/Task/Crew/Flow/Tools。
5. 按 AG-UI 协议输出事件,优先使用 `ag-ui-crewai` 适配库。
6. 使用 LiteLLM 统计每次 LLM 调用的 token 和 cost。
## 设计原则
- 单一职责:HTTP 层只做协议和鉴权,编排与执行下沉到核心层。
- 异步优先:长耗时推理、工具调用、恢复流程全部异步化。
- 协议优先:AG-UI 作为唯一事件契约,不维护自定义事件方言。
- 可观测性优先:每次 run、每次 stage、每次 LLM 调用可追踪。
- 配置单一来源:所有密钥和模型配置只走 `core.config.settings`
## 目标架构
### 1) 分层
- `backend/src/v1/agent/`
- `router.py`: 暴露 HTTP/SSE 接口。
- `schemas.py`: 请求/响应 DTO 和输入校验。
- `dependencies.py`: DI 装配。
- `service.py`: 薄服务,仅调用 `core/agent` 应用服务。
- `backend/src/core/agent/`
- `application/`: run/resume 应用服务。
- `domain/`: run 状态机、resume 幂等语义、错误模型。
- `infrastructure/crewai/`: CrewAI Agent/Task/Crew/Flow 装配与执行。
- `infrastructure/agui/`: AG-UI 事件映射与 SSE 序列化。
- `infrastructure/litellm/`: LiteLLM 客户端与 usage/cost 拦截器。
- `infrastructure/queue/`: Celery task producer/consumer。
### 1.1) 配置来源与合并策略
- Agent 运行配置由两部分组成:
- 数据库存量配置:`system_agents`(每种 agent_type 对应 llm 与 llm_config)。
- 静态模板配置:`backend/src/core/config/static/crewai/*.yaml`(角色描述、任务模板、workflow、tools)。
- 合并策略:
- `llm``llm_config``system_agents` 为准。
- prompt 模板、task 描述、flow stage、tool 白名单以 static/crewai 为准。
- 若任一 agent_type 在 `system_agents` 缺失,运行前失败并返回受控错误。
### 2) 核心运行链路
1. `POST /api/v1/agent/runs` 只负责参数校验和鉴权。
2. 路由调用 `AgentRunAppService.enqueue_run()`,写入 run 记录并投递 Celery。
3. Worker 执行 `run_agent_task`
- 读取 run 上下文。
- 构建 CrewAI `Agent/Task/Crew/Flow`
- 通过 `ag-ui-crewai` 将执行事件转为 AG-UI 标准事件。
- 每次 LLM 调用由 LiteLLM 中间层记录 token/cost。
4. 事件落库并发布到事件通道(Redis Stream/Channel)。
5. SSE 接口从事件通道读取并持续推送,直到 `RUN_FINISHED``RUN_ERROR`
### 3) Resume 链路
1. `POST /api/v1/agent/runs/{run_id}/resume` 校验 `interrupt_id` 与决策 payload。
2. 调用 `enqueue_resume()` 投递 `resume_agent_task`
3. Worker 在事务内做并发控制:
- `run_id + interrupt_id` 幂等锁。
- 过期校验与状态迁移。
4. 恢复后继续 CrewAI Flow,事件按 AG-UI 继续输出。
### 4) Session 状态持久化
- 使用 `sessions.state_snapshot` 作为运行态单一快照来源。
- 快照至少包含:
- run 上下文(thread_id、run_id、stage
- pending_tool_callstool_call_id、tool_name、args、status、expires_at
- correlation 索引(tool_call_id -> message_id / step_id
- 所有中断/恢复均以 `state_snapshot` 事务更新为准,避免内存态漂移。
### 5) 会话与消息落库模型
- 会话主表:`sessions`
- 新建 run 时写入:`id/user_id/session_type/status=running/last_activity_at`
- 运行中持续更新:`status``last_activity_at``message_count``total_tokens``total_cost``state_snapshot`
- 运行结束更新:
- 成功:`status=completed`
- 失败:`status=failed`
- 消息表:`messages`
- 用户输入落库为 `role=user`(每次 run 开始时先写入)。
- 模型输出落库为 `role=assistant`(按最终聚合文本落库,保留 metadata 记录增量信息)。
- 工具调用结果落库为 `role=tool`,并写入 `tool_name``metadata.tool_call_id`
- `seq` 由每个 `session_id` 内单调递增分配,满足 `uq_messages_session_seq`
- 计量落库:每次 LLM 调用的 usage/cost 先写消息级,再聚合更新到 session 级。
## 六项要求落地映射
### 要求 1: `.env` 驱动 LLM API Key
- 新增 `LLMSettings``core.config.settings.Settings`,统一定义:
- `SOCIAL_LLM__PROVIDER_KEYS__DASHSCOPE`
- `SOCIAL_LLM__PROVIDER_KEYS__MINIMAX`
- `SOCIAL_LLM__PROVIDER_KEYS__MOONSHOT`
- `SOCIAL_LLM__PROVIDER_KEYS__DEEPSEEK`
- `SOCIAL_LLM__PROVIDER_KEYS__ARK`
- `SOCIAL_LLM__PROVIDER_KEYS__ZAI`
- 禁止 `os.environ` 直接读取密钥。
### 要求 2: 对话和 resume 走 Celery
- Web 层不直接执行编排。
- `run`/`resume` 一律入队,Worker 处理,Web 仅做事件流转发。
- 加入任务级超时、重试、死信策略。
### 要求 3: v1 仅路由与调用
- `v1/agent/service.py` 仅保留应用服务调用和错误映射。
- 任何编排、状态机、工具执行逻辑禁止进入 `v1`
### 要求 4: CrewAI 官方流程
- 采用 CrewAI 原生对象:`Agent``Task``Crew``Flow`
- tools 通过 CrewAI Tool 机制注册,不做平行实现。
- 任务模板与 agent 配置集中化(静态模板 + 运行时拼装)。
- 配置拼装明确依赖 `system_agents + static/crewai`,不再使用双套来源。
### 要求 5: AG-UI + ag-ui-crewai
- 事件集遵循 AG-UI 协议,生命周期闭环:
- `RUN_STARTED`
- 流式消息和工具事件
- 终态 `RUN_FINISHED``RUN_ERROR`
- 优先引入 `ag-ui-crewai` 做 CrewAI 到 AG-UI 的桥接,避免重复造轮子。
### 要求 6: LiteLLM token/cost 统计
- 所有 LLM 调用通过 LiteLLM 统一出入口。
- 按调用粒度记录:`input_tokens``output_tokens``total_tokens``cost``currency`
- 按 run 粒度聚合并落库,支持后续计费和审计。
## 数据与可观测性
- 保留现有 Agent 相关表结构,不在本次硬切做数据库破坏性变更。
- 新增事件日志与调用指标落点(如已有字段不足,后续增量迁移)。
- 日志使用结构化字段:`run_id``task_id``stage``tool_name``llm_model``latency_ms`
- 持久化原则:run/resume 的关键状态变更必须可重放,禁止仅保存在内存。
## 事务边界
- `run` 入口事务:创建或加载 `session` + 写入用户消息。
- `worker` 执行事务(可分阶段短事务):
- 阶段开始:更新 `session.status/state_snapshot`
- LLM 返回:写 assistant/tool 消息 + 更新 token/cost 聚合。
- 中断:写 `pending_tool_calls``state_snapshot` 并提交。
- 完成:更新终态 `session.status` 并提交。
- `resume` 事务:校验 `interrupt_id` 与 ownershipCAS 更新 `state_snapshot`,然后进入后续执行事务。
## 错误处理与安全
- API Key 缺失启动即失败,不进入运行态。
- 外部工具入参统一白名单和 schema 校验。
- resume 决策必须鉴权与会话所有权校验。
- 错误响应遵循 RFC 7807,避免泄漏敏感上下文。
## 工具调用与恢复语义
- 工具分三类:
- 前端工具:由 `RunAgentInput.tools` 提供能力声明,触发 interrupt,由客户端执行并回传 result。
- 后端工具(需审批):先 interrupt 给前端审批;审批通过后由后端执行,不由前端执行。
- 后端工具(直执):后端直接执行。
- 一致性约束:
- 每个 tool_result 必须携带 `tool_call_id`
- 后端仅接受当前 `state_snapshot.pending_tool_calls` 中存在且状态合法的 `tool_call_id`
- 若收到未知/已消费/过期 `tool_call_id`,立即产出 `RUN_ERROR` 并记录审计日志。
## 测试策略
- 单元测试:
- 配置解析与 key 解析
- run/resume 状态机与幂等
- LiteLLM usage 聚合
- 集成测试:
- API 入队
- Worker 消费
- SSE 事件顺序与终态
- E2E
- run 成功链路
- interrupt + resume 链路
- tool 调用链路
## 迁移策略
- 阶段 0(本次):硬切删除旧代码、旧测试、旧文档契约。
- 阶段 1:搭建新架构骨架和最小可运行 run 流程。
- 阶段 2:接入 CrewAI + ag-ui-crewai + LiteLLM 完整链路。
- 阶段 3:补齐可观测性、压测与稳定性治理。
## 验收标准
- 后端仓库不存在旧 `v1/agent``core/agent` 旧实现。
- 所有 Agent 相关旧测试与旧文档契约已移除。
- 新方案设计文档明确覆盖六项要求并可进入实现阶段。
@@ -0,0 +1,574 @@
# Agent 后端重建 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在后端重建 Agent 运行时,满足队列异步、CrewAI 配置打通、AG-UI 工具中断恢复、LiteLLM 计量、以及 `sessions.state_snapshot` 持久化要求。
**Architecture:** `v1/agent` 仅做 API/鉴权/参数校验与 SSE 输出,`core/agent` 负责编排与执行。Agent 创建配置由 `system_agents`(数据库)+ `core/config/static/crewai/*.yaml`(静态模板)合并生成。run/resume 全链路通过 Celery Worker 执行,状态写入 `sessions.state_snapshot`
**Tech Stack:** FastAPI, Celery, Redis, CrewAI, ag-ui-crewai, LiteLLM, SQLAlchemy, Alembic, pytest
---
### Task 1: 建立配置聚合器(system_agents + static/crewai
**Files:**
- Create: `backend/src/core/agent/infrastructure/config/resolver.py`
- Modify: `backend/src/core/config/static/crewai/agents.yaml`
- Modify: `backend/src/core/config/static/crewai/tasks.yaml`
- Create: `backend/src/core/config/static/crewai/workflow.yaml`
- Create: `backend/src/core/config/static/crewai/tools.yaml`
- Test: `backend/tests/unit/core/agent/test_config_resolver.py`
**Step 1: Write the failing test**
```python
def test_resolver_merges_system_agents_and_static_templates():
resolved = resolve_agent_runtime_config(...)
assert resolved.intent.llm.model_code == "deepseek-v3.2"
assert "intent" in resolved.workflow_stages
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_config_resolver.py::test_resolver_merges_system_agents_and_static_templates -q`
Expected: FAIL with `NameError` or import not found
**Step 3: Write minimal implementation**
```python
def resolve_agent_runtime_config(system_agents: list[dict], static_cfg: dict) -> RuntimeConfig:
by_type = {item["agent_type"]: item for item in system_agents}
return RuntimeConfig.from_sources(by_type=by_type, static_cfg=static_cfg)
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_config_resolver.py::test_resolver_merges_system_agents_and_static_templates -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/infrastructure/config/resolver.py backend/src/core/config/static/crewai backend/tests/unit/core/agent/test_config_resolver.py
git commit -m "feat: add system_agents and static crewai config resolver"
```
### Task 2: 统一 LLM Key 与模型配置入口
**Files:**
- Modify: `backend/src/core/config/settings.py`
- Modify: `.env.example`
- Create: `backend/tests/unit/core/config/test_llm_settings.py`
**Step 1: Write the failing test**
```python
def test_llm_keys_read_from_settings(monkeypatch):
monkeypatch.setenv("SOCIAL_LLM__PROVIDER_KEYS__DEEPSEEK", "k1")
s = Settings()
assert s.llm.provider_keys.deepseek == "k1"
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/config/test_llm_settings.py::test_llm_keys_read_from_settings -q`
Expected: FAIL with missing `llm` field
**Step 3: Write minimal implementation**
```python
class LLMProviderKeys(BaseModel):
deepseek: str | None = None
class LLMSettings(BaseModel):
provider_keys: LLMProviderKeys = LLMProviderKeys()
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/config/test_llm_settings.py::test_llm_keys_read_from_settings -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/config/settings.py .env.example backend/tests/unit/core/config/test_llm_settings.py
git commit -m "feat: centralize llm provider keys in settings"
```
### Task 3: sessions 表状态快照契约落地
**Files:**
- Create: `backend/alembic/versions/20260304_add_sessions_state_snapshot_contract.py`
- Modify: `backend/src/models/agent_chat_session.py`
- Create: `backend/tests/unit/database/test_sessions_state_snapshot_contract.py`
**Step 1: Write the failing test**
```python
def test_sessions_has_state_snapshot_column(db_inspector):
columns = db_inspector.get_columns("sessions")
assert "state_snapshot" in [c["name"] for c in columns]
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/database/test_sessions_state_snapshot_contract.py::test_sessions_has_state_snapshot_column -q`
Expected: FAIL when migration not applied
**Step 3: Write minimal implementation**
```python
def upgrade() -> None:
op.add_column("sessions", sa.Column("state_snapshot", postgresql.JSONB, nullable=True))
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/database/test_sessions_state_snapshot_contract.py::test_sessions_has_state_snapshot_column -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/alembic/versions/20260304_add_sessions_state_snapshot_contract.py backend/src/models/agent_chat_session.py backend/tests/unit/database/test_sessions_state_snapshot_contract.py
git commit -m "feat(db): enforce sessions state_snapshot contract"
```
### Task 3.1: 会话与消息持久化仓储
**Files:**
- Create: `backend/src/core/agent/infrastructure/persistence/session_repository.py`
- Create: `backend/src/core/agent/infrastructure/persistence/message_repository.py`
- Create: `backend/tests/integration/core/agent/test_session_message_persistence.py`
**Step 1: Write the failing test**
```python
def test_run_persists_user_and_assistant_messages(db_session):
run = execute_run(...)
rows = list_messages(session_id=run.session_id)
assert rows[0].role == "user"
assert rows[1].role == "assistant"
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/core/agent/test_session_message_persistence.py::test_run_persists_user_and_assistant_messages -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
async def append_message(...):
session.add(AgentChatMessage(...))
async def update_session_aggregate(...):
session_obj.message_count = message_count
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/core/agent/test_session_message_persistence.py::test_run_persists_user_and_assistant_messages -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/infrastructure/persistence backend/tests/integration/core/agent/test_session_message_persistence.py
git commit -m "feat: persist session lifecycle and messages for agent runs"
```
### Task 4: 定义 state_snapshot 结构与并发语义
**Files:**
- Create: `backend/src/core/agent/domain/state_snapshot.py`
- Create: `backend/tests/unit/core/agent/test_state_snapshot.py`
**Step 1: Write the failing test**
```python
def test_pending_tool_call_snapshot_contains_correlation_fields():
snap = StateSnapshot.new(...)
pending = snap.pending_tool_calls[0]
assert pending.tool_call_id
assert pending.status == "PENDING_APPROVAL"
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_state_snapshot.py::test_pending_tool_call_snapshot_contains_correlation_fields -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
class PendingToolCall(BaseModel):
tool_call_id: str
tool_name: str
status: Literal["PENDING_APPROVAL", "APPROVED", "EXECUTED", "REJECTED", "EXPIRED"]
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_state_snapshot.py::test_pending_tool_call_snapshot_contains_correlation_fields -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/domain/state_snapshot.py backend/tests/unit/core/agent/test_state_snapshot.py
git commit -m "feat: define sessions state_snapshot schema for run and tool state"
```
### Task 5: 工具路由策略(前端/后端/审批)
**Files:**
- Create: `backend/src/core/agent/domain/tool_policy.py`
- Create: `backend/tests/unit/core/agent/test_tool_policy.py`
**Step 1: Write the failing test**
```python
def test_frontend_tool_requires_interrupt_and_client_execution():
decision = classify_tool_call(name="ui.navigate_to", source="request.tools")
assert decision.mode == "FRONTEND_EXECUTE"
def test_backend_approval_tool_returns_interrupt_but_executes_on_backend_after_approve():
decision = classify_tool_call(name="srv.transfer_funds", requires_approval=True)
assert decision.mode == "BACKEND_APPROVAL_INTERRUPT"
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_tool_policy.py -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
if tool_name.startswith("ui."):
return ToolDecision(mode="FRONTEND_EXECUTE")
if requires_approval:
return ToolDecision(mode="BACKEND_APPROVAL_INTERRUPT")
return ToolDecision(mode="BACKEND_DIRECT_EXECUTE")
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_tool_policy.py -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/domain/tool_policy.py backend/tests/unit/core/agent/test_tool_policy.py
git commit -m "feat: add frontend/backend tool policy and approval routing"
```
### Task 6: tool_call 与 tool_result 对账机制
**Files:**
- Create: `backend/src/core/agent/domain/tool_correlation.py`
- Create: `backend/tests/unit/core/agent/test_tool_correlation.py`
**Step 1: Write the failing test**
```python
def test_rejects_tool_result_when_tool_call_id_not_pending():
store = PendingToolStore([])
with pytest.raises(ToolCorrelationError):
store.apply_result(tool_call_id="unknown", result={"ok": True})
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_tool_correlation.py::test_rejects_tool_result_when_tool_call_id_not_pending -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def apply_result(self, *, tool_call_id: str, result: dict) -> None:
pending = self._pending.get(tool_call_id)
if pending is None:
raise ToolCorrelationError("tool_call_id not pending")
pending.result = result
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_tool_correlation.py::test_rejects_tool_result_when_tool_call_id_not_pending -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/domain/tool_correlation.py backend/tests/unit/core/agent/test_tool_correlation.py
git commit -m "feat: add tool call/result correlation guard"
```
### Task 7: Celery run/resume 异步任务
**Files:**
- Create: `backend/src/core/agent/infrastructure/queue/tasks.py`
- Create: `backend/src/core/agent/application/run_service.py`
- Create: `backend/src/core/agent/application/resume_service.py`
- Test: `backend/tests/integration/core/agent/test_queue_run_resume.py`
**Step 1: Write the failing test**
```python
def test_run_api_enqueues_celery_task(client):
resp = client.post("/api/v1/agent/runs", json={...})
assert resp.status_code == 202
def test_resume_updates_session_status_and_snapshot(client):
resp = client.post("/api/v1/agent/runs/r1/resume", json={...})
assert resp.status_code == 202
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py::test_run_api_enqueues_celery_task -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def enqueue_run(cmd: RunCommand) -> str:
task = run_agent_task.apply_async(args=[cmd.model_dump()])
return task.id
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/core/agent/test_queue_run_resume.py::test_run_api_enqueues_celery_task -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/application backend/src/core/agent/infrastructure/queue backend/tests/integration/core/agent/test_queue_run_resume.py
git commit -m "feat: add celery-based run and resume tasks"
```
### Task 8: CrewAI 运行时加载与创建
**Files:**
- Create: `backend/src/core/agent/infrastructure/crewai/runtime.py`
- Create: `backend/src/core/agent/infrastructure/crewai/factory.py`
- Test: `backend/tests/unit/core/agent/test_crewai_runtime.py`
**Step 1: Write the failing test**
```python
def test_runtime_creates_agents_tasks_from_resolved_config():
runtime = CrewAIRuntime(...)
crew = runtime.build_crew(message="hello")
assert len(crew.agents) >= 1
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_crewai_runtime.py::test_runtime_creates_agents_tasks_from_resolved_config -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def build_crew(self, *, message: str) -> Crew:
agents = self._factory.build_agents(self._config)
tasks = self._factory.build_tasks(self._config, message=message)
return Crew(agents=agents, tasks=tasks)
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_crewai_runtime.py::test_runtime_creates_agents_tasks_from_resolved_config -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/infrastructure/crewai backend/tests/unit/core/agent/test_crewai_runtime.py
git commit -m "feat: create crewai runtime from resolved config"
```
### Task 9: AG-UI 与 ag-ui-crewai 事件桥
**Files:**
- Create: `backend/src/core/agent/infrastructure/agui/bridge.py`
- Create: `backend/src/core/agent/infrastructure/agui/stream.py`
- Test: `backend/tests/unit/core/agent/test_agui_bridge.py`
**Step 1: Write the failing test**
```python
def test_agui_stream_emits_required_lifecycle():
events = to_agui_events(internal_events=[...])
assert events[0]["type"] == "RUN_STARTED"
assert events[-1]["type"] in {"RUN_FINISHED", "RUN_ERROR"}
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_agui_bridge.py::test_agui_stream_emits_required_lifecycle -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def to_agui_events(internal_events: list[dict]) -> list[dict]:
return [map_event(e) for e in internal_events]
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_agui_bridge.py::test_agui_stream_emits_required_lifecycle -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/infrastructure/agui backend/tests/unit/core/agent/test_agui_bridge.py
git commit -m "feat: add ag-ui and ag-ui-crewai event bridge"
```
### Task 10: LiteLLM 调用统计与会话聚合
**Files:**
- Create: `backend/src/core/agent/infrastructure/litellm/client.py`
- Create: `backend/src/core/agent/infrastructure/litellm/usage_tracker.py`
- Test: `backend/tests/unit/core/agent/test_litellm_usage.py`
**Step 1: Write the failing test**
```python
def test_tracker_aggregates_per_call_usage_and_cost():
t = UsageTracker()
t.add({"input_tokens": 10, "output_tokens": 5, "cost": "0.1"})
assert t.snapshot()["total_tokens"] == 15
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_litellm_usage.py::test_tracker_aggregates_per_call_usage_and_cost -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
def add(self, usage: dict[str, object]) -> None:
self.input_tokens += int(usage.get("input_tokens", 0))
self.output_tokens += int(usage.get("output_tokens", 0))
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent/test_litellm_usage.py::test_tracker_aggregates_per_call_usage_and_cost -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agent/infrastructure/litellm backend/tests/unit/core/agent/test_litellm_usage.py
git commit -m "feat: add litellm usage and cost tracking"
```
### Task 11: v1/agent 薄层 API + SSE 出口
**Files:**
- Create: `backend/src/v1/agent/router.py`
- Create: `backend/src/v1/agent/schemas.py`
- Create: `backend/src/v1/agent/dependencies.py`
- Create: `backend/src/v1/agent/service.py`
- Modify: `backend/src/v1/router.py`
- Test: `backend/tests/integration/v1/agent/test_routes.py`
**Step 1: Write the failing test**
```python
def test_run_endpoint_returns_sse_and_not_blocking(client):
resp = client.post("/api/v1/agent/runs", json={...})
assert resp.status_code == 202
```
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/v1/agent/test_routes.py::test_run_endpoint_returns_sse_and_not_blocking -q`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
@router.post("/runs", status_code=202)
async def create_run(...):
task_id = service.enqueue_run(input_data)
return {"task_id": task_id}
```
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/v1/agent/test_routes.py::test_run_endpoint_returns_sse_and_not_blocking -q`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/v1/agent backend/src/v1/router.py backend/tests/integration/v1/agent/test_routes.py
git commit -m "feat: add thin v1 agent api and sse endpoints"
```
### Task 12: 端到端验证与文档回填
**Files:**
- Modify: `docs/runtime/runtime-route.md`
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: Run unit tests**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent backend/tests/unit/core/config backend/tests/unit/database -q`
Expected: PASS
**Step 2: Run integration tests**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration/core/agent backend/tests/integration/v1/agent -q`
Expected: PASS
**Step 3: Run lint and typecheck**
Run: `PYTHONPATH=backend/src uv run ruff check backend/src backend/tests`
Expected: PASS
Run: `PYTHONPATH=backend/src uv run basedpyright backend/src`
Expected: PASS
**Step 4: Document protocol contracts**
在运行手册中补充以下固定规则:
- `system_agents` + `static/crewai` 配置合并优先级。
- `sessions.state_snapshot` 字段结构与版本号。
- `messages` 入库顺序与 `sessions` 聚合字段更新规则。
- 工具调用审批与恢复时序图。
- tool_call/result 不匹配时的错误语义(`RUN_ERROR` + 可审计日志)。
**Step 5: Commit**
```bash
git add docs/runtime/runtime-route.md docs/runtime/runtime-runbook.md
git commit -m "docs: add new agent runtime contracts and operational guide"
```
## Success Criteria
- [ ] Agent 创建配置由 `system_agents``core/config/static/crewai` 合并生成。
- [ ] run/resume 仅通过 Celery Worker 执行,Web 不执行编排。
- [ ] `v1/agent` 无业务编排代码。
- [ ] `sessions.state_snapshot` 承担运行态和工具审批恢复状态。
- [ ] 每次 run/resume 的会话状态变更均落库到 `sessions`
- [ ] 用户/助手/工具消息按 `messages` 约束落库,`seq` 单调递增。
- [ ] 前端工具与后端工具(审批/非审批)策略完整可测。
- [ ] tool_call 与 tool_result 具备强关联校验并可恢复/报错。
- [ ] LiteLLM 逐次计量与 run 聚合可落库。
-66
View File
@@ -786,72 +786,6 @@
---
## Agent
### POST /agent/runs
创建 Agent 运行(需要认证,SSE 响应)。
**Request (RunAgentInput):**
```json
{
"threadId": "string",
"runId": "string",
"parentRunId": "string?",
"state": {},
"messages": [],
"tools": [],
"context": [],
"forwardedProps": {},
"resume": null
}
```
**Response:** 200 OK (`text/event-stream`)
**Errors:**
- 401: 未认证
- 422: 请求参数无效
### POST /agent/runs/{run_id}/resume
恢复被中断运行(需要认证,SSE 响应)。
**Request (RunAgentInput):**
```json
{
"threadId": "string",
"runId": "string",
"state": {},
"messages": [],
"tools": [],
"context": [],
"forwardedProps": {},
"resume": {
"interruptId": "string",
"payload": {}
}
}
```
**State Snapshot Contract:**
- `state_snapshot` 仅支持 `version = 2`
- 顶层必须包含 `run_context``pending_tool_call`
- 旧格式或缺失字段会被拒绝
**Resume Semantics:**
- 同一 `interrupt_id` 并发恢复仅允许一个请求成功
- `expires_at` 超时后会标记为 `EXPIRED`,恢复请求不再生效
**Errors:**
- 401: 未认证
- 404: 会话不存在
- 409: `run_id``interrupt_id` 冲突,或状态已被消费
- 410: 挂起调用已过期
- 422: `state_snapshot` 非法或版本不匹配
---
## Infra
### GET /infra/health
+2 -24
View File
@@ -159,22 +159,6 @@ curl -sS "${WEB_BASE_URL}/api/v1/profile/me" \
通过标准:接口返回符合预期的 2xx 或受控业务错误,无 5xx。
### L3 可选(Agent Chat 回归)
```bash
PYTHONPATH=backend/src uv run pytest backend/tests/unit -k agent_chat -q
PYTHONPATH=backend/src uv run pytest backend/tests/integration -k agent_chat -q
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py backend/tests/e2e/test_agent_chat_recent_session_home.py -q
curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent" \
-H 'Content-Type: application/json' \
-d '{"message":"hello"}'
```
通过标准:测试通过,`/api/v1/agent` 返回有效 `session_id` 与事件序列。
---
## Incident Playbook
### 1) 迁移未生效(常见于旧镜像)
@@ -195,13 +179,7 @@ curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent" \
- 定位:核对 `.env` 中 Supabase JWT 配置与签发方设置。
- 修复:修正配置后重启 web 进程并执行 L1/L2 验证。
### 4) Agent Chat 启动后异常
- 症状:`/api/v1/agent` 返回 5xx 或事件不完整。
- 定位:先跑 L3 测试,再看 `logs/errors/web.error.log`
- 修复:先恢复到可用版本,再排查迁移、配置与依赖差异。
### 5) Auth 邮件模板未生效 / 注册返回超时但邮件已发送
### 4) Auth 邮件模板未生效 / 注册返回超时但邮件已发送
- 症状:
- 收到默认英文模板(非 `infra/mail-templates`)。
@@ -261,7 +239,7 @@ docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-
| 2026-02-25 | 补充迁移防遗漏规则:容器迁移命令统一追加 --build;开发调试优先使用本地 CLI 一次性迁移脚本 |
| 2026-02-25 | Auth 注册切换为 OTP 三段式:signup/start、signup/verify、signup/resend;邮件模板改为纯验证码展示 |
| 2026-02-25 | 清理未使用配置类:删除 WebSettings/GunicornSettings/WorkerSettings/WorkerGroupSettings(脚本仍使用环境变量启动服务) |
| 2026-02-25 | 新增 Agent Chat 验证章节:bootstrap gate、分层测试命令与 run 接口 smoke 示例 |
| 2026-03-04 | Agent 运行时进入硬切重构:移除旧 Agent Chat 验证章节,待新方案落地后补充 |
| 2026-02-25 | 简化启动方式:dev-app-up -> app-up,分离 bootstrap 与服务启动 |
| 2026-02-25 | 重构为运维分层手册:Bootstrap Gate、分层验证、故障与回滚流程 |
| 2026-02-25 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 |