chore: 清理opencode技能文件、旧设计文档并更新配置文档
This commit is contained in:
@@ -143,3 +143,8 @@ 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
|
||||
|
||||
######
|
||||
# LLM API KEY
|
||||
LLM_DEEPSEEK_API_KEY=
|
||||
LLM_DASHSCOPE_API_KEY=
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
name: ag-ui
|
||||
description: AG-UI protocol for agent-user interaction. Use when implementing agent chat, streaming events, tool calls, state synchronization, SSE, multimodal messages, MCP/A2A integration, or any AG-UI protocol development.
|
||||
---
|
||||
|
||||
# AG-UI Skills
|
||||
|
||||
AG-UI 协议开发权威指南。**必须使用**场景:构建 agentic 应用、实现 agent 与用户交互、处理流式事件、工具调用生命周期、状态同步、多模态消息、MCP/A2A 集成。提供完整模块索引与源文件行号映射。
|
||||
|
||||
## 何时使用
|
||||
|
||||
**必须使用**的场景:
|
||||
- 实现 Agent 与前端的流式交互
|
||||
- 处理 Agent 生命周期事件(RunStarted/Finished、StepStarted/Finished)
|
||||
- 实现工具调用(ToolCall 事件流)
|
||||
- Agent 状态管理与前端同步
|
||||
- 集成 MCP/A2A 协议的 agent 应用
|
||||
- 实现人机协作(Interrupts、Approval 流程)
|
||||
- 处理多模态消息(文本、图片、音频、视频)
|
||||
|
||||
**查询模式**:
|
||||
- "如何实现 agent 流式响应"
|
||||
- "tool call 事件流程"
|
||||
- "agent state delta 同步"
|
||||
- "human-in-the-loop interrupt"
|
||||
- "AG-UI 与 MCP 集成"
|
||||
|
||||
## 模块索引
|
||||
|
||||
按功能模块查看源文件对应章节:
|
||||
|
||||
| 模块 | 作用 | 源文件行号 |
|
||||
|------|------|------------|
|
||||
| [protocol](modules/protocol.md) | 协议概述,与 MCP、A2A 关系 | 1-33 |
|
||||
| [agents](modules/agents.md) | Agent 概念、架构、类型、实现 | 35-451 |
|
||||
| [architecture](modules/architecture.md) | 核心架构、设计原则、运行机制 | 453-679 |
|
||||
| [events](modules/events.md) | 所有事件类型详解 | 680-1475 |
|
||||
| [generative-ui](modules/generative-ui.md) | 生成式 UI 规范(A2UI/MCP-UI) | 1476-1496 |
|
||||
| [messages](modules/messages.md) | 消息结构、类型、同步机制 | 1498-1952 |
|
||||
| [middleware](modules/middleware.md) | 中间件:转换、过滤、增强事件流 | 1954-2158 |
|
||||
| [reasoning](modules/reasoning.md) | LLM 推理支持,加密推理内容 | 2160-2638 |
|
||||
| [serialization](modules/serialization.md) | 事件流序列化、压缩、分支 | 2640-2827 |
|
||||
| [state](modules/state.md) | Agent 与前端状态同步 | 2829-3080 |
|
||||
| [tools](modules/tools.md) | 工具定义、调用生命周期 | 3082-3441 |
|
||||
| [drafts](modules/drafts.md) | 提案功能:Generative UI, Interrupts, Meta Events, Multimodal | 3492-4846 |
|
||||
| [contributing](modules/contributing.md) | 贡献指南、路线图、更新日志 | 3443-3485 |
|
||||
| [overview](modules/overview.md) | **AG-UI 协议总体介绍** | 4894-5261 |
|
||||
|
||||
## 源文件
|
||||
|
||||
- `llms-full.txt` - AG-UI 协议完整文档(唯一信源,10632 行)
|
||||
- `scripts/` - 可执行示例代码(见下方"示例脚本")
|
||||
|
||||
## 示例脚本
|
||||
|
||||
`scripts/` 目录包含可直接运行的 TypeScript 示例:
|
||||
|
||||
| 示例 | 用途 | 参考文档 |
|
||||
|------|------|---------|
|
||||
| [minimal_agent.ts](scripts/minimal_agent.ts) | 最小 Agent 实现 | [agents](modules/agents.md) 行 132-197 |
|
||||
| [tool_call_example.ts](scripts/tool_call_example.ts) | 工具调用流程 | [events](modules/events.md) 行 938-1066 |
|
||||
| [state_sync_example.ts](scripts/state_sync_example.ts) | Snapshot-Delta 状态同步 | [events](modules/events.md) 行 1067-1155 |
|
||||
|
||||
**运行示例**:
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install @ag-ui/client rxjs
|
||||
|
||||
# 运行
|
||||
npx ts-node scripts/minimal_agent.ts
|
||||
```
|
||||
|
||||
详见 [scripts/README.md](scripts/README.md)
|
||||
|
||||
## 常见事件速查
|
||||
|
||||
| 场景 | 关键事件 | 详见 |
|
||||
|------|---------|------|
|
||||
| 流式响应 | TextMessageStart → Content → End | [events](modules/events.md) 行 835-937 |
|
||||
| 工具调用 | ToolCallStart → Args → End → Result | [events](modules/events.md) 行 938-1066 |
|
||||
| 状态同步 | StateSnapshot, StateDelta | [events](modules/events.md) 行 1067-1155 |
|
||||
| 生命周期 | RunStarted/Finished, StepStarted/Finished | [events](modules/events.md) 行 715-754 |
|
||||
| 人机中断 | RunFinished(interrupt) | [drafts](modules/drafts.md) 行 3897-3920 |
|
||||
|
||||
## 快速路径
|
||||
|
||||
**新手入门**:
|
||||
1. [overview](modules/overview.md) - **理解 AG-UI 协议全貌与定位**
|
||||
2. [protocol](modules/protocol.md) - AG-UI 在 AI 协议栈的位置(与 MCP/A2A 关系)
|
||||
3. [architecture](modules/architecture.md) - 核心概念与设计原则
|
||||
4. [agents](modules/agents.md) - Agent 基础实现
|
||||
5. 运行 [minimal_agent.ts](scripts/minimal_agent.ts) 体验基础事件流
|
||||
|
||||
**实现功能**:
|
||||
- 流式响应 → [events](modules/events.md) (TextMessage 事件) + [minimal_agent.ts](scripts/minimal_agent.ts)
|
||||
- 工具调用 → [tools](modules/tools.md) + [events](modules/events.md) (ToolCall 事件) + [tool_call_example.ts](scripts/tool_call_example.ts)
|
||||
- 状态同步 → [state](modules/state.md) + [events](modules/events.md) (StateDelta) + [state_sync_example.ts](scripts/state_sync_example.ts)
|
||||
- 中间件 → [middleware](modules/middleware.md)
|
||||
|
||||
**高级特性**:
|
||||
- 人机协作 → [drafts](modules/drafts.md) (Interrupts)
|
||||
- 多模态 → [drafts](modules/drafts.md) (Multimodal Messages)
|
||||
- 生成式 UI → [generative-ui](modules/generative-ui.md) + [drafts](modules/drafts.md) (Generative UI)
|
||||
- 推理加密 → [reasoning](modules/reasoning.md)
|
||||
|
||||
## 建议使用方式
|
||||
|
||||
1. 先阅读 [architecture](modules/architecture.md) 了解核心概念
|
||||
2. 根据需要查看具体模块
|
||||
3. 事件类型参考 [events](modules/events.md)
|
||||
4. 实现细节参考对应功能模块
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
# Agents
|
||||
|
||||
**作用**: 介绍 AG-UI 中 Agent 的概念、架构、类型和实现方式。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 35-451
|
||||
|
||||
**内容索引**:
|
||||
- 什么是 Agent (行 47-62)
|
||||
- Agent Architecture - AbstractAgent, 核心组件 (行 63-91)
|
||||
- Agent Types - HttpAgent, Custom Agents (行 93-130)
|
||||
- Implementing Agents - 基本实现示例 (行 132-197)
|
||||
- Agent Capabilities - 交互通信、工具使用、状态管理、多 Agent 协作、人机协作、对话记忆 (行 199-358)
|
||||
- 使用 Agent (行 360-399)
|
||||
- Agent Configuration (行 401-424)
|
||||
- Agent State Management (行 426-439)
|
||||
@@ -1,17 +0,0 @@
|
||||
# Core Architecture
|
||||
|
||||
**作用**: 介绍 AG-UI 的核心架构、设计原则和运行机制。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 453-679
|
||||
|
||||
**内容索引**:
|
||||
- Design Principles - 事件驱动、双向交互、灵活中间件 (行 463-489)
|
||||
- Architectural Overview - 客户端-服务器架构 (行 491-534)
|
||||
- Protocol layer - run(input) -> Observable<BaseEvent> (行 537-567)
|
||||
- Standard HTTP client - HttpAgent, SSE/HTTP binary (行 569-585)
|
||||
- Message types - Lifecycle, Text, Tool, State, Special 事件 (行 587-609)
|
||||
- Running Agents (行 611-640)
|
||||
- State Management - STATE_SNAPSHOT, STATE_DELTA (行 642-652)
|
||||
- Tools and Handoff (行 653-662)
|
||||
- Base Event 属性 (行 664-677)
|
||||
@@ -1,15 +0,0 @@
|
||||
# Contributing & Roadmap
|
||||
|
||||
**作用**: 贡献指南、路线图和更新日志。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 3443-3485
|
||||
|
||||
**内容索引**:
|
||||
- Contributing (行 3443-3460):
|
||||
- Naming conventions - integrations/, wip-, community- 前缀 (行 3448-3459)
|
||||
- Roadmap (行 3462-3481):
|
||||
- 公开路线图链接 (行 3467-3468)
|
||||
- Get Involved - 贡献方式 (行 3470-3474)
|
||||
- What's New (行 3477-3485):
|
||||
- 2025-04-09: AG-UI 仓库公开发布 (行 3482-3484)
|
||||
@@ -1,53 +0,0 @@
|
||||
# Drafts
|
||||
|
||||
**作用**: 介绍 AG-UI 协议中正在考虑或开发中的提案功能。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 3492-3854 (Generative UI), 3860-4105 (Interrupts), 4111-4349 (Meta Events), 4355-4846 (Multimodal)
|
||||
|
||||
**Drafts 概述** (行 4847-4887):
|
||||
- Drafts 状态定义 - Draft/Under Review/Accepted/Implemented/Withdrawn (行 4880-4886)
|
||||
|
||||
**Generative User Interfaces** (行 3492-3854):
|
||||
- Summary - 问题陈述与动机 (行 3494-3508)
|
||||
- Challenges and Limitations - 工具描述长度、JSON Schema 约束 (行 3515-3531)
|
||||
- Detailed Specification:
|
||||
- Two-Step Generation Process - 两步生成流程图 (行 3535-3543)
|
||||
- Step 1: What to Generate? - generateUserInterface 工具 (行 3545-3596)
|
||||
- Step 2: How to Generate? - 次级 LLM 生成实际 UI (行 3598-3612)
|
||||
- Implementation Examples:
|
||||
- UISchemaGenerator - JSON Schema 输出 (行 3615-3673)
|
||||
- ReactFormHookGenerator - React Hook Form 代码生成 (行 3675-3795)
|
||||
- Implementation Considerations - SDK 变更 (行 3797-3819)
|
||||
- Use Cases - 动态表单、数据可视化、交互式工作流 (行 3821-3838)
|
||||
|
||||
**Interrupt-Aware Run Lifecycle** (行 3860-4105):
|
||||
- Summary - 人机协作暂停机制 (行 3862-3875)
|
||||
- Updates to RUN_FINISHED Event - outcome, interrupt 字段 (行 3897-3920)
|
||||
- Updates to RunAgentInput - resume 字段 (行 3922-3937)
|
||||
- Contract Rules (行 3939-3945)
|
||||
- Implementation Examples (行 3947-4026)
|
||||
- Use Cases - 人类批准、信息收集、策略强制 (行 4028-4051)
|
||||
- Implementation Considerations (行 4052-4091)
|
||||
|
||||
**Meta Events** (行 4111-4349):
|
||||
- Summary - 独立于 Agent 运行的事件注解 (行 4115-4127)
|
||||
- MetaEvent Type - metaType, payload (行 4145-4170)
|
||||
- Implementation Examples:
|
||||
- User Feedback - thumbs_up, thumbs_down (行 4174-4206)
|
||||
- Annotations - note, tag (行 4208-4239)
|
||||
- External System Events - analytics, moderation (行 4241-4276)
|
||||
- Common Meta Event Types 表 (行 4278-4292)
|
||||
- Use Cases (行 4294-4318)
|
||||
|
||||
**Multimodal Messages** (行 4355-4846):
|
||||
- Summary - 支持多模态输入消息 (行 4357-4371)
|
||||
- Status: Implemented (行 4373-4376)
|
||||
- Detailed Specification:
|
||||
- Modality Types 表 - text, image, audio, video, document (行 4473-4483)
|
||||
- Source Types - InputContentDataSource, InputContentUrlSource (行 4485-4509)
|
||||
- Content Part Types - TextInputPart, ImageInputPart, AudioInputPart, VideoInputPart, DocumentInputPart (行 4511-4561)
|
||||
- Provider Metadata (行 4562-4571)
|
||||
- Implementation Examples (行 4573-4764)
|
||||
- Implementation Considerations (行 4766-4798)
|
||||
- Use Cases (行 4800-4827)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Events
|
||||
|
||||
**作用**: 详细介绍 AG-UI 协议中的所有事件类型,包括生命周期、文本、工具调用、状态管理、推理等事件。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 680-1475
|
||||
|
||||
**内容索引**:
|
||||
- Event Types Overview - 事件分类表 (行 692-703)
|
||||
- Base Event Properties (行 705-713)
|
||||
- Lifecycle Events - RunStarted, RunFinished, RunError, StepStarted, StepFinished (行 715-754)
|
||||
- Text Message Events - TextMessageStart, TextMessageContent, TextMessageEnd, TextMessageChunk (行 835-937)
|
||||
- Tool Call Events - ToolCallStart, ToolCallArgs, ToolCallEnd, ToolCallResult, ToolCallChunk (行 938-1066)
|
||||
- State Management Events - StateSnapshot, StateDelta, MessagesSnapshot (行 1067-1155)
|
||||
- Activity Events - ActivitySnapshot, ActivityDelta (行 1156-1189)
|
||||
- Special Events - Raw, Custom (行 1191-1233)
|
||||
- Reasoning Events - ReasoningStart, ReasoningMessageStart/Content/End/Chunk, ReasoningEnd, ReasoningEncryptedValue (行 1234-1368)
|
||||
- Deprecated Events - THINKING_* 事件迁移 (行 1369-1389)
|
||||
- Draft Events - Meta Events, Modified Lifecycle Events (行 1391-1446)
|
||||
- Event Flow Patterns - Start-Content-End, Snapshot-Delta, Lifecycle (行 1447-1474)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Generative UI Specs
|
||||
|
||||
**作用**: 介绍 AG-UI 与生成式 UI 规范的关系(A2UI、MCP-UI、Open-JSON-UI)。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 1476-1496
|
||||
|
||||
**内容索引**:
|
||||
- AG-UI and Generative UI Specs - AG-UI 不是生成式 UI 规范,而是用户交互协议 (行 1476-1496)
|
||||
- AG-UI 与 A2UI、MCP-UI、Open-JSON-UI 的关系说明
|
||||
- Generative UI 实现详情见 [drafts](drafts.md) 的 Generative User Interfaces 章节 (行 3492-3854)
|
||||
@@ -1,21 +0,0 @@
|
||||
# Messages
|
||||
|
||||
**作用**: 介绍 AG-UI 中消息的结构、类型和同步机制。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 1498-1952
|
||||
|
||||
**内容索引**:
|
||||
- Message Structure - BaseMessage 接口, role, encryptedContent (行 1510-1537)
|
||||
- Message Types:
|
||||
- UserMessage - 用户消息, 支持多模态 InputContent (行 1543-1576)
|
||||
- AssistantMessage - 助手消息, 含 toolCalls (行 1578-1591)
|
||||
- SystemMessage - 系统消息 (行 1593-1604)
|
||||
- ToolMessage - 工具结果消息 (行 1606-1627)
|
||||
- ActivityMessage - 前端专用活动消息 (行 1628-1653)
|
||||
- DeveloperMessage - 开发/调试消息 (行 1654-1665)
|
||||
- ReasoningMessage - 推理消息 (行 1667-1704)
|
||||
- Vendor Neutrality - 供应商中立性, 格式转换示例 (行 1705-1735)
|
||||
- Message Synchronization - MESSAGES_SNAPSHOT, 流式消息 (行 1736-1794)
|
||||
- Tool Integration - ToolCall, ToolResult 结构 (行 1795-1889)
|
||||
- Practical Example - 完整对话示例 (行 1891-1939)
|
||||
@@ -1,18 +0,0 @@
|
||||
# Middleware
|
||||
|
||||
**作用**: 介绍 AG-UI 中间件,用于转换、过滤和增强事件流。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 1954-2158
|
||||
|
||||
**内容索引**:
|
||||
- What is Middleware? - 中间件的作用 (行 1965-1973)
|
||||
- How Middleware Works - 中间件链式调用 (行 1975-1991)
|
||||
- Function-Based Middleware - 函数式中间件示例 (行 1993-2019)
|
||||
- Class-Based Middleware - 类中间件示例 (行 2021-2055)
|
||||
- Built-in Middleware - FilterToolCallsMiddleware (行 2062-2086)
|
||||
- Middleware Patterns - 日志、认证、限流 (行 2088-2090)
|
||||
- Combining Middleware (行 2092-2103)
|
||||
- Execution Order - 中间件执行顺序 (行 2104-2124)
|
||||
- Best Practices (行 2126-2134)
|
||||
- Conditional Middleware (行 2136-2153)
|
||||
@@ -1,28 +0,0 @@
|
||||
# AG-UI Overview
|
||||
|
||||
**作用**: AG-UI 协议总体介绍,包括协议定位、核心特性、与其他协议的关系、以及构建块概览。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 4894-5261
|
||||
|
||||
**内容索引**:
|
||||
- 协议定义 - 开放、轻量、事件驱动的 Agent-User 交互协议 (行 4894-4901)
|
||||
- Agentic Protocols - MCP/A2A/AG-UI 三层协议栈 (行 4910-4923)
|
||||
- Building Blocks (行 4926-5088):
|
||||
- Streaming chat - 流式对话 (行 4930-4941)
|
||||
- Tool calling - 工具调用 (行 4943-4953)
|
||||
- Structured state - 结构化状态 (行 4955-4965)
|
||||
- Generative UI - 生成式 UI (行 4967-4977)
|
||||
- Contextual context - 上下文管理 (行 4979-4989)
|
||||
- Client-side tools - 客户端工具 (行 4991-5001)
|
||||
- Auth & multi-tenancy - 认证与多租户 (行 5003-5013)
|
||||
- Debugging & evals - 调试与评估 (行 5015-5025)
|
||||
- Upcoming: Reasoning continuity - 推理连续性 (行 5027-5037)
|
||||
- Upcoming: Multi-modal - 多模态 (行 5039-5049)
|
||||
- Upcoming: Generative UI - 生成式 UI (行 5051-5061)
|
||||
- Upcoming: Interrupts & approval flows - 中断与审批流程 (行 5063-5073)
|
||||
- Upcoming: Meta events - 元事件 (行 5075-5085)
|
||||
- Protocol Basics (行 5090-5261):
|
||||
- Agent Definition - Agent 定义 (行 5095-5109)
|
||||
- Event Stream - 事件流 (行 5111-5129)
|
||||
- Common Patterns - 常见模式 (行 5131-5151)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Protocol
|
||||
|
||||
**作用**: 介绍 AG-UI 与 MCP、A2A 协议的关系,以及 AG-UI 作为连接 Agent 与用户应用的协议定位。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 1-33
|
||||
|
||||
**内容索引**:
|
||||
- Agentic Protocols 概述 (MCP, A2A, AG-UI)
|
||||
- AG-UI 与 MCP、A2A 的握手
|
||||
- Generative UI Specs 介绍
|
||||
@@ -1,26 +0,0 @@
|
||||
# Reasoning
|
||||
|
||||
**作用**: 介绍 AG-UI 对 LLM 推理的支持,包括链式思维可视化和加密推理内容。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 2160-2638
|
||||
|
||||
**内容索引**:
|
||||
- Overview - 推理可见性、状态连续性、隐私合规 (行 2171-2188)
|
||||
- ReasoningMessage - 推理消息结构 (行 2190-2217)
|
||||
- Reasoning Events:
|
||||
- Event Flow - 推理事件流程图 (行 2223-2246)
|
||||
- Event Types 表 (行 2248-2258)
|
||||
- Privacy and Compliance:
|
||||
- Zero Data Retention (ZDR) - 零数据保留 (行 2264-2273)
|
||||
- Visibility Control - 可见性控制 (行 2274-2284)
|
||||
- Compliance Considerations 表 - GDPR, SOC 2, HIPAA (行 2285-2293)
|
||||
- Example Implementations:
|
||||
- Basic Reasoning Flow (行 2296-2348)
|
||||
- Encrypted Content for State Continuity (行 2350-2396)
|
||||
- Attaching Encrypted Reasoning to Tool Calls (行 2398-2430)
|
||||
- ZDR-Compliant Implementation (行 2432-2475)
|
||||
- Using Convenience Chunk Event (行 2477-2503)
|
||||
- Client Integration - 处理推理事件, 传递加密推理 (行 2505-2563)
|
||||
- Migration from Thinking Events - THINKING_* 迁移到 REASONING_* (行 2565-2617)
|
||||
- Best Practices (行 2619-2632)
|
||||
@@ -1,21 +0,0 @@
|
||||
# Serialization
|
||||
|
||||
**作用**: 介绍 AG-UI 事件流的序列化,用于历史恢复、分支和时间旅行。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 2640-2827
|
||||
|
||||
**内容索引**:
|
||||
- Core Concepts:
|
||||
- Stream serialization - 事件流转为 JSON (行 2660-2662)
|
||||
- Event compaction - 压缩事件流 (行 2662-2663)
|
||||
- Run lineage - parentRunId 实现 git 类日志 (行 2664-2665)
|
||||
- Updated Event Fields - RunStarted 新增 parentRunId, input (行 2667-2685)
|
||||
- Event Compaction - compactEvents 函数, 压缩规则 (行 2686-2704)
|
||||
- Branching and Time Travel - parentRunId 创建分支, git 类日志 (行 2705-2729)
|
||||
- Examples:
|
||||
- Basic Serialization (行 2732-2744)
|
||||
- Event Compaction - 压缩前后示例 (行 2746-2774)
|
||||
- Branching With parentRunId (行 2776-2795)
|
||||
- Normalized Input (行 2797-2814)
|
||||
- Implementation Notes (行 2816-2822)
|
||||
@@ -1,18 +0,0 @@
|
||||
# State Management
|
||||
|
||||
**作用**: 介绍 AG-UI 中 Agent 与前端之间的状态同步机制。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 2829-3080
|
||||
|
||||
**内容索引**:
|
||||
- Shared State Architecture - 共享状态架构, 双向通信 (行 2842-2857)
|
||||
- State Synchronization Methods:
|
||||
- State Snapshots - STATE_SNAPSHOT 事件 (行 2862-2882)
|
||||
- State Deltas - STATE_DELTA 事件, JSON Patch (行 2884-2902)
|
||||
- JSON Patch Format:
|
||||
- RFC 6902 操作: add, replace, remove, move, copy, test (行 2903-2944)
|
||||
- State Processing in AG-ui - fast-json-patch 使用示例 (行 2946-2977)
|
||||
- Human-in-the-Loop Collaboration - 人机协作示例 (行 2978-3005)
|
||||
- CopilotKit Implementation - useCoAgent, copilotkit_emit_state (行 3007-3050)
|
||||
- Best Practices (行 3052-3068)
|
||||
@@ -1,24 +0,0 @@
|
||||
# Tools
|
||||
|
||||
**作用**: 介绍 AG-UI 中工具的定义、使用和人在环工作流。
|
||||
|
||||
**源文件**: `llms-full.txt`
|
||||
**行号范围**: 3082-3441
|
||||
|
||||
**内容索引**:
|
||||
- What Are Tools? - 工具的作用 (行 3095-3105)
|
||||
- Tool Structure - Tool 接口定义 (行 3107-3130)
|
||||
- Frontend-Defined Tools - 工具由前端定义并传递给 Agent (行 3132-3175)
|
||||
- Tool Call Lifecycle:
|
||||
- ToolCallStart (行 3179-3191)
|
||||
- ToolCallArgs - 流式参数 (行 3193-3217)
|
||||
- ToolCallEnd (行 3219-3229)
|
||||
- Tool Results - ToolMessage 结构 (行 3231-3246)
|
||||
- Human-in-the-Loop Workflows - 人机协作工作流 (行 3248-3270)
|
||||
- CopilotKit Integration - useCopilotAction hook (行 3272-3304)
|
||||
- Tool Examples:
|
||||
- User Confirmation (行 3310-3332)
|
||||
- Data Retrieval (行 3334-3358)
|
||||
- User Interface Control (行 3360-3381)
|
||||
- Content Generation (行 3383-3412)
|
||||
- Best Practices (行 3414-3428)
|
||||
@@ -1,163 +0,0 @@
|
||||
# AG-UI 示例脚本
|
||||
|
||||
本目录包含 AG-UI 协议的实现示例,帮助开发者快速上手。
|
||||
|
||||
## 前置要求
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install @ag-ui/client rxjs
|
||||
|
||||
# 或
|
||||
pnpm add @ag-ui/client rxjs
|
||||
```
|
||||
|
||||
## 示例列表
|
||||
|
||||
### 1. minimal_agent.ts - 最小 Agent 实现
|
||||
|
||||
展示如何创建一个最基本的 AG-UI Agent,实现事件流。
|
||||
|
||||
**核心概念**:
|
||||
- 继承 `AbstractAgent` 类
|
||||
- 实现 `run()` 方法返回 Observable 事件流
|
||||
- 发送生命周期事件 (RUN_STARTED/RUN_FINISHED)
|
||||
- 发送文本消息事件 (TEXT_MESSAGE_START/CONTENT/END)
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
# 使用 ts-node
|
||||
npx ts-node scripts/minimal_agent.ts
|
||||
|
||||
# 或编译后运行
|
||||
npx tsc scripts/minimal_agent.ts --esModuleInterop
|
||||
node scripts/minimal_agent.js
|
||||
```
|
||||
|
||||
**参考文档**: [modules/agents.md](../modules/agents.md) 行 132-197
|
||||
|
||||
---
|
||||
|
||||
### 2. tool_call_example.ts - 工具调用流程
|
||||
|
||||
展示 Agent 如何调用工具并流式传输参数和结果。
|
||||
|
||||
**核心概念**:
|
||||
- 定义工具 (Tool)
|
||||
- 工具调用事件流: ToolCallStart → ToolCallArgs → ToolCallEnd → ToolCallResult
|
||||
- 流式传输工具参数(分块发送)
|
||||
- 基于工具结果生成响应
|
||||
|
||||
**事件流**:
|
||||
```
|
||||
ToolCallStart (工具名称)
|
||||
↓
|
||||
ToolCallArgs (参数片段 1)
|
||||
ToolCallArgs (参数片段 2)
|
||||
ToolCallArgs (参数片段 3)
|
||||
↓
|
||||
ToolCallEnd (参数传输完成)
|
||||
↓
|
||||
ToolCallResult (工具执行结果)
|
||||
```
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
npx ts-node scripts/tool_call_example.ts
|
||||
```
|
||||
|
||||
**参考文档**: [modules/events.md](../modules/events.md) 行 938-1066
|
||||
|
||||
---
|
||||
|
||||
### 3. state_sync_example.ts - 状态同步
|
||||
|
||||
展示 Agent 与前端的 Snapshot-Delta 状态同步模式。
|
||||
|
||||
**核心概念**:
|
||||
- StateSnapshot - 完整状态快照
|
||||
- StateDelta - 增量更新 (JSON Patch RFC 6902)
|
||||
- MessagesSnapshot - 消息历史快照
|
||||
- 前端状态管理器实现
|
||||
|
||||
**状态同步模式**:
|
||||
```
|
||||
初始同步:
|
||||
StateSnapshot (完整状态)
|
||||
MessagesSnapshot (消息历史)
|
||||
↓
|
||||
增量更新:
|
||||
StateDelta (JSON Patch 操作)
|
||||
StateDelta (另一个更新)
|
||||
↓
|
||||
周期性刷新:
|
||||
StateSnapshot (确保一致性)
|
||||
```
|
||||
|
||||
**JSON Patch 操作类型**:
|
||||
- `replace` - 替换值
|
||||
- `add` - 添加字段
|
||||
- `remove` - 删除字段
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
npx ts-node scripts/state_sync_example.ts
|
||||
```
|
||||
|
||||
**参考文档**: [modules/events.md](../modules/events.md) 行 1067-1155
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 这些示例可以直接用于生产环境吗?
|
||||
|
||||
A: 这些示例仅用于教学目的。生产环境应考虑:
|
||||
- 错误处理和重试机制
|
||||
- 认证和授权
|
||||
- 日志和监控
|
||||
- 性能优化(如事件节流)
|
||||
|
||||
### Q: 如何处理工具调用的并发?
|
||||
|
||||
A: 每个工具调用有唯一的 `toolCallId`,可以并发执行多个工具:
|
||||
```typescript
|
||||
// 工具调用 1
|
||||
ToolCallStart(toolCallId: "tool_1")
|
||||
ToolCallArgs(toolCallId: "tool_1", delta: "...")
|
||||
ToolCallEnd(toolCallId: "tool_1")
|
||||
|
||||
// 工具调用 2(并发)
|
||||
ToolCallStart(toolCallId: "tool_2")
|
||||
ToolCallArgs(toolCallId: "tool_2", delta: "...")
|
||||
ToolCallEnd(toolCallId: "tool_2")
|
||||
```
|
||||
|
||||
### Q: StateDelta 的 JSON Patch 格式如何工作?
|
||||
|
||||
A: JSON Patch (RFC 6902) 是标准的增量更新格式:
|
||||
```json
|
||||
[
|
||||
{ "op": "replace", "path": "/session/currentPage", "value": 2 },
|
||||
{ "op": "add", "path": "/formData", "value": {...} },
|
||||
{ "op": "remove", "path": "/tempField" }
|
||||
]
|
||||
```
|
||||
|
||||
推荐使用 [fast-json-patch](https://github.com/Starcounter-Jack/Fast-JSON-Patch) 库处理。
|
||||
|
||||
---
|
||||
|
||||
## 进阶示例
|
||||
|
||||
需要更复杂的示例?查看官方仓库:
|
||||
- [AG-UI GitHub](https://github.com/ag-ui/ag-ui)
|
||||
- [CopilotKit Examples](https://github.com/CopilotKit/CopilotKit/tree/main/examples)
|
||||
|
||||
---
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [AG-UI 协议文档](../llms-full.txt) - 完整协议规范
|
||||
- [模块索引](../SKILL.md#模块索引) - 按功能查找文档
|
||||
- [常见事件速查](../SKILL.md#常见事件速查) - 高频事件流程
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* 最小 AG-UI Agent 实现示例
|
||||
*
|
||||
* 展示如何创建一个自定义 Agent,实现基本的事件流
|
||||
*
|
||||
* 参考文档: modules/agents.md (行 132-197)
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractAgent,
|
||||
RunAgent,
|
||||
RunAgentInput,
|
||||
EventType,
|
||||
BaseEvent,
|
||||
} from "@ag-ui/client"
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
class MinimalAgent extends AbstractAgent {
|
||||
/**
|
||||
* 实现 run 方法,返回事件流
|
||||
*/
|
||||
run(input: RunAgentInput): RunAgent {
|
||||
const { threadId, runId } = input
|
||||
|
||||
return () =>
|
||||
new Observable<BaseEvent>((observer) => {
|
||||
// 1. 发送 RUN_STARTED 事件
|
||||
observer.next({
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
// 2. 发送文本消息
|
||||
const messageId = Date.now().toString()
|
||||
|
||||
// 消息开始
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId,
|
||||
role: "assistant",
|
||||
})
|
||||
|
||||
// 消息内容(流式)
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId,
|
||||
delta: "Hello! ",
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId,
|
||||
delta: "I'm a minimal AG-UI agent.",
|
||||
})
|
||||
|
||||
// 消息结束
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId,
|
||||
})
|
||||
|
||||
// 3. 发送 RUN_FINISHED 事件
|
||||
observer.next({
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
// 完成流
|
||||
observer.complete()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const agent = new MinimalAgent({
|
||||
agentId: "minimal-agent",
|
||||
description: "A minimal AG-UI agent example",
|
||||
})
|
||||
|
||||
// 运行 Agent 并订阅事件流
|
||||
agent
|
||||
.runAgent({
|
||||
runId: "run_123",
|
||||
threadId: "thread_456",
|
||||
messages: [],
|
||||
tools: [],
|
||||
context: [],
|
||||
})
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
console.log(`[${event.type}]`, event)
|
||||
},
|
||||
error: (error) => console.error("Error:", error),
|
||||
complete: () => console.log("Agent run completed"),
|
||||
})
|
||||
|
||||
export { MinimalAgent }
|
||||
@@ -1,255 +0,0 @@
|
||||
/**
|
||||
* AG-UI 状态同步示例
|
||||
*
|
||||
* 展示 Agent 与前端的 Snapshot-Delta 状态同步模式
|
||||
*
|
||||
* 参考文档: modules/events.md (行 1067-1155)
|
||||
*
|
||||
* 状态管理模式:
|
||||
* 1. StateSnapshot - 完整状态快照(初始同步/周期性刷新)
|
||||
* 2. StateDelta - 增量更新(JSON Patch RFC 6902)
|
||||
* 3. MessagesSnapshot - 消息历史快照
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractAgent,
|
||||
RunAgent,
|
||||
RunAgentInput,
|
||||
EventType,
|
||||
BaseEvent,
|
||||
} from "@ag-ui/client"
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
/**
|
||||
* Agent 状态定义示例
|
||||
*/
|
||||
interface AgentState {
|
||||
user: {
|
||||
name: string
|
||||
preferences: {
|
||||
theme: "light" | "dark"
|
||||
language: string
|
||||
}
|
||||
}
|
||||
session: {
|
||||
currentPage: number
|
||||
itemsPerPage: number
|
||||
totalItems: number
|
||||
}
|
||||
formData?: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
class StateSyncAgent extends AbstractAgent {
|
||||
private state: AgentState = {
|
||||
user: {
|
||||
name: "Alice",
|
||||
preferences: {
|
||||
theme: "light",
|
||||
language: "en",
|
||||
},
|
||||
},
|
||||
session: {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: 100,
|
||||
},
|
||||
}
|
||||
|
||||
run(input: RunAgentInput): RunAgent {
|
||||
const { threadId, runId } = input
|
||||
|
||||
return () =>
|
||||
new Observable<BaseEvent>((observer) => {
|
||||
observer.next({
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
// 1. 发送初始状态快照
|
||||
observer.next({
|
||||
type: EventType.STATE_SNAPSHOT,
|
||||
snapshot: this.state,
|
||||
})
|
||||
|
||||
// 2. 发送消息历史快照
|
||||
observer.next({
|
||||
type: EventType.MESSAGES_SNAPSHOT,
|
||||
messages: [
|
||||
{
|
||||
id: "msg_1",
|
||||
role: "user",
|
||||
content: "Show me page 2",
|
||||
},
|
||||
{
|
||||
id: "msg_2",
|
||||
role: "assistant",
|
||||
content: "Loading page 2...",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 3. 模拟状态变化 - 分页更新
|
||||
setTimeout(() => {
|
||||
// 发送 Delta 更新(JSON Patch 格式)
|
||||
observer.next({
|
||||
type: EventType.STATE_DELTA,
|
||||
delta: [
|
||||
{ op: "replace", path: "/session/currentPage", value: 2 },
|
||||
],
|
||||
})
|
||||
}, 500)
|
||||
|
||||
// 4. 模拟用户偏好更新
|
||||
setTimeout(() => {
|
||||
observer.next({
|
||||
type: EventType.STATE_DELTA,
|
||||
delta: [
|
||||
{ op: "replace", path: "/user/preferences/theme", value: "dark" },
|
||||
],
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
// 5. 添加新字段(表单数据)
|
||||
setTimeout(() => {
|
||||
observer.next({
|
||||
type: EventType.STATE_DELTA,
|
||||
delta: [
|
||||
{
|
||||
op: "add",
|
||||
path: "/formData",
|
||||
value: {
|
||||
searchQuery: "AG-UI tutorial",
|
||||
filters: ["beginner", "typescript"],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}, 1500)
|
||||
|
||||
// 6. 周期性完整快照(确保状态一致性)
|
||||
setTimeout(() => {
|
||||
const updatedState: AgentState = {
|
||||
...this.state,
|
||||
session: {
|
||||
...this.state.session,
|
||||
currentPage: 2,
|
||||
},
|
||||
user: {
|
||||
...this.state.user,
|
||||
preferences: {
|
||||
...this.state.user.preferences,
|
||||
theme: "dark",
|
||||
},
|
||||
},
|
||||
formData: {
|
||||
searchQuery: "AG-UI tutorial",
|
||||
filters: ["beginner", "typescript"],
|
||||
},
|
||||
}
|
||||
|
||||
observer.next({
|
||||
type: EventType.STATE_SNAPSHOT,
|
||||
snapshot: updatedState,
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
observer.complete()
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端状态管理示例(接收端)
|
||||
*/
|
||||
class StateManager {
|
||||
private state: AgentState | null = null
|
||||
|
||||
handleEvent(event: BaseEvent) {
|
||||
switch (event.type) {
|
||||
case EventType.STATE_SNAPSHOT:
|
||||
// 完整替换状态
|
||||
this.state = (event as any).snapshot as AgentState
|
||||
console.log("[State] Snapshot received:", this.state)
|
||||
break
|
||||
|
||||
case EventType.STATE_DELTA:
|
||||
// 应用 JSON Patch 增量更新
|
||||
if (this.state) {
|
||||
const patches = (event as any).delta
|
||||
this.state = this.applyPatches(this.state, patches)
|
||||
console.log("[State] Delta applied:", patches)
|
||||
console.log("[State] Current state:", this.state)
|
||||
}
|
||||
break
|
||||
|
||||
case EventType.MESSAGES_SNAPSHOT:
|
||||
console.log("[Messages] Snapshot:", (event as any).messages)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 JSON Patch 操作(简化实现)
|
||||
* 生产环境应使用 json-patch 库
|
||||
*/
|
||||
private applyPatches(state: AgentState, patches: any[]): AgentState {
|
||||
const newState = JSON.parse(JSON.stringify(state))
|
||||
|
||||
for (const patch of patches) {
|
||||
const { op, path, value } = patch
|
||||
const pathParts = path.split("/").filter(Boolean)
|
||||
let target: any = newState
|
||||
|
||||
// 导航到目标对象的父级
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
target = target[pathParts[i]]
|
||||
}
|
||||
|
||||
const lastKey = pathParts[pathParts.length - 1]
|
||||
|
||||
switch (op) {
|
||||
case "replace":
|
||||
target[lastKey] = value
|
||||
break
|
||||
case "add":
|
||||
target[lastKey] = value
|
||||
break
|
||||
case "remove":
|
||||
delete target[lastKey]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const agent = new StateSyncAgent()
|
||||
const stateManager = new StateManager()
|
||||
|
||||
agent
|
||||
.runAgent({
|
||||
runId: "run_state_sync",
|
||||
threadId: "thread_123",
|
||||
messages: [],
|
||||
tools: [],
|
||||
context: [],
|
||||
})
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
stateManager.handleEvent(event)
|
||||
},
|
||||
complete: () => console.log("\n[Complete] State sync demo finished"),
|
||||
})
|
||||
|
||||
export { StateSyncAgent, StateManager, AgentState }
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* AG-UI 工具调用示例
|
||||
*
|
||||
* 展示 Agent 如何调用工具并流式传输参数和结果
|
||||
*
|
||||
* 参考文档: modules/events.md (行 938-1066)
|
||||
*
|
||||
* 事件流:
|
||||
* 1. ToolCallStart - 工具调用开始
|
||||
* 2. ToolCallArgs (多次) - 流式传输参数
|
||||
* 3. ToolCallEnd - 参数传输完成
|
||||
* 4. ToolCallResult - 工具执行结果
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractAgent,
|
||||
RunAgent,
|
||||
RunAgentInput,
|
||||
EventType,
|
||||
BaseEvent,
|
||||
} from "@ag-ui/client"
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
/**
|
||||
* 工具定义示例
|
||||
*/
|
||||
interface Tool {
|
||||
name: string
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
const weatherTool: Tool = {
|
||||
name: "get_weather",
|
||||
description: "Get current weather for a location",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "City name",
|
||||
},
|
||||
unit: {
|
||||
type: "string",
|
||||
enum: ["celsius", "fahrenheit"],
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
}
|
||||
|
||||
class ToolCallingAgent extends AbstractAgent {
|
||||
run(input: RunAgentInput): RunAgent {
|
||||
const { threadId, runId } = input
|
||||
|
||||
return () =>
|
||||
new Observable<BaseEvent>((observer) => {
|
||||
observer.next({
|
||||
type: EventType.RUN_STARTED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
// 模拟 Agent 分析用户请求后决定调用工具
|
||||
const toolCallId = `tool_${Date.now()}`
|
||||
const messageId = `msg_${Date.now()}`
|
||||
|
||||
// 1. 发送文本消息说明
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId,
|
||||
role: "assistant",
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId,
|
||||
delta: "Let me check the weather for you.",
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId,
|
||||
})
|
||||
|
||||
// 2. 开始工具调用
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_START,
|
||||
toolCallId,
|
||||
toolCallName: "get_weather",
|
||||
parentMessageId: messageId,
|
||||
})
|
||||
|
||||
// 3. 流式传输参数(分块发送)
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_ARGS,
|
||||
toolCallId,
|
||||
delta: '{"loc', // 参数片段 1
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_ARGS,
|
||||
toolCallId,
|
||||
delta: 'ation":', // 参数片段 2
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_ARGS,
|
||||
toolCallId,
|
||||
delta: ' "San Francisco"}', // 参数片段 3
|
||||
})
|
||||
|
||||
// 4. 参数传输完成
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_END,
|
||||
toolCallId,
|
||||
})
|
||||
|
||||
// 5. 工具执行结果(模拟)
|
||||
setTimeout(() => {
|
||||
observer.next({
|
||||
type: EventType.TOOL_CALL_RESULT,
|
||||
toolCallId,
|
||||
content: JSON.stringify({
|
||||
location: "San Francisco",
|
||||
temperature: "18°C",
|
||||
condition: "Partly cloudy",
|
||||
}),
|
||||
})
|
||||
|
||||
// 6. 基于工具结果的响应
|
||||
const responseMsgId = `msg_${Date.now()}_response`
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId: responseMsgId,
|
||||
role: "assistant",
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId: responseMsgId,
|
||||
delta: "The current weather in San Francisco is 18°C and partly cloudy.",
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId: responseMsgId,
|
||||
})
|
||||
|
||||
observer.next({
|
||||
type: EventType.RUN_FINISHED,
|
||||
threadId,
|
||||
runId,
|
||||
})
|
||||
|
||||
observer.complete()
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const agent = new ToolCallingAgent()
|
||||
|
||||
agent
|
||||
.runAgent({
|
||||
runId: "run_tool_example",
|
||||
threadId: "thread_123",
|
||||
messages: [
|
||||
{
|
||||
id: "user_1",
|
||||
role: "user",
|
||||
content: "What's the weather in San Francisco?",
|
||||
},
|
||||
],
|
||||
tools: [weatherTool as any],
|
||||
context: [],
|
||||
})
|
||||
.subscribe({
|
||||
next: (event) => {
|
||||
switch (event.type) {
|
||||
case EventType.TOOL_CALL_START:
|
||||
console.log(`[Tool Call] Starting: ${(event as any).toolCallName}`)
|
||||
break
|
||||
case EventType.TOOL_CALL_ARGS:
|
||||
process.stdout.write((event as any).delta)
|
||||
break
|
||||
case EventType.TOOL_CALL_END:
|
||||
console.log("\n[Tool Call] Arguments complete")
|
||||
break
|
||||
case EventType.TOOL_CALL_RESULT:
|
||||
console.log("[Tool Result]", (event as any).content)
|
||||
break
|
||||
default:
|
||||
console.log(`[${event.type}]`)
|
||||
}
|
||||
},
|
||||
complete: () => console.log("Tool call flow completed"),
|
||||
})
|
||||
|
||||
export { ToolCallingAgent, weatherTool }
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
name: crewai
|
||||
description: CrewAI framework for multi-agent orchestration. Use when building multi-agent systems, agent collaboration, task automation, crew orchestration, agent teams, delegation, or any CrewAI-related development.
|
||||
---
|
||||
|
||||
# CrewAI Skills
|
||||
|
||||
CrewAI 框架开发权威指南。**必须使用**场景:构建多 Agent 协作系统、编排 Agent 团队、任务自动化、工具集成、知识管理、LLM 应用开发。提供完整模块索引与源文件行号映射。
|
||||
|
||||
## 何时使用
|
||||
|
||||
**必须使用**的场景:
|
||||
- 创建和管理 AI Agent 团队
|
||||
- 实现 Agent 间的协作和任务委托
|
||||
- 编排多步骤工作流和任务流程
|
||||
- 集成 RAG、向量存储和知识管理
|
||||
- 配置和管理 LLM 提供商
|
||||
- 构建自动化任务和工具
|
||||
- 实现 Agent 记忆和推理能力
|
||||
- 训练和微调 Agent 性能
|
||||
|
||||
**查询模式**:
|
||||
- "如何创建 crewai agent"
|
||||
- "agent collaboration 委托任务"
|
||||
- "crew kickoff 执行流程"
|
||||
- "crewai tools 集成"
|
||||
- "knowledge RAG 配置"
|
||||
- "llm 多模型切换"
|
||||
|
||||
## 模块索引
|
||||
|
||||
按功能模块查看源文件对应章节:
|
||||
|
||||
| 模块 | 作用 | 源文件行号 |
|
||||
|------|------|------------|
|
||||
| [agents](modules/agents.md) | Agent 概念、属性、创建、高级特性 | 1-1276 |
|
||||
| [collaboration](modules/collaboration.md) | Agent 协作、委托、层级管理 | 1277-1655 |
|
||||
| [crews](modules/crews.md) | Crew 概念、创建、执行流程 | 1656-2658 |
|
||||
| [flows](modules/flows.md) | Flow 流程控制、状态管理 | 2659-3712 |
|
||||
| [knowledge](modules/knowledge.md) | 知识管理、向量存储、RAG | 3713-4838 |
|
||||
| [llms](modules/llms.md) | LLM 配置、多模型支持 | 4839-6469 |
|
||||
| [memory](modules/memory.md) | Memory 记忆系统 | 6470-7341 |
|
||||
| [planning](modules/planning.md) | Planning 任务规划 | 7342-7729 |
|
||||
| [reasoning](modules/reasoning.md) | Reasoning 推理和反思 | 7730-7877 |
|
||||
| [tasks](modules/tasks.md) | Task 概念、属性、执行流程 | 7878-9005 |
|
||||
| [tools](modules/tools.md) | Tool 概念、创建、内置工具 | 9006-9292 |
|
||||
| [training](modules/training.md) | Training 训练和微调 | 9293-12843 |
|
||||
| [installation](modules/installation.md) | 安装、配置、项目创建 | 12844-14875 |
|
||||
| [quickstart-tools](modules/quickstart-tools.md) | **快速开始 + 50+ 工具参考** | 14876-53221 |
|
||||
|
||||
## 源文件
|
||||
|
||||
- `llms-full.md` - CrewAI 完整文档(唯一信源,53221 行)
|
||||
|
||||
## 核心概念速查
|
||||
|
||||
| 概念 | 说明 | 详见 |
|
||||
|------|------|------|
|
||||
| **Agent** | 自主单元,执行任务、使用工具、协作 | [agents](modules/agents.md) |
|
||||
| **Task** | Agent 完成的具体任务 | [tasks](modules/tasks.md) |
|
||||
| **Crew** | Agent 团队,协作完成任务 | [crews](modules/crews.md) |
|
||||
| **Tool** | Agent 可用的能力或功能 | [tools](modules/tools.md) |
|
||||
| **Flow** | 工作流编排和状态管理 | [flows](modules/flows.md) |
|
||||
| **Knowledge** | 知识存储和 RAG 检索 | [knowledge](modules/knowledge.md) |
|
||||
|
||||
## 快速路径
|
||||
|
||||
**新手入门**:
|
||||
1. [installation](modules/installation.md) - 安装和项目创建
|
||||
2. [agents](modules/agents.md) - 理解 Agent 核心概念
|
||||
3. [tasks](modules/tasks.md) - 创建第一个 Task
|
||||
4. [crews](modules/crews.md) - 组建 Crew 并执行
|
||||
5. [quickstart-tools](modules/quickstart-tools.md) - 完整快速开始示例
|
||||
|
||||
**实现功能**:
|
||||
- Agent 协作 → [collaboration](modules/collaboration.md) (委托、层级)
|
||||
- 任务编排 → [crews](modules/crews.md) + [flows](modules/flows.md)
|
||||
- 知识管理 → [knowledge](modules/knowledge.md) (RAG、向量存储)
|
||||
- 工具集成 → [tools](modules/tools.md) + [quickstart-tools](modules/quickstart-tools.md)
|
||||
- LLM 配置 → [llms](modules/llms.md)
|
||||
|
||||
**高级特性**:
|
||||
- Agent 记忆 → [memory](modules/memory.md)
|
||||
- 任务规划 → [planning](modules/planning.md)
|
||||
- 推理能力 → [reasoning](modules/reasoning.md)
|
||||
- 性能优化 → [training](modules/training.md)
|
||||
|
||||
## 建议使用方式
|
||||
|
||||
1. 先阅读 [installation](modules/installation.md) 了解安装和项目结构
|
||||
2. 根据需求查看核心概念模块(agents/tasks/crews/tools)
|
||||
3. 高级功能参考对应模块(collaboration/knowledge/flows)
|
||||
4. 工具集成参考 [quickstart-tools](modules/quickstart-tools.md)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
# Agents
|
||||
|
||||
**作用**: 介绍 CrewAI 中 Agent 的概念、属性、创建方式和高级特性。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 1-1276
|
||||
|
||||
**内容索引**:
|
||||
- Overview of an Agent - Agent 定义和核心能力 (行 6-22)
|
||||
- Agent Attributes - 完整属性表 (行 37-68)
|
||||
- Creating Agents (行 70-741):
|
||||
- YAML Configuration (推荐) (行 74-115)
|
||||
- Direct Code Definition (行 117-155)
|
||||
- 完整参数示例 (行 156-357)
|
||||
- Tools 配置 (行 358-365)
|
||||
- Context Management (行 366-543)
|
||||
- Structured Output (行 578-741)
|
||||
- CLI - 命令行工具 (行 742-1276)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Collaboration
|
||||
|
||||
**作用**: 介绍 Agent 间的协作机制,包括任务委托、层级管理和最佳实践。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 1277-1655
|
||||
|
||||
**内容索引**:
|
||||
- Enable collaboration for agents - 启用协作 (行 1291-1323)
|
||||
- Delegation Tools - 委托工具 (行 1325-1343):
|
||||
- Delegate work to coworker (行 1326)
|
||||
- Ask question to coworker (行 1335)
|
||||
- Implementation Examples (行 1345-1445):
|
||||
- Collaborative agents 示例 (行 1345-1386)
|
||||
- Hierarchical crew 示例 (行 1447-1481)
|
||||
- Best Practices (行 1495-1597):
|
||||
- 角色定义 (行 1495-1514)
|
||||
- 任务依赖 (行 1525-1545)
|
||||
- 常见问题和解决方案 (行 1556-1597)
|
||||
- Collaboration Guidelines (行 1598-1655)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Crews
|
||||
|
||||
**作用**: 介绍 Crew 的概念、创建方式、执行流程和高级特性。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 1656-2658
|
||||
|
||||
**内容索引**:
|
||||
- Crew Overview - Crew 定义 (行 1656-1891)
|
||||
- Example crew execution - 执行示例 (行 1892-1900)
|
||||
- Accessing crew output (行 1901-1918)
|
||||
- Save crew logs (行 1919-1938)
|
||||
- Usage metrics (行 1939-1954)
|
||||
- Execution Methods (行 1955-2024):
|
||||
- kickoff() - 同步执行 (行 1955-1988)
|
||||
- kickoff_for_each() - 批量执行 (行 1989-2010)
|
||||
- kickoff_async() - 异步执行 (行 1995-2024)
|
||||
- Streaming - 流式输出 (行 2025-2073)
|
||||
- Event Listeners - 事件监听器 (行 2074-2245)
|
||||
- Files - 文件处理 (行 2387-2658)
|
||||
@@ -1,13 +0,0 @@
|
||||
# Flows
|
||||
|
||||
**作用**: 介绍 Flow 的概念、状态管理、流程控制和与 Crew 的集成。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 2659-3712
|
||||
|
||||
**内容索引**:
|
||||
- Flow Overview - Flow 定义和用途 (行 2659-3293)
|
||||
- Structured Output - 结构化输出 (行 3294-3371)
|
||||
- Usage example (行 3372-3379)
|
||||
- Run the flow (行 3380-3538)
|
||||
- Streaming - 流式输出 (行 3539-3712)
|
||||
@@ -1,13 +0,0 @@
|
||||
# Installation
|
||||
|
||||
**作用**: 介绍 CrewAI 的安装、配置和项目创建。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 12844-14875
|
||||
|
||||
**内容索引**:
|
||||
- Installation Guide - 安装指南 (行 12844-12944)
|
||||
- Creating a CrewAI Project - 项目创建 (行 12945-13047)
|
||||
- What is CrewAI? - 框架介绍 (行 13053-13179)
|
||||
- MCP Integration - MCP 集成 (行 13763-14450)
|
||||
- Quickstart Guide - 快速开始 (行 14876-15252)
|
||||
@@ -1,17 +0,0 @@
|
||||
# Knowledge
|
||||
|
||||
**作用**: 介绍 Knowledge 的概念、知识源、向量存储和 RAG 集成。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 3713-4838
|
||||
|
||||
**内容索引**:
|
||||
- Knowledge Overview - 知识管理概念 (行 3713-3750)
|
||||
- Vector Stores - 向量存储 (行 3751-3780):
|
||||
- ChromaDB (默认) (行 3751-3755)
|
||||
- Qdrant (行 3756-3760)
|
||||
- Knowledge Sources (行 3781-3952):
|
||||
- 创建知识源 (行 3781-3824)
|
||||
- Web content (行 3825-3835)
|
||||
- Agent-level vs Crew-level (行 3953-4000)
|
||||
- Advanced Usage (行 4001-4838)
|
||||
@@ -1,11 +0,0 @@
|
||||
# LLMs
|
||||
|
||||
**作用**: 介绍 LLM 配置、多模型支持、自定义 LLM 和最佳实践。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 4839-6469
|
||||
|
||||
**内容索引**:
|
||||
- LLM Overview - LLM 配置概念 (行 4839-6281)
|
||||
- Supported Providers - 支持的 LLM 提供商 (行 6282-6469)
|
||||
- Custom LLM Integration (行 6990-7496)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Memory
|
||||
|
||||
**作用**: 介绍 Memory 的概念、类型、配置和使用场景。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 6470-7341
|
||||
|
||||
**内容索引**:
|
||||
- Memory Overview - 记忆系统概念 (行 6470-7341)
|
||||
- Memory Types - 记忆类型 (行 6470-7341)
|
||||
- Configuration and Usage (行 6470-7341)
|
||||
@@ -1,10 +0,0 @@
|
||||
# Planning
|
||||
|
||||
**作用**: 介绍 Planning 功能、任务规划和自动化流程设计。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 7342-7729
|
||||
|
||||
**内容索引**:
|
||||
- Planning Overview - 任务规划概念 (行 7342-7571)
|
||||
- Flow-First Mindset - 流程优先思维 (行 7572-7729)
|
||||
@@ -1,52 +0,0 @@
|
||||
# Quickstart & Tools Reference
|
||||
|
||||
**作用**: 快速开始指南和完整的工具集成参考。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 14876-53221
|
||||
|
||||
**内容索引**:
|
||||
- Quickstart Guide - 快速开始 (行 14876-15252)
|
||||
- Core Tools (行 15253-16645):
|
||||
- AI Mind Tool (行 15253-15338)
|
||||
- Code Interpreter (行 15373-15582)
|
||||
- DALL-E Tool (行 15583-15635)
|
||||
- LangChain Tool (行 15636-15694)
|
||||
- LlamaIndex Tool (行 15695-15841)
|
||||
- RAG Tool (行 15906-16595)
|
||||
- Vision Tool (行 16596-16645)
|
||||
- Cloud & Database Tools (行 16646-18003):
|
||||
- MongoDB Vector Search (行 16698-16864)
|
||||
- MySQL RAG Search (行 16865-16934)
|
||||
- NL2SQL Tool (行 16935-17104)
|
||||
- PG RAG Search (行 17105-17187)
|
||||
- Qdrant Vector Search (行 17188-17542)
|
||||
- SingleStore Search (行 17543-17602)
|
||||
- Snowflake Search (行 17603-17732)
|
||||
- Weaviate Vector Search (行 17806-17973)
|
||||
- File Tools (行 17974-18868):
|
||||
- CSV RAG Search (行 17974-18051)
|
||||
- Directory Read (行 18052-18105)
|
||||
- Directory RAG Search (行 18106-18175)
|
||||
- DOCX RAG Search (行 18176-18255)
|
||||
- File Read (行 18256-18300)
|
||||
- File Write (行 18301-18351)
|
||||
- JSON RAG Search (行 18352-18427)
|
||||
- MDX RAG Search (行 18428-18499)
|
||||
- OCR Tool (行 18500-18684)
|
||||
- PDF Text Writing (行 18685-18759)
|
||||
- PDF RAG Search (行 18760-18868)
|
||||
- TXT RAG Search (行 18869-18966)
|
||||
- XML RAG Search (行 18967-19044)
|
||||
- Search & Research Tools (行 19045-20000+):
|
||||
- Tools Overview (行 19045-19128)
|
||||
- Arxiv Paper Tool (行 19151-19262)
|
||||
- Brave Search (行 19263-19341)
|
||||
- Code Docs RAG Search (行 19360-19445)
|
||||
- Databricks SQL Query (行 19446-19524)
|
||||
- EXA Search Web Loader (行 19525-19579)
|
||||
- Github Search (行 19580-19666)
|
||||
- Linkup Search (行 19667-19737)
|
||||
- SerpApi Tools (行 19738-20000+)
|
||||
|
||||
**注**: 此章节包含 50+ 工具的详细文档,按类别组织
|
||||
@@ -1,10 +0,0 @@
|
||||
# Reasoning
|
||||
|
||||
**作用**: 介绍 Reasoning 功能、反思机制和任务执行前的规划。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 7730-7877
|
||||
|
||||
**内容索引**:
|
||||
- Reasoning Overview - 推理和反思概念 (行 7730-7877)
|
||||
- Configuration (行 7730-7877)
|
||||
@@ -1,20 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
**作用**: 介绍 Task 的概念、属性、创建方式、执行流程和高级特性。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 7878-9005
|
||||
|
||||
**内容索引**:
|
||||
- Task Overview - Task 定义 (行 7883-7902)
|
||||
- Task Execution Flow - 执行流程 (行 7904-7919)
|
||||
- Task Attributes - 完整属性表 (行 7921-7994)
|
||||
- Creating Tasks (行 7995-8277):
|
||||
- YAML Configuration (行 7995-8125)
|
||||
- Direct Code Definition (行 8126-8161)
|
||||
- Markdown formatting (行 8162-8277)
|
||||
- Advanced Features (行 8278-9005):
|
||||
- Guardrails - 任务护栏 (行 8396-8547)
|
||||
- Callbacks (行 8548-8657)
|
||||
- Output files (行 8658-8773)
|
||||
- Execute tasks (行 9112-9227)
|
||||
@@ -1,13 +0,0 @@
|
||||
# Tools
|
||||
|
||||
**作用**: 介绍 Tool 的概念、创建方式、内置工具和自定义工具。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 9006-9292
|
||||
|
||||
**内容索引**:
|
||||
- Tools Overview - 工具概念 (行 9006-9292)
|
||||
- Creating Custom Tools (行 9006-9292)
|
||||
- Built-in Tools List (行 9006-9292)
|
||||
|
||||
**注**: 完整的工具集成文档见 Quickstart 章节 (行 14876-53221)
|
||||
@@ -1,12 +0,0 @@
|
||||
# Training
|
||||
|
||||
**作用**: 介绍 Training 功能、模型训练、微调和性能优化。
|
||||
|
||||
**源文件**: `llms-full.md`
|
||||
**行号范围**: 9293-12843
|
||||
|
||||
**内容索引**:
|
||||
- Training Overview - 训练概念 (行 9293-9494)
|
||||
- Training Methods (行 9495-12843)
|
||||
- Best Practices (行 9616-9725)
|
||||
- Custom Templates (行 9953-10166)
|
||||
@@ -1,651 +0,0 @@
|
||||
# Plan: social-app 数据库数据模型重设计(支持社交/事项/自动化)
|
||||
|
||||
**Date:** 2026-02-26
|
||||
**Author:** AI Assistant
|
||||
**Status:** Draft
|
||||
|
||||
## 枚举存储约定
|
||||
|
||||
**统一使用枚举名称(字符串)存储,不使用整数值。**
|
||||
|
||||
- 数据库层:`VARCHAR(20)` + `CHECK` 约束
|
||||
- 代码层:Python `Enum` 类继承 `str`
|
||||
- 优势:调试可读、易扩展(新增枚举值无需迁移旧数据)、ORM 友好
|
||||
|
||||
```python
|
||||
class AgentType(str, Enum):
|
||||
INTENT_RECOGNITION = "INTENT_RECOGNITION"
|
||||
TASK_EXECUTION = "TASK_EXECUTION"
|
||||
RESULT_REPORTING = "RESULT_REPORTING"
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Migration
|
||||
ALTER TABLE user_agents ADD CONSTRAINT chk_agent_type
|
||||
CHECK (agent_type IN ('INTENT_RECOGNITION', 'TASK_EXECUTION', 'RESULT_REPORTING'));
|
||||
```
|
||||
|
||||
## 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 形态演进 |
|
||||
| 记忆系统采用单表 + memory_type 区分 | user 类型可选 agent_id,work 类型必须绑定 agent_id |
|
||||
| 好友关系用单表双向规范化表示 | 避免 A-B / B-A 重复,降低去重成本 |
|
||||
| 事项权限采用 ACL 表而非仅 owner | 满足“仅特定人可修改”的协作场景 |
|
||||
| 待办采用主表 + 关联表 | `todos` + `todo_sources` 保证来源关系可校验 |
|
||||
| 自动化采用 Jobs 单表 + Sessions 关联 | `sessions` 通过 `session_type + job_id` 区分普通对话与自动化运行 |
|
||||
| inbox 采用单表接收者视角 | 发送者 + 消息类型 + 关联业务,一表搞定待处理消息 |
|
||||
|
||||
## A. 设计原则与边界
|
||||
|
||||
### 1) 核心实体与聚合边界
|
||||
- 用户聚合:`profiles`(含 settings JSONB), `user_agents`, `memories`
|
||||
- 社交聚合:`friendships`, `groups`, `group_members`
|
||||
- 协作事项聚合:`schedule_items`, `schedule_subscriptions`(当前仅用户主体)
|
||||
- 消息聚合:`inbox_messages`
|
||||
- 待办聚合:`todos`
|
||||
- 自动化聚合:`automation_jobs`
|
||||
|
||||
### 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 (1) - (N) memories`
|
||||
- `user_agents (1) - (N) memories`(work 类型)
|
||||
- `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_subscriptions`,`auth.users (1) - (N) schedule_subscriptions`
|
||||
- `auth.users (1) - (N) inbox_messages`
|
||||
- `auth.users (1) - (N) todos`
|
||||
- `auth.users (1) - (N) automation_jobs`
|
||||
- `automation_jobs (1) - (N) sessions`(通过 `sessions.job_id` 关联)
|
||||
|
||||
### 关键约束
|
||||
- 唯一性:
|
||||
- `user_agents.user_id` 唯一
|
||||
- `friendships(user_low_id, user_high_id)` 唯一
|
||||
- `group_members(group_id, user_id)` 唯一
|
||||
- `schedule_subscriptions(item_id, subscriber_id)` 唯一
|
||||
- CHECK:
|
||||
- `friendships`: `user_low_id < user_high_id` 且 `user_low_id <> user_high_id`
|
||||
- `schedule_subscriptions`: `permission BETWEEN 0 AND 7`
|
||||
- `memories`: `work` 类型必须有 `agent_id`,`user` 类型必须无 `agent_id`
|
||||
- `sessions`: `session_type/job_id` 组合一致
|
||||
- 外键:统一显式 `ON DELETE` 策略(见下)
|
||||
- 可空性:权限关键字段、状态字段默认 `NOT NULL`
|
||||
- 删除策略:
|
||||
- 用户删除:大部分 `CASCADE`(用户私有数据);跨用户协作数据优先软删
|
||||
- 事项删除:对子表 `CASCADE`;待办保留历史,改 `status = 'archived'`
|
||||
|
||||
### 外键删除策略明细(必做)
|
||||
- `sessions.job_id -> automation_jobs.id`: `ON DELETE RESTRICT`
|
||||
- `todo_sources.todo_id -> todos.id`: `ON DELETE CASCADE`
|
||||
- `todo_sources.schedule_item_id -> schedule_items.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.friendship_id -> friendships.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.schedule_item_id -> schedule_items.id`: `ON DELETE CASCADE`
|
||||
- `inbox_messages.group_id -> groups.id`: `ON DELETE CASCADE`
|
||||
|
||||
## C. 数据库表设计(PostgreSQL)
|
||||
|
||||
以下为推荐主表(方案 1,规范化优先)。字段示例采用 `UUID + timestamptz + enum/text-check`。
|
||||
|
||||
### 1) 用户与 agent
|
||||
|
||||
#### `profiles`(已有,建议补齐)
|
||||
- PK: `id UUID` (`auth.users.id`)
|
||||
- 关键字段: `username`, `avatar_url`, `bio`
|
||||
- **新增 JSONB 字段**:
|
||||
- `settings JSONB`(用户自定义设置,含 `version`, `preferences`, `privacy`, `notification` 四大块)
|
||||
- 时间字段: `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 配置参数)
|
||||
- 时间字段: `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`
|
||||
|
||||
#### `memories`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`(用户,NOT NULL)
|
||||
- `agent_id`(work 类型时必需)
|
||||
- `memory_type`(枚举:`user | work`)
|
||||
- `title`
|
||||
- `content`(JSONB,存储具体记忆结构)
|
||||
- `source`(`manual | agent | imported`)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | disabled`)
|
||||
- 索引:
|
||||
- `INDEX(owner_id, memory_type, status)`
|
||||
- `INDEX(agent_id, memory_type, status)`
|
||||
- `GIN(content)`(支持 JSONB 内容查询)
|
||||
- 约束: `CHECK ((memory_type = 'work' AND agent_id IS NOT NULL) OR (memory_type = 'user' AND agent_id IS NULL))`
|
||||
|
||||
**memory_type 说明**:
|
||||
| 类型 | agent_id | 说明 |
|
||||
|------|----------|------|
|
||||
| `user` | 可空 | 用户记忆:偏好、背景信息、实体等 |
|
||||
| `work` | 必需 | 工作记忆:长期运行后对工作流程的经验整理,避免重复错误 |
|
||||
|
||||
**content JSONB 示例**:
|
||||
```json
|
||||
// 用户记忆
|
||||
{"type": "preference", "data": {"style": "concise", "language": "zh-CN"}}
|
||||
|
||||
// 工作记忆
|
||||
{"type": "workflow_summary", "data": {"task": "代码审查", "learnings": ["优先检查安全漏洞", "关注性能热点"], "improvements": []}}
|
||||
```
|
||||
|
||||
### 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`, `owner_id`
|
||||
- 时间字段: `created_at`, `updated_at`, `deleted_at`
|
||||
- 状态字段: `status`(`active|archived`)
|
||||
- 索引: `INDEX(owner_id, status)`
|
||||
- 审计: `created_by`, `updated_by`
|
||||
|
||||
#### `group_members`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `group_id`, `user_id`
|
||||
- `role`(枚举:`owner` | `admin` | `member`)
|
||||
- `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, role, status)`
|
||||
- `INDEX(user_id, status)`
|
||||
- 审计: `created_by`, `updated_by`
|
||||
|
||||
**role 说明**:
|
||||
| role | 含义 |
|
||||
|------|------|
|
||||
| `owner` | 群主/创建者 |
|
||||
| `admin` | 管理员 |
|
||||
| `member` | 普通成员 |
|
||||
|
||||
- 角色可升降:服务层变更 role 字段即可
|
||||
|
||||
### 3) 用户设置(已合并至 profiles 表)
|
||||
|
||||
用户设置采用 JSONB 内嵌方式,渐进式扩展无需改表结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"preferences": {
|
||||
"interface_language": "zh-CN",
|
||||
"ai_language": "zh-CN",
|
||||
"timezone": "Asia/Shanghai"
|
||||
},
|
||||
"privacy": {},
|
||||
"notification": {}
|
||||
}
|
||||
```
|
||||
- 索引策略:对高频查询字段使用表达式索引
|
||||
- 更新方式:服务层使用 JSONB merge 或字段级 UPDATE,避免读-改-写并发问题(建议用 `jsonb_set` 原子操作)
|
||||
|
||||
### 4) 事项与订阅/权限
|
||||
|
||||
#### `schedule_items`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`
|
||||
- `title`
|
||||
- `description`
|
||||
- `start_at`
|
||||
- `end_at`
|
||||
- `timezone`(用于将日程时间转换为用户本地时间显示)
|
||||
- `metadata`(JSONB,扩展字段)
|
||||
- `recurrence_rule`(可选,支持循环日程)
|
||||
- `source_type`(`manual | imported | agent_generated`)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | completed | canceled | archived`)
|
||||
- 索引:
|
||||
- `INDEX(owner_id, start_at)`
|
||||
- `INDEX(status, start_at)`
|
||||
- 审计: `created_by`
|
||||
|
||||
**metadata JSONB 示例**:
|
||||
```json
|
||||
{
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得提前准备投影仪",
|
||||
"attachments": [
|
||||
{
|
||||
"name": "会议纪要.pdf",
|
||||
"url": "https://...",
|
||||
"visible_to": [],
|
||||
"type": "document"
|
||||
},
|
||||
{
|
||||
"name": "投影仪提醒",
|
||||
"visible_to": ["uuid1"],
|
||||
"type": "reminder",
|
||||
"content": "记得带投影仪"
|
||||
},
|
||||
{
|
||||
"name": "技术方案.docx",
|
||||
"url": "https://...",
|
||||
"visible_to": ["uuid2"],
|
||||
"type": "document",
|
||||
"note": "需要他确认预算"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
| type | 说明 | 特殊字段 |
|
||||
|------|------|----------|
|
||||
| document | 文档/文件 | url, note |
|
||||
| reminder | 提醒 | content |
|
||||
|
||||
#### `schedule_subscriptions`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `item_id`
|
||||
- `subscriber_id`
|
||||
- `permission`(INTEGER,用位运算存储权限组合,`NOT NULL DEFAULT 1`)
|
||||
- `notify_level`(`all | mentions | none`,`NOT NULL DEFAULT 'all'`)
|
||||
- 时间字段: `created_at`
|
||||
- 状态字段: `status`(`active | paused | unsubscribed`,`NOT NULL DEFAULT 'active'`)
|
||||
- 约束: `UNIQUE(item_id, subscriber_id)`
|
||||
- 约束补充: `CHECK(permission BETWEEN 0 AND 7)`(`view=1, invite=2, edit=4`,`0` 表示无权限)
|
||||
- 索引: `INDEX(subscriber_id, status)`, `INDEX(item_id, status)`
|
||||
- 审计: `created_by`
|
||||
|
||||
**权柄说明(位运算)**:
|
||||
| 权柄 | 值 | 二进制 | 说明 |
|
||||
|------|-----|--------|------|
|
||||
| view | 1 | 001 | 查看事项详情 |
|
||||
| invite | 2 | 010 | 邀请其他人订阅此事项 |
|
||||
| edit | 4 | 100 | 修改事项内容、管理订阅 |
|
||||
|
||||
- 权限检查:`permission & 2 = 2` 检查是否有 invite 权限
|
||||
- 权限添加:`permission | 2` 添加 invite 权限
|
||||
- 事项 owner 默认拥有全部权柄:`7`(111)
|
||||
- owner 权柄由服务层恒等判定为 `7`,不依赖 owner 是否在 `schedule_subscriptions` 中存在记录
|
||||
|
||||
**当前版本边界**:
|
||||
- `schedule_subscriptions` 仅支持用户订阅(`subscriber_id -> auth.users.id`)
|
||||
- 事项协作暂不引入群主体授权
|
||||
|
||||
### 5) 待处理消息(Inbox)
|
||||
|
||||
#### `inbox_messages`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `recipient_id`(接收者)
|
||||
- `sender_id`(发送者,系统消息可为 NULL)
|
||||
- `message_type`(枚举:`friend_request | calendar | system | group`)
|
||||
- `friendship_id`(可空,`friend_request` 时必填)
|
||||
- `schedule_item_id`(可空,`calendar` 时必填)
|
||||
- `group_id`(可空,`group` 时必填)
|
||||
- `content`(TEXT,消息内容,系统消息用)
|
||||
- 时间字段: `created_at`
|
||||
- 状态字段:
|
||||
- `is_read`(BOOLEAN,是否已读)
|
||||
- `status`(`pending | accepted | rejected | dismissed`)
|
||||
- 索引:
|
||||
- `INDEX(recipient_id, status, created_at DESC)`
|
||||
- 部分索引 `INDEX(recipient_id, created_at DESC) WHERE status='pending'`
|
||||
- 审计: `created_by`
|
||||
|
||||
**message_type 与业务字段对应关系**:
|
||||
| message_type | 对应业务字段 |
|
||||
|--------------|-----------------|
|
||||
| friend_request | friendship_id -> friendships.id |
|
||||
| calendar | schedule_item_id -> schedule_items.id |
|
||||
| system | 三个业务字段均为 NULL(内容直接在 content) |
|
||||
| group | group_id -> groups.id |
|
||||
|
||||
**说明**:一张表搞定,接收者视角,通过 `message_type + 对应业务字段` 直接定位要处理的业务,避免单列多态外键带来的引用不一致问题。
|
||||
|
||||
**一致性约束(必做)**:
|
||||
- 使用 `CHECK` 保证不同 `message_type` 下仅允许对应业务字段非空(`system` 时业务字段全空)
|
||||
- 使用 `CHECK` 保证 `message_type='system'` 时 `sender_id IS NULL`,否则 `sender_id IS NOT NULL`
|
||||
- `friendship_id`、`schedule_item_id`、`group_id` 分别建立 FK,并显式声明 `ON DELETE` 策略
|
||||
|
||||
### 6) 待办
|
||||
|
||||
#### `todos`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`
|
||||
- `title`
|
||||
- `description`
|
||||
- `due_at`
|
||||
- `priority`(INTEGER,用于四象限:1=重要且紧急, 2=重要不紧急, 3=紧急不重要, 4=不重要不紧急)
|
||||
- 时间字段: `created_at`, `completed_at`
|
||||
- 状态字段: `status`(`pending | done | canceled`)
|
||||
- 索引:
|
||||
- `INDEX(owner_id, status, due_at)`
|
||||
- `INDEX(owner_id, created_at DESC)`
|
||||
- 部分索引 `INDEX(owner_id, due_at) WHERE status='pending'`
|
||||
- 审计: `created_by`
|
||||
|
||||
#### `todo_sources`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `todo_id`(FK -> todos.id)
|
||||
- `schedule_item_id`(FK -> schedule_items.id)
|
||||
- 时间字段: `created_at`
|
||||
- 约束: `UNIQUE(todo_id, schedule_item_id)`
|
||||
- 索引: `INDEX(todo_id)`, `INDEX(schedule_item_id)`
|
||||
|
||||
**说明**:
|
||||
- 手动创建待办:不写 `todo_sources`
|
||||
- 从事项提取待办:写入 `todo_sources`,替代 JSONB 数组,保证来源关系可校验
|
||||
|
||||
### 7) 自动化定时任务
|
||||
|
||||
#### `automation_jobs`
|
||||
- PK: `id UUID`
|
||||
- 关键字段:
|
||||
- `owner_id`
|
||||
- `title`(任务标题)
|
||||
- `prompt`(AI 执行的 prompt)
|
||||
- `schedule_type`(枚举:`daily | weekly`)
|
||||
- `run_at`(首次运行时间)
|
||||
- `next_run_at`(下次运行时间,调度器扫描主字段)
|
||||
- `timezone`(时区,如 `Asia/Shanghai`)
|
||||
- `last_run_at`(最近运行时间,可空)
|
||||
- 时间字段: `created_at`, `updated_at`
|
||||
- 状态字段: `status`(`active | disabled`)
|
||||
- 索引: `INDEX(owner_id, status)`, `INDEX(status, next_run_at)`
|
||||
- 约束补充: `UNIQUE(id, owner_id)`(用于 `sessions(job_id, user_id)` 归属一致性外键)
|
||||
- 审计: `created_by`
|
||||
|
||||
**说明**:定时任务执行时,在 sessions 表创建记录存储 AI 对话内容。
|
||||
|
||||
### 8) 会话表扩展(已有 sessions)
|
||||
|
||||
#### `sessions`(更新)
|
||||
- 新增字段:
|
||||
- `session_type`(`chat | automation`)
|
||||
- `job_id`(可空,FK -> automation_jobs.id)
|
||||
- 一致性约束:
|
||||
- `CHECK((session_type = 'chat' AND job_id IS NULL) OR (session_type = 'automation' AND job_id IS NOT NULL))`
|
||||
- 通过复合 FK 约束归属一致性:`FOREIGN KEY(job_id, user_id) -> automation_jobs(id, owner_id)`
|
||||
- 索引:
|
||||
- `INDEX(user_id, session_type, last_activity_at DESC)`
|
||||
- `INDEX(job_id)`
|
||||
|
||||
## D. 权限与协作模型
|
||||
|
||||
### 1) 事项权限落表
|
||||
- 权限直接存储在 `schedule_subscriptions.permission` 整数中(位运算)
|
||||
- owner 不写入 `schedule_subscriptions`,owner 权限仅由 `schedule_items.owner_id` 推导
|
||||
- 权限决策顺序:
|
||||
1. `schedule_items.owner_id` → 服务层恒等全部权柄 `["view", "invite", "edit"]`(7)
|
||||
2. `schedule_subscriptions` 中该用户的 `permission` 位图
|
||||
3. 非 owner 且非 subscriber 默认无权限(0)
|
||||
|
||||
### 2) 当前版本边界
|
||||
- 事项权限仅处理用户主体(owner + subscriber)
|
||||
- 群组与事项权限继承关系不在本期范围
|
||||
|
||||
## E. 消息与待办联动
|
||||
|
||||
### 1) inbox 关联业务对象
|
||||
- `inbox_messages.message_type` 枚举:
|
||||
- `friend_request`(好友请求)→ `friendship_id` 指向 friendships
|
||||
- `calendar`(日程邀请)→ `schedule_item_id` 指向 schedule_items
|
||||
- `system`(系统消息)→ 业务字段均为 NULL
|
||||
- `group`(群组邀请)→ `group_id` 指向 groups
|
||||
- 通过 `message_type + 对应业务字段` 直接定位业务对象,并用 `CHECK` 约束保证字段一致性
|
||||
|
||||
### 2) 待办来源提取
|
||||
- 从事项提取待办时,写入 `todo_sources(todo_id, schedule_item_id)`
|
||||
- 手动创建的待办不写 `todo_sources`
|
||||
- 支持多来源:同一待办可关联多个日程事项(多行 `todo_sources`)
|
||||
- 待办完成时无需反向更新来源事项状态(简化设计)
|
||||
|
||||
## F. 定时任务模型
|
||||
|
||||
### 1) 调度规则
|
||||
- `schedule_type` 枚举:`daily`(每日) | `weekly`(每周)
|
||||
- `run_at` 用于首次执行时间,`next_run_at` 用于后续调度
|
||||
- 调度器扫描 `status='active' AND next_run_at <= now()` 的任务,执行后回写下一次 `next_run_at`
|
||||
- `timezone` 参与下一次执行时间计算,避免时区偏差
|
||||
|
||||
### 2) 执行记录
|
||||
- 每次执行在 sessions 表创建记录,通过 `sessions.job_id` 关联 job
|
||||
- `sessions` 通过 `session_type` 区分 `chat` 与 `automation`
|
||||
- 执行失败时记录在 `automation_jobs`(如 `last_error`,可后续细化)
|
||||
|
||||
## G. 数据库迁移思路
|
||||
|
||||
### 策略:重建数据库 + Alembic ORM 迁移
|
||||
|
||||
由于是全新设计的数据模型,且当前处于开发初期(可清除旧数据),采用**重建数据库**策略:
|
||||
|
||||
**执行门禁(强制)**:
|
||||
- 仅允许在本地开发环境执行
|
||||
- 禁止在生产/共享环境执行 `rm backend/alembic/versions/*.py`
|
||||
- 执行前必须备份数据库或创建 git tag
|
||||
|
||||
1. **删除所有旧 migration 脚本**(保留 `env.py`)
|
||||
2. **创建 ORM 模型文件**
|
||||
3. **生成 Alembic migration**
|
||||
4. **重建数据库并执行迁移**
|
||||
|
||||
### 执行步骤
|
||||
|
||||
1. 删除旧 migration 文件
|
||||
```bash
|
||||
rm backend/alembic/versions/*.py
|
||||
```
|
||||
|
||||
2. 重建空数据库(确保以空库基线生成 initial migration)
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml down -v
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d db
|
||||
```
|
||||
|
||||
3. 创建 ORM 模型文件(参考 `models/` 目录结构)
|
||||
- 新增:`user_agents.py`, `memories.py`, `friendships.py`, `groups.py`, `group_members.py`, `schedule_items.py`, `schedule_subscriptions.py`, `inbox_messages.py`, `todos.py`, `todo_sources.py`, `automation_jobs.py`
|
||||
- 更新:`profile.py` - 添加 `settings` 字段
|
||||
- 更新:`agent_chat_session.py` - 添加 `session_type`、`job_id` 字段
|
||||
- 重写:`create_profile_for_new_user` 触发器,确保 `profiles.settings` 有默认值
|
||||
|
||||
4. 更新 `models/__init__.py` 导出所有模型
|
||||
|
||||
5. 更新 `alembic/env.py` 添加模型导入
|
||||
|
||||
6. 生成 initial migration(以空库为对比基线)
|
||||
```bash
|
||||
cd backend && uv run alembic revision --autogenerate -m "initial schema"
|
||||
```
|
||||
|
||||
7. 为所有新建 `public` 业务表补齐 RLS(`SELECT/INSERT/UPDATE/DELETE` policy)
|
||||
- 每张表都执行 `ENABLE ROW LEVEL SECURITY`
|
||||
- 每张表都显式创建 `SELECT/INSERT/UPDATE/DELETE` policy
|
||||
- downgrade 必须可逆,不得弱化既定安全边界
|
||||
- `anon/authenticated` 默认全部 deny
|
||||
|
||||
RLS 最小策略矩阵(本期统一模板):
|
||||
- `anon`:`SELECT/INSERT/UPDATE/DELETE` 全部 deny
|
||||
- `authenticated`:`SELECT/INSERT/UPDATE/DELETE` 全部 deny
|
||||
- `service_role`:由后端服务连接使用,不依赖 RLS 放行
|
||||
|
||||
8. 执行迁移
|
||||
```bash
|
||||
cd backend && uv run alembic upgrade head
|
||||
```
|
||||
|
||||
9. 验证表结构
|
||||
|
||||
## H. 交付物
|
||||
|
||||
### ORM 模型文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `models/user_agents.py` | 用户专属 agent |
|
||||
| `models/memories.py` | 用户/工作记忆 |
|
||||
| `models/friendships.py` | 好友关系 |
|
||||
| `models/groups.py` | 群组 |
|
||||
| `models/group_members.py` | 群组成员 |
|
||||
| `models/schedule_items.py` | 日程事项 |
|
||||
| `models/schedule_subscriptions.py` | 日程订阅与权限 |
|
||||
| `models/inbox_messages.py` | 待处理消息 |
|
||||
| `models/todos.py` | 待办 |
|
||||
| `models/todo_sources.py` | 待办与事项来源关联 |
|
||||
| `models/automation_jobs.py` | 定时任务 |
|
||||
| `models/profile.py` | 更新:添加 `settings` 字段 |
|
||||
| `models/agent_chat_session.py` | 更新:添加 `session_type`、`job_id` 字段 |
|
||||
|
||||
### 执行步骤
|
||||
|
||||
1. 删除旧 migration 文件
|
||||
```bash
|
||||
rm backend/alembic/versions/*.py
|
||||
```
|
||||
|
||||
2. 重建空数据库(确保以空库基线生成 initial migration)
|
||||
```bash
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml down -v
|
||||
docker compose --env-file .env -f infra/docker/docker-compose.yml up -d db
|
||||
```
|
||||
|
||||
3. 创建/更新 ORM 模型文件
|
||||
|
||||
4. 更新 `models/__init__.py` 导出所有模型
|
||||
|
||||
5. 更新 `alembic/env.py` 添加模型导入
|
||||
|
||||
6. 生成 initial migration(以空库为对比基线)
|
||||
```bash
|
||||
cd backend && uv run alembic revision --autogenerate -m "initial schema"
|
||||
```
|
||||
|
||||
7. 为所有新建 `public` 业务表补齐 RLS(`SELECT/INSERT/UPDATE/DELETE` policy)
|
||||
- 每张表都执行 `ENABLE ROW LEVEL SECURITY`
|
||||
- 每张表都显式创建 `SELECT/INSERT/UPDATE/DELETE` policy
|
||||
- downgrade 必须可逆,不得弱化既定安全边界
|
||||
|
||||
8. 执行迁移
|
||||
```bash
|
||||
cd backend && uv run alembic upgrade head
|
||||
```
|
||||
|
||||
9. 更新测试文件适配新表结构
|
||||
|
||||
## I. 数据库表名规范与审计
|
||||
|
||||
### 1) 命名规范(统一执行)
|
||||
- 使用 `snake_case`
|
||||
- 业务表统一使用复数名词(如 `profiles`, `friendships`, `automation_jobs`)
|
||||
- 关联表使用 `<主实体复数>_<从实体复数>` 或约定俗成复数短语(如 `group_members`, `todo_sources`)
|
||||
- 禁止过于泛化的表名(如 `messages`, `sessions`),必须带业务前缀
|
||||
- 存量历史表允许短期例外,但必须在审计表中登记并给出迁移计划
|
||||
- 缩写保持一致:LLM 统一使用 `llm` 前缀,不混用 `model`/`llm` 两套命名
|
||||
|
||||
### 2) 表名审计结果
|
||||
|
||||
| 当前表名 | 审计结论 | 建议表名 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| `profiles` | 通过 | - | 符合复数名词规范 |
|
||||
| `user_agents` | 通过 | - | 语义清晰 |
|
||||
| `memories` | 通过 | - | 语义清晰 |
|
||||
| `friendships` | 通过 | - | 关系表命名清晰 |
|
||||
| `groups` | 通过 | - | 符合规范 |
|
||||
| `group_members` | 通过 | - | 关联表命名清晰 |
|
||||
| `schedule_items` | 通过 | - | 语义清晰 |
|
||||
| `schedule_subscriptions` | 通过 | - | 语义清晰 |
|
||||
| `inbox_messages` | 通过 | - | 带业务前缀,避免歧义 |
|
||||
| `todos` | 通过 | - | 简洁且清晰 |
|
||||
| `todo_sources` | 通过 | - | 关联关系明确 |
|
||||
| `automation_jobs` | 通过 | - | 语义清晰 |
|
||||
| `llms` | 通过 | - | 与 LLM 语义一致 |
|
||||
| `llm_factory` | 建议调整 | `llm_factories` | 当前为单数,建议改复数以统一规范 |
|
||||
| `sessions` | 建议调整 | `agent_chat_sessions` | 过于泛化,建议加业务前缀 |
|
||||
| `messages` | 建议调整 | `agent_chat_messages` | 过于泛化,建议加业务前缀 |
|
||||
|
||||
### 3) 落地建议
|
||||
- 本期命名边界:不重命名 `llm_factory/sessions/messages`,仅在新表严格执行命名规范
|
||||
- 本期最小可行:先保持现有表名可运行,新增表全部遵循规范
|
||||
- 下期统一治理:通过一次性迁移将 `llm_factory/sessions/messages` 重命名到规范名
|
||||
- 若本期直接重命名,需同步 ORM 模型、外键、索引、RLS policy 名称与运行文档
|
||||
@@ -1,161 +0,0 @@
|
||||
# 邀请码机制设计
|
||||
|
||||
**Date**: 2026-02-27
|
||||
**Status**: Approved
|
||||
**Author**: User + AI
|
||||
|
||||
## 背景
|
||||
|
||||
为用户注册增加邀请码机制,支持:
|
||||
- 每个用户注册后自动获得专属邀请码
|
||||
- 注册时可填写他人邀请码
|
||||
- 记录邀请关系和使用统计
|
||||
- 支持运营邀请码(批量、限额、过期、禁用)
|
||||
- 预留奖励策略配置
|
||||
|
||||
## 数据模型
|
||||
|
||||
### invite_codes 表
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | PK | 主键 |
|
||||
| code | VARCHAR(8) | UNIQUE, NOT NULL | 邀请码 |
|
||||
| owner_id | UUID | FK → profiles.id, nullable | 所属用户,NULL 为运营码 |
|
||||
| max_uses | INT | nullable | 最大使用次数,NULL 无限制 |
|
||||
| used_count | INT | DEFAULT 0 | 已用次数 |
|
||||
| expires_at | TIMESTAMPTZ | nullable | 过期时间,NULL 永不过期 |
|
||||
| status | VARCHAR(20) | NOT NULL | active / disabled |
|
||||
| reward_config | JSONB | DEFAULT '{}' | 奖励策略配置 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL | 创建时间 |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL | 更新时间 |
|
||||
| deleted_at | TIMESTAMPTZ | nullable | 软删除 |
|
||||
|
||||
**索引:**
|
||||
- `ix_invite_codes_code` ON (code) UNIQUE
|
||||
- `ix_invite_codes_owner_id` ON (owner_id)
|
||||
- `ix_invite_codes_status_expires` ON (status, expires_at)
|
||||
|
||||
**CHECK 约束:**
|
||||
- `status IN ('active', 'disabled')`
|
||||
- `used_count >= 0`
|
||||
- `max_uses IS NULL OR max_uses > 0`
|
||||
|
||||
### profiles 表变更
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| referred_by | UUID | FK → profiles.id, nullable | 被谁邀请 |
|
||||
|
||||
**索引:**
|
||||
- `ix_profiles_referred_by` ON (referred_by)
|
||||
|
||||
## API 变更
|
||||
|
||||
### POST /auth/verifications
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "string (3-30 chars)",
|
||||
"email": "string (email)",
|
||||
"password": "string (min 6 chars)",
|
||||
"redirect_to": "string?",
|
||||
"invite_code": "string (8 chars)?" // 新增,可选
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 202 Accepted(不变)
|
||||
|
||||
## 注册流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. POST /auth/verifications │
|
||||
│ - 存储 username + invite_code 到 Supabase metadata │
|
||||
│ - 发送 OTP 邮件 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. POST /auth/verifications/verify │
|
||||
│ - 验证 OTP │
|
||||
│ - 创建 auth.users 记录 │
|
||||
│ - 触发 on_auth_user_created trigger │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. Trigger: on_auth_user_created │
|
||||
│ a. INSERT INTO profiles (id, username, ...) │
|
||||
│ b. 生成 8 位随机邀请码 │
|
||||
│ c. INSERT INTO invite_codes (code, owner_id, ...) │
|
||||
│ d. 从 metadata 取 invite_code,执行邀请校验逻辑 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. 邀请码校验逻辑 │
|
||||
│ IF invite_code 存在 AND │
|
||||
│ status = 'active' AND │
|
||||
│ (expires_at IS NULL OR expires_at > now()) AND │
|
||||
│ (max_uses IS NULL OR used_count < max_uses) │
|
||||
│ THEN │
|
||||
│ UPDATE profiles SET referred_by = invite_codes.owner_id │
|
||||
│ UPDATE invite_codes SET used_count = used_count + 1 │
|
||||
│ END IF │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 邀请码生成规则
|
||||
|
||||
- 8 位随机字符串
|
||||
- 字符集:`ABCDEFGHJKLMNPQRSTUVWXYZ23456789`(排除易混淆字符 0/O/1/I/L)
|
||||
- 唯一性:数据库 UNIQUE 约束 + 生成时冲突重试(最多 10 次)
|
||||
|
||||
## 使用记录查询
|
||||
|
||||
通过 profiles 表查询:
|
||||
|
||||
```sql
|
||||
-- 查询某个邀请码的使用记录
|
||||
SELECT p.id, p.username, p.created_at
|
||||
FROM profiles p
|
||||
JOIN invite_codes ic ON ic.owner_id = :owner_id
|
||||
WHERE p.referred_by = ic.owner_id
|
||||
ORDER BY p.created_at DESC;
|
||||
|
||||
-- 查询某个用户邀请了多少人
|
||||
SELECT COUNT(*) FROM profiles WHERE referred_by = :user_id;
|
||||
```
|
||||
|
||||
## 边界情况
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 邀请码不存在 | 跳过邀请,注册正常成功 |
|
||||
| 邀请码已禁用 | 跳过邀请 |
|
||||
| 邀请码已过期 | 跳过邀请 |
|
||||
| 邀请码已达上限 | 跳过邀请 |
|
||||
| 用户自邀(用自己的码) | 不可能,用户注册时还没有邀请码 |
|
||||
| 重复使用同一邀请码 | 允许(until max_uses) |
|
||||
|
||||
## 后续扩展
|
||||
|
||||
1. **奖励系统**:通过 `reward_config` JSONB 字段配置不同奖励策略
|
||||
2. **运营批量码**:`owner_id = NULL` 的邀请码,支持市场推广
|
||||
3. **邀请排行榜**:基于 `used_count` 或 profiles 关联查询
|
||||
4. **邀请码回收**:软删除 `deleted_at`,保留历史记录
|
||||
|
||||
## 迁移计划
|
||||
|
||||
1. 新增迁移文件创建 `invite_codes` 表
|
||||
2. 新增迁移文件给 `profiles` 表添加 `referred_by` 字段
|
||||
3. 修改 `on_auth_user_created` trigger 增加邀请码逻辑
|
||||
4. 修改 `VerificationCreateRequest` schema 添加 `invite_code` 字段
|
||||
5. 修改 `create_verification` gateway 传递 `invite_code` 到 metadata
|
||||
|
||||
## 测试用例
|
||||
|
||||
1. 注册时不填邀请码 → 正常注册,生成专属邀请码
|
||||
2. 注册时填写有效邀请码 → 关联邀请关系,used_count +1
|
||||
3. 注册时填写无效邀请码 → 正常注册,无邀请关系
|
||||
4. 邀请码达上限后使用 → 正常注册,无邀请关系
|
||||
5. 运营邀请码使用 → 正常注册,无 referred_by(owner_id = NULL)
|
||||
@@ -1,309 +0,0 @@
|
||||
# Invite Code Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 在现有 OTP 注册链路中引入邀请码能力,支持用户自动生成专属邀请码、注册时可选填邀请码并记录邀请关系与使用次数。
|
||||
|
||||
**Architecture:** 采用数据库中心实现:通过 Alembic 新增 `invite_codes` 表、扩展 `profiles` 字段,并在 `auth.users` 的现有 trigger 函数中完成邀请码校验与记账,保证注册与邀请关系写入尽量原子。应用层只负责透传 `invite_code` 到 Supabase `raw_user_meta_data`。
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy, Alembic, Supabase Auth, PostgreSQL PL/pgSQL, Pytest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 更新注册请求 Schema(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/auth/schemas.py`
|
||||
- Modify: `backend/tests/integration/test_auth_routes.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `test_signup_start_returns_pending_response` 基础上新增断言路径:请求体带 `invite_code` 时返回仍为 202,且未触发 422。
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k signup_start_returns_pending_response -v`
|
||||
Expected: FAIL(`invite_code` 为额外字段或校验不通过)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
在 `VerificationCreateRequest` 增加可选字段:
|
||||
|
||||
```python
|
||||
invite_code: str | None = Field(default=None, min_length=8, max_length=8)
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k signup_start_returns_pending_response -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/auth/schemas.py backend/tests/integration/test_auth_routes.py
|
||||
git commit -m "feat: accept invite code in signup request"
|
||||
```
|
||||
|
||||
### Task 2: 透传 invite_code 到 Supabase metadata(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/auth/gateway.py`
|
||||
- Modify: `backend/tests/unit/v1/auth/test_auth_service.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `test_supabase_signup_passes_username_in_metadata` 增加 `invite_code` 并断言:
|
||||
|
||||
```python
|
||||
assert captured_payload["data"] == {
|
||||
"username": "demo",
|
||||
"invite_code": "A1B2C3D4",
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/auth/test_auth_service.py -k metadata -v`
|
||||
Expected: FAIL(metadata 未包含 `invite_code`)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
在 `create_verification` 中构建 metadata:
|
||||
|
||||
```python
|
||||
metadata = {"username": request.username}
|
||||
if request.invite_code:
|
||||
metadata["invite_code"] = request.invite_code
|
||||
payload = {
|
||||
"email": request.email,
|
||||
"password": request.password,
|
||||
"data": metadata,
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/auth/test_auth_service.py -k metadata -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/auth/gateway.py backend/tests/unit/v1/auth/test_auth_service.py
|
||||
git commit -m "feat: pass invite code through signup metadata"
|
||||
```
|
||||
|
||||
### Task 3: 新增 invite_codes 表与 profiles.referred_by(迁移先行)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py`
|
||||
- Modify: `backend/src/models/profile.py`
|
||||
- Create: `backend/src/models/invite_code.py`
|
||||
- Modify: `backend/src/models/__init__.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
在 `backend/tests/unit/database/test_profile_models.py` 新增 `referred_by` 读写测试;新增 `backend/tests/unit/database/test_invite_code_models.py` 验证 `InviteCode` 基本创建与约束字段。
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/database/test_profile_models.py tests/unit/database/test_invite_code_models.py -v`
|
||||
Expected: FAIL(字段/模型不存在)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- Alembic 创建 `invite_codes`:
|
||||
- `code` 唯一索引
|
||||
- `owner_id` 外键到 `profiles.id`(可空)
|
||||
- `status`、`used_count`、`max_uses` check 约束
|
||||
- `max_uses` 默认 `NULL`(无限制)
|
||||
- `expires_at` 默认 `NULL`(无限制)
|
||||
- `reward_config` JSONB 默认 `{}`
|
||||
- 启用 RLS(按项目默认 deny-all)
|
||||
- **注意**:本期不开放 invite_codes 表直接读取,用户邀请码通过 profile 聚合接口返回(后续实现)
|
||||
|
||||
- Alembic 给 `profiles` 增加 `referred_by` + 索引 + 外键
|
||||
- ORM 同步 `Profile.referred_by` 与 `InviteCode` 模型
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/database/test_profile_models.py tests/unit/database/test_invite_code_models.py -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py backend/src/models/profile.py backend/src/models/invite_code.py backend/src/models/__init__.py backend/tests/unit/database/test_profile_models.py backend/tests/unit/database/test_invite_code_models.py
|
||||
git commit -m "feat: add invite code schema and profile referral fields"
|
||||
```
|
||||
|
||||
### Task 4: 扩展注册 trigger 生成邀请码并消费邀请(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py`
|
||||
- Modify: `backend/tests/integration/test_auth_routes.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增集成测试(建议通过测试替身/fixture 验证行为):
|
||||
- 注册不带邀请码时,profile 创建后存在 owner 邀请码
|
||||
- 注册带有效邀请码时,`referred_by` 生效且 `used_count + 1`
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k invite -v`
|
||||
Expected: FAIL(触发器逻辑尚未实现)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
在迁移 SQL 中:
|
||||
- 新增 helper function:生成 8 位随机码(排除易混淆字符 0/O/1/I/L,冲突重试)
|
||||
- 重建 `public.create_profile_for_new_user()`:
|
||||
1. 插入 `profiles`
|
||||
2. 创建该用户专属 `invite_codes`(`owner_id = NEW.id`)
|
||||
3. 读取 `NEW.raw_user_meta_data ->> 'invite_code'`
|
||||
4. 校验邀请码状态/过期/次数
|
||||
5. 若有效:更新 `profiles.referred_by`,并 `used_count = used_count + 1`
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k invite -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py backend/tests/integration/test_auth_routes.py
|
||||
git commit -m "feat: extend signup trigger for invite code generation and usage"
|
||||
```
|
||||
|
||||
### Task 5: 覆盖邀请码边界场景(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/tests/integration/test_auth_routes.py`
|
||||
- Optional Modify: `backend/tests/e2e/test_auth_flow.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
新增场景测试:
|
||||
- 邀请码不存在
|
||||
- 邀请码 disabled
|
||||
- 邀请码 expires_at 已过期
|
||||
- 邀请码达到 `max_uses`
|
||||
|
||||
断言:注册仍成功(202/200 链路正常),仅邀请关系不建立。
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k "invite and (expired or disabled or max_uses or invalid)" -v`
|
||||
Expected: FAIL
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
修正 trigger 判断顺序和条件,确保“邀请无效不影响注册”原则。
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k invite -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/tests/integration/test_auth_routes.py backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py
|
||||
git commit -m "test: cover invite code edge cases in signup flow"
|
||||
```
|
||||
|
||||
### Task 6: 文档同步与运行手册更新
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
- Modify: `docs/runtime/runtime-runbook.md`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
无自动化测试;改为文档一致性检查清单(手工):
|
||||
- 注册接口 request 字段包含 `invite_code`
|
||||
- 说明邀请码消费时机与“无效码不阻断注册”
|
||||
|
||||
**Step 2: Run check to verify missing docs**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k signup_start -v`
|
||||
Expected: PASS(作为行为基线),文档尚未同步
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
- 更新 `POST /auth/verifications` 请求字段
|
||||
- 新增邀请码行为说明
|
||||
- 在 runbook 变更日志添加本次改动记录
|
||||
|
||||
**Step 4: Run check after docs update**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/integration/test_auth_routes.py -k signup_start -v`
|
||||
Expected: PASS(行为与文档一致)
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-route.md docs/runtime/runtime-runbook.md
|
||||
git commit -m "docs: document invite code behavior in signup flow"
|
||||
```
|
||||
|
||||
### Task 7: 全量验证与风险审查(L2)
|
||||
|
||||
**Files:**
|
||||
- Verify only
|
||||
|
||||
**Step 1: Run lint/type checks**
|
||||
|
||||
Run:
|
||||
- `cd backend && uv run ruff check src tests`
|
||||
- `cd backend && uv run basedpyright src`
|
||||
|
||||
Expected: 全部通过
|
||||
|
||||
**Step 2: Run test suites**
|
||||
|
||||
Run:
|
||||
- `cd backend && uv run pytest tests/unit -v`
|
||||
- `cd backend && uv run pytest tests/integration -v`
|
||||
- `cd backend && uv run pytest tests/e2e/test_auth_flow.py -v`
|
||||
|
||||
Expected: 通过
|
||||
|
||||
**Step 3: Run mandatory review gates for L2**
|
||||
|
||||
- `refactor-cleaner` agent:确认无死代码/重复代码
|
||||
- `code-reviewer` agent:检查 DB trigger、安全边界、可维护性
|
||||
|
||||
Expected: CRITICAL/HIGH 为 0
|
||||
|
||||
**Step 4: Security-specific sanity checks**
|
||||
|
||||
检查项:
|
||||
- 未硬编码密钥
|
||||
- SQL 逻辑无注入风险(trigger 中仅参数/列操作)
|
||||
- 邀请码校验失败不泄露内部细节
|
||||
|
||||
**Step 5: Commit verification evidence (if needed in docs/PR notes)**
|
||||
|
||||
```bash
|
||||
git add <updated verification notes if any>
|
||||
git commit -m "chore: record invite code verification results"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 交付验收标准
|
||||
|
||||
1. 新用户注册后必有 1 条专属邀请码。
|
||||
2. 注册时传入有效邀请码会建立 `profiles.referred_by` 并增加 `used_count`。
|
||||
3. 无效邀请码不会阻断注册成功。
|
||||
4. 支持运营码(`owner_id IS NULL`)与后续奖励扩展(`reward_config`)。
|
||||
5. 文档已同步,测试与检查通过。
|
||||
|
||||
## 备注
|
||||
|
||||
- 本需求触发 L2(数据库迁移 + trigger + 多文件大改),必须走双审查 gate。
|
||||
- 不在本期实现运营后台批量发码 API;仅完成数据层与注册链路支撑。
|
||||
@@ -1,191 +0,0 @@
|
||||
# Design: Schedule Items API
|
||||
|
||||
**Date:** 2026-02-27
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
实现日历事项(Schedule Items)的后端 CRUD API,支持用户创建、查询、更新、删除日历事项。
|
||||
|
||||
## Scope
|
||||
|
||||
- 仅后端 API,不涉及前端
|
||||
- 全量 CRUD
|
||||
- 查询按时间范围筛选
|
||||
- 暂不支持重复日程(recurrence_rule 留空)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 创建日历事项
|
||||
|
||||
```
|
||||
POST /api/v1/schedule-items
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"title": "string (1-255 chars, required)",
|
||||
"description": "string? (max 2000 chars)",
|
||||
"start_at": "string (ISO 8601 datetime, required)",
|
||||
"end_at": "string? (ISO 8601 datetime, must be after start_at)",
|
||||
"timezone": "string? (default: UTC)",
|
||||
"metadata": {
|
||||
"color": "#FF6B6B",
|
||||
"location": "会议室A",
|
||||
"notes": "记得带身份证",
|
||||
"attachments": [],
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 201 Created
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "string",
|
||||
"description": "string?",
|
||||
"start_at": "string",
|
||||
"end_at": "string?",
|
||||
"timezone": "string",
|
||||
"metadata": {...},
|
||||
"status": "active",
|
||||
"source_type": "manual",
|
||||
"created_at": "string",
|
||||
"updated_at": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 查询日历事项列表
|
||||
|
||||
```
|
||||
GET /api/v1/schedule-items?start_at=2026-02-01&end_at=2026-02-28
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `start_at`: ISO 8601 date/datetime(查询范围起始)
|
||||
- `end_at`: ISO 8601 date/datetime(查询范围结束)
|
||||
|
||||
**Response:** 200 OK
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "string",
|
||||
"start_at": "string",
|
||||
"end_at": "string?",
|
||||
"timezone": "string",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 3. 获取单个事项
|
||||
|
||||
```
|
||||
GET /api/v1/schedule-items/{id}
|
||||
```
|
||||
|
||||
**Response:** 200 OK(完整字段,同创建响应)
|
||||
|
||||
### 4. 更新事项
|
||||
|
||||
```
|
||||
PATCH /api/v1/schedule-items/{id}
|
||||
```
|
||||
|
||||
**Request:** 支持 `title`/`description`/`start_at`/`end_at`/`timezone`/`metadata`/`status` 部分更新
|
||||
|
||||
**Response:** 200 OK
|
||||
|
||||
### 5. 删除事项
|
||||
|
||||
```
|
||||
DELETE /api/v1/schedule-items/{id}
|
||||
```
|
||||
|
||||
**Response:** 204 No Content(软删除)
|
||||
|
||||
## Data Models
|
||||
|
||||
### Metadata 结构(Pydantic)
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
|
||||
class AttachmentType(str, Enum):
|
||||
DOCUMENT = "document"
|
||||
REMINDER = "reminder"
|
||||
|
||||
class ScheduleItemMetadataAttachment(BaseModel):
|
||||
name: str
|
||||
type: AttachmentType
|
||||
visible_to: list[UUID] = []
|
||||
# document 类型
|
||||
url: str | None = None
|
||||
note: str | None = None
|
||||
# reminder 类型
|
||||
content: str | None = None
|
||||
|
||||
class ScheduleItemMetadata(BaseModel):
|
||||
color: str | None = None
|
||||
location: str | None = None
|
||||
notes: str | None = None
|
||||
attachments: list[ScheduleItemMetadataAttachment] = []
|
||||
version: int = 1
|
||||
```
|
||||
|
||||
### 数据库模型(已有)
|
||||
|
||||
参见 `backend/src/models/schedule_items.py`:
|
||||
- `id`: UUID
|
||||
- `owner_id`: UUID
|
||||
- `title`: String(255)
|
||||
- `description`: Text
|
||||
- `start_at`: DateTime(timezone=True)
|
||||
- `end_at`: DateTime(timezone=True)
|
||||
- `timezone`: String(50)
|
||||
- `extra_metadata`: JSONB (mapped as "metadata")
|
||||
- `recurrence_rule`: String(255)
|
||||
- `source_type`: Enum (MANUAL/IMPORTED/AGENT_GENERATED)
|
||||
- `status`: Enum (ACTIVE/COMPLETED/CANCELED/ARCHIVED)
|
||||
- `created_by`: UUID
|
||||
|
||||
## Architecture
|
||||
|
||||
遵循项目 `schemas / repository / service / router` 分层模式:
|
||||
|
||||
```
|
||||
backend/src/v1/schedule_items/
|
||||
├── __init__.py
|
||||
├── schemas.py # Pydantic 请求/响应模型
|
||||
├── repository.py # CRUD 操作(无 auth,无 commit)
|
||||
├── service.py # 业务逻辑 + 授权 + 事务边界
|
||||
├── router.py # FastAPI 路由定义
|
||||
└── dependencies.py # DI(如有)
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- 所有端点需要认证(JWT)
|
||||
- `owner_id` 从 JWT `sub` 提取,不从请求体读取
|
||||
- 用户只能操作自己的日历事项(`owner_id` 过滤)
|
||||
- RLS 已在数据库层启用(防御边界)
|
||||
|
||||
## Error Handling
|
||||
|
||||
使用 RFC 7807 `application/problem+json` 格式:
|
||||
- 400: 请求参数无效
|
||||
- 401: 未认证
|
||||
- 404: 事项不存在或无权限访问
|
||||
- 422: 验证失败
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 重复日程(recurrence_rule)
|
||||
- 日程订阅与协作(schedule_subscriptions)
|
||||
- 待办事项联动(todos/todo_sources)
|
||||
- 前端实现
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,567 +0,0 @@
|
||||
# AG-UI 聊天功能设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档描述如何使用 AG-UI 协议实现 AI 聊天功能,包括:
|
||||
- 消息的发送与接收(通过 AG-UI 事件流)
|
||||
- AI 工具调用(Tool Call)机制
|
||||
- 日历卡片作为 Tool Result 渲染
|
||||
- 前端工具注册与执行
|
||||
- 本地持久化
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体流程
|
||||
|
||||
```
|
||||
用户输入消息
|
||||
↓
|
||||
AgUiService.sendMessage()
|
||||
↓
|
||||
[Mock Mode] 规则引擎决策 → 事件流模拟
|
||||
[Real Mode] POST /api/chat → SSE 监听
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AG-UI Event Stream (按序处理) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END │
|
||||
│ TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END │
|
||||
│ TOOL_CALL_RESULT │
|
||||
│ RUN_STARTED → ... → RUN_FINISHED │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
ChatListItem 渲染
|
||||
```
|
||||
|
||||
### 2.2 核心组件
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `AgUiEvent` | AG-UI 事件数据模型 |
|
||||
| `AgUiService` | 事件流处理:发送消息、解析事件 |
|
||||
| `ToolRegistry` | 前端工具注册表:定义工具 + handler |
|
||||
| `AiDecisionEngine` | Mock 模式:规则引擎决定是否调用工具 |
|
||||
| `UiSchemaParser` | 解析 tool result 中的 UI Schema |
|
||||
| `UiSchemaRenderer` | 根据 schema 渲染对应组件 |
|
||||
| `ChatHistoryRepository` | 本地持久化:IndexedDB/localStorage |
|
||||
|
||||
### 2.3 状态管理
|
||||
|
||||
```
|
||||
ChatState {
|
||||
messages: ChatListItem[] // 渲染列表
|
||||
pendingToolCalls: Map<call_id, ToolCallState>
|
||||
isLoading: bool
|
||||
runId: string | null
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 数据模型
|
||||
|
||||
### 3.1 AG-UI 事件模型
|
||||
|
||||
```dart
|
||||
// 基类
|
||||
abstract class AgUiEvent {
|
||||
final String type;
|
||||
final String? timestamp;
|
||||
}
|
||||
|
||||
// 生命周期事件
|
||||
class RunStartedEvent extends AgUiEvent {
|
||||
final String threadId;
|
||||
final String runId;
|
||||
final String? parentRunId;
|
||||
}
|
||||
|
||||
class RunFinishedEvent extends AgUiEvent {
|
||||
final String threadId;
|
||||
final String runId;
|
||||
final dynamic result;
|
||||
}
|
||||
|
||||
// 文本消息事件
|
||||
class TextMessageStartEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String role; // "user" | "assistant" | "system"
|
||||
}
|
||||
|
||||
class TextMessageContentEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String delta;
|
||||
}
|
||||
|
||||
class TextMessageEndEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
}
|
||||
|
||||
// 工具调用事件
|
||||
class ToolCallStartEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String toolCallName;
|
||||
final String? parentMessageId;
|
||||
}
|
||||
|
||||
class ToolCallArgsEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String delta; // JSON fragment
|
||||
}
|
||||
|
||||
class ToolCallEndEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
}
|
||||
|
||||
class ToolCallResultEvent extends AgUiEvent {
|
||||
final String messageId;
|
||||
final String toolCallId;
|
||||
final ToolResult result; // 给 AI 的原始结果
|
||||
final UiCard? ui; // 给 UI 的渲染数据
|
||||
}
|
||||
|
||||
class ToolCallErrorEvent extends AgUiEvent {
|
||||
final String toolCallId;
|
||||
final String error;
|
||||
final String? code;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Tool Result Schema(v1)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_result",
|
||||
"version": "v1",
|
||||
"call_id": "call_abc123",
|
||||
"tool_name": "create_calendar_event",
|
||||
"result": {
|
||||
"eventId": "evt_xxx",
|
||||
"ok": true,
|
||||
"message": "日程已创建"
|
||||
},
|
||||
"ui": {
|
||||
"type": "card",
|
||||
"cardType": "calendar_card.v1",
|
||||
"data": {
|
||||
"id": "evt_xxx",
|
||||
"title": "产品评审会议",
|
||||
"description": "讨论Q2路线图",
|
||||
"startAt": "2026-03-01T10:00:00+08:00",
|
||||
"endAt": "2026-03-01T11:00:00+08:00",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"location": "会议室A",
|
||||
"color": "#4F46E5",
|
||||
"sourceType": "agentGenerated"
|
||||
},
|
||||
"actions": [
|
||||
{"type": "open", "label": "打开", "target": "calendar/evt_xxx"},
|
||||
{"type": "edit", "label": "编辑", "action": "edit_event"},
|
||||
{"type": "delete", "label": "删除", "action": "delete_event"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 工具定义(前端 Tool Registry)
|
||||
|
||||
```dart
|
||||
// 工具定义
|
||||
class ToolDefinition {
|
||||
final String name;
|
||||
final String description;
|
||||
final Map<String, dynamic> parameters;
|
||||
final ToolHandler handler;
|
||||
}
|
||||
|
||||
// create_calendar_event 工具
|
||||
{
|
||||
"name": "create_calendar_event",
|
||||
"description": "创建一个日历事件或待办事项",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "事件标题",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "事件描述"
|
||||
},
|
||||
"startAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "开始时间 (ISO8601)"
|
||||
},
|
||||
"endAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "结束时间 (ISO8601)"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"default": "Asia/Shanghai"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "startAt"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 ChatListItem 模型
|
||||
|
||||
```dart
|
||||
enum ChatItemType {
|
||||
message, // 纯文本消息
|
||||
toolCall, // 工具调用中
|
||||
toolResult, // 工具结果卡片
|
||||
schedule // 日历事件(兼容旧数据)
|
||||
}
|
||||
|
||||
abstract class ChatListItem {
|
||||
String get id;
|
||||
DateTime get timestamp;
|
||||
ChatItemType get type;
|
||||
MessageSender get sender;
|
||||
}
|
||||
|
||||
class TextMessageItem extends ChatListItem {
|
||||
final String id;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
final MessageSender sender;
|
||||
final bool isStreaming; // 是否正在流式输出
|
||||
}
|
||||
|
||||
class ToolCallItem extends ChatListItem {
|
||||
final String id;
|
||||
final String callId;
|
||||
final String toolName;
|
||||
final Map<String, dynamic> args; // 解析后的参数
|
||||
final ToolCallStatus status; // pending | executing | completed | error
|
||||
final ToolResult? result;
|
||||
final UiCard? uiCard;
|
||||
}
|
||||
|
||||
class CalendarCardItem extends ChatListItem {
|
||||
final String id;
|
||||
final String callId; // 关联的 tool call
|
||||
final CalendarCardData data;
|
||||
final List<CardAction> actions;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 核心流程
|
||||
|
||||
### 4.1 发送消息
|
||||
|
||||
```dart
|
||||
Future<void> sendMessage(String content) async {
|
||||
// 1. 添加用户消息到列表
|
||||
final userMessage = TextMessageItem(
|
||||
id: generateId(),
|
||||
content: content,
|
||||
timestamp: DateTime.now(),
|
||||
sender: MessageSender.user,
|
||||
);
|
||||
_chatItems.add(userMessage);
|
||||
|
||||
// 2. 发起请求
|
||||
if (Env.isMockApi) {
|
||||
await _mockEventStream(content);
|
||||
} else {
|
||||
await _realEventStream(content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Mock 事件流(规则引擎)
|
||||
|
||||
```dart
|
||||
class AiDecisionEngine {
|
||||
// 意图关键词映射
|
||||
static final Map<Intent, List<Pattern>> _intentPatterns = {
|
||||
Intent.createEvent: [
|
||||
RegExp(r'提醒|开会|预约|日程|安排'),
|
||||
RegExp(r'明天|今天|后天|下周'),
|
||||
RegExp(r'\d{1,2}点|\d{1,2}:\d{2}'),
|
||||
],
|
||||
Intent.searchEvent: [
|
||||
RegExp(r'查看|有什么|今天.*日程|明天.*安排'),
|
||||
],
|
||||
};
|
||||
|
||||
Intent? matchIntent(String text) {
|
||||
for (final entry in _intentPatterns.entries) {
|
||||
for (final pattern in entry.value) {
|
||||
if (pattern.hasMatch(text)) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 支持强制触发:#tool:create_calendar_event {"title": "test"}
|
||||
bool tryForceTrigger(String text) {...}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 事件解析与处理
|
||||
|
||||
```dart
|
||||
Future<void> _processEvent(AgUiEvent event) async {
|
||||
switch (event.type) {
|
||||
case 'TEXT_MESSAGE_START':
|
||||
_handleTextMessageStart(event);
|
||||
break;
|
||||
case 'TEXT_MESSAGE_CONTENT':
|
||||
_handleTextMessageContent(event);
|
||||
break;
|
||||
case 'TEXT_MESSAGE_END':
|
||||
_handleTextMessageEnd(event);
|
||||
break;
|
||||
case 'TOOL_CALL_START':
|
||||
_handleToolCallStart(event);
|
||||
break;
|
||||
case 'TOOL_CALL_ARGS':
|
||||
_handleToolCallArgs(event);
|
||||
break;
|
||||
case 'TOOL_CALL_END':
|
||||
await _handleToolCallEnd(event);
|
||||
break;
|
||||
case 'TOOL_CALL_RESULT':
|
||||
_handleToolCallResult(event);
|
||||
break;
|
||||
case 'TOOL_CALL_ERROR':
|
||||
_handleToolCallError(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleToolCallStart(ToolCallStartEvent event) {
|
||||
// 创建 pending 状态的 tool call item
|
||||
final item = ToolCallItem(
|
||||
id: event.toolCallId,
|
||||
callId: event.toolCallId,
|
||||
toolName: event.toolCallName,
|
||||
args: {},
|
||||
status: ToolCallStatus.pending,
|
||||
);
|
||||
_chatItems.add(item);
|
||||
}
|
||||
|
||||
Future<void> _handleToolCallEnd(ToolCallEndEvent event) async {
|
||||
// 1. 找到对应的 pending tool call
|
||||
final toolCall = _findPendingToolCall(event.toolCallId);
|
||||
if (toolCall == null) return;
|
||||
|
||||
// 2. 校验参数 JSON Schema
|
||||
final validation = validateToolArgs(toolCall.toolName, toolCall.args);
|
||||
if (!validation.ok) {
|
||||
_emitToolCallError(event.toolCallId, validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 执行工具 handler
|
||||
toolCall.status = ToolCallStatus.executing;
|
||||
final result = await ToolRegistry.execute(
|
||||
toolCall.toolName,
|
||||
toolCall.args,
|
||||
);
|
||||
|
||||
// 4. 构建 tool result(包含 result + ui)
|
||||
final toolResult = ToolResult(
|
||||
eventId: result['eventId'],
|
||||
ok: result['ok'] ?? true,
|
||||
message: result['message'],
|
||||
);
|
||||
|
||||
final uiCard = _buildUiCard(toolCall.toolName, result);
|
||||
|
||||
// 5. 发送 TOOL_CALL_RESULT 事件
|
||||
_emitToolCallResult(event.toolCallId, toolResult, uiCard);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 UI Schema 渲染
|
||||
|
||||
```dart
|
||||
class UiSchemaRenderer {
|
||||
static final Map<String, Widget Function(UiCard)> _renderers = {
|
||||
'calendar_card.v1': (card) => CalendarCardWidget(
|
||||
data: CalendarCardData.fromJson(card.data),
|
||||
actions: card.actions,
|
||||
),
|
||||
};
|
||||
|
||||
static Widget render(UiCard card) {
|
||||
final renderer = _renderers[card.cardType];
|
||||
if (renderer != null) {
|
||||
return renderer(card);
|
||||
}
|
||||
// Unknown card type fallback
|
||||
return _renderUnknownCard(card);
|
||||
}
|
||||
|
||||
static Widget _renderUnknownCard(UiCard card) {
|
||||
return GenericCardWidget(
|
||||
rawJson: jsonEncode(card.toJson()),
|
||||
cardType: card.cardType,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 日历卡片组件
|
||||
|
||||
```dart
|
||||
class CalendarCardWidget extends StatelessWidget {
|
||||
final CalendarCardData data;
|
||||
final List<CardAction> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = ColorExt.parse(data.color ?? '#4F46E5');
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [...],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 颜色条
|
||||
Container(
|
||||
height: 4,
|
||||
color: color,
|
||||
),
|
||||
// 内容
|
||||
Padding(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(data.title, style: ...),
|
||||
if (data.description != null) ...,
|
||||
_buildTimeRow(),
|
||||
if (data.location != null) ...,
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
if (actions.isNotEmpty) _buildActions(actions),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 持久化设计
|
||||
|
||||
### 5.1 存储结构
|
||||
|
||||
```dart
|
||||
// localStorage / IndexedDB
|
||||
{
|
||||
"chat_sessions": {
|
||||
"current_thread_id": {
|
||||
"messages": [...], // ChatListItem JSON
|
||||
"lastRunId": "run_xxx",
|
||||
"updatedAt": "2026-02-28T12:00:00Z"
|
||||
}
|
||||
},
|
||||
"calendar_events": {
|
||||
"evt_xxx": {...} // 独立存储的日历事件
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 恢复逻辑
|
||||
|
||||
```dart
|
||||
Future<void> restoreSession() async {
|
||||
final session = await ChatHistoryRepository.load('current_thread_id');
|
||||
if (session != null) {
|
||||
_chatItems.clear();
|
||||
_chatItems.addAll(session.messages);
|
||||
_runId = session.lastRunId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
### 6.1 Tool Call 错误
|
||||
|
||||
```dart
|
||||
void _emitToolCallError(String callId, String error) {
|
||||
// 1. 更新 item 状态
|
||||
final item = _findToolCallItem(callId);
|
||||
item?.status = ToolCallStatus.error;
|
||||
item?.errorMessage = error;
|
||||
|
||||
// 2. 渲染错误卡片
|
||||
final errorCard = UiCard(
|
||||
cardType: 'error_card.v1',
|
||||
data: {'message': error},
|
||||
);
|
||||
|
||||
// 3. 触发 UI 更新
|
||||
notifyListeners();
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 事件流重连
|
||||
|
||||
```dart
|
||||
// 断线重连时从 snapshot 恢复
|
||||
Future<void> reconnect() async {
|
||||
final snapshot = await _fetchMessagesSnapshot();
|
||||
_chatItems.clear();
|
||||
_chatItems.addAll(snapshot.messages);
|
||||
|
||||
// 重新订阅事件流
|
||||
_subscribeToEvents();
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 实施计划
|
||||
|
||||
### Phase 1: 基础框架
|
||||
- [ ] 定义 AG-UI 事件模型
|
||||
- [ ] 实现 AgUiService 基础结构
|
||||
- [ ] 实现 ToolRegistry
|
||||
|
||||
### Phase 2: Mock 实现
|
||||
- [ ] 实现 AiDecisionEngine 规则引擎
|
||||
- [ ] 实现 Mock 事件流
|
||||
- [ ] 集成现有 HomeScreen
|
||||
|
||||
### Phase 3: UI 渲染
|
||||
- [ ] 实现 UiSchemaParser
|
||||
- [ ] 实现 CalendarCardWidget
|
||||
- [ ] 实现 ToolPending / ToolError 状态卡片
|
||||
|
||||
### Phase 4: 持久化
|
||||
- [ ] 实现 ChatHistoryRepository
|
||||
- [ ] 实现会话恢复
|
||||
|
||||
### Phase 5: 真实后端对接
|
||||
- [ ] 实现 SSE 客户端
|
||||
- [ ] 实现事件流解析器
|
||||
|
||||
## 8. 版本历史
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| v1.0 | 2026-02-28 | 初始版本 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,714 +0,0 @@
|
||||
# Calendar Sharing Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 实现日历事件分享功能 - 用户可以分享日历事件给其他人(通过邮箱),被邀请人会收到待办消息,可以同意或忽略邀请。
|
||||
|
||||
**Architecture:** 使用现有的 schemas/repository/service/router 分层架构。新增 inbox_messages 模块处理邀请消息。复用 auth gateway 的 get_user_by_email 通过邮箱查找用户。
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy (async), Pydantic, Supabase Auth
|
||||
|
||||
---
|
||||
|
||||
## Permission Bits (from design doc)
|
||||
|
||||
| Permission | Value | Binary |
|
||||
|------------|-------|--------|
|
||||
| view | 1 | 001 |
|
||||
| invite | 2 | 010 |
|
||||
| edit | 4 | 100 |
|
||||
|
||||
- Owner has all permissions: 7 (111)
|
||||
- Check permission: `permission & 2 == 2` (has invite)
|
||||
- Add permission: `permission | 2`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add inbox_messages module (schemas, repository, service, router)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/src/v1/inbox_messages/__init__.py`
|
||||
- Create: `backend/src/v1/inbox_messages/schemas.py`
|
||||
- Create: `backend/src/v1/inbox_messages/repository.py`
|
||||
- Create: `backend/src/v1/inbox_messages/service.py`
|
||||
- Create: `backend/src/v1/inbox_messages/router.py`
|
||||
- Modify: `backend/src/v1/router.py` - include inbox_messages router
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# backend/tests/unit/v1/inbox_messages/test_schemas.py
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from v1.inbox_messages.schemas import (
|
||||
InboxMessageResponse,
|
||||
InboxMessageListRequest,
|
||||
InboxMessageAcceptRequest,
|
||||
)
|
||||
|
||||
def test_inbox_message_response_schema():
|
||||
msg_id = uuid4()
|
||||
response = InboxMessageResponse(
|
||||
id=msg_id,
|
||||
recipient_id=uuid4(),
|
||||
sender_id=uuid4(),
|
||||
message_type="calendar",
|
||||
schedule_item_id=uuid4(),
|
||||
content="Join my calendar",
|
||||
is_read=False,
|
||||
status="pending",
|
||||
)
|
||||
assert response.message_type == "calendar"
|
||||
assert response.status == "pending"
|
||||
|
||||
def test_inbox_message_accept_request_schema():
|
||||
request = InboxMessageAcceptRequest(
|
||||
permission_view=True,
|
||||
permission_edit=False,
|
||||
permission_invite=False,
|
||||
)
|
||||
assert request.permission_view is True
|
||||
assert request.permission_edit is False
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/inbox_messages/test_schemas.py -v`
|
||||
Expected: FAIL with "ModuleNotFoundError: No module named 'v1.inbox_messages'"
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Create `backend/src/v1/inbox_messages/__init__.py`:
|
||||
```python
|
||||
```
|
||||
|
||||
Create `backend/src/v1/inbox_messages/schemas.py`:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InboxMessageType(str, Enum):
|
||||
FRIEND_REQUEST = "friend_request"
|
||||
CALENDAR = "calendar"
|
||||
SYSTEM = "system"
|
||||
GROUP = "group"
|
||||
|
||||
|
||||
class InboxMessageStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ACCEPTED = "accepted"
|
||||
REJECTED = "rejected"
|
||||
DISMISSED = "dismissed"
|
||||
|
||||
|
||||
class InboxMessageResponse(BaseModel):
|
||||
id: UUID
|
||||
recipient_id: UUID
|
||||
sender_id: Optional[UUID] = None
|
||||
message_type: InboxMessageType
|
||||
schedule_item_id: Optional[UUID] = None
|
||||
content: Optional[str] = None
|
||||
is_read: bool = False
|
||||
status: InboxMessageStatus = InboxMessageStatus.PENDING
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class InboxMessageListRequest(BaseModel):
|
||||
status: Optional[InboxMessageStatus] = None
|
||||
|
||||
|
||||
class InboxMessageAcceptRequest(BaseModel):
|
||||
permission_view: bool = True
|
||||
permission_edit: bool = False
|
||||
permission_invite: bool = False
|
||||
```
|
||||
|
||||
Create `backend/src/v1/inbox_messages/repository.py`:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.inbox_messages import InboxMessage, InboxMessageStatus
|
||||
|
||||
|
||||
class InboxMessageRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def create(self, data: dict) -> InboxMessage:
|
||||
msg = InboxMessage(**data)
|
||||
self._session.add(msg)
|
||||
await self._session.flush()
|
||||
return msg
|
||||
|
||||
async def get_by_id(self, message_id: UUID, recipient_id: UUID) -> Optional[InboxMessage]:
|
||||
result = await self._session.execute(
|
||||
select(InboxMessage).where(
|
||||
InboxMessage.id == message_id,
|
||||
InboxMessage.recipient_id == recipient_id,
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_by_recipient(
|
||||
self, recipient_id: UUID, status: Optional[InboxMessageStatus] = None
|
||||
) -> list[InboxMessage]:
|
||||
query = select(InboxMessage).where(InboxMessage.recipient_id == recipient_id)
|
||||
if status:
|
||||
query = query.where(InboxMessage.status == status)
|
||||
query = query.order_by(InboxMessage.created_at.desc())
|
||||
result = await self._session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def update_status(
|
||||
self, message_id: UUID, recipient_id: UUID, status: InboxMessageStatus
|
||||
) -> Optional[InboxMessage]:
|
||||
msg = await self.get_by_id(message_id, recipient_id)
|
||||
if msg:
|
||||
msg.status = status
|
||||
await self._session.flush()
|
||||
return msg
|
||||
```
|
||||
|
||||
Create `backend/src/v1/inbox_messages/service.py`:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from core.auth.models import CurrentUser
|
||||
from core.db.base_service import BaseService
|
||||
from core.logging import get_logger
|
||||
from models.inbox_messages import InboxMessageStatus
|
||||
from v1.inbox_messages.repository import InboxMessageRepository
|
||||
from v1.inbox_messages.schemas import (
|
||||
InboxMessageAcceptRequest,
|
||||
InboxMessageListRequest,
|
||||
InboxMessageResponse,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = get_logger("v1.inbox_messages.service")
|
||||
|
||||
|
||||
class InboxMessageService(BaseService):
|
||||
_repository: InboxMessageRepository
|
||||
_session: AsyncSession
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repository: InboxMessageRepository,
|
||||
session: AsyncSession,
|
||||
current_user: Optional[CurrentUser] = None,
|
||||
) -> None:
|
||||
super().__init__(current_user=current_user)
|
||||
self._repository = repository
|
||||
self._session = session
|
||||
|
||||
async def list_messages(
|
||||
self, request: InboxMessageListRequest
|
||||
) -> list[InboxMessageResponse]:
|
||||
user_id = self.require_user_id()
|
||||
|
||||
try:
|
||||
messages = await self._repository.list_by_recipient(
|
||||
user_id, request.status
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Failed to list inbox messages")
|
||||
raise HTTPException(status_code=503, detail="Inbox unavailable")
|
||||
|
||||
return [
|
||||
InboxMessageResponse(
|
||||
id=m.id,
|
||||
recipient_id=m.recipient_id,
|
||||
sender_id=m.sender_id,
|
||||
message_type=m.message_type,
|
||||
schedule_item_id=m.schedule_item_id,
|
||||
content=m.content,
|
||||
is_read=m.is_read,
|
||||
status=m.status,
|
||||
created_at=m.created_at,
|
||||
)
|
||||
for m in messages
|
||||
]
|
||||
|
||||
async def accept_invitation(
|
||||
self, message_id: UUID, request: InboxMessageAcceptRequest
|
||||
) -> None:
|
||||
user_id = self.require_user_id()
|
||||
|
||||
try:
|
||||
message = await self._repository.get_by_id(message_id, user_id)
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Failed to get inbox message", message_id=str(message_id))
|
||||
raise HTTPException(status_code=503, detail="Inbox unavailable")
|
||||
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
if message.message_type != InboxMessageStatus.PENDING:
|
||||
raise HTTPException(status_code=400, detail="Message already processed")
|
||||
|
||||
message.status = InboxMessageStatus.ACCEPTED
|
||||
await self._session.flush()
|
||||
await self._session.commit()
|
||||
|
||||
async def dismiss_invitation(self, message_id: UUID) -> None:
|
||||
user_id = self.require_user_id()
|
||||
|
||||
try:
|
||||
message = await self._repository.get_by_id(message_id, user_id)
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Failed to get inbox message", message_id=str(message_id))
|
||||
raise HTTPException(status_code=503, detail="Inbox unavailable")
|
||||
|
||||
if message is None:
|
||||
raise HTTPException(status_code=404, detail="Message not found")
|
||||
|
||||
message.status = InboxMessageStatus.DISMISSED
|
||||
await self._session.flush()
|
||||
await self._session.commit()
|
||||
```
|
||||
|
||||
Create `backend/src/v1/inbox_messages/dependencies.py`:
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.auth.dependencies import get_current_user
|
||||
from core.db.session import get_db
|
||||
from models.auth.models import CurrentUser
|
||||
from v1.inbox_messages.repository import InboxMessageRepository
|
||||
from v1.inbox_messages.service import InboxMessageService
|
||||
|
||||
|
||||
def get_inbox_message_repository(
|
||||
session: Annotated[AsyncSession, Depends(get_db)]
|
||||
) -> InboxMessageRepository:
|
||||
return InboxMessageRepository(session)
|
||||
|
||||
|
||||
def get_inbox_message_service(
|
||||
repository: Annotated[InboxMessageRepository, Depends(get_inbox_message_repository)],
|
||||
current_user: Annotated[CurrentUser | None, Depends(get_current_user)],
|
||||
) -> InboxMessageService:
|
||||
return InboxMessageService(
|
||||
repository=repository,
|
||||
session=repository._session,
|
||||
current_user=current_user,
|
||||
)
|
||||
```
|
||||
|
||||
Create `backend/src/v1/inbox_messages/router.py`:
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from v1.inbox_messages.dependencies import get_inbox_message_service
|
||||
from v1.inbox_messages.schemas import (
|
||||
InboxMessageAcceptRequest,
|
||||
InboxMessageListRequest,
|
||||
InboxMessageResponse,
|
||||
InboxMessageStatus,
|
||||
)
|
||||
from v1.inbox_messages.service import InboxMessageService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/inbox", tags=["inbox"])
|
||||
|
||||
|
||||
@router.get("/messages", response_model=list[InboxMessageResponse])
|
||||
async def list_inbox_messages(
|
||||
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
|
||||
status: InboxMessageStatus | None = Query(None, description="Filter by status"),
|
||||
) -> list[InboxMessageResponse]:
|
||||
request = InboxMessageListRequest(status=status)
|
||||
return await service.list_messages(request)
|
||||
|
||||
|
||||
@router.post("/messages/{message_id}/accept", status_code=204)
|
||||
async def accept_invitation(
|
||||
message_id: UUID,
|
||||
request: InboxMessageAcceptRequest,
|
||||
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
|
||||
) -> None:
|
||||
await service.accept_invitation(message_id, request)
|
||||
|
||||
|
||||
@router.post("/messages/{message_id}/dismiss", status_code=204)
|
||||
async def dismiss_invitation(
|
||||
message_id: UUID,
|
||||
service: Annotated[InboxMessageService, Depends(get_inbox_message_service)],
|
||||
) -> None:
|
||||
await service.dismiss_invitation(message_id)
|
||||
```
|
||||
|
||||
Modify `backend/src/v1/router.py`:
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
|
||||
from core.http.models import HealthResponse
|
||||
from v1.agent_chat.router import router as agent_chat_router
|
||||
from v1.auth.router import router as auth_router
|
||||
from v1.infra.router import router as infra_router
|
||||
from v1.inbox_messages.router import router as inbox_messages_router
|
||||
from v1.schedule_items.router import router as schedule_items_router
|
||||
from v1.users.router import router as users_router
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
router.include_router(auth_router)
|
||||
router.include_router(infra_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(agent_chat_router)
|
||||
router.include_router(schedule_items_router)
|
||||
router.include_router(inbox_messages_router)
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
async def health() -> HealthResponse:
|
||||
return HealthResponse(status="ok")
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/inbox_messages/test_schemas.py -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/inbox_messages/ backend/src/v1/router.py
|
||||
git commit -m "feat: add inbox messages module for calendar invitations"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add share calendar API to schedule_items
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/schemas.py` - add share schemas
|
||||
- Modify: `backend/src/v1/schedule_items/repository.py` - add subscription create
|
||||
- Modify: `backend/src/v1/schedule_items/service.py` - add share method
|
||||
- Modify: `backend/src/v1/schedule_items/router.py` - add share endpoint
|
||||
- Modify: `backend/src/v1/schedule_items/dependencies.py` - add dependencies
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# backend/tests/unit/v1/schedule_items/test_share.py
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from v1.schedule_items.schemas import ScheduleItemShareRequest
|
||||
|
||||
def test_share_request_schema():
|
||||
request = ScheduleItemShareRequest(
|
||||
email="friend@example.com",
|
||||
permission_view=True,
|
||||
permission_edit=True,
|
||||
permission_invite=False,
|
||||
)
|
||||
assert request.email == "friend@example.com"
|
||||
assert request.permission_view is True
|
||||
|
||||
def test_permission_bits_calculation():
|
||||
request = ScheduleItemShareRequest(
|
||||
email="friend@example.com",
|
||||
permission_view=True,
|
||||
permission_edit=True,
|
||||
permission_invite=False,
|
||||
)
|
||||
# view=1, edit=4, invite=0 -> 1|4 = 5
|
||||
assert request._permission_value() == 5
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/schedule_items/test_share.py -v`
|
||||
Expected: FAIL with "cannot import 'ScheduleItemShareRequest'"
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Add to `backend/src/v1/schedule_items/schemas.py`:
|
||||
```python
|
||||
class ScheduleItemShareRequest(BaseModel):
|
||||
email: str = Field(..., description="Email of user to share with")
|
||||
permission_view: bool = Field(True, description="Grant view permission")
|
||||
permission_edit: bool = Field(False, description="Grant edit permission")
|
||||
permission_invite: bool = Field(False, description="Grant invite permission")
|
||||
|
||||
def _permission_value(self) -> int:
|
||||
value = 0
|
||||
if self.permission_view:
|
||||
value |= 1 # 001
|
||||
if self.permission_edit:
|
||||
value |= 4 # 100
|
||||
if self.permission_invite:
|
||||
value |= 2 # 010
|
||||
return value
|
||||
|
||||
|
||||
class ScheduleItemShareResponse(BaseModel):
|
||||
message: str
|
||||
```
|
||||
|
||||
Add to `backend/src/v1/schedule_items/repository.py`:
|
||||
```python
|
||||
from models.schedule_subscriptions import ScheduleSubscription
|
||||
|
||||
|
||||
class ScheduleItemRepository:
|
||||
# ... existing code ...
|
||||
|
||||
async def create_subscription(self, data: dict) -> ScheduleSubscription:
|
||||
sub = ScheduleSubscription(**data)
|
||||
self._session.add(sub)
|
||||
await self._session.flush()
|
||||
return sub
|
||||
```
|
||||
|
||||
Add to `backend/src/v1/schedule_items/service.py`:
|
||||
```python
|
||||
from uuid import UUID
|
||||
|
||||
from core.auth.models import CurrentUser
|
||||
from v1.auth.gateway import SupabaseAuthGateway
|
||||
from models.schedule_subscriptions import ScheduleSubscription
|
||||
|
||||
|
||||
class ScheduleItemService:
|
||||
# ... existing code ...
|
||||
|
||||
async def share(
|
||||
self, item_id: UUID, request: ScheduleItemShareRequest
|
||||
) -> ScheduleItemShareResponse:
|
||||
user_id = self.require_user_id()
|
||||
|
||||
# Check item exists and user is owner
|
||||
try:
|
||||
item = await self._repository.get_by_item_id(item_id, user_id)
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Failed to get schedule item", item_id=str(item_id))
|
||||
raise HTTPException(status_code=503, detail="Schedule item store unavailable")
|
||||
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="Schedule item not found")
|
||||
|
||||
if item.owner_id != user_id:
|
||||
raise HTTPException(status_code=403, detail="Only owner can share")
|
||||
|
||||
# Lookup user by email
|
||||
auth_gateway = SupabaseAuthGateway()
|
||||
try:
|
||||
target_user = await auth_gateway.get_user_by_email(request.email)
|
||||
except HTTPException as exc:
|
||||
if exc.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise
|
||||
|
||||
target_user_id = UUID(target_user.id)
|
||||
|
||||
# Create inbox message
|
||||
from models.inbox_messages import InboxMessage, InboxMessageType
|
||||
inbox_data = {
|
||||
"recipient_id": target_user_id,
|
||||
"sender_id": user_id,
|
||||
"message_type": InboxMessageType.CALENDAR,
|
||||
"schedule_item_id": item_id,
|
||||
"content": f"{item.title} shared with you",
|
||||
"created_by": user_id,
|
||||
}
|
||||
try:
|
||||
inbox_msg = InboxMessage(**inbox_data)
|
||||
self._session.add(inbox_msg)
|
||||
await self._session.flush()
|
||||
except SQLAlchemyError:
|
||||
logger.exception("Failed to create inbox message")
|
||||
raise HTTPException(status_code=503, detail="Failed to send invitation")
|
||||
|
||||
await self._session.commit()
|
||||
return ScheduleItemShareResponse(
|
||||
message=f"Invitation sent to {request.email}"
|
||||
)
|
||||
```
|
||||
|
||||
Add to `backend/src/v1/schedule_items/router.py`:
|
||||
```python
|
||||
from v1.schedule_items.schemas import (
|
||||
ScheduleItemCreateRequest,
|
||||
ScheduleItemListItem,
|
||||
ScheduleItemListRequest,
|
||||
ScheduleItemResponse,
|
||||
ScheduleItemShareRequest,
|
||||
ScheduleItemShareResponse,
|
||||
ScheduleItemUpdateRequest,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{item_id}/share", response_model=ScheduleItemShareResponse)
|
||||
async def share_schedule_item(
|
||||
item_id: UUID,
|
||||
request: ScheduleItemShareRequest,
|
||||
service: Annotated[ScheduleItemService, Depends(get_schedule_item_service)],
|
||||
) -> ScheduleItemShareResponse:
|
||||
return await service.share(item_id, request)
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd backend && uv run pytest tests/unit/v1/schedule_items/test_share.py -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/schedule_items/
|
||||
git commit -m "feat: add share calendar API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add accept invitation - create subscription
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/inbox_messages/service.py` - add subscription creation on accept
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# backend/tests/unit/v1/inbox_messages/test_accept_invitation.py
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from uuid import uuid4
|
||||
from v1.inbox_messages.service import InboxMessageService
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_creates_subscription():
|
||||
# Setup mocks
|
||||
mock_repo = MagicMock()
|
||||
mock_session = MagicMock()
|
||||
mock_message = MagicMock()
|
||||
mock_message.id = uuid4()
|
||||
mock_message.message_type = "calendar"
|
||||
mock_message.status = "pending"
|
||||
mock_message.schedule_item_id = uuid4()
|
||||
mock_repo.get_by_id = AsyncMock(return_value=mock_message)
|
||||
mock_repo._session = mock_session
|
||||
|
||||
service = InboxMessageService(
|
||||
repository=mock_repo,
|
||||
session=mock_session,
|
||||
current_user=MagicMock(user_id=uuid4()),
|
||||
)
|
||||
|
||||
# This should be implemented
|
||||
await service.accept_invitation(mock_message.id, ...)
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Expected: FAIL (test will fail because accept doesn't create subscription yet)
|
||||
|
||||
**Step 3: Write implementation**
|
||||
|
||||
Modify `backend/src/v1/inbox_messages/service.py` to import ScheduleSubscriptionRepository and create subscription on accept.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run tests and verify pass.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Fix permission enum reference bug
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/inbox_messages/service.py` - fix InboxMessageStatus reference
|
||||
|
||||
**Bug:** In Task 1, we used `InboxMessageStatus.PENDING` but should check against the actual enum type. Fix the bug.
|
||||
|
||||
**Step 1: Write test to verify bug**
|
||||
|
||||
```python
|
||||
def test_accept_checks_message_type_not_status():
|
||||
# Current code incorrectly checks message_type == PENDING
|
||||
# Should check status == PENDING
|
||||
```
|
||||
|
||||
**Step 2: Fix the implementation**
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Write unit tests
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/unit/v1/inbox_messages/test_service.py`
|
||||
- Create: `backend/tests/unit/v1/schedule_items/test_share.py`
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Write integration tests
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/integration/test_inbox_messages_routes.py`
|
||||
- Create: `backend/tests/integration/test_schedule_share_routes.py`
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Update API documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-route.md` - add share/inbox endpoints
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Run all tests and fix issues
|
||||
|
||||
Run full test suite and fix any issues.
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Run lint and typecheck
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd backend && uv run ruff check src/v1/schedule_items/ src/v1/inbox_messages/
|
||||
cd backend && uv run basedpyright src/v1/schedule_items/ src/v1/inbox_messages/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Final commit and create PR
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: add calendar sharing with invitations"
|
||||
git push -u origin feature-calendar-sharing
|
||||
gh pr create --title "feat: add calendar sharing" --body "..."
|
||||
```
|
||||
@@ -1,136 +0,0 @@
|
||||
# 好友申请与待办消息功能设计
|
||||
|
||||
**Date:** 2026-02-28
|
||||
**Status:** Approved
|
||||
|
||||
## 1. 数据模型
|
||||
|
||||
### Friendship 表 (已存在)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID | 主键 |
|
||||
| user_low_id | UUID | 用户A ID (固定排序小值) |
|
||||
| user_high_id | UUID | 用户B ID (固定排序大值) |
|
||||
| initiator_id | UUID? | 发起请求者 |
|
||||
| status | VARCHAR(20) | pending/accepted/blocked/declined/canceled |
|
||||
| requested_at | TIMESTAMP? | 请求时间 |
|
||||
| accepted_at | TIMESTAMP? | 接受时间 |
|
||||
| blocked_by | UUID? | 被谁屏蔽 |
|
||||
| created_by/updated_by | UUID? | 审计字段 |
|
||||
|
||||
### InboxMessage 表 (复用)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID | 主键 |
|
||||
| recipient_id | UUID | 接收方 |
|
||||
| sender_id | UUID? | 发送方 |
|
||||
| message_type | VARCHAR(20) | FRIEND_REQUEST / CALENDAR / SYSTEM / GROUP |
|
||||
| friendship_id | UUID? | 关联 Friendship |
|
||||
| content | TEXT? | 附加消息 |
|
||||
| is_read | BOOLEAN | 已读状态 |
|
||||
| status | VARCHAR(20) | pending/accepted/rejected/dismissed |
|
||||
|
||||
## 2. API 设计
|
||||
|
||||
| 方法 | 路径 | 功能 |
|
||||
|------|------|------|
|
||||
| POST | /friends/requests | 发送好友请求 |
|
||||
| GET | /friends/requests/outgoing | 获取我发出的请求 |
|
||||
| GET | /friends/requests/inbox | 获取收到的好友请求 |
|
||||
| POST | /friends/requests/{id}/accept | 接受好友请求 |
|
||||
| POST | /friends/requests/{id}/decline | 拒绝好友请求 |
|
||||
| DELETE | /friends/requests/{id} | 取消我的请求 |
|
||||
| GET | /friends | 获取好友列表 |
|
||||
| DELETE | /friends/{id} | 删除好友 |
|
||||
|
||||
## 3. 业务逻辑流程
|
||||
|
||||
### 3.1 发送好友请求
|
||||
|
||||
```
|
||||
1. 验证 target_user_id != current_user_id
|
||||
2. 检查是否已存在 Friendship 记录
|
||||
- 已 accepted: 返回 409
|
||||
- 已 pending: 返回 409
|
||||
- 已 blocked: 返回 403
|
||||
3. 创建 Friendship (status=pending, initiator_id=current_user)
|
||||
4. 创建 InboxMessage (message_type=FRIEND_REQUEST, recipient=target_user)
|
||||
5. 提交事务
|
||||
```
|
||||
|
||||
### 3.2 接受好友请求
|
||||
|
||||
```
|
||||
1. 查询 Friendship 和 InboxMessage
|
||||
2. 验证 current_user == recipient
|
||||
3. 更新 Friendship (status=accepted, accepted_at=now)
|
||||
4. 更新 InboxMessage (status=accepted)
|
||||
5. 提交事务
|
||||
```
|
||||
|
||||
### 3.3 拒绝好友请求
|
||||
|
||||
```
|
||||
1. 查询 Friendship 和 InboxMessage
|
||||
2. 验证 current_user == recipient
|
||||
3. 更新 Friendship (status=declined)
|
||||
4. 更新 InboxMessage (status=rejected)
|
||||
5. 提交事务
|
||||
```
|
||||
|
||||
### 3.4 获取好友列表
|
||||
|
||||
```
|
||||
查询 Friendship WHERE (user_low_id=current OR user_high_id=current) AND status=accepted
|
||||
```
|
||||
|
||||
## 4. 响应 Schema
|
||||
|
||||
### FriendRequestResponse
|
||||
```python
|
||||
{
|
||||
"id": "uuid",
|
||||
"sender": {"id": "uuid", "username": "string", "avatar_url": "string?"},
|
||||
"recipient": {"id": "uuid", "username": "string", "avatar_url": "string?"},
|
||||
"content": "string?",
|
||||
"status": "pending",
|
||||
"created_at": "datetime"
|
||||
}
|
||||
```
|
||||
|
||||
### FriendResponse
|
||||
```python
|
||||
{
|
||||
"id": "uuid",
|
||||
"friend": {"id": "uuid", "username": "string", "avatar_url": "string?"},
|
||||
"status": "accepted",
|
||||
"created_at": "datetime",
|
||||
"accepted_at": "datetime?"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 边界处理
|
||||
|
||||
| 场景 | 状态码 | 响应 |
|
||||
|------|--------|------|
|
||||
| 对自己发送请求 | 400 | Cannot send friend request to yourself |
|
||||
| 已是好友 | 409 | Already friends |
|
||||
| 已有待处理请求 | 409 | Friend request already exists |
|
||||
| 被对方屏蔽 | 403 | Blocked by user |
|
||||
| 请求不存在 | 404 | Friend request not found |
|
||||
| 无权限操作 | 403 | Not authorized |
|
||||
|
||||
## 6. 测试用例
|
||||
|
||||
### 单元测试
|
||||
- FriendshipService 业务逻辑
|
||||
- 状态转换验证
|
||||
- 边界条件处理
|
||||
|
||||
### 集成测试
|
||||
- POST /friends/requests - 成功/失败场景
|
||||
- GET /friends/requests/inbox - 返回正确列表
|
||||
- POST /friends/requests/{id}/accept - 状态更新
|
||||
- DELETE /friends/{id} - 删除好友
|
||||
@@ -1,870 +0,0 @@
|
||||
# 好友申请功能实现计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 实现好友申请、待办消息、添加/删除好友等系列功能的后端API
|
||||
|
||||
**Architecture:** 使用 repository/service/router 模式,复用已有的 Friendship 和 InboxMessage 模型,通过 inbox_messages 表存储好友请求通知
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy, Pydantic
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 创建 friendships 模块目录结构和基础文件
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/src/v1/friendships/__init__.py`
|
||||
- Create: `backend/src/v1/friendships/schemas.py`
|
||||
- Create: `backend/src/v1/friendships/repository.py`
|
||||
- Create: `backend/src/v1/friendships/service.py`
|
||||
- Create: `backend/src/v1/friendships/dependencies.py`
|
||||
- Create: `backend/src/v1/friendships/router.py`
|
||||
|
||||
**Step 1: 创建目录和基础 schema**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/__init__.py
|
||||
```
|
||||
|
||||
**Step 2: 创建 Pydantic schemas**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/schemas.py
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserBasicInfo(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
avatar_url: Optional[str] = None
|
||||
|
||||
|
||||
class FriendRequestCreate(BaseModel):
|
||||
target_user_id: UUID
|
||||
content: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
class FriendRequestResponse(BaseModel):
|
||||
id: UUID
|
||||
sender: UserBasicInfo
|
||||
recipient: UserBasicInfo
|
||||
content: Optional[str]
|
||||
status: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class FriendResponse(BaseModel):
|
||||
id: UUID
|
||||
friend: UserBasicInfo
|
||||
status: str
|
||||
created_at: datetime
|
||||
accepted_at: Optional[datetime]
|
||||
|
||||
|
||||
class FriendRequestAction(BaseModel):
|
||||
# For accept/decline - no body needed but kept for extensibility
|
||||
pass
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/friendships/
|
||||
git commit -m "feat(friendships): create module structure and schemas"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 实现 FriendshipRepository
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/friendships/repository.py`
|
||||
|
||||
**Step 1: 写入失败的测试**
|
||||
|
||||
```python
|
||||
# backend/tests/unit/v1/friendships/test_friendship_repository.py
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from v1.friendships.repository import FriendshipRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
# Create mock async session
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_friendship_request(mock_session):
|
||||
repository = FriendshipRepository(mock_session)
|
||||
# Test creating friendship request
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_pending_request_between_users(mock_session):
|
||||
repository = FriendshipRepository(mock_session)
|
||||
# Test checking existing requests
|
||||
pass
|
||||
```
|
||||
|
||||
**Step 2: 运行测试确认失败**
|
||||
|
||||
**Step 3: 实现 repository**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/repository.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import select, and_, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.friendships import Friendship, FriendshipStatus
|
||||
from models.inbox_messages import InboxMessage, InboxMessageType, InboxMessageStatus
|
||||
|
||||
|
||||
class FriendshipRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._session = session
|
||||
|
||||
async def create_request(
|
||||
self,
|
||||
user_low_id: UUID,
|
||||
user_high_id: UUID,
|
||||
initiator_id: UUID,
|
||||
recipient_id: UUID,
|
||||
content: Optional[str] = None,
|
||||
) -> tuple[Friendship, InboxMessage]:
|
||||
friendship = Friendship(
|
||||
user_low_id=user_low_id,
|
||||
user_high_id=user_high_id,
|
||||
initiator_id=initiator_id,
|
||||
status=FriendshipStatus.PENDING,
|
||||
)
|
||||
self._session.add(friendship)
|
||||
await self._session.flush()
|
||||
|
||||
inbox_message = InboxMessage(
|
||||
recipient_id=recipient_id,
|
||||
sender_id=initiator_id,
|
||||
message_type=InboxMessageType.FRIEND_REQUEST,
|
||||
friendship_id=friendship.id,
|
||||
content=content,
|
||||
status=InboxMessageStatus.PENDING,
|
||||
)
|
||||
self._session.add(inbox_message)
|
||||
return friendship, inbox_message
|
||||
|
||||
async def get_friendship_between_users(
|
||||
self, user_a_id: UUID, user_b_id: UUID
|
||||
) -> Optional[Friendship]:
|
||||
low_id = min(user_a_id, user_b_id)
|
||||
high_id = max(user_a_id, user_b_id)
|
||||
stmt = select(Friendship).where(
|
||||
and_(
|
||||
Friendship.user_low_id == low_id,
|
||||
Friendship.user_high_id == high_id,
|
||||
)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_pending_inbox_for_recipient(
|
||||
self, friendship_id: UUID, recipient_id: UUID
|
||||
) -> Optional[InboxMessage]:
|
||||
stmt = select(InboxMessage).where(
|
||||
and_(
|
||||
InboxMessage.friendship_id == friendship_id,
|
||||
InboxMessage.recipient_id == recipient_id,
|
||||
InboxMessage.message_type == InboxMessageType.FRIEND_REQUEST,
|
||||
)
|
||||
)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_friendship_by_id(self, friendship_id: UUID) -> Optional[Friendship]:
|
||||
stmt = select(Friendship).where(Friendship.id == friendship_id)
|
||||
result = await self._session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_inbox_messages_for_user(
|
||||
self, user_id: UUID, status: Optional[InboxMessageStatus] = None
|
||||
) -> list[InboxMessage]:
|
||||
stmt = select(InboxMessage).where(
|
||||
and_(
|
||||
InboxMessage.recipient_id == user_id,
|
||||
InboxMessage.message_type == InboxMessageType.FRIEND_REQUEST,
|
||||
)
|
||||
)
|
||||
if status:
|
||||
stmt = stmt.where(InboxMessage.status == status)
|
||||
stmt = stmt.order_by(InboxMessage.created_at.desc())
|
||||
result = await self._session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_outgoing_requests(
|
||||
self, user_id: UUID, status: Optional[FriendshipStatus] = None
|
||||
) -> list[Friendship]:
|
||||
stmt = select(Friendship).where(Friendship.initiator_id == user_id)
|
||||
if status:
|
||||
stmt = stmt.where(Friendship.status == status)
|
||||
else:
|
||||
stmt = stmt.where(Friendship.status == FriendshipStatus.PENDING)
|
||||
stmt = stmt.order_by(Friendship.created_at.desc())
|
||||
result = await self._session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_friends_list(self, user_id: UUID) -> list[Friendship]:
|
||||
stmt = select(Friendship).where(
|
||||
or_(
|
||||
Friendship.user_low_id == user_id,
|
||||
Friendship.user_high_id == user_id,
|
||||
),
|
||||
Friendship.status == FriendshipStatus.ACCEPTED,
|
||||
).order_by(Friendship.updated_at.desc())
|
||||
result = await self._session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
```
|
||||
|
||||
**Step 4: 运行测试确认通过**
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/friendships/repository.py backend/tests/unit/v1/friendships/
|
||||
git commit -m "feat(friendships): implement repository"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 实现 FriendshipService
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/friendships/service.py`
|
||||
|
||||
**Step 1: 写入失败的测试**
|
||||
|
||||
```python
|
||||
# backend/tests/unit/v1/friendships/test_friendship_service.py
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from v1.friendships.service import FriendshipService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_repository():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_friend_request_success(mock_repository):
|
||||
service = FriendshipService(mock_repository, current_user)
|
||||
# Test successful friend request
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_friend_request_to_self_fails():
|
||||
# Test that sending to self returns 400
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_friend_request_when_already_friends():
|
||||
# Test that sending to existing friend returns 409
|
||||
pass
|
||||
```
|
||||
|
||||
**Step 2: 运行测试确认失败**
|
||||
|
||||
**Step 3: 实现 service**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/service.py
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.auth.models import CurrentUser
|
||||
from core.db.base_service import BaseService
|
||||
from core.logging import get_logger
|
||||
from models.friendships import Friendship, FriendshipStatus
|
||||
from models.inbox_messages import InboxMessageStatus
|
||||
from models.profile import Profile
|
||||
from v1.friendships.repository import FriendshipRepository
|
||||
from v1.friendships.schemas import (
|
||||
FriendRequestCreate,
|
||||
FriendRequestResponse,
|
||||
FriendResponse,
|
||||
UserBasicInfo,
|
||||
)
|
||||
|
||||
logger = get_logger("v1.friendships.service")
|
||||
|
||||
|
||||
class FriendshipService(BaseService):
|
||||
def __init__(
|
||||
self,
|
||||
repository: FriendshipRepository,
|
||||
session: AsyncSession,
|
||||
current_user: CurrentUser,
|
||||
) -> None:
|
||||
super().__init__(current_user=current_user)
|
||||
self._repository = repository
|
||||
self._session = session
|
||||
|
||||
async def send_request(
|
||||
self, payload: FriendRequestCreate
|
||||
) -> FriendRequestResponse:
|
||||
current_user_id = self.require_user_id()
|
||||
target_user_id = payload.target_user_id
|
||||
|
||||
if current_user_id == target_user_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot send friend request to yourself"
|
||||
)
|
||||
|
||||
# Check existing relationship
|
||||
existing = await self._repository.get_friendship_between_users(
|
||||
current_user_id, target_user_id
|
||||
)
|
||||
|
||||
if existing:
|
||||
if existing.status == FriendshipStatus.ACCEPTED:
|
||||
raise HTTPException(status_code=409, detail="Already friends")
|
||||
if existing.status == FriendshipStatus.PENDING:
|
||||
raise HTTPException(status_code=409, detail="Friend request already exists")
|
||||
if existing.status == FriendshipStatus.BLOCKED:
|
||||
raise HTTPException(status_code=403, detail="Blocked by user")
|
||||
|
||||
user_low_id = min(current_user_id, target_user_id)
|
||||
user_high_id = max(current_user_id, target_user_id)
|
||||
|
||||
friendship, inbox = await self._repository.create_request(
|
||||
user_low_id=user_low_id,
|
||||
user_high_id=user_high_id,
|
||||
initiator_id=current_user_id,
|
||||
recipient_id=target_user_id,
|
||||
content=payload.content,
|
||||
)
|
||||
await self._session.commit()
|
||||
|
||||
sender_info = await self._get_profile_info(current_user_id)
|
||||
recipient_info = await self._get_profile_info(target_user_id)
|
||||
|
||||
return FriendRequestResponse(
|
||||
id=friendship.id,
|
||||
sender=sender_info,
|
||||
recipient=recipient_info,
|
||||
content=payload.content,
|
||||
status=friendship.status.value,
|
||||
created_at=friendship.created_at,
|
||||
)
|
||||
|
||||
async def accept_request(self, friendship_id: UUID) -> FriendRequestResponse:
|
||||
current_user_id = self.require_user_id()
|
||||
|
||||
friendship = await self._repository.get_friendship_by_id(friendship_id)
|
||||
if not friendship:
|
||||
raise HTTPException(status_code=404, detail="Friend request not found")
|
||||
|
||||
# Determine recipient - must be the current user
|
||||
recipient_id = friendship.user_low_id if friendship.user_high_id == current_user_id else friendship.user_high_id
|
||||
if recipient_id != current_user_id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
inbox = await self._repository.get_pending_inbox_for_recipient(
|
||||
friendship_id, current_user_id
|
||||
)
|
||||
|
||||
friendship.status = FriendshipStatus.ACCEPTED
|
||||
friendship.accepted_at = datetime.utcnow()
|
||||
|
||||
if inbox:
|
||||
inbox.status = InboxMessageStatus.ACCEPTED
|
||||
|
||||
await self._session.commit()
|
||||
|
||||
initiator_info = await self._get_profile_info(friendship.initiator_id)
|
||||
recipient_info = await self._get_profile_info(current_user_id)
|
||||
|
||||
return FriendRequestResponse(
|
||||
id=friendship.id,
|
||||
sender=initiator_info,
|
||||
recipient=recipient_info,
|
||||
content=inbox.content if inbox else None,
|
||||
status=friendship.status.value,
|
||||
created_at=friendship.created_at,
|
||||
)
|
||||
|
||||
async def decline_request(self, friendship_id: UUID) -> FriendRequestResponse:
|
||||
current_user_id = self.require_user_id()
|
||||
|
||||
friendship = await self._repository.get_friendship_by_id(friendship_id)
|
||||
if not friendship:
|
||||
raise HTTPException(status_code=404, detail="Friend request not found")
|
||||
|
||||
recipient_id = friendship.user_low_id if friendship.user_high_id == current_user_id else friendship.user_high_id
|
||||
if recipient_id != current_user_id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
inbox = await self._repository.get_pending_inbox_for_recipient(
|
||||
friendship_id, current_user_id
|
||||
)
|
||||
|
||||
friendship.status = FriendshipStatus.DECLINED
|
||||
|
||||
if inbox:
|
||||
inbox.status = InboxMessageStatus.REJECTED
|
||||
|
||||
await self._session.commit()
|
||||
|
||||
initiator_info = await self._get_profile_info(friendship.initiator_id)
|
||||
recipient_info = await self._get_profile_info(current_user_id)
|
||||
|
||||
return FriendRequestResponse(
|
||||
id=friendship.id,
|
||||
sender=initiator_info,
|
||||
recipient=recipient_info,
|
||||
content=inbox.content if inbox else None,
|
||||
status=friendship.status.value,
|
||||
created_at=friendship.created_at,
|
||||
)
|
||||
|
||||
async def cancel_request(self, friendship_id: UUID) -> None:
|
||||
current_user_id = self.require_user_id()
|
||||
|
||||
friendship = await self._repository.get_friendship_by_id(friendship_id)
|
||||
if not friendship:
|
||||
raise HTTPException(status_code=404, detail="Friend request not found")
|
||||
|
||||
if friendship.initiator_id != current_user_id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
if friendship.status != FriendshipStatus.PENDING:
|
||||
raise HTTPException(status_code=400, detail="Can only cancel pending requests")
|
||||
|
||||
inbox = await self._repository.get_pending_inbox_for_recipient(
|
||||
friendship_id, friendship.user_low_id if friendship.user_high_id == current_user_id else friendship.user_high_id
|
||||
)
|
||||
|
||||
friendship.status = FriendshipStatus.CANCELED
|
||||
|
||||
if inbox:
|
||||
inbox.status = InboxMessageStatus.DISMISSED
|
||||
|
||||
await self._session.commit()
|
||||
|
||||
async def get_inbox(self) -> list[FriendRequestResponse]:
|
||||
current_user_id = self.require_user_id()
|
||||
inbox_messages = await self._repository.get_pending_inbox_for_user(
|
||||
current_user_id, InboxMessageStatus.PENDING
|
||||
)
|
||||
|
||||
results = []
|
||||
for msg in inbox_messages:
|
||||
friendship = await self._repository.get_friendship_by_id(msg.friendship_id)
|
||||
if not friendship:
|
||||
continue
|
||||
|
||||
sender_info = await self._get_profile_info(msg.sender_id)
|
||||
recipient_info = await self._get_profile_info(current_user_id)
|
||||
|
||||
results.append(FriendRequestResponse(
|
||||
id=friendship.id,
|
||||
sender=sender_info,
|
||||
recipient=recipient_info,
|
||||
content=msg.content,
|
||||
status=msg.status.value,
|
||||
created_at=msg.created_at,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_outgoing_requests(self) -> list[FriendRequestResponse]:
|
||||
current_user_id = self.require_user_id()
|
||||
friendships = await self._repository.get_outgoing_requests(current_user_id)
|
||||
|
||||
results = []
|
||||
for friendship in friendships:
|
||||
sender_info = await self._get_profile_info(current_user_id)
|
||||
recipient_id = friendship.user_low_id if friendship.user_high_id == current_user_id else friendship.user_high_id
|
||||
recipient_info = await self._get_profile_info(recipient_id)
|
||||
|
||||
inbox = await self._repository.get_pending_inbox_for_recipient(
|
||||
friendship.id, recipient_id
|
||||
)
|
||||
|
||||
results.append(FriendRequestResponse(
|
||||
id=friendship.id,
|
||||
sender=sender_info,
|
||||
recipient=recipient_info,
|
||||
content=inbox.content if inbox else None,
|
||||
status=friendship.status.value,
|
||||
created_at=friendship.created_at,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_friends_list(self) -> list[FriendResponse]:
|
||||
current_user_id = self.require_user_id()
|
||||
friendships = await self._repository.get_friends_list(current_user_id)
|
||||
|
||||
results = []
|
||||
for friendship in friendships:
|
||||
friend_id = friendship.user_low_id if friendship.user_high_id == current_user_id else friendship.user_high_id
|
||||
friend_info = await self._get_profile_info(friend_id)
|
||||
|
||||
results.append(FriendResponse(
|
||||
id=friendship.id,
|
||||
friend=friend_info,
|
||||
status=friendship.status.value,
|
||||
created_at=friendship.created_at,
|
||||
accepted_at=friendship.accepted_at,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def remove_friend(self, friendship_id: UUID) -> None:
|
||||
current_user_id = self.require_user_id()
|
||||
|
||||
friendship = await self._repository.get_friendship_by_id(friendship_id)
|
||||
if not friendship:
|
||||
raise HTTPException(status_code=404, detail="Friendship not found")
|
||||
|
||||
if friendship.status != FriendshipStatus.ACCEPTED:
|
||||
raise HTTPException(status_code=400, detail="Can only remove accepted friends")
|
||||
|
||||
# Verify user is part of this friendship
|
||||
if friendship.user_low_id != current_user_id and friendship.user_high_id != current_user_id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
# Soft delete - mark as canceled
|
||||
friendship.status = FriendshipStatus.CANCELED
|
||||
await self._session.commit()
|
||||
|
||||
async def _get_profile_info(self, user_id: UUID) -> UserBasicInfo:
|
||||
from sqlalchemy import select
|
||||
from models.profile import Profile
|
||||
|
||||
stmt = select(Profile).where(Profile.id == user_id)
|
||||
result = await self._session.execute(stmt)
|
||||
profile = result.scalar_one_or_none()
|
||||
|
||||
if not profile:
|
||||
return UserBasicInfo(id=str(user_id), username="Unknown")
|
||||
|
||||
return UserBasicInfo(
|
||||
id=str(profile.id),
|
||||
username=profile.username,
|
||||
avatar_url=profile.avatar_url,
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: 运行测试确认通过**
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/friendships/service.py
|
||||
git commit -m "feat(friendships): implement service layer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 实现 Dependencies 和 Router
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/friendships/dependencies.py`
|
||||
- Modify: `backend/src/v1/friendships/router.py`
|
||||
|
||||
**Step 1: 实现 dependencies**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/dependencies.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.auth.models import CurrentUser
|
||||
from core.db import get_db
|
||||
from v1.friendships.repository import FriendshipRepository
|
||||
from v1.friendships.service import FriendshipService
|
||||
from v1.users.dependencies import get_current_user
|
||||
|
||||
|
||||
async def get_friendship_repository(
|
||||
session: Annotated[AsyncSession, Depends(get_db)]
|
||||
) -> FriendshipRepository:
|
||||
return FriendshipRepository(session)
|
||||
|
||||
|
||||
async def get_friendship_service(
|
||||
repository: Annotated[FriendshipRepository, Depends(get_friendship_repository)],
|
||||
session: Annotated[AsyncSession, Depends(get_db)],
|
||||
user: Annotated[CurrentUser, Depends(get_current_user)],
|
||||
) -> FriendshipService:
|
||||
return FriendshipService(
|
||||
repository=repository,
|
||||
session=session,
|
||||
current_user=user,
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2: 实现 router**
|
||||
|
||||
```python
|
||||
# backend/src/v1/friendships/router.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, HTTPException
|
||||
|
||||
from v1.friendships.dependencies import get_friendship_service
|
||||
from v1.friendships.schemas import (
|
||||
FriendRequestCreate,
|
||||
FriendRequestResponse,
|
||||
FriendResponse,
|
||||
)
|
||||
from v1.friendships.service import FriendshipService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/friends", tags=["friends"])
|
||||
|
||||
|
||||
@router.post("/requests", response_model=FriendRequestResponse, status_code=201)
|
||||
async def send_friend_request(
|
||||
payload: FriendRequestCreate,
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> FriendRequestResponse:
|
||||
return await service.send_request(payload)
|
||||
|
||||
|
||||
@router.get("/requests/inbox", response_model=list[FriendRequestResponse])
|
||||
async def get_inbox(
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> list[FriendRequestResponse]:
|
||||
return await service.get_inbox()
|
||||
|
||||
|
||||
@router.get("/requests/outgoing", response_model=list[FriendRequestResponse])
|
||||
async def get_outgoing_requests(
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> list[FriendRequestResponse]:
|
||||
return await service.get_outgoing_requests()
|
||||
|
||||
|
||||
@router.post("/requests/{friendship_id}/accept", response_model=FriendRequestResponse)
|
||||
async def accept_friend_request(
|
||||
friendship_id: Annotated[UUID, Path()],
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> FriendRequestResponse:
|
||||
return await service.accept_request(friendship_id)
|
||||
|
||||
|
||||
@router.post("/requests/{friendship_id}/decline", response_model=FriendRequestResponse)
|
||||
async def decline_friend_request(
|
||||
friendship_id: Annotated[UUID, Path()],
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> FriendRequestResponse:
|
||||
return await service.decline_request(friendship_id)
|
||||
|
||||
|
||||
@router.delete("/requests/{friendship_id}", status_code=204)
|
||||
async def cancel_friend_request(
|
||||
friendship_id: Annotated[UUID, Path()],
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> None:
|
||||
await service.cancel_request(friendship_id)
|
||||
|
||||
|
||||
@router.get("", response_model=list[FriendResponse])
|
||||
async def get_friends_list(
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> list[FriendResponse]:
|
||||
return await service.get_friends_list()
|
||||
|
||||
|
||||
@router.delete("/{friendship_id}", status_code=204)
|
||||
async def remove_friend(
|
||||
friendship_id: Annotated[UUID, Path()],
|
||||
service: Annotated[FriendshipService, Depends(get_friendship_service)],
|
||||
) -> None:
|
||||
await service.remove_friend(friendship_id)
|
||||
```
|
||||
|
||||
**Step 3: 注册 router 到主路由**
|
||||
|
||||
```python
|
||||
# backend/src/v1/router.py
|
||||
from fastapi import APIRouter
|
||||
from v1.auth.router import router as auth_router
|
||||
from v1.users.router import router as users_router
|
||||
from v1.profile.router import router as profile_router
|
||||
from v1.friendships.router import router as friendships_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(auth_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(profile_router)
|
||||
router.include_router(friendships_router)
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/v1/friendships/dependencies.py backend/src/v1/friendships/router.py backend/src/v1/router.py
|
||||
git commit -m "feat(friendships): implement router and dependencies"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 集成测试
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/integration/test_friendship_routes.py`
|
||||
|
||||
**Step 1: 写入测试**
|
||||
|
||||
```python
|
||||
# backend/tests/integration/test_friendship_routes.py
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from main import app # FastAPI app
|
||||
from core.db.base import Base
|
||||
from core.db import get_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
# Setup test database
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async def override_get_db():
|
||||
async with AsyncSession(engine) as session:
|
||||
yield session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_friend_request_requires_auth(async_client):
|
||||
response = await async_client.post(
|
||||
"/api/v1/friends/requests",
|
||||
json={"target_user_id": "..."}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# More tests...
|
||||
```
|
||||
|
||||
**Step 2: 运行测试**
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 运行 Lint 和 Typecheck
|
||||
|
||||
**Step 1: 运行 ruff**
|
||||
|
||||
```bash
|
||||
cd backend && uv run ruff check src/v1/friendships/
|
||||
```
|
||||
|
||||
**Step 2: 运行 typecheck**
|
||||
|
||||
```bash
|
||||
cd backend && uv run basedpyright src/v1/friendships/
|
||||
```
|
||||
|
||||
**Step 3: Commit (if any fixes needed)**
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 更新文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
|
||||
**Step 1: 添加 API 文档**
|
||||
|
||||
```markdown
|
||||
## Friends
|
||||
|
||||
### Send Friend Request
|
||||
- **POST** `/api/v1/friends/requests`
|
||||
- **Auth:** Required
|
||||
- **Body:** `{ "target_user_id": "uuid", "content": "string?" }`
|
||||
- **Response:** `FriendRequestResponse`
|
||||
|
||||
### Get Inbox
|
||||
- **GET** `/api/v1/friends/requests/inbox`
|
||||
- **Auth:** Required
|
||||
- **Response:** `FriendRequestResponse[]`
|
||||
|
||||
### Accept Request
|
||||
- **POST** `/api/v1/friends/requests/{id}/accept`
|
||||
- **Auth:** Required
|
||||
- **Response:** `FriendRequestResponse`
|
||||
|
||||
### Decline Request
|
||||
- **POST** `/api/v1/friends/requests/{id}/decline`
|
||||
- **Auth:** Required
|
||||
- **Response:** `FriendRequestResponse`
|
||||
|
||||
### Get Friends List
|
||||
- **GET** `/api/v1/friends`
|
||||
- **Auth:** Required
|
||||
- **Response:** `FriendResponse[]`
|
||||
|
||||
### Remove Friend
|
||||
- **DELETE** `/api/v1/friends/{id}`
|
||||
- **Auth:** Required
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-route.md
|
||||
git commit -m "docs: add friendship API documentation"
|
||||
```
|
||||
@@ -30,8 +30,11 @@
|
||||
| `todo_sources` | 待办与日程来源关联 |
|
||||
| `automation_jobs` | 定时任务 |
|
||||
| `sessions` | Agent 对话会话 |
|
||||
| `llm_factories` | LLM 工厂配置 |
|
||||
| `messages` | 会话消息记录 |
|
||||
| `llm_factory` | LLM 工厂配置 |
|
||||
| `llms` | LLM 模型实例 |
|
||||
| `user_agent_catalog` | Agent 类型目录 |
|
||||
| `invite_codes` | 邀请码 |
|
||||
|
||||
---
|
||||
|
||||
@@ -44,14 +47,17 @@
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK,`auth.users.id` |
|
||||
| `username` | VARCHAR(50) | 用户名 |
|
||||
| `username` | VARCHAR(30) | 用户名 |
|
||||
| `avatar_url` | TEXT | 头像 URL |
|
||||
| `bio` | TEXT | 个人简介 |
|
||||
| `bio` | VARCHAR(200) | 个人简介 |
|
||||
| `settings` | JSONB | 用户设置 |
|
||||
| `referred_by` | UUID | 邀请人 ID |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `username` 唯一
|
||||
|
||||
**settings JSONB 默认结构:**
|
||||
```json
|
||||
{
|
||||
@@ -75,15 +81,19 @@
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK |
|
||||
| `user_id` | UUID | 用户 ID(唯一) |
|
||||
| `user_id` | UUID | 用户 ID |
|
||||
| `llm_id` | UUID | 关联的 LLM 模型 |
|
||||
| `agent_type` | VARCHAR(20) | 枚举:`INTENT_RECOGNITION`, `TASK_EXECUTION`, `RESULT_REPORTING` |
|
||||
| `config` | JSONB | Agent 配置参数 |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `paused`, `migrating` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `updated_by` | UUID | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `(user_id, agent_type)` 唯一
|
||||
|
||||
---
|
||||
|
||||
### memories
|
||||
@@ -130,8 +140,11 @@
|
||||
| `requested_at` | TIMESTAMPTZ | 请求时间 |
|
||||
| `accepted_at` | TIMESTAMPTZ | 接受时间 |
|
||||
| `blocked_by` | UUID | 阻止者用户 ID |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `updated_by` | UUID | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `user_low_id < user_high_id`,`(user_low_id, user_high_id)` 唯一
|
||||
|
||||
@@ -148,6 +161,8 @@
|
||||
| `description` | TEXT | 群组描述 |
|
||||
| `owner_id` | UUID | 创建者 ID |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `archived` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `updated_by` | UUID | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
@@ -167,10 +182,13 @@
|
||||
| `join_source` | VARCHAR(20) | 加入方式:`invited`, `joined` |
|
||||
| `invited_by` | UUID | 邀请人 ID |
|
||||
| `joined_at` | TIMESTAMPTZ | 加入时间 |
|
||||
| `removed_at` | TIMESTAMPTZ | 移除时间 |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `muted`, `removed` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `updated_by` | UUID | 更新者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `removed_at` | TIMESTAMPTZ | 移除时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `(group_id, user_id)` 唯一
|
||||
|
||||
@@ -190,11 +208,13 @@
|
||||
| `end_at` | TIMESTAMPTZ | 结束时间 |
|
||||
| `timezone` | VARCHAR(50) | 时区 |
|
||||
| `metadata` | JSONB | 扩展字段 |
|
||||
| `recurrence_rule` | VARCHAR(100) | 循环规则 |
|
||||
| `recurrence_rule` | VARCHAR(255) | 循环规则 |
|
||||
| `source_type` | VARCHAR(20) | 来源:`manual`, `imported`, `agent_generated` |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `completed`, `canceled`, `archived` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**metadata JSONB 默认结构:**
|
||||
```json
|
||||
@@ -231,10 +251,12 @@
|
||||
| `id` | UUID | PK |
|
||||
| `item_id` | UUID | 日程事项 ID |
|
||||
| `subscriber_id` | UUID | 订阅者 ID |
|
||||
| `permission` | INTEGER | 权限位图(view=1, invite=2, edit=4) |
|
||||
| `notify_level` | VARCHAR(20) | 通知级别:`all`, `mentions`, `none` |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `paused`, `unsubscribed` |
|
||||
| `permission` | INTEGER | 权限位图(view=1, invite=2, edit=4),默认 1 |
|
||||
| `notify_level` | VARCHAR(20) | 通知级别:`all`, `mentions`, `none`,默认 `all` |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `paused`, `unsubscribed`,默认 `active` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
**约束:** `(item_id, subscriber_id)` 唯一,`permission BETWEEN 0 AND 7`
|
||||
|
||||
@@ -254,9 +276,11 @@
|
||||
| `schedule_item_id` | UUID | 日程关联(calendar 时必填) |
|
||||
| `group_id` | UUID | 群组关联(group 时必填) |
|
||||
| `content` | TEXT | 消息内容(system 用) |
|
||||
| `is_read` | BOOLEAN | 是否已读 |
|
||||
| `is_read` | BOOLEAN | 是否已读,默认 false |
|
||||
| `status` | VARCHAR(20) | 状态:`pending`, `accepted`, `rejected`, `dismissed` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
**message_type 与业务字段对应:**
|
||||
| message_type | 必填字段 |
|
||||
@@ -266,6 +290,8 @@
|
||||
| system | 全部可空 |
|
||||
| group | group_id |
|
||||
|
||||
**sender 约束:** system 类型 sender_id 为空,其他类型 sender_id 必填
|
||||
|
||||
---
|
||||
|
||||
### todos
|
||||
@@ -277,12 +303,17 @@
|
||||
| `id` | UUID | PK |
|
||||
| `owner_id` | UUID | 所有者 ID |
|
||||
| `title` | VARCHAR(255) | 标题 |
|
||||
| `description` | TEXT | 描述 |
|
||||
| `description` | VARCHAR(1000) | 描述 |
|
||||
| `due_at` | TIMESTAMPTZ | 截止时间 |
|
||||
| `priority` | INTEGER | 优先级(1=重要且紧急, 2=重要不紧急, 3=紧急不重要, 4=不重要不紧急) |
|
||||
| `priority` | INTEGER | 优先级(1-4,1=重要且紧急) |
|
||||
| `status` | VARCHAR(20) | 状态:`pending`, `done`, `canceled` |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `completed_at` | TIMESTAMPTZ | 完成时间 |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `priority BETWEEN 1 AND 4`
|
||||
|
||||
---
|
||||
|
||||
@@ -296,6 +327,7 @@
|
||||
| `todo_id` | UUID | 待办 ID |
|
||||
| `schedule_item_id` | UUID | 日程事项 ID |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
**约束:** `(todo_id, schedule_item_id)` 唯一
|
||||
|
||||
@@ -317,8 +349,12 @@
|
||||
| `timezone` | VARCHAR(50) | 时区 |
|
||||
| `last_run_at` | TIMESTAMPTZ | 最近运行时间 |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `disabled` |
|
||||
| `created_by` | UUID | 创建者 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `(id, owner_id)` 唯一
|
||||
|
||||
---
|
||||
|
||||
@@ -332,14 +368,48 @@ Agent 对话会话。
|
||||
| `user_id` | UUID | 用户 ID |
|
||||
| `session_type` | VARCHAR(20) | 会话类型:`chat`, `automation` |
|
||||
| `job_id` | UUID | 自动化任务 ID(automation 时必填) |
|
||||
| `title` | VARCHAR(255) | 会话标题 |
|
||||
| `status` | VARCHAR(20) | 状态:`pending`, `running`, `completed`, `failed` |
|
||||
| `last_activity_at` | TIMESTAMPTZ | 最后活跃时间 |
|
||||
| `message_count` | INTEGER | 消息计数,默认 0 |
|
||||
| `total_tokens` | INTEGER | 总 token 数,默认 0 |
|
||||
| `total_cost` | NUMERIC(12,6) | 总费用,默认 0 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `session_type='chat' → job_id IS NULL`, `session_type='automation' → job_id IS NOT NULL`
|
||||
|
||||
---
|
||||
|
||||
### llm_factories
|
||||
### messages
|
||||
|
||||
会话消息记录。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK |
|
||||
| `session_id` | UUID | 会话 ID |
|
||||
| `seq` | INTEGER | 消息序号 |
|
||||
| `role` | VARCHAR(20) | 角色:`user`, `assistant`, `system`, `tool` |
|
||||
| `content` | TEXT | 消息内容 |
|
||||
| `model_code` | VARCHAR(50) | 模型标识 |
|
||||
| `tool_name` | VARCHAR(100) | 工具名称 |
|
||||
| `input_tokens` | INTEGER | 输入 token 数,默认 0 |
|
||||
| `output_tokens` | INTEGER | 输出 token 数,默认 0 |
|
||||
| `cost` | NUMERIC(12,6) | 费用,默认 0 |
|
||||
| `currency` | VARCHAR(3) | 货币,默认 USD |
|
||||
| `latency_ms` | INTEGER | 延迟(毫秒) |
|
||||
| `metadata` | JSONB | 扩展字段 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `(session_id, seq)` 唯一
|
||||
|
||||
---
|
||||
|
||||
### llm_factory
|
||||
|
||||
LLM 工厂配置。
|
||||
|
||||
@@ -347,11 +417,13 @@ LLM 工厂配置。
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK |
|
||||
| `name` | VARCHAR(50) | 工厂名称 |
|
||||
| `base_url` | TEXT | API 基础 URL |
|
||||
| `api_key` | TEXT | API 密钥 |
|
||||
| `enabled` | BOOLEAN | 是否启用 |
|
||||
| `request_url` | VARCHAR(255) | API 请求 URL |
|
||||
| `avatar` | TEXT | 头像 URL |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `name` 唯一
|
||||
|
||||
---
|
||||
|
||||
@@ -363,12 +435,48 @@ LLM 模型实例。
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK |
|
||||
| `factory_id` | UUID | 工厂 ID |
|
||||
| `model_id` | VARCHAR(50) | 模型标识 |
|
||||
| `name` | VARCHAR(100) | 显示名称 |
|
||||
| `context_window` | INTEGER | 上下文窗口大小 |
|
||||
| `enabled` | BOOLEAN | 是否启用 |
|
||||
| `model_code` | VARCHAR(50) | 模型标识 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
| `deleted_at` | TIMESTAMPTZ | 软删时间 |
|
||||
|
||||
**约束:** `model_code` 唯一
|
||||
|
||||
---
|
||||
|
||||
### user_agent_catalog
|
||||
|
||||
Agent 类型目录。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `agent_type` | VARCHAR(20) | PK,Agent 类型 |
|
||||
| `llm_id` | UUID | 关联的 LLM 模型 |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `paused`, `migrating` |
|
||||
| `config` | JSONB | Agent 配置参数 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
---
|
||||
|
||||
### invite_codes
|
||||
|
||||
邀请码。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | UUID | PK |
|
||||
| `code` | VARCHAR(8) | 邀请码(8 位大写字母数字) |
|
||||
| `owner_id` | UUID | 拥有者 ID |
|
||||
| `status` | VARCHAR(20) | 状态:`active`, `disabled`, `expired` |
|
||||
| `used_count` | INTEGER | 已使用次数,默认 0 |
|
||||
| `max_uses` | INTEGER | 最大使用次数 |
|
||||
| `expires_at` | TIMESTAMPTZ | 过期时间 |
|
||||
| `reward_config` | JSONB | 奖励配置 |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
**约束:** `code` 唯一,`code` 符合 `[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{8}`,`used_count >= 0`
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user