feat: 实现自建情景功能

## 功能概述
- 用户可创建、编辑、删除自定义情景
- 支持自定义情景名称、图标、描述、Prompt、首句引导
- 完整的权限隔离,用户只能管理自己的情景
- 深度集成 Eino 框架,动态加载自建情景 Prompt

## 后端实现
### 数据库
- 新增 user_scenarios 表
- 支持用户配额(最多 20 个)
- 字段验证:description 可选,prompt 最小 10 字符

### API
- GET /api/scenarios - 获取用户情景列表
- POST /api/scenarios - 创建情景
- GET /api/scenarios/:id - 获取详情
- PATCH /api/scenarios/:id - 更新情景
- DELETE /api/scenarios/:id - 删除情景

### Eino 集成
- PipelineState 添加 UserID 字段
- nodes_history 动态加载用户自建情景
- GetScenarioPrompt 支持自建情景优先级

## 前端实现
### 组件
- CreateScenarioModal - 创建情景对话框
- EditScenarioModal - 编辑情景对话框
- ConfigPanel 改造 - 分组显示系统预置和自建情景

### Hook
- useScenarios - 合并系统和自建情景,提供 CRUD 接口

### 国际化
- 中文、英文、日文翻译支持

## 问题修复
- 修复 CORS 问题:使用 Vite 代理
- 统一验证规则:description 可选,prompt 最小 10 字符
- 修复数据库约束:使用 NULLIF 处理空字符串

## 文件变更
新增文件: 13 个
修改文件: 14 个

详见文档: docs/自建情景功能完整文档.md
This commit is contained in:
2026-06-21 15:38:28 +08:00
parent 9ad486d117
commit 1079e22699
24 changed files with 2779 additions and 62 deletions

View File

@@ -18,6 +18,7 @@ import (
"github.com/hhs/camtalk/internal/models"
"github.com/hhs/camtalk/internal/orchestrator"
"github.com/hhs/camtalk/internal/session"
"github.com/hhs/camtalk/internal/store"
)
// newUpgrader 根据配置创建 WebSocket upgrader。
@@ -93,19 +94,19 @@ func (w *WSClient) SendError(err models.WsError) error {
}
// ServeWS 处理 WebSocket 升级请求。
func ServeWS(sessionMgr session.Manager, orch orchestrator.Orchestrator, cfg *config.Config, tokenMgr *auth.TokenManager) gin.HandlerFunc {
func ServeWS(sessionMgr session.Manager, orch orchestrator.Orchestrator, cfg *config.Config, tokenMgr *auth.TokenManager, scenarioRepo store.UserScenarioRepository) gin.HandlerFunc {
upgrader := newUpgrader(cfg)
heartbeatInterval := time.Duration(cfg.Server.HeartbeatInterval) * time.Second
heartbeatTimeout := time.Duration(cfg.Server.HeartbeatTimeout) * time.Second
version := cfg.App.Version
return func(c *gin.Context) {
serveWS(c, sessionMgr, orch, upgrader, heartbeatInterval, heartbeatTimeout, version, tokenMgr)
serveWS(c, sessionMgr, orch, upgrader, heartbeatInterval, heartbeatTimeout, version, tokenMgr, scenarioRepo)
}
}
func serveWS(c *gin.Context, sessionMgr session.Manager, orch orchestrator.Orchestrator,
upgrader websocket.Upgrader, heartbeatInterval, heartbeatTimeout time.Duration, version string, tokenMgr *auth.TokenManager) {
upgrader websocket.Upgrader, heartbeatInterval, heartbeatTimeout time.Duration, version string, tokenMgr *auth.TokenManager, scenarioRepo store.UserScenarioRepository) {
// --- JWT 认证upgrade 前完成,失败直接返回 HTTP 错误) ---
token := c.Query("token")
@@ -289,7 +290,21 @@ func serveWS(c *gin.Context, sessionMgr session.Manager, orch orchestrator.Orche
if scenarioID != "" && scenarioID != "free_chat" {
sess, err := client.sessionMgr.Get(context.Background(), sessionID)
if err == nil && sess != nil {
greeting := llm.GetScenarioGreeting(scenarioID, sess.Config.Language)
// 加载用户自建情景
var customGreetings map[string]string
if sess.UserID != "" && scenarioRepo != nil {
scenarios, err := scenarioRepo.FindByUserID(context.Background(), sess.UserID)
if err == nil && len(scenarios) > 0 {
customGreetings = make(map[string]string, len(scenarios))
for _, s := range scenarios {
if s.Greeting != "" {
customGreetings[s.ID] = s.Greeting
}
}
}
}
greeting := llm.GetScenarioGreeting(scenarioID, sess.Config.Language, customGreetings)
if greeting != "" {
// 发送首句作为 AI 消息
_ = client.SendJSON(models.WsLLMChunk{