feat: 增强日历功能并集成 AgentScope 代理服务
This commit is contained in:
@@ -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.0x(17px ~ 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;日历创建弹窗与详情页升级为可编辑/展示完整 metadata(location、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: 后端响应完整 metadata(TDD)
|
||||
|
||||
**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 (如需要)
|
||||
Reference in New Issue
Block a user