refactor: 重构 Agent 模块为 AgentScope,删除旧版 CrewAI/LiteLLM 实现
This commit is contained in:
@@ -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.0x(17px ~ 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 (如需要)
|
||||
Reference in New Issue
Block a user