docs: cleanup old plans and add new design docs

This commit is contained in:
qzl
2026-02-26 11:21:27 +08:00
parent 656b2a1793
commit 8294c67d27
12 changed files with 981 additions and 3207 deletions
@@ -1,136 +0,0 @@
# Auth Profile Enhancement Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 基于 Supabase 能力补齐注册/登录/按邮箱查用户/更新 profile 的一致化实现,并移除 profiles.display_name。
**Architecture:** 认证流程继续走 Supabase Authsignup/login/refresh/logout),后端仅做薄封装与输入校验。profiles 通过 auth.users 触发器自动创建并绑定同一 id,profile 资料更新仍走业务接口。新增按邮箱查用户接口走 service_role 的 Admin API。
**Tech Stack:** FastAPI, Supabase Python SDK, SQLAlchemy, Alembic, PostgreSQL
---
### Task 1: 调整 profiles 数据模型与迁移
**Files:**
- Modify: `backend/src/models/profile.py`
- Create: `backend/alembic/versions/20260224_drop_profile_display_name_and_trigger_username.py`
**Step 1: Write the failing test**
- 新增迁移验证脚本测试:断言 `profiles` 不含 `display_name` 且触发器使用 metadata.username。
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=src uv run python -m pytest tests/integration -k profile_migration -q`
Expected: FAIL(旧结构仍有 display_name 或触发器逻辑不匹配)
**Step 3: Write minimal implementation**
- model 删除 `display_name`
- 迁移删除列并重建触发器函数:`profiles.username = NEW.raw_user_meta_data->>'username'`
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=src uv run python -m pytest tests/integration -k profile_migration -q`
Expected: PASS
### Task 2: 注册接口改为 username+email+password
**Files:**
- Modify: `backend/src/v1/auth/schemas.py`
- Modify: `backend/src/v1/auth/service.py`
**Step 1: Write the failing test**
- 测试 signup 缺 username 返回 422
- 测试 signup 将 `data.username` 传递给 Supabase gateway
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k auth_signup -q`
Expected: FAIL
**Step 3: Write minimal implementation**
- `SignupRequest` 添加必填 `username`
- `SupabaseAuthGateway.signup` payload 增加 `data.username`
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k auth_signup -q`
Expected: PASS
### Task 3: 新增按邮箱查询 auth 用户接口
**Files:**
- Modify: `backend/src/v1/auth/router.py`
- Modify: `backend/src/v1/auth/service.py`
- Modify: `backend/src/v1/auth/schemas.py`
**Step 1: Write the failing test**
- 测试 `GET /auth/users/by-email` 命中返回用户最小字段
- 测试未命中返回 404
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k auth_by_email -q`
Expected: FAIL
**Step 3: Write minimal implementation**
- service 新增 `get_user_by_email`
- gateway 用 service_role client 调用 Supabase Admin 查询
- router 暴露 `GET /auth/users/by-email`
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k auth_by_email -q`
Expected: PASS
### Task 4: profile 更新协议去除 display_name
**Files:**
- Modify: `backend/src/v1/profile/schemas.py`
- Modify: `backend/src/v1/profile/service.py`
- Modify: `backend/src/v1/profile/router.py`
**Step 1: Write the failing test**
- 测试 `PATCH /profile/me` 仅允许 `username/avatar_url/bio`
**Step 2: Run test to verify it fails**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k profile_update -q`
Expected: FAIL
**Step 3: Write minimal implementation**
- 移除 display_name 字段与映射
- 保留原有更新路径和事务边界
**Step 4: Run test to verify it passes**
Run: `PYTHONPATH=src uv run python -m pytest tests/unit -k profile_update -q`
Expected: PASS
### Task 5: 集成验证
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 运行关键验证**
Run: `docker compose --env-file .env -f infra/docker/docker-compose.yml --profile job run --rm init-job`
Expected: 迁移成功
Run: `PYTHONPATH=src uv run python -m pytest tests -q`
Expected: 全部通过
**Step 2: 手工 API 验证**
- signup(username,email,password) 成功
- login(email,password) 成功
- by-email 查询命中
- patch profile 更新成功
@@ -1,528 +0,0 @@
# Backend Agent Chat Core (CrewAI + AG-UI) Implementation Plan
> 实施建议:按任务顺序执行,每个任务先测试失败再实现通过。
**Goal:** 为后端新增基于 AG-UI 协议的 Agent Chat 核心能力,使用 CrewAI 三阶段模型链路,支持成本追踪、会话落库、多模态输入与 ASR。
**Architecture:** 采用分层架构:`v1/agent_chat` 作为 AG-UI 协议边界,`core/agent_chat` 承担编排与能力层,`models + repository` 承担持久化。所有调用以 `sessions.id` 作为链路标识,并在调用级与会话级记录 token/cost。
**Tech Stack:** FastAPI, Pydantic, SQLAlchemy, Alembic, CrewAI, AG-UI 官方 CrewAI 集成与 Python SDK, DashScope Python SDK, Supabase Storage, pytest。
**Replaces:** `docs/plans/PLAN-agent-chat-crewai-ag-ui-2026-02-25.md`
---
## Scope
- In Scope
- CrewAI 三阶段链路:Intent -> Execution -> Organization
- LLM 成本采集与会话级聚合
- `llm_factory``llms``sessions``messages` 四张核心表
- AG-UI 协议路由与事件映射
- CrewAI 模板与静态配置
- 图片/音频/文本文档输入支持
- FunASR 工具接入 `fun-asr-realtime-2025-11-07`
- Out of Scope
- 前端 UI 改造
- 新增向量数据库与长期记忆系统
- 多租户账单结算系统
## Acceptance Criteria
- AG-UI 路由可完成文本和附件会话,并输出标准事件流。
- 三阶段模型可配置替换,并在会话中完整落库模型链路。
- 每次调用记录 token/cost,会话聚合成本可审计。
- `llm_factory` 初始包含 6 厂商,`llms` 初始包含 2 模型。
- CrewAI agents/tasks/workflow 模板可按配置加载并执行。
- ASR 工具可将音频转换文本并参与回答。
- 单测 + 集成 + E2E 通过,覆盖率达到仓库要求。
## Requirement Traceability Matrix
| Requirement | Description | Tasks | Tests |
|---|---|---|---|
| R1 | 三阶段模型(意图/执行/整理) | Task 5 | `backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py` |
| R2 | LLM 与工具调用成本采集(统一写入 messages | Task 5, Task 8 | `backend/tests/unit/core/agent_chat/test_cost_tracker.py`, `backend/tests/integration/test_agent_chat_session_persistence.py` |
| R3 | `llm_factory` + `llms` 表 | Task 2, Task 3 | `backend/tests/integration/test_agent_chat_migration.py`, `backend/tests/integration/test_agent_chat_seed_data.py` |
| R4 | `sessions` + `messages` 会话与历史落库 | Task 2, Task 8 | `backend/tests/integration/test_agent_chat_session_persistence.py` |
| R5 | AG-UI 协议路由与事件 | Task 6 | `backend/tests/unit/core/agent_chat/test_agui_adapter.py`, `backend/tests/integration/test_agent_chat_routes.py` |
| R6 | 三阶段 prompt/llm/tools 静态配置 | Task 4 | `backend/tests/unit/core/agent_chat/test_crewai_template_loader.py` |
| R7 | CrewAI templates 加载并供编排执行 | Task 4, Task 5 | `backend/tests/unit/core/agent_chat/test_crewai_template_loader.py`, `backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py` |
| R8 | 图片/音频/文档输入支持 | Task 7 | `backend/tests/unit/core/agent_chat/test_multimodal.py`, `backend/tests/e2e/test_agent_chat_flow.py` |
| R9 | FunASR 工具接入 qwen 模型 | Task 7 | `backend/tests/unit/core/agent_chat/test_asr_fun_asr_tool.py`, `backend/tests/e2e/test_agent_chat_flow.py` |
## Data Model Design
### llm_factory (LLM 厂商表)
继承 `TimestampMixin` + `SoftDeleteMixin`
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| id | UUID | PK, auto | 主键 |
| name | String(50) | UNIQUE, NOT NULL | 厂商名称:qwen/minimax/kimi/deepseek/doubao/zai |
| request_url | String(255) | NOT NULL | API 请求 URL |
| avatar | String(255) | NULL | 厂商图标 CDN URL |
| created_at | DateTime | NOT NULL, DEFAULT now | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| deleted_at | DateTime | NULL | 软删除时间 |
### llms (大模型表)
继承 `TimestampMixin` + `SoftDeleteMixin`
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| id | UUID | PK, auto | 主键 |
| factory_id | UUID | FK -> llm_factory(id), NOT NULL | 关联厂商 |
| model_code | String(50) | UNIQUE, NOT NULL | 模型代码:qwen3.5-flash、deepseek-v3.2 |
| created_at | DateTime | NOT NULL, DEFAULT now | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| deleted_at | DateTime | NULL | 软删除时间 |
### sessions (会话表)
继承 `TimestampMixin` + `SoftDeleteMixin`。表示一个完整的对话会话。
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| id | UUID | PK, auto | 主键 |
| user_id | UUID | FK -> users(id), NOT NULL | 关联用户 |
| title | String(255) | NULL | 会话标题(首条消息摘要) |
| status | Enum | DEFAULT 'pending' | 状态:pending/running/completed/failed |
| last_activity_at | DateTime | NOT NULL | 最近活跃时间(首页排序) |
| message_count | Integer | DEFAULT 0, CHECK >= 0 | 消息数量冗余字段 |
| total_tokens | Integer | DEFAULT 0, CHECK >= 0 | 会话总 token 冗余字段 |
| total_cost | Decimal(12,6) | DEFAULT 0, CHECK >= 0 | 会话总成本冗余字段 |
| created_at | DateTime | NOT NULL, DEFAULT now | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| deleted_at | DateTime | NULL | 软删除时间 |
**Indexes:**
- `idx_sessions_user_created`: (user_id, created_at DESC)
- `idx_sessions_user_last_activity`: (user_id, last_activity_at DESC)
### messages (对话历史表)
继承 `TimestampMixin` + `SoftDeleteMixin`。表示会话中的单条对话记录(一对多)。
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| id | UUID | PK, auto | 主键 |
| session_id | UUID | FK -> sessions(id), NOT NULL | 关联会话 |
| seq | Integer | NOT NULL | 会话内顺序号(唯一:session_id + seq |
| role | Enum | NOT NULL | 角色:user/assistant/system/tool |
| content | Text | NOT NULL | 对话内容 |
| model_code | String(50) | NULL | 使用的模型代码 |
| tool_name | String(100) | NULL | 若为工具结果消息,记录工具名 |
| input_tokens | Integer | DEFAULT 0, CHECK >= 0 | 输入 token 数 |
| output_tokens | Integer | DEFAULT 0, CHECK >= 0 | 输出 token 数 |
| cost | Decimal(12,6) | DEFAULT 0, CHECK >= 0 | 本条成本 |
| currency | String(3) | DEFAULT 'USD' | 货币单位 |
| latency_ms | Integer | NULL | 本条耗时(毫秒) |
| metadata | JSONB | NULL | 扩展字段(工具调用、附件信息等) |
| created_at | DateTime | NOT NULL, DEFAULT now | 创建时间 |
| updated_at | DateTime | NOT NULL | 更新时间 |
| deleted_at | DateTime | NULL | 软删除时间 |
**Indexes:**
- `idx_messages_session_created`: (session_id, created_at DESC)
- `idx_messages_session_role`: (session_id, role)
- `uq_messages_session_seq`: UNIQUE(session_id, seq)
## LLM Seed Configuration
- 配置文件位置:`backend/src/core/config/static/agent_chat/llm_catalog.yaml`
- 此文件作为 `init_data.py` 的唯一种子源,包含 `llm_factory``llms` 初始化数据。
```yaml
factories:
- name: qwen
request_url: https://dashscope.aliyuncs.com/compatible-mode/v1
avatar: https://cdn.simpleicons.org/alibabacloud/FF6A00
- name: minimax
request_url: https://api.minimax.chat/v1
avatar: https://cdn.simpleicons.org/minimax/1A1A1A
- name: kimi
request_url: https://api.moonshot.cn/v1
avatar: https://cdn.simpleicons.org/moonrepo/3B82F6
- name: deepseek
request_url: https://api.deepseek.com/v1
avatar: https://cdn.simpleicons.org/deepseek/4D6BFE
- name: doubao
request_url: https://ark.cn-beijing.volces.com/api/v3
avatar: https://cdn.simpleicons.org/volkswagen/001E50
- name: zai
request_url: https://api.z.ai/v1
avatar: https://cdn.simpleicons.org/zotero/CC2936
llms:
- model_code: qwen3.5-flash
factory_id: qwen
- model_code: deepseek-v3.2
factory_id: deepseek
```
- 说明:`llms.factory_id` 在配置中使用厂商 `name` 作为逻辑标识;`init_data.py` 先 upsert `llm_factory(name, request_url, avatar)`,再按 `name` 解析真实数据库 `llm_factory.id` 填充 `llms.factory_id`
## Attachment Storage Policy
- 用户上传附件与 AI 生成附件均保存到 **Supabase Storage**(对象存储),不存数据库二进制。
- 建议 bucket`agent-chat-attachments`(私有桶,签名 URL 短时访问)。
- 路径规范:`agent-chat/{user_id}/{session_id}/{message_seq}/{sha256}.{ext}`
- 访问规范:后端仅保存 `object_path`,读取时签发短时 URL(默认 10 分钟)。
- 生命周期:
- 原始附件保留 30 天(可配置)。
- 超期由定时任务清理对象并同步标记 `messages.metadata.attachments[].expired=true`
- 安全规范:
- 仅后端 service_role 可写对象。
- 前端不得直传 service_role;如需直传使用受限 upload token。
- 所有附件写入前必须完成 MIME/大小校验并记录 `checksum_sha256`
- 数据库存储位置:`messages.metadata.attachments[]`,保存以下元数据:
- `object_path`
- `mime_type`
- `size`
- `checksum_sha256`
- `origin`user_upload/assistant_output
- `preview_text`(可选,截断)
## Environment Variables Policy
- `.env.example` 不新增任何厂商 API 变量(qwen/minimax/kimi/deepseek/doubao/zai 均不写入)。
- `.env.example` 仅保留通用基础设施变量(本需求新增仅限存储相关):
- `SOCIAL_STORAGE__PROVIDER=supabase`
- `SOCIAL_STORAGE__BUCKET=agent-chat-attachments`
- `SOCIAL_STORAGE__SIGNED_URL_TTL_SECONDS=600`
- `SOCIAL_STORAGE__MAX_FILE_SIZE_MB=20`
- `SOCIAL_STORAGE__RETENTION_DAYS=30`
- 厂商密钥与 Base URL 通过部署平台密文配置注入,运行时统一由 `Settings()` 读取。
## CrewAI Template Configuration
- 模板目录:`backend/src/core/config/static/agent_chat/crewai/`
- 约定文件:
- `agents.yaml`intent/execution/organization 三个 agent 的 role/goal/backstory/default_model
- `tasks.yaml`:三阶段 task 描述、输入映射、输出 schema
- `workflow.yaml`:阶段顺序、短路规则、重试策略、超时策略
- `../tools.yaml`:可调用工具白名单与参数 schema(位于 `agent_chat` 根配置)
- `prompts/*.md`:各阶段 prompt 模板
## CrewAI <-> AG-UI Event Bridge
- 适配入口:`backend/src/core/agent_chat/agui_adapter.py`
- AG-UI 适配来源:优先使用 `ag-ui-protocol/ag-ui` 官方仓库中的 `integrations/crewai` 与 Python SDK(以官方发布版本为准,避免自定义协议漂移)。
- 运行流程:
1. 接收 AG-UI 请求并转为内部命令。
2. CrewAI 每个阶段通过回调产出内部事件(start/progress/tool/final/error)。
3. `agui_adapter.py` 将内部事件映射为 AG-UI 事件(`message.delta/tool.started/tool.completed/run.completed/run.failed`)。
4. `service.py` 在同一事务边界内写入 `messages`(含模型、token、cost、tool 信息),并增量更新 `sessions` 汇总字段。
5. 事件流输出与 DB 落库共用 `session.id + message.seq` 保证顺序一致。
## Session and Cost Rules
- Title 生成
- 在首条 `user` 消息写入后同步生成 `sessions.title`
- 策略:取首条用户消息前 24 个可见字符,去空白与换行,不调用额外 LLM。
- 若首条消息为空或仅附件:兜底为 `新对话 YYYY-MM-DD HH:MM`
- 工具调用记账
- 工具调用不单独建表,统一写入 `messages``role=tool`,并在 `metadata.tool` 记录 name/status/args_digest)。
- 若工具内部触发 LLM,请将 `input_tokens/output_tokens/cost` 写入该条 tool message。
- 会话级成本采用增量聚合:`sessions.total_cost = sum(messages.cost)`
- 失败调用也可记成本(例如上游已计费但业务失败),通过 `metadata.tool.status` 区分。
- 首页“最近打开会话”选择
- 定义:最近一次有消息或工具活动的会话(按 `sessions.last_activity_at DESC`)。
- 查询:`WHERE user_id=:uid AND deleted_at IS NULL ORDER BY last_activity_at DESC LIMIT 1`
- 会话列表同样按 `last_activity_at DESC` 分页,默认高亮首条结果。
## Naming and File Convention
- 文档命名统一:`docs/plans/YYYY-MM-DD-<topic>-plan.md`
- 本计划文件:`docs/plans/2026-02-25-agent-chat-crewai-ag-ui-plan.md`
- 配置目录统一:`backend/src/core/config/static/agent_chat/`
- 术语约定:代码实体统一使用 `AgentChatSession``AgentChatMessage`,避免与 SQLAlchemy `Session` 混淆。
## Milestones
1. M1: Spike 完成,依赖与接口可用
2. M2: 数据层落地(表、迁移、种子)
3. M3: 编排层与成本追踪可运行
4. M4: AG-UI 事件流打通
5. M5: 多模态 + ASR 打通
6. M6: 全链路验证 + 文档门禁
### Task 1: Spike 与接口基线确认
**Files:**
- Modify: `docs/plans/2026-02-25-agent-chat-crewai-ag-ui-plan.md`
- Create: `docs/plans/2026-02-25-agent-chat-crewai-ag-ui-spike-notes.md`
**Step 1: 写兼容性验证用例(文档化)**
- 验证点:`crewai`、AG-UI 官方 CrewAI 集成与 Python SDK、DashScope FunASR SDK 返回 usage 字段可得性。
**Step 2: 运行最小可用验证命令**
- Run: `uv run python -m pip show crewai`
- Expected: 能看到版本信息;若无则列入依赖安装任务。
**Step 3: 记录替代策略**
- 若 AG-UI 官方 CrewAI 集成不可用,保留最小自定义事件映射适配层(仅实现标准事件,不扩展私有字段)。
**Step 4: 提交 spike 结论到 notes**
### Task 2: 数据模型与迁移(llm_factory, llms, sessions, messages
**Files:**
- Create: `backend/src/models/llm_factory.py`
- Create: `backend/src/models/llm.py`
- Create: `backend/src/models/agent_chat_session.py`
- Create: `backend/src/models/agent_chat_message.py`
- Modify: `backend/src/models/__init__.py`
- Create: `backend/alembic/versions/<timestamp>_create_agent_chat_core_tables.py`
- Modify: `backend/alembic/env.py`
- Test: `backend/tests/integration/test_agent_chat_migration.py`
**Step 1: 先写失败的迁移测试**
- 覆盖:建表成功、索引存在、降级可回滚。
**Step 2: 运行测试确认失败**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_migration.py -v`
- Expected: FAIL(缺少模型或迁移)。
**Step 3: 实现 ORM 与 Alembic 迁移**
- `llm_factory`:唯一 `name`,新增 `request_url``avatar`,并继承 `TimestampMixin` + `SoftDeleteMixin`
- `llms`:关联 `factory_id`,仅保留 `model_code`,并继承 `TimestampMixin` + `SoftDeleteMixin`
- `sessions`:使用 `id` 作为链路标识,状态为 `pending/running/completed/failed`,维护 `last_activity_at``message_count``total_tokens``total_cost`
- `messages`:按 `session_id + seq` 一对多存储对话历史,记录单条模型与 token/cost;工具调用统一写 `role=tool` + `metadata.tool`
**Step 4: 重新运行测试确认通过**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_migration.py -v`
- Expected: PASS。
### Task 3: 初始化种子数据
**Files:**
- Create: `backend/src/core/config/static/agent_chat/llm_catalog.yaml`
- Modify: `backend/src/core/initialization/init_data.py`
- Test: `backend/tests/integration/test_agent_chat_seed_data.py`
**Step 1: 写失败测试**
- 断言 `llm_catalog.yaml` 可被加载,且 `factories` 包含 `name/request_url/avatar`
- 断言 `llms` 仅包含 `model_code/factory_id`(逻辑标识)。
- 断言落库后 `llm_factory` 包含:`qwen,minimax,kimi,deepseek,doubao,zai`
- 断言落库后 `llms` 包含:`qwen3.5-flash,deepseek-v3.2` 且能正确关联到厂商。
**Step 2: 跑测试确认失败**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_seed_data.py -v`
- Expected: FAIL。
**Step 3: 实现种子插入逻辑(幂等)**
-`backend/src/core/config/static/agent_chat/llm_catalog.yaml` 读取初始化数据。
- 先 upsert `llm_factory(name, request_url, avatar)`,再解析逻辑 `factory_id` 写入 `llms.factory_id`
**Step 4: 再跑测试确认通过**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_seed_data.py -v`
- Expected: PASS。
### Task 4: 静态配置与 CrewAI 模板加载器
**Files:**
- Create: `backend/src/core/config/static/agent_chat/crewai/agents.yaml`
- Create: `backend/src/core/config/static/agent_chat/crewai/tasks.yaml`
- Create: `backend/src/core/config/static/agent_chat/crewai/workflow.yaml`
- Create: `backend/src/core/config/static/agent_chat/crewai/prompts/intent.md`
- Create: `backend/src/core/config/static/agent_chat/crewai/prompts/execution.md`
- Create: `backend/src/core/config/static/agent_chat/crewai/prompts/organization.md`
- Create: `backend/src/core/config/static/agent_chat/tools.yaml`
- Create: `backend/src/core/agent_chat/crewai/template_loader.py`
- Modify: `backend/src/core/config/settings.py`
- Test: `backend/tests/unit/core/agent_chat/test_crewai_template_loader.py`
**Step 1: 写模板加载器失败测试**
- 覆盖:模板缺失、workflow 非法阶段、非法工具、合法加载。
**Step 2: 运行测试确认失败**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_crewai_template_loader.py -v`
- Expected: FAIL。
**Step 3: 实现配置文件与加载器**
- 支持按任务阶段加载 agent/task/workflow/prompt 与工具白名单。
**Step 4: 运行测试确认通过**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_crewai_template_loader.py -v`
- Expected: PASS。
### Task 5: CrewAI 编排与成本追踪
**Files:**
- Create: `backend/src/core/agent_chat/orchestrator.py`
- Create: `backend/src/core/agent_chat/cost_tracker.py`
- Create: `backend/src/core/agent_chat/events.py`
- Test: `backend/tests/unit/core/agent_chat/test_cost_tracker.py`
- Test: `backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py`
**Step 1: 写失败测试(成本计算与阶段顺序)**
- 验证调用级 usage 记录、会话级 total_cost 聚合。
**Step 2: 运行测试确认失败**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_cost_tracker.py backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py -v`
- Expected: FAIL。
**Step 3: 实现最小可用编排**
- Intent/Execution/Organization 三阶段串行。
- 每阶段记录 `model_id/factory_id/tokens/cost`
**Step 4: 运行测试确认通过**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_cost_tracker.py backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py -v`
- Expected: PASS。
### Task 6: AG-UI 路由与事件映射
**Files:**
- Create: `backend/src/core/agent_chat/agui_adapter.py`
- Create: `backend/src/core/agent_chat/event_bridge.py`
- Create: `backend/src/v1/agent_chat/schemas.py`
- Create: `backend/src/v1/agent_chat/service.py`
- Create: `backend/src/v1/agent_chat/dependencies.py`
- Create: `backend/src/v1/agent_chat/router.py`
- Modify: `backend/src/v1/router.py`
- Test: `backend/tests/unit/core/agent_chat/test_agui_adapter.py`
- Test: `backend/tests/unit/core/agent_chat/test_event_bridge.py`
- Test: `backend/tests/integration/test_agent_chat_routes.py`
- Test: `backend/tests/integration/test_agent_chat_event_persistence.py`
**Step 1: 写失败测试(事件映射)**
- 覆盖 `message.delta/tool.started/tool.completed/run.completed/run.failed`
**Step 2: 写失败测试(路由集成)**
- 覆盖鉴权、正常会话、错误返回、事件与 `messages.seq` 顺序一致。
**Step 3: 运行测试确认失败**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_agui_adapter.py backend/tests/integration/test_agent_chat_routes.py -v`
- Expected: FAIL。
**Step 4: 实现适配与路由**
- AG-UI 请求转换为内部 command。
- 使用 AG-UI 官方 CrewAI 集成 SDK 接收/发送标准事件,CrewAI 回调事件通过 `event_bridge.py` 统一转换。
- 内部事件转换为 AG-UI 事件流。
-`service.py` 中同事务写入 `messages` 并更新 `sessions.last_activity_at/message_count/total_tokens/total_cost`
**Step 5: 运行测试确认通过**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_agui_adapter.py backend/tests/unit/core/agent_chat/test_event_bridge.py backend/tests/integration/test_agent_chat_routes.py backend/tests/integration/test_agent_chat_event_persistence.py -v`
- Expected: PASS。
### Task 7: 多模态输入与 ASR 工具
**Files:**
- Create: `backend/src/core/agent_chat/multimodal.py`
- Create: `backend/src/core/agent_chat/storage_adapter.py`
- Create: `backend/src/core/agent_chat/tools/asr_fun_asr.py`
- Test: `backend/tests/unit/core/agent_chat/test_multimodal.py`
- Test: `backend/tests/unit/core/agent_chat/test_storage_adapter.py`
- Test: `backend/tests/unit/core/agent_chat/test_asr_fun_asr_tool.py`
**Step 1: 写失败测试(文件校验与解析)**
- 覆盖图片/音频/文档 MIME、大小、异常分支。
- 覆盖附件对象存储路径生成、签名 URL 获取与元数据回写 `messages.metadata.attachments`
**Step 2: 写失败测试(ASR 工具)**
- 覆盖 DashScope SDK 请求构造、响应解析、超时降级。
**Step 3: 运行测试确认失败**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_multimodal.py backend/tests/unit/core/agent_chat/test_storage_adapter.py backend/tests/unit/core/agent_chat/test_asr_fun_asr_tool.py -v`
- Expected: FAIL。
**Step 4: 实现最小可用功能**
- 音频走 DashScope Python SDK`fun-asr-realtime-2025-11-07`qwen)。
- 输出统一 `AttachmentContext` 结构。
- 附件二进制写入 Supabase Storage 私有桶 `agent-chat-attachments`,数据库仅写元数据。
**Step 5: 运行测试确认通过**
- Run: `uv run pytest backend/tests/unit/core/agent_chat/test_multimodal.py backend/tests/unit/core/agent_chat/test_storage_adapter.py backend/tests/unit/core/agent_chat/test_asr_fun_asr_tool.py -v`
- Expected: PASS。
### Task 8: 会话落库、可观测性与安全约束
**Files:**
- Modify: `backend/src/v1/agent_chat/service.py`
- Modify: `backend/src/core/agent_chat/orchestrator.py`
- Test: `backend/tests/unit/core/agent_chat/test_session_title_strategy.py`
- Test: `backend/tests/integration/test_agent_chat_session_recent_selection.py`
- Test: `backend/tests/integration/test_agent_chat_session_persistence.py`
**Step 1: 写失败测试(sessions 写入)**
- 断言 `session.id` 链路可追踪、`title/status/last_activity_at`、消息与工具成本明细均入库。
- 用例 A(title 生成):首条用户消息自动生成 `title`,超过长度截断,空文本回退 `新对话 YYYY-MM-DD HH:MM`
- 用例 B(工具成本聚合):工具调用写 `role=tool` 消息,`sessions.total_cost = sum(messages.cost)`,失败调用可记费。
- 用例 C(最近会话选择):同一用户多个会话按 `last_activity_at DESC` 返回首页默认会话。
**Step 2: 运行测试确认失败**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_session_persistence.py -v`
- Expected: FAIL。
**Step 3: 实现持久化与审计字段写入**
- 禁止明文存储密钥与敏感音频原始数据。
- 新增路由级限流与滥用保护策略(按用户或 token)。
- 附件审计日志记录拒绝原因(MIME、大小、扩展名、解析失败)。
**Step 4: 运行测试确认通过**
- Run: `uv run pytest backend/tests/integration/test_agent_chat_session_persistence.py -v`
- Expected: PASS。
### Task 9: 全链路验证与文档更新
**Files:**
- Create: `backend/tests/e2e/test_agent_chat_flow.py`
- Create: `backend/tests/e2e/test_agent_chat_recent_session_home.py`
- Modify: `.env.example`
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 写 E2E 失败测试**
- 场景:文本、图片+文本、音频+ASR、文档问答、最近会话首页默认选中。
**Step 2: 运行 E2E 确认失败**
- Run: `uv run pytest backend/tests/e2e/test_agent_chat_flow.py -v`
- Expected: FAIL(功能未完全联通)。
**Step 3: 修补缺口直至通过**
**Step 3.1: 更新环境变量样例**
- 仅新增存储相关变量到 `.env.example`,不新增任何厂商 API 变量。
**Step 4: 全量回归验证**
- Run: `bash infra/scripts/app-up.sh` (or `docker compose run --rm init-job bootstrap` for production)
- Run: `uv run pytest backend/tests/unit/core/agent_chat -v`
- Run: `uv run pytest backend/tests/integration -k agent_chat -v`
- Run: `uv run pytest backend/tests/e2e/test_agent_chat_flow.py -v`
- Expected: 全部 PASS。
**Step 5: 供应链与安全检查**
- Run: `uv run pip check`
- Run: `uv run pytest backend/tests/integration -k security -v`
- Expected: 依赖冲突为 0,关键安全测试通过。
## Dependency Graph
- Task 1 -> Task 2/4(先确认依赖与接口)
- Task 2 -> Task 3/5/8(数据层先行)
- Task 4 -> Task 5(编排依赖静态配置与 CrewAI 模板)
- Task 5 -> Task 6/7(路由与多模态依赖编排核心)
- Task 6 + Task 7 + Task 8 -> Task 9(全链路验证)
## Risk Controls
- AG-UI 官方 CrewAI 集成版本波动:固定版本并保留 `agui_adapter.py` 最小兜底映射。
- usage 字段不一致:`cost_tracker.py` 保留 `raw_usage` 并标准化。
- 附件安全:严格 MIME/大小限制,禁止敏感原文落库。
- 路由滥用风险:对 `agent_chat` 接口增加限流与失败惩罚策略。
- 供应链风险:新增依赖必须完成许可证和漏洞扫描。
- 延迟风险:三阶段支持短路策略(简单意图可跳过整理阶段)。
## Verification Checklist
- [ ] Alembic upgrade/downgrade 成功
- [ ] init-data 幂等且种子完整
- [ ] AG-UI 事件流字段符合标准
- [ ] sessions 审计字段完整
- [ ] 多模态与 ASR 路径可用
- [ ] 覆盖率与关键测试通过
@@ -1,36 +0,0 @@
# Agent Chat CrewAI + AG-UI Spike Notes
## Scope
- 验证 CrewAI 依赖可用性与版本探测方式。
- 验证 AG-UI 官方 CrewAI 集成在当前仓库中的落地路径。
- 验证 DashScope FunASR 响应中的 usage 字段可得性与兜底策略。
## Findings
### CrewAI
- `uv run python -m pip show crewai` 在当前虚拟环境不可用(无 pip 模块)。
- `uv pip show crewai` 返回未安装,说明当前工作树尚未安装 CrewAI 依赖。
- 若需启用真实编排,需在 `pyproject.toml` 中声明依赖并执行 `uv sync --extra dev`
### AG-UI 官方 CrewAI 集成
- 目标对齐官方标准事件语义(如 `message.delta``tool.started``tool.completed``run.completed``run.failed`)。
- 当前仓库采取“适配层隔离”策略:由 `agui_adapter.py` 进行请求与事件映射,避免协议细节扩散到业务层。
### DashScope FunASR
- 优先读取上游响应 usage 字段用于成本统计。
- 若 usage 缺失,落库时保持 `raw_usage` 与空标准字段,并标记 `metadata.usage_missing=true` 以便审计。
## Fallback Strategy
- 当官方集成能力或版本存在不确定性时,启用最小兜底事件映射:
- 仅输出标准 AG-UI 事件。
- 不扩展私有协议字段。
-`event_bridge.py` 中统一做字段校验与错误转换。
## Decision
- 继续按计划推进:先补齐编排与成本核心,再完善 AG-UI 适配、多模态与 E2E 闭环。
@@ -1,49 +0,0 @@
# Agent Chat Gap Closure Design
**Goal:** 在不重做已完成任务的前提下,按既定 Task 顺序补齐 Agent Chat Core 的缺口,实现可验证、可审计的端到端闭环。
## Current State
- 已完成:Task 2/3/4 的核心数据层、静态配置、模板加载;Task 6/7 的部分骨架(`event_bridge``v1/agent_chat``storage_adapter``asr_fun_asr`)。
- 未完成或缺口:Task 1 的 spike 结论文档;Task 5 编排与成本追踪;Task 6 `agui_adapter` 与缺失测试;Task 7 `multimodal`Task 8 会话审计与 recent 规则;Task 9 E2E 与运行文档闭环。
## Design Decisions
- 以“缺口优先”方式执行:仅新增/修改缺失能力,已稳定模块不重构。
- 严格遵循顺序:Task 1 -> 5 -> 6 -> 7 -> 8 -> 9。
- 每个 Task 均采用 TDD:先写失败测试,再做最小实现,通过后再小步重构。
- 统一事件与持久化顺序:以 `session.id + seq` 为唯一顺序锚点,避免流式输出与落库顺序漂移。
- 工具调用成本仍归集到 `messages(role=tool)`,会话总成本由增量聚合维护。
## Component Plan
- Task 1: 新增 spike notes,记录 CrewAI/AG-UI/FunASR 依赖可用性与兜底策略。
- Task 5: 新增 `orchestrator.py``cost_tracker.py``events.py`,完成三阶段执行与 usage/cost 归一。
- Task 6: 新增 `agui_adapter.py`,对接现有 `event_bridge.py``v1/agent_chat/service.py`
- Task 7: 新增 `multimodal.py`,衔接附件校验、存储元数据、ASR 文本提取。
- Task 8: 增强会话标题策略、recent session 查询、审计字段与限流保护。
- Task 9: 补齐 E2E 与 runbook,执行 bootstrap gate + 分层测试验证。
## Data Flow
1. 路由接收 AG-UI 请求并解析输入文本/附件。
2. `agui_adapter` 生成内部命令并触发编排器三阶段执行。
3. 每阶段产出内部事件,经 `event_bridge` 映射为 AG-UI 标准事件。
4. `service` 在事务内写入 `messages` 并更新 `sessions` 汇总字段。
5. 流式事件向外输出,顺序与 `messages.seq` 保持一致。
## Error Handling
- 配置/模板错误:启动前校验并快速失败,返回可追踪错误码。
- 第三方调用错误(LLM/ASR/Storage):记录标准化失败事件与审计元数据,不泄露敏感信息。
- 持久化冲突:对 `session_id + seq` 冲突执行有限重试并记录告警。
## Testing Strategy
- Unit`cost_tracker``orchestrator``agui_adapter``multimodal``title strategy`
- Integration`agent_chat` 路由、事件落库、recent session 选择、会话成本聚合。
- E2E:文本、图片+文本、音频+ASR、文档问答、首页最近会话默认选中。
## Approval Note
该设计基于用户确认的“仅按未完成 Task 顺序推进”执行策略。
@@ -1,230 +0,0 @@
# Agent Chat Gap Closure Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 按未完成 Task 顺序补齐 Agent Chat Core 缺口,形成可运行、可测试、可审计的后端链路。
**Architecture:** 复用已完成的数据层与路由骨架,在 `core/agent_chat` 补齐编排、成本与多模态能力,并通过 `v1/agent_chat/service.py` 统一持久化与事件顺序。全流程以 `session.id + messages.seq` 作为一致性锚点,保证事件输出与落库一致。
**Tech Stack:** FastAPI, SQLAlchemy, Pydantic, pytest, CrewAI, AG-UI adapter, DashScope SDK, Supabase Storage。
---
### Task 1: 补齐 Spike 结论文档
**Files:**
- Create: `docs/plans/2026-02-25-agent-chat-crewai-ag-ui-spike-notes.md`
**Step 1: 写失败校验(文档存在性)**
```bash
test -f docs/plans/2026-02-25-agent-chat-crewai-ag-ui-spike-notes.md
```
**Step 2: 运行并确认失败**
Run: `test -f docs/plans/2026-02-25-agent-chat-crewai-ag-ui-spike-notes.md`
Expected: non-zero exit code。
**Step 3: 写最小文档实现**
```markdown
- CrewAI 版本探测结论
- AG-UI 官方 CrewAI 集成可用性结论
- DashScope FunASR usage 字段策略
- 不可用时的最小兜底映射策略
```
**Step 4: 运行并确认通过**
Run: `test -f docs/plans/2026-02-25-agent-chat-crewai-ag-ui-spike-notes.md`
Expected: zero exit code。
### Task 5: 补齐编排与成本追踪
**Files:**
- Create: `backend/src/core/agent_chat/events.py`
- Create: `backend/src/core/agent_chat/cost_tracker.py`
- Create: `backend/src/core/agent_chat/orchestrator.py`
- Test: `backend/tests/unit/core/agent_chat/test_cost_tracker.py`
- Test: `backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py`
**Step 1: 写失败测试**
```python
def test_normalize_usage_and_cost_aggregation():
assert False
def test_orchestrator_runs_three_stages_in_order():
assert False
```
**Step 2: 运行并确认失败**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_cost_tracker.py backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py -v`
Expected: FAIL。
**Step 3: 写最小实现**
```python
class CostTracker:
def add_usage(self, usage: dict) -> None: ...
def total(self) -> dict: ...
class AgentChatOrchestrator:
async def run(self, command):
# intent -> execution -> organization
...
```
**Step 4: 运行并确认通过**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_cost_tracker.py backend/tests/unit/core/agent_chat/test_orchestrator_pipeline.py -v`
Expected: PASS。
### Task 6: 补齐 AG-UI 适配层缺口
**Files:**
- Create: `backend/src/core/agent_chat/agui_adapter.py`
- Modify: `backend/src/core/agent_chat/event_bridge.py`
- Modify: `backend/src/v1/agent_chat/service.py`
- Test: `backend/tests/unit/core/agent_chat/test_agui_adapter.py`
- Test: `backend/tests/integration/test_agent_chat_event_persistence.py`
**Step 1: 写失败测试**
```python
def test_agui_adapter_maps_internal_events_to_protocol_events():
assert False
```
**Step 2: 运行并确认失败**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_agui_adapter.py backend/tests/integration/test_agent_chat_event_persistence.py -v`
Expected: FAIL。
**Step 3: 写最小实现**
```python
class AguiAdapter:
def to_command(self, request): ...
def to_protocol_event(self, event): ...
```
**Step 4: 运行并确认通过**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_agui_adapter.py backend/tests/integration/test_agent_chat_event_persistence.py -v`
Expected: PASS。
### Task 7: 补齐多模态输入编排
**Files:**
- Create: `backend/src/core/agent_chat/multimodal.py`
- Modify: `backend/src/core/agent_chat/storage_adapter.py`
- Modify: `backend/src/core/agent_chat/tools/asr_fun_asr.py`
- Test: `backend/tests/unit/core/agent_chat/test_multimodal.py`
**Step 1: 写失败测试**
```python
def test_multimodal_validates_and_builds_attachment_context():
assert False
```
**Step 2: 运行并确认失败**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_multimodal.py -v`
Expected: FAIL。
**Step 3: 写最小实现**
```python
class MultimodalProcessor:
async def build_context(self, attachments): ...
```
**Step 4: 运行并确认通过**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_multimodal.py backend/tests/unit/core/agent_chat/test_storage_adapter.py backend/tests/unit/core/agent_chat/test_asr_fun_asr_tool.py -v`
Expected: PASS。
### Task 8: 补齐会话审计与 recent 规则
**Files:**
- Modify: `backend/src/v1/agent_chat/service.py`
- Modify: `backend/src/v1/agent_chat/router.py`
- Test: `backend/tests/unit/core/agent_chat/test_session_title_strategy.py`
- Test: `backend/tests/integration/test_agent_chat_session_recent_selection.py`
- Test: `backend/tests/integration/test_agent_chat_session_persistence.py`
**Step 1: 写失败测试**
```python
def test_title_generated_from_first_user_message():
assert False
def test_recent_session_selected_by_last_activity_at_desc():
assert False
```
**Step 2: 运行并确认失败**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_session_title_strategy.py backend/tests/integration/test_agent_chat_session_recent_selection.py backend/tests/integration/test_agent_chat_session_persistence.py -v`
Expected: FAIL。
**Step 3: 写最小实现**
```python
def build_session_title(first_message: str) -> str: ...
```
**Step 4: 运行并确认通过**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat/test_session_title_strategy.py backend/tests/integration/test_agent_chat_session_recent_selection.py backend/tests/integration/test_agent_chat_session_persistence.py -v`
Expected: PASS。
### Task 9: 补齐 E2E 与运行文档闭环
**Files:**
- Create: `backend/tests/e2e/test_agent_chat_flow.py`
- Create: `backend/tests/e2e/test_agent_chat_recent_session_home.py`
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 写失败 E2E 用例**
```python
def test_agent_chat_text_image_audio_document_flow():
assert False
```
**Step 2: 运行并确认失败**
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py -v`
Expected: FAIL。
**Step 3: 写最小实现与文档补充**
```markdown
- bootstrap gate 执行顺序
- agent_chat 验证命令
```
**Step 4: 全量验证**
Run: `make runtime-bootstrap-gate`
Expected: bootstrap 通过。
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/unit/core/agent_chat -v`
Expected: PASS。
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/integration -k agent_chat -v`
Expected: PASS。
Run: `PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py backend/tests/e2e/test_agent_chat_recent_session_home.py -v`
Expected: PASS。
Run: `PYTHONPATH=backend/src uv run pip check`
Expected: no broken requirements。
@@ -1,85 +0,0 @@
# Auth Signup OTP Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将注册流程改为两阶段 OTPstart/verify/resend),并移除旧 `/auth/signup` 路由。
**Architecture:** 后端继续作为 Supabase Auth 的薄封装层。`signup/start` 只创建待验证用户并触发验证码邮件;`signup/verify` 通过 `verifyOtp(type=signup)` 完成验证并返回 token`signup/resend` 负责重发验证码。保留现有 token 响应模型,最小化客户端和网关改造。
**Tech Stack:** FastAPI, Pydantic, supabase-py, pytest
---
### Task 1: 更新认证 Schema
**Files:**
- Modify: `backend/src/v1/auth/schemas.py`
- Test: `backend/tests/unit/v1/auth/test_auth_models.py`
**Step 1: Write the failing test**
-`SignupStartRequest``SignupVerifyRequest``SignupResendRequest` 增加字段校验测试。
**Step 2: Run test to verify it fails**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/unit/v1/auth/test_auth_models.py -q`
**Step 3: Write minimal implementation**
- 新增 start/verify/resend 的请求与响应模型。
**Step 4: Run test to verify it passes**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/unit/v1/auth/test_auth_models.py -q`
### Task 2: 改造 Service/Gateway 为三阶段 OTP
**Files:**
- Modify: `backend/src/v1/auth/service.py`
- Modify: `backend/src/v1/auth/gateway.py`
- Test: `backend/tests/unit/v1/auth/test_auth_service.py`
**Step 1: Write the failing test**
-`signup_start/signup_verify/signup_resend` 增加 service 转发与 gateway Supabase 调用行为测试。
**Step 2: Run test to verify it fails**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/unit/v1/auth/test_auth_service.py -q`
**Step 3: Write minimal implementation**
- 删除旧 `signup` 入口,新增三个方法。
- `signup_verify` 使用 `verify_otp` 并返回 `AuthTokenResponse`
- `signup_resend` 调用 `resend(type=signup)` 并返回通用消息。
**Step 4: Run test to verify it passes**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/unit/v1/auth/test_auth_service.py -q`
### Task 3: 替换 Router 路由并删除旧 signup
**Files:**
- Modify: `backend/src/v1/auth/router.py`
- Test: `backend/tests/integration/test_auth_routes.py`
- Test: `backend/tests/e2e/test_auth_flow.py`
**Step 1: Write the failing test**
- 集成测试改为 `/auth/signup/start``/auth/signup/verify``/auth/signup/resend`
- 删除对旧 `/auth/signup` 的断言。
**Step 2: Run test to verify it fails**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/integration/test_auth_routes.py -q`
**Step 3: Write minimal implementation**
- Router 新增三条路由并移除旧 `/signup`
- 保持 RFC7807 错误映射行为。
**Step 4: Run test to verify it passes**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/integration/test_auth_routes.py -q`
### Task 4: 全量验证
**Files:**
- Test: `backend/tests/unit/v1/auth/test_auth_models.py`
- Test: `backend/tests/unit/v1/auth/test_auth_service.py`
- Test: `backend/tests/integration/test_auth_routes.py`
- Test: `backend/tests/e2e/test_auth_flow.py`
**Step 1: Run focused suite**
- Run: `PYTHONPATH=backend/src uv run python -m pytest backend/tests/unit/v1/auth backend/tests/integration/test_auth_routes.py backend/tests/e2e/test_auth_flow.py -q`
**Step 2: Report evidence**
- 记录通过/失败数量与关键行为验证结果。
@@ -1,235 +0,0 @@
# Flutter Auth Integration Design
Date: 2026-02-25
Status: Approved
## Summary
Integrate Flutter mobile app with backend auth APIs, including signup, login, token management, and route protection.
## Scope
- Signup flow: `/auth/signup/start` + `/auth/signup/verify`
- Login flow: `/auth/login`
- Token management: access token, refresh token, secure storage
- Route protection: redirect unauthenticated users
Out of scope:
- OTP login (removed from UI)
- Password reset
- Social login
## Architecture
### Directory Structure
```
lib/
├── core/
│ ├── api/
│ │ ├── api_client.dart # dio wrapper
│ │ ├── api_interceptor.dart # token injection, refresh logic
│ │ └── api_exception.dart # unified error types
│ ├── storage/
│ │ └── token_storage.dart # flutter_secure_storage wrapper
│ └── di/
│ └── injection.dart # get_it configuration
├── features/auth/
│ ├── data/
│ │ ├── models/
│ │ │ ├── signup_request.dart
│ │ │ ├── login_request.dart
│ │ │ └── auth_response.dart
│ │ ├── auth_api.dart # API interface
│ │ ├── auth_repository.dart # abstract interface
│ │ └── auth_repository_impl.dart # implementation
│ ├── presentation/
│ │ ├── bloc/
│ │ │ ├── auth_bloc.dart
│ │ │ ├── auth_event.dart
│ │ │ └── auth_state.dart
│ │ └── cubits/
│ │ ├── register_cubit.dart # signup form state
│ │ └── login_cubit.dart # login form state
│ └── ui/screens/ # existing UI adjustments
└── main.dart # DI initialization
```
### API Layer
**ApiInterceptor responsibilities**:
1. Auto-inject `Authorization: Bearer <access_token>`
2. On 401 response, auto-refresh token and retry
3. If refresh fails, trigger logout
**AuthApi interface**:
```dart
abstract class AuthApi {
Future<AuthResponse> signupStart(SignupStartRequest request);
Future<AuthResponse> signupVerify(SignupVerifyRequest request);
Future<AuthResponse> signupResend(String email);
Future<AuthResponse> login(LoginRequest request);
Future<AuthResponse> refresh(String refreshToken);
Future<void> logout(String refreshToken);
}
```
**TokenStorage interface**:
```dart
abstract class TokenStorage {
Future<String?> getAccessToken();
Future<String?> getRefreshToken();
Future<void> saveTokens({required String access, required String refresh});
Future<void> clear();
}
```
**Error handling**:
- `ApiException` wraps network errors, timeouts, 4xx/5xx
- UI layer displays user-friendly messages via `ApiException`
### State Management
**AuthBloc (global auth state)**:
```dart
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
final String accessToken;
}
class AuthUnauthenticated extends AuthState {}
class AuthLoading extends AuthState {}
// Events
class AuthStarted extends AuthEvent {} // Check token on app start
class AuthLoggedIn extends AuthEvent {} // Login success
class AuthLoggedOut extends AuthEvent {} // Logout
class AuthTokenRefreshed extends AuthEvent {} // Token refreshed
```
**RegisterCubit (signup form)**:
```dart
class RegisterState {
final Username username; // formz input
final Email email;
final Password password;
final FormzSubmissionStatus status;
final String? errorMessage;
final String? pendingEmail; // stored after signup/start success
}
```
**LoginCubit (login form)**:
```dart
class LoginState {
final Email email;
final Password password;
final FormzSubmissionStatus status;
final String? errorMessage;
}
```
### UI Adjustments
**Signup flow changes**:
| Original | New |
|----------|-----|
| Step1: nickname + email | Step1: username + email + password -> call `/signup/start` |
| Step2: password + code + invite | Step2: code + invite(kept) -> call `/signup/verify` |
**Login flow changes**:
- Remove "Login with OTP" button
- `login_email_screen.dart` -> `login_password_screen.dart` passes email
- Password page calls `/login`, navigates to `/home` on success
**Route protection**:
- Use `GoRouter` `redirect` logic
- Unauthenticated user accessing protected route -> redirect to `/`
- Authenticated user accessing `/login` or `/register` -> redirect to `/home`
```dart
redirect: (context, state) {
final isAuthenticated = // check AuthBloc state;
final isAuthRoute = state.matchedLocation.startsWith('/login')
|| state.matchedLocation.startsWith('/register');
if (!isAuthenticated && !isAuthRoute) return '/';
if (isAuthenticated && isAuthRoute) return '/home';
return null;
}
```
### Dependency Injection
**DI configuration**:
```dart
final sl = GetIt.instance;
Future<void> configureDependencies() async {
// Core
sl.registerSingleton<Dio>(Dio(BaseOptions(baseUrl: env.apiUrl)));
sl.registerSingleton<TokenStorage>(SecureTokenStorage());
sl.registerSingleton<ApiClient>(ApiClient(sl(), sl()));
// Auth
sl.registerSingleton<AuthApi>(AuthApiImpl(sl()));
sl.registerSingleton<AuthRepository>(AuthRepositoryImpl(sl(), sl()));
sl.registerSingleton<AuthBloc>(AuthBloc(sl()));
}
```
**App initialization flow**:
```dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
final authBloc = sl<AuthBloc>();
authBloc.add(AuthStarted()); // Check local token
runApp(MyApp(authBloc: authBloc));
}
```
**Complete flow**:
```
App Start
|
AuthStarted event
|
Check TokenStorage
|
Has token? --No--> AuthUnauthenticated -> Show login
|
Yes
|
Validate token (call /auth/refresh)
|
Success? --No--> Clear token -> AuthUnauthenticated
|
Yes
|
AuthAuthenticated -> Navigate to /home
```
## Decisions
1. **Adjust Flutter UI to match backend API** - Move password to Step1, call `/signup/start` with all data
2. **Keep invite code UI but don't send** - Backend doesn't support yet, preserve UI for future
3. **Remove OTP login entry** - Backend doesn't have OTP login API
4. **Use complete Bloc architecture** - AuthBloc + Cubits for forms, better testability and extensibility
## Risks
- Token refresh race conditions: ApiInterceptor should handle concurrent requests during refresh
- Secure storage availability: fallback to SharedPreferences on platforms without secure storage
## Testing Strategy
- Unit tests for: Cubits, AuthBloc, Repository, API client
- Widget tests for: form validation, error display
- Integration tests for: complete signup/login flows
File diff suppressed because it is too large Load Diff
@@ -1,100 +0,0 @@
# Runtime Runbook Optimization Design
**Date:** 2026-02-25
**Audience:** 运维 / 后端值班同学
## Goal
`docs/runtime/runtime-runbook.md` 从“开发流程说明”升级为“运维可直接执行手册”,围绕上线、巡检、故障处置与回滚提供可操作步骤与判定标准,减少值班期间的判断成本与误操作风险。
## Non-Goals
- 不改动运行时代码行为。
- 不新增部署工具,仅整理现有脚本和命令。
- 不引入额外平台依赖。
## Current Gaps
- 结构偏开发说明,缺少运维分层(前置检查、门禁、验证、故障、回滚)。
- 生产章节仍有占位内容(TODO),缺乏可执行流程。
- 验证步骤有命令,但缺少“通过判定”与执行优先级。
- 部分历史命名已变化(`dev-app-up` -> `app-up`),需要统一语义与历史记录。
## Target Structure
1. Scope & Preconditions
2. Bootstrap Gate (Mandatory)
3. Service Start/Stop (tmux)
4. Operational Verification (L1/L2/L3)
5. Incident Playbook
6. Rollback Procedure
7. Change Log
## Design Principles
- **Command-first**: 每一步先给命令,再给判定标准。
- **Fail-fast**: 关键步骤失败即停止,禁止跳过门禁。
- **Ops-centric**: 按值班动作组织,而非按模块组织。
- **Consistency**: 统一 compose 与环境变量书写方式。
## Section-Level Content Design
### Scope & Preconditions
- 说明手册适用场景:开发环境值班、发布前检查、生产排障参考。
- 明确前置依赖:Docker、tmux、`uv``.env`
- 明确禁止动作:未完成 bootstrap gate 前禁止启动业务进程。
### Bootstrap Gate
- 固定顺序:
1) 启动基础设施
2) 执行 bootstrap / init-job(带 `--build`
3) 校验版本与关键表状态
- 对每一步给“成功判定”。
### Service Start/Stop
- 使用 `infra/scripts/app-up.sh` 作为唯一运维入口。
- 标准化 tmux 管理命令(list / attach / kill)。
- 给出日志文件映射,覆盖 web + 3 worker。
### Operational Verification
- 分层验证:
- **L1 必跑**health、compose 状态、核心 API smoke。
- **L2 可选**Auth/Profile 链路。
- **L3 可选**Agent Chat 相关回归命令。
- 每层包含通过判定。
### Incident Playbook
- 按故障模式给“症状 -> 定位 -> 修复”:
- 迁移未生效(镜像旧)
- worker 不消费
- JWT/模板配置异常
- agent-chat 依赖或迁移异常
### Rollback Procedure
- 给出回滚前检查、回滚执行、回滚后复核三段流程。
- 标明数据风险提醒与必要确认点。
## Validation Plan
- 对照现有脚本与路径逐条校对命令可用性:
- `infra/scripts/app-up.sh`
- `infra/docker/docker-compose.yml`
- `backend/src/core/runtime/cli.py`
- 至少执行 shell 语法检查与关键命令可达性检查。
## Risks
- 若未来启动脚本或 compose profile 变更,runbook 会过期。
- 回滚流程依赖实际数据库策略,若策略变化需同步修订。
## Acceptance Criteria
- 手册不再包含 TODO 占位段。
- 运维值班可按章节完成从启动到验证再到故障处置。
- 所有命令路径与脚本名称与仓库现状一致。
@@ -1,132 +0,0 @@
# Runtime Runbook Optimization Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:**`docs/runtime/runtime-runbook.md` 重构为面向运维的可执行手册,覆盖门禁、启动、验证、故障与回滚全流程。
**Architecture:** 保持单文档模式,在不改变脚本和运行时代码的前提下重排章节与命令。先做命令基线校对,再做文档结构重构,最后执行可达性验证并提交。所有命令以仓库现有脚本和 compose 路径为准。
**Tech Stack:** Markdown, Bash, Docker Compose, tmux, uv。
---
### Task 1: 命令与脚本基线核对
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`
- Verify: `infra/scripts/app-up.sh`
**Step 1: 写失败校验(当前 runbook 存在 TODO 与历史表述)**
```bash
grep -n "TODO\|dev-app-up" docs/runtime/runtime-runbook.md
```
**Step 2: 运行并确认失败**
Run: `grep -n "TODO\|dev-app-up" docs/runtime/runtime-runbook.md`
Expected: 命中至少 1 条(表示需重构)。
**Step 3: 写最小实现(命令映射清单)**
```markdown
- 启动脚本统一为 infra/scripts/app-up.sh
- bootstrap 命令统一为 docker compose --env-file .env -f infra/docker/docker-compose.yml ...
- 迁移/初始化强调 init-job --build
```
**Step 4: 运行验证**
Run: `bash -n infra/scripts/app-up.sh`
Expected: exit 0。
### Task 2: 文档结构重构为运维分层
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 写失败校验(缺失目标章节)**
```bash
grep -n "Bootstrap Gate\|Operational Verification\|Incident Playbook\|Rollback" docs/runtime/runtime-runbook.md
```
**Step 2: 运行并确认失败**
Run: `grep -n "Bootstrap Gate\|Operational Verification\|Incident Playbook\|Rollback" docs/runtime/runtime-runbook.md`
Expected: 命中不完整或为空。
**Step 3: 写最小实现(章节重排)**
```markdown
1. Scope & Preconditions
2. Bootstrap Gate (Mandatory)
3. Service Start/Stop
4. Operational Verification (L1/L2/L3)
5. Incident Playbook
6. Rollback Procedure
```
**Step 4: 运行验证**
Run: `grep -n "Bootstrap Gate\|Operational Verification\|Incident Playbook\|Rollback Procedure" docs/runtime/runtime-runbook.md`
Expected: 4 个目标章节都能命中。
### Task 3: 补齐运维验证与故障处理细则
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 写失败校验(缺少通过判定)**
```bash
grep -n "通过标准\|判定" docs/runtime/runtime-runbook.md
```
**Step 2: 运行并确认失败**
Run: `grep -n "通过标准\|判定" docs/runtime/runtime-runbook.md`
Expected: 命中不足。
**Step 3: 写最小实现(每段加判定)**
```markdown
- L1 必跑:health/compose/smoke + 通过标准
- L2 可选:auth/profile + 通过标准
- L3 可选:agent_chat tests + 通过标准
- 故障条目:症状/定位/修复
```
**Step 4: 运行验证**
Run: `grep -n "L1 必跑\|L2 可选\|L3 可选\|通过标准" docs/runtime/runtime-runbook.md`
Expected: 关键段落均命中。
### Task 4: 收尾校验与提交
**Files:**
- Modify: `docs/runtime/runtime-runbook.md`
**Step 1: 运行文档语义检查(关键命令可达)**
```bash
bash -n infra/scripts/app-up.sh
PYTHONPATH=backend/src uv run python -c "import core.runtime.cli"
```
**Step 2: 运行并确认通过**
Run: `bash -n infra/scripts/app-up.sh`
Expected: exit 0。
Run: `PYTHONPATH=backend/src uv run python -c "import core.runtime.cli"`
Expected: 无报错并 exit 0。
**Step 3: 提交**
```bash
git add docs/runtime/runtime-runbook.md \
docs/plans/2026-02-25-runtime-runbook-optimization-design.md \
docs/plans/2026-02-25-runtime-runbook-optimization-implementation-plan.md
git commit -m "docs(runtime): optimize runbook for ops workflow"
```
@@ -0,0 +1,404 @@
# 注册验证码流程 UX 优化实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 优化注册流程的界面响应速度和重发验证码按钮的交互体验
**Architecture:** 乐观跳转策略 + Timer 倒计时状态管理,后台异步发送验证码
**Tech Stack:** Flutter, dart:async Timer, flutter_bloc
---
## Task 1: 扩展 RegisterCubit 状态
**Files:**
- Modify: `apps/lib/features/auth/presentation/cubits/register_cubit.dart`
- Modify: `apps/test/features/auth/presentation/cubits/register_cubit_test.dart`
**Step 1: 添加 isSending 状态字段**
`RegisterState` 中添加 `isSending` 字段:
```dart
// RegisterState
final bool isSending;
const RegisterState({
// ... existing fields
this.isSending = false,
});
// copyWith
bool? isSending,
// copyWith return
isSending: isSending ?? this.isSending,
// props
isSending,
```
**Step 2: 添加 sendCodeSilently 方法**
`RegisterCubit` 中添加不阻塞的发送方法:
```dart
Future<void> sendCodeSilently() async {
if (!state.isStep1Valid) return;
emit(state.copyWith(isSending: true));
try {
final response = await _repository.signupStart(
SignupStartRequest(
username: state.username.value,
email: state.email.value,
password: state.password.value,
),
);
emit(
state.copyWith(
isSending: false,
pendingEmail: response.email,
codeSent: true,
errorMessage: null,
),
);
} catch (e) {
final message = e is ApiException ? e.message : '验证码发送失败,请重试';
emit(
state.copyWith(
isSending: false,
errorMessage: message,
),
);
}
}
```
**Step 3: 添加测试**
```dart
// 在 register_cubit_test.dart 添加
group('sendCodeSilently', () {
blocTest<RegisterCubit, RegisterState>(
'sets isSending to true then false on success',
build: () => cubit,
seed: () => RegisterState(
username: const Username.dirty('testuser'),
email: const Email.dirty('test@example.com'),
password: const Password.dirty('password123'),
),
setUp: () {
when(() => mockRepository.signupStart(any()))
.thenAnswer((_) async => SignupStartResponse(email: 'test@example.com'));
},
act: (c) => c.sendCodeSilently(),
verify: (_) {
verify(() => mockRepository.signupStart(any())).called(1);
},
);
});
```
**Step 4: 运行测试**
Run: `cd apps && flutter test test/features/auth/presentation/cubits/register_cubit_test.dart`
Expected: PASS
**Step 5: Commit**
```bash
git add apps/lib/features/auth/presentation/cubits/register_cubit.dart apps/test/features/auth/presentation/cubits/register_cubit_test.dart
git commit -m "feat(auth): add sendCodeSilently with isSending state"
```
---
## Task 2: 实现乐观跳转
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart`
**Step 1: 修改 _handleNext 方法**
将同步等待改为乐观跳转:
```dart
Future<void> _handleNext() async {
final cubit = context.read<RegisterCubit>();
cubit.usernameChanged(_nicknameController.text);
cubit.emailChanged(_emailController.text);
cubit.passwordChanged(_passwordController.text);
if (!cubit.state.isStep1Valid) {
return;
}
// 乐观跳转:立即跳转到验证码界面
if (mounted) {
context.push('/register/verification', extra: cubit);
}
// 后台发送验证码
cubit.sendCodeSilently();
}
```
**Step 2: Commit**
```bash
git add apps/lib/features/auth/ui/screens/register_screen.dart
git commit -m "feat(auth): optimistic navigation to verification screen"
```
---
## Task 3: 实现倒计时状态管理
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
**Step 1: 添加 Timer 状态**
`_RegisterVerificationViewState` 中添加:
```dart
import 'dart:async';
// 状态变量
Timer? _countdownTimer;
int _countdown = 60;
@override
void initState() {
super.initState();
_startCountdown();
}
@override
void dispose() {
_countdownTimer?.cancel();
_codeController.dispose();
super.dispose();
}
void _startCountdown() {
setState(() {
_countdown = 60;
});
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown > 0) {
setState(() {
_countdown--;
});
} else {
timer.cancel();
}
});
}
```
**Step 2: Commit**
```bash
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
git commit -m "feat(auth): add countdown timer for resend button"
```
---
## Task 4: 优化重发按钮样式
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
**Step 1: 重构 _buildCodeInput 方法**
替换重发按钮部分:
```dart
Widget _buildCodeInput(RegisterState state) {
final canResend = _countdown == 0 && state.status != FormzSubmissionStatus.inProgress;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'邮箱验证码',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF475569),
),
),
const SizedBox(height: 6),
Row(
children: [
Expanded(
child: SizedBox(
height: 40,
child: TextField(
controller: _codeController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
hintText: '输入验证码',
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
),
),
const SizedBox(width: 8),
_buildResendButton(canResend),
],
),
],
);
}
Widget _buildResendButton(bool canResend) {
final borderColor = canResend ? AppColors.primary : AppColors.slate300;
final textColor = canResend ? AppColors.primary : AppColors.slate400;
final text = canResend ? '重新发送' : '$_countdown s';
return SizedBox(
width: 90,
height: 40,
child: OutlinedButton(
onPressed: canResend ? _handleResendCode : null,
style: OutlinedButton.styleFrom(
backgroundColor: AppColors.background,
side: BorderSide(color: borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
text,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textColor,
),
),
),
);
}
```
**Step 2: Commit**
```bash
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
git commit -m "feat(auth): improve resend button style with countdown"
```
---
## Task 5: 添加 Toast 错误提示
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_verification_screen.dart`
**Step 1: 添加 BlocListener 监听错误**
`BlocConsumer` 替换 `BlocBuilder`,添加监听:
```dart
import '../../../../shared/widgets/toast/toast.dart';
Widget _buildFormContainer() {
return BlocConsumer<RegisterCubit, RegisterState>(
listener: (context, state) {
// 监听后台发送验证码的错误
if (state.errorMessage != null && !state.isStep2Valid) {
Toast.show(context, state.errorMessage!, type: ToastType.error);
}
},
builder: (context, state) {
return SizedBox(
width: 327,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildCodeInput(state),
const SizedBox(height: 12),
_buildStepIndicator(),
const SizedBox(height: 12),
AppButton(
text: '完成注册',
onPressed: state.status == FormzSubmissionStatus.inProgress
? null
: _handleComplete,
),
],
),
);
},
);
}
```
**Step 2: 修改 _handleResendCode 重置倒计时**
```dart
Future<void> _handleResendCode() async {
final cubit = context.read<RegisterCubit>();
await cubit.resendCode();
// 重发成功后重置倒计时
if (cubit.state.codeSent && mounted) {
_startCountdown();
Toast.show(context, '验证码已发送', type: ToastType.success);
}
}
```
**Step 3: Commit**
```bash
git add apps/lib/features/auth/ui/screens/register_verification_screen.dart
git commit -m "feat(auth): add toast feedback for code sending"
```
---
## Task 6: 集成测试与验收
**Step 1: 运行全部测试**
Run: `cd apps && flutter test`
Expected: All tests PASS
**Step 2: 手动验收清单**
- [ ] 点击"下一步"后立即跳转到验证码界面
- [ ] 验证码界面显示倒计时按钮 "60 s" ... "1 s"
- [ ] 倒计时期间按钮禁用
- [ ] 倒计时结束后显示"重新发送",可点击
- [ ] 点击重发后重新开始 60 秒倒计时
- [ ] 发送失败时 Toast 提示错误
**Step 3: Final commit**
```bash
git add -A
git commit -m "feat(auth): complete register verification UX optimization"
```
---
## 文件变更汇总
| 文件 | 变更 |
|------|------|
| `register_cubit.dart` | 添加 `isSending` 状态、`sendCodeSilently` 方法 |
| `register_screen.dart` | 乐观跳转,后台发送 |
| `register_verification_screen.dart` | Timer 倒计时、按钮样式、Toast 提示 |
| `register_cubit_test.dart` | 新增 `sendCodeSilently` 测试 |
@@ -0,0 +1,577 @@
# Plan: social-app 数据库数据模型重设计(支持社交/事项/自动化)
**Date:** 2026-02-26
**Author:** AI Assistant
**Status:** Draft
## Overview
本方案面向 `social-app` 的下一阶段功能升级,重设计 PostgreSQL 数据模型,统一支持用户专属 agent、好友/群组协作、待处理消息、设置、可订阅且可授权编辑的日程事项、待办联动与自动化定时任务。目标是在 FastAPI + Flutter 协作场景下提供长期稳定的数据基础,降低后续 API 演进和跨端同步复杂度。
## Requirements
### Functional
- [x] 每个用户有专属 agent,且模型可扩展到未来多 agent 能力
- [x] 用户支持好友关系、群组创建与成员管理
- [x] 用户支持 inbox/pending 待处理消息
- [x] 用户支持个性化设置(偏好/隐私/通知)
- [x] 用户支持“绑定日程的事项”,可多人订阅,且仅特定人可修改
- [x] 用户支持待办事项(可由日程事项提取,也可手动创建)
- [x] 用户支持自动化定时任务(循环触发)
### Non-Functional
- [x] 性能:核心读路径(inbox 列表、待办列表、事项列表)P95 < 150ms(单用户典型数据量)
- [x] 安全:权限以后端业务授权为准;数据库层保留 RLS 防御边界
- [x] 一致性:关键写路径(好友状态、权限变更、任务触发)使用事务保障
- [x] 可演进:支持旧表迁移、双写与灰度切换
## Technical Approach
采用“认证域(`auth.users`+ 业务域(`public.*`)”分层建模。保持 `auth.users` 作为身份主键来源,业务表统一引用 `user_id UUID -> auth.users.id`。领域边界拆分为:Identity/Profile、Social Graph、Collaboration(事项/订阅/权限)、Inbox、Todo、Automation。通过“规范化主模型 + 局部物化/冗余快照”平衡一致性与查询性能。
### Key Decisions
| Decision | Rationale |
|----------|-----------|
| 用户与 agent 采用 1:1 主约束 + 可扩展结构 | 当前满足“每用户专属 agent”,未来允许多 agent 形态演进 |
| 好友关系用单表双向规范化表示 | 避免 A-B / B-A 重复,降低去重成本 |
| 事项权限采用 ACL 表而非仅 owner | 满足“仅特定人可修改”的协作场景 |
| 待办采用主表 + 源映射表 | 支持从事项提取、手动创建、去重与追踪来源 |
| 自动化调度采用 `rrule + cron + interval` 三选一 | 同时覆盖日历型循环和工程型间隔任务 |
| inbox 采用事件聚合模型 | 将好友请求/群组邀请/事项变更统一进入待处理中心 |
## A. 设计原则与边界
### 1) 核心实体与聚合边界
- 用户聚合:`profiles`(含 settings JSONB, `user_agents`
- 社交聚合:`friendships`, `groups`, `group_members`
- 协作事项聚合:`schedule_items`, `schedule_item_subscriptions`, `schedule_item_permissions`
- 消息聚合:`inbox_events`, `inbox_receipts`
- 待办聚合:`todos`, `todo_sources`
- 自动化聚合:`automation_jobs`, `automation_schedules`, `automation_runs`
### 2) 一致性分级
- 强一致(同事务):好友关系状态迁移、群组成员角色变更、事项权限写入、定时任务抢占执行
- 最终一致:inbox 衍生、待办同步、提醒派发(允许异步补偿)
### 3) 多租户假设
- 默认假设:单租户产品(同一业务库服务所有用户),以 `user_id` 做数据隔离
- 扩展预留:各核心表可预留 `tenant_id UUID NULL`(需业务确认是否近期引入组织空间)
## B. 领域模型与关系图(文字化)
### 实体与关系
- `auth.users (1) - (1) profiles`settings 作为 JSONB 内嵌)
- `auth.users (1) - (1) user_agents`
- `auth.users (N) - (N) auth.users` 通过 `friendships`
- `auth.users (1) - (N) groups`(创建者)
- `groups (1) - (N) group_members``auth.users (1) - (N) group_members`
- `auth.users (1) - (N) schedule_items`(创建者)
- `schedule_items (1) - (N) schedule_item_subscriptions``auth.users (1) - (N) schedule_item_subscriptions`
- `schedule_items (1) - (N) schedule_item_permissions``auth.users (1) - (N) schedule_item_permissions`
- `inbox_events (1) - (N) inbox_receipts``auth.users (1) - (N) inbox_receipts`
- `auth.users (1) - (N) todos`
- `todos (1) - (N) todo_sources`(一条待办可有多个来源记录,默认 1 条)
- `auth.users (1) - (N) automation_jobs`
- `automation_jobs (1) - (N) automation_schedules`
- `automation_jobs (1) - (N) automation_runs`
### 关键约束
- 唯一性:
- `user_agents.user_id` 唯一
- `friendships(user_low_id, user_high_id)` 唯一
- `group_members(group_id, user_id)` 唯一
- `schedule_item_subscriptions(item_id, subscriber_id)` 唯一
- `schedule_item_permissions(item_id, subject_type, subject_id, permission)` 唯一
- `todo_sources(source_type, source_id, owner_id)` 唯一(防止重复抽取)
- `automation_runs(job_id, idempotency_key)` 唯一
- 外键:统一显式 `ON DELETE` 策略(见下)
- 可空性:权限关键字段、状态字段、外部幂等键默认 `NOT NULL`
- 删除策略:
- 用户删除:大部分 `CASCADE`(用户私有数据);跨用户协作数据优先软删
- 事项删除:对子表 `CASCADE`;待办来源保留历史可改 `SET NULL` + 软删
## C. 数据库表设计(PostgreSQL
以下为推荐主表(方案 1,规范化优先)。字段示例采用 `UUID + timestamptz + enum/text-check`
### 1) 用户与 agent
#### `profiles`(已有,建议补齐)
- PK: `id UUID` (`auth.users.id`)
- 关键字段: `username`, `avatar_url`, `bio`
- **新增 JSONB 字段**:
- `settings JSONB`(用户自定义设置,含 `preferences`, `privacy`, `notification` 三大块)
- `settings_version INTEGER DEFAULT 1`(兼容旧数据的版本字段)
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 索引:
- `INDEX(username)`(允许重名,仅用于列表查询)
- `GIN(settings)`(支持 JSONB 表达式查询)
- 表达式索引:`(settings->>'notification_enabled')`(按需,对高频查询字段单独建)
- 审计: `created_by`, `updated_by`(可等于 id
- 删除策略: 用户删除时 `CASCADE`
#### `user_agents`
- PK: `id UUID`
- 关键字段:
- `user_id UNIQUE`(每用户专属 agent
- `llm_id UUID NOT NULL`(关联绑定的 LLM 模型)
- `agent_type VARCHAR(20) NOT NULL`(枚举限制:`INTENT_RECOGNITION` | `TASK_EXECUTION` | `RESULT_REPORTING`
- `config JSONB`agent 配置参数)
- `capability_version INTEGER DEFAULT 1`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``active|paused|migrating`
- 索引:
- `UNIQUE(user_id) WHERE deleted_at IS NULL`
- `INDEX(status)`
- `INDEX(agent_type)`
- `GIN(config)`(按需)
- 审计: `created_by`, `updated_by`
### 2) 社交关系
#### `friendships`
- PK: `id UUID`
- 关键字段:
- `user_low_id`(两者中较小的 UUID
- `user_high_id`(两者中较大的 UUID
- `initiator_id`(发起请求方的 user_id,用于追溯谁主动)
- `status`, `requested_at`, `accepted_at`, `blocked_by`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``pending|accepted|blocked|declined|canceled`
- 约束:
- `CHECK(user_low_id < user_high_id)`(强制小值放 low,大值放 high,确保 A→B 和 B→A 是同一行)
- `CHECK(initiator_id IN (user_low_id, user_high_id))`
- `UNIQUE(user_low_id, user_high_id)`
- 索引:
- `INDEX(user_low_id, status)`
- `INDEX(user_high_id, status)`
- 部分索引 `INDEX(status) WHERE status='pending'`
- 审计: `created_by`, `updated_by`
**查询示例**
- 查询用户 A 的所有好友:`SELECT * FROM friendships WHERE user_low_id = A OR user_high_id = A`
#### `groups`
- PK: `id UUID`
- 关键字段: `name`, `description`, `visibility`, `owner_id`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``active|archived`
- 索引: `INDEX(owner_id, status)`, `INDEX(visibility)`
- 审计: `created_by`, `updated_by`
#### `group_members`
- PK: `id UUID`
- 关键字段:
- `group_id`, `user_id`
- `role`JSONB 数组,权柄组合,如 `["view"]` / `["view", "invite"]` / `["view", "invite", "edit"]`
- `join_source``invited|joined`
- `invited_by`, `joined_at`
- 时间字段: `created_at`, `updated_at`, `removed_at`
- 状态字段: `status``active|muted|removed`
- 约束: `UNIQUE(group_id, user_id)`
- 索引:
- `INDEX(group_id, status)`
- `INDEX(user_id, status)`
- GIN 索引支持权柄查询:`INDEX group_members_role USING GIN(role)`
- 审计: `created_by`, `updated_by`
**权柄说明**
| 权柄 | 含义 |
|------|------|
| `view` | 查看群组信息、成员列表、聊天记录 |
| `invite` | 邀请新成员入群 |
| `edit` | 修改群组信息、管理成员(禁言/移除) |
- 群主(创建者)默认拥有全部权柄:`["view", "invite", "edit"]`
- 权柄可动态变更:服务层使用 `jsonb ||``jsonb -` 原子操作增减权柄
### 3) 用户设置(已合并至 profiles 表)
用户设置采用 JSONB 内嵌方式,渐进式扩展无需改表结构:
```json
{
"version": 1,
"preferences": {
"theme": "dark",
"language": "zh-CN",
"timezone": "Asia/Shanghai"
},
"privacy": {
"profile_visible_to": "friends",
"activity_visible_to": "friends",
"allow_friend_requests": true
},
"notification": {
"enabled": true,
"push_enabled": true,
"email_enabled": false,
"quiet_hours_start": "22:00",
"quiet_hours_end": "08:00"
}
}
```
- 扩展方式:新增字段时递增 `settings_version`,应用层做 schema 兼容
- 索引策略:对高频查询字段(如 `notification.enabled`)使用表达式索引
- 更新方式:服务层使用 JSONB merge 或字段级 UPDATE,避免读-改-写并发问题(建议用 `jsonb_set` 原子操作)
### 4) 事项与订阅/权限
#### `schedule_items`
- PK: `id UUID`
- 关键字段:
- `owner_id`, `title`, `description`
- `start_at`, `end_at`, `timezone`
- `recurrence_rule`(可空)
- `source_type``manual|imported|agent_generated`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``running|completed|archived`
- 索引:
- `INDEX(owner_id, start_at)`
- `INDEX(status, start_at)`
- `INDEX(updated_at DESC)`
- 审计: `created_by`, `updated_by`
#### `schedule_item_subscriptions`
- PK: `id UUID`
- 关键字段: `item_id`, `subscriber_id`, `notify_level`, `subscription_source`
- 时间字段: `created_at`, `updated_at`, `unsubscribed_at`
- 状态字段: `status``active|paused|unsubscribed`
- 约束: `UNIQUE(item_id, subscriber_id)`
- 索引: `INDEX(subscriber_id, status)`, `INDEX(item_id, status)`
- 审计: `created_by`, `updated_by`
#### `schedule_item_permissions`
- PK: `id UUID`
- 关键字段: `item_id`, `subject_type``user|group|friend_circle`, `subject_id`, `permission`
- 时间字段: `created_at`, `updated_at`, `expires_at`
- 状态字段: `status``active|revoked|expired`
- 约束:
- `UNIQUE(item_id, subject_type, subject_id, permission)`
- `permission` 建议枚举:`view|comment|edit|manage`
- 索引:
- `INDEX(item_id, permission, status)`
- `INDEX(subject_type, subject_id, status)`
- 审计: `granted_by`, `updated_by`
### 5) 待处理消息(Inbox
#### `inbox_events`
- PK: `id UUID`
- 关键字段: `event_type`, `actor_id`, `object_type`, `object_id`, `payload_jsonb`, `dedupe_key`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``open|resolved|canceled`
- 约束: `UNIQUE(dedupe_key)`
- 索引: `INDEX(event_type, created_at DESC)`, `GIN(payload_jsonb)`
- 审计: `created_by`, `updated_by`
#### `inbox_receipts`
- PK: `id UUID`
- 关键字段: `event_id`, `recipient_id`, `inbox_state`, `action_required`, `acted_at`
- 时间字段: `created_at`, `updated_at`, `read_at`
- 状态字段: `inbox_state``pending|read|accepted|rejected|dismissed|expired`
- 约束: `UNIQUE(event_id, recipient_id)`
- 索引:
- `INDEX(recipient_id, inbox_state, created_at DESC)`
- 部分索引 `INDEX(recipient_id, created_at DESC) WHERE inbox_state='pending'`
- 审计: `created_by`, `updated_by`
### 6) 待办与来源映射
#### `todos`
- PK: `id UUID`
- 关键字段: `owner_id`, `title`, `description`, `due_at`, `priority`, `origin_type`, `source_hash`
- 时间字段: `created_at`, `updated_at`, `completed_at`, `deleted_at`
- 状态字段: `status``pending|in_progress|done|canceled|archived`
- 索引:
- `INDEX(owner_id, status, due_at)`
- `INDEX(owner_id, updated_at DESC)`
- 部分索引 `INDEX(owner_id, due_at) WHERE status IN ('pending','in_progress')`
- 审计: `created_by`, `updated_by`
#### `todo_sources`
- PK: `id UUID`
- 关键字段: `todo_id`, `owner_id`, `source_type``schedule_item|manual|inbox_event|automation`, `source_id`, `extracted_at`, `sync_mode`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``linked|unlinked|stale`
- 约束: `UNIQUE(owner_id, source_type, source_id)`(去重关键)
- 索引:
- `INDEX(todo_id)`
- `INDEX(owner_id, source_type, status)`
- 审计: `created_by`, `updated_by`
### 7) 自动化定时任务
#### `automation_jobs`
- PK: `id UUID`
- 关键字段: `owner_id`, `name`, `job_type`, `target_type`, `target_id`, `params_jsonb`
- 时间字段: `created_at`, `updated_at`, `deleted_at`
- 状态字段: `status``active|paused|disabled`
- 索引: `INDEX(owner_id, status)`, `INDEX(target_type, target_id)`
- 审计: `created_by`, `updated_by`
#### `automation_schedules`
- PK: `id UUID`
- 关键字段:
- `job_id UNIQUE`
- `schedule_type``cron|rrule|interval`
- `cron_expr` / `rrule_text` / `interval_seconds`(三选一)
- `timezone`, `start_at`, `end_at`, `next_run_at`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``active|paused|expired|invalid`
- 约束:
- `CHECK` 保证三种表达互斥且至少一项有效
- 索引:
- 部分索引 `INDEX(next_run_at) WHERE status='active'`
- `INDEX(job_id, status)`
- 审计: `created_by`, `updated_by`
#### `automation_runs`
- PK: `id UUID`
- 关键字段: `job_id`, `scheduled_for`, `started_at`, `finished_at`, `attempt`, `max_retries`, `idempotency_key`, `worker_id`, `result_jsonb`, `error_code`
- 时间字段: `created_at`, `updated_at`
- 状态字段: `status``queued|running|succeeded|failed|canceled|dead_letter`
- 约束:
- `UNIQUE(job_id, idempotency_key)`
- `CHECK(attempt <= max_retries + 1)`
- 索引:
- `INDEX(job_id, scheduled_for DESC)`
- `INDEX(status, scheduled_for)`
- 部分索引 `INDEX(status, updated_at) WHERE status IN ('queued','running')`
- 审计: `created_by`, `updated_by`
## D. 权限与协作模型
### 1) 事项编辑权限落表
- 权限决策顺序:
1. `schedule_items.owner_id`(天然 `manage`
2. `schedule_item_permissions` 针对 `subject_type=user` 的显式授权
3. 用户所在群组在 `subject_type=group` 的授权
4. 默认无编辑权限
- 建议在服务层计算“有效权限”,可落缓存字段 `effective_permission`(可选)
### 2) 群组角色与事项关系
- `group_members.role`JSONB 数组,权柄组合 `["view"]` / `["view", "invite"]` / `["view", "invite", "edit"]`
- 群主(创建者)默认拥有全部权柄
- 权柄与事项权限映射:
- 群成员 `view` → 事项 `view`
- 群成员 `invite` → 隐含 `view`
- 群成员 `edit` → 事项 `edit|manage`
- 若事项绑定群组上下文,可增 `schedule_items.context_group_id NULL`
## E. 消息与待办联动
### 1) inbox 关联来源
- `inbox_events.event_type` 建议枚举:
- `friend_request`
- `group_invitation`
- `group_role_changed`
- `schedule_item_changed`
- `schedule_item_permission_granted`
- `automation_run_failed`
- 通过 `object_type/object_id` 直接关联业务对象
### 2) 待办提取与防循环
- 从事项提取待办:
- 先计算 `source_hash = sha256(owner_id + source_type + source_id + title + due_at_bucket)`
-`todo_sources(owner_id, source_type, source_id)` 唯一约束防重复
- 防循环同步:
- `todo_sources.sync_mode``one_way|two_way`
- 当来源为 `schedule_item``two_way` 时,写回需携带 `sync_trace_id`
- 同一 `sync_trace_id` 在同一对象链路只消费一次(应用层幂等)
## F. 定时任务模型
### 1) 循环表达建议
- 推荐优先级:
- 用户型日历任务:`rrule`
- 运维/工程任务:`cron`
- 简单轮询:`interval_seconds`
- 数据层统一在 `automation_schedules`,并通过 `schedule_type` 区分
### 2) 触发记录与重试
- 任务调度器按 `next_run_at` 扫描活跃计划,插入 `automation_runs(status='queued')`
- 执行器抢占:`SELECT ... FOR UPDATE SKIP LOCKED`
- 重试策略:`attempt` 递增,失败后按退避策略更新下一次 `scheduled_for`
### 3) 幂等与并发冲突
- 幂等键:`idempotency_key = sha256(job_id + scheduled_for + logical_partition)`
- 冲突处理:唯一约束冲突时视为重复投递,直接忽略或合并结果
- 并发安全:`automation_jobs.version`(乐观锁)可选
## G. 演进与迁移计划(从旧表到新模型)
### Phase 1: 基础并行建模(8-12 小时)
1. 新建核心表(不删旧表):`user_agents`, `friendships`, `groups`, `group_members`
2.`profiles` 表新增 `settings JSONB`, `settings_version` 字段
3. 建立最小外键和索引,启用 RLS deny-all 策略
4. 提供只写新表的影子接口(内部开关)
### Phase 2: 协作与联动接入(12-16 小时)
1. 新建 `schedule_items*`, `inbox_*`, `todos*`, `automation_*`
2. 编写回填脚本(从旧事项/旧消息结构回填,若不存在则跳过)
3. 开启双写:旧接口写旧表同时写新表,记录双写差异日志
### Phase 3: 读切换与一致性校验(8-12 小时)
1. API 读路径灰度切换到新表(按用户百分比)
2. 每日对账:记录数、状态分布、关键字段哈希比对
3. 指标稳定后停止旧表写入,保留只读回滚窗口
### Phase 4: 收尾与清理(4-8 小时)
1. 下线旧读路径和旧双写逻辑
2. 保留旧表冷备份后归档/删除
3. 固化运行手册与告警阈值
### 回滚策略
- 任意阶段回滚:读切回旧表 + 关闭新表写开关
- 双写阶段故障:保留操作日志,可按时间窗重放补偿
- 最终切换前必须满足:新旧关键查询结果偏差 < 0.1%
## H. 两套方案对比
### 方案 1(推荐):规范化、可维护性优先
- 特点:按领域拆表,ACL 与来源映射独立,严格约束
- 优点:一致性好、权限边界清晰、长期演进成本低
- 缺点:联表较多,初期 API 复杂度较高
### 方案 2:开发效率优先、适度反规范化
- 特点:将部分结构合并为 JSONB(如 `permissions_snapshot`, `todo_origin`
- 优点:开发快、迁移初期改动小
- 缺点:约束弱、查询和审计困难、后续重构成本高
- 注:`user_settings` 已按此思路内嵌至 profiles,其他模块仍建议保持规范化
### 对比矩阵
| 维度 | 方案 1(规范化) | 方案 2(反规范化) |
|------|------------------|--------------------|
| 复杂度 | 中高 | 中 |
| 查询性能 | 读热点需索引与缓存优化 | 单行读取快,复杂筛选慢 |
| 写入成本 | 中(多表事务) | 低到中 |
| 扩展性 | 高 | 中低 |
| 风险 | 中(实施复杂) | 中高(数据质量和权限风险) |
### 推荐结论
- 推荐方案 1:当前业务已包含社交关系、协作权限、跨域联动和自动化调度,若选择反规范化将把复杂性转移到应用层并放大后续维护风险。方案 1 在 FastAPI + PostgreSQL 下更符合长期可维护与可审计目标。
## I. 交付物
### I-1. 可直接进入实现计划的结论性摘要(12 条)
1.`auth.users` 为身份主键,业务表统一 `user_id` 外键。
2. 引入 `user_agents`,通过 `UNIQUE(user_id)` 满足每用户专属 agent。
3. 用户设置内嵌至 `profiles.settings JSONB`,支持渐进式扩展。
4. 好友关系采用标准化双向一行模型,避免重复边。
4. 群组采用 `groups + group_members`,角色内建 owner/admin/member。
5. 事项、订阅、权限三表解耦,支持多人订阅与精细编辑授权。
6. inbox 采用 `events + receipts`,统一承载待处理动作。
7. 待办采用 `todos + todo_sources`,实现来源追踪与去重。
8. 自动化采用 `jobs + schedules + runs`,支持 cron/rrule/interval。
9. 所有关键表补齐状态机字段与审计字段,支持可观测与追责。
10. 索引以“用户维度 + 状态 + 时间”为主,兼顾移动端列表查询。
11. 迁移走“四阶段”:并行建模 -> 双写回填 -> 读切换 -> 清理。
12. 通过幂等键、部分索引和事务边界保障高并发稳定性。
### I-2. 后续 API 设计所需数据契约清单
- 用户/agent
- `UserAgentDTO`: `id,user_id,llm_id,agent_type,status,capability_version,config,updated_at`
- `UserSettingsDTO`(内嵌于 Profile: `settings JSONB, settings_version`
- 好友
- `FriendshipDTO`: `id,user_a,user_b,status,initiator_id,requested_at,accepted_at`
- 状态流转:`pending -> accepted|declined|canceled|blocked`
- 群组
- `GroupDTO`: `id,name,owner_id,visibility,status`
- `GroupMemberDTO`: `group_id,user_id,role,status,joined_at`
- 事项
- `ScheduleItemDTO`: `id,owner_id,title,start_at,end_at,status,timezone,recurrence_rule`
- `ScheduleSubscriptionDTO`: `item_id,subscriber_id,status,notify_level`
- `SchedulePermissionDTO`: `item_id,subject_type,subject_id,permission,status`
- Inbox
- `InboxEventDTO`: `id,event_type,object_type,object_id,payload,status,created_at`
- `InboxReceiptDTO`: `event_id,recipient_id,inbox_state,action_required,read_at,acted_at`
- Todo
- `TodoDTO`: `id,owner_id,title,due_at,status,priority,origin_type`
- `TodoSourceDTO`: `todo_id,source_type,source_id,sync_mode,status`
- 自动化
- `AutomationJobDTO`: `id,owner_id,job_type,target_type,target_id,status`
- `AutomationScheduleDTO`: `job_id,schedule_type,cron_expr,rrule_text,interval_seconds,next_run_at,status`
- `AutomationRunDTO`: `id,job_id,scheduled_for,status,attempt,idempotency_key,error_code`
### I-3. 最小可行迁移 DDL 清单(按优先级)
P0(身份与社交基础)
1. `ALTER TABLE profiles ADD COLUMN settings JSONB DEFAULT '{}'`
2. `ALTER TABLE profiles ADD COLUMN settings_version INTEGER DEFAULT 1`
3. `CREATE INDEX idx_profiles_settings_notification ON profiles ((settings->>'notification_enabled'))`
4. `CREATE TABLE user_agents (...)`
5. `CREATE TABLE friendships (...)`
6. `CREATE TABLE groups (...)`
7. `CREATE TABLE group_members (...)`
8. `CREATE INDEX/UNIQUE/CHECK`friendships 规范化约束)
P1(协作事项)
7. `CREATE TABLE schedule_items (...)`
8. `CREATE TABLE schedule_item_subscriptions (...)`
9. `CREATE TABLE schedule_item_permissions (...)`
10. `CREATE INDEX`owner/status/time + permission 查询)
P2(消息与待办)
11. `CREATE TABLE inbox_events (...)`
12. `CREATE TABLE inbox_receipts (...)`
13. `CREATE TABLE todos (...)`
14. `CREATE TABLE todo_sources (...)`
15. `CREATE UNIQUE INDEX uq_todo_sources_owner_source (...)`
P3(自动化)
16. `CREATE TABLE automation_jobs (...)`
17. `CREATE TABLE automation_schedules (...)`
18. `CREATE TABLE automation_runs (...)`
19. `CREATE UNIQUE INDEX uq_automation_runs_job_idempotency (...)`
20. `CREATE INDEX idx_automation_schedules_next_run_active (...)`
P4(安全与治理)
21. 对新增 `public` 表执行 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
22.`anon, authenticated` 创建默认 deny-all `SELECT/INSERT/UPDATE/DELETE` policy
23. 审计字段回填脚本与触发器(如需)
## Dependencies
- [ ] PostgreSQL 扩展:`pgcrypto`UUID/哈希,若已启用可跳过)
- [ ] Alembic 迁移体系(现有)
- [ ] 后端任务执行器(Celery)用于自动化触发与补偿
## Testing Strategy
- **Unit Tests:** 状态流转、权限决策、去重哈希、幂等键生成
- **Integration Tests:** 迁移升级/回滚、双写一致性、RLS policy 基线、并发抢占执行
- **E2E Tests:** 好友请求到 inbox、群组邀请处理、事项订阅变更到待办、自动化任务触发到结果回显
## Risks & Mitigations
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| 旧表结构未知导致回填失败 | High | Medium | 先做 schema 探测脚本,按表存在性分支回填 |
| 双写期间新旧数据不一致 | High | Medium | 引入操作日志 + 对账任务 + 幂等补偿 |
| 权限模型过复杂影响上线进度 | Medium | Medium | 先上线最小权限集(owner/user/group),后续扩展 |
| 自动化任务并发冲突 | Medium | Medium | `SKIP LOCKED` + 幂等键 + 乐观锁版本号 |
| 索引不足导致移动端列表慢 | Medium | Medium | 先覆盖 P0/P1 热路径索引,监控后增量优化 |
## Estimated Effort
| Phase | Effort |
|-------|--------|
| Phase 1 | 8-12 hours |
| Phase 2 | 12-16 hours |
| Phase 3 | 8-12 hours |
| Phase 4 | 4-8 hours |
| **Total** | **32-48 hours** |
## 需业务确认(关键不确定项)
1. ~~`profiles.username` 是否允许重名~~(已确认:允许重名,仅建普通索引)。
2. ~~group_members.role 权柄组合~~(已确认:JSONB 数组 `["view", "invite", "edit"]`)。
3. 是否近期需要“组织/团队”多租户(决定 `tenant_id` 是否立即强制)。
4. 事项是否必须绑定群组上下文(`context_group_id` 是否必需)。
5. 待办与事项同步是否默认双向(推荐默认单向,降低循环风险)。
6. 自动化任务失败重试上限与退避策略(固定/指数)。