refactor: 移除前端 Mock API,新增共享组件,优化认证流程

- 删除 mock_api_client、mock_calendar_service、mock_history_service
- 新增 fixed_length_code_input、link_button、message_composer 共享组件
- 优化登录/注册/密码重置页面使用新组件
- 简化 injection.dart 移除 mock 分支
- 更新 env.dart 配置(BACKEND_URL 替换 API_URL)
- 后端 agentscope 工具和测试更新
- 重构 AGENTS.md 文档结构
- 新增 deploy/ 目录和 protocol 文档
This commit is contained in:
qzl
2026-03-12 16:41:45 +08:00
parent d7fbb74bf8
commit 01c36eb32e
70 changed files with 5138 additions and 5829 deletions
@@ -1,126 +0,0 @@
# Agent Multimodal Smoke Runbook
**Goal:** 固化 agent 三条主链路(runs/events/history)的真实冒烟标准与输入基线。
## 1. 覆盖范围
1. `POST /api/v1/agent/runs` - 接收多模态消息(文本+图片)
2. `GET /api/v1/agent/runs/{thread_id}/events` - SSE 事件流,事件名符合 AG-UI 标准(`RUN_STARTED``STEP_STARTED``TOOL_CALL_*``RUN_FINISHED`/`RUN_ERROR`
3. `GET /api/v1/agent/runs/{thread_id}/history` - 返回 `STATE_SNAPSHOT`,含 `attachments` metadata
4. `sessions/messages` 落库完整:message_count、tokens、cost、latency、title、metadata
5. tool result 存储:大 payload 写 storagemetadata 记录 `storage_bucket`/`storage_path`
6. storage bucket 来源:必须来自环境变量 `SOCIAL_STORAGE__BUCKET`
## 2. 固定测试输入
- 图片夹具:`backend/tests/fixtures/images/calendar_text_cn.png`
- 多模态消息:
- 文本:`"识别图片中的日历内容并调用 calendar.write 创建日程"`
- 图片:`{"type":"binary","data":"<base64>","mimeType":"image/png"}`
## 3. 账号与凭据
- 冒烟账号:`dagronl@126.com` / `123456`
- 通过环境变量注入:`AGENT_LIVE_EMAIL``AGENT_LIVE_PASSWORD`
## 4. 执行命令
```bash
AGENT_LIVE_INTEGRATION=1 \
AGENT_LIVE_EMAIL="dagronl@126.com" \
AGENT_LIVE_PASSWORD="123456" \
uv run pytest tests/integration/v1/agent/test_sse_flow_live.py::test_agent_runs_events_history_live_with_image_input -q -s
```
## 5. 结果记录模板
- `thread_id` / `run_id`
- `runs` 状态码与响应
- `events` 事件序列
- `history` 是否含 `attachments[].bucket/path/mimeType`
- `sessions` 字段:message_count / total_tokens / total_cost / status / title
- `messages` 字段:role / content / metadata / tokens / cost / latency
- `tool_result` 是否写 storage
## 6. 安全注意
- 禁止将密码/token 写入 git 跟踪文件
## 7. 已修复问题清单
| 问题 | 修复内容 |
|------|----------|
| bucket 写入失败回退 | 改为直接报错,禁止回退到硬编码 bucket |
| user.resolve 工具 | 新增按 email/name 解析 user_id |
| calendar.write 邀请参数 | 增加 invite 参数透传 |
| inbox_repository 缺失 | 修复 calendar runtime 依赖 |
| runtime 模型名拼接 | 修复无效 model name |
| 多模态透传 | runtime 透传 binary.data,不过滤为 `<omitted>` |
| sessions.title 生成 | 首条用户消息持久化时自动生成 |
| assistant latency 入库 | `messages.latency_ms` 列写入 |
| intent/execution 阶段消息落库 | 新增 `text.*``tool.result` 事件 |
| DIRECT_RESPONSE 早返回 | intent 判定后直接返回,不进入 report 阶段 |
## 8. 待修复问题(用户新增)
1. **意图/执行阶段 tokens/cost 入库** - 目前仅 report 阶段入库
2. **连续会话记忆测试** - 验证 session 是否从数据库读取历史上下文
3. **工具调用测试** - calendar 读/写/删/分享 + 用户查找 + 时间感知
4. **session 失败排查** - 找出最新失败原因并修复
## 9. 本轮进展与结论(2026-03-12
### 9.1 反馈闭环状态
1. **intent/execution 阶段 tokens/cost 入库**:已解决。
2. **连续会话记忆(今天+昨天上下文)**:已解决。
3. **工具调用冒烟(读/写/删/分享 + user 查询 + 时间感知)**:部分解决。
4. **最新失败 session 根因定位与修复**:已解决。
5. **反馈同步到文档**:已完成(本节)。
### 9.2 关键修复
1. **stage telemetry 补齐**intent/execution):
- usage 缺失时补 token 估算;
- 通过 `LiteLLMService.calculate_cost` 按项目定价估算 cost
- 回填 `response_metadata.inputTokens/outputTokens/cost` 并落库。
2. **会话记忆上下文注入**
- runtime 在执行前读取同一 session 最近两天(今天+昨天)的 user/assistant 消息;
- intent prompt 增加 `[Conversation Context]`,避免只看最新用户输入。
3. **工具调用稳定性修复**
- tool 名统一为下划线(`calendar_read`/`calendar_write`/`user_resolve`),修复 OpenAI/LiteLLM tool name 正则错误;
- intent prompt 注入 intent+execution 合并工具 schema,避免误判“无可用写入工具”。
### 9.3 Live 证据
#### A) tokens/cost 入库(thread=`cb1681c2-c223-4ced-bcfd-76f7252ba2d8`
- intent: `input_tokens=1541``output_tokens=37``cost=0.000382`
- execution: `input_tokens=2161``output_tokens=376``cost=0.005450`
- report: `input_tokens=3266``output_tokens=318``cost=0.007256`
- session 聚合:`total_tokens=13518``total_cost=0.019473`
#### B) 连续会话记忆(thread=`9c456736-d5e5-48a4-b9db-55f507baf573`
- run `mem-1``请记住口令是蓝鲸42,只回复已记住。`
- run `mem-2``只回复我刚才让你记住的口令,不要解释。`
- assistant 回复:`蓝鲸42`(记忆命中)。
#### C) 工具调用 + 时间感知(thread=`cb1681c2-c223-4ced-bcfd-76f7252ba2d8`run=`run-tool-1`
- 事件序列含 execution 阶段与多次 `TOOL_CALL_RESULT`
- 工具调用结果:`calendar_write``calendar_read`(多次)
- assistant 回复包含时间感知信息(北京时间日期/星期/时刻)
### 9.4 最新失败 session 根因
- 失败样本:`d6bc4dbd-8361-4a39-bf09-12b3392e0e70`
- 根因:tool 名含点号(如 `calendar.write`)触发校验失败:
- `Invalid 'tools[0].function.name' ... expected pattern ^[a-zA-Z0-9_-]+$`
- 修复后:同类执行链路已可稳定进入 execution 并产出 `TOOL_CALL_RESULT`
### 9.5 当前未闭环项
- `user_resolve` + calendar **分享 + 删除** 组合链路的完整 live 证据还未补齐(本轮执行中断:`Tool execution aborted`)。
@@ -1,173 +0,0 @@
# Agent Tool UI Schema and Frontend Event Wiring Design
## Goal
修正 agent 工具结果的数据契约与前后端对接:
1. SSE `TOOL_CALL_RESULT` 继续携带可实时渲染的 `ui`
2. 落库时 `messages.content` 仅存关键摘要,完整工具结果(含 `ui schema`)存对象存储。
3. `messages.metadata` 仅存访问路径和索引字段,history 通过 metadata 回填完整工具卡片数据。
4. 前端正式接通 runs/events/history 三路,并统一实时与历史渲染行为。
## Constraints
- 暂缓冒烟测试,先完成工具数据修正与前后端接口对接。
- 保持现有前端 `UiSchemaRenderer` 可解析格式,不做破坏性协议改动。
- `resume` 新需求暂不扩展。
- 遵循 AG-UI 事件语义和现有 FastAPI 路由约定。
## Selected Approach
采用兼容增强方案:
- 事件流对前端保持兼容(`TOOL_CALL_RESULT``ui` + `content`)。
- 持久化与回放做结构化增强(storage + metadata 索引 + 摘要 content)。
- 前端实时与历史统一映射层,保证同类消息一致渲染。
## Design A: Unified Data Contract
### SSE Event Contract (Realtime)
`TOOL_CALL_RESULT` 事件继续包含前端当前可解析字段:
- `callId`
- `toolName`
- `args`
- `result`
- `error`
- `content` (关键结果摘要)
- `ui` (工具卡片 schema)
这保证前端实时流不需要等待 history 即可显示工具卡片。
### Persistence Contract (Database + Storage)
对 tool message 持久化采用双层:
- `messages.content`: 仅保存 `content_summary`(短文本,供低成本上下文和兜底展示)。
- 对象存储: 保存完整 payload(`ui``args``result``error`、时间戳、工具标识等)。
- `messages.metadata`: 只保存索引和访问路径:
- `tool_call_id`
- `tool_name`
- `run_id`
- `stage`
- `task_id`
- `storage_bucket`
- `storage_path`
- `summary_version`
### History Contract
history 序列化时:
1. 先通过 `metadata.storage_bucket/storage_path` 读取完整 payload。
2. 从 payload 回填 `ui`,并保留摘要 `content`
3. storage 读取失败时,回退 `messages.content`,确保历史可读。
## Design B: Frontend Wiring (runs/events/history)
### runs
- `POST /api/v1/agent/runs` 仅负责创建 run 与启动执行。
- 前端保留 `threadId/runId` 和本地流状态,不承载渲染业务。
### events
- SSE 作为唯一实时渲染来源。
- `TOOL_CALL_RESULT` 直接读取事件内 `ui` 渲染 `ToolResultItem`
- `STEP_STARTED/STEP_FINISHED` 显示三阶段状态(intent/execution/report)。
### history
- 通过 `/api/v1/agent/history``/api/v1/agent/runs/{threadId}/history` 回放。
- tool message 优先读 `ui`(由后端从 metadata+storage 回填)。
- user message 读取 `attachments` 渲染多模态内容。
### Consistency Rule
- 实时事件与历史快照统一进入同一 `ChatListItem` 映射层。
- `content` 只做兜底文本,不作为工具卡片主数据。
## Design C: Backend Implementation Details
### Modules to Change
- `backend/src/core/agentscope/events/store.py`
- 增加 tool result 的摘要生成与 storage 上传。
- `append_message` 时写入摘要 content + metadata 索引。
- `backend/src/core/agentscope/tools/tool_result_storage.py`
- 复用现有 `upload_json/read_json`,作为完整 payload 存取层。
- `backend/src/v1/agent/repository.py`
- `_to_snapshot_message` 对 tool message 优先按 metadata 读取 storage 并回填 `ui`
- `backend/src/core/agentscope/runtime/agent_route_runtime.py`
- 确保 `tool.result` 事件继续带 `ui` 和摘要 `content`
### Failure Fallback
- storage 写失败:不阻断主流程,至少保证 `messages.content` 可读,metadata 标记缺失。
- storage 读失败:history 返回摘要 `content``ui` 为空。
## Design D: content_summary Rule Engine
### Function
新增纯函数:
`build_tool_content_summary(tool_name, args, result, error) -> str`
### Rules (Priority)
1. 错误优先:有 `error` 直接输出失败摘要。
2. 工具专用模板:
- `calendar_write`: `已创建日程:{title}{start_time}`
- `calendar_read`: `查询到 {count} 条日程({date_range}`
- `calendar_delete`: `已删除日程:{title_or_id}`
- `calendar_share`: `已分享日程给 {target}`
- `user_resolve`: `已匹配用户:{name_or_id}`
3. 通用回退:优先 `result.content`,否则抽取常见键拼句。
4. 最终兜底:`{tool_name} 执行完成/执行失败`
5. 清洗:去换行与多空格,限制长度,避免大段 JSON。
### Summary Storage Policy
- `messages.content` 存摘要。
- `summary_version` 存入 metadata,支持未来摘要算法演进。
## Testing and Acceptance
### Backend
- 单元测试:
- `events/store`: tool result 摘要写入、metadata 路径写入、storage 异常回退。
- `v1/agent/repository`: history 按 metadata 回填 `ui`storage 缺失回退 content。
- 摘要函数:覆盖成功/失败/缺字段/超长文本场景。
- 集成测试:
- `/runs` + `/events`:实时 `TOOL_CALL_RESULT``ui`
- `/history`:返回 tool message 的 `ui` 来自 metadata+storage。
### Frontend
- 单元/组件测试:
- `AgUiService` 解析 `TOOL_CALL_RESULT``ui`
- `ChatBloc`:实时事件与 history 快照都能产出 `ToolResultItem`
- `UiSchemaRenderer`history 回放卡片渲染一致。
- user message 附件渲染(history)。
- 页面行为验证:
- events 到达即实时更新消息列表。
- step 三阶段状态正确切换。
- 上拉历史后工具卡片可正常显示。
## Risks and Mitigations
- 风险:storage 不可用导致 history 卡片缺失。
- 缓解:保底展示摘要 content,不阻断对话。
- 风险:事件格式变更导致前端实时解析失败。
- 缓解:维持现有 `ToolCallResultEvent` 字段,不做破坏性改名。
- 风险:摘要规则覆盖不足。
- 缓解:规则版本化 + 测试样例扩展。
## Out of Scope
- resume 扩展协议与交互策略。
- 新一轮 live 冒烟验收。
- 新 UI 风格重构,仅实现链路打通与数据契约修正。
@@ -1,283 +0,0 @@
# Agent UI Schema and Event Wiring Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 打通 agent 工具结果在实时事件与历史回放的一致渲染链路:SSE 实时带 UI,落库 content 存摘要,完整 UI schema 存 storage 并通过 metadata 回填。
**Architecture:** 后端在 `TOOL_CALL_RESULT` 持久化链路中引入“摘要 + 全量分离”策略:摘要写 `messages.content`,全量 payload 写对象存储,metadata 仅存索引路径;history 读取时按 metadata 反查 storage 回填 `ui`。前端复用现有 AG-UI 事件模型,实现 runs/events/history 三路统一映射到 `ChatListItem`,并补齐 step 事件渲染与 history 多模态渲染。
**Tech Stack:** FastAPI, SQLAlchemy, AgentScope runtime/events, Supabase Storage, Flutter (Bloc/Cubit), Dart models/tests, AG-UI events
---
### Task 1: Add Tool Summary Rule Engine (Backend)
**Files:**
- Create: `backend/src/core/agentscope/events/tool_result_summary.py`
- Test: `backend/tests/unit/core/agentscope/events/test_tool_result_summary.py`
**Step 1: Write the failing test**
```python
from core.agentscope.events.tool_result_summary import build_tool_content_summary
def test_calendar_write_summary() -> None:
text = build_tool_content_summary(
tool_name="calendar_write",
args={"title": "项目评审"},
result={"start_time": "明天 10:00"},
error=None,
)
assert text.startswith("已创建日程")
```
**Step 2: Run test to verify it fails**
Run: `uv run pytest backend/tests/unit/core/agentscope/events/test_tool_result_summary.py -q`
Expected: FAIL with import/module/function missing.
**Step 3: Write minimal implementation**
```python
def build_tool_content_summary(*, tool_name: str, args, result, error) -> str:
if error:
return f"{tool_name} 执行失败"
if tool_name == "calendar_write":
return "已创建日程"
return f"{tool_name} 执行完成"
```
**Step 4: Run test to verify it passes**
Run: `uv run pytest backend/tests/unit/core/agentscope/events/test_tool_result_summary.py -q`
Expected: PASS.
**Step 5: Extend tests for all rules and refactor**
Add cases for `calendar_read/calendar_delete/calendar_share/user_resolve/error/fallback/truncation` and implement full rule table.
**Step 6: Commit**
```bash
git add backend/src/core/agentscope/events/tool_result_summary.py backend/tests/unit/core/agentscope/events/test_tool_result_summary.py
git commit -m "feat: add deterministic tool result summary engine"
```
### Task 2: Persist Full Tool Payload to Storage and Keep Content Lightweight
**Files:**
- Modify: `backend/src/core/agentscope/events/store.py`
- Test: `backend/tests/unit/core/agentscope/events/test_store.py`
**Step 1: Write the failing tests**
Add tests asserting:
- `TOOL_CALL_RESULT` persists summary to `content`.
- metadata includes `storage_bucket/storage_path/tool_call_id`.
- uploaded payload includes full `ui/args/result/error`.
**Step 2: Run targeted tests (RED)**
Run: `uv run pytest backend/tests/unit/core/agentscope/events/test_store.py -q`
Expected: FAIL on new assertions.
**Step 3: Implement minimal storage write path**
In `_persist_tool_call_result`:
- build `full_payload` from event fields.
- call summary engine for `content`.
- upload payload via tool result storage (inject dependency if needed).
- store only path/index in metadata.
**Step 4: Run tests (GREEN)**
Run: `uv run pytest backend/tests/unit/core/agentscope/events/test_store.py -q`
Expected: PASS.
**Step 5: Add fallback test and implementation**
Add case where storage upload fails but tool message still persists with summary and no crash.
**Step 6: Commit**
```bash
git add backend/src/core/agentscope/events/store.py backend/tests/unit/core/agentscope/events/test_store.py
git commit -m "feat: store tool payload in object storage with metadata index"
```
### Task 3: Hydrate History Tool UI from Metadata Storage Path
**Files:**
- Modify: `backend/src/v1/agent/repository.py`
- Test: `backend/tests/unit/v1/agent/test_repository.py`
**Step 1: Write failing tests**
Add/adjust assertions:
- history tool payload resolves `ui` from storage payload.
- when storage missing, fallback to `messages.content` summary.
**Step 2: Run tests (RED)**
Run: `uv run pytest backend/tests/unit/v1/agent/test_repository.py -q`
Expected: FAIL on `ui` hydration and fallback assertions.
**Step 3: Implement minimal hydration logic**
In `_to_snapshot_message` for tool role:
- read storage via `metadata.storage_bucket/storage_path`.
- map hydrated payload fields to snapshot (`ui`, `content`, `toolCallId`).
- keep safe fallback when storage read fails.
**Step 4: Run tests (GREEN)**
Run: `uv run pytest backend/tests/unit/v1/agent/test_repository.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/v1/agent/repository.py backend/tests/unit/v1/agent/test_repository.py
git commit -m "fix: hydrate tool ui from metadata storage in history snapshots"
```
### Task 4: Keep SSE TOOL_CALL_RESULT Compatible with Existing Frontend Parsing
**Files:**
- Modify: `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 test**
Add assertion that emitted `TOOL_CALL_RESULT` data contains expected renderable fields (`callId/toolName/result/error` and `ui` path from result payload).
**Step 2: Run tests (RED)**
Run: `uv run pytest backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py -q`
Expected: FAIL on missing/incorrect payload fields.
**Step 3: Implement minimal payload normalization**
Normalize tool result event payload so frontend can keep current parsing without contract breaks.
**Step 4: Run tests (GREEN)**
Run: `uv run pytest backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py -q`
Expected: PASS.
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/runtime/agent_route_runtime.py backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py
git commit -m "fix: preserve frontend-compatible tool result event payload"
```
### Task 5: Wire Frontend History + Events to Unified Rendering Path
**Files:**
- Modify: `apps/lib/features/chat/data/services/ag_ui_service.dart`
- Modify: `apps/lib/features/chat/presentation/bloc/chat_bloc.dart`
- Modify: `apps/lib/features/chat/data/models/tool_result.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Test: `apps/test/features/chat/ag_ui_service_test.dart`
- Create/Modify: `apps/test/features/chat/chat_bloc_test.dart`
**Step 1: Write failing tests**
Add tests asserting:
- history tool message with `ui` becomes `ToolResultItem`.
- SSE `TOOL_CALL_RESULT` with `ui` renders same item shape.
- attachments in history user message are mapped for multimodal rendering.
**Step 2: Run tests (RED)**
Run: `cd apps && flutter test test/features/chat/ag_ui_service_test.dart`
Expected: FAIL on new mapping assertions.
**Step 3: Implement minimal mapping changes**
- In service/bloc, unify history and event mapping into same conversion path.
- Keep existing `UiSchemaRenderer` input format untouched.
- Ensure fallback to content text when `ui` missing.
**Step 4: Run tests (GREEN)**
Run: `cd apps && flutter test test/features/chat/ag_ui_service_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/chat/data/services/ag_ui_service.dart apps/lib/features/chat/presentation/bloc/chat_bloc.dart apps/lib/features/chat/data/models/tool_result.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/chat/ag_ui_service_test.dart apps/test/features/chat/chat_bloc_test.dart
git commit -m "feat: unify realtime and history tool card rendering"
```
### Task 6: Add Step Event Rendering for Intent/Execution/Report
**Files:**
- Modify: `apps/lib/features/chat/presentation/bloc/chat_bloc.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Test: `apps/test/features/chat/chat_bloc_test.dart`
**Step 1: Write failing test**
Add test verifying `STEP_STARTED/STEP_FINISHED` transitions produce visible stage state.
**Step 2: Run tests (RED)**
Run: `cd apps && flutter test test/features/chat/chat_bloc_test.dart`
Expected: FAIL on missing stage state.
**Step 3: Implement minimal state and UI**
- Track current stage enum in `ChatState`.
- Render compact stage progress row in chat screen.
**Step 4: Run tests (GREEN)**
Run: `cd apps && flutter test test/features/chat/chat_bloc_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/chat/presentation/bloc/chat_bloc.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/chat/chat_bloc_test.dart
git commit -m "feat: render agent step progress from AG-UI events"
```
### Task 7: Verification Gate (Backend + Frontend)
**Files:**
- Modify (if needed): `docs/plans/2026-03-11-agent-multimodal-smoke-runbook.md`
**Step 1: Run backend targeted tests**
Run: `uv run pytest backend/tests/unit/core/agentscope/events/test_tool_result_summary.py backend/tests/unit/core/agentscope/events/test_store.py backend/tests/unit/v1/agent/test_repository.py backend/tests/unit/core/agentscope/runtime/test_agent_route_runtime.py -q`
Expected: PASS.
**Step 2: Run frontend targeted tests**
Run: `cd apps && flutter test test/features/chat/ag_ui_service_test.dart test/features/chat/chat_bloc_test.dart`
Expected: PASS.
**Step 3: Run backend quality checks**
Run: `uv run ruff check backend/src backend/tests`
Expected: PASS.
**Step 4: Run backend type checks**
Run: `uv run basedpyright`
Expected: 0 errors.
**Step 5: Update runbook evidence**
Record changed contract, test evidence, and known follow-ups.
**Step 6: Commit**
```bash
git add docs/plans/2026-03-11-agent-multimodal-smoke-runbook.md
git commit -m "docs: record tool ui schema storage and rendering verification"
```
@@ -0,0 +1,122 @@
# Home 输入组件重做设计(HomeComposer Redesign
## 1. 目标与范围
### 1.1 目标
- 解决当前输入组件“质感弱、结构割裂、录音时布局漂移”的问题。
- 统一 `+` 按钮、输入区、右侧动作图标到一个圆角矩形主容器内。
- 保留并复用现有录音、转写、自动发送、停止生成、Toast 错误处理逻辑。
### 1.2 非目标
- 不改动聊天流、消息发送后端协议、语音识别接口。
- 不改动 `+` 按钮业务行为。
- 不新增独立的页面级浮层录音面板。
## 2. 问题诊断(现状)
- 当前输入区由多个分离容器拼接,视觉上像“纸片贴上去”。
- 输入框本体和右侧图标视觉上未合为一个整体容器。
- “按住说话”提示与录音动画在主布局外追加,录音时造成结构上下跳动。
## 3. 方案对比
### 方案 A:单容器双模式(推荐)
- 单一胶囊主容器承载三段:左操作、中间主内容、右操作。
- 中间区域在文本模式与按住说话模式之间替换(`AnimatedSwitcher`)。
- 录音动画仅在中间区域内部切换显示,不改变主容器高度。
优点:结构稳定、状态清晰、维护成本低。
缺点:视觉表达自由度略低于 Overlay 方案。
### 方案 B:双容器交叉切换
- 文本容器和语音容器完整分离,做交叉淡入。
优点:切换动效可做得更明显。
缺点:状态同步复杂,容易再次出现错位与边界问题。
### 方案 COverlay 浮层
- 保持输入容器不变,录音时叠加浮层。
优点:动画自由度高。
缺点:与“整块替换”诉求不一致,事件命中与无障碍处理更复杂。
结论:采用方案 A。
## 4. 信息架构与组件边界
## 4.1 新组件
- 新建 `HomeComposer`(从 `home_screen.dart` 抽离输入区渲染职责)。
- `HomeScreen` 继续持有业务状态与行为方法,`HomeComposer` 负责展示与手势分发。
## 4.2 主容器结构
- 一个圆角矩形主容器(轻拟物胶囊风格)。
- 左侧:`+` 按钮(行为不变)。
- 中间:
- 文本模式:无边框输入区(文字垂直居中)。
- 语音模式:按住说话按钮区。
- 录音中/识别中:在语音模式内部替换状态内容。
- 右侧:动作图标(声波/键盘/发送/停止)。
## 5. 状态机设计
## 5.1 状态定义
- 模式层:`text` / `holdToSpeak`
- 过程层:`idle` / `recording` / `transcribing`
## 5.2 核心约束
- 主容器高度固定,状态变化不得引发外层布局高度变化。
- `recording` 时禁止模式切换,避免状态错位。
## 5.3 右侧图标决策
- Agent 等待中:停止图标。
- 非等待:
- 有文本:发送图标。
- 无文本且 `text``LucideIcons.activity`
- 无文本且 `holdToSpeak``LucideIcons.keyboard`
## 6. 交互与动画
## 6.1 长按语音流程
1. `onLongPressStart`:触发 `HapticFeedback.lightImpact()`,开始录音。
2. `onLongPressMoveUpdate`:上滑超过阈值,取消录音。
3. `onLongPressEnd`:未取消则停止录音,转写并自动发送。
## 6.2 提示文案显示策略
- “松开发送,上滑取消”仅在 `recording` 显示。
- 空闲按住说话模式不显示该提示。
## 6.3 动画策略
- 模式切换:短时 `AnimatedSwitcher`(淡入/轻位移)。
- 录音波形:仅在 `recording` 驱动;停止后立刻回收。
- 动画渲染在中间区域内部,不新增外部占位。
## 7. 视觉规范(Design Tokens
- 严格使用 `apps/lib/core/theme/design_tokens.dart` 中的 `AppColors``AppSpacing``AppRadius`
- 禁止硬编码颜色、间距、圆角、尺寸、阴影。
- 主容器采用白色底 + 细边 + 柔和阴影,形成轻拟物层次。
- 输入区内部无额外边框,确保文字垂直居中和图标视觉对齐。
## 8. 验收标准
- 图标与输入区在同一圆角矩形中,不再分离。
- 录音全流程不出现输入组件上移、下坠或高度抖动。
- 提示文案仅在实际录音中显示。
- 文本/语音模式切换平滑;`+` 与发送逻辑行为保持一致。
## 9. 风险与缓解
- 风险:重构过程中影响现有发送/停止生成分支。
- 缓解:优先复用原有行为方法,仅调整 UI 结构与状态映射。
- 风险:手势与模式切换并发导致状态错乱。
- 缓解:录音期间加切换锁,结束后释放。
## 10. 验证计划
- 手工验证:
- 文本发送、停止生成、`+` 弹层、语音长按录音、上滑取消、自动发送。
- 文本模式与语音模式往返切换稳定性。
- Widget 测试(建议新增):
- 右侧图标状态映射测试。
- 录音中提示文案显示条件测试。
- 模式切换时主容器高度恒定测试。
@@ -0,0 +1,275 @@
# Home Composer Redesign Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 重做 Home 输入组件,统一为单胶囊容器并稳定语音长按交互,消除布局漂移,同时保持 `+` 与发送等既有业务逻辑不变。
**Architecture:** 采用“单容器双模式”方案:`HomeScreen` 继续持有业务状态与动作,新增 `HomeComposer` 专注 UI 与手势分发;中间区域用受控状态在文本/按住说话/录音中/识别中之间切换,外层高度固定。录音提示与声波动画都内聚在主容器内部渲染,避免额外布局占位。
**Tech Stack:** Flutter, flutter_bloc, lucide_icons, design tokens (`AppColors`/`AppSpacing`/`AppRadius`), widget test
---
### Task 1: 建立 HomeComposer 组件骨架与参数契约
**Files:**
- Create: `apps/lib/features/home/ui/widgets/home_composer.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: 写失败测试(渲染结构)**
```dart
testWidgets('renders one unified rounded composer container', (tester) async {
// pump HomeComposer with minimum required callbacks/states
// expect: one root container, plus button, center content slot, right action slot
});
```
**Step 2: 运行测试确认失败**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"`
Expected: FAIL`HomeComposer` 未定义或结构不匹配)
**Step 3: 写最小实现**
```dart
class HomeComposer extends StatelessWidget {
const HomeComposer({
super.key,
required this.isHoldToSpeakMode,
required this.isRecording,
required this.isTranscribing,
required this.hasMessage,
required this.isWaitingAgent,
required this.onTapPlus,
required this.onTapRightAction,
required this.centerChild,
});
// unified capsule container with left/center/right slots
}
```
**Step 4: 再跑测试确认通过**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"`
Expected: PASS
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "refactor: extract unified home composer container"
```
### Task 2: 完成右侧图标状态映射(activity/keyboard/send/stop
**Files:**
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: 写失败测试(图标状态机)**
```dart
testWidgets('right action icon follows state priority', (tester) async {
// waiting > hasMessage > holdToSpeakMode > textMode
// expect LucideIcons.square/send/keyboard/activity respectively
});
```
**Step 2: 运行测试确认失败**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"`
Expected: FAIL(图标选择逻辑尚未完整实现)
**Step 3: 最小实现图标决策**
```dart
IconData resolveRightIcon(...) {
if (isWaitingAgent) return LucideIcons.square;
if (hasMessage) return LucideIcons.send;
return isHoldToSpeakMode ? LucideIcons.keyboard : LucideIcons.activity;
}
```
**Step 4: 再跑测试确认通过**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"`
Expected: PASS
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "refactor: stabilize composer right action icon mapping"
```
### Task 3: 实现中间区域双模式替换并固定高度
**Files:**
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: 写失败测试(模式切换不改变高度)**
```dart
testWidgets('composer height remains stable across mode switches', (tester) async {
// measure size in text mode and hold-to-speak mode
// expect equal heights
});
```
**Step 2: 运行测试确认失败**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"`
Expected: FAIL(当前结构切换时高度波动)
**Step 3: 最小实现(AnimatedSwitcher + fixed constraints**
```dart
SizedBox(
height: composerHeight,
child: AnimatedSwitcher(
duration: switchDuration,
child: isHoldToSpeakMode ? holdToSpeakChild : textInputChild,
),
)
```
**Step 4: 再跑测试确认通过**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"`
Expected: PASS
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "refactor: keep composer layout stable during mode switch"
```
### Task 4: 实现长按录音交互(开始/上滑取消/松开发送)
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: 写失败测试(录音提示只在 recording)**
```dart
testWidgets('recording hint appears only while recording', (tester) async {
// idle hold-to-speak: no hint
// recording: show "松开发送,上滑取消"
});
```
**Step 2: 运行测试确认失败**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"`
Expected: FAIL
**Step 3: 最小实现录音流程映射**
```dart
onLongPressStart => HapticFeedback.lightImpact() + onHoldStart();
onLongPressMoveUpdate => if (dy < threshold) onHoldCancel();
onLongPressEnd => onHoldEnd(autoSend: true);
```
**Step 4: 再跑测试确认通过**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"`
Expected: PASS
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "feat: rework hold-to-speak interaction with stable recording state"
```
### Task 5: 视觉重构为轻拟物胶囊(仅 tokens)
**Files:**
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
- Modify: `apps/lib/core/theme/design_tokens.dart`(仅当现有 token 不足时新增)
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: 写失败测试(主容器统一性)**
```dart
testWidgets('plus, center and right action are inside same capsule', (tester) async {
// find one capsule host and verify children are descendants
});
```
**Step 2: 运行测试确认失败**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"`
Expected: FAIL
**Step 3: 最小视觉实现(不硬编码)**
```dart
// use AppColors/AppSpacing/AppRadius and existing shadow tokens
// no hardcoded color/spacing/radius/size
```
**Step 4: 再跑测试确认通过**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"`
Expected: PASS
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/core/theme/design_tokens.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "refactor: redesign home composer with neumorphic capsule style"
```
### Task 6: 集成回归与文档同步
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Modify: `docs/runtime/runtime-route.md`(若交互说明有变化)
**Step 1: 运行目标测试文件**
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart`
Expected: PASS
**Step 2: 运行 Home 相关回归测试(若新增)**
Run: `flutter test test/features/home`
Expected: PASS(若目录存在)
**Step 3: 运行应用侧基础回归**
Run: `flutter test`
Expected: PASS 或仅存在与本改动无关的已知失败
**Step 4: 记录验证结论**
```text
- 输入组件统一容器:通过
- 模式切换稳定:通过
- 录音提示条件:通过
- + 按钮/发送逻辑:通过
```
**Step 5: 小步提交(仅用户明确要求时)**
```bash
git add apps/lib/features/home/ui/screens/home_screen.dart apps/lib/features/home/ui/widgets/home_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart docs/runtime/runtime-route.md
git commit -m "test: add regression coverage for home composer redesign"
```
## 实施注意事项
- 保持 `HomeScreen` 作为业务状态单一来源,避免在 `HomeComposer` 内部复制业务状态。
- 录音中 (`recording`) 禁止触发模式切换,防止并发手势引发错位。
- 严格遵守 `apps/AGENTS.md`:不硬编码视觉值,必须使用 design tokens。
- 用户反馈统一使用 `Toast.show(...)`,不得引入 `SnackBar`