chore: 清理opencode技能文件、旧设计文档并更新配置文档

This commit is contained in:
qzl
2026-03-03 17:29:01 +08:00
parent 30a4a1af5d
commit a4f684466c
48 changed files with 134 additions and 72641 deletions
+5
View File
@@ -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=
-111
View File
@@ -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
-16
View File
@@ -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)
-53
View File
@@ -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)
-20
View File
@@ -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)
-18
View File
@@ -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)
-24
View File
@@ -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)
-163
View File
@@ -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 }
-93
View File
@@ -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
-18
View File
@@ -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)
-20
View File
@@ -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)
-13
View File
@@ -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)
-11
View File
@@ -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)
-11
View File
@@ -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)
-20
View File
@@ -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)
-13
View File
@@ -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_idwork 类型必须绑定 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 名称与运行文档
-161
View File
@@ -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_byowner_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: 更新注册请求 SchemaTDD
**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 metadataTDD
**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: FAILmetadata 未包含 `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
-567
View File
@@ -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 Schemav1
```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 "..."
```
-136
View File
@@ -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"
```
+129 -21
View File
@@ -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-41=重要紧急) |
| `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 | 自动化任务 IDautomation 时必填) |
| `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) | PKAgent 类型 |
| `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`
---