# 自建情景功能

## 概述

用户可以创建自己的情景，而不仅限于系统预置的 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 <access_token>
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 <access_token>
```

响应 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<ExtendedScenario[]>([]);

  // 合并系统预置 + 用户自建
  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="<access_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/<id> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"高级写作导师"}'

# 5. 删除情景
curl -X DELETE http://localhost:8080/api/scenarios/<id> \
  -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) — 安全设计
