# AI 对话记录服务
> **文档版本**:v2.2
> **最后更新**:2026-02-03
> **变更说明**:修复技术栈合规性问题(日志格式、枚举定义、异常处理)
> **符合规范**:jointo-tech-stack v1.0
---
## 技术规范说明
本文档遵循 jointo-tech-stack 规范:
- **UUID 规范**:所有 UUID 字段使用 UUID v7(应用层生成),符合 ADR 001 规范
- **时间戳规范**:所有时间字段使用 TIMESTAMPTZ 类型,符合 ADR 006 规范
- **枚举类型**:使用 SMALLINT 存储,Python 使用 IntEnum
- **无物理外键**:应用层保证引用完整性
- **日志格式**:使用 %-formatting,错误日志包含 exc_info=True
- **异步编程**:所有数据库操作使用 async/await
---
## 目录
1. [服务概述](#服务概述)
2. [核心功能](#核心功能)
3. [数据库设计](#数据库设计)
4. [服务实现](#服务实现)
5. [API 接口](#api-接口)
6. [业务流程时序图](#业务流程时序图)
7. [与 AI Service 集成](#与-ai-service-集成)
---
## 服务概述
AI 对话记录服务负责管理用户与 AI 的对话历史,支持多轮对话、上下文管理和任务关联。用户可以通过对话的方式提交 AI 生图/生视频任务,系统会记录完整的对话历史。
### 核心设计:多态关联 + 标签系统
**设计理念**:不同类型的 AI 生成任务需要独立的对话上下文,同一对象的不同变体(如角色的不同装扮、分镜的不同角度)也需要独立的对话,避免对话混淆。
**多态关联模式**:
- `target_type`:目标类型(分镜/角色/场景/道具/资源/音效/配音)
- `target_id`:目标对象 ID(具体的分镜ID/角色ID/场景ID等)
- `tag_id`:标签 ID(可选,用于区分同一对象的不同变体)
- `media_type`:媒体类型(图片/视频/音频/3D模型/文本)
**隔离策略**:
1. **类型隔离**:分镜图对话 ≠ 分镜视频对话
2. **对象隔离**:分镜A的对话 ≠ 分镜B的对话
3. **变体隔离**:角色-张三-少年装扮 ≠ 角色-张三-成年装扮
4. **用户隔离**:用户A的对话 ≠ 用户B的对话
**示例场景**:
```
用户在"分镜001"的"生成图片"界面:
- 只能看到"分镜001"的"图片生成"对话历史
- 看不到"分镜001"的"视频生成"对话历史
- 看不到"分镜002"的任何对话历史
用户在"分镜001-正面角度"的"生成图片"界面:
- 只能看到"分镜001-正面角度"的"图片生成"对话历史
- 看不到"分镜001-侧面角度"的对话历史
用户在"角色-张三-少年装扮"的"生成图片"界面:
- 只能看到"角色-张三-少年装扮"的"图片生成"对话历史
- 看不到"角色-张三-成年装扮"的对话历史
- 看不到"角色-李四"的对话历史
```
### 职责
- **对话会话管理**:创建、查询、删除对话会话
- **消息记录**:记录用户消息和 AI 回复
- **上下文管理**:维护对话上下文,支持多轮对话
- **任务关联**:将对话与 AI 任务(图片/视频/音频生成)关联
- **对话历史查询**:支持按项目、用户、类型、对象、标签查询
- **标签支持**:支持同一对象的不同变体(如角色的不同装扮、分镜的不同角度)
### 应用场景
1. **分镜图片生成对话**:
- 基础图片:用户在分镜编辑页面,通过对话生成分镜图片
- 不同角度:分镜001-正面角度、分镜001-侧面角度、分镜001-俯视角度
2. **分镜视频生成对话**:用户在分镜编辑页面,通过对话生成分镜视频
3. **角色图片生成对话**:
- 基础形象:用户在角色管理页面,通过对话生成角色图片
- 不同装扮:角色-张三-少年装扮、角色-张三-成年装扮、角色-张三-老年装扮
4. **场景图片生成对话**:
- 基础场景:用户在场景管理页面,通过对话生成场景图片
- 不同时间:场景-花果山-白天、场景-花果山-夜晚、场景-花果山-黄昏
5. **道具图片生成对话**:
- 基础道具:用户在道具管理页面,通过对话生成道具图片
- 不同状态:道具-金箍棒-新状态、道具-金箍棒-旧状态
6. **独立音效生成对话**:用户在音效库页面,通过对话生成音效
7. **独立配音生成对话**:用户在配音库页面,通过对话生成配音
8. **提示词优化**:用户输入初始提示词,AI 提供优化建议
9. **参数调整**:用户通过对话调整生成参数(风格、尺寸等)
10. **历史回溯**:查看之前的对话历史,复用成功的提示词
---
## 核心功能
### Python 枚举定义
```python
# app/models/ai_conversation.py
from enum import IntEnum
class ConversationStatus(IntEnum):
"""对话会话状态"""
ACTIVE = 1 # 活跃
ARCHIVED = 2 # 已归档
DELETED = 3 # 已删除
class TargetType(IntEnum):
"""目标类型"""
STORYBOARD = 1 # 分镜
CHARACTER = 2 # 角色
SCENE = 3 # 场景
PROP = 4 # 道具
RESOURCE = 5 # 通用资源
SOUND_EFFECT = 6 # 音效
VOICEOVER = 7 # 配音
class MediaType(IntEnum):
"""媒体类型"""
IMAGE = 1 # 图片
VIDEO = 2 # 视频
AUDIO = 3 # 音频
MODEL_3D = 4 # 3D模型
TEXT = 5 # 文本
```
### 设计说明:多态关联 + 标签系统
#### 为什么需要多态关联?
**问题场景**:
- 用户在"分镜001"生成图片时,不应该看到"分镜002"的对话历史
- 用户在"角色-张三-少年装扮"生成图片时,不应该看到"角色-张三-成年装扮"的对话历史
- 用户在"分镜001-正面角度"生成图片时,不应该看到"分镜001-侧面角度"的对话历史
- 用户在生成"图片"时,不应该看到"视频"的对话历史
**优化后的方案**:
```sql
-- ✅ 多态关联 + 标签系统(最终方案)
target_type SMALLINT -- 目标类型(分镜/角色/场景/道具/资源/音效/配音)
target_id UUID -- 目标对象 ID
tag_id UUID -- 标签 ID(可选,用于区分变体)
media_type SMALLINT -- 媒体类型(图片/视频/音频/3D模型/文本)
-- 优势:
-- 1. 消除冗余(target_type + media_type 独立)
-- 2. 支持变体(通过 tag_id 区分装扮/角度/时间/状态)
-- 3. 灵活扩展(新增类型只需添加枚举值)
-- 4. 查询高效(复合索引优化)
```
#### 枚举定义
**目标类型(TargetType)**:
```python
class TargetType(IntEnum):
"""目标类型"""
STORYBOARD = 1 # 分镜
CHARACTER = 2 # 角色
SCENE = 3 # 场景
PROP = 4 # 道具
RESOURCE = 5 # 通用资源
SOUND_EFFECT = 6 # 音效
VOICEOVER = 7 # 配音
```
**媒体类型(MediaType)**:
```python
class MediaType(IntEnum):
"""媒体类型"""
IMAGE = 1 # 图片
VIDEO = 2 # 视频
AUDIO = 3 # 音频
MODEL_3D = 4 # 3D模型
TEXT = 5 # 文本(剧本生成、提示词优化等)
```
#### 使用场景映射
| 场景 | target_type | target_id | tag_id | media_type | 说明 |
|------|-------------|-----------|--------|------------|------|
| 分镜001基础图片 | STORYBOARD (1) | 分镜001ID | NULL | IMAGE (1) | 生成基础分镜图片 |
| 分镜001正面角度 | STORYBOARD (1) | 分镜001ID | 正面TagID | IMAGE (1) | 生成正面角度图片 |
| 分镜001侧面角度 | STORYBOARD (1) | 分镜001ID | 侧面TagID | IMAGE (1) | 生成侧面角度图片 |
| 分镜001视频 | STORYBOARD (1) | 分镜001ID | NULL | VIDEO (2) | 生成分镜视频 |
| 角色-张三基础形象 | CHARACTER (2) | 张三ID | NULL | IMAGE (1) | 生成基础角色形象 |
| 角色-张三-少年装扮 | CHARACTER (2) | 张三ID | 少年TagID | IMAGE (1) | 生成少年装扮图片 |
| 角色-张三-成年装扮 | CHARACTER (2) | 张三ID | 成年TagID | IMAGE (1) | 生成成年装扮图片 |
| 角色-张三对话配音 | CHARACTER (2) | 张三ID | NULL | AUDIO (3) | 生成角色对话配音 |
| 场景-花果山基础 | SCENE (3) | 花果山ID | NULL | IMAGE (1) | 生成基础场景图片 |
| 场景-花果山-白天 | SCENE (3) | 花果山ID | 白天TagID | IMAGE (1) | 生成白天场景图片 |
| 场景-花果山-夜晚 | SCENE (3) | 花果山ID | 夜晚TagID | IMAGE (1) | 生成夜晚场景图片 |
| 道具-金箍棒基础 | PROP (4) | 金箍棒ID | NULL | IMAGE (1) | 生成基础道具图片 |
| 道具-金箍棒-新状态 | PROP (4) | 金箍棒ID | 新状态TagID | IMAGE (1) | 生成新状态道具图片 |
| 独立音效生成 | SOUND_EFFECT (6) | 音效ID | NULL | AUDIO (3) | 生成独立音效 |
| 独立配音生成 | VOICEOVER (7) | 配音ID | NULL | AUDIO (3) | 生成独立配音 |
#### 查询示例
**查询"分镜001"的所有对话(所有角度+所有媒体类型)**:
```sql
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 1 -- STORYBOARD
AND target_id = '019d1234-5678-7abc-def0-222222222222' -- 分镜001的ID
AND status = 1; -- ACTIVE
```
**查询"分镜001-正面角度"的"图片生成"对话**:
```sql
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 1 -- STORYBOARD
AND target_id = '019d1234-5678-7abc-def0-222222222222' -- 分镜001的ID
AND tag_id = '019d1234-5678-7abc-def0-333333333333' -- 正面角度TagID
AND media_type = 1 -- IMAGE
AND status = 1;
```
**查询"角色-张三"的所有对话(所有装扮+所有媒体类型)**:
```sql
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 2 -- CHARACTER
AND target_id = '019d1234-5678-7abc-def0-444444444444' -- 角色-张三的ID
AND status = 1;
```
**查询"角色-张三-少年装扮"的"图片生成"对话**:
```sql
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 2 -- CHARACTER
AND target_id = '019d1234-5678-7abc-def0-444444444444' -- 角色-张三的ID
AND tag_id = '019d1234-5678-7abc-def0-555555555555' -- 少年装扮TagID
AND media_type = 1 -- IMAGE
AND status = 1;
```
#### 唯一性约束
**规则**:同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话。
```sql
CREATE UNIQUE INDEX idx_ai_conversations_unique_active
ON ai_conversations (user_id, target_type, target_id, tag_id, media_type)
NULLS NOT DISTINCT
WHERE status = 1;
```
**示例**:
- ✅ 用户A可以同时有"分镜001-图片"和"分镜001-视频"两个活跃会话
- ✅ 用户A可以同时有"分镜001-正面角度-图片"和"分镜001-侧面角度-图片"两个活跃会话
- ✅ 用户A可以同时有"角色-张三-少年装扮-图片"和"角色-张三-成年装扮-图片"两个活跃会话
- ❌ 用户A不能同时有两个"分镜001-正面角度-图片"活跃会话(会自动归档旧会话)
**注意**:PostgreSQL 14+ 支持 `NULLS NOT DISTINCT`,使得 NULL 值被视为相同,确保唯一性约束正确工作。
#### 应用层引用完整性保证
**验证逻辑**:
```python
async def _validate_target(self, target_type: int, target_id: UUID) -> bool:
"""验证目标对象是否存在"""
if target_type == TargetType.STORYBOARD:
from app.repositories.storyboard_repository import StoryboardRepository
repo = StoryboardRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.CHARACTER:
from app.repositories.screenplay_character_repository import ScreenplayCharacterRepository
repo = ScreenplayCharacterRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.SCENE:
from app.repositories.screenplay_scene_repository import ScreenplaySceneRepository
repo = ScreenplaySceneRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.PROP:
from app.repositories.screenplay_prop_repository import ScreenplayPropRepository
repo = ScreenplayPropRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.RESOURCE:
from app.repositories.project_resource_repository import ProjectResourceRepository
repo = ProjectResourceRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.SOUND_EFFECT:
from app.repositories.sound_effect_repository import SoundEffectRepository
repo = SoundEffectRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.VOICEOVER:
from app.repositories.voiceover_repository import VoiceoverRepository
repo = VoiceoverRepository(self.db)
return await repo.exists(target_id)
else:
raise ValidationError(f"不支持的目标类型: {target_type}")
async def _validate_tag(self, tag_id: UUID, target_type: int, target_id: UUID) -> bool:
"""验证标签是否存在且属于该对象"""
from app.repositories.screenplay_tag_repository import ScreenplayTagRepository
repo = ScreenplayTagRepository(self.db)
tag = await repo.get_by_id(tag_id)
if not tag:
return False
# 验证标签是否属于该对象
if tag.element_id != target_id:
return False
# 验证标签的元素类型是否匹配
from app.models.screenplay_tag import ElementType
element_type_map = {
TargetType.STORYBOARD: ElementType.STORYBOARD,
TargetType.CHARACTER: ElementType.CHARACTER,
TargetType.SCENE: ElementType.SCENE,
TargetType.PROP: ElementType.PROP,
}
expected_element_type = element_type_map.get(target_type)
if tag.element_type != expected_element_type:
return False
return True
async def _validate_combination(self, target_type: int, media_type: int) -> bool:
"""验证 target_type 和 media_type 的组合是否有效"""
# 定义有效的组合
VALID_COMBINATIONS = {
TargetType.STORYBOARD: [MediaType.IMAGE, MediaType.VIDEO, MediaType.AUDIO],
TargetType.CHARACTER: [MediaType.IMAGE, MediaType.AUDIO],
TargetType.SCENE: [MediaType.IMAGE],
TargetType.PROP: [MediaType.IMAGE],
TargetType.RESOURCE: [MediaType.IMAGE, MediaType.VIDEO, MediaType.AUDIO],
TargetType.SOUND_EFFECT: [MediaType.AUDIO],
TargetType.VOICEOVER: [MediaType.AUDIO],
}
valid_media_types = VALID_COMBINATIONS.get(target_type, [])
if media_type not in valid_media_types:
raise ValidationError(
f"目标类型 {target_type} 不支持媒体类型 {media_type}"
)
return True
```
---
### 1. 对话会话管理
**功能**:
- 创建新的对话会话
- 查询对话会话列表
- 查询对话会话详情
- 删除对话会话
**特性**:
- 每个会话关联到项目或分镜
- 支持会话标题自动生成
- 支持会话归档和删除
### 2. 消息记录
**功能**:
- 记录用户发送的消息
- 记录 AI 的回复消息
- 支持消息编辑和删除
**消息类型**:
- `user`:用户消息
- `assistant`:AI 回复
- `system`:系统消息(如任务状态通知)
### 3. 上下文管理
**功能**:
- 维护对话上下文(最近 N 条消息)
- 自动截断过长的上下文
- 支持上下文重置
**上下文策略**:
- 默认保留最近 10 条消息
- 超过限制时自动截断最早的消息
- 用户可手动清空上下文
### 4. 任务关联
**功能**:
- 将对话消息与 AI 任务关联
- 支持从对话直接触发 AI 生成任务
- 任务完成后自动回复结果
**关联方式**:
- 对话消息 → AI 任务(一对一)
- 对话会话 → 多个 AI 任务(一对多)
### 5. 对话历史查询
**功能**:
- 按项目查询对话历史
- 按用户查询对话历史
- 按时间范围查询
- 按关键词搜索
---
## 数据库设计
### 3.1 ai_conversations(AI 对话会话表)
**核心表**,记录对话会话的基本信息。
```sql
-- Python 枚举定义(app/models/ai_conversation.py)
-- class ConversationStatus(IntEnum):
-- ACTIVE = 1 # 活跃
-- ARCHIVED = 2 # 已归档
-- DELETED = 3 # 已删除
-- class TargetType(IntEnum):
-- STORYBOARD = 1 # 分镜
-- CHARACTER = 2 # 角色
-- SCENE = 3 # 场景
-- PROP = 4 # 道具
-- RESOURCE = 5 # 通用资源
-- SOUND_EFFECT = 6 # 音效
-- VOICEOVER = 7 # 配音
-- class MediaType(IntEnum):
-- IMAGE = 1 # 图片
-- VIDEO = 2 # 视频
-- AUDIO = 3 # 音频
-- MODEL_3D = 4 # 3D模型
-- TEXT = 5 # 文本
CREATE TABLE ai_conversations (
conversation_id UUID PRIMARY KEY, -- 对话会话唯一标识
-- 关联信息(无外键约束,应用层保证引用完整性)
user_id UUID NOT NULL, -- 用户 ID
project_id UUID, -- 项目 ID(可选)
-- 多态关联(最终版)
target_type SMALLINT NOT NULL, -- 目标类型(1=分镜 2=角色 3=场景 4=道具 5=资源 6=音效 7=配音)
target_id UUID NOT NULL, -- 目标对象 ID
tag_id UUID, -- 标签 ID(可选,用于区分同一对象的不同变体)
media_type SMALLINT NOT NULL, -- 媒体类型(1=图片 2=视频 3=音频 4=3D模型 5=文本)
-- 会话信息
title TEXT, -- 会话标题(自动生成或用户自定义)
status SMALLINT NOT NULL DEFAULT 1, -- 会话状态(1=活跃 2=已归档 3=已删除)
-- 统计信息
message_count INTEGER NOT NULL DEFAULT 0, -- 消息数量
last_message_at TIMESTAMPTZ, -- 最后一条消息时间
-- 元数据
meta_data JSONB NOT NULL DEFAULT '{}', -- 额外元数据
-- 时间信息
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
-- 字段注释
COMMENT ON TABLE ai_conversations IS 'AI 对话会话表 - 多态关联 + 标签系统 - 应用层保证引用完整性';
COMMENT ON COLUMN ai_conversations.conversation_id IS '对话会话唯一标识';
COMMENT ON COLUMN ai_conversations.user_id IS '用户 ID - 应用层验证';
COMMENT ON COLUMN ai_conversations.project_id IS '项目 ID(可选)- 应用层验证';
COMMENT ON COLUMN ai_conversations.target_type IS '目标类型(1=分镜 2=角色 3=场景 4=道具 5=资源 6=音效 7=配音)';
COMMENT ON COLUMN ai_conversations.target_id IS '目标对象 ID - 应用层验证';
COMMENT ON COLUMN ai_conversations.tag_id IS '标签 ID(可选,用于区分同一对象的不同变体)- 应用层验证';
COMMENT ON COLUMN ai_conversations.media_type IS '媒体类型(1=图片 2=视频 3=音频 4=3D模型 5=文本)';
COMMENT ON COLUMN ai_conversations.title IS '会话标题';
COMMENT ON COLUMN ai_conversations.status IS '会话状态(1=活跃 2=已归档 3=已删除)';
COMMENT ON COLUMN ai_conversations.message_count IS '消息数量';
COMMENT ON COLUMN ai_conversations.last_message_at IS '最后一条消息时间';
COMMENT ON COLUMN ai_conversations.meta_data IS '额外元数据';
COMMENT ON COLUMN ai_conversations.created_at IS '创建时间';
COMMENT ON COLUMN ai_conversations.updated_at IS '更新时间';
-- 索引(优化后)
CREATE INDEX idx_ai_conversations_user_id ON ai_conversations (user_id);
CREATE INDEX idx_ai_conversations_project_id ON ai_conversations (project_id)
WHERE project_id IS NOT NULL;
CREATE INDEX idx_ai_conversations_target ON ai_conversations (target_type, target_id);
CREATE INDEX idx_ai_conversations_target_tag ON ai_conversations (target_type, target_id, tag_id);
CREATE INDEX idx_ai_conversations_tag_id ON ai_conversations (tag_id)
WHERE tag_id IS NOT NULL;
CREATE INDEX idx_ai_conversations_media_type ON ai_conversations (media_type);
CREATE INDEX idx_ai_conversations_status ON ai_conversations (status);
CREATE INDEX idx_ai_conversations_created_at ON ai_conversations (created_at);
CREATE INDEX idx_ai_conversations_last_message_at ON ai_conversations (last_message_at)
WHERE last_message_at IS NOT NULL;
CREATE INDEX idx_ai_conversations_user_status ON ai_conversations (user_id, status);
CREATE INDEX idx_ai_conversations_user_target_tag_media ON ai_conversations (user_id, target_type, target_id, tag_id, media_type);
CREATE INDEX idx_ai_conversations_meta_data_gin ON ai_conversations USING GIN (meta_data);
-- 唯一约束:同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话
-- 注意:PostgreSQL 14+ 支持 NULLS NOT DISTINCT,使得 NULL 值被视为相同
CREATE UNIQUE INDEX idx_ai_conversations_unique_active
ON ai_conversations (user_id, target_type, target_id, tag_id, media_type)
NULLS NOT DISTINCT
WHERE status = 1;
-- 触发器
CREATE TRIGGER update_ai_conversations_updated_at
BEFORE UPDATE ON ai_conversations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
```
### 3.2 ai_conversation_messages(AI 对话消息表)
**核心表**,记录对话中的每条消息。
```sql
-- Python 枚举定义(app/models/ai_conversation_message.py)
-- class MessageRole(IntEnum):
-- USER = 1 # 用户消息
-- ASSISTANT = 2 # AI 回复
-- SYSTEM = 3 # 系统消息
CREATE TABLE ai_conversation_messages (
message_id UUID PRIMARY KEY, -- 消息唯一标识
-- 关联信息(无外键约束,应用层保证引用完整性)
conversation_id UUID NOT NULL, -- 对话会话 ID
user_id UUID NOT NULL, -- 用户 ID
ai_job_id UUID, -- 关联的 AI 任务 ID(可选)
-- 消息信息
role SMALLINT NOT NULL, -- 消息角色(1=用户 2=AI 3=系统)
content TEXT NOT NULL, -- 消息内容
-- 消息元数据
meta_data JSONB NOT NULL DEFAULT '{}', -- 额外元数据(如生成参数、附件等)
-- 排序
order_index INTEGER NOT NULL, -- 消息顺序
-- 时间信息
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
-- 字段注释
COMMENT ON TABLE ai_conversation_messages IS 'AI 对话消息表 - 应用层保证引用完整性';
COMMENT ON COLUMN ai_conversation_messages.message_id IS '消息唯一标识';
COMMENT ON COLUMN ai_conversation_messages.conversation_id IS '对话会话 ID - 应用层验证';
COMMENT ON COLUMN ai_conversation_messages.user_id IS '用户 ID - 应用层验证';
COMMENT ON COLUMN ai_conversation_messages.ai_job_id IS '关联的 AI 任务 ID - 应用层验证';
COMMENT ON COLUMN ai_conversation_messages.role IS '消息角色(1=用户 2=AI 3=系统)';
COMMENT ON COLUMN ai_conversation_messages.content IS '消息内容';
COMMENT ON COLUMN ai_conversation_messages.meta_data IS '额外元数据';
COMMENT ON COLUMN ai_conversation_messages.order_index IS '消息顺序';
COMMENT ON COLUMN ai_conversation_messages.created_at IS '创建时间';
COMMENT ON COLUMN ai_conversation_messages.updated_at IS '更新时间';
-- 索引
CREATE INDEX idx_ai_conversation_messages_conversation_id ON ai_conversation_messages (conversation_id);
CREATE INDEX idx_ai_conversation_messages_user_id ON ai_conversation_messages (user_id);
CREATE INDEX idx_ai_conversation_messages_ai_job_id ON ai_conversation_messages (ai_job_id)
WHERE ai_job_id IS NOT NULL;
CREATE INDEX idx_ai_conversation_messages_role ON ai_conversation_messages (role);
CREATE INDEX idx_ai_conversation_messages_created_at ON ai_conversation_messages (created_at);
CREATE INDEX idx_ai_conversation_messages_conversation_order ON ai_conversation_messages (conversation_id, order_index);
CREATE INDEX idx_ai_conversation_messages_meta_data_gin ON ai_conversation_messages USING GIN (meta_data);
CREATE INDEX idx_ai_conversation_messages_content_gin ON ai_conversation_messages USING GIN (to_tsvector('simple', content));
-- 触发器
CREATE TRIGGER update_ai_conversation_messages_updated_at
BEFORE UPDATE ON ai_conversation_messages
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 自动更新会话的 message_count 和 last_message_at
CREATE OR REPLACE FUNCTION update_conversation_stats()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE ai_conversations
SET message_count = message_count + 1,
last_message_at = NEW.created_at,
updated_at = now()
WHERE conversation_id = NEW.conversation_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE ai_conversations
SET message_count = message_count - 1,
updated_at = now()
WHERE conversation_id = OLD.conversation_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_conversation_stats_trigger
AFTER INSERT OR DELETE ON ai_conversation_messages
FOR EACH ROW
EXECUTE FUNCTION update_conversation_stats();
```
### 3.3 数据表关系图
```
┌─────────────────────┐
│ users │
└──────┬──────────────┘
│
├──────────────────────────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ ai_conversations │ │ projects │
└──────┬──────────────┘ └─────────────────────┘
│ ▲
│ │
▼ │
┌─────────────────────┐ │
│ai_conversation_ │ │
│messages │──────────────────┘
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ ai_jobs │
└─────────────────────┘
```
---
## 服务实现
### AIConversationService 类
```python
# app/services/ai_conversation_service.py
from typing import Dict, Any, Optional, List
from uuid import UUID
from datetime import datetime, timezone
from sqlalchemy.orm import Session
from app.repositories.ai_conversation_repository import AIConversationRepository
from app.repositories.ai_conversation_message_repository import AIConversationMessageRepository
from app.services.ai_service import AIService
from app.core.exceptions import ValidationError, NotFoundError, PermissionDeniedError
from app.core.logging import get_logger
logger = get_logger(__name__)
class AIConversationService:
"""AI 对话记录服务"""
def __init__(self, db: Session):
self.db = db
self.conversation_repo = AIConversationRepository(db)
self.message_repo = AIConversationMessageRepository(db)
self.ai_service = AIService(db)
# ==================== 对话会话管理 ====================
async def create_conversation(
self,
user_id: UUID,
target_type: int,
target_id: UUID,
media_type: int,
project_id: Optional[UUID] = None,
tag_id: Optional[UUID] = None,
title: Optional[str] = None
) -> Dict[str, Any]:
"""创建新的对话会话
Args:
user_id: 用户 ID
target_type: 目标类型(TargetType 枚举值)
target_id: 目标对象 ID
media_type: 媒体类型(MediaType 枚举值)
project_id: 项目 ID(可选)
tag_id: 标签 ID(可选,用于区分变体)
title: 会话标题(可选,默认自动生成)
"""
logger.info(
"创建对话会话: user_id=%s, target_type=%d, target_id=%s, tag_id=%s, media_type=%d",
user_id, target_type, target_id, tag_id, media_type
)
# 验证用户是否存在
from app.repositories.user_repository import UserRepository
user_repo = UserRepository(self.db)
if not await user_repo.exists(user_id):
raise ValidationError("用户不存在")
# 验证目标对象是否存在(应用层引用完整性保证)
if not await self._validate_target(target_type, target_id):
raise ValidationError("目标对象不存在")
# 验证标签是否存在(如果提供了)
if tag_id:
if not await self._validate_tag(tag_id, target_type, target_id):
raise ValidationError("标签不存在或不属于该对象")
# 验证组合是否有效
await self._validate_combination(target_type, media_type)
# 如果指定了项目,验证项目是否存在
if project_id:
from app.repositories.project_repository import ProjectRepository
project_repo = ProjectRepository(self.db)
if not await project_repo.exists(project_id):
raise ValidationError("项目不存在")
# 检查是否已存在活跃会话
existing = await self.conversation_repo.get_active_conversation(
user_id=user_id,
target_type=target_type,
target_id=target_id,
tag_id=tag_id,
media_type=media_type
)
if existing:
# 如果已存在活跃会话,直接返回
logger.info(
"已存在活跃会话: conversation_id=%s",
existing.conversation_id
)
return {
'conversation_id': str(existing.conversation_id),
'title': existing.title,
'status': existing.status,
'message_count': existing.message_count,
'created_at': existing.created_at.isoformat()
}
# 生成默认标题
if not title:
title = await self._generate_title(target_type, target_id, tag_id, media_type)
# 创建对话会话
from app.models.ai_conversation import ConversationStatus
try:
conversation = await self.conversation_repo.create({
'user_id': user_id,
'project_id': project_id,
'target_type': target_type,
'target_id': target_id,
'tag_id': tag_id,
'media_type': media_type,
'title': title,
'status': ConversationStatus.ACTIVE
})
logger.info(
"对话会话创建成功: conversation_id=%s",
conversation.conversation_id
)
except Exception as e:
logger.error(
"创建对话会话失败: user_id=%s, target_type=%d, 错误=%s",
user_id, target_type, str(e),
exc_info=True
)
raise
return {
'conversation_id': str(conversation.conversation_id),
'title': conversation.title,
'target_type': conversation.target_type,
'target_id': str(conversation.target_id),
'tag_id': str(conversation.tag_id) if conversation.tag_id else None,
'media_type': conversation.media_type,
'status': conversation.status,
'message_count': conversation.message_count,
'created_at': conversation.created_at.isoformat()
}
async def get_conversation(
self,
conversation_id: UUID,
user_id: UUID
) -> Dict[str, Any]:
"""获取对话会话详情
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID(用于权限验证)
"""
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限访问此对话")
# 获取标签名称(如果有)
tag_label = await self._get_tag_name(conversation.tag_id) if conversation.tag_id else None
return {
'conversation_id': str(conversation.conversation_id),
'user_id': str(conversation.user_id),
'project_id': str(conversation.project_id) if conversation.project_id else None,
'target_type': conversation.target_type,
'target_id': str(conversation.target_id),
'tag_id': str(conversation.tag_id) if conversation.tag_id else None,
'tag_label': tag_label,
'media_type': conversation.media_type,
'title': conversation.title,
'status': conversation.status,
'message_count': conversation.message_count,
'last_message_at': conversation.last_message_at.isoformat() if conversation.last_message_at else None,
'created_at': conversation.created_at.isoformat(),
'updated_at': conversation.updated_at.isoformat()
}
async def list_conversations(
self,
user_id: UUID,
project_id: Optional[UUID] = None,
target_type: Optional[int] = None,
target_id: Optional[UUID] = None,
tag_id: Optional[UUID] = None,
media_type: Optional[int] = None,
status: Optional[int] = None,
page: int = 1,
page_size: int = 20
) -> Dict[str, Any]:
"""获取对话会话列表
Args:
user_id: 用户 ID
project_id: 项目 ID(可选,筛选条件)
target_type: 目标类型(可选,筛选条件)
target_id: 目标对象 ID(可选,筛选条件)
tag_id: 标签 ID(可选,筛选条件)
media_type: 媒体类型(可选,筛选条件)
status: 会话状态(可选,筛选条件)
page: 页码
page_size: 每页数量
"""
conversations, total = await self.conversation_repo.list_by_user(
user_id=user_id,
project_id=project_id,
target_type=target_type,
target_id=target_id,
tag_id=tag_id,
media_type=media_type,
status=status,
page=page,
page_size=page_size
)
# 批量获取标签名称
tag_ids = [c.tag_id for c in conversations if c.tag_id]
tag_labels = {}
if tag_ids:
from app.repositories.screenplay_tag_repository import ScreenplayTagRepository
tag_repo = ScreenplayTagRepository(self.db)
tags = await tag_repo.get_by_ids(tag_ids)
tag_labels = {tag.tag_id: tag.tag_label for tag in tags}
return {
'items': [
{
'conversation_id': str(c.conversation_id),
'title': c.title,
'target_type': c.target_type,
'target_id': str(c.target_id),
'tag_id': str(c.tag_id) if c.tag_id else None,
'tag_label': tag_labels.get(c.tag_id) if c.tag_id else None,
'media_type': c.media_type,
'status': c.status,
'message_count': c.message_count,
'last_message_at': c.last_message_at.isoformat() if c.last_message_at else None,
'created_at': c.created_at.isoformat()
}
for c in conversations
],
'total': total,
'page': page,
'page_size': page_size,
'total_pages': (total + page_size - 1) // page_size
}
async def update_conversation(
self,
conversation_id: UUID,
user_id: UUID,
title: Optional[str] = None,
status: Optional[int] = None
) -> Dict[str, Any]:
"""更新对话会话
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID(用于权限验证)
title: 新标题(可选)
status: 新状态(可选)
"""
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限修改此对话")
# 更新字段
update_data = {}
if title is not None:
update_data['title'] = title
if status is not None:
update_data['status'] = status
if update_data:
conversation = await self.conversation_repo.update(conversation_id, update_data)
return {
'conversation_id': str(conversation.conversation_id),
'title': conversation.title,
'status': conversation.status
}
async def delete_conversation(
self,
conversation_id: UUID,
user_id: UUID
) -> None:
"""删除对话会话(软删除)
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID(用于权限验证)
"""
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限删除此对话")
# 软删除
from app.models.ai_conversation import ConversationStatus
await self.conversation_repo.update(conversation_id, {
'status': ConversationStatus.DELETED
})
logger.info("对话会话已删除: conversation_id=%s", conversation_id)
# ==================== 消息管理 ====================
async def send_message(
self,
conversation_id: UUID,
user_id: UUID,
content: str,
meta_data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""发送用户消息
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID
content: 消息内容
meta_data: 额外元数据(可选)
"""
logger.info(
"发送消息: conversation_id=%s, user_id=%s",
conversation_id, user_id
)
# 验证对话会话是否存在
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限在此对话中发送消息")
# 获取当前消息数量(用于 order_index)
order_index = conversation.message_count
# 创建用户消息
from app.models.ai_conversation_message import MessageRole
try:
message = await self.message_repo.create({
'conversation_id': conversation_id,
'user_id': user_id,
'role': MessageRole.USER,
'content': content,
'meta_data': meta_data or {},
'order_index': order_index
})
logger.info(
"用户消息已创建: message_id=%s",
message.message_id
)
except Exception as e:
logger.error(
"创建用户消息失败: conversation_id=%s, 错误=%s",
conversation_id, str(e),
exc_info=True
)
raise
return {
'message_id': str(message.message_id),
'role': 'user',
'content': message.content,
'created_at': message.created_at.isoformat()
}
async def get_ai_response(
self,
conversation_id: UUID,
user_id: UUID,
user_message_id: UUID
) -> Dict[str, Any]:
"""获取 AI 回复(调用 AI 模型)
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID
user_message_id: 用户消息 ID
"""
logger.info(
"获取 AI 回复: conversation_id=%s, user_message_id=%s",
conversation_id, user_message_id
)
# 验证对话会话
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限访问此对话")
# 获取对话上下文(最近 10 条消息)
messages = await self.message_repo.get_recent_messages(
conversation_id=conversation_id,
limit=10
)
# 构建上下文
context = [
{
'role': 'user' if m.role == 1 else 'assistant',
'content': m.content
}
for m in messages
]
# 调用 AI 模型
try:
ai_response = await self._call_ai_model(context)
except Exception as e:
logger.error(
"调用 AI 模型失败: conversation_id=%s, 错误=%s",
conversation_id, str(e),
exc_info=True
)
raise
# 保存 AI 回复
from app.models.ai_conversation_message import MessageRole
order_index = conversation.message_count
try:
ai_message = await self.message_repo.create({
'conversation_id': conversation_id,
'user_id': user_id,
'role': MessageRole.ASSISTANT,
'content': ai_response['content'],
'meta_data': ai_response.get('meta_data', {}),
'order_index': order_index
})
logger.info(
"AI 回复已创建: message_id=%s",
ai_message.message_id
)
except Exception as e:
logger.error(
"创建 AI 回复失败: conversation_id=%s, 错误=%s",
conversation_id, str(e),
exc_info=True
)
raise
return {
'message_id': str(ai_message.message_id),
'role': 'assistant',
'content': ai_message.content,
'created_at': ai_message.created_at.isoformat()
}
async def list_messages(
self,
conversation_id: UUID,
user_id: UUID,
page: int = 1,
page_size: int = 50
) -> Dict[str, Any]:
"""获取对话消息列表
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID(用于权限验证)
page: 页码
page_size: 每页数量
"""
# 验证对话会话
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限访问此对话")
# 获取消息列表
messages, total = await self.message_repo.list_by_conversation(
conversation_id=conversation_id,
page=page,
page_size=page_size
)
return {
'items': [
{
'message_id': str(m.message_id),
'role': 'user' if m.role == 1 else 'assistant' if m.role == 2 else 'system',
'content': m.content,
'meta_data': m.meta_data,
'ai_job_id': str(m.ai_job_id) if m.ai_job_id else None,
'created_at': m.created_at.isoformat()
}
for m in messages
],
'total': total,
'page': page,
'page_size': page_size,
'total_pages': (total + page_size - 1) // page_size
}
# ==================== AI 任务关联 ====================
async def trigger_ai_generation(
self,
conversation_id: UUID,
user_id: UUID,
message_id: UUID,
generation_type: str,
params: Dict[str, Any]
) -> Dict[str, Any]:
"""从对话触发 AI 生成任务
Args:
conversation_id: 对话会话 ID
user_id: 用户 ID
message_id: 消息 ID(用于关联)
generation_type: 生成类型(image/video)
params: 生成参数
"""
logger.info(
"触发 AI 生成: conversation_id=%s, type=%s",
conversation_id, generation_type
)
# 验证对话会话
conversation = await self.conversation_repo.get_by_id(conversation_id)
if not conversation:
raise NotFoundError("对话会话不存在")
# 验证权限
if conversation.user_id != user_id:
raise PermissionDeniedError("没有权限在此对话中触发 AI 生成")
# 调用 AI Service 创建任务
try:
if generation_type == 'image':
result = await self.ai_service.generate_image(
user_id=user_id,
prompt=params.get('prompt'),
model=params.get('model'),
width=params.get('width', 1024),
height=params.get('height', 1024),
style=params.get('style')
)
elif generation_type == 'video':
result = await self.ai_service.generate_video(
user_id=user_id,
video_type=params.get('video_type'),
prompt=params.get('prompt'),
image_url=params.get('image_url'),
duration=params.get('duration', 5),
fps=params.get('fps', 30),
model=params.get('model')
)
else:
raise ValidationError(f"不支持的生成类型: {generation_type}")
except Exception as e:
logger.error(
"创建 AI 生成任务失败: conversation_id=%s, type=%s, 错误=%s",
conversation_id, generation_type, str(e),
exc_info=True
)
raise
# 更新消息,关联 AI 任务
await self.message_repo.update(message_id, {
'ai_job_id': result['job_id']
})
# 创建系统消息通知任务已创建
from app.models.ai_conversation_message import MessageRole
order_index = conversation.message_count
await self.message_repo.create({
'conversation_id': conversation_id,
'user_id': user_id,
'role': MessageRole.SYSTEM,
'content': f'AI {generation_type} 生成任务已创建,任务 ID: {result["job_id"]}',
'meta_data': {'job_id': result['job_id'], 'generation_type': generation_type},
'order_index': order_index,
'ai_job_id': result['job_id']
})
logger.info(
"AI 生成任务已创建: job_id=%s",
result['job_id']
)
return result
# ==================== 私有方法 ====================
async def _validate_target(self, target_type: int, target_id: UUID) -> bool:
"""验证目标对象是否存在(应用层引用完整性保证)
Args:
target_type: 目标类型(TargetType 枚举值)
target_id: 目标对象 ID
"""
from app.models.ai_conversation import TargetType
if target_type == TargetType.STORYBOARD:
from app.repositories.storyboard_repository import StoryboardRepository
repo = StoryboardRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.CHARACTER:
from app.repositories.screenplay_character_repository import ScreenplayCharacterRepository
repo = ScreenplayCharacterRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.SCENE:
from app.repositories.screenplay_scene_repository import ScreenplaySceneRepository
repo = ScreenplaySceneRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.PROP:
from app.repositories.screenplay_prop_repository import ScreenplayPropRepository
repo = ScreenplayPropRepository(self.db)
return await repo.exists(target_id)
elif target_type == TargetType.RESOURCE:
from app.repositories.project_resource_repository import ProjectResourceRepository
repo = ProjectResourceRepository(self.db)
return await repo.exists(target_id)
else:
raise ValidationError(f"不支持的目标类型: {target_type}")
async def _generate_title(
self,
target_type: int,
target_id: UUID,
tag_id: Optional[UUID],
media_type: int
) -> str:
"""自动生成对话标题
Args:
target_type: 目标类型
target_id: 目标对象 ID
tag_id: 标签 ID(可选)
media_type: 媒体类型
"""
from app.models.ai_conversation import TargetType, MediaType
# 获取目标对象名称
target_name = await self._get_target_name(target_type, target_id)
# 获取标签名称(如果有)
tag_name = await self._get_tag_name(tag_id) if tag_id else None
# 媒体类型名称映射
media_type_names = {
MediaType.IMAGE: "图片生成",
MediaType.VIDEO: "视频生成",
MediaType.AUDIO: "音频生成",
MediaType.MODEL_3D: "3D模型生成",
MediaType.TEXT: "文本生成"
}
media_name = media_type_names.get(media_type, "AI生成")
# 组合标题
if tag_name:
return f"{target_name}-{tag_name} - {media_name}"
else:
return f"{target_name} - {media_name}"
async def _get_target_name(self, target_type: int, target_id: UUID) -> str:
"""获取目标对象名称
Args:
target_type: 目标类型
target_id: 目标对象 ID
"""
from app.models.ai_conversation import TargetType
if target_type == TargetType.STORYBOARD:
from app.repositories.storyboard_repository import StoryboardRepository
repo = StoryboardRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.title if obj else "未知分镜"
elif target_type == TargetType.CHARACTER:
from app.repositories.screenplay_character_repository import ScreenplayCharacterRepository
repo = ScreenplayCharacterRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.name if obj else "未知角色"
elif target_type == TargetType.SCENE:
from app.repositories.screenplay_scene_repository import ScreenplaySceneRepository
repo = ScreenplaySceneRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.title if obj else "未知场景"
elif target_type == TargetType.PROP:
from app.repositories.screenplay_prop_repository import ScreenplayPropRepository
repo = ScreenplayPropRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.name if obj else "未知道具"
elif target_type == TargetType.RESOURCE:
from app.repositories.project_resource_repository import ProjectResourceRepository
repo = ProjectResourceRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.name if obj else "未知资源"
elif target_type == TargetType.SOUND_EFFECT:
from app.repositories.sound_effect_repository import SoundEffectRepository
repo = SoundEffectRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.name if obj else "未知音效"
elif target_type == TargetType.VOICEOVER:
from app.repositories.voiceover_repository import VoiceoverRepository
repo = VoiceoverRepository(self.db)
obj = await repo.get_by_id(target_id)
return obj.name if obj else "未知配音"
else:
return "未知对象"
async def _get_tag_name(self, tag_id: UUID) -> Optional[str]:
"""获取标签名称
Args:
tag_id: 标签 ID
"""
from app.repositories.screenplay_tag_repository import ScreenplayTagRepository
repo = ScreenplayTagRepository(self.db)
tag = await repo.get_by_id(tag_id)
return tag.tag_label if tag else None
async def _call_ai_model(self, context: List[Dict[str, str]]) -> Dict[str, Any]:
"""调用 AI 模型获取回复
Args:
context: 对话上下文
"""
# 这里调用 AI 模型(如 GPT-4、Claude 等)
# 实际实现需要根据具体的 AI 提供商进行调用
# 示例:调用 OpenAI API
import openai
response = await openai.ChatCompletion.acreate(
model="gpt-4",
messages=context,
temperature=0.7,
max_tokens=1000
)
return {
'content': response.choices[0].message.content,
'meta_data': {
'model': response.model,
'usage': {
'prompt_tokens': response.usage.prompt_tokens,
'completion_tokens': response.usage.completion_tokens,
'total_tokens': response.usage.total_tokens
}
}
}
```
---
## API 接口
### 1. 创建对话会话
```
POST /api/v1/ai/conversations
```
**请求体**:
```json
{
"projectId": "019d1234-5678-7abc-def0-111111111111",
"targetType": 1,
"targetId": "019d1234-5678-7abc-def0-222222222222",
"tagId": "019d1234-5678-7abc-def0-333333333333",
"mediaType": 1,
"title": "分镜001-正面角度 - 生成图片"
}
```
**字段说明**:
- `projectId`(可选):项目 ID
- `targetType`(必填):目标类型
- `1`:分镜(STORYBOARD)
- `2`:角色(CHARACTER)
- `3`:场景(SCENE)
- `4`:道具(PROP)
- `5`:资源(RESOURCE)
- `6`:音效(SOUND_EFFECT)
- `7`:配音(VOICEOVER)
- `targetId`(必填):目标对象 ID
- `tagId`(可选):标签 ID(用于区分变体,如角色装扮、分镜角度)
- `mediaType`(必填):媒体类型
- `1`:图片(IMAGE)
- `2`:视频(VIDEO)
- `3`:音频(AUDIO)
- `4`:3D模型(MODEL_3D)
- `5`:文本(TEXT)
- `title`(可选):会话标题(不提供则自动生成)
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"conversationId": "019d1234-5678-7abc-def0-444444444444",
"title": "分镜001-正面角度 - 图片生成",
"targetType": 1,
"targetId": "019d1234-5678-7abc-def0-222222222222",
"tagId": "019d1234-5678-7abc-def0-333333333333",
"mediaType": 1,
"status": 1,
"messageCount": 0,
"createdAt": "2026-01-30T10:00:00Z"
}
}
```
**使用示例**:
```javascript
// 前端:在分镜编辑页面,点击"AI生成图片"按钮(基础图片,无标签)
const createStoryboardConversation = async (storyboardId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 1, // STORYBOARD
targetId: storyboardId,
tagId: null, // 无标签(基础图片)
mediaType: 1 // IMAGE
})
});
return response.json();
};
// 前端:在分镜编辑页面,选择"正面角度"后点击"AI生成图片"
const createStoryboardAngleConversation = async (storyboardId, angleTagId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 1, // STORYBOARD
targetId: storyboardId,
tagId: angleTagId, // 正面角度标签ID
mediaType: 1 // IMAGE
})
});
return response.json();
};
// 前端:在角色管理页面,选择"少年装扮"后点击"AI生成图片"
const createCharacterConversation = async (characterId, costumeTagId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 2, // CHARACTER
targetId: characterId,
tagId: costumeTagId, // 少年装扮标签ID
mediaType: 1 // IMAGE
})
});
return response.json();
};
// 前端:在音效库页面,点击"AI生成音效"
const createSoundEffectConversation = async (soundEffectId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetType: 6, // SOUND_EFFECT
targetId: soundEffectId,
tagId: null, // 音效通常无标签
mediaType: 3 // AUDIO
})
});
return response.json();
};
```
### 2. 获取对话会话列表
```
GET /api/v1/ai/conversations
```
**查询参数**:
- `projectId`(可选):项目 ID
- `targetType`(可选):目标类型(1-7)
- `targetId`(可选):目标对象 ID
- `tagId`(可选):标签 ID
- `mediaType`(可选):媒体类型(1-5)
- `status`(可选):会话状态(1=活跃 2=已归档 3=已删除)
- `page`(可选):页码(默认 1)
- `pageSize`(可选):每页数量(默认 20)
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"items": [
{
"conversationId": "019d1234-5678-7abc-def0-444444444444",
"title": "分镜001-正面角度 - 图片生成",
"targetType": 1,
"targetId": "019d1234-5678-7abc-def0-222222222222",
"tagId": "019d1234-5678-7abc-def0-333333333333",
"tagLabel": "正面角度",
"mediaType": 1,
"status": 1,
"messageCount": 5,
"lastMessageAt": "2026-01-30T10:05:00Z",
"createdAt": "2026-01-30T10:00:00Z"
},
{
"conversationId": "019d1234-5678-7abc-def0-555555555555",
"title": "分镜001-侧面角度 - 图片生成",
"targetType": 1,
"targetId": "019d1234-5678-7abc-def0-222222222222",
"tagId": "019d1234-5678-7abc-def0-666666666666",
"tagLabel": "侧面角度",
"mediaType": 1,
"status": 1,
"messageCount": 3,
"lastMessageAt": "2026-01-30T10:10:00Z",
"createdAt": "2026-01-30T10:08:00Z"
}
],
"total": 2,
"page": 1,
"pageSize": 20,
"totalPages": 1
}
}
```
**使用示例**:
```javascript
// 前端:查询"分镜001"的所有对话(所有角度+所有媒体类型)
const getStoryboardConversations = async (storyboardId) => {
const response = await fetch(
`/api/v1/ai/conversations?targetType=1&targetId=${storyboardId}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
return response.json();
};
// 前端:查询"分镜001-正面角度"的"图片生成"对话
const getStoryboardAngleImageConversation = async (storyboardId, angleTagId) => {
const response = await fetch(
`/api/v1/ai/conversations?targetType=1&targetId=${storyboardId}&tagId=${angleTagId}&mediaType=1`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
return response.json();
};
// 前端:查询"角色-张三"的所有对话(所有装扮+所有媒体类型)
const getCharacterConversations = async (characterId) => {
const response = await fetch(
`/api/v1/ai/conversations?targetType=2&targetId=${characterId}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
return response.json();
};
// 前端:查询"角色-张三-少年装扮"的"图片生成"对话
const getCharacterCostumeImageConversation = async (characterId, costumeTagId) => {
const response = await fetch(
`/api/v1/ai/conversations?targetType=2&targetId=${characterId}&tagId=${costumeTagId}&mediaType=1`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
return response.json();
};
```
### 3. 发送消息
```
POST /api/v1/ai/conversations/{conversation_id}/messages
```
**请求体**:
```json
{
"content": "帮我生成一张咖啡厅的图片,风格要温馨浪漫"
}
```
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"messageId": "019d1234-5678-7abc-def0-444444444444",
"role": "user",
"content": "帮我生成一张咖啡厅的图片,风格要温馨浪漫",
"createdAt": "2026-01-30T10:01:00Z"
}
}
```
### 4. 获取 AI 回复
```
POST /api/v1/ai/conversations/{conversation_id}/messages/{message_id}/reply
```
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"messageId": "019d1234-5678-7abc-def0-555555555555",
"role": "assistant",
"content": "好的,我理解您的需求。您想要生成一张温馨浪漫的咖啡厅图片。我建议使用以下参数:\n\n- 风格:realistic\n- 尺寸:1024x1024\n- 提示词:A cozy and romantic coffee shop with warm lighting, wooden furniture, and large windows letting in natural sunlight\n\n是否需要我帮您生成这张图片?",
"createdAt": "2026-01-30T10:01:05Z"
}
}
```
### 5. 触发 AI 生成任务
```
POST /api/v1/ai/conversations/{conversation_id}/generate
```
**请求体**:
```json
{
"messageId": "019d1234-5678-7abc-def0-555555555555",
"generationType": "image",
"params": {
"prompt": "A cozy and romantic coffee shop with warm lighting",
"model": "stable_diffusion",
"width": 1024,
"height": 1024,
"style": "realistic"
}
}
```
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"jobId": "019d1234-5678-7abc-def0-666666666666",
"taskId": "abc-123-def",
"status": "pending",
"estimatedCredits": 10
}
}
```
### 6. 获取对话消息列表
```
GET /api/v1/ai/conversations/{conversation_id}/messages
```
**查询参数**:
- `page`(可选):页码(默认 1)
- `pageSize`(可选):每页数量(默认 50)
**响应**:
```json
{
"code": 200,
"message": "Success",
"data": {
"items": [
{
"messageId": "019d1234-5678-7abc-def0-444444444444",
"role": "user",
"content": "帮我生成一张咖啡厅的图片",
"meta_data": {},
"aiJobId": null,
"feedback": null,
"createdAt": "2026-01-30T10:01:00Z"
},
{
"messageId": "019d1234-5678-7abc-def0-555555555555",
"role": "assistant",
"content": "好的,我理解您的需求...",
"meta_data": {},
"aiJobId": null,
"feedback": 1,
"createdAt": "2026-01-30T10:01:05Z"
}
],
"total": 5,
"page": 1,
"pageSize": 50,
"totalPages": 1
}
}
```
---
## 业务流程时序图
### 6.1 创建对话并发送消息流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant A as API 服务
participant CS as ConversationService
participant DB as 数据库
Note over U,DB: 阶段1:创建对话会话
U->>F: 点击"AI 对话"按钮(选择角度/装扮)
F->>A: POST /api/v1/ai/conversations
{targetType, targetId, tagId, mediaType}
A->>CS: create_conversation()
CS->>DB: 验证用户/项目/目标对象
CS->>DB: 验证标签(如果提供)
CS->>DB: 验证组合有效性
CS->>DB: 检查是否已存在活跃会话
alt 已存在活跃会话
CS-->>A: 返回现有会话
else 不存在活跃会话
CS->>DB: 创建对话会话记录
DB-->>CS: 返回 conversation_id
end
CS-->>A: 返回对话信息
A-->>F: 返回成功
F->>F: 打开对话窗口
F-->>U: 显示对话界面
Note over U,DB: 阶段2:发送用户消息
U->>F: 输入消息并发送
F->>A: POST /api/v1/ai/conversations/{id}/messages
A->>CS: send_message()
CS->>DB: 验证对话会话和权限
CS->>DB: 创建用户消息记录
CS->>DB: 更新会话统计信息
DB-->>CS: 返回 message_id
CS-->>A: 返回消息信息
A-->>F: 返回成功
F->>F: 显示用户消息
F-->>U: 消息已发送
Note over U,DB: 阶段3:获取 AI 回复
F->>A: POST /api/v1/ai/conversations/{id}/messages/{msgId}/reply
A->>CS: get_ai_response()
CS->>DB: 获取对话上下文(最近10条消息)
CS->>CS: 构建上下文数组
CS->>CS: 调用 AI 模型(GPT-4/Claude)
Note over CS: AI 模型分析上下文
生成回复
CS->>DB: 创建 AI 回复消息记录
CS->>DB: 更新会话统计信息
DB-->>CS: 返回 message_id
CS-->>A: 返回 AI 回复
A-->>F: 返回成功
F->>F: 显示 AI 回复
F-->>U: AI 回复已生成
```
### 6.2 从对话触发 AI 生成任务流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant A as API 服务
participant CS as ConversationService
participant AS as AIService
participant DB as 数据库
participant Q as Celery 队列
participant W as Worker
participant AI as AI 提供商
participant S3 as 对象存储
Note over U,S3: 阶段1:用户确认生成参数
U->>F: 查看 AI 建议的参数
U->>F: 点击"生成图片"按钮
F->>A: POST /api/v1/ai/conversations/{id}/generate
A->>CS: trigger_ai_generation()
CS->>DB: 验证对话会话和权限
Note over U,S3: 阶段2:创建 AI 任务
CS->>AS: generate_image()
AS->>DB: 检查用户积分
AS->>DB: 创建 AI 任务记录
AS->>DB: 扣除积分
AS->>Q: 提交异步任务
AS-->>CS: 返回 job_id
Note over U,S3: 阶段3:更新对话记录
CS->>DB: 更新消息,关联 ai_job_id
CS->>DB: 创建系统消息(任务已创建)
CS-->>A: 返回任务信息
A-->>F: 返回成功
F->>F: 显示系统消息
F-->>U: 任务已创建,生成中...
Note over U,S3: 阶段4:异步生成图片
Q->>W: 分配任务
W->>AI: 调用 AI API 生成图片
AI-->>W: 返回生成的图片
W->>S3: 上传图片到对象存储
S3-->>W: 返回图片 URL
W->>DB: 更新 AI 任务状态(completed)
W->>DB: 保存图片 URL
Note over U,S3: 阶段5:通知用户任务完成
W->>DB: 查询关联的对话会话
W->>DB: 创建系统消息(任务已完成)
Note over U,S3: 阶段6:用户查看结果
loop 轮询任务状态
F->>A: GET /api/v1/ai/jobs/{jobId}
A->>DB: 查询任务状态
alt 任务完成
A-->>F: 返回完成状态 + 图片 URL
F->>F: 显示系统消息
F->>F: 显示生成的图片
F-->>U: 图片生成完成
else 任务进行中
A-->>F: 返回进行中状态
end
end
```
### 6.3 多轮对话优化提示词流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant A as API 服务
participant CS as ConversationService
participant DB as 数据库
Note over U,DB: 第1轮对话
U->>F: "帮我生成一张咖啡厅的图片"
F->>A: POST /api/v1/ai/conversations/{id}/messages
A->>CS: send_message()
CS->>DB: 创建用户消息
CS-->>A: 返回成功
A-->>F: 返回消息
F->>A: POST /api/v1/ai/conversations/{id}/messages/{msgId}/reply
A->>CS: get_ai_response()
CS->>DB: 获取上下文(1条消息)
CS->>CS: 调用 AI 模型
Note over CS: AI: "您想要什么风格的咖啡厅?
现代简约、复古怀旧还是温馨浪漫?"
CS->>DB: 创建 AI 回复消息
CS-->>A: 返回 AI 回复
A-->>F: 返回成功
F-->>U: 显示 AI 回复
Note over U,DB: 第2轮对话
U->>F: "温馨浪漫的风格"
F->>A: POST /api/v1/ai/conversations/{id}/messages
A->>CS: send_message()
CS->>DB: 创建用户消息
CS-->>A: 返回成功
F->>A: POST /api/v1/ai/conversations/{id}/messages/{msgId}/reply
A->>CS: get_ai_response()
CS->>DB: 获取上下文(3条消息)
CS->>CS: 调用 AI 模型
Note over CS: AI: "好的,温馨浪漫风格。
建议参数:
- 暖色调灯光
- 木质家具
- 大窗户自然光
是否需要我生成?"
CS->>DB: 创建 AI 回复消息
CS-->>A: 返回 AI 回复
A-->>F: 返回成功
F-->>U: 显示 AI 回复和"生成"按钮
Note over U,DB: 第3轮对话 - 触发生成
U->>F: 点击"生成"按钮
F->>A: POST /api/v1/ai/conversations/{id}/generate
A->>CS: trigger_ai_generation()
Note over CS: 使用对话上下文中的参数
创建 AI 生成任务
CS-->>A: 返回任务信息
A-->>F: 返回成功
F-->>U: 任务已创建,生成中...
```
---
## 与 AI Service 集成
### 集成架构
```
AI Conversation Service (对话管理) → AI Service (任务执行)
```
### 数据关联
```sql
-- ai_conversation_messages 表关联 ai_jobs 表
ALTER TABLE ai_conversation_messages
ADD COLUMN ai_job_id UUID;
CREATE INDEX idx_ai_conversation_messages_ai_job_id
ON ai_conversation_messages (ai_job_id)
WHERE ai_job_id IS NOT NULL;
COMMENT ON COLUMN ai_conversation_messages.ai_job_id IS 'AI 任务 ID - 应用层验证';
```
### 集成流程
1. **用户发送消息** → AI Conversation Service 记录消息
2. **获取 AI 回复** → AI Conversation Service 调用 AI 模型
3. **触发 AI 生成** → AI Conversation Service 调用 AI Service
4. **任务完成通知** → AI Service 回调 AI Conversation Service
---
## 完整使用场景示例
### 场景1:分镜基础图片生成对话(无标签)
**用户操作流程**:
1. 用户在项目编辑页面,点击"分镜001"
2. 点击"AI生成图片"按钮(不选择角度)
3. 系统打开对话窗口
4. 用户输入:"帮我生成一张咖啡厅的图片"
5. AI 回复:"您想要什么风格?"
6. 用户回复:"温馨浪漫的风格"
7. AI 回复:"好的,建议参数:..."
8. 用户点击"生成"按钮
9. 系统创建 AI 任务,生成图片
**前端代码示例**:
```javascript
// 1. 打开对话窗口时,创建或获取对话会话(基础图片,无标签)
const openConversation = async (storyboardId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 1, // STORYBOARD
targetId: storyboardId,
tagId: null, // 无标签(基础图片)
mediaType: 1 // IMAGE
})
});
const { data } = await response.json();
return data.conversationId;
};
// 2. 发送用户消息
const sendMessage = async (conversationId, content) => {
const response = await fetch(`/api/v1/ai/conversations/${conversationId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
return response.json();
};
// 3. 获取 AI 回复
const getAIReply = async (conversationId, messageId) => {
const response = await fetch(
`/api/v1/ai/conversations/${conversationId}/messages/${messageId}/reply`,
{ method: 'POST' }
);
return response.json();
};
// 4. 触发 AI 生成
const triggerGeneration = async (conversationId, messageId, params) => {
const response = await fetch(`/api/v1/ai/conversations/${conversationId}/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messageId,
generationType: 'image',
params
})
});
return response.json();
};
```
### 场景2:分镜不同角度图片生成对话(有标签)
**用户操作流程**:
1. 用户在项目编辑页面,点击"分镜001"
2. 选择"正面角度"标签
3. 点击"AI生成图片"按钮
4. 系统打开对话窗口(只显示"分镜001-正面角度"的对话历史)
5. 用户输入:"生成一张正面角度的咖啡厅图片"
6. AI 回复:"好的,建议参数:..."
7. 用户点击"生成"按钮
**前端代码示例**:
```javascript
// 打开"分镜001-正面角度"的对话窗口
const openAngleConversation = async (storyboardId, angleTagId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 1, // STORYBOARD
targetId: storyboardId,
tagId: angleTagId, // 正面角度标签ID
mediaType: 1 // IMAGE
})
});
const { data } = await response.json();
return data.conversationId;
};
```
**数据隔离验证**:
```sql
-- 查询"分镜001-正面角度"的对话(只能看到正面角度的)
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 1 -- STORYBOARD
AND target_id = '019d1234-5678-7abc-def0-222222222222' -- 分镜001的ID
AND tag_id = '019d1234-5678-7abc-def0-333333333333' -- 正面角度TagID
AND media_type = 1 -- IMAGE
AND status = 1;
-- 结果:只返回"分镜001-正面角度"的对话,不会返回"分镜001-侧面角度"的对话
```
### 场景3:角色不同装扮图片生成对话
**用户操作流程**:
1. 用户在角色管理页面,点击"角色-张三"
2. 选择"少年装扮"标签
3. 点击"AI生成图片"按钮
4. 系统打开对话窗口(只显示"角色-张三-少年装扮"的对话历史)
5. 用户输入:"生成一张15岁的张三"
6. AI 回复:"好的,建议参数:..."
7. 用户点击"生成"按钮
**前端代码示例**:
```javascript
// 打开"角色-张三-少年装扮"的对话窗口
const openCharacterCostumeConversation = async (characterId, costumeTagId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: currentProjectId,
targetType: 2, // CHARACTER
targetId: characterId,
tagId: costumeTagId, // 少年装扮标签ID
mediaType: 1 // IMAGE
})
});
const { data } = await response.json();
return data.conversationId;
};
```
**数据隔离验证**:
```sql
-- 查询"角色-张三-少年装扮"的对话
SELECT * FROM ai_conversations
WHERE user_id = '019d1234-5678-7abc-def0-111111111111'
AND target_type = 2 -- CHARACTER
AND target_id = '019d1234-5678-7abc-def0-444444444444' -- 张三的ID
AND tag_id = '019d1234-5678-7abc-def0-555555555555' -- 少年装扮TagID
AND media_type = 1 -- IMAGE
AND status = 1;
-- 结果:只返回"角色-张三-少年装扮"的对话,不会返回"角色-张三-成年装扮"的对话
```
### 场景4:同一对象的不同媒体类型对话
**用户操作流程**:
1. 用户在"分镜001"生成图片(对话A)
2. 用户在"分镜001"生成视频(对话B)
3. 两个对话是独立的,互不干扰
**数据验证**:
```sql
-- 对话A:分镜001的图片生成对话
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-aaaaaaaaaaaa',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
NULL, -- 无标签
1, -- IMAGE
'分镜001 - 图片生成',
1
);
-- 对话B:分镜001的视频生成对话
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-bbbbbbbbbbbb',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
NULL, -- 无标签
2, -- VIDEO
'分镜001 - 视频生成',
1
);
-- ✅ 两条记录可以同时存在(media_type 不同)
```
### 场景5:同一对象的不同标签对话
**用户操作流程**:
1. 用户在"分镜001-正面角度"生成图片(对话A)
2. 用户在"分镜001-侧面角度"生成图片(对话B)
3. 两个对话是独立的,互不干扰
**数据验证**:
```sql
-- 对话A:分镜001-正面角度的图片生成对话
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-aaaaaaaaaaaa',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
'019d1234-5678-7abc-def0-333333333333', -- 正面角度TagID
1, -- IMAGE
'分镜001-正面角度 - 图片生成',
1
);
-- 对话B:分镜001-侧面角度的图片生成对话
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-bbbbbbbbbbbb',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
'019d1234-5678-7abc-def0-444444444444', -- 侧面角度TagID
1, -- IMAGE
'分镜001-侧面角度 - 图片生成',
1
);
-- ✅ 两条记录可以同时存在(tag_id 不同)
```
### 场景6:唯一性约束验证
**规则**:同一用户、同一目标、同一标签、同一媒体类型只能有一个活跃会话。
**测试用例**:
```sql
-- 用户A在"分镜001-正面角度"创建"图片生成"对话
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-aaaaaaaaaaaa',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
'019d1234-5678-7abc-def0-333333333333', -- 正面角度TagID
1, -- IMAGE
'分镜001-正面角度 - 图片生成',
1 -- ACTIVE
);
-- 用户A再次在"分镜001-正面角度"创建"图片生成"对话
-- ❌ 违反唯一约束,插入失败
INSERT INTO ai_conversations (
conversation_id, user_id, project_id,
target_type, target_id, tag_id, media_type,
title, status
) VALUES (
'019d1234-5678-7abc-def0-bbbbbbbbbbbb',
'019d1234-5678-7abc-def0-111111111111',
'019d1234-5678-7abc-def0-999999999999',
1, -- STORYBOARD
'019d1234-5678-7abc-def0-222222222222', -- 分镜001
'019d1234-5678-7abc-def0-333333333333', -- 正面角度TagID
1, -- IMAGE
'分镜001-正面角度 - 图片生成',
1 -- ACTIVE
);
-- ERROR: duplicate key value violates unique constraint "idx_ai_conversations_unique_active"
-- 解决方案:应用层先查询是否存在活跃会话,如果存在则直接返回
```
### 场景7:独立音效生成对话
**用户操作流程**:
1. 用户在音效库页面,点击"新建音效"
2. 点击"AI生成音效"按钮
3. 系统打开对话窗口
4. 用户输入:"生成一段雨声音效"
5. AI 回复:"好的,建议参数:时长5秒,音量适中"
6. 用户点击"生成"按钮
**前端代码示例**:
```javascript
// 打开音效生成对话窗口
const openSoundEffectConversation = async (soundEffectId) => {
const response = await fetch('/api/v1/ai/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
targetType: 6, // SOUND_EFFECT
targetId: soundEffectId,
tagId: null, // 音效通常无标签
mediaType: 3 // AUDIO
})
});
const { data } = await response.json();
return data.conversationId;
};
```
---
## 相关文档
- [AI 对话 @ 提及与参考图系统](./ai-conversation-mention-system.md) ⭐ **新增功能**
- [AI 生成服务](./ai-service.md)
- [数据库设计](../../database-design.md)
- [API 设计规范](../../api-design-specification.md)
---
**文档版本**:v2.1
**最后更新**:2026-01-30
**变更说明**:
- v2.1 (2026-01-30): 完善 API 接口、业务流程时序图、使用场景示例,移除消息反馈功能,新增 @ 提及系统文档链接
- v2.0 (2026-01-30): 重构为多态关联 + 标签系统设计,移除 `generation_type` 字段
- v1.0 (2026-01-29): 初始版本