世界书部分基本完成

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;

View File

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