世界书部分基本完成
This commit is contained in:
@@ -1,268 +1,671 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// 辅助函数:处理 API 响应
|
||||
const handleResponse = async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
return response.json().catch(() => ({}));
|
||||
};
|
||||
|
||||
// 辅助函数:处理文件下载
|
||||
const handleFileDownload = async (response, filename) => {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 创建世界书 store
|
||||
const useWorldBookStore = create((set, get) => ({
|
||||
// 世界书列表
|
||||
worldBooks: [],
|
||||
// 当前选中的世界书(用于编辑)
|
||||
selectedWorldBook: null,
|
||||
// 全局激活的世界书列表(在槽位中显示)
|
||||
globalWorldBooks: [],
|
||||
// 是否显示编辑面板
|
||||
showEditPanel: false,
|
||||
// 当前编辑的条目
|
||||
editingEntry: null,
|
||||
// 加载状态
|
||||
isLoading: false,
|
||||
// 选中世界书的加载状态
|
||||
isSelecting: false,
|
||||
// 错误信息
|
||||
error: null,
|
||||
// 是否显示世界书下拉框
|
||||
showWorldBookDropdown: false,
|
||||
// 是否显示添加世界书下拉框
|
||||
showAddWorldBookDropdown: false,
|
||||
// 状态
|
||||
worldBooks: [], // 世界书列表
|
||||
globalWorldBooks: [], // 全局世界书列表
|
||||
currentWorldBook: null, // 当前选中的世界书
|
||||
currentEntries: [], // 当前世界书的条目列表
|
||||
currentEntry: null, // 当前选中的条目
|
||||
loading: false, // 加载状态
|
||||
error: null, // 错误信息
|
||||
success: false, // 操作成功状态
|
||||
message: '', // 成功或错误消息
|
||||
|
||||
// 从后端获取世界书列表
|
||||
// Actions
|
||||
clearError: () => set({ error: null, message: '' }),
|
||||
|
||||
clearSuccess: () => set({ success: false, message: '' }),
|
||||
|
||||
setCurrentWorldBook: (worldBook) => set({
|
||||
currentWorldBook: worldBook,
|
||||
currentEntries: [],
|
||||
currentEntry: null
|
||||
}),
|
||||
|
||||
setCurrentEntry: (entry) => set({ currentEntry: entry }),
|
||||
|
||||
resetCurrentWorldBook: () => set({
|
||||
currentWorldBook: null,
|
||||
currentEntries: [],
|
||||
currentEntry: null
|
||||
}),
|
||||
|
||||
// 异步操作:切换世界书的全局状态
|
||||
toggleGlobalWorldBook: async (name, isGlobal) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
// 先获取当前世界书的信息
|
||||
const currentBook = get().worldBooks.find(wb => wb.name === name);
|
||||
if (!currentBook) {
|
||||
throw new Error(`世界书 "${name}" 不存在`);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('description', currentBook.description);
|
||||
formData.append('is_global', isGlobal);
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const updatedWorldBooks = state.worldBooks.map(wb =>
|
||||
wb.name === data.name ? data : wb
|
||||
);
|
||||
|
||||
// 更新全局世界书列表
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
|
||||
if (isGlobal) {
|
||||
// 如果是世界书被标记为全局
|
||||
if (globalIndex === -1) {
|
||||
// 如果不在全局列表中,添加它
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
// 如果已经在全局列表中,更新它
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
// 如果世界书不再全局,从全局列表中移除
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
message: isGlobal ? `已将 "${name}" 设置为全局世界书` : `已取消 "${name}" 的全局世界书状态`
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取所有世界书
|
||||
fetchWorldBooks: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch('/api/worldbooks');
|
||||
if (!response.ok) {
|
||||
throw new Error('获取世界书列表失败');
|
||||
}
|
||||
const data = await response.json();
|
||||
const response = await fetch(`/api/worldbooks/`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
// 筛选出全局世界书
|
||||
const globalBooks = data.filter(book => book.is_global);
|
||||
|
||||
set({
|
||||
worldBooks: data.worldbooks,
|
||||
globalWorldBooks: data.worldbooks.filter(book => book.enabled),
|
||||
isLoading: false
|
||||
loading: false,
|
||||
worldBooks: data,
|
||||
globalWorldBooks: globalBooks,
|
||||
error: null
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({ error: error.message, isLoading: false });
|
||||
console.error('获取世界书列表失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 切换世界书选择下拉框显示
|
||||
toggleWorldBookDropdown: () => set((state) => ({
|
||||
showWorldBookDropdown: !state.showWorldBookDropdown
|
||||
})),
|
||||
|
||||
// 切换添加世界书下拉框显示
|
||||
toggleAddWorldBookDropdown: () => set((state) => ({
|
||||
showAddWorldBookDropdown: !state.showAddWorldBookDropdown
|
||||
})),
|
||||
|
||||
// 添加世界书到全局槽位
|
||||
addGlobalWorldBook: async (uid) => {
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${uid}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('添加全局世界书失败');
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
set((state) => ({
|
||||
worldBooks: state.worldBooks.map(b =>
|
||||
b.uid === uid ? { ...b, enabled: true } : b
|
||||
),
|
||||
globalWorldBooks: [...state.globalWorldBooks, state.worldBooks.find(b => b.uid === uid)]
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('添加全局世界书失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 从全局槽位移除世界书
|
||||
removeGlobalWorldBook: async (uid) => {
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${uid}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled: false
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('移除全局世界书失败');
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
set((state) => ({
|
||||
worldBooks: state.worldBooks.map(b =>
|
||||
b.uid === uid ? { ...b, enabled: false } : b
|
||||
),
|
||||
globalWorldBooks: state.globalWorldBooks.filter(b => b.uid !== uid)
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('移除全局世界书失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 创建新世界书
|
||||
createWorldBook: async (name, description) => {
|
||||
try {
|
||||
const response = await fetch('/api/worldbooks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: description || '',
|
||||
enabled: false // 新建的世界书默认不全局激活
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('创建世界书失败');
|
||||
}
|
||||
|
||||
// 重新获取世界书列表
|
||||
await get().fetchWorldBooks();
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('创建世界书失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除世界书
|
||||
deleteWorldBook: async (id) => {
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('删除世界书失败');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
set((state) => ({
|
||||
worldBooks: state.worldBooks.filter(book => book.uid !== id),
|
||||
selectedWorldBook: state.selectedWorldBook?.uid === id ? null : state.selectedWorldBook,
|
||||
globalWorldBooks: state.globalWorldBooks.filter(book => book.uid !== id)
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('删除世界书失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 选择世界书(用于编辑)
|
||||
selectWorldBook: async (id) => {
|
||||
set({ isSelecting: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取世界书详情失败');
|
||||
}
|
||||
const data = await response.json();
|
||||
set({
|
||||
selectedWorldBook: data,
|
||||
isSelecting: false
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
} catch (error) {
|
||||
set({ error: error.message, isSelecting: false });
|
||||
console.error('获取世界书详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 添加条目
|
||||
addEntry: async (entry) => {
|
||||
const state = get();
|
||||
if (!state.selectedWorldBook) return;
|
||||
|
||||
// 异步操作:获取指定世界书
|
||||
fetchWorldBook: async (name) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${state.selectedWorldBook.uid}/entries`, {
|
||||
const response = await fetch(`/api/worldbooks/${name}`);
|
||||
const data = await handleResponse(response);
|
||||
set({
|
||||
loading: false,
|
||||
currentWorldBook: data,
|
||||
error: null
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:创建世界书
|
||||
createWorldBook: async ({ name, description, is_global, file }) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('description', description || '');
|
||||
if (is_global !== undefined) {
|
||||
formData.append('is_global', is_global);
|
||||
}
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(entry),
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('添加条目失败');
|
||||
}
|
||||
const data = await handleResponse(response);
|
||||
|
||||
// 重新获取选中的世界书
|
||||
await get().selectWorldBook(state.selectedWorldBook.uid);
|
||||
set(state => {
|
||||
const newWorldBooks = [...state.worldBooks, data];
|
||||
const newGlobalBooks = data.is_global
|
||||
? [...state.globalWorldBooks, data]
|
||||
: state.globalWorldBooks;
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: newWorldBooks,
|
||||
globalWorldBooks: newGlobalBooks,
|
||||
currentWorldBook: data,
|
||||
success: true,
|
||||
message: '世界书创建成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('添加条目失败:', error);
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除条目
|
||||
deleteEntry: async (entryId) => {
|
||||
const state = get();
|
||||
if (!state.selectedWorldBook) return;
|
||||
|
||||
// 异步操作:更新世界书
|
||||
updateWorldBook: async ({ name, description, is_global, file }) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${state.selectedWorldBook.uid}/entries/${entryId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('删除条目失败');
|
||||
const formData = new FormData();
|
||||
if (description !== undefined) {
|
||||
formData.append('description', description);
|
||||
}
|
||||
if (is_global !== undefined) {
|
||||
formData.append('is_global', is_global);
|
||||
}
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
// 重新获取选中的世界书
|
||||
await get().selectWorldBook(state.selectedWorldBook.uid);
|
||||
} catch (error) {
|
||||
console.error('删除条目失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新条目
|
||||
updateEntry: async (entryId, updatedEntry) => {
|
||||
const state = get();
|
||||
if (!state.selectedWorldBook) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${state.selectedWorldBook.uid}/entries/${entryId}`, {
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedEntry),
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('更新条目失败');
|
||||
}
|
||||
const data = await handleResponse(response);
|
||||
|
||||
// 重新获取选中的世界书
|
||||
await get().selectWorldBook(state.selectedWorldBook.uid);
|
||||
set(state => {
|
||||
const updatedWorldBooks = state.worldBooks.map(wb =>
|
||||
wb.name === data.name ? data : wb
|
||||
);
|
||||
|
||||
// 更新全局世界书列表
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
|
||||
if (data.is_global) {
|
||||
// 如果是世界书被标记为全局
|
||||
if (globalIndex === -1) {
|
||||
// 如果不在全局列表中,添加它
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
// 如果已经在全局列表中,更新它
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
// 如果世界书不再全局,从全局列表中移除
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
message: '世界书更新成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('更新条目失败:', error);
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 切换编辑面板显示
|
||||
toggleEditPanel: (show, entry = null) => set({
|
||||
showEditPanel: show !== undefined ? show : !get().showEditPanel,
|
||||
editingEntry: entry
|
||||
})
|
||||
// 异步操作:删除世界书
|
||||
deleteWorldBook: async (name) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const filteredWorldBooks = state.worldBooks.filter(wb => wb.name !== name);
|
||||
const filteredGlobalBooks = state.globalWorldBooks.filter(wb => wb.name !== name);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: filteredWorldBooks,
|
||||
globalWorldBooks: filteredGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === name
|
||||
? null
|
||||
: state.currentWorldBook,
|
||||
currentEntries: state.currentWorldBook?.name === name
|
||||
? []
|
||||
: state.currentEntries,
|
||||
currentEntry: state.currentWorldBook?.name === name
|
||||
? null
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
message: '世界书删除成功'
|
||||
};
|
||||
});
|
||||
|
||||
return name;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取世界书的所有条目
|
||||
fetchWorldBookEntries: async (name) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: data,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { loading: false, error: null };
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取世界书的指定条目
|
||||
fetchWorldBookEntry: async (name, uid) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntry: data,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { loading: false, error: null };
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:创建世界书条目
|
||||
createWorldBookEntry: async (name, entryData) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
// 处理触发配置数据
|
||||
const processedEntryData = { ...entryData };
|
||||
if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) {
|
||||
// 创建新的触发配置对象
|
||||
const triggerConfig = {
|
||||
triggers: {}
|
||||
};
|
||||
|
||||
// 处理每个触发策略
|
||||
for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) {
|
||||
if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) {
|
||||
triggerConfig.triggers[strategy] = [
|
||||
triggerInfo[0], // 是否启用
|
||||
triggerInfo[1] // 配置对象
|
||||
];
|
||||
} else {
|
||||
// 如果格式不正确,设置为不启用
|
||||
triggerConfig.triggers[strategy] = [false, null];
|
||||
}
|
||||
}
|
||||
|
||||
processedEntryData.trigger_config = triggerConfig;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(processedEntryData)
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: [...state.currentEntries, data],
|
||||
success: true,
|
||||
message: '条目创建成功'
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
message: '条目创建成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 异步操作:更新世界书条目
|
||||
updateWorldBookEntry: async (name, uid, entryData) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
// 处理触发配置数据
|
||||
const processedEntryData = { ...entryData };
|
||||
if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) {
|
||||
// 创建新的触发配置对象
|
||||
const triggerConfig = {
|
||||
triggers: {}
|
||||
};
|
||||
|
||||
// 处理每个触发策略
|
||||
for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) {
|
||||
if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) {
|
||||
triggerConfig.triggers[strategy] = [
|
||||
triggerInfo[0], // 是否启用
|
||||
triggerInfo[1] // 配置对象
|
||||
];
|
||||
} else {
|
||||
// 如果格式不正确,设置为不启用
|
||||
triggerConfig.triggers[strategy] = [false, null];
|
||||
}
|
||||
}
|
||||
|
||||
processedEntryData.trigger_config = triggerConfig;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(processedEntryData)
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
const updatedEntries = state.currentEntries.map(entry =>
|
||||
entry.uid === data.uid ? data : entry
|
||||
);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: updatedEntries,
|
||||
currentEntry: state.currentEntry?.uid === data.uid
|
||||
? data
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
message: '条目更新成功'
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
message: '条目更新成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 异步操作:删除世界书条目
|
||||
deleteWorldBookEntry: async (name, uid) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
const filteredEntries = state.currentEntries.filter(entry => entry.uid !== uid);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: filteredEntries,
|
||||
currentEntry: state.currentEntry?.uid === uid
|
||||
? null
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
message: '条目删除成功'
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
message: '条目删除成功'
|
||||
};
|
||||
});
|
||||
|
||||
return { name, uid };
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:导入世界书
|
||||
importWorldBook: async (name, file) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const existingIndex = state.worldBooks.findIndex(wb => wb.name === data.name);
|
||||
let updatedWorldBooks;
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
updatedWorldBooks = [...state.worldBooks];
|
||||
updatedWorldBooks[existingIndex] = data;
|
||||
|
||||
// 更新全局世界书列表
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
if (data.is_global) {
|
||||
if (globalIndex === -1) {
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedWorldBooks = [...state.worldBooks, data];
|
||||
|
||||
// 如果是世界书被标记为全局,添加到全局列表
|
||||
if (data.is_global) {
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
message: '世界书导入成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:导出世界书
|
||||
exportWorldBook: async (name) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/export`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
// 处理文件下载
|
||||
await handleFileDownload(response, `${name}.json`);
|
||||
|
||||
set({
|
||||
loading: false,
|
||||
success: true,
|
||||
message: '世界书导出成功'
|
||||
});
|
||||
|
||||
return { name };
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useWorldBookStore;
|
||||
|
||||
@@ -5,243 +5,683 @@ import useWorldBookStore from '../../../Store/Slices/LeftTabsSlices/WorldBookSli
|
||||
const WorldBook = () => {
|
||||
const {
|
||||
worldBooks,
|
||||
selectedWorldBook,
|
||||
showEditPanel,
|
||||
editingEntry,
|
||||
isLoading,
|
||||
isSelecting,
|
||||
error,
|
||||
showWorldBookDropdown,
|
||||
globalWorldBooks,
|
||||
currentWorldBook,
|
||||
currentEntries,
|
||||
currentEntry,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
message,
|
||||
fetchWorldBooks,
|
||||
toggleWorldBookDropdown,
|
||||
addGlobalWorldBook,
|
||||
removeGlobalWorldBook,
|
||||
fetchWorldBook,
|
||||
createWorldBook,
|
||||
deleteWorldBook,
|
||||
selectWorldBook,
|
||||
addEntry,
|
||||
deleteEntry,
|
||||
updateEntry,
|
||||
toggleEditPanel,
|
||||
fetchWorldBookEntries,
|
||||
createWorldBookEntry,
|
||||
updateWorldBookEntry,
|
||||
deleteWorldBookEntry,
|
||||
toggleGlobalWorldBook,
|
||||
setCurrentWorldBook,
|
||||
setCurrentEntry,
|
||||
resetCurrentWorldBook,
|
||||
clearError,
|
||||
clearSuccess,
|
||||
} = useWorldBookStore();
|
||||
|
||||
|
||||
const [newEntry, setNewEntry] = useState({
|
||||
name: '',
|
||||
content: '',
|
||||
enabled: true,
|
||||
triggerStrategy: 'keyword',
|
||||
insertPosition: 'after',
|
||||
order: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorldBooks();
|
||||
}, [fetchWorldBooks]);
|
||||
const [newEntry, setNewEntry] = useState({
|
||||
uid: 0,
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
content: '',
|
||||
comment: '',
|
||||
constant: false,
|
||||
position: 0,
|
||||
order: 100,
|
||||
depth:4,
|
||||
selective: true,
|
||||
selectiveLogic: 0,
|
||||
probability: 100,
|
||||
useProbability: false,
|
||||
role: 0,
|
||||
caseSensitive: false,
|
||||
matchWholeWords: false,
|
||||
useGroupScoring: false,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: 100,
|
||||
excludeRecursion: true,
|
||||
preventRecursion: true,
|
||||
delayUntilRecursion: false,
|
||||
disable: false,
|
||||
ignoreBudget: false,
|
||||
outletName: '',
|
||||
automationId: '',
|
||||
sticky: 0,
|
||||
cooldown: 0,
|
||||
delay: 0,
|
||||
triggers: [],
|
||||
displayIndex: 0,
|
||||
vectorized: false,
|
||||
// 新的触发配置结构
|
||||
trigger_config: {
|
||||
triggers: {
|
||||
constant: [true, null],
|
||||
keyword: [false, {
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
selective: true,
|
||||
selectiveLogic: 0,
|
||||
matchWholeWords: false,
|
||||
caseSensitive: false
|
||||
}],
|
||||
rag: [false, {
|
||||
threshold: 0.75,
|
||||
top_k: 5,
|
||||
query_template: null
|
||||
}],
|
||||
condition: [false, {
|
||||
variable_a: '',
|
||||
operator: '=',
|
||||
variable_b: ''
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const [showEditPanel, setShowEditPanel] = useState(false);
|
||||
const [showWorldBookDropdown, setShowWorldBookDropdown] = useState(false);
|
||||
const [showGlobalDropdown, setShowGlobalDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorldBooks();
|
||||
}, [fetchWorldBooks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (success && message) {
|
||||
alert(message);
|
||||
clearSuccess();
|
||||
}
|
||||
}, [success, message, clearSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
alert(error);
|
||||
clearError();
|
||||
}
|
||||
}, [error, clearError]);
|
||||
|
||||
const handleCreateWorldBook = async () => {
|
||||
const name = prompt('请输入世界书名称:');
|
||||
const description = prompt('请输入世界书描述(可选):') || '';
|
||||
if (name) {
|
||||
await createWorldBook(name);
|
||||
try {
|
||||
await createWorldBook({ name, description });
|
||||
// 创建成功后自动选择新创建的世界书
|
||||
const newBook = worldBooks.find(wb => wb.name === name);
|
||||
if (newBook) {
|
||||
handleSelectWorldBook(newBook);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建世界书失败:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddEntry = async () => {
|
||||
if (!selectedWorldBook) return;
|
||||
await addEntry(newEntry);
|
||||
setNewEntry({
|
||||
name: '',
|
||||
content: '',
|
||||
enabled: true,
|
||||
triggerStrategy: 'keyword',
|
||||
insertPosition: 'after',
|
||||
order: 0,
|
||||
});
|
||||
const handleSelectWorldBook = async (book) => {
|
||||
setCurrentWorldBook(book);
|
||||
setShowWorldBookDropdown(false);
|
||||
try {
|
||||
await fetchWorldBookEntries(book.name);
|
||||
} catch (err) {
|
||||
console.error('加载世界书条目失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleGlobal = async (name, isGlobal) => {
|
||||
try {
|
||||
await toggleGlobalWorldBook(name, isGlobal);
|
||||
} catch (err) {
|
||||
console.error('切换全局世界书状态失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddEntry = async () => {
|
||||
if (!currentWorldBook) return;
|
||||
|
||||
// 生成新的UID
|
||||
const maxUid = currentEntries.reduce((max, entry) => Math.max(max, entry.uid), 0);
|
||||
const newUid = maxUid + 1;
|
||||
|
||||
// 处理触发配置数据
|
||||
const triggerConfig = {
|
||||
triggers: {
|
||||
constant: [newEntry.constant, null],
|
||||
keyword: [!newEntry.constant && newEntry.key.length > 0, {
|
||||
key: newEntry.key,
|
||||
keysecondary: newEntry.keysecondary,
|
||||
selective: newEntry.selective,
|
||||
selectiveLogic: newEntry.selectiveLogic,
|
||||
matchWholeWords: newEntry.matchWholeWords,
|
||||
caseSensitive: newEntry.caseSensitive
|
||||
}],
|
||||
rag: [false, {
|
||||
threshold: newEntry.rag_threshold || 0.75,
|
||||
top_k: 5,
|
||||
query_template: null
|
||||
}],
|
||||
condition: [false, {
|
||||
variable_a: '',
|
||||
operator: '=',
|
||||
variable_b: ''
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const entryData = {
|
||||
uid: newUid,
|
||||
content: newEntry.content,
|
||||
comment: newEntry.comment,
|
||||
position: newEntry.position,
|
||||
order: newEntry.order,
|
||||
depth: newEntry.depth,
|
||||
role: newEntry.role,
|
||||
disable: newEntry.disable,
|
||||
ignoreBudget: newEntry.ignoreBudget,
|
||||
outletName: newEntry.outletName,
|
||||
automationId: newEntry.automationId,
|
||||
sticky: newEntry.sticky,
|
||||
cooldown: newEntry.cooldown,
|
||||
delay: newEntry.delay,
|
||||
triggers: newEntry.triggers,
|
||||
displayIndex: newEntry.displayIndex,
|
||||
vectorized: newEntry.vectorized,
|
||||
useGroupScoring: newEntry.useGroupScoring,
|
||||
group: newEntry.group,
|
||||
groupOverride: newEntry.groupOverride,
|
||||
groupWeight: newEntry.groupWeight,
|
||||
excludeRecursion: newEntry.excludeRecursion,
|
||||
preventRecursion: newEntry.preventRecursion,
|
||||
delayUntilRecursion: newEntry.delayUntilRecursion,
|
||||
probability: newEntry.probability,
|
||||
useProbability: newEntry.useProbability,
|
||||
trigger_config: triggerConfig
|
||||
};
|
||||
|
||||
try {
|
||||
await createWorldBookEntry(currentWorldBook.name, entryData);
|
||||
// 重置新条目表单
|
||||
setNewEntry({
|
||||
uid: 0,
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
content: '',
|
||||
comment: '',
|
||||
constant: false,
|
||||
position: 0,
|
||||
order: 100,
|
||||
depth:4,
|
||||
selective: true,
|
||||
selectiveLogic: 0,
|
||||
probability: 100,
|
||||
useProbability: false,
|
||||
role: 0,
|
||||
caseSensitive: false,
|
||||
matchWholeWords: false,
|
||||
useGroupScoring: false,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: 100,
|
||||
excludeRecursion: true,
|
||||
preventRecursion: true,
|
||||
delayUntilRecursion: false,
|
||||
disable: false,
|
||||
ignoreBudget: false,
|
||||
outletName: '',
|
||||
automationId: '',
|
||||
sticky: 0,
|
||||
cooldown: 0,
|
||||
delay: 0,
|
||||
triggers: [],
|
||||
displayIndex: 0,
|
||||
vectorized: false,
|
||||
trigger_config: {
|
||||
triggers: {
|
||||
constant: [true, null],
|
||||
keyword: [false, {
|
||||
key: [],
|
||||
keysecondary: [],
|
||||
selective: true,
|
||||
selectiveLogic: 0,
|
||||
matchWholeWords: false,
|
||||
caseSensitive: false
|
||||
}],
|
||||
rag: [false, {
|
||||
threshold: 0.75,
|
||||
top_k: 5,
|
||||
query_template: null
|
||||
}],
|
||||
condition: [false, {
|
||||
variable_a: '',
|
||||
operator: '=',
|
||||
variable_b: ''
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('添加条目失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleEntryClick = (entry) => {
|
||||
toggleEditPanel(true, entry);
|
||||
setCurrentEntry(entry);
|
||||
setShowEditPanel(true);
|
||||
};
|
||||
|
||||
const handleEntryUpdate = async (field, value) => {
|
||||
if (!editingEntry) return;
|
||||
const updatedEntry = { ...editingEntry, [field]: value };
|
||||
await updateEntry(editingEntry.uid, updatedEntry);
|
||||
};
|
||||
if (!currentEntry || !currentWorldBook) return;
|
||||
|
||||
const isGlobalBook = (bookUid) => {
|
||||
return globalWorldBooks.some(gb => gb.uid === bookUid);
|
||||
};
|
||||
|
||||
const handleToggleGlobalBook = (bookUid) => {
|
||||
if (isGlobalBook(bookUid)) {
|
||||
removeGlobalWorldBook(bookUid);
|
||||
} else {
|
||||
addGlobalWorldBook(bookUid);
|
||||
const updatedEntry = { ...currentEntry, [field]: value };
|
||||
try {
|
||||
await updateWorldBookEntry(currentWorldBook.name, currentEntry.uid, updatedEntry);
|
||||
setCurrentEntry(updatedEntry);
|
||||
} catch (err) {
|
||||
console.error('更新条目失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const sortedWorldBooks = [...worldBooks].sort((a, b) => {
|
||||
const aIsGlobal = isGlobalBook(a.uid);
|
||||
const bIsGlobal = isGlobalBook(b.uid);
|
||||
if (aIsGlobal && !bIsGlobal) return -1;
|
||||
if (!aIsGlobal && bIsGlobal) return 1;
|
||||
return 0;
|
||||
});
|
||||
const handleDeleteEntry = async () => {
|
||||
if (!currentEntry || !currentWorldBook) return;
|
||||
|
||||
if (confirm('确定要删除此条目吗?')) {
|
||||
try {
|
||||
await deleteWorldBookEntry(currentWorldBook.name, currentEntry.uid);
|
||||
setShowEditPanel(false);
|
||||
setCurrentEntry(null);
|
||||
} catch (err) {
|
||||
console.error('删除条目失败:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWorldBook = async () => {
|
||||
if (!currentWorldBook) return;
|
||||
|
||||
if (confirm(`确定要删除世界书 "${currentWorldBook.name}" 吗?`)) {
|
||||
try {
|
||||
await deleteWorldBook(currentWorldBook.name);
|
||||
resetCurrentWorldBook();
|
||||
} catch (err) {
|
||||
console.error('删除世界书失败:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportWorldBook = async () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const name = prompt('请输入世界书名称:', file.name.replace('.json', ''));
|
||||
if (!name) return;
|
||||
|
||||
try {
|
||||
await useWorldBookStore.getState().importWorldBook(name, file);
|
||||
await fetchWorldBooks();
|
||||
// 导入成功后选择新导入的世界书
|
||||
const importedBook = worldBooks.find(wb => wb.name === name);
|
||||
if (importedBook) {
|
||||
handleSelectWorldBook(importedBook);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('导入世界书失败:', err);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleExportWorldBook = async () => {
|
||||
if (!currentWorldBook) return;
|
||||
|
||||
try {
|
||||
await useWorldBookStore.getState().exportWorldBook(currentWorldBook.name);
|
||||
} catch (err) {
|
||||
console.error('导出世界书失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="worldbook-content">
|
||||
{/* 全局世界书区域 */}
|
||||
<div className="global-worldbooks-section">
|
||||
<div className="global-worldbooks-slot">
|
||||
<div className="global-books-header">
|
||||
<span className="title-text">全局世界书</span>
|
||||
{globalWorldBooks.length > 0 && (
|
||||
<div className="active-books-list">
|
||||
{globalWorldBooks.map(book => (
|
||||
<span
|
||||
key={book.uid}
|
||||
className="active-book-item"
|
||||
onClick={() => selectWorldBook(book.uid)}
|
||||
>
|
||||
{book.name}
|
||||
<span
|
||||
className="remove-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeGlobalWorldBook(book.uid);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮组 */}
|
||||
<div className="worldbook-actions">
|
||||
<button className="action-btn" onClick={handleCreateWorldBook}>
|
||||
+ 新建
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
📋 复制
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
📥 导入
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
📤 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 世界书选择区域 */}
|
||||
<div className="worldbook-selector">
|
||||
<div className="dropdown" style={{ flex: 1 }}>
|
||||
<button className="dropdown-btn" onClick={toggleWorldBookDropdown}>
|
||||
{selectedWorldBook ? selectedWorldBook.name : '选择世界书'}
|
||||
<span>▼</span>
|
||||
</button>
|
||||
{showWorldBookDropdown && (
|
||||
<div className="dropdown-menu">
|
||||
{worldBooks.map(book => (
|
||||
<div
|
||||
key={book.uid}
|
||||
className={`dropdown-item ${selectedWorldBook?.uid === book.uid ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
selectWorldBook(book.uid);
|
||||
toggleWorldBookDropdown(false);
|
||||
}}
|
||||
>
|
||||
{book.name}
|
||||
{/* 全局世界书区域 */}
|
||||
<div className="worldbook-selector-section">
|
||||
{/* 全局世界书展示区域 */}
|
||||
<div className="global-books-display">
|
||||
<div className="global-books-header">
|
||||
<span className="title-text">全局世界书</span>
|
||||
</div>
|
||||
{globalWorldBooks.length > 0 ? (
|
||||
<div className="global-books-list">
|
||||
{globalWorldBooks.map(book => (
|
||||
<div key={book.name} className="global-book-item">
|
||||
<span className="global-book-name">{book.name}</span>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleGlobal(book.name, false);
|
||||
}}
|
||||
title="取消全局"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-global-books">暂无全局世界书</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 世界书管理区域 */}
|
||||
<div className="worldbook-management">
|
||||
<div className="worldbook-header">
|
||||
<span className="title-text">世界书管理</span>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮组 */}
|
||||
<div className="worldbook-actions">
|
||||
<button className="action-btn" onClick={handleCreateWorldBook}>
|
||||
+ 新建
|
||||
</button>
|
||||
<button className="action-btn" onClick={handleImportWorldBook}>
|
||||
📥 导入
|
||||
</button>
|
||||
{currentWorldBook && (
|
||||
<button className="action-btn" onClick={handleExportWorldBook}>
|
||||
📤 导出
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 世界书选择区域 */}
|
||||
<div className="worldbook-selector">
|
||||
<div className="dropdown" style={{ flex: 1 }}>
|
||||
<button className="dropdown-btn" onClick={() => setShowWorldBookDropdown(!showWorldBookDropdown)}>
|
||||
{currentWorldBook ? currentWorldBook.name : '选择世界书'}
|
||||
<span>▼</span>
|
||||
</button>
|
||||
{showWorldBookDropdown && (
|
||||
<div className="dropdown-menu">
|
||||
{worldBooks.map(book => (
|
||||
<div
|
||||
key={book.name}
|
||||
className={`dropdown-item ${currentWorldBook?.name === book.name ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
// 防止点击复选框时触发选择世界书
|
||||
if (e.target.type !== 'checkbox') {
|
||||
handleSelectWorldBook(book);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={book.is_global}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggleGlobal(book.name, e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="book-name">{book.name}</span>
|
||||
{book.description && (
|
||||
<span className="book-desc">{book.description}</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentWorldBook && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteWorldBook}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 条目列表区域 */}
|
||||
{loading ? (
|
||||
<div className="loading">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="error">{error}</div>
|
||||
) : currentWorldBook ? (
|
||||
<div className="entries-container" style={{ maxHeight: 'calc(100vh - 400px)', overflowY: 'auto' }}>
|
||||
|
||||
{currentEntries.length > 0 ? (
|
||||
currentEntries.map(entry => (
|
||||
<div
|
||||
key={entry.uid}
|
||||
className={`entry-item ${currentEntry?.uid === entry.uid ? 'active' : ''}`}
|
||||
onClick={() => handleEntryClick(entry)}
|
||||
>
|
||||
<div className="entry-header">
|
||||
<span className="entry-name">
|
||||
{entry.comment || `条目 #${entry.uid}`}
|
||||
</span>
|
||||
<span className={`entry-status ${!entry.disable ? 'enabled' : ''}`}>
|
||||
{!entry.disable ? '启用' : '禁用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="entry-meta">
|
||||
<span>策略: {entry.trigger_strategy}</span>
|
||||
<span>位置: {entry.position}</span>
|
||||
<span>顺序: {entry.order}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="loading">暂无条目</div>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={handleAddEntry}>
|
||||
+ 添加条目
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="loading">请选择一个世界书</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedWorldBook && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => deleteWorldBook(selectedWorldBook.uid)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 条目列表区域 */}
|
||||
{isLoading ? (
|
||||
<div className="loading">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="error">{error}</div>
|
||||
) : selectedWorldBook ? (
|
||||
<div className="entries-container">
|
||||
{isSelecting ? (
|
||||
<div className="loading">加载条目中...</div>
|
||||
) : selectedWorldBook.entries && selectedWorldBook.entries.length > 0 ? (
|
||||
selectedWorldBook.entries.map(entry => (
|
||||
<div
|
||||
key={entry.uid}
|
||||
className={`entry-item ${editingEntry?.uid === entry.uid ? 'active' : ''}`}
|
||||
onClick={() => handleEntryClick(entry)}
|
||||
>
|
||||
<div className="entry-header">
|
||||
<span className="entry-name">{entry.name}</span>
|
||||
<span className={`entry-status ${entry.enabled ? 'enabled' : ''}`}>
|
||||
{entry.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="entry-meta">
|
||||
<span>策略: {entry.triggerStrategy}</span>
|
||||
<span>位置: {entry.insertPosition}</span>
|
||||
<span>顺序: {entry.order}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="loading">暂无条目</div>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={handleAddEntry}>
|
||||
+ 添加条目
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="loading">请选择一个世界书</div>
|
||||
)}
|
||||
|
||||
{/* 编辑面板 */}
|
||||
{showEditPanel && editingEntry && (
|
||||
{showEditPanel && currentEntry && (
|
||||
<div className={`edit-panel ${showEditPanel ? 'open' : ''}`}>
|
||||
<div className="edit-panel-header">
|
||||
<h2>编辑条目</h2>
|
||||
<button className="close-btn" onClick={() => toggleEditPanel(false)}>
|
||||
<button className="close-btn" onClick={() => setShowEditPanel(false)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">主关键词</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={currentEntry.trigger_config?.triggers?.keyword?.[1]?.key?.join(', ') || ''}
|
||||
onChange={(e) => {
|
||||
const updatedTriggerConfig = {
|
||||
...currentEntry.trigger_config,
|
||||
triggers: {
|
||||
...currentEntry.trigger_config?.triggers,
|
||||
keyword: [
|
||||
currentEntry.trigger_config?.triggers?.keyword?.[0] || false,
|
||||
{
|
||||
...currentEntry.trigger_config?.triggers?.keyword?.[1],
|
||||
key: e.target.value.split(',').map(k => k.trim())
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
handleEntryUpdate('trigger_config', updatedTriggerConfig);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">次要关键词</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={currentEntry.trigger_config?.triggers?.keyword?.[1]?.keysecondary?.join(', ') || ''}
|
||||
onChange={(e) => {
|
||||
const updatedTriggerConfig = {
|
||||
...currentEntry.trigger_config,
|
||||
triggers: {
|
||||
...currentEntry.trigger_config?.triggers,
|
||||
keyword: [
|
||||
currentEntry.trigger_config?.triggers?.keyword?.[0] || false,
|
||||
{
|
||||
...currentEntry.trigger_config?.triggers?.keyword?.[1],
|
||||
keysecondary: e.target.value.split(',').map(k => k.trim())
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
handleEntryUpdate('trigger_config', updatedTriggerConfig);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentEntry.trigger_config?.triggers?.keyword?.[1]?.selective || false}
|
||||
onChange={(e) => {
|
||||
const updatedTriggerConfig = {
|
||||
...currentEntry.trigger_config,
|
||||
triggers: {
|
||||
...currentEntry.trigger_config?.triggers,
|
||||
keyword: [
|
||||
currentEntry.trigger_config?.triggers?.keyword?.[0] || false,
|
||||
{
|
||||
...currentEntry.trigger_config?.triggers?.keyword?.[1],
|
||||
selective: e.target.checked
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
handleEntryUpdate('trigger_config', updatedTriggerConfig);
|
||||
}}
|
||||
/>
|
||||
选择性匹配
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentEntry.trigger_config?.triggers?.keyword?.[1]?.matchWholeWords || false}
|
||||
onChange={(e) => {
|
||||
const updatedTriggerConfig = {
|
||||
...currentEntry.trigger_config,
|
||||
triggers: {
|
||||
...currentEntry.trigger_config?.triggers,
|
||||
keyword: [
|
||||
currentEntry.trigger_config?.triggers?.keyword?.[0] || false,
|
||||
{
|
||||
...currentEntry.trigger_config?.triggers?.keyword?.[1],
|
||||
matchWholeWords: e.target.checked
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
handleEntryUpdate('trigger_config', updatedTriggerConfig);
|
||||
}}
|
||||
/>
|
||||
全词匹配
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentEntry.trigger_config?.triggers?.keyword?.[1]?.caseSensitive || false}
|
||||
onChange={(e) => {
|
||||
const updatedTriggerConfig = {
|
||||
...currentEntry.trigger_config,
|
||||
triggers: {
|
||||
...currentEntry.trigger_config?.triggers,
|
||||
keyword: [
|
||||
currentEntry.trigger_config?.triggers?.keyword?.[0] || false,
|
||||
{
|
||||
...currentEntry.trigger_config?.triggers?.keyword?.[1],
|
||||
caseSensitive: e.target.checked
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
handleEntryUpdate('trigger_config', updatedTriggerConfig);
|
||||
}}
|
||||
/>
|
||||
区分大小写
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">名称</label>
|
||||
<label className="form-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentEntry.useProbability}
|
||||
onChange={(e) => handleEntryUpdate('useProbability', e.target.checked)}
|
||||
/>
|
||||
使用概率判定
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{currentEntry.useProbability && (
|
||||
<div className="form-group">
|
||||
<label className="form-label">触发概率 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
min="0"
|
||||
max="100"
|
||||
value={currentEntry.probability}
|
||||
onChange={(e) => handleEntryUpdate('probability', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">分组</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={editingEntry.name}
|
||||
onChange={(e) => handleEntryUpdate('name', e.target.value)}
|
||||
value={currentEntry.group || ''}
|
||||
onChange={(e) => handleEntryUpdate('group', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">内容</label>
|
||||
<textarea
|
||||
className="form-textarea"
|
||||
value={editingEntry.content}
|
||||
onChange={(e) => handleEntryUpdate('content', e.target.value)}
|
||||
<label className="form-label">分组权重</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={currentEntry.groupWeight}
|
||||
onChange={(e) => handleEntryUpdate('groupWeight', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -249,52 +689,16 @@ const WorldBook = () => {
|
||||
<label className="form-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingEntry.enabled}
|
||||
onChange={(e) => handleEntryUpdate('enabled', e.target.checked)}
|
||||
checked={currentEntry.groupOverride}
|
||||
onChange={(e) => handleEntryUpdate('groupOverride', e.target.checked)}
|
||||
/>
|
||||
启用此条目
|
||||
覆盖分组限制
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">触发策略</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={editingEntry.triggerStrategy}
|
||||
onChange={(e) => handleEntryUpdate('triggerStrategy', e.target.value)}
|
||||
>
|
||||
<option value="persistent">持久</option>
|
||||
<option value="keyword">关键词</option>
|
||||
<option value="rag">RAG</option>
|
||||
<option value="calculation">运算</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">插入位置</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={editingEntry.insertPosition}
|
||||
onChange={(e) => handleEntryUpdate('insertPosition', e.target.value)}
|
||||
>
|
||||
<option value="before">之前</option>
|
||||
<option value="after">之后</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">顺序</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={editingEntry.order}
|
||||
onChange={(e) => handleEntryUpdate('order', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => deleteEntry(editingEntry.uid)}
|
||||
onClick={handleDeleteEntry}
|
||||
>
|
||||
删除条目
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user