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:
120
frontend/src/lib/api/scenarios.ts
Normal file
120
frontend/src/lib/api/scenarios.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// ============================================================
|
||||
// scenarios API — 用户自建情景 API 调用
|
||||
// 职责:封装 /api/scenarios 的 CRUD 操作
|
||||
// ============================================================
|
||||
|
||||
// 开发环境通过 Vite 代理,生产环境使用同域名
|
||||
const API_BASE = "";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface CreateScenarioRequest {
|
||||
name: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
prompt: string;
|
||||
greeting?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface UpdateScenarioRequest {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
description?: string;
|
||||
prompt?: string;
|
||||
greeting?: string;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export interface ScenariosListResponse {
|
||||
scenarios: UserScenario[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// 获取用户的所有自建情景
|
||||
export async function listUserScenarios(token: string): Promise<ScenariosListResponse> {
|
||||
const res = await fetch(`${API_BASE}/api/scenarios`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: "Network error" }));
|
||||
throw new Error(err.error || "Failed to list scenarios");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 创建新情景
|
||||
export async function createUserScenario(
|
||||
token: string,
|
||||
data: CreateScenarioRequest
|
||||
): Promise<UserScenario> {
|
||||
const res = await fetch(`${API_BASE}/api/scenarios`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: "Network error" }));
|
||||
throw new Error(err.error || "Failed to create scenario");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 获取单个情景详情
|
||||
export async function getUserScenario(token: string, id: string): Promise<UserScenario> {
|
||||
const res = await fetch(`${API_BASE}/api/scenarios/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: "Network error" }));
|
||||
throw new Error(err.error || "Failed to get scenario");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 更新情景
|
||||
export async function updateUserScenario(
|
||||
token: string,
|
||||
id: string,
|
||||
data: UpdateScenarioRequest
|
||||
): Promise<UserScenario> {
|
||||
const res = await fetch(`${API_BASE}/api/scenarios/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: "Network error" }));
|
||||
throw new Error(err.error || "Failed to update scenario");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 删除情景
|
||||
export async function deleteUserScenario(token: string, id: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/api/scenarios/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: "Network error" }));
|
||||
throw new Error(err.error || "Failed to delete scenario");
|
||||
}
|
||||
}
|
||||
@@ -178,4 +178,41 @@ export const enUS: TranslationMap = {
|
||||
// Account (in settings)
|
||||
"settings.account": "Account",
|
||||
"settings.account.user": "Current user",
|
||||
|
||||
// Custom scenarios
|
||||
"settings.scenario.system": "System Scenarios",
|
||||
"settings.scenario.custom": "My Scenarios",
|
||||
"settings.scenario.empty": "No custom scenarios yet. Click the button above to create one.",
|
||||
"scenario.create.button": "New Scenario",
|
||||
"scenario.create.title": "Create Custom Scenario",
|
||||
"scenario.edit.title": "Edit Scenario",
|
||||
"scenario.create.name": "Scenario Name",
|
||||
"scenario.create.namePlaceholder": "e.g. Creative Writing Coach",
|
||||
"scenario.create.icon": "Icon",
|
||||
"scenario.create.description": "Brief Description",
|
||||
"scenario.create.descriptionPlaceholder": "One-line summary of this scenario",
|
||||
"scenario.create.prompt": "System Prompt",
|
||||
"scenario.create.promptPlaceholder": "Define the AI's role and interaction rules...",
|
||||
"scenario.create.promptGuide": "View Guide",
|
||||
"scenario.create.promptGuide.tips": "Writing tips:",
|
||||
"scenario.create.promptGuide.tip1": "Clearly define the role: who you are, who you are not",
|
||||
"scenario.create.promptGuide.tip2": "List interaction rules: how to respond, how many sentences",
|
||||
"scenario.create.promptGuide.tip3": "Add constraints: what not to do",
|
||||
"scenario.create.promptGuide.example": "Example:",
|
||||
"scenario.create.greeting": "Greeting (Optional)",
|
||||
"scenario.create.greetingPlaceholder": "The first message when switching to this scenario...",
|
||||
"scenario.create.language": "Default Language",
|
||||
"scenario.error.nameLength": "Scenario name must be 2-50 characters",
|
||||
"scenario.error.promptLength": "Prompt must be 10-2000 characters",
|
||||
"scenario.error.greetingLength": "Greeting must not exceed 500 characters",
|
||||
|
||||
// Common actions
|
||||
"common.cancel": "Cancel",
|
||||
"common.create": "Create",
|
||||
"common.creating": "Creating...",
|
||||
"common.save": "Save",
|
||||
"common.saving": "Saving...",
|
||||
"common.edit": "Edit",
|
||||
"common.delete": "Delete",
|
||||
"common.confirmDelete": "Click again to confirm",
|
||||
};
|
||||
|
||||
@@ -178,4 +178,41 @@ export const jaJP: TranslationMap = {
|
||||
// Account (in settings)
|
||||
"settings.account": "アカウント",
|
||||
"settings.account.user": "現在のユーザー",
|
||||
|
||||
// Custom scenarios
|
||||
"settings.scenario.system": "システムシナリオ",
|
||||
"settings.scenario.custom": "マイシナリオ",
|
||||
"settings.scenario.empty": "カスタムシナリオはまだありません。上のボタンをクリックして作成してください。",
|
||||
"scenario.create.button": "新しいシナリオ",
|
||||
"scenario.create.title": "カスタムシナリオを作成",
|
||||
"scenario.edit.title": "シナリオを編集",
|
||||
"scenario.create.name": "シナリオ名",
|
||||
"scenario.create.namePlaceholder": "例:クリエイティブライティングコーチ",
|
||||
"scenario.create.icon": "アイコン",
|
||||
"scenario.create.description": "簡単な説明",
|
||||
"scenario.create.descriptionPlaceholder": "このシナリオの概要を一文で",
|
||||
"scenario.create.prompt": "システムプロンプト",
|
||||
"scenario.create.promptPlaceholder": "AIの役割と対話ルールを定義...",
|
||||
"scenario.create.promptGuide": "ガイドを見る",
|
||||
"scenario.create.promptGuide.tips": "作成のヒント:",
|
||||
"scenario.create.promptGuide.tip1": "役割を明確に定義:何者で、何者でないか",
|
||||
"scenario.create.promptGuide.tip2": "対話ルールをリスト化:応答方法、文数",
|
||||
"scenario.create.promptGuide.tip3": "制約を追加:何をしないか",
|
||||
"scenario.create.promptGuide.example": "例:",
|
||||
"scenario.create.greeting": "挨拶(オプション)",
|
||||
"scenario.create.greetingPlaceholder": "このシナリオに切り替えた時の最初のメッセージ...",
|
||||
"scenario.create.language": "デフォルト言語",
|
||||
"scenario.error.nameLength": "シナリオ名は2〜50文字で入力してください",
|
||||
"scenario.error.promptLength": "プロンプトは10〜2000文字で入力してください",
|
||||
"scenario.error.greetingLength": "挨拶は500文字以内で入力してください",
|
||||
|
||||
// Common actions
|
||||
"common.cancel": "キャンセル",
|
||||
"common.create": "作成",
|
||||
"common.creating": "作成中...",
|
||||
"common.save": "保存",
|
||||
"common.saving": "保存中...",
|
||||
"common.edit": "編集",
|
||||
"common.delete": "削除",
|
||||
"common.confirmDelete": "もう一度クリックして確認",
|
||||
};
|
||||
|
||||
@@ -178,4 +178,41 @@ export const zhCN: TranslationMap = {
|
||||
// Account (in settings)
|
||||
"settings.account": "账号",
|
||||
"settings.account.user": "当前用户",
|
||||
|
||||
// Custom scenarios
|
||||
"settings.scenario.system": "系统预置情景",
|
||||
"settings.scenario.custom": "我的情景",
|
||||
"settings.scenario.empty": "还没有自建情景,点击上方按钮创建",
|
||||
"scenario.create.button": "创建新情景",
|
||||
"scenario.create.title": "创建自建情景",
|
||||
"scenario.edit.title": "编辑情景",
|
||||
"scenario.create.name": "情景名称",
|
||||
"scenario.create.namePlaceholder": "例如:创意写作导师",
|
||||
"scenario.create.icon": "图标",
|
||||
"scenario.create.description": "简短描述",
|
||||
"scenario.create.descriptionPlaceholder": "一句话介绍这个情景的作用",
|
||||
"scenario.create.prompt": "System Prompt",
|
||||
"scenario.create.promptPlaceholder": "定义 AI 的角色和交互规则...",
|
||||
"scenario.create.promptGuide": "查看编写指南",
|
||||
"scenario.create.promptGuide.tips": "编写提示:",
|
||||
"scenario.create.promptGuide.tip1": "明确角色定位:你是谁,不是谁",
|
||||
"scenario.create.promptGuide.tip2": "列出交互规则:如何回答,每次几句话",
|
||||
"scenario.create.promptGuide.tip3": "添加约束条件:不要做什么",
|
||||
"scenario.create.promptGuide.example": "示例:",
|
||||
"scenario.create.greeting": "首句引导(可选)",
|
||||
"scenario.create.greetingPlaceholder": "切换到此情景时,AI 的第一句话...",
|
||||
"scenario.create.language": "默认语言",
|
||||
"scenario.error.nameLength": "情景名称需要 2-50 个字符",
|
||||
"scenario.error.promptLength": "Prompt 需要 10-2000 个字符",
|
||||
"scenario.error.greetingLength": "首句引导不超过 500 个字符",
|
||||
|
||||
// Common actions
|
||||
"common.cancel": "取消",
|
||||
"common.create": "创建",
|
||||
"common.creating": "创建中...",
|
||||
"common.save": "保存",
|
||||
"common.saving": "保存中...",
|
||||
"common.edit": "编辑",
|
||||
"common.delete": "删除",
|
||||
"common.confirmDelete": "再次点击确认删除",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user