refactor: 重构 Agent 模块为 AgentScope,删除旧版 CrewAI/LiteLLM 实现

This commit is contained in:
qzl
2026-03-11 20:51:56 +08:00
parent 177ed616bf
commit 145e3dc615
149 changed files with 5120 additions and 11356 deletions
@@ -1,141 +0,0 @@
# AgentScope Agent Route Migration Handoff Plan
## 1) Reconfirmed Objective
- Keep external API paths unchanged under `/api/v1/agent/*`.
- Replace internal run/resume/events runtime path with `core/agentscope` modules.
- Use five modules only: `runtime`, `prompts`, `schemas`, `tools`, `events`.
- Put AG-UI event conversion + persistence + Redis export in `events`.
- Keep `/transcribe` under the same router prefix but independent from agent runtime.
- Continue migration until legacy `core/agent` is removable.
## 2) Current Progress Snapshot
### Completed
- Task 1 (schemas) finished:
- Added runtime-facing schemas in `core/agentscope/schemas/agent_runtime.py`.
- Exported aliases for compatibility (`AcceptedTaskResponse`, `TaskAcceptedResponse`, `TaskAccepted`).
- Task 2 (events) finished:
- Added `events` module with AG-UI conversion, SSE encoding, Redis stream bus, pipeline, and store abstraction.
- Security fixes applied:
- Prevent reserved key overwrite in AG-UI codec.
- Sanitize SSE stream id.
- Support Redis bytes payload decoding.
- SSE now reuses AG-UI protocol encoder (`EventEncoder`) instead of custom JSON-only logic.
- Task 3 (runtime adapter) finished:
- Added `AgentRouteRuntime` to emit internal events around orchestrator execution.
- Added step events for stage identification:
- `step.start/step.finish` for `intent`, `execution`, `report`.
- Error event payload no longer leaks raw exception text to clients.
- Task 4 (route/service wiring) largely finished:
- `/v1/agent/router.py` now uses `core.agentscope.events.to_sse_event`.
- `/v1/agent/dependencies.py` queue tasks switched to `core.agentscope.runtime.tasks`.
- `/v1/agent/dependencies.py` stream reads switched to `RedisStreamBus`.
- `/v1/agent/service.py` enqueue payload now carries `owner_id` and extracted `user_token`.
- Added tests for runtime task entrypoint dispatch/validation.
### In Progress / Not Finished
- Task 4 review wrap-up:
- One review already returned PASS for spec compliance after fixes.
- Final quality/security confirmation for latest delta should be re-run once before moving to Task 5.
- Task 5 (sessions/messages persistence ownership, cost/tokens/latency full persistence) not started.
- Task 6 (remove `core/agent` and clean imports) not started.
- Task 7 (frontend AG-UI contract and E2E validation) not started.
## 3) What Was Changed (Relevant Files)
### New Files
- `backend/src/core/agentscope/schemas/agent_runtime.py`
- `backend/src/core/agentscope/events/__init__.py`
- `backend/src/core/agentscope/events/agui_codec.py`
- `backend/src/core/agentscope/events/sse.py`
- `backend/src/core/agentscope/events/redis_bus.py`
- `backend/src/core/agentscope/events/store.py`
- `backend/src/core/agentscope/events/pipeline.py`
- `backend/src/core/agentscope/runtime/agent_route_runtime.py`
- `backend/src/core/agentscope/runtime/tasks.py`
- `backend/tests/unit/core/agentscope/schemas/test_agent_runtime_schemas.py`
- `backend/tests/unit/core/agentscope/events/test_agui_codec.py`
- `backend/tests/unit/core/agentscope/events/test_sse.py`
- `backend/tests/unit/core/agentscope/events/test_redis_bus.py`
- `backend/tests/unit/core/agentscope/events/test_pipeline.py`
- `backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py`
- `backend/tests/unit/core/agentscope/runtime/test_tasks.py`
### Modified Files
- `backend/src/core/agentscope/runtime/__init__.py`
- `backend/src/core/agentscope/schemas/__init__.py`
- `backend/src/v1/agent/router.py`
- `backend/src/v1/agent/dependencies.py`
- `backend/src/v1/agent/service.py`
## 4) Key References Used
### In-repo references
- Current agent route/service contracts:
- `backend/src/v1/agent/router.py`
- `backend/src/v1/agent/service.py`
- `backend/src/v1/agent/dependencies.py`
- `backend/src/v1/agent/repository.py`
- Existing runtime/orchestrator basis:
- `backend/src/core/agentscope/runtime/orchestrator.py`
### External reference project
- DIVA-backend async stream/task patterns (for architecture guidance only):
- `/home/qzl/Code/DIVA-backend/src/diva/services/app/conversation/task_event_stream_service.py`
- `/home/qzl/Code/DIVA-backend/src/diva/services/app/conversation/tasks.py`
- `/home/qzl/Code/DIVA-backend/src/diva/utils/agui_events.py`
### Protocol/framework references
- AG-UI protocol skill docs (event naming/shape guidance)
- AgentScope skill docs (`ReActAgent`, model/runtime usage)
## 5) Next Execution Plan (Continue From Here)
### Step A: Close Task 4 gates (quick)
- Re-run targeted checks for the latest Task 4 code:
- `uv run pytest tests/unit/v1/agent/test_service.py tests/unit/core/agentscope/runtime/test_tasks.py tests/unit/core/agentscope/runtime/test_agent_route_runtime.py tests/unit/core/agentscope/events -q`
- `uv run ruff check src/v1/agent src/core/agentscope/runtime src/core/agentscope/events tests/unit/core/agentscope/runtime tests/unit/core/agentscope/events`
- `uv run basedpyright src/v1/agent src/core/agentscope/runtime src/core/agentscope/events tests/unit/core/agentscope/runtime tests/unit/core/agentscope/events`
- Run one explicit code/security review pass on Task 4 final diff.
### Step B: Execute Task 5 (persistence migration)
- Implement `events.store` real persistence (replace `NullEventStore` path in runtime task assembly):
- persist sessions/messages from AG-UI wire events.
- include tokens/cost/latency fields.
- maintain session aggregates.
- Add unit + integration tests for persistence correctness and aggregation.
### Step C: Execute Task 6 (remove legacy core/agent)
- Move remaining required data structures into `core/agentscope/schemas`.
- Replace all `core.agent.*` imports in active code paths.
- Delete `backend/src/core/agent/**` when no runtime path depends on it.
- Add guard test to ensure no legacy imports remain.
### Step D: Execute Task 7 (frontend contract validation)
- Validate AG-UI event stream compatibility with current Flutter parser and bloc flow.
- Run impacted frontend tests for chat/event handling.
## 6) Risks and Notes
- Workspace is currently dirty with many unrelated app/backend files; avoid mixing commits.
- This handoff only tracks the AgentScope migration subset above.
- `/transcribe` remains in `v1/agent/router.py` and intentionally independent.
## 7) Resume Checklist (first actions next session)
1. Read this handoff file.
2. Re-run Task 4 final checks and review gates.
3. Start Task 5 by replacing `NullEventStore` with real store implementation.
4. Keep route contract stable (`/api/v1/agent/*`) until Task 7 is verified.
@@ -1,308 +0,0 @@
# AgentScope Agent Route Migration Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Keep `/api/v1/agent/*` routes stable while fully replacing old `core/agent` runtime with `core/agentscope` runtime, AG-UI event pipeline, Redis streaming, and session/message persistence.
**Architecture:** Route handlers remain under `v1/agent`, but all runtime behavior moves to `core/agentscope` across five modules (`runtime`, `prompts`, `schemas`, `tools`, `events`). The `events` module owns AG-UI conversion, persistence, and Redis stream publishing/reading. Runtime orchestrator emits internal events only, then delegates to `events.pipeline` for normalization, persistence, and transport.
**Tech Stack:** FastAPI, SQLAlchemy async, Redis streams, Taskiq, AgentScope ReActAgent, LiteLLM proxy, Pydantic v2, pytest.
---
### Task 1: Define AgentScope Runtime Schemas
**Files:**
- Modify: `backend/src/core/agentscope/schemas/__init__.py`
- Create: `backend/src/core/agentscope/schemas/agent_runtime.py`
- Test: `backend/tests/unit/core/agentscope/schemas/test_agent_runtime_schemas.py`
**Step 1: Write failing schema tests**
```python
def test_run_command_schema_roundtrip() -> None:
payload = {"threadId": "...", "runId": "...", "messages": []}
model = RunCommand.model_validate(payload)
assert model.model_dump(by_alias=True)["threadId"] == payload["threadId"]
```
**Step 2: Run tests to verify failure**
Run: `uv run pytest tests/unit/core/agentscope/schemas/test_agent_runtime_schemas.py -q`
Expected: FAIL because schema module/classes are missing.
**Step 3: Implement schemas**
```python
class RunCommand(BaseModel):
thread_id: str = Field(alias="threadId")
run_id: str = Field(alias="runId")
```
Also define: ResumeCommand, InternalRuntimeEvent, AgUiWireEvent, HistorySnapshotResponse, AcceptedTaskResponse.
**Step 4: Re-run tests**
Run: `uv run pytest tests/unit/core/agentscope/schemas/test_agent_runtime_schemas.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/schemas/agent_runtime.py backend/src/core/agentscope/schemas/__init__.py backend/tests/unit/core/agentscope/schemas/test_agent_runtime_schemas.py
git commit -m "feat: add agentscope runtime schemas for agent routes"
```
### Task 2: Build Events Module (AG-UI + Redis + Persistence)
**Files:**
- Create: `backend/src/core/agentscope/events/pipeline.py`
- Create: `backend/src/core/agentscope/events/agui_codec.py`
- Create: `backend/src/core/agentscope/events/redis_bus.py`
- Create: `backend/src/core/agentscope/events/sse.py`
- Create: `backend/src/core/agentscope/events/store.py`
- Create: `backend/src/core/agentscope/events/__init__.py`
- Test: `backend/tests/unit/core/agentscope/events/test_agui_codec.py`
- Test: `backend/tests/unit/core/agentscope/events/test_sse.py`
- Test: `backend/tests/unit/core/agentscope/events/test_pipeline.py`
**Step 1: Write failing tests for codec/sse/pipeline**
```python
def test_codec_maps_internal_text_delta_to_agui() -> None:
event = to_agui_wire(...)
assert event["type"] == "TEXT_MESSAGE_CONTENT"
```
**Step 2: Run tests to verify failure**
Run: `uv run pytest tests/unit/core/agentscope/events -q`
Expected: FAIL due to missing modules.
**Step 3: Implement module**
```python
class AgentScopeEventPipeline:
async def emit(self, event: InternalRuntimeEvent) -> str:
wire = to_agui_wire(event)
await self._store.persist(wire)
return await self._redis.append(wire)
```
Implement SSE encoder and Redis read with cursor support.
**Step 4: Re-run tests**
Run: `uv run pytest tests/unit/core/agentscope/events -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/events backend/tests/unit/core/agentscope/events
git commit -m "feat: add agentscope events pipeline for ag-ui redis and persistence"
```
### Task 3: Rebuild Runtime Orchestrator to Emit Internal Events
**Files:**
- Modify: `backend/src/core/agentscope/runtime/orchestrator.py`
- Modify: `backend/src/core/agentscope/runtime/__init__.py`
- Create: `backend/src/core/agentscope/runtime/agent_route_runtime.py`
- Test: `backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py`
**Step 1: Write failing runtime tests**
```python
@pytest.mark.asyncio
async def test_runtime_emits_run_started_and_finished() -> None:
events = await runtime.run(...)
assert events[0].type == "run_started"
```
**Step 2: Run tests to verify failure**
Run: `uv run pytest tests/unit/core/agentscope/runtime/test_agent_route_runtime.py -q`
Expected: FAIL before runtime adapter exists.
**Step 3: Implement runtime adapter**
```python
class AgentRouteRuntime:
async def run(self, command: RunCommand) -> RuntimeResult:
await self._events.emit(run_started_event(...))
...
```
Hook existing stage runtime (intent/execution/report) and stream text/tool events into pipeline.
**Step 4: Re-run tests**
Run: `uv run pytest tests/unit/core/agentscope/runtime/test_agent_route_runtime.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/runtime backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py
git commit -m "feat: add agentscope runtime adapter for agent route commands"
```
### Task 4: Replace v1 Agent Service Dependencies with AgentScope
**Files:**
- Modify: `backend/src/v1/agent/dependencies.py`
- Modify: `backend/src/v1/agent/service.py`
- Modify: `backend/src/v1/agent/router.py`
- Test: `backend/tests/unit/v1/agent/test_service.py`
- Test: `backend/tests/integration/v1/agent/test_sse_flow_live.py`
**Step 1: Write failing tests for route/service integration contracts**
```python
@pytest.mark.asyncio
async def test_enqueue_run_uses_agentscope_runtime() -> None:
resp = await service.enqueue_run(...)
assert resp.thread_id == input.thread_id
```
**Step 2: Run tests to verify failure**
Run: `uv run pytest tests/unit/v1/agent/test_service.py -q`
Expected: FAIL before dependency rewiring.
**Step 3: Implement rewiring**
```python
service = AgentService(runtime=AgentRouteRuntime(...), events=AgentScopeEventsFacade(...))
```
Keep paths unchanged (`/runs`, `/resume`, `/events`, `/history`), keep `/transcribe` standalone.
**Step 4: Re-run tests**
Run: `uv run pytest tests/unit/v1/agent/test_service.py tests/integration/v1/agent/test_sse_flow_live.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/v1/agent backend/tests/unit/v1/agent backend/tests/integration/v1/agent
git commit -m "refactor: route v1 agent endpoints to agentscope runtime"
```
### Task 5: Migrate Session/Message Persistence Ownership to AgentScope Events
**Files:**
- Modify: `backend/src/models/agent_chat_session.py`
- Modify: `backend/src/models/agent_chat_message.py`
- Modify/Create migrations under `backend/alembic/versions/*`
- Create: `backend/tests/integration/core/agentscope/test_persistence_metrics.py`
**Step 1: Write failing integration tests for metrics persistence**
```python
@pytest.mark.asyncio
async def test_message_tokens_cost_latency_persisted() -> None:
...
assert row.input_tokens > 0
```
**Step 2: Run tests to verify failure**
Run: `uv run pytest tests/integration/core/agentscope/test_persistence_metrics.py -q`
Expected: FAIL until event store persists metrics.
**Step 3: Implement persistence updates/migration if needed**
```python
await store.persist_message(..., input_tokens=..., latency_ms=...)
```
**Step 4: Re-run tests**
Run: `uv run pytest tests/integration/core/agentscope/test_persistence_metrics.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/events/store.py backend/src/models backend/alembic/versions backend/tests/integration/core/agentscope/test_persistence_metrics.py
git commit -m "feat: persist agentscope session and message metrics"
```
### Task 6: Remove core/agent and Finalize Imports
**Files:**
- Delete: `backend/src/core/agent/**`
- Modify: all import sites found by grep
- Test: `backend/tests/**` impacted suites
**Step 1: Write guard tests proving no core.agent imports remain**
```python
def test_no_core_agent_imports() -> None:
...
```
**Step 2: Run guard test and verify failure**
Run: `uv run pytest tests/unit/core/agentscope/test_no_legacy_agent_imports.py -q`
Expected: FAIL before cleanup.
**Step 3: Remove old module and update imports**
```python
# replace from core.agent... with core.agentscope...
```
**Step 4: Run full verification**
Run:
- `uv run pytest tests/unit/core/agentscope tests/unit/v1/agent -q`
- `uv run pytest tests/integration/core/agentscope tests/integration/v1/agent -q`
- `uv run ruff check src tests`
- `uv run basedpyright src tests`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src backend/tests
git commit -m "refactor: remove legacy core agent module after agentscope migration"
```
### Task 7: Frontend Contract Verification (No Route Change)
**Files:**
- Verify: `apps/lib/features/chat/data/models/ag_ui_event.dart`
- Verify: `apps/lib/features/chat/data/services/ag_ui_service.dart`
- Test: `apps/test/features/chat/**`
**Step 1: Add failing compatibility test for required AG-UI events**
```dart
test('supports run/text/tool event sequence') { ... }
```
**Step 2: Run test to verify failure**
Run: `cd apps && flutter test test/features/chat/...`
Expected: FAIL until backend event payload normalization is aligned.
**Step 3: Implement backend compatibility fixes only**
Keep frontend route and event type expectations unchanged where possible.
**Step 4: Re-run Flutter tests**
Run: `cd apps && flutter test`
Expected: PASS on impacted suites.
**Step 5: Commit**
```bash
git add apps/lib apps/test
git commit -m "test: verify ag-ui event contract compatibility for chat client"
```
@@ -1,47 +0,0 @@
# 日视图改进设计
**Date:** 2026-03-11
**Status:** 已确认
## 需求概述
对日历日视图进行三项改进:
1. 固定顶部头部
2. 添加「今天」快捷按钮
3. 双指缩放时间轴高度
## 设计方案
### 1. 固定顶部头部
使用 `Stack` + `Positioned` 布局:
- 外层 `Stack` 包含头部和可滚动内容
- 头部使用 `Positioned` 固定在顶部 `top: 0`
- 时间轴内容使用 `SingleChildScrollView` 可滚动
- 头部高度:68px
### 2. 「今天」按钮
- **位置**+ 号按钮左侧(`const Spacer()` 在返回和日期之间,+号和今天按钮靠近)
- **样式**
- 圆角按钮(`BorderRadius.circular(AppRadius.xl)`
- 背景:`AppColors.messageBtnWrap`
- 文字:黑色,「今天」
- **显示条件**:只有当 `_selectedDate` 不是今天时显示
- **点击行为**:调用 `_goToToday()` 跳转到今天
### 3. 双指缩放时间轴高度
使用 `GestureDetector` 监听缩放手势:
- `_hourHeight``const` 改为变量 `double _hourHeight = 34.0;`
- 添加缩放状态变量:
```dart
double _baseHourHeight = 34.0;
double _currentScale = 1.0;
```
- 缩放范围:0.5x ~ 2.0x17px ~ 68px/小时)
- 在 `_buildTimelineBoard()` 中使用 `_hourHeight` 动态计算高度
## 实现计划
见 `docs/plans/2026-03-11-calendar-dayview-improvement-impl.md`
@@ -1,223 +0,0 @@
# 日视图改进实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 对日历日视图进行三项改进:固定顶部头部、添加「今天」按钮、双指缩放时间轴高度
**Architecture:** 使用 Stack + Positioned 布局固定头部,使用 GestureDetector 监听缩放手势动态调整时间轴高度
**Tech Stack:** Flutter, Dart
---
### Task 1: 修改 _hourHeight 为变量并添加缩放状态
**Files:**
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart:27-38`
**Step 1: 添加状态变量**
`_CalendarDayWeekScreenState` 类中:
-`static const double _hourHeight = 34;` 改为 `double _hourHeight = 34.0;`
- 添加缩放相关变量:
```dart
double _baseHourHeight = 34.0;
double _currentScale = 1.0;
```
**Step 2: Commit**
```bash
git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "refactor: 将 _hourHeight 改为变量支持缩放"
```
---
### Task 2: 实现双指缩放时间轴高度功能
**Files:**
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart`
**Step 1: 添加缩放手势监听**
在 `build` 方法的外层 `Scaffold` 包装 `GestureDetector`
```dart
return Scaffold(
backgroundColor: AppColors.todoBg,
body: GestureDetector(
onScaleStart: (details) {
_baseHourHeight = _hourHeight;
},
onScaleUpdate: (details) {
setState(() {
_currentScale = details.scale.clamp(0.5, 2.0);
_hourHeight = (_baseHourHeight * _currentScale).clamp(17.0, 68.0);
});
},
child: SafeArea(...),
),
);
```
**Step 2: 运行测试验证**
运行 Flutter 测试确保没有破坏现有功能。
**Step 3: Commit**
```bash
git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "feat: 添加双指缩放时间轴高度功能"
```
---
### Task 3: 实现固定顶部头部布局
**Files:**
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart:78-113`
**Step 1: 重构 build 方法为 Stack 布局**
将 `Column` 改为 `Stack`,头部使用 `Positioned` 固定:
```dart
return Scaffold(
backgroundColor: AppColors.todoBg,
body: Stack(
children: [
// 可滚动内容
Positioned.fill(
top: 68, // 头部高度
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: AppSpacing.lg,
right: AppSpacing.lg,
top: 2,
bottom: 104,
),
child: Column(
children: [
_buildWeekStrip(),
const SizedBox(height: 8),
KeyedSubtree(
key: _eventsKey,
child: _buildTimelineBoard(),
),
],
),
),
),
),
// 固定头部
Positioned(
top: 0,
left: 0,
right: 0,
child: _buildHeader(),
),
// 底部 dock
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildBottomDock(),
),
],
),
);
```
**Step 2: 运行验证**
确保头部固定在顶部,内容可滚动。
**Step 3: Commit**
```bash
git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "feat: 固定日视图头部在顶部"
```
---
### Task 4: 添加「今天」快捷按钮
**Files:**
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart:115-183`
**Step 1: 修改 _buildHeader 添加「今天」按钮**
在 `_buildHeader` 方法中:
- 在 + 号按钮左侧添加「今天」按钮
- 使用 `isSameDay(_selectedDate, DateTime.now())` 判断是否显示
- 添加 `_goToToday()` 方法:
```dart
void _goToToday() {
final today = DateTime.now();
setState(() {
_selectedDate = today;
});
_calendarManager.setSelectedDate(today);
_updateMonthDates();
_scrollToSelectedDate(animate: true);
_loadEvents();
}
```
**Step 2: 修改 + 号按钮位置**
将 + 号按钮移到最右侧,今天按钮在 + 号左侧。
**Step 3: 运行验证**
- 查看非今天日期时是否显示「今天」按钮
- 点击后是否正确跳转到今天
**Step 4: Commit**
```bash
git add apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart
git commit -m "feat: 添加今天快捷按钮"
```
---
### Task 5: 运行完整测试验证
**Step 1: 运行 Flutter 测试**
```bash
cd apps && flutter test
```
**Step 2: 手动验证**
- 日视图固定头部
- 「今天」按钮显示和跳转
- 双指缩放高度
**Step 3: Commit**
```bash
git add .
git commit -m "test: 验证日视图改进功能"
```
---
### Task 6: 更新文档并合并
**Step 1: 更新 runtime-route.md**
同步更新 `docs/runtime/runtime-route.md` 中的日历相关描述。
**Step 2: 提交并推送到远程**
```bash
git push origin dev
```
---
**Plan complete.**
@@ -0,0 +1,500 @@
# 日历邀请弹窗优化 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 优化日历邀请消息弹窗,显示完整信息(发送者名称 + 日历标题),复用公共弹窗组件
**Architecture:**
- 后端新增用户信息查询接口
- 前端创建公共弹窗组件 MessageActionSheet
- 日历邀请通过 scheduleItemId 获取标题,通过 senderId 获取发送者名称
**Tech Stack:** Flutter (Dart), FastAPI (Python)
---
### Task 1: 后端添加用户信息查询接口
**Files:**
- Modify: `backend/src/v1/users/router.py`
- Modify: `backend/src/v1/users/service.py`
- Modify: `backend/src/v1/users/repository.py`
**Step 1: 添加 repository 方法**
修改 `backend/src/v1/users/repository.py`,在 `UserRepository``SQLAlchemyUserRepository` 中添加:
```python
async def get_by_user_id(self, user_id: UUID) -> Profile | None: ...
```
**Step 2: 添加 service 方法**
修改 `backend/src/v1/users/service.py`,添加:
```python
async def get_user_by_id(self, user_id: UUID) -> UserBasicInfo:
profile = await self._repository.get_by_user_id(user_id)
if not profile:
raise HTTPException(status_code=404, detail="User not found")
return UserBasicInfo(
id=str(profile.user_id),
username=profile.username,
avatar_url=profile.avatar_url,
)
```
**Step 3: 添加 router 接口**
修改 `backend/src/v1/users/router.py`,添加:
```python
@router.get("/{user_id}", response_model=UserBasicInfo)
async def get_user(
user_id: UUID,
service: Annotated[UserService, Depends(get_user_service)],
):
return await service.get_user_by_id(user_id)
```
**Step 4: 运行 lint 和 typecheck**
```bash
cd backend && uv run ruff check src/v1/users/ && uv run basedpyright src/v1/users/
```
**Step 5: 提交**
```bash
git add backend/src/v1/users/ && git commit -m "feat(users): add get user by id endpoint"
```
---
### Task 2: 前端添加用户 API 接口
**Files:**
- Modify: `apps/lib/features/users/data/users_api.dart`
**Step 1: 添加 getById 方法**
修改 `apps/lib/features/users/data/users_api.dart`,添加:
```dart
class UsersApi {
// ... existing code
Future<UserBasicInfo> getById(String userId) async {
final response = await _client.get('$_prefix/$userId');
return UserBasicInfo.fromJson(response.data);
}
}
class UserBasicInfo {
final String id;
final String username;
final String? avatarUrl;
factory UserBasicInfo.fromJson(Map<String, dynamic> json) {
return UserBasicInfo(
id: json['id'] as String,
username: json['username'] as String,
avatarUrl: json['avatar_url'] as String?,
);
}
}
```
**Step 2: 注册到 DI**
修改 `apps/lib/core/di/injection.dart`,添加:
```dart
sl<UsersApi>();
```
**Step 3: 运行 flutter analyze**
```bash
cd apps && flutter analyze lib/features/users/
```
**Step 4: 提交**
```bash
git add apps/lib/features/users/ apps/lib/core/di/injection.dart && git commit -m "feat(users): add getById API method"
```
---
### Task 3: 创建公共弹窗组件 MessageActionSheet
**Files:**
- Create: `apps/lib/features/messages/ui/widgets/message_action_sheet.dart`
- Modify: `apps/lib/features/messages/ui/screens/message_invite_list_screen.dart`
**Step 1: 创建弹窗组件**
创建 `apps/lib/features/messages/ui/widgets/message_action_sheet.dart`
```dart
import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
class MessageActionSheet extends StatelessWidget {
final String title;
final String? description;
final String? statusText;
final bool isReadOnly;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final IconData? icon;
final Color? iconColor;
const MessageActionSheet({
super.key,
required this.title,
this.description,
this.statusText,
this.isReadOnly = false,
this.onAccept,
this.onDecline,
this.icon,
this.iconColor,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
decoration: const BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColors.slate300,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 20),
if (icon != null) ...[
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: (iconColor ?? AppColors.blue500).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(icon, size: 32, color: iconColor ?? AppColors.blue500),
),
const SizedBox(height: 16),
],
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
textAlign: TextAlign.center,
),
if (description != null && description!.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
description!,
style: const TextStyle(fontSize: 14, color: AppColors.slate500),
textAlign: TextAlign.center,
),
],
if (statusText != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.slate100,
borderRadius: BorderRadius.circular(16),
),
child: Text(
statusText!,
style: const TextStyle(fontSize: 14, color: AppColors.slate600),
),
),
],
if (isReadOnly) ...[
const SizedBox(height: 24),
] else ...[
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: AppButton(
text: '拒绝',
isOutlined: true,
onPressed: () {
Navigator.pop(context);
onDecline?.call();
},
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: AppButton(
text: '接受',
onPressed: () {
Navigator.pop(context);
onAccept?.call();
},
),
),
],
),
],
const SizedBox(height: AppSpacing.xl),
],
),
);
}
}
```
**Step 2: 运行 flutter analyze**
```bash
cd apps && flutter analyze lib/features/messages/ui/widgets/message_action_sheet.dart
```
**Step 3: 提交**
```bash
git add apps/lib/features/messages/ui/widgets/message_action_sheet.dart && git commit -m "feat(messages): add MessageActionSheet component"
```
---
### Task 4: 重构日历邀请弹窗使用公共组件
**Files:**
- Modify: `apps/lib/features/messages/ui/screens/message_invite_list_screen.dart`
- Modify: `apps/lib/features/messages/ui/widgets/calendar_message_card.dart`
**Step 1: 添加依赖注入**
修改 `message_invite_list_screen.dart`,添加:
```dart
import '../../../users/data/users_api.dart';
import '../widgets/message_action_sheet.dart';
```
`_MessageInviteListScreenState` 中添加:
```dart
late final UsersApi _usersApi;
```
`initState` 中添加:
```dart
_usersApi = sl<UsersApi>();
```
**Step 2: 添加获取信息方法**
在类中添加:
```dart
Future<(String calendarTitle, String senderName)?> _getCalendarInviteInfo(
InboxMessageResponse message,
) async {
if (message.scheduleItemId == null || message.senderId == null) {
return null;
}
try {
final calendar = await _calendarApi.getById(message.scheduleItemId!);
final sender = await _usersApi.getById(message.senderId!);
return (calendar.title, sender.username);
} catch (e) {
return null;
}
}
```
**Step 3: 修改 _showCalendarInviteSheet 方法**
修改 `_showCalendarInviteSheet`,使用公共组件:
```dart
Future<void> _showCalendarInviteSheet(InboxMessageResponse message) async {
final itemId = message.scheduleItemId;
if (itemId == null) return;
final info = await _getCalendarInviteInfo(message);
final title = info != null
? '${info.$2} 邀请你加入日历'
: '日历邀请';
final description = info?.$1;
if (!mounted) return;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
icon: Icons.calendar_today,
iconColor: AppColors.blue500,
onAccept: () async {
try {
await _calendarApi.acceptSubscription(itemId);
await _inboxApi.markAsRead(message.id);
if (mounted) {
Toast.show(context, '已接受', type: ToastType.success);
_loadMessages();
}
} catch (e) {
if (mounted) {
Toast.show(context, '操作失败', type: ToastType.error);
}
}
},
onDecline: () async {
try {
await _calendarApi.rejectSubscription(itemId);
await _inboxApi.markAsRead(message.id);
if (mounted) {
Toast.show(context, '已拒绝', type: ToastType.success);
_loadMessages();
}
} catch (e) {
if (mounted) {
Toast.show(context, '操作失败', type: ToastType.error);
}
}
},
),
);
}
```
**Step 4: 添加已读日历邀请弹窗方法**
在类中添加:
```dart
Future<void> _showCalendarInviteReadOnlySheet(InboxMessageResponse message) async {
final itemId = message.scheduleItemId;
if (itemId == null) return;
final info = await _getCalendarInviteInfo(message);
final title = info != null
? '${info.$2} 邀请你加入日历'
: '日历邀请';
final description = info?.$1;
final statusText = message.status.value == 'accepted' ? '已接受' : '已拒绝';
if (!mounted) return;
showModalBottomSheet<void>(
context: context,
backgroundColor: Colors.transparent,
builder: (ctx) => MessageActionSheet(
title: title,
description: description,
statusText: statusText,
isReadOnly: true,
icon: Icons.calendar_today,
iconColor: AppColors.blue500,
),
);
}
```
**Step 5: 修改 _handleMessageTap 方法**
修改日历邀请部分的处理逻辑:
```dart
case InboxMessageType.calendar:
final content = _parseCalendarContent(message.content);
if (content == null) return;
final type = content['type'] as String?;
if (type == 'invite') {
if (message.status.value == 'pending') {
await _showCalendarInviteSheet(message);
} else {
// 已读:显示弹窗,点击跳转日历
await _showCalendarInviteReadOnlySheet(message);
if (message.scheduleItemId != null && context.mounted) {
context.push('/calendar/events/${message.scheduleItemId}');
}
}
} else if (type == 'update') {
if (message.scheduleItemId != null) {
context.push('/calendar/events/${message.scheduleItemId}');
}
}
return;
```
**Step 6: 运行 flutter analyze**
```bash
cd apps && flutter analyze lib/features/messages/ui/screens/message_invite_list_screen.dart
```
**Step 7: 提交**
```bash
git add apps/lib/features/messages/ && git commit -m "refactor(messages): use MessageActionSheet for calendar invites"
```
---
### Task 5: 验证和测试
**Step 1: 运行完整测试**
```bash
cd apps && flutter test test/features/messages/
cd backend && uv run pytest tests/unit/v1/users/ -v
```
**Step 2: 手动测试场景**
1. 用户 A 发送日历邀请给用户 B
2. 用户 B 打开未读消息,点击日历邀请
3. 弹窗显示:"XXX 邀请你加入 [日历标题]"(发送者名称 + 日历标题)
4. 点击接受/拒绝
5. 用户 B 打开已读消息,点击日历邀请
6. 弹窗显示状态标签,点击弹窗外部跳转到日历详情页
---
## Summary
| Task | Description |
|------|-------------|
| 1 | 后端添加用户信息查询接口 `/api/v1/users/{user_id}` |
| 2 | 前端添加 UsersApi.getById 方法 |
| 3 | 创建公共弹窗组件 MessageActionSheet |
| 4 | 重构日历邀请弹窗使用公共组件,获取发送者名称和日历标题 |
| 5 | 验证测试 |
**Plan complete and saved to `docs/plans/2026-03-11-calendar-invite-sheet.md`. Two execution options:**
1. **Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
2. **Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
Which approach?
@@ -1,63 +0,0 @@
# 日历提醒字段与详情页对齐设计
**Date:** 2026-03-11
**Status:** 已确认
## 目标
- 修复日历事件详情页字段映射错误,去掉 raw metadata 直出
- 新增可持久化的提醒字段(方案1):`metadata.reminder_minutes`
- 打通前后端和 AgentScope 工具调用链
- 用前端本地通知实现系统提醒与震动
## 数据契约
### metadata 结构
```json
{
"color": "#4F46E5",
"location": "会议室A",
"notes": "带电脑",
"attachments": [],
"reminder_minutes": 15,
"version": 1
}
```
### 字段规则
- `reminder_minutes`: `int | null`
- 取值范围:`0..10080`(0 表示准时提醒,10080 表示最多提前 7 天)
- 兼容历史数据:缺失或 null 视为无提醒
## 前端设计
1. 模型层(`ScheduleMetadata`)新增 `reminderMinutes`
2. 详情页:提醒时间改为结构化渲染
- null: `无`
- 0: `准时提醒`
- n: `开始前 n 分钟`
3. 创建/编辑弹层新增提醒选项,默认值为 `15`
4. 删除 metadata raw 原样渲染区块
## 本地通知设计
- 采用 Flutter 本地通知,调度时间:`startAt - reminderMinutes`
- 创建/编辑成功:重建该事件通知
- 删除成功:取消该事件通知
- App 启动后:扫描未来事件并重建通知(补偿机制)
## 后端与 AgentScope 设计
1. `ScheduleItemMetadata` 增加 `reminder_minutes`
2. service 继续走 `metadata -> extra_metadata`,不加新 DB 列
3. AgentScope `calendar.write` 增加 `reminder_minutes` 参数
4. CrewAI calendar tool 将 `reminderMinutes` 映射为 `metadata.reminder_minutes`
5. calendar tool 回包增加 `reminderMinutes` 字段
## 验证策略
- 后端:schemas/service/agentscope 单元测试
- 前端:calendar_api 与详情页渲染测试
- 手动:创建提醒 -> 等待系统通知与震动 -> 更新/删除后确认调度变更
@@ -1,170 +0,0 @@
# Calendar Reminder Metadata Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add `metadata.reminder_minutes` end-to-end (frontend/backend/AgentScope), fix detail-page field rendering, and enable local system reminders.
**Architecture:** Keep calendar schema additive via `metadata` JSON (no new DB columns). Backend validates and persists `reminder_minutes`; AgentScope tools accept and pass reminder values; frontend parses/edits/displays reminder and schedules local notifications based on event time.
**Tech Stack:** Flutter, FastAPI, Pydantic v2, AgentScope toolkit, pytest, flutter_test.
---
### Task 1: Backend metadata schema tests first
**Files:**
- Test: `backend/tests/unit/v1/schedule_items/test_schemas.py`
- Modify: `backend/src/v1/schedule_items/schemas.py`
**Step 1: Write failing tests**
- Add tests for `reminder_minutes` accepted values (`None`, `0`, `15`, `10080`)
- Add tests for invalid values (`-1`, `10081`)
**Step 2: Run tests to verify RED**
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_schemas.py -q`
Expected: FAIL for missing/invalid field support.
**Step 3: Minimal implementation**
- Add `reminder_minutes: int | None = Field(default=None, ge=0, le=10080)` to `ScheduleItemMetadata`
**Step 4: Verify GREEN**
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_schemas.py -q`
Expected: PASS.
### Task 2: Backend service mapping tests first
**Files:**
- Test: `backend/tests/unit/v1/schedule_items/test_service.py`
- Modify: `backend/src/v1/schedule_items/service.py`
**Step 1: Write failing tests**
- Assert create/update `extra_metadata` includes `reminder_minutes`
**Step 2: Run RED**
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py -q`
**Step 3: Minimal implementation**
- Ensure model_dump path includes new field naturally, no special-case stripping
**Step 4: Verify GREEN**
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py -q`
### Task 3: AgentScope custom tool tests first
**Files:**
- Test: `backend/tests/unit/core/agentscope/test_calendar_tools.py`
- Modify: `backend/src/core/agentscope/tools/custom/calendar.py`
**Step 1: Write failing tests**
- `calendar_write` maps `reminder_minutes` to tool args `reminderMinutes`
- rejects out-of-range reminder values
**Step 2: Run RED**
Run: `uv run pytest backend/tests/unit/core/agentscope/test_calendar_tools.py -q`
**Step 3: Minimal implementation**
- Add `reminder_minutes` parameter and validation in `calendar_write`
- Add mapping into `tool_args`
**Step 4: Verify GREEN**
Run: `uv run pytest backend/tests/unit/core/agentscope/test_calendar_tools.py -q`
### Task 4: CrewAI calendar bridge tests first
**Files:**
- Test: `backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py`
- Modify: `backend/src/core/agent/infrastructure/crewai/tools/create_calendar_event_tool.py`
**Step 1: Write failing tests**
- create path maps `reminderMinutes -> metadata.reminder_minutes`
- update path can patch `reminder_minutes`
**Step 2: Run RED**
Run: `uv run pytest backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py -q`
**Step 3: Minimal implementation**
- Extend `_resolve_metadata`, `_execute_update`, and `_event_payload`
**Step 4: Verify GREEN**
Run: `uv run pytest backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py -q`
### Task 5: Frontend model/API tests first
**Files:**
- Test: `apps/test/features/calendar/data/calendar_api_test.dart`
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
**Step 1: Write failing tests**
- parse `metadata.reminder_minutes`
- serialize `metadata.reminder_minutes` in create/update payload
**Step 2: Run RED**
Run: `cd apps && flutter test test/features/calendar/data/calendar_api_test.dart`
**Step 3: Minimal implementation**
- add `reminderMinutes` in model + json mapping
**Step 4: Verify GREEN**
Run: `cd apps && flutter test test/features/calendar/data/calendar_api_test.dart`
### Task 6: Detail UI rendering fix tests first
**Files:**
- Create/Test: `apps/test/features/calendar/ui/screens/calendar_event_detail_screen_test.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart`
**Step 1: Write failing widget tests**
- reminder text for null/0/15
- metadata raw block no longer visible
**Step 2: Run RED**
Run: `cd apps && flutter test test/features/calendar/ui/screens/calendar_event_detail_screen_test.dart`
**Step 3: Minimal implementation**
- remove raw metadata section
- render structured reminder text
**Step 4: Verify GREEN**
Run: `cd apps && flutter test test/features/calendar/ui/screens/calendar_event_detail_screen_test.dart`
### Task 7: Local notification service integration
**Files:**
- Create: `apps/lib/core/notifications/local_notification_service.dart`
- Modify: `apps/lib/core/di/injection.dart`
- Modify: `apps/lib/main.dart`
- Modify: `apps/lib/features/calendar/data/services/mock_calendar_service.dart`
- Modify: `apps/lib/features/calendar/ui/widgets/create_event_sheet.dart`
**Step 1: Add local notification dependencies**
- Update `apps/pubspec.yaml` with `flutter_local_notifications`
**Step 2: Implement scheduling API**
- init permissions
- schedule/update/cancel by event id
- vibration enabled for Android notification details
**Step 3: Integrate into calendar flow**
- create/update/delete hooks call notification service
- startup rebuild for future events
**Step 4: Verify manually**
- create reminder 1-2 min event and verify system notification + vibration
### Task 8: Full verification
**Step 1: Backend checks**
Run:
- `uv run pytest backend/tests/unit/v1/schedule_items/test_schemas.py -q`
- `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py -q`
- `uv run pytest backend/tests/unit/core/agentscope/test_calendar_tools.py -q`
- `uv run pytest backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py -q`
**Step 2: Frontend checks**
Run:
- `cd apps && flutter test test/features/calendar/data/calendar_api_test.dart`
- `cd apps && flutter test test/features/calendar/ui/screens/calendar_event_detail_screen_test.dart`
- `cd apps && flutter analyze lib/features/calendar lib/core/notifications`
**Step 3: Manual verification evidence**
- create/update/delete reminder event and capture observed notification behavior.
@@ -1,136 +0,0 @@
# 首页图片选择功能设计
## 1. 需求概述
在首页聊天界面的加号按钮弹出的底部面板中,实现拍照和相册选择图片功能:
- 最多选择 3 张图片
- 图片预览显示在输入框上方
- 图片可被取消移除
- 点击发送后图片随文本一起发送到后端
## 2. 技术方案
### 2.1 依赖
添加 `image_picker: ^1.0.7``pubspec.yaml`
### 2.2 状态管理
`HomeScreen` 中添加图片状态:
```dart
List<XFile> _selectedImages = []; // 最多3张
```
### 2.3 图片选择逻辑
修改 `home_sheet.dart`
- `image_picker` 选择图片(最多3张)
- 返回选中的 `List<XFile>``HomeScreen`
### 2.4 AG-UI 消息格式
修改 `ag_ui_service.dart``_buildRunInput` 方法,支持多模态消息:
```dart
Map<String, dynamic> _buildRunInput({
required String content,
List<XFile>? images,
}) {
final threadId = _threadId ?? _newUuid();
final runId = _nextId(_runIdPrefix);
// 构建多模态内容块
final contentBlocks = <Map<String, dynamic>>[];
// 添加文本
if (content.isNotEmpty) {
contentBlocks.add({'type': 'text', 'text': content});
}
// 添加图片(Base64 编码)
for (final image in images ?? []) {
final bytes = await image.readAsBytes();
final base64 = base64Encode(bytes);
contentBlocks.add({
'type': 'image',
'source': {
'type': 'base64',
'media_type': 'image/jpeg',
'data': base64,
},
});
}
return {
'threadId': threadId,
'runId': runId,
'state': <String, dynamic>{},
'messages': [
{
'id': _nextId('user_'),
'role': 'user',
'content': contentBlocks.length == 1
? (contentBlocks[0]['type'] == 'text'
? contentBlocks[0]['text']
: contentBlocks)
: contentBlocks,
},
],
// ...
};
}
```
## 3. UI 设计
### 3.1 图片预览区
位置:输入框上方,聊天消息区域下方
```
┌─────────────────────────────────────┐
│ 聊天消息区域 │
│ │
├─────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌────────┐│
│ │ ✕ │ │ ✕ │ │ ✕ ││ ← 预览区
│ │ [图片] │ │ [图片] │ │ [图片] ││
│ └─────────┘ └─────────┘ └────────┘│
├─────────────────────────────────────┤
│ [+] [ 输入消息... ] [发送]│
└─────────────────────────────────────┘
```
### 3.2 样式规格
| 元素 | 值 |
|------|-----|
| 预览卡片尺寸 | 80x80 dp |
| 圆角 | `AppRadius.md` (12dp) |
| 间距 | `AppSpacing.sm` (8dp) |
| 取消按钮 | 24x24 圆形,红色背景,白色 X 图标 |
| 边框 | 1dp `AppColors.slate200` |
### 3.3 交互
- 点击加号 → 底部弹出选择面板
- 选择图片 → 预览区显示缩略图
- 点击 X → 移除对应图片
- 输入文本 + 有图片 → 点击发送发送组合消息
## 4. 文件改动
| 文件 | 改动 |
|------|------|
| `pubspec.yaml` | 添加 image_picker 依赖 |
| `home_sheet.dart` | 实现拍照/相册选择 |
| `home_screen.dart` | 添加图片状态、预览区 UI |
| `ag_ui_service.dart` | 修改 _buildRunInput 支持多模态 |
## 5. 测试要点
- [ ] 选择 1-3 张图片正常显示
- [ ] 选择超过 3 张时提示或限制
- [ ] 图片可以成功移除
- [ ] 发送消息时图片 Base64 正确编码
- [ ] AG-UI 消息格式符合规范
@@ -1,463 +0,0 @@
# 首页图片选择功能实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在首页聊天界面实现拍照/相册选择图片功能,最多3张,图片随文本一起发送
**Architecture:** 使用 image_picker 选择图片,通过 AG-UI 多模态消息格式发送到后端
**Tech Stack:** Flutter, image_picker, AG-UI Protocol
---
### Task 1: 添加 image_picker 依赖
**Files:**
- Modify: `apps/pubspec.yaml`
**Step 1: 添加依赖**
`dependencies` 节点下添加:
```yaml
image_picker: ^1.0.7
```
**Step 2: 安装依赖**
Run: `cd apps && flutter pub get`
Expected: image_picker 添加成功
---
### Task 2: 实现 HomeSheet 图片选择功能
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_sheet.dart:1-113`
**Step 1: 添加 image_picker 导入和修改 HomeSheet**
```dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../../../core/theme/design_tokens.dart';
class HomeSheet extends StatelessWidget {
final Function(List<XFile>) onImagesSelected;
const HomeSheet({super.key, required this.onImagesSelected});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
color: const Color(0x4D0F172A),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
child: Column(
children: [
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: AppColors.slate300,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
_buildSheetContent(context),
],
),
),
),
],
),
),
);
}
Widget _buildSheetContent(BuildContext context) {
return SizedBox(
height: 280,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildOptionCard(
context: context,
icon: LucideIcons.camera,
label: '拍照',
onTap: () => _handleCameraTap(context),
),
const SizedBox(width: 24),
_buildOptionCard(
context: context,
icon: LucideIcons.image,
label: '相册',
onTap: () => _handlePhotoTap(context),
),
],
),
);
}
Widget _buildOptionCard({
required BuildContext context,
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.blue50,
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, size: 32, color: AppColors.blue500),
),
const SizedBox(height: 12),
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.slate700,
),
),
],
),
);
}
Future<void> _handleCameraTap(BuildContext context) async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image != null) {
onImagesSelected([image]);
}
if (context.mounted) {
Navigator.of(context).pop();
}
}
Future<void> _handlePhotoTap(BuildContext context) async {
final picker = ImagePicker();
final images = await picker.pickMultiImage(
imageQuality: 80,
limit: 3,
);
if (images.isNotEmpty) {
onImagesSelected(images);
}
if (context.mounted) {
Navigator.of(context).pop();
}
}
}
```
**Step 2: 验证编译**
Run: `cd apps && flutter analyze lib/features/home/ui/screens/home_sheet.dart`
Expected: No errors
---
### Task 3: 修改 HomeScreen 添加图片预览区
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart:1-820`
**Step 1: 添加导入和状态变量**
在文件顶部添加导入:
```dart
import 'package:image_picker/image_picker.dart';
```
`_HomeScreenState` 类中添加状态变量:
```dart
List<XFile> _selectedImages = [];
```
**Step 2: 添加图片预览 Widget**
`_buildInputContainer` 方法之前添加:
```dart
Widget _buildImagePreview() {
if (_selectedImages.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
left: _inputPadding,
right: _inputPadding,
bottom: AppSpacing.sm,
),
child: Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: _selectedImages.asMap().entries.map((entry) {
final index = entry.key;
final image = entry.value;
return _buildImageThumbnail(image, index);
}).toList(),
),
);
}
Widget _buildImageThumbnail(XFile image, int index) {
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.md),
child: Image.file(
File(image.path),
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => _removeImage(index),
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: AppColors.red500,
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.x,
size: 14,
color: AppColors.white,
),
),
),
),
],
);
}
void _removeImage(int index) {
setState(() {
_selectedImages.removeAt(index);
});
}
```
**Step 3: 修改 _buildInputContainer 调用位置**
`_buildInputContainer` 调用之前插入图片预览:
```dart
// 在 build 方法中修改
body: SafeArea(
child: Column(
children: [
_buildHeader(context),
Expanded(child: _buildChatArea(context, state)),
_buildImagePreview(), // 添加这行
_buildInputContainer(context, state),
],
),
),
```
**Step 4: 修改 _showBottomSheet 传递回调**
`_showBottomSheet` 方法修改为:
```dart
void _showBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => HomeSheet(
onImagesSelected: (images) {
setState(() {
// 限制最多3张
final remaining = 3 - _selectedImages.length;
if (remaining > 0) {
_selectedImages.addAll(images.take(remaining));
}
});
},
),
);
}
```
**Step 5: 验证编译**
Run: `cd apps && flutter analyze lib/features/home/ui/screens/home_screen.dart`
Expected: No errors
---
### Task 4: 修改 AgUiService 支持多模态消息
**Files:**
- Modify: `apps/lib/features/chat/data/services/ag_ui_service.dart:1-643`
**Step 1: 添加 base64 导入**
在文件顶部添加:
```dart
import 'dart:convert';
import 'package:image_picker/image_picker.dart';
```
**Step 2: 修改 sendMessage 方法签名**
修改 `sendMessage` 方法接受可选的图片参数:
```dart
Future<void> sendMessage(String content, {List<XFile>? images}) async {
final streamToken = ++_activeStreamToken;
final runInput = _buildRunInput(content: content, images: images);
// ... 后续代码不变
}
```
**Step 3: 修改 _buildRunInput 方法**
```dart
Map<String, dynamic> _buildRunInput({
required String content,
List<XFile>? images,
}) {
final threadId = _threadId ?? _newUuid();
final runId = _nextId(_runIdPrefix);
// 构建多模态内容块
final contentBlocks = <Map<String, dynamic>>[];
// 添加文本(如果有)
if (content.isNotEmpty) {
contentBlocks.add({'type': 'text', 'text': content});
}
// 添加图片(如果有)
if (images != null && images.isNotEmpty) {
for (final image in images) {
final bytes = await image.readAsBytes();
final base64 = base64Encode(bytes);
contentBlocks.add({
'type': 'image',
'source': {
'type': 'base64',
'media_type': 'image/jpeg',
'data': base64,
},
});
}
}
// 根据内容块数量决定消息格式
final messageContent;
if (contentBlocks.isEmpty) {
messageContent = '';
} else if (contentBlocks.length == 1 && contentBlocks[0]['type'] == 'text') {
// 纯文本使用简单格式(兼容现有逻辑)
messageContent = contentBlocks[0]['text'];
} else {
// 多模态消息使用内容块数组
messageContent = contentBlocks;
}
return {
'threadId': threadId,
'runId': runId,
'state': <String, dynamic>{},
'messages': [
{'id': _nextId('user_'), 'role': 'user', 'content': messageContent},
],
'tools': _buildTools(),
'context': <Map<String, dynamic>>[],
'forwardedProps': <String, dynamic>{},
};
}
```
**Step 4: 修改 _sendMessage 方法传递图片**
`home_screen.dart` 中修改 `_sendMessage` 方法:
```dart
Future<void> _sendMessage(BuildContext context) async {
final content = _messageController.text.trim();
if (content.isEmpty && _selectedImages.isEmpty) return;
// 保存图片引用
final images = List<XFile>.from(_selectedImages);
FocusScope.of(context).unfocus();
_messageController.clear();
// 清除图片
setState(() {
_selectedImages.clear();
});
await context.read<ChatBloc>().sendMessage(content, images: images);
// ... 后续代码不变
}
```
**Step 5: 需要修改 ChatBloc 接口**
检查 ChatBloc 的 sendMessage 方法签名,如果需要修改,添加 images 参数。
Run: `grep -n "sendMessage" apps/lib/features/chat/presentation/bloc/chat_bloc.dart`
根据结果修改 ChatBloc 和相关调用。
**Step 6: 验证编译**
Run: `cd apps && flutter analyze lib/features/chat/data/services/ag_ui_service.dart`
Expected: No errors
---
### Task 5: 测试验证
**Step 1: 运行 Flutter 分析**
Run: `cd apps && flutter analyze`
Expected: No errors
**Step 2: 运行单元测试(如果有)**
Run: `cd apps && flutter test`
Expected: Tests pass
---
### 实施提示
1. Task 2 和 Task 3 可以并行开发(HomeSheet 和 HomeScreen 独立)
2. Task 4 需要在 Task 3 完成后进行,因为需要确定 ChatBloc 接口
3. 如果遇到编译错误,检查 ImagePicker 是否正确导入
4. AG-UI 格式可以参考: https://docs.ag-ui.com (如需要)