# 自建情景功能 ## 概述 用户可以创建自己的情景,而不仅限于系统预置的 5 种情景。 **系统预置情景**(不可修改): - 💬 自由对话 - 🎯 模拟面试官 - 📚 英语老师 - ⚔️ 辩论对手 - 🌐 同声翻译 **用户自建情景**(可增删改): - 🎨 创意写作导师 - 🧘 心理咨询师 - 👨‍🍳 私人厨师 - 📖 历史学家 - ... (用户自由创建) **用户旅程**: ``` 1. 用户点击"创建情景"按钮 ↓ 2. 弹出创建对话框 ↓ 3. 填写表单: - 情景名称(必填) - 情景图标(可选) - 简短描述(可选) - 角色 Prompt(必填,最少 10 字) - 首句引导(可选) ↓ 4. 点击"创建" ↓ 5. 情景保存到数据库 ↓ 6. 情景出现在选择列表中 ↓ 7. 用户切换到自建情景 ↓ 8. AI 按照用户设定的 Prompt 扮演角色 ``` **核心特性**:完整 CRUD 操作(创建/查看/编辑/删除),通过 `user_id` 实现用户数据完全隔离,Eino Graph 管线深度集成(动态加载自建情景 Prompt),中文/英文/日文全覆盖,Modal 对话框 + 图标选择器 + Prompt 编写指南,创建后立即可用无需刷新。 ## 技术架构 ### 数据流 **创建情景**: ``` 用户填写表单 → POST /api/scenarios → Handler 验证 → Repository.Create → PostgreSQL 插入 → 返回情景对象 ``` **AI 对话使用自建情景**: ``` WebSocket 连接 → ServeWS 获取 userID → Eino Graph 初始化 → nodes_history 查询 user_scenarios → GetScenarioPrompt(customScenarios) → 构建 System Prompt → LLM 生成回复 ``` ### Eino 框架集成 **数据传递链路**: ``` JWT Token → userID ↓ Session.UserID ↓ PipelineInput.UserID ↓ PipelineState.UserID ↓ nodes_history.go: scenarioRepo.FindByUserID(userID) ↓ 构建 customScenarios map[string]string ↓ llm.GetScenarioPrompt(scenarioID, language, customScenarios) ↓ LLM 使用自建情景 Prompt ``` **关键修改文件**: | 文件 | 变更说明 | |------|----------| | `backend/internal/eino/state.go` | PipelineState 添加 `UserID` | | `backend/internal/eino/types.go` | PipelineInput 添加 `UserID` | | `backend/internal/eino/graph.go` | 接受 `scenarioRepo` 参数 | | `backend/internal/eino/adapter.go` | 设置 UserID | | `backend/internal/eino/nodes_history.go` | 查询自建情景 | | `backend/internal/ws/handler.go` | 首句引导支持自建情景 | ## 数据模型 ### 数据库表结构 **表名**: `user_scenarios` ```sql CREATE TABLE user_scenarios ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(50) NOT NULL, icon VARCHAR(10) DEFAULT '✨', description VARCHAR(100), -- 可选 prompt TEXT NOT NULL, greeting VARCHAR(500), -- 可选 language VARCHAR(10) DEFAULT 'zh-CN', created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW(), CONSTRAINT unique_user_scenario UNIQUE(user_id, name), CONSTRAINT check_name_length CHECK (char_length(name) >= 2 AND char_length(name) <= 50), CONSTRAINT check_description_length CHECK (description IS NULL OR char_length(description) <= 100), CONSTRAINT check_prompt_length CHECK (char_length(prompt) >= 10 AND char_length(prompt) <= 2000), CONSTRAINT check_greeting_length CHECK (greeting IS NULL OR char_length(greeting) <= 500) ); CREATE INDEX idx_user_scenarios_user_id ON user_scenarios(user_id); CREATE INDEX idx_user_scenarios_created_at ON user_scenarios(created_at DESC); ``` **字段说明**: | 字段 | 说明 | |------|------| | `id` | 情景唯一标识 | | `user_id` | 所属用户,实现数据隔离 | | `name` | 情景名称(2-50 字符) | | `icon` | Emoji 图标(默认 ✨) | | `description` | 简短描述(可选,最多 100 字符) | | `prompt` | 角色 System Prompt(10-2000 字符) | | `greeting` | 首句引导(可选,最多 500 字符) | | `language` | 默认语言(zh-CN / en-US / ja-JP) | ### 后端数据模型 ```go // backend/internal/models/user_scenario.go type UserScenario struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` Icon string `json:"icon"` Description string `json:"description"` Prompt string `json:"prompt"` Greeting string `json:"greeting,omitempty"` Language string `json:"language"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type CreateUserScenarioRequest struct { Name string `json:"name" binding:"required,min=2,max=50"` Icon string `json:"icon,omitempty"` Description string `json:"description,omitempty" binding:"omitempty,max=100"` Prompt string `json:"prompt" binding:"required,min=10,max=2000"` Greeting string `json:"greeting,omitempty" binding:"omitempty,max=500"` Language string `json:"language,omitempty"` } ``` ### 前端数据结构 ```typescript // frontend/src/lib/api/scenarios.ts export interface UserScenario { id: string; user_id: string; name: string; icon: string; description: string; prompt: string; greeting?: string; language: string; created_at: string; updated_at: string; } // frontend/src/hooks/useScenarios.ts export interface ExtendedScenario { id: string; icon: string; name: string; nameKey?: string; description?: string; descKey?: string; isCustom: boolean; prompt?: string; greeting?: string; language?: string; } ``` ## REST API ### API 端点 | 方法 | 路径 | 说明 | 权限 | |------|------|------|------| | GET | `/api/scenarios` | 获取用户的所有自建情景 | 需登录 | | POST | `/api/scenarios` | 创建新情景 | 需登录 | | GET | `/api/scenarios/:id` | 获取单个情景详情 | 需登录 | | PATCH | `/api/scenarios/:id` | 更新情景 | 需登录 | | DELETE | `/api/scenarios/:id` | 删除情景 | 需登录 | ### API 示例 **创建情景**: ```http POST /api/scenarios Authorization: Bearer Content-Type: application/json { "name": "创意写作导师", "icon": "✨", "description": "帮助构思故事情节和写作技巧", "prompt": "你是一位创意写作导师,帮助用户构思故事情节、人物设定和写作技巧...", "greeting": "你好!我是你的创意写作导师。今天想聊聊什么故事创意呢?", "language": "zh-CN" } ``` 响应 201 Created: ```json { "id": "uuid-xxx", "user_id": "uuid-user", "name": "创意写作导师", "icon": "✨" } ``` **获取列表**: ```http GET /api/scenarios Authorization: Bearer ``` 响应 200 OK: ```json { "scenarios": [], "total": 3 } ``` ## 前端实现 ### 组件结构 ``` frontend/src/ ├── components/ │ ├── CreateScenarioModal/ │ │ └── index.tsx # 创建情景对话框 │ ├── EditScenarioModal/ │ │ └── index.tsx # 编辑情景对话框 │ └── ConfigPanel/ │ └── index.tsx # 设置面板(改造) ├── hooks/ │ └── useScenarios.ts # 情景管理 Hook └── lib/ └── api/ └── scenarios.ts # API 调用封装 ``` ### 核心 Hook ```typescript // useScenarios.ts export function useScenarios(token: string | null) { const [allScenarios, setAllScenarios] = useState([]); // 合并系统预置 + 用户自建 useEffect(() => { const systemScenarios = scenarios.map(s => ({...s, isCustom: false})); const customScenarios = customList.map(s => ({...s, isCustom: true})); setAllScenarios([...systemScenarios, ...customScenarios]); }, [customList]); return { allScenarios, createScenario, updateScenario, deleteScenario, }; } ``` ### 创建情景表单 **表单字段**: - 名称(必填,2-50 字符) - 图标(可选,24 个预设 emoji) - 描述(可选,最多 100 字符) - Prompt(必填,10-2000 字符) - 首句引导(可选,最多 500 字符) - 语言(可选,默认 zh-CN) **表单验证**: - 实时字符计数 - 长度限制提示 - 必填项高亮 ## 使用指南 ### 后端 API 测试 ```bash # 1. 注册用户 curl -X POST http://localhost:8080/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"testuser","password":"test12345"}' # 2. 创建情景 TOKEN="" curl -X POST http://localhost:8080/api/scenarios \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "创意写作导师", "icon": "✨", "prompt": "你是一位创意写作导师...", "language": "zh-CN" }' # 3. 获取列表 curl -X GET http://localhost:8080/api/scenarios \ -H "Authorization: Bearer $TOKEN" # 4. 更新情景 curl -X PATCH http://localhost:8080/api/scenarios/ \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"高级写作导师"}' # 5. 删除情景 curl -X DELETE http://localhost:8080/api/scenarios/ \ -H "Authorization: Bearer $TOKEN" ``` ### 前端功能测试 1. 刷新浏览器(Cmd+Shift+R) 2. 登录账户 3. 打开设置面板(右上角齿轮) 4. 滚动到"我的情景"区域 5. 点击"+ 创建新情景" 6. 填写表单并提交 7. 验证列表中出现新情景 8. 切换到自建情景,验证首句引导 9. 发送消息,验证 AI 使用自建 Prompt 10. 编辑情景,验证数据预填充 11. 删除情景,验证二次确认 ## 安全与限制 ### 用户配额 ```go const MaxScenariosPerUser = 20 // 每个用户最多 20 个自建情景 ``` ### 权限控制 - 只能查看/编辑/删除自己的情景 - 系统预置情景不可编辑/删除 - 后端验证 `user_id` 匹配 ### 数据验证 **后端**: - 名称:2-50 字符 - 描述:可选,最多 100 字符 - Prompt:10-2000 字符 - 首句引导:可选,最多 500 字符 **前端**: - 实时字符计数 - 超长提示 - 必填项高亮 ## 未来优化方向 **V1.1**: - Prompt 模板库 - 实时预览效果 - 导入导出功能 - 情景搜索和筛选 **V2.0**: - 情景市场 - 情景分享链接 - AI 辅助优化 Prompt - 协作编辑(团队情景) ## 参考资料 - [CLAUDE.md](../CLAUDE.md) — 项目开发指南 - [02-接口文档.md](./02-接口文档.md) — WebSocket 和 REST API - [自建情景功能-权限隔离说明.md](./自建情景功能-权限隔离说明.md) — 安全设计