世界书部分基本完成

This commit is contained in:
2026-04-07 01:56:13 +08:00
parent e8dedb5ec4
commit 4f9cf4b725
10 changed files with 2505 additions and 862 deletions

View File

@@ -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;