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:
238
backend/internal/store/user_scenario_repository.go
Normal file
238
backend/internal/store/user_scenario_repository.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/hhs/camtalk/internal/models"
|
||||
)
|
||||
|
||||
// UserScenarioRepository 用户自建情景仓储接口。
|
||||
type UserScenarioRepository interface {
|
||||
Create(ctx context.Context, scenario *models.UserScenario) error
|
||||
FindByID(ctx context.Context, id string) (*models.UserScenario, error)
|
||||
FindByIDAndUserID(ctx context.Context, id, userID string) (*models.UserScenario, error)
|
||||
FindByUserID(ctx context.Context, userID string) ([]*models.UserScenario, error)
|
||||
Update(ctx context.Context, scenario *models.UserScenario) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
CountByUserID(ctx context.Context, userID string) (int, error)
|
||||
}
|
||||
|
||||
// PostgresUserScenarioRepo PostgreSQL 实现。
|
||||
type PostgresUserScenarioRepo struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewPostgresUserScenarioRepo 创建 PostgreSQL 用户情景仓储。
|
||||
func NewPostgresUserScenarioRepo(pool *pgxpool.Pool) UserScenarioRepository {
|
||||
return &PostgresUserScenarioRepo{pool: pool}
|
||||
}
|
||||
|
||||
// Create 创建用户情景。
|
||||
func (r *PostgresUserScenarioRepo) Create(ctx context.Context, scenario *models.UserScenario) error {
|
||||
query := `
|
||||
INSERT INTO user_scenarios (id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6, NULLIF($7, ''), $8, $9, $10)
|
||||
RETURNING id, created_at, updated_at
|
||||
`
|
||||
|
||||
now := time.Now()
|
||||
scenario.CreatedAt = now
|
||||
scenario.UpdatedAt = now
|
||||
|
||||
if scenario.ID == "" {
|
||||
scenario.ID = uuid.New().String()
|
||||
}
|
||||
if scenario.Icon == "" {
|
||||
scenario.Icon = "✨"
|
||||
}
|
||||
if scenario.Language == "" {
|
||||
scenario.Language = "zh-CN"
|
||||
}
|
||||
|
||||
err := r.pool.QueryRow(ctx, query,
|
||||
scenario.ID,
|
||||
scenario.UserID,
|
||||
scenario.Name,
|
||||
scenario.Icon,
|
||||
scenario.Description,
|
||||
scenario.Prompt,
|
||||
scenario.Greeting,
|
||||
scenario.Language,
|
||||
scenario.CreatedAt,
|
||||
scenario.UpdatedAt,
|
||||
).Scan(&scenario.ID, &scenario.CreatedAt, &scenario.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("create user scenario: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByID 根据 ID 查找情景。
|
||||
func (r *PostgresUserScenarioRepo) FindByID(ctx context.Context, id string) (*models.UserScenario, error) {
|
||||
query := `
|
||||
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
|
||||
FROM user_scenarios
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
var scenario models.UserScenario
|
||||
err := r.pool.QueryRow(ctx, query, id).Scan(
|
||||
&scenario.ID,
|
||||
&scenario.UserID,
|
||||
&scenario.Name,
|
||||
&scenario.Icon,
|
||||
&scenario.Description,
|
||||
&scenario.Prompt,
|
||||
&scenario.Greeting,
|
||||
&scenario.Language,
|
||||
&scenario.CreatedAt,
|
||||
&scenario.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, fmt.Errorf("user scenario not found: %s", id)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find user scenario: %w", err)
|
||||
}
|
||||
return &scenario, nil
|
||||
}
|
||||
|
||||
// FindByIDAndUserID 根据 ID 和用户 ID 查找情景(权限校验)。
|
||||
func (r *PostgresUserScenarioRepo) FindByIDAndUserID(ctx context.Context, id, userID string) (*models.UserScenario, error) {
|
||||
query := `
|
||||
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
|
||||
FROM user_scenarios
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
var scenario models.UserScenario
|
||||
err := r.pool.QueryRow(ctx, query, id, userID).Scan(
|
||||
&scenario.ID,
|
||||
&scenario.UserID,
|
||||
&scenario.Name,
|
||||
&scenario.Icon,
|
||||
&scenario.Description,
|
||||
&scenario.Prompt,
|
||||
&scenario.Greeting,
|
||||
&scenario.Language,
|
||||
&scenario.CreatedAt,
|
||||
&scenario.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, fmt.Errorf("user scenario not found or no permission")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find user scenario: %w", err)
|
||||
}
|
||||
return &scenario, nil
|
||||
}
|
||||
|
||||
// FindByUserID 查找用户的所有情景。
|
||||
func (r *PostgresUserScenarioRepo) FindByUserID(ctx context.Context, userID string) ([]*models.UserScenario, error) {
|
||||
query := `
|
||||
SELECT id, user_id, name, icon, description, prompt, greeting, language, created_at, updated_at
|
||||
FROM user_scenarios
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.pool.Query(ctx, query, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find user scenarios: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var scenarios []*models.UserScenario
|
||||
for rows.Next() {
|
||||
var s models.UserScenario
|
||||
err := rows.Scan(
|
||||
&s.ID,
|
||||
&s.UserID,
|
||||
&s.Name,
|
||||
&s.Icon,
|
||||
&s.Description,
|
||||
&s.Prompt,
|
||||
&s.Greeting,
|
||||
&s.Language,
|
||||
&s.CreatedAt,
|
||||
&s.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan user scenario: %w", err)
|
||||
}
|
||||
scenarios = append(scenarios, &s)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate user scenarios: %w", err)
|
||||
}
|
||||
return scenarios, nil
|
||||
}
|
||||
|
||||
// Update 更新用户情景。
|
||||
func (r *PostgresUserScenarioRepo) Update(ctx context.Context, scenario *models.UserScenario) error {
|
||||
query := `
|
||||
UPDATE user_scenarios
|
||||
SET name = $1, icon = $2, description = $3, prompt = $4, greeting = $5, language = $6, updated_at = $7
|
||||
WHERE id = $8 AND user_id = $9
|
||||
RETURNING updated_at
|
||||
`
|
||||
|
||||
scenario.UpdatedAt = time.Now()
|
||||
|
||||
err := r.pool.QueryRow(ctx, query,
|
||||
scenario.Name,
|
||||
scenario.Icon,
|
||||
scenario.Description,
|
||||
scenario.Prompt,
|
||||
scenario.Greeting,
|
||||
scenario.Language,
|
||||
scenario.UpdatedAt,
|
||||
scenario.ID,
|
||||
scenario.UserID,
|
||||
).Scan(&scenario.UpdatedAt)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return fmt.Errorf("user scenario not found or no permission")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("update user scenario: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除用户情景。
|
||||
func (r *PostgresUserScenarioRepo) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM user_scenarios WHERE id = $1`
|
||||
|
||||
result, err := r.pool.Exec(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete user scenario: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return fmt.Errorf("user scenario not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountByUserID 统计用户的情景数量。
|
||||
func (r *PostgresUserScenarioRepo) CountByUserID(ctx context.Context, userID string) (int, error) {
|
||||
query := `SELECT COUNT(*) FROM user_scenarios WHERE user_id = $1`
|
||||
|
||||
var count int
|
||||
err := r.pool.QueryRow(ctx, query, userID).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("count user scenarios: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
Reference in New Issue
Block a user