feat: 增强日历功能并集成 AgentScope 代理服务

This commit is contained in:
qzl
2026-03-11 17:16:11 +08:00
parent e20e7d2a02
commit 85b314cf64
53 changed files with 3642 additions and 297 deletions
@@ -1,69 +0,0 @@
# Auth Token Compatibility + Refresh Singleflight Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 兼容云 Supabase 实际 access token claims(缺失 `iss` 仍可通过),并修复前端 401 导致 refresh 风暴问题,消除日志中的批量 401/429 警告。
**Architecture:** 后端保持 HS256 签名校验、`exp/sub` 必检,将 `iss` 从“强制存在”改为“存在时校验”;前端在拦截器中加入 refresh 单飞与防重入,避免并发 401 触发多次 refresh 或 refresh 自递归。同步清理无效分支与冗余状态。
**Tech Stack:** FastAPI, PyJWT, Flutter, Dio, flutter_test
---
### Task 1: 后端 JWT claim 兼容化(无 `iss` 可通过)
**Files:**
- Modify: `backend/src/core/auth/jwt_verifier.py`
- Test: `backend/tests/unit/core/auth/test_jwt_verifier.py`
**Step 1: Write failing test**
- 新增用例:token 不含 `iss`、但 `sub/exp` 与 HS256 签名合法时应验证成功。
**Step 2: Run test to verify it fails**
- Run: `cd backend && uv run pytest tests/unit/core/auth/test_jwt_verifier.py -q`
**Step 3: Write minimal implementation**
- `jwt.decode``require` 去掉 `iss`,仅保留 `sub/exp`
- 若 payload 中存在 `iss` 且配置了 issuer,则手动比对 issuer;不一致时报错。
**Step 4: Run test to verify it passes**
- Run: `cd backend && uv run pytest tests/unit/core/auth/test_jwt_verifier.py -q`
### Task 2: 前端 refresh 单飞 + 防递归
**Files:**
- Modify: `apps/lib/core/api/api_interceptor.dart`
- Test: `apps/test/core/api/api_interceptor_test.dart`
**Step 1: Write failing tests**
- 并发 401 时只调用一次 `onTokenRefresh`
- `/api/v1/auth/sessions/refresh` 自身 401 不触发 refresh 重试。
**Step 2: Run tests to verify failures**
- Run: `cd apps && flutter test test/core/api/api_interceptor_test.dart`
**Step 3: Write minimal implementation**
- 增加 `_refreshFuture` 单飞字段。
- 非 refresh 请求命中 401 时 await 同一个 refresh future。
- 对 refresh/logout 认证端点和已重试请求加短路,避免无限重入。
**Step 4: Run tests to verify pass**
- Run: `cd apps && flutter test test/core/api/api_interceptor_test.dart`
### Task 3: 清理无效/旧分支并做回归验证
**Files:**
- Modify: `apps/lib/core/api/api_interceptor.dart`(移除无效重试分支)
- Modify: `backend/src/core/auth/jwt_verifier.py`(删除不再使用的路径)
**Step 1: Refactor cleanup**
- 删除不再可达的分支与重复逻辑,保持行为不变。
**Step 2: Full targeted verification**
- Run: `cd backend && uv run ruff check src tests`
- Run: `cd backend && uv run basedpyright`
- Run: `cd backend && uv run pytest tests/unit/core/auth/test_jwt_verifier.py tests/unit/v1/users -q`
- Run: `cd apps && flutter test test/core/api/api_interceptor_test.dart test/features/auth`
**Step 3: Runtime spot-check**
- Run: 登录拿 token 后请求 `/api/v1/agent/history`,确认不再因缺失 `iss` 返回 401。
@@ -0,0 +1,141 @@
# 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.
@@ -0,0 +1,308 @@
# 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"
```
@@ -0,0 +1,47 @@
# 日视图改进设计
**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`
@@ -0,0 +1,223 @@
# 日视图改进实现计划
> **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.**
@@ -1,78 +0,0 @@
# Calendar Metadata And API Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 统一后端 `schedule-items` 与 Agent 日历卡片的 metadata v1 约束,并让前端日历模块完成真实 API 接入与 metadata 全字段渲染。
**Architecture:** 后端以 `v1.schedule_items.schemas` 作为 metadata 单一真源,路由响应与 Agent 工具 payload 统一复用该结构。前端新增 Calendar API 数据层,使用 DTO 与领域模型映射驱动 UI;日历创建弹窗与详情页升级为可编辑/展示完整 metadatalocation、notes、attachments、version)。
**Tech Stack:** FastAPI, Pydantic v2, SQLAlchemy, Flutter, Dio, GetIt, widget/unit tests
---
### Task 1: 后端 metadata v1 校验(TDD
**Files:**
- Modify: `backend/tests/unit/v1/schedule_items/test_schemas.py`
- Modify: `backend/src/v1/schedule_items/schemas.py`
**Steps:**
1. 增加失败测试:`metadata.color``#RRGGBB` 拒绝、`metadata.version` 非 1 拒绝、metadata/attachment 非法额外字段拒绝。
2. 运行 `uv run pytest backend/tests/unit/v1/schedule_items/test_schemas.py -q`,确认 RED。
3. 在 schema 中补齐约束:`extra="forbid"``Field(pattern=...)``Literal[1]`
4. 再跑同一测试文件确认 GREEN。
### Task 2: 后端响应完整 metadataTDD
**Files:**
- Modify: `backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py`
- Modify: `backend/tests/unit/core/agent/test_list_calendar_events_tool.py`
- Modify: `backend/src/core/agent/infrastructure/crewai/tools/create_calendar_event_tool.py`
**Steps:**
1. 增加失败测试:`calendar_card.v1``calendar_event_list.v1` 的 data 含完整 `metadata`,并兼容已有扁平字段。
2. 运行 `uv run pytest backend/tests/unit/core/agent/test_mutate_calendar_event_tool.py backend/tests/unit/core/agent/test_list_calendar_events_tool.py -q`,确认 RED。
3. 调整 `_event_payload` 输出,补齐 `metadata`color/location/notes/attachments/version)。
4. 再跑测试确认 GREEN。
### Task 3: 前端日历真实 API 数据层(TDD)
**Files:**
- Add: `apps/lib/features/calendar/data/calendar_api.dart`
- Modify: `apps/lib/features/calendar/data/models/schedule_item_model.dart`
- Modify: `apps/lib/features/calendar/data/services/mock_calendar_service.dart`
- Modify: `apps/lib/core/di/injection.dart`
- Add: `apps/test/features/calendar/data/calendar_api_test.dart`
**Steps:**
1. 新增失败测试覆盖 GET/POST/PATCH/DELETE 与 metadata 映射(含 attachments/version)。
2. 运行 `cd apps && flutter test test/features/calendar/data/calendar_api_test.dart`,确认 RED。
3. 实现 API 与模型序列化/反序列化,`CalendarService` 在真实环境走 API,在 mock 环境走现有内存服务。
4. 再跑测试确认 GREEN。
### Task 4: 前端完整 metadata 渲染与创建/查看增强(TDD)
**Files:**
- Modify: `apps/lib/features/calendar/ui/widgets/create_event_sheet.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_event_detail_screen.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_dayweek_screen.dart`
- Modify: `apps/lib/features/calendar/ui/screens/calendar_month_screen.dart`
- Modify: `apps/lib/features/chat/data/models/tool_result.dart`
- Modify: `apps/lib/features/chat/ui/widgets/ui_schema_renderer.dart`
- Add: `apps/test/features/calendar/ui/calendar_event_detail_screen_test.dart`
**Steps:**
1. 增加失败测试:详情页显示 attachments/version;创建弹窗支持 attachments 输入并提交。
2. 运行对应 flutter test,确认 RED。
3. 改造 UI 与数据写回逻辑,保证 metadata 全字段渲染。
4. 再跑测试确认 GREEN。
### Task 5: 文档与验证
**Files:**
- Modify: `docs/runtime/runtime-route.md`
**Steps:**
1. 更新 metadata v1 校验规则与返回示例。
2. 运行后端+前端相关测试集合,记录结果。
3. 执行 L2 门禁:`refactor-cleaner``code-reviewer``security-reviewer` 并修复问题。
@@ -0,0 +1,63 @@
# 日历提醒字段与详情页对齐设计
**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 与详情页渲染测试
- 手动:创建提醒 -> 等待系统通知与震动 -> 更新/删除后确认调度变更
@@ -0,0 +1,170 @@
# 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.
@@ -0,0 +1,136 @@
# 首页图片选择功能设计
## 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 消息格式符合规范
@@ -0,0 +1,463 @@
# 首页图片选择功能实现计划
> **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 (如需要)