docs: cleanup old plans and add new design docs
This commit is contained in:
@@ -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 Auth(signup/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:** 将注册流程改为两阶段 OTP(start/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. 自动化任务失败重试上限与退避策略(固定/指数)。
|
||||
Reference in New Issue
Block a user