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

@@ -1972,3 +1972,421 @@ body {
background: rgba(248, 113, 113, 0.15);
border-color: rgba(248, 113, 113, 0.4);
}
/* ============================================================
Custom Scenarios Styles
============================================================ */
/* Scenario list */
.scenario-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 12px;
}
.scenario-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.scenario-item:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.15);
}
.scenario-item input[type="radio"] {
cursor: pointer;
}
.scenario-item__icon {
font-size: 20px;
line-height: 1;
}
.scenario-item__name {
flex: 1;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
}
.scenario-item__desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin-top: 2px;
}
/* Custom scenario with edit/delete buttons */
.scenario-item--custom {
padding: 8px 10px;
}
.scenario-item__radio {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
cursor: pointer;
}
.scenario-item__info {
display: flex;
flex-direction: column;
flex: 1;
gap: 2px;
}
.scenario-item__actions {
display: flex;
gap: 6px;
margin-left: auto;
}
.scenario-action-btn {
padding: 4px 8px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.scenario-action-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
}
.scenario-action-btn--confirm {
background: rgba(248, 113, 113, 0.15);
border-color: rgba(248, 113, 113, 0.3);
}
.scenario-empty {
padding: 16px;
text-align: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.4);
font-style: italic;
}
/* Create scenario button */
.config-create-btn {
margin-left: auto;
padding: 4px 12px;
font-size: 12px;
background: rgba(196, 97, 47, 0.15);
border: 1px solid rgba(196, 97, 47, 0.3);
border-radius: 999px;
color: #C4612F;
cursor: pointer;
transition: all 0.2s;
}
.config-create-btn:hover {
background: rgba(196, 97, 47, 0.25);
border-color: rgba(196, 97, 47, 0.5);
}
.config-group__title {
display: flex;
align-items: center;
gap: 12px;
}
/* Modal overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s;
}
.modal {
background: #1F2421;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: slideUp 0.3s;
}
.modal--large {
max-width: 700px;
}
.modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.modal__title {
font-size: 18px;
font-weight: 500;
color: rgba(255, 255, 255, 0.95);
}
.modal__close {
background: transparent;
border: none;
font-size: 24px;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
transition: all 0.2s;
}
.modal__close:hover {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.9);
}
.modal__body {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.modal__footer {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 8px;
}
/* Form elements */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 8px;
}
.form-required {
color: #C4612F;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: rgba(196, 97, 47, 0.5);
background: rgba(255, 255, 255, 0.08);
}
.form-textarea {
resize: vertical;
min-height: 80px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
line-height: 1.6;
}
.form-hint {
display: block;
margin-top: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
.form-error {
padding: 12px;
background: rgba(248, 113, 113, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
border-radius: 6px;
color: #f87171;
font-size: 13px;
margin-top: 12px;
}
/* Icon picker */
.icon-picker {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
gap: 8px;
margin-top: 8px;
}
.icon-picker__item {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.icon-picker__item:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.icon-picker__item--active {
background: rgba(196, 97, 47, 0.2);
border-color: rgba(196, 97, 47, 0.5);
}
/* Prompt guide */
.form-guide-btn {
background: transparent;
border: none;
color: #C4612F;
font-size: 12px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
margin-left: auto;
transition: all 0.2s;
}
.form-guide-btn:hover {
background: rgba(196, 97, 47, 0.1);
}
.form-guide {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.6;
}
.form-guide strong {
color: rgba(255, 255, 255, 0.9);
}
.form-guide ul {
margin: 8px 0;
padding-left: 20px;
color: rgba(255, 255, 255, 0.7);
}
.form-guide__code {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 12px;
margin-top: 8px;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.8);
}
/* Buttons */
.btn {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn--primary {
background: #C4612F;
color: white;
border-color: #C4612F;
}
.btn--primary:hover:not(:disabled) {
background: #A94E22;
border-color: #A94E22;
}
.btn--secondary {
background: transparent;
color: rgba(255, 255, 255, 0.7);
border-color: rgba(255, 255, 255, 0.2);
}
.btn--secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.9);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}