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:
@@ -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 写 storage,metadata 记录 `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:双容器交叉切换
|
||||
- 文本容器和语音容器完整分离,做交叉淡入。
|
||||
|
||||
优点:切换动效可做得更明显。
|
||||
缺点:状态同步复杂,容易再次出现错位与边界问题。
|
||||
|
||||
### 方案 C:Overlay 浮层
|
||||
- 保持输入容器不变,录音时叠加浮层。
|
||||
|
||||
优点:动画自由度高。
|
||||
缺点:与“整块替换”诉求不一致,事件命中与无障碍处理更复杂。
|
||||
|
||||
结论:采用方案 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`。
|
||||
@@ -0,0 +1,13 @@
|
||||
# Auth Routes Protocol Notes
|
||||
|
||||
## POST `/api/v1/auth/verifications`
|
||||
|
||||
- `invite_code` is optional.
|
||||
- Recommended format is fixed `4` chars and pattern `^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$`.
|
||||
- Backend normalizes invite codes to uppercase and validates in service logic.
|
||||
- Invalid invite code values are ignored (treated as empty), and signup verification email flow still continues.
|
||||
|
||||
## Verification Token Input Convention
|
||||
|
||||
- Verification token for signup/recovery uses fixed `6` digits.
|
||||
- Client UI should use fixed-length segmented input to reduce mistyped values.
|
||||
@@ -0,0 +1,466 @@
|
||||
# UI Schema Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
A generic UI schema for rendering tool/agent execution results. Designed for AI Agent / Tool ecosystem with extensibility.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Frozen (no new node types)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UiSchemaDocument (root) │
|
||||
│ - version / schemaType / status / docId │
|
||||
│ - meta (protocol-level metadata) │
|
||||
│ - nodes (array of UiNode) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Field Layers: │
|
||||
│ 1. Public fields (all renderers must handle) │
|
||||
│ id / type / title / description / icon / status / │
|
||||
│ timestamp / actions │
|
||||
│ 2. meta (protocol-level, not rendered) │
|
||||
│ requestId / toolId / traceId / userId │
|
||||
│ 3. extensions (tool私有扩展, renderer透传) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Types
|
||||
|
||||
### SchemaType
|
||||
|
||||
```typescript
|
||||
type SchemaType = 'tool_result' | 'agent_response' | 'notification';
|
||||
```
|
||||
|
||||
### UiStatus
|
||||
|
||||
```typescript
|
||||
type UiStatus = 'info' | 'success' | 'warning' | 'error' | 'pending';
|
||||
```
|
||||
|
||||
### IconSource
|
||||
|
||||
```typescript
|
||||
type IconSource = 'icon' | 'emoji' | 'url';
|
||||
```
|
||||
|
||||
### ActionType
|
||||
|
||||
```typescript
|
||||
type ActionType = 'navigation' | 'url' | 'event' | 'tool' | 'copy' | 'payload';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Structure
|
||||
|
||||
```typescript
|
||||
interface UiSchemaDocument {
|
||||
// Protocol identifier
|
||||
version: string; // "1.0"
|
||||
schemaType: SchemaType; // tool_result | agent_response | notification
|
||||
|
||||
// Document metadata
|
||||
docId?: string; // For local refresh / diff / analytics
|
||||
timestamp?: string; // ISO 8601
|
||||
locale?: string; // "zh-CN"
|
||||
|
||||
// Unified status
|
||||
status: UiStatus;
|
||||
|
||||
// Render control
|
||||
renderer?: {
|
||||
renderer?: string; // Dedicated renderer name
|
||||
theme?: 'default' | 'dark' | 'light';
|
||||
};
|
||||
|
||||
// Protocol-level metadata (not rendered)
|
||||
meta?: {
|
||||
requestId?: string;
|
||||
toolId?: string;
|
||||
traceId?: string;
|
||||
userId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// Root nodes
|
||||
nodes: UiNode[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node Types (v1 Whitelist)
|
||||
|
||||
```
|
||||
✅ Supported in v1:
|
||||
- card 卡片
|
||||
- list 列表
|
||||
- table 表格
|
||||
- text 文本/Markdown
|
||||
- kv 键值对
|
||||
- operation 操作结果
|
||||
- error 错误提示
|
||||
- container 容器
|
||||
|
||||
❌ Not supported in v1 (reserved for v2):
|
||||
- chart / metric / image / video / tabs / accordion / form
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Node Fields
|
||||
|
||||
All nodes share these fields:
|
||||
|
||||
```typescript
|
||||
interface UiBaseNode {
|
||||
id?: string; // For local refresh / action targeting
|
||||
type: string; // Node type identifier
|
||||
}
|
||||
|
||||
interface UiTitledNode extends UiBaseNode {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: UiIcon;
|
||||
status?: UiStatus;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
interface UiActionableNode extends UiTitledNode {
|
||||
actions?: UiAction[];
|
||||
}
|
||||
|
||||
interface UiExtendableNode extends UiActionableNode {
|
||||
extensions?: Record<string, any>; // Tool私有扩展, 通用renderer透传
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node Definitions
|
||||
|
||||
### 1. Card Node
|
||||
|
||||
```typescript
|
||||
interface UiCardNode extends UiExtendableNode {
|
||||
type: 'card';
|
||||
children?: UiNode[]; // Nested nodes
|
||||
footer?: UiTextNode;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. List Node
|
||||
|
||||
```typescript
|
||||
interface UiListNode extends UiExtendableNode {
|
||||
type: 'list';
|
||||
items: ListItem[];
|
||||
pagination?: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
};
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
interface ListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
icon?: UiIcon;
|
||||
badge?: { label: string; variant?: 'default' | 'success' | 'warning' | 'error' | 'info' };
|
||||
metadata?: Record<string, any>;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Table Node
|
||||
|
||||
```typescript
|
||||
interface UiTableNode extends UiExtendableNode {
|
||||
type: 'table';
|
||||
columns: TableColumn[];
|
||||
rows: TableRow[];
|
||||
pagination?: Pagination;
|
||||
}
|
||||
|
||||
interface TableColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
width?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
id: string;
|
||||
cells: Record<string, any>;
|
||||
metadata?: Record<string, any>;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Text Node
|
||||
|
||||
```typescript
|
||||
interface UiTextNode extends UiBaseNode {
|
||||
type: 'text';
|
||||
content: string;
|
||||
format?: 'plain' | 'markdown';
|
||||
icon?: UiIcon;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Key-Value Node
|
||||
|
||||
```typescript
|
||||
interface UiKvNode extends UiExtendableNode {
|
||||
type: 'kv';
|
||||
pairs: KeyValuePair[];
|
||||
layout?: 'vertical' | 'horizontal' | 'grid';
|
||||
}
|
||||
|
||||
interface KeyValuePair {
|
||||
key: string;
|
||||
label?: string;
|
||||
value: string | number | boolean;
|
||||
copyable?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Operation Node
|
||||
|
||||
```typescript
|
||||
interface UiOperationNode extends UiExtendableNode {
|
||||
type: 'operation';
|
||||
operation: 'create' | 'update' | 'delete' | 'execute';
|
||||
result: 'success' | 'failure' | 'partial';
|
||||
message?: string;
|
||||
affectedCount?: number;
|
||||
details?: UiNode;
|
||||
rollback?: UiAction;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Error Node
|
||||
|
||||
```typescript
|
||||
interface UiErrorNode extends UiBaseNode {
|
||||
type: 'error';
|
||||
title?: string;
|
||||
icon?: UiIcon;
|
||||
errorCode: string;
|
||||
message: string;
|
||||
details?: string;
|
||||
stack?: string;
|
||||
retryable: boolean;
|
||||
suggestions?: string[];
|
||||
retry?: UiAction;
|
||||
support?: UiAction;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Container Node
|
||||
|
||||
```typescript
|
||||
interface UiContainerNode extends UiBaseNode {
|
||||
type: 'container';
|
||||
direction: 'vertical' | 'horizontal';
|
||||
gap?: number;
|
||||
children: UiNode[];
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Action Structure
|
||||
|
||||
```typescript
|
||||
interface UiAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: UiIcon;
|
||||
style?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
disabled?: boolean;
|
||||
action: ActionSpec;
|
||||
confirm?: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ActionSpec =
|
||||
| { type: 'navigation'; path: string; params?: Record<string, any> }
|
||||
| { type: 'url'; url: string; target?: '_self' | '_blank' }
|
||||
| { type: 'event'; event: string; payload?: Record<string, any> }
|
||||
| { type: 'tool'; toolId: string; params?: Record<string, any> }
|
||||
| { type: 'copy'; content: string; successMessage?: string }
|
||||
| { type: 'payload'; payload: Record<string, any>; submitTo?: string };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Structure
|
||||
|
||||
```typescript
|
||||
interface UiIcon {
|
||||
source: IconSource; // 'icon' | 'emoji' | 'url'
|
||||
value: string; // icon name / emoji / URL
|
||||
color?: string; // "#FF0000"
|
||||
size?: number; // pixels
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Rules
|
||||
|
||||
### operation vs error
|
||||
|
||||
| Scenario | Node Type |
|
||||
|----------|-----------|
|
||||
| Tool execution failed, system exception | `UiErrorNode` |
|
||||
| Business operation result (CRUD) | `UiOperationNode` |
|
||||
| Network error / permission denied | `UiErrorNode` |
|
||||
|
||||
### extensions Usage Constraints
|
||||
|
||||
1. ❌ NO: Any rendering-related style / text / layout
|
||||
2. ❌ NO: Any fields other renderers need to read
|
||||
3. ✅ YES: Business identifiers (eventId, orderId)
|
||||
4. ✅ YES: Dedicated renderer private config
|
||||
5. ✅ YES: Analytics / logging context data
|
||||
6. ✅ YES: Data that generic renderer doesn't care about
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Example 1: Success Card
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"docId": "doc_evt_001",
|
||||
"timestamp": "2026-03-12T10:30:00Z",
|
||||
"status": "success",
|
||||
"renderer": { "renderer": "calendar" },
|
||||
"meta": { "requestId": "req_abc", "toolId": "calendar.create_event" },
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_card_1",
|
||||
"type": "card",
|
||||
"title": "日程已创建",
|
||||
"description": "会议日程创建成功",
|
||||
"icon": { "source": "icon", "value": "event_available" },
|
||||
"extensions": { "eventId": "evt_abc123", "color": "#3B82F6" },
|
||||
"children": [
|
||||
{
|
||||
"type": "kv",
|
||||
"pairs": [
|
||||
{ "key": "title", "label": "主题", "value": "Q1 规划会议" },
|
||||
{ "key": "time", "label": "时间", "value": "2026-03-15 14:00" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "action_view",
|
||||
"label": "查看详情",
|
||||
"style": "primary",
|
||||
"action": { "type": "navigation", "path": "/calendar/evt_abc123" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: List Result
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"status": "success",
|
||||
"meta": { "toolId": "search.documents" },
|
||||
"nodes": [
|
||||
{ "type": "text", "content": "找到 **3** 个相关文档" },
|
||||
{
|
||||
"id": "node_list_1",
|
||||
"type": "list",
|
||||
"items": [
|
||||
{
|
||||
"id": "item_1",
|
||||
"title": "API 设计规范",
|
||||
"subtitle": "v2.1",
|
||||
"icon": { "source": "emoji", "value": "📄" },
|
||||
"actions": [
|
||||
{ "id": "a1", "label": "查看", "action": { "type": "url", "url": "/docs/api" } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": { "page": 1, "pageSize": 10, "total": 3, "hasMore": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Error Result
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"status": "error",
|
||||
"meta": { "toolId": "user.delete" },
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_error_1",
|
||||
"type": "error",
|
||||
"title": "删除用户失败",
|
||||
"icon": { "source": "icon", "value": "error_outline" },
|
||||
"errorCode": "PERMISSION_DENIED",
|
||||
"message": "您没有权限执行此操作",
|
||||
"details": "需要管理员权限",
|
||||
"retryable": true,
|
||||
"suggestions": ["联系管理员", "联系技术支持"],
|
||||
"retry": {
|
||||
"id": "action_retry",
|
||||
"label": "重试",
|
||||
"style": "primary",
|
||||
"action": { "type": "tool", "toolId": "user.delete", "params": { "userId": "u1" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Evolution (v2)
|
||||
|
||||
Reserved for future:
|
||||
|
||||
- Nested containers: `tabs`, `accordion`, `carousel`
|
||||
- Data visualization: `chart`, `metric`, `progress`
|
||||
- Rich media: `image`, `video`, `audio`, `file`
|
||||
- Internationalization: `i18nKey` field
|
||||
- Version migration: `deprecated` flag
|
||||
- Offline support: `offline` flag
|
||||
@@ -1,545 +0,0 @@
|
||||
# Database Schema
|
||||
|
||||
**Status:** Active
|
||||
**Last Updated:** 2026-03-06
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
### 数据库层职责
|
||||
|
||||
- **Supabase**: 认证(JWT 签发与验证)
|
||||
- **Backend**: 业务授权(Service 层)、数据访问(Repository 层)
|
||||
- **ORM**: SQLAlchemy(async + asyncpg,使用 service_role 连接)
|
||||
|
||||
### 核心模块
|
||||
|
||||
| 模块 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| Base Classes | `backend/src/core/db/` | ORM 基类、Session 管理、Repository 基类 |
|
||||
| Models | `backend/src/models/` | 数据模型定义 |
|
||||
| Migrations | `backend/alembic/versions/` | 数据库迁移脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 设计约定
|
||||
|
||||
### 枚举存储
|
||||
|
||||
**所有枚举使用字符串存储,不使用整数值:**
|
||||
|
||||
- Database: `VARCHAR(20)` + `CHECK` 约束
|
||||
- Code: Python `Enum` 继承 `str`
|
||||
|
||||
```python
|
||||
class AgentType(str, Enum):
|
||||
INTENT_RECOGNITION = "INTENT_RECOGNITION"
|
||||
TASK_EXECUTION = "TASK_EXECUTION"
|
||||
RESULT_REPORTING = "RESULT_REPORTING"
|
||||
```
|
||||
|
||||
### 软删除
|
||||
|
||||
**软删除标记数据为不可见,不级联删除:**
|
||||
|
||||
- 使用 `deleted_at: datetime | None` 列(通过 `SoftDeleteMixin`)
|
||||
- 查询过滤:Repository `_apply_soft_delete_filter()` 自动排除已删除记录
|
||||
- 级联策略:默认不级联,强依赖关系在 Service 层手动处理
|
||||
- 恢复策略:只恢复记录本身,关联数据通过查询自动恢复可见
|
||||
- 唯一约束:使用 partial index 排除 `deleted_at IS NOT NULL`
|
||||
|
||||
```sql
|
||||
-- Partial unique index in migration
|
||||
CREATE UNIQUE INDEX ux_user_email
|
||||
ON users(email)
|
||||
WHERE deleted_at IS NULL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 表清单
|
||||
|
||||
| 表名 | 说明 | 状态 |
|
||||
|------|------|------|
|
||||
| `profiles` | 用户资料(含 settings JSONB) | Active |
|
||||
| `memories` | 用户记忆 | Active |
|
||||
| `friendships` | 好友关系 | Active |
|
||||
| `groups` | 群组 | Active |
|
||||
| `group_members` | 群组成员 | Active |
|
||||
| `schedule_items` | 日程事项 | Active |
|
||||
| `schedule_subscriptions` | 日程订阅与权限 | Active |
|
||||
| `inbox_messages` | 待处理消息 | Active |
|
||||
| `todos` | 待办 | Active |
|
||||
| `todo_sources` | 待办与日程来源关联 | Active |
|
||||
| `automation_jobs` | 定时任务 | Active |
|
||||
| `agent_chat_sessions` | Agent 对话会话 | Active |
|
||||
| `agent_chat_messages` | 会话消息记录 | Active |
|
||||
| `llm_factory` | LLM 工厂配置 | Active |
|
||||
| `llms` | LLM 模型实例 | Active |
|
||||
| `system_agents` | 系统级 Agent 配置 | Active |
|
||||
| `invite_codes` | 邀请码 | Active |
|
||||
|
||||
---
|
||||
|
||||
## 表结构详细
|
||||
|
||||
### profiles
|
||||
|
||||
用户资料表,含用户设置。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK, FK → auth.users.id | 用户 ID |
|
||||
| `username` | VARCHAR(30) | UNIQUE, NOT NULL | 用户名 |
|
||||
| `avatar_url` | TEXT | NULLABLE | 头像 URL |
|
||||
| `bio` | VARCHAR(200) | NULLABLE | 个人简介 |
|
||||
| `settings` | JSONB | NOT NULL, DEFAULT '{}' | 用户设置 |
|
||||
| `referred_by` | UUID | NULLABLE, FK → profiles.id | 邀请人 ID |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**settings JSONB 结构:**
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai"
|
||||
},
|
||||
"agent_prompts": {
|
||||
"INTENT_RECOGNITION": "自定义提示词...",
|
||||
"TASK_EXECUTION": "自定义提示词..."
|
||||
},
|
||||
"privacy": {},
|
||||
"notification": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### memories
|
||||
|
||||
用户记忆。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 记忆 ID |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 所有者 ID |
|
||||
| `memory_type` | VARCHAR(20) | NOT NULL, CHECK | 枚举:`user`, `work` |
|
||||
| `title` | VARCHAR(255) | NOT NULL | 标题 |
|
||||
| `content` | JSONB | NOT NULL | 记忆内容 |
|
||||
| `source` | VARCHAR(20) | NOT NULL | 来源:`manual`, `agent`, `imported` |
|
||||
| `status` | VARCHAR(20) | NOT NULL | 状态:`active`, `disabled` |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**content JSONB 示例:**
|
||||
```json
|
||||
// 用户记忆
|
||||
{"type": "preference", "data": {"style": "concise", "language": "zh-CN"}}
|
||||
|
||||
// 工作记忆
|
||||
{"type": "workflow_summary", "data": {"task": "代码审查", "learnings": ["优先检查安全漏洞"], "improvements": []}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### friendships
|
||||
|
||||
好友关系(双向规范化)。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 关系 ID |
|
||||
| `user_low_id` | UUID | NOT NULL | 较小 UUID |
|
||||
| `user_high_id` | UUID | NOT NULL | 较大 UUID |
|
||||
| `initiator_id` | UUID | NOT NULL | 发起方用户 ID |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`pending`, `accepted`, `blocked`, `declined`, `canceled` |
|
||||
| `requested_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 请求时间 |
|
||||
| `accepted_at` | TIMESTAMPTZ | NULLABLE | 接受时间 |
|
||||
| `blocked_by` | UUID | NULLABLE | 阻止者用户 ID |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `updated_by` | UUID | NULLABLE | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:**
|
||||
- `user_low_id < user_high_id`
|
||||
- `UNIQUE(user_low_id, user_high_id)`
|
||||
|
||||
---
|
||||
|
||||
### groups
|
||||
|
||||
群组。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 群组 ID |
|
||||
| `name` | VARCHAR(100) | NOT NULL | 群组名称 |
|
||||
| `description` | TEXT | NULLABLE | 群组描述 |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 创建者 ID |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `archived` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `updated_by` | UUID | NULLABLE | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
---
|
||||
|
||||
### group_members
|
||||
|
||||
群组成员。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 成员 ID |
|
||||
| `group_id` | UUID | NOT NULL, FK → groups.id | 群组 ID |
|
||||
| `user_id` | UUID | NOT NULL, FK → profiles.id | 用户 ID |
|
||||
| `role` | VARCHAR(20) | NOT NULL, CHECK | 角色:`owner`, `admin`, `member` |
|
||||
| `join_source` | VARCHAR(20) | NOT NULL | 加入方式:`invited`, `joined` |
|
||||
| `invited_by` | UUID | NULLABLE, FK → profiles.id | 邀请人 ID |
|
||||
| `joined_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 加入时间 |
|
||||
| `removed_at` | TIMESTAMPTZ | NULLABLE | 移除时间 |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `muted`, `removed` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `updated_by` | UUID | NULLABLE | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:** `UNIQUE(group_id, user_id)`
|
||||
|
||||
---
|
||||
|
||||
### schedule_items
|
||||
|
||||
日程事项。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 事项 ID |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 所有者 ID |
|
||||
| `title` | VARCHAR(255) | NOT NULL | 标题 |
|
||||
| `description` | TEXT | NULLABLE | 描述 |
|
||||
| `start_at` | TIMESTAMPTZ | NOT NULL | 开始时间 |
|
||||
| `end_at` | TIMESTAMPTZ | NULLABLE | 结束时间 |
|
||||
| `timezone` | VARCHAR(50) | NOT NULL, DEFAULT 'UTC' | 时区 |
|
||||
| `metadata` | JSONB | NOT NULL, DEFAULT '{}' | 扩展字段 |
|
||||
| `recurrence_rule` | VARCHAR(255) | NULLABLE | 循环规则 |
|
||||
| `source_type` | VARCHAR(20) | NOT NULL, CHECK | 来源:`manual`, `imported`, `agent_generated` |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `completed`, `canceled`, `archived` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**metadata JSONB 结构:**
|
||||
```json
|
||||
{
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得提前准备投影仪",
|
||||
"attachments": [
|
||||
{
|
||||
"name": "会议纪要.pdf",
|
||||
"url": "https://...",
|
||||
"visible_to": [],
|
||||
"type": "document"
|
||||
},
|
||||
{
|
||||
"name": "投影仪提醒",
|
||||
"visible_to": ["uuid1"],
|
||||
"type": "reminder",
|
||||
"content": "记得带投影仪"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### schedule_subscriptions
|
||||
|
||||
日程订阅与权限。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 订阅 ID |
|
||||
| `item_id` | UUID | NOT NULL, FK → schedule_items.id | 日程事项 ID |
|
||||
| `subscriber_id` | UUID | NOT NULL, FK → profiles.id | 订阅者 ID |
|
||||
| `permission` | INTEGER | NOT NULL, CHECK, DEFAULT 1 | 权限位图(view=1, invite=2, edit=4) |
|
||||
| `notify_level` | VARCHAR(20) | NOT NULL, DEFAULT 'all' | 通知级别:`all`, `mentions`, `none` |
|
||||
| `status` | VARCHAR(20) | NOT NULL, DEFAULT 'active' | 状态:`active`, `paused`, `unsubscribed` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**约束:**
|
||||
- `UNIQUE(item_id, subscriber_id)`
|
||||
- `permission BETWEEN 0 AND 7`
|
||||
|
||||
---
|
||||
|
||||
### inbox_messages
|
||||
|
||||
待处理消息(接收者视角)。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 消息 ID |
|
||||
| `recipient_id` | UUID | NOT NULL, FK → profiles.id | 接收者 ID |
|
||||
| `sender_id` | UUID | NULLABLE, FK → profiles.id | 发送者 ID(系统消息可为 NULL) |
|
||||
| `message_type` | VARCHAR(20) | NOT NULL, CHECK | 类型:`friend_request`, `calendar`, `system`, `group` |
|
||||
| `friendship_id` | UUID | NULLABLE, FK → friendships.id | 好友请求关联(friend_request 时必填) |
|
||||
| `schedule_item_id` | UUID | NULLABLE, FK → schedule_items.id | 日程关联(calendar 时必填) |
|
||||
| `group_id` | UUID | NULLABLE, FK → groups.id | 群组关联(group 时必填) |
|
||||
| `content` | TEXT | NULLABLE | 消息内容(system 用) |
|
||||
| `is_read` | BOOLEAN | NOT NULL, DEFAULT false | 是否已读 |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`pending`, `accepted`, `rejected`, `dismissed` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**message_type 与业务字段对应:**
|
||||
| message_type | 必填字段 |
|
||||
|--------------|----------|
|
||||
| friend_request | friendship_id |
|
||||
| calendar | schedule_item_id |
|
||||
| system | 全部可空 |
|
||||
| group | group_id |
|
||||
|
||||
**sender 约束:** system 类型 sender_id 为空,其他类型 sender_id 必填
|
||||
|
||||
---
|
||||
|
||||
### todos
|
||||
|
||||
待办事项。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 待办 ID |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 所有者 ID |
|
||||
| `title` | VARCHAR(255) | NOT NULL | 标题 |
|
||||
| `description` | VARCHAR(1000) | NULLABLE | 描述 |
|
||||
| `due_at` | TIMESTAMPTZ | NULLABLE | 截止时间 |
|
||||
| `priority` | INTEGER | NOT NULL, CHECK, DEFAULT 3 | 优先级(1-4,1=重要且紧急) |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`pending`, `done`, `canceled` |
|
||||
| `completed_at` | TIMESTAMPTZ | NULLABLE | 完成时间 |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:** `priority BETWEEN 1 AND 4`
|
||||
|
||||
---
|
||||
|
||||
### todo_sources
|
||||
|
||||
待办与日程来源关联。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 关联 ID |
|
||||
| `todo_id` | UUID | NOT NULL, FK → todos.id ON DELETE CASCADE | 待办 ID |
|
||||
| `schedule_item_id` | UUID | NOT NULL, FK → schedule_items.id ON DELETE CASCADE | 日程事项 ID |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**约束:** `UNIQUE(todo_id, schedule_item_id)`
|
||||
|
||||
---
|
||||
|
||||
### automation_jobs
|
||||
|
||||
自动化定时任务。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 任务 ID |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 所有者 ID |
|
||||
| `title` | VARCHAR(255) | NOT NULL | 任务标题 |
|
||||
| `prompt` | TEXT | NOT NULL | AI 执行 prompt |
|
||||
| `schedule_type` | VARCHAR(20) | NOT NULL, CHECK | 调度类型:`daily`, `weekly` |
|
||||
| `run_at` | TIMESTAMPTZ | NOT NULL | 首次运行时间 |
|
||||
| `next_run_at` | TIMESTAMPTZ | NULLABLE | 下次运行时间 |
|
||||
| `timezone` | VARCHAR(50) | NOT NULL, DEFAULT 'UTC' | 时区 |
|
||||
| `last_run_at` | TIMESTAMPTZ | NULLABLE | 最近运行时间 |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `disabled` |
|
||||
| `created_by` | UUID | NULLABLE | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:** `UNIQUE(id, owner_id)`
|
||||
|
||||
---
|
||||
|
||||
### agent_chat_sessions
|
||||
|
||||
Agent 对话会话。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 会话 ID |
|
||||
| `user_id` | UUID | NOT NULL, FK → profiles.id | 用户 ID |
|
||||
| `session_type` | VARCHAR(20) | NOT NULL, CHECK | 会话类型:`chat`, `automation` |
|
||||
| `job_id` | UUID | NULLABLE, FK → automation_jobs.id ON DELETE RESTRICT | 自动化任务 ID(automation 时必填) |
|
||||
| `title` | VARCHAR(255) | NULLABLE | 会话标题 |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`pending`, `running`, `completed`, `failed` |
|
||||
| `last_activity_at` | TIMESTAMPTZ | NULLABLE | 最后活跃时间 |
|
||||
| `message_count` | INTEGER | NOT NULL, DEFAULT 0 | 消息计数 |
|
||||
| `total_tokens` | INTEGER | NOT NULL, DEFAULT 0 | 总 token 数 |
|
||||
| `total_cost` | NUMERIC(12,6) | NOT NULL, DEFAULT 0 | 总费用 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:**
|
||||
- `session_type='chat' → job_id IS NULL`
|
||||
- `session_type='automation' → job_id IS NOT NULL`
|
||||
|
||||
---
|
||||
|
||||
### agent_chat_messages
|
||||
|
||||
会话消息记录。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 消息 ID |
|
||||
| `session_id` | UUID | NOT NULL, FK → agent_chat_sessions.id | 会话 ID |
|
||||
| `seq` | INTEGER | NOT NULL | 消息序号 |
|
||||
| `role` | VARCHAR(20) | NOT NULL, CHECK | 角色:`user`, `assistant`, `system`, `tool` |
|
||||
| `content` | TEXT | NOT NULL | 消息内容 |
|
||||
| `model_code` | VARCHAR(50) | NULLABLE | 模型标识 |
|
||||
| `tool_name` | VARCHAR(100) | NULLABLE | 工具名称 |
|
||||
| `input_tokens` | INTEGER | NOT NULL, DEFAULT 0 | 输入 token 数 |
|
||||
| `output_tokens` | INTEGER | NOT NULL, DEFAULT 0 | 输出 token 数 |
|
||||
| `cost` | NUMERIC(12,6) | NOT NULL, DEFAULT 0 | 费用 |
|
||||
| `currency` | VARCHAR(3) | NOT NULL, DEFAULT 'USD' | 货币 |
|
||||
| `latency_ms` | INTEGER | NULLABLE | 延迟(毫秒) |
|
||||
| `metadata` | JSONB | NOT NULL, DEFAULT '{}' | 扩展字段 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
**约束:** `UNIQUE(session_id, seq)`
|
||||
|
||||
---
|
||||
|
||||
### llm_factory
|
||||
|
||||
LLM 工厂配置。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 工厂 ID |
|
||||
| `name` | VARCHAR(50) | UNIQUE, NOT NULL | 工厂名称 |
|
||||
| `request_url` | VARCHAR(255) | NOT NULL | API 请求 URL |
|
||||
| `avatar` | TEXT | NULLABLE | 头像 URL |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
---
|
||||
|
||||
### llms
|
||||
|
||||
LLM 模型实例。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 模型 ID |
|
||||
| `factory_id` | UUID | NOT NULL, FK → llm_factory.id ON DELETE RESTRICT | 工厂 ID |
|
||||
| `model_code` | VARCHAR(50) | UNIQUE, NOT NULL | 模型标识 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | NULLABLE | 软删时间 |
|
||||
|
||||
---
|
||||
|
||||
### system_agents
|
||||
|
||||
系统级 Agent 配置(原 user_agent_catalog)。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `agent_type` | VARCHAR(20) | PK, CHECK | Agent 类型 |
|
||||
| `llm_id` | UUID | NOT NULL, FK → llms.id ON DELETE RESTRICT | 关联的 LLM 模型 |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `paused`, `migrating` |
|
||||
| `config` | JSONB | NOT NULL, DEFAULT '{}' | Agent 配置参数 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**agent_type 枚举值:**
|
||||
- `INTENT_RECOGNITION` - 意图识别
|
||||
- `TASK_EXECUTION` - 任务执行
|
||||
- `RESULT_REPORTING` - 结果报告
|
||||
|
||||
---
|
||||
|
||||
### invite_codes
|
||||
|
||||
邀请码。
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `id` | UUID | PK | 邀请码 ID |
|
||||
| `code` | VARCHAR(8) | UNIQUE, NOT NULL, CHECK | 邀请码(8 位大写字母数字) |
|
||||
| `owner_id` | UUID | NOT NULL, FK → profiles.id | 拥有者 ID |
|
||||
| `status` | VARCHAR(20) | NOT NULL, CHECK | 状态:`active`, `disabled`, `expired` |
|
||||
| `used_count` | INTEGER | NOT NULL, DEFAULT 0, CHECK | 已使用次数 |
|
||||
| `max_uses` | INTEGER | NULLABLE | 最大使用次数 |
|
||||
| `expires_at` | TIMESTAMPTZ | NULLABLE | 过期时间 |
|
||||
| `reward_config` | JSONB | NULLABLE | 奖励配置 |
|
||||
| `created_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
**约束:**
|
||||
- `code` 符合 `[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{8}`
|
||||
- `used_count >= 0`
|
||||
|
||||
---
|
||||
|
||||
## 外键删除策略
|
||||
|
||||
| 外键 | 删除策略 | 说明 |
|
||||
|------|----------|------|
|
||||
| `agent_chat_sessions.job_id` | RESTRICT | 禁止删除正在使用的自动化任务 |
|
||||
| `todo_sources.todo_id` | CASCADE | 删除待办时级联删除关联 |
|
||||
| `todo_sources.schedule_item_id` | CASCADE | 删除日程时级联删除关联 |
|
||||
| `inbox_messages.friendship_id` | CASCADE | 删除好友关系时级联删除消息 |
|
||||
| `inbox_messages.schedule_item_id` | CASCADE | 删除日程时级联删除消息 |
|
||||
| `inbox_messages.group_id` | CASCADE | 删除群组时级联删除消息 |
|
||||
| `llms.factory_id` | RESTRICT | 禁止删除正在使用的工厂配置 |
|
||||
| `system_agents.llm_id` | RESTRICT | 禁止删除正在使用的 LLM 模型 |
|
||||
|
||||
---
|
||||
|
||||
## RLS 策略
|
||||
|
||||
所有 `public` 业务表默认启用 RLS:
|
||||
- `anon`: 全部 DENY
|
||||
- `authenticated`: 全部 DENY
|
||||
- `service_role`: 由后端服务连接,不依赖 RLS
|
||||
|
||||
**例外:** 迁移表 `alembic_version` 不暴露给任何角色。
|
||||
|
||||
---
|
||||
|
||||
## Change Log
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-03-06 | 删除 `user_agents` 表,重命名 `user_agent_catalog` → `system_agents`,更新 `agent_chat_sessions` / `agent_chat_messages` 表名,删除 `memories.agent_id` 字段 |
|
||||
| 2026-02-28 | 新增 `invite_codes` 表、`profiles.referred_by` 字段 |
|
||||
| 2026-02-26 | 初始版本,基于数据模型重设计 |
|
||||
@@ -1,264 +0,0 @@
|
||||
# Frontend Runtime
|
||||
|
||||
**Date:** 2026-03-06
|
||||
**Status:** Active
|
||||
**Audience:** 前端开发
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Framework:** Flutter (Dart)
|
||||
- **Routing:** go_router
|
||||
- **State Management:** BLoC + Cubit
|
||||
- **API Client:** Dio + Retrofit
|
||||
- **Mock Mode:** 支持 `--dart-define=MOCK_API=true`
|
||||
|
||||
---
|
||||
|
||||
## 开发环境
|
||||
|
||||
### Mock 模式
|
||||
|
||||
前端开发时可通过 `--dart-define` 切换 Mock 模式,无需后端即可运行:
|
||||
|
||||
```bash
|
||||
# Mock 模式(本地开发,无需后端)
|
||||
flutter run --dart-define=MOCK_API=true
|
||||
|
||||
# 正式模式(需要后端运行)
|
||||
flutter run
|
||||
```
|
||||
|
||||
### Mock 自动登录
|
||||
|
||||
Mock 模式下,启动 App 时会自动使用测试账号登录并跳转到首页。
|
||||
|
||||
**测试账号(Mock):**
|
||||
|
||||
| 场景 | 邮箱 | 密码 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 正常登录 | 任意非 error@test.com | 任意 | 登录成功 |
|
||||
| 登录失败 | error@test.com | 任意 | 返回 401 |
|
||||
|
||||
**验证码:** 任意 6 位数字(建议使用 `123456`)
|
||||
|
||||
---
|
||||
|
||||
## 路由结构
|
||||
|
||||
### 认证路由(无需登录)
|
||||
|
||||
| 路由 | 页面 | 说明 |
|
||||
|------|------|------|
|
||||
| `/` | LoginScreen | 登录页(默认首页) |
|
||||
| `/register` | RegisterScreen | 注册页 |
|
||||
| `/register/verification` | RegisterVerificationScreen | 注册验证码页 |
|
||||
| `/reset-password` | ResetPasswordScreen | 重置密码页 |
|
||||
|
||||
### 受保护路由(需要登录)
|
||||
|
||||
| 路由 | 页面 | 说明 |
|
||||
|------|------|------|
|
||||
| `/home` | HomeScreen | 首页(AI 助手) |
|
||||
| `/contacts` | ContactsScreen | 通讯录 |
|
||||
| `/contacts/add` | AddContactScreen | 添加联系人 |
|
||||
| `/calendar/month` | CalendarMonthScreen | 月视图 |
|
||||
| `/calendar/dayweek` | CalendarDayweekScreen | 日/周视图 |
|
||||
| `/calendar/events/:id` | CalendarEventDetailScreen | 日程详情 |
|
||||
| `/todo` | TodoQuadrantsScreen | 待办四象限 |
|
||||
| `/messages/invites` | MessageInviteListScreen | 消息邀请列表 |
|
||||
| `/messages/invites/:id` | MessageInviteDetailScreen | 消息邀请详情 |
|
||||
| `/settings` | SettingsScreen | 设置首页 |
|
||||
| `/settings/features` | FeaturesScreen | 功能开关 |
|
||||
| `/settings/memory` | MemoryScreen | 记忆管理 |
|
||||
| `/settings/account` | AccountScreen | 账号设置 |
|
||||
|
||||
---
|
||||
|
||||
## 功能模块
|
||||
|
||||
### Auth(认证)
|
||||
|
||||
**路径:** `apps/lib/features/auth/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `presentation/bloc/auth_bloc.dart` | 认证状态管理 |
|
||||
| `presentation/cubits/` | 登录/注册/重置密码 Cubit |
|
||||
| `ui/screens/` | 认证相关页面 |
|
||||
| `data/repositories/auth_repository.dart` | 认证 API 调用 |
|
||||
|
||||
**流程:**
|
||||
1. 注册: `/register` → 输入邮箱/用户名/密码 → `/register/verification` → 输入验证码 → `/home`
|
||||
2. 登录: `/` → 输入邮箱/密码 → `/home`
|
||||
3. 重置密码: `/reset-password` → 输入邮箱 → 收到邮件 → 输入验证码+新密码 → `/`
|
||||
|
||||
---
|
||||
|
||||
### Home(首页/AI 助手)
|
||||
|
||||
**路径:** `apps/lib/features/home/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/home_screen.dart` | 首页(AI 助手入口) |
|
||||
| `ui/screens/home_sheet.dart` | 首页底部弹出面板 |
|
||||
|
||||
**功能:**
|
||||
- AI 助手对话入口
|
||||
- 快速访问常用功能
|
||||
|
||||
---
|
||||
|
||||
### Calendar(日历)
|
||||
|
||||
**路径:** `apps/lib/features/calendar/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/calendar_month_screen.dart` | 月视图 |
|
||||
| `ui/screens/calendar_dayweek_screen.dart` | 日/周视图 |
|
||||
| `ui/screens/calendar_event_detail_screen.dart` | 日程详情 |
|
||||
| `ui/calendar_time_utils.dart` | 时间工具函数 |
|
||||
|
||||
**功能:**
|
||||
- 月/日/周三视图切换
|
||||
- 日程创建/编辑/删除
|
||||
- 日程分享
|
||||
|
||||
---
|
||||
|
||||
### Todo(待办)
|
||||
|
||||
**路径:** `apps/lib/features/todo/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/todo_quadrants_screen.dart` | 四象限视图 |
|
||||
| `ui/screens/todo_detail_screen.dart` | 待办详情 |
|
||||
|
||||
**功能:**
|
||||
- 四象限管理(重要/紧急矩阵)
|
||||
- 待办创建/编辑/完成/删除
|
||||
|
||||
---
|
||||
|
||||
### Contacts(通讯录)
|
||||
|
||||
**路径:** `apps/lib/features/contacts/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/contacts_screen.dart` | 通讯录列表 |
|
||||
| `ui/screens/add_contact_screen.dart` | 添加联系人 |
|
||||
|
||||
**功能:**
|
||||
- 好友列表
|
||||
- 搜索用户
|
||||
- 添加好友
|
||||
|
||||
---
|
||||
|
||||
### Messages(消息)
|
||||
|
||||
**路径:** `apps/lib/features/messages/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/message_invite_list_screen.dart` | 邀请消息列表 |
|
||||
| `ui/screens/message_invite_detail_screen.dart` | 邀请详情 |
|
||||
|
||||
**功能:**
|
||||
- 日程邀请通知
|
||||
- 好友请求通知
|
||||
- 群组邀请通知
|
||||
|
||||
---
|
||||
|
||||
### Settings(设置)
|
||||
|
||||
**路径:** `apps/lib/features/settings/`
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `ui/screens/settings_screen.dart` | 设置首页 |
|
||||
| `ui/screens/features_screen.dart` | 功能开关 |
|
||||
| `ui/screens/memory_screen.dart` | 记忆管理 |
|
||||
| `ui/screens/account_screen.dart` | 账号设置 |
|
||||
|
||||
**功能:**
|
||||
- 个人资料编辑
|
||||
- 记忆管理(用户/工作记忆)
|
||||
- 功能开关
|
||||
- 账号安全设置
|
||||
|
||||
---
|
||||
|
||||
## 打包构建
|
||||
|
||||
### Debug Build
|
||||
|
||||
```bash
|
||||
# Mock 模式
|
||||
flutter build apk --debug --dart-define=MOCK_API=true
|
||||
|
||||
# 正式模式
|
||||
flutter build apk --debug
|
||||
```
|
||||
|
||||
### Release Build
|
||||
|
||||
Release 构建强制使用正式 API,不受 `MOCK_API` 影响:
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调试运行
|
||||
|
||||
### 命令行调试
|
||||
|
||||
```bash
|
||||
# Mock 模式(无需后端,自动登录)
|
||||
flutter run --dart-define=MOCK_API=true -d emulator-5554
|
||||
|
||||
# 正式模式(需要后端运行)
|
||||
flutter run -d emulator-5554
|
||||
```
|
||||
|
||||
### VSCode 调试配置
|
||||
|
||||
在 `.vscode/launch.json` 中添加配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Mock Mode",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": ["--dart-define=MOCK_API=true"]
|
||||
},
|
||||
{
|
||||
"name": "正式模式",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
配置完成后,在 VSCode 左侧 Debug 面板的 dropdown 中选择 "Mock Mode" 或 "正式模式" 进行调试。
|
||||
|
||||
---
|
||||
|
||||
## Change Log
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-03-06 | 完善路由结构、功能模块说明,补充技术栈信息 |
|
||||
| 2026-02-27 | 新增 Frontend Runbook,支持 --dart-define=MOCK_API=true 切换 Mock 模式 |
|
||||
@@ -1,891 +0,0 @@
|
||||
# Runtime API Routes
|
||||
|
||||
本文档记录所有 HTTP API 端点。修改路由时必须同步更新此文档。
|
||||
|
||||
## 格式说明
|
||||
|
||||
- Request/Response 使用 JSON 格式
|
||||
- 错误响应使用 RFC 7807 `application/problem+json`
|
||||
- 所有端点前缀: `/api/v1`
|
||||
|
||||
## Auth
|
||||
|
||||
### POST /auth/verifications
|
||||
|
||||
创建验证码(注册发起)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "string (3-30 chars)",
|
||||
"email": "string (email)",
|
||||
"password": "string (min 6 chars)",
|
||||
"redirect_to": "string? (optional)",
|
||||
"invite_code": "string? (8 chars, 排除易混淆字符 0/1/I/L/O)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 202 Accepted
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**邀请码说明:**
|
||||
- 可选字段,不填则注册不受影响
|
||||
- 格式:8 位字母数字组合,排除易混淆字符 (0, 1, I, L, O)
|
||||
- 注册时传入有效邀请码会建立邀请关系并增加邀请码使用次数
|
||||
- 无效邀请码(不存在/已禁用/已过期/已达上限)不会阻断注册成功
|
||||
|
||||
**Errors:**
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/resend
|
||||
|
||||
重发验证码(统一端点,支持注册/找回密码)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"type": "signup | recovery (default: signup)",
|
||||
"email": "string (email)",
|
||||
"redirect_to": "string? (仅 recovery 可选)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/verify
|
||||
|
||||
验证码校验(统一端点,按 `type` 区分场景)。
|
||||
|
||||
**Request (signup):**
|
||||
```json
|
||||
{
|
||||
"type": "signup",
|
||||
"email": "string (email)",
|
||||
"token": "string (6 digits)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (signup):** 200 OK
|
||||
```json
|
||||
{
|
||||
"access_token": "string",
|
||||
"refresh_token": "string",
|
||||
"expires_in": 3600,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"email": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Request (recovery):**
|
||||
```json
|
||||
{
|
||||
"type": "recovery",
|
||||
"email": "string (email)",
|
||||
"token": "string (6 digits)",
|
||||
"new_password": "string (min 6 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (recovery):** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 401: 验证码无效或已过期
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/sessions
|
||||
|
||||
登录(创建会话)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"email": "string (email)",
|
||||
"password": "string (min 6 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"access_token": "string",
|
||||
"refresh_token": "string",
|
||||
"expires_in": 3600,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"email": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 邮箱或密码错误
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### POST /auth/sessions/refresh
|
||||
|
||||
刷新 Token。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refresh_token": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"access_token": "string",
|
||||
"refresh_token": "string",
|
||||
"expires_in": 3600,
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"email": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 无效的 refresh token
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### DELETE /auth/sessions
|
||||
|
||||
登出(删除会话)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refresh_token": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 422: 请求参数无效
|
||||
- 429: 请求过于频繁
|
||||
|
||||
---
|
||||
|
||||
### GET /auth/users
|
||||
|
||||
按邮箱查询用户(需要认证)。
|
||||
|
||||
**Query Parameters:**
|
||||
- `email`: string (required)
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"email": "string",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"email_confirmed_at": "string? (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 403: 无权限访问
|
||||
- 404: 用户不存在
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
## Schedule Items
|
||||
|
||||
### POST /schedule-items
|
||||
|
||||
创建日历事项(需要认证)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"title": "string (1-255 chars, required)",
|
||||
"description": "string? (max 2000 chars)",
|
||||
"start_at": "string (ISO 8601 datetime, required)",
|
||||
"end_at": "string? (ISO 8601 datetime)",
|
||||
"timezone": "string? (default: UTC)",
|
||||
"metadata": {
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得带身份证",
|
||||
"attachments": [],
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 201 Created
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "string",
|
||||
"description": "string?",
|
||||
"start_at": "string",
|
||||
"end_at": "string?",
|
||||
"timezone": "string",
|
||||
"metadata": {},
|
||||
"status": "active",
|
||||
"source_type": "manual",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 400: end_at 早于 start_at
|
||||
- 401: 未认证
|
||||
- 503: 服务不可用
|
||||
|
||||
---
|
||||
|
||||
### GET /schedule-items
|
||||
|
||||
按时间范围查询日历事项列表(需要认证)。
|
||||
|
||||
**Query Parameters:**
|
||||
- `start_at`: ISO 8601 date/datetime(查询范围起始)
|
||||
- `end_at`: ISO 8601 date/datetime(查询范围结束)
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "string",
|
||||
"description": "string?",
|
||||
"start_at": "string",
|
||||
"end_at": "string?",
|
||||
"timezone": "string",
|
||||
"metadata": {
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得带身份证",
|
||||
"attachments": [],
|
||||
"version": 1
|
||||
},
|
||||
"status": "active",
|
||||
"source_type": "manual",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 400: end_at 早于 start_at
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
### GET /schedule-items/{id}
|
||||
|
||||
获取单个日历事项详情(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 事项不存在
|
||||
|
||||
---
|
||||
|
||||
### PATCH /schedule-items/{id}
|
||||
|
||||
更新日历事项(需要认证)。
|
||||
|
||||
**Request:** 支持 `title`/`description`/`start_at`/`end_at`/`timezone`/`metadata`/`status` 部分更新
|
||||
|
||||
**Response:** 200 OK
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 事项不存在
|
||||
|
||||
---
|
||||
|
||||
### DELETE /schedule-items/{id}
|
||||
|
||||
删除日历事项(软删除,需要认证)。
|
||||
|
||||
**Response:** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 事项不存在
|
||||
|
||||
---
|
||||
|
||||
### POST /schedule-items/{id}/share
|
||||
|
||||
分享日历事项给他人(需要认证)。
|
||||
|
||||
通过邮箱邀请其他用户,被邀请人将收到待办消息邀请。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"email": "string (required, email of user to share with)",
|
||||
"permission_view": "boolean (default: true)",
|
||||
"permission_edit": "boolean (default: false)",
|
||||
"permission_invite": "boolean (default: false)"
|
||||
}
|
||||
```
|
||||
|
||||
**Permission 位说明:**
|
||||
| 权限 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| view | 1 | 查看事项详情 |
|
||||
| invite | 2 | 邀请其他人订阅此事项 |
|
||||
| edit | 4 | 修改事项内容、管理订阅 |
|
||||
|
||||
可组合使用,如 view+edit = 5,view+invite+edit = 7。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"message": "Invitation sent to user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 403: 非日历所有者无权分享
|
||||
- 404: 日历事项不存在或用户不存在
|
||||
|
||||
---
|
||||
|
||||
## Inbox Messages
|
||||
|
||||
### GET /inbox/messages
|
||||
|
||||
获取当前用户的待办消息列表(需要认证)。
|
||||
|
||||
**Query Parameters:**
|
||||
- `status`: string (optional) - 过滤状态:`pending`/`accepted`/`rejected`/`dismissed`
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"recipient_id": "uuid",
|
||||
"sender_id": "uuid?",
|
||||
"message_type": "calendar",
|
||||
"schedule_item_id": "uuid?",
|
||||
"content": "string?",
|
||||
"is_read": false,
|
||||
"status": "pending",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
## Users
|
||||
|
||||
### GET /users/me
|
||||
|
||||
获取当前用户信息(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"avatar_url": "string?",
|
||||
"bio": "string?"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
### PATCH /users/me
|
||||
|
||||
更新当前用户信息(需要认证)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "string? (3-30 chars)",
|
||||
"avatar_url": "string? (URL)",
|
||||
"bio": "string? (max 200 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"avatar_url": "string?",
|
||||
"bio": "string?"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
### POST /users/search
|
||||
|
||||
搜索用户(需要认证)。
|
||||
|
||||
支持两种查询模式:
|
||||
- **用户名查询**:模糊匹配,返回最多 20 个结果
|
||||
- **邮箱查询**:精确匹配,返回 0 或 1 个结果
|
||||
|
||||
查询类型自动识别:包含 `@` 符号视为邮箱查询。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"query": "string (1-100 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"avatar_url": "string?",
|
||||
"bio": "string?"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 503: Auth 服务不可用(仅邮箱查询)
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
## Friends
|
||||
|
||||
### POST /friends/requests
|
||||
|
||||
发送好友请求(需要认证)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"target_user_id": "string (uuid)",
|
||||
"content": "string? (max 500 chars)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 201 Created
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"from_user_id": "uuid",
|
||||
"to_user_id": "uuid",
|
||||
"content": "string?",
|
||||
"status": "pending",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"updated_at": "string (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 400: 不能添加自己为好友
|
||||
- 401: 未认证
|
||||
- 404: 目标用户不存在
|
||||
- 409: 已是好友或请求已存在
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
### GET /friends/requests/inbox
|
||||
|
||||
获取收到的好友请求(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"from_user_id": "uuid",
|
||||
"to_user_id": "uuid",
|
||||
"content": "string?",
|
||||
"status": "pending",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"updated_at": "string (ISO 8601)"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
### GET /friends/requests/outgoing
|
||||
|
||||
获取发出的好友请求(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"from_user_id": "uuid",
|
||||
"to_user_id": "uuid",
|
||||
"content": "string?",
|
||||
"status": "pending",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"updated_at": "string (ISO 8601)"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
### POST /friends/requests/{id}/accept
|
||||
|
||||
接受好友请求(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"from_user_id": "uuid",
|
||||
"to_user_id": "uuid",
|
||||
"content": "string?",
|
||||
"status": "accepted",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"updated_at": "string (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 请求不存在
|
||||
- 409: 请求已被处理
|
||||
|
||||
---
|
||||
|
||||
### POST /friends/requests/{id}/decline
|
||||
|
||||
拒绝好友请求(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"from_user_id": "uuid",
|
||||
"to_user_id": "uuid",
|
||||
"content": "string?",
|
||||
"status": "declined",
|
||||
"created_at": "string (ISO 8601)",
|
||||
"updated_at": "string (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 请求不存在
|
||||
- 409: 请求已被处理
|
||||
|
||||
---
|
||||
|
||||
### DELETE /friends/requests/{id}
|
||||
|
||||
取消发出的好友请求(需要认证)。
|
||||
|
||||
**Response:** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 请求不存在
|
||||
|
||||
---
|
||||
|
||||
### GET /friends
|
||||
|
||||
获取好友列表(需要认证)。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"friend_id": "uuid",
|
||||
"username": "string",
|
||||
"avatar_url": "string?",
|
||||
"bio": "string?",
|
||||
"created_at": "string (ISO 8601)"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
|
||||
---
|
||||
|
||||
### DELETE /friends/{id}
|
||||
|
||||
删除好友(需要认证)。
|
||||
|
||||
**Response:** 204 No Content
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 404: 好友关系不存在
|
||||
|
||||
---
|
||||
|
||||
## Agent Runtime
|
||||
|
||||
### POST /agent/runs
|
||||
|
||||
创建一次 Agent 异步运行任务(需要认证)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"threadId": "string (UUID, required)",
|
||||
"runId": "string (required)",
|
||||
"parentRunId": "string? (optional)",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "string",
|
||||
"role": "user",
|
||||
"content": "string | InputContent[]"
|
||||
}
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 202 Accepted
|
||||
```json
|
||||
{
|
||||
"taskId": "string",
|
||||
"threadId": "string",
|
||||
"runId": "string",
|
||||
"created": false
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 403: 非会话 owner
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
### POST /agent/runs/{thread_id}/resume
|
||||
|
||||
恢复一次等待工具结果的 Agent 运行(需要认证)。
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"threadId": "string (must match path thread_id)",
|
||||
"runId": "string",
|
||||
"parentRunId": "string? (optional)",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "string",
|
||||
"role": "tool",
|
||||
"toolCallId": "string",
|
||||
"content": "string (JSON string, AG-UI ToolMessage content)"
|
||||
}
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 202 Accepted
|
||||
```json
|
||||
{
|
||||
"taskId": "string",
|
||||
"threadId": "string",
|
||||
"runId": "string",
|
||||
"created": false
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 403: 非会话 owner
|
||||
- 422: 请求参数无效
|
||||
|
||||
---
|
||||
|
||||
### GET /agent/runs/{thread_id}/events
|
||||
|
||||
订阅 Agent SSE 事件流(需要认证)。
|
||||
|
||||
**Headers:**
|
||||
- `Last-Event-ID` (optional): 断点续传游标,格式 `^\d+-\d+$`
|
||||
|
||||
**Response:** 200 OK
|
||||
`Content-Type: text/event-stream`
|
||||
|
||||
```text
|
||||
id: 2-0
|
||||
event: RUN_STARTED
|
||||
data: {"type":"RUN_STARTED","threadId":"...","runId":"..."}
|
||||
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 403: 非会话 owner
|
||||
|
||||
---
|
||||
|
||||
### GET /agent/runs/{thread_id}/history
|
||||
|
||||
按“天”读取指定会话的历史快照(需要认证)。
|
||||
|
||||
**Query:**
|
||||
- `before` (optional, `YYYY-MM-DD`): 读取该日期之前的最近一天
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"type": "STATE_SNAPSHOT",
|
||||
"threadId": "string",
|
||||
"snapshot": {
|
||||
"scope": "history_day",
|
||||
"threadId": "string",
|
||||
"day": "2026-03-07",
|
||||
"hasMore": true,
|
||||
"messages": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- 401: 未认证
|
||||
- 403: 非会话 owner
|
||||
|
||||
---
|
||||
|
||||
### GET /agent/history
|
||||
|
||||
读取当前用户历史快照(需要认证)。当未传 `threadId` 时,默认返回最近活跃会话的按天快照。
|
||||
|
||||
**Query:**
|
||||
- `threadId` (optional): 指定会话
|
||||
- `before` (optional, `YYYY-MM-DD`): 读取该日期之前的最近一天
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"type": "STATE_SNAPSHOT",
|
||||
"threadId": "string?",
|
||||
"snapshot": {
|
||||
"scope": "history_day",
|
||||
"threadId": "string?",
|
||||
"day": "2026-03-07",
|
||||
"hasMore": false,
|
||||
"messages": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Infra
|
||||
|
||||
### GET /infra/health
|
||||
|
||||
检查基础设施健康状态。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"status": "healthy" | "unhealthy",
|
||||
"services": {
|
||||
"redis": {
|
||||
"status": "healthy" | "unhealthy",
|
||||
"latency_ms": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /health
|
||||
|
||||
检查服务健康状态。
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
{
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Response Format (RFC 7807)
|
||||
|
||||
所有错误响应使用 `application/problem+json` 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "about:blank",
|
||||
"title": "Unauthorized",
|
||||
"status": 401,
|
||||
"detail": "验证码无效或已过期",
|
||||
"instance": "/api/v1/auth/verify"
|
||||
}
|
||||
```
|
||||
|
||||
前端应优先读取 `detail` 字段显示给用户。
|
||||
@@ -1,263 +0,0 @@
|
||||
# Runtime Runbook
|
||||
|
||||
**Date:** 2026-02-25
|
||||
**Status:** Active
|
||||
**Audience:** 运维 / 后端值班
|
||||
|
||||
## Scope & Preconditions
|
||||
|
||||
本手册用于日常值班、发布前检查、故障处置与回滚。
|
||||
|
||||
### 前置条件
|
||||
|
||||
- 已配置 `.env`(仓库根目录)。
|
||||
- 主机可用:`docker`、`docker compose`、`tmux`、`uv`。
|
||||
- 已拉取最新代码并确认当前分支与目标发布版本一致。
|
||||
|
||||
### 红线规则
|
||||
|
||||
- 禁止跳过 bootstrap gate 直接启动 web/worker。
|
||||
- 迁移/初始化容器执行时必须带 `--build`,避免旧镜像导致迁移不生效。
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap Gate (Mandatory)
|
||||
|
||||
以下流程必须按顺序执行。
|
||||
|
||||
### Step 1: 启动基础设施
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d redis
|
||||
```
|
||||
|
||||
通过标准:`docker compose ... ps` 中 `redis` 容器为 `running`/`healthy`。
|
||||
|
||||
### Step 2: 执行迁移与初始化
|
||||
|
||||
#### 生产环境
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml run --rm --build init-job uv run python -m core.runtime.cli bootstrap
|
||||
```
|
||||
|
||||
#### 开发环境(推荐)
|
||||
开发阶段推荐使用脚本,直接使用本地代码,无需构建镜像:
|
||||
|
||||
```bash
|
||||
bash infra/scripts/dev-migrate.sh bootstrap
|
||||
```
|
||||
|
||||
可选命令:
|
||||
- `bash infra/scripts/dev-migrate.sh migrate` - 仅运行迁移
|
||||
- `bash infra/scripts/dev-migrate.sh init-data` - 仅初始化数据
|
||||
|
||||
通过标准:命令退出码为 0,日志中无 migration/init-data 错误。
|
||||
|
||||
### Step 3: 版本核对(建议)
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml exec -T db \
|
||||
psql -U postgres -d postgres -c "SELECT version_num FROM public.alembic_version;"
|
||||
```
|
||||
|
||||
通过标准:返回 1 行版本号,且与发布预期版本一致。
|
||||
|
||||
---
|
||||
|
||||
## Service Start / Stop (tmux)
|
||||
|
||||
### 启动应用进程
|
||||
|
||||
```bash
|
||||
bash infra/scripts/app.sh start
|
||||
```
|
||||
|
||||
该脚本会在 tmux `social-dev` 会话中拉起:
|
||||
|
||||
- web
|
||||
- worker-critical
|
||||
- worker-default
|
||||
- worker-bulk
|
||||
|
||||
通过标准:`tmux list-windows -t social-dev` 可见上述窗口。
|
||||
|
||||
### 常用 tmux 命令
|
||||
|
||||
```bash
|
||||
tmux list-windows -t social-dev
|
||||
tmux attach -t social-dev
|
||||
tmux kill-session -t social-dev
|
||||
```
|
||||
|
||||
### 日志文件
|
||||
|
||||
| 服务 | 日志文件 |
|
||||
|------|---------|
|
||||
| Web | `logs/web.log`, `logs/errors/web.error.log` |
|
||||
| Worker Critical | `logs/worker-critical.log`, `logs/errors/worker-critical.error.log` |
|
||||
| Worker Default | `logs/worker-default.log`, `logs/errors/worker-default.error.log` |
|
||||
| Worker Bulk | `logs/worker-bulk.log`, `logs/errors/worker-bulk.error.log` |
|
||||
|
||||
---
|
||||
|
||||
## Operational Verification
|
||||
|
||||
按优先级分层执行。
|
||||
|
||||
### L1 必跑(发布前/故障恢复后必须)
|
||||
|
||||
```bash
|
||||
# 先导入 .env,确保端口与配置一致
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
WEB_BASE_URL="http://127.0.0.1:${SOCIAL_WEB__PORT:-5775}"
|
||||
|
||||
# 基础健康(redis/web;数据库使用云 Supabase Postgres)
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml exec -T redis \
|
||||
sh -lc 'if [ -n "${REDIS_PASSWORD:-}" ]; then redis-cli -a "${REDIS_PASSWORD}" ping; else redis-cli ping; fi'
|
||||
|
||||
curl -fsS "${WEB_BASE_URL}/health"
|
||||
|
||||
# compose 状态
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||
|
||||
# 核心接口 smoke
|
||||
curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/sessions" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"demo@example.com","password":"secret123"}'
|
||||
```
|
||||
|
||||
通过标准:redis 健康检查成功,web `/health` 返回 2xx,容器 `running`,核心接口返回预期业务状态码。
|
||||
|
||||
### L2 可选(Auth/Profile 业务回归)
|
||||
|
||||
```bash
|
||||
# signup start
|
||||
curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/verifications" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"demo","email":"demo@example.com","password":"secret123"}'
|
||||
|
||||
# signup verify
|
||||
curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/verify" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"type":"signup","email":"demo@example.com","token":"123456"}'
|
||||
|
||||
# signup resend
|
||||
curl -sS -X POST "${WEB_BASE_URL}/api/v1/auth/resend" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"type":"signup","email":"demo@example.com"}'
|
||||
```
|
||||
|
||||
通过标准:接口返回符合预期的 2xx 或受控业务错误,无 5xx。
|
||||
|
||||
## Incident Playbook
|
||||
|
||||
### 1) 迁移未生效(常见于旧镜像)
|
||||
|
||||
- 症状:字段/表结构与代码不一致,接口报 schema 错误。
|
||||
- 定位:检查 `alembic_version` 与容器镜像构建时间。
|
||||
- 修复:重新执行 `init-job --build`,并复核版本号。
|
||||
|
||||
### 2) Worker 不消费任务
|
||||
|
||||
- 症状:队列堆积,任务长时间 pending。
|
||||
- 定位:检查 `worker-*` tmux 窗口和对应日志文件。
|
||||
- 修复:重启 tmux 会话,确认并发配置与队列名(critical/default/bulk)。
|
||||
- 说明:Taskiq 路径当前仅消费 `SOCIAL_WORKER__GROUPS__*__CONCURRENCY`,旧 Celery 参数(prefetch/time_limit 等)已废弃。
|
||||
|
||||
### 2.1) Agent Runtime run/resume 事件不闭环
|
||||
|
||||
- 症状:`POST /api/v1/agent/runs` 返回 202,但前端事件流没有 `RUN_FINISHED`。
|
||||
- 定位步骤:
|
||||
|
||||
```bash
|
||||
# 1) 检查 taskiq worker 是否消费 agent 任务
|
||||
grep -E "tasks\.agent\.run_command|RUN_STARTED|RUN_FINISHED|RUN_ERROR" logs/worker-default.log
|
||||
|
||||
# 2) 检查 API SSE 事件读取(带 Last-Event-ID)
|
||||
curl -N "${WEB_BASE_URL}/api/v1/agent/runs/<session_id>/events" \
|
||||
-H "Authorization: Bearer <access_token>" \
|
||||
-H "Last-Event-ID: 1-0"
|
||||
|
||||
# 3) 检查 Redis 连通(必要时)
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml exec -T redis redis-cli ping
|
||||
```
|
||||
|
||||
- 修复建议:
|
||||
- 若 worker 无消费:重启 `worker-default` 窗口并确认 `core.agent.infrastructure.queue.tasks` 已被 Taskiq worker 加载。
|
||||
- 若 worker 有事件但 API 无输出:排查 Redis stream 前缀配置与 session_id 是否一致。
|
||||
- 若出现 `RUN_ERROR`:按 error_id 回查后端日志,不在 API/SSE 中暴露敏感上下文。
|
||||
|
||||
### 3) JWT 或认证异常
|
||||
|
||||
- 症状:接口持续 401/403。
|
||||
- 定位:核对 `.env` 中 Supabase JWT 配置与签发方设置。
|
||||
- 修复:修正配置后重启 web 进程并执行 L1/L2 验证。
|
||||
|
||||
### 4) 基础设施容器异常(db/redis)
|
||||
|
||||
- 症状:web 启动失败、迁移失败、任务队列连接报错。
|
||||
- 定位:
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml ps
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml logs db --tail=100
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml logs redis --tail=100
|
||||
```
|
||||
|
||||
- 修复:按依赖顺序重建基础设施后重新 bootstrap。
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d --force-recreate redis
|
||||
bash infra/scripts/dev-migrate.sh bootstrap
|
||||
```
|
||||
|
||||
- 复核标准:`redis` 健康检查通过,L1 核心接口 smoke 无 5xx。
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedure
|
||||
|
||||
### 回滚前检查
|
||||
|
||||
- 确认目标回滚提交或版本号。
|
||||
- 确认是否涉及不可逆数据变更。
|
||||
|
||||
### 回滚执行
|
||||
|
||||
1. 停止应用进程:`tmux kill-session -t social-dev`
|
||||
2. 切换代码到目标版本。
|
||||
3. 按目标版本要求执行迁移回滚(如有)。
|
||||
4. 重新执行 bootstrap gate 与 service 启动。
|
||||
|
||||
### 回滚后复核
|
||||
|
||||
- 执行 L1 必跑检查。
|
||||
- 记录回滚原因、时间、影响范围和后续修复计划。
|
||||
|
||||
---
|
||||
|
||||
## Change Log
|
||||
|
||||
| 日期 | 变更 |
|
||||
|------|------|
|
||||
| 2026-02-24 | 创建运行时手册,删除 legacy 脚本,统一使用 gunicorn |
|
||||
| 2026-02-24 | 清理配置:合并 AppSettings 到 WebSettings,删除 Worker 旧配置 (enabled_queues/queues),统一使用 SOCIAL_WEB__GUNICORN__* 命名 |
|
||||
| 2026-02-24 | 开发阶段 compose 暂不编排 web/worker,仅保留 redis/db 与 init-job |
|
||||
| 2026-02-24 | 新增 dev-app-up 脚本:手动基础设施后,一键 bootstrap + tmux 拉起 web/worker |
|
||||
| 2026-02-25 | 补充迁移防遗漏规则:容器迁移命令统一追加 --build;开发调试优先使用本地 CLI 一次性迁移脚本 |
|
||||
| 2026-02-25 | Auth 注册切换为 OTP 三段式:signup/start、signup/verify、signup/resend;邮件模板改为纯验证码展示 |
|
||||
| 2026-02-25 | 清理未使用配置类:删除 WebSettings/GunicornSettings/WorkerSettings/WorkerGroupSettings(脚本仍使用环境变量启动服务) |
|
||||
| 2026-03-04 | Agent 运行时进入硬切重构:移除旧 Agent Chat 验证章节,待新方案落地后补充 |
|
||||
| 2026-02-25 | 简化启动方式:dev-app-up -> app-up,分离 bootstrap 与服务启动 |
|
||||
| 2026-02-25 | 重构为运维分层手册:Bootstrap Gate、分层验证、故障与回滚流程 |
|
||||
| 2026-02-25 | 新增配置漂移故障条目:修复 Auth 邮件模板失效与 signup 超时场景 |
|
||||
| 2026-02-27 | 用户搜索支持邮箱精确匹配:query 含 @ 符号时走 auth.users → profiles 两步查询 |
|
||||
| 2026-02-28 | 邀请码功能:新增 invite_codes 表、profiles.referred_by,注册时可选填邀请码并记录邀请关系 |
|
||||
| 2026-03-02 | 文档整理:修正 auth 端点名称(/verifications)、补充 profile 路由文档、修复 L2/L3 验证命令 |
|
||||
| 2026-03-02 | 修正 bootstrap 命令:init-job 需要使用 `uv run python -m core.runtime.cli bootstrap` |
|
||||
| 2026-03-05 | 新增 Agent Runtime run/resume/events 运维排障流程(Taskiq + Redis + Last-Event-ID) |
|
||||
| 2026-03-06 | Web 启动从 gunicorn 迁移为纯 uvicorn,移除 `SOCIAL_WEB__GUNICORN__*` 配置,统一使用 `SOCIAL_WEB__WORKERS` |
|
||||
| 2026-03-09 | 清理本地 Supabase 依赖描述:基础设施启动与巡检统一为 redis/db/web |
|
||||
Reference in New Issue
Block a user