修复导入顺序错误
This commit is contained in:
@@ -231,35 +231,40 @@ const usePresetStore = create(
|
||||
presets: [...state.presets, preset]
|
||||
})),
|
||||
|
||||
// 保存当前设置为预设
|
||||
// 保存当前设置为预设 - 使用 SillyTavern 标准格式
|
||||
saveCurrentAsPreset: async ({ name }) => {
|
||||
const state = get();
|
||||
try {
|
||||
// 构建预设数据 - 使用内部专有结构
|
||||
// 构建 SillyTavern 标准格式的预设数据
|
||||
const presetData = {
|
||||
// GenerationPreset 部分 - 采样参数
|
||||
id: `preset_${Date.now()}`,
|
||||
// 基本参数 - 使用 SillyTavern 标准字段名
|
||||
name: name,
|
||||
temperature: state.parameters.temperature,
|
||||
topP: state.parameters.top_p,
|
||||
topK: state.parameters.top_k,
|
||||
frequencyPenalty: state.parameters.frequency_penalty,
|
||||
presencePenalty: state.parameters.presence_penalty,
|
||||
maxLength: state.parameters.max_tokens,
|
||||
isDefault: false,
|
||||
frequency_penalty: state.parameters.frequency_penalty,
|
||||
presence_penalty: state.parameters.presence_penalty,
|
||||
top_p: state.parameters.top_p,
|
||||
top_k: state.parameters.top_k,
|
||||
max_tokens: state.parameters.max_tokens,
|
||||
request_timeout: state.parameters.request_timeout || 60,
|
||||
|
||||
// PromptPresetView 部分 - prompt组件列表
|
||||
characterId: 'global', // 全局预设,不绑定特定角色
|
||||
entries: state.promptComponents.map((component, index) => ({
|
||||
// SillyTavern 标准的 prompts 数组
|
||||
prompts: state.promptComponents.map((component) => ({
|
||||
identifier: component.identifier,
|
||||
name: component.name,
|
||||
enabled: component.enabled !== false,
|
||||
content: component.content || '',
|
||||
order: index,
|
||||
role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'ai',
|
||||
tokenCount: component.content ? component.content.length : 0,
|
||||
isSystemNode: component.marker || false
|
||||
}))
|
||||
system_prompt: component.role === 0,
|
||||
role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'assistant',
|
||||
enabled: component.enabled !== false
|
||||
})),
|
||||
|
||||
// prompt_order - SillyTavern 用于管理顺序和启用状态
|
||||
prompt_order: [{
|
||||
character_id: 'global',
|
||||
order: state.promptComponents.map(component => ({
|
||||
identifier: component.identifier,
|
||||
enabled: component.enabled !== false
|
||||
}))
|
||||
}]
|
||||
};
|
||||
|
||||
// 发送到后端
|
||||
@@ -279,7 +284,7 @@ const usePresetStore = create(
|
||||
|
||||
// 添加到本地预设列表
|
||||
const newPreset = {
|
||||
id: presetData.id,
|
||||
id: name, // 使用名称作为 ID
|
||||
name,
|
||||
description: '',
|
||||
component_count: state.promptComponents.length,
|
||||
@@ -288,7 +293,7 @@ const usePresetStore = create(
|
||||
|
||||
set((state) => ({
|
||||
presets: [...state.presets, newPreset],
|
||||
selectedPreset: presetData.id
|
||||
selectedPreset: name
|
||||
}));
|
||||
|
||||
return result;
|
||||
|
||||
@@ -33,13 +33,11 @@ const PresetPanel = () => {
|
||||
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||
const [showComponentEditDialog, setShowComponentEditDialog] = useState(false);
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
const [newPresetName, setNewPresetName] = useState('');
|
||||
const [editPresetId, setEditPresetId] = useState('');
|
||||
const [editPresetName, setEditPresetName] = useState('');
|
||||
const [importPresetData, setImportPresetData] = useState('');
|
||||
const [tooltip, setTooltip] = useState({ visible: false, content: '', x: 0, y: 0 });
|
||||
|
||||
// 组件编辑状态
|
||||
@@ -258,8 +256,6 @@ const PresetPanel = () => {
|
||||
// 重新加载预设列表
|
||||
setCurrentPage(1);
|
||||
fetchPresets();
|
||||
|
||||
alert('预设已删除!');
|
||||
} catch (error) {
|
||||
console.error('删除预设失败:', error);
|
||||
alert('删除预设失败: ' + error.message);
|
||||
@@ -301,9 +297,20 @@ const PresetPanel = () => {
|
||||
};
|
||||
|
||||
// 导入预设
|
||||
const handleImportPreset = async () => {
|
||||
const handleImportPreset = async (file) => {
|
||||
console.log('[Preset Import] 开始导入文件:', file.name);
|
||||
try {
|
||||
const importedPreset = JSON.parse(importPresetData);
|
||||
// 读取文件内容
|
||||
const importData = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target.result);
|
||||
reader.onerror = (e) => reject(new Error('读取文件失败'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
console.log('[Preset Import] 文件内容读取成功');
|
||||
const importedPreset = JSON.parse(importData);
|
||||
console.log('[Preset Import] JSON 解析成功');
|
||||
|
||||
// 支持内部结构和SillyTavern结构
|
||||
let presetName = '';
|
||||
@@ -313,7 +320,8 @@ const PresetPanel = () => {
|
||||
// 检测是否为内部结构(有 entries 字段)
|
||||
if (importedPreset.entries && Array.isArray(importedPreset.entries)) {
|
||||
// 内部结构
|
||||
presetName = importedPreset.name;
|
||||
presetName = importedPreset.name || file.name.replace('.json', '');
|
||||
console.log('[Preset Import] 检测到内部格式, preset name:', presetName);
|
||||
importedParameters = {
|
||||
temperature: importedPreset.temperature,
|
||||
top_p: importedPreset.topP,
|
||||
@@ -339,9 +347,13 @@ const PresetPanel = () => {
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
// 兼容SillyTavern结构(有 parameters 和 promptComponents 字段)
|
||||
else if (importedPreset.name && (importedPreset.parameters || importedPreset.temperature)) {
|
||||
presetName = importedPreset.name;
|
||||
// 兼容SillyTavern结构(有 prompts 或 parameters 字段)
|
||||
else if (importedPreset.name || importedPreset.temperature || importedPreset.parameters || importedPreset.prompts) {
|
||||
// SillyTavern 格式通常没有 name 字段,使用文件名
|
||||
presetName = importedPreset.name || file.name.replace('.json', '');
|
||||
console.log('[Preset Import] 检测到 SillyTavern 格式, preset name:', presetName);
|
||||
|
||||
// 提取参数
|
||||
importedParameters = importedPreset.parameters || {
|
||||
temperature: importedPreset.temperature,
|
||||
frequency_penalty: importedPreset.frequency_penalty,
|
||||
@@ -351,29 +363,128 @@ const PresetPanel = () => {
|
||||
max_tokens: importedPreset.max_tokens || importedPreset.openai_max_tokens,
|
||||
request_timeout: importedPreset.request_timeout || 60
|
||||
};
|
||||
importedComponents = importedPreset.promptComponents || [];
|
||||
|
||||
// 转换 SillyTavern 的 prompts 为内部格式
|
||||
if (importedPreset.prompts && Array.isArray(importedPreset.prompts)) {
|
||||
console.log('[Preset Import] 找到', importedPreset.prompts.length, '个 prompts');
|
||||
|
||||
// 获取 prompt_order(支持多个 prompt_order,优先使用最后一个或组件数最多的)
|
||||
let promptOrder = [];
|
||||
if (importedPreset.prompt_order && Array.isArray(importedPreset.prompt_order)) {
|
||||
// 策略:优先使用最后一个 prompt_order(通常是角色特定的,包含更多组件)
|
||||
// 如果只有一个,直接使用
|
||||
if (importedPreset.prompt_order.length === 1) {
|
||||
if (importedPreset.prompt_order[0].order) {
|
||||
promptOrder = importedPreset.prompt_order[0].order;
|
||||
console.log('[Preset Import] 使用唯一的 prompt_order');
|
||||
}
|
||||
} else if (importedPreset.prompt_order.length > 1) {
|
||||
// 有多个时,使用最后一个(通常是最完整的)
|
||||
const lastOrder = importedPreset.prompt_order[importedPreset.prompt_order.length - 1];
|
||||
if (lastOrder && lastOrder.order) {
|
||||
promptOrder = lastOrder.order;
|
||||
console.log('[Preset Import] 使用最后一个 prompt_order (character_id:', lastOrder.character_id, ', 组件数:', promptOrder.length, ')');
|
||||
}
|
||||
|
||||
console.log('[Preset Import] 检测到', importedPreset.prompt_order.length, '个 prompt_order');
|
||||
importedPreset.prompt_order.forEach((po, idx) => {
|
||||
console.log(` [${idx}] character_id: ${po.character_id}, order_count: ${po.order?.length || 0}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 identifier 到 enabled 状态的映射
|
||||
const enabledMap = {};
|
||||
promptOrder.forEach(item => {
|
||||
if (item && item.identifier) {
|
||||
enabledMap[item.identifier] = item.enabled !== false;
|
||||
}
|
||||
});
|
||||
|
||||
// 转换每个 prompt
|
||||
importedComponents = importedPreset.prompts.map((prompt, index) => {
|
||||
// 确定角色:system_prompt=true -> 0 (system), 否则根据 role 字段
|
||||
let role = 0; // 默认 system
|
||||
if (prompt.role !== undefined) {
|
||||
role = typeof prompt.role === 'string'
|
||||
? (prompt.role === 'system' ? 0 : prompt.role === 'user' ? 1 : 2)
|
||||
: prompt.role;
|
||||
} else if (prompt.system_prompt) {
|
||||
role = 0; // system
|
||||
} else {
|
||||
role = 1; // user
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: prompt.identifier || `prompt_${index}`,
|
||||
name: prompt.name || '未命名组件',
|
||||
content: prompt.content || '',
|
||||
enabled: enabledMap[prompt.identifier] !== undefined
|
||||
? enabledMap[prompt.identifier]
|
||||
: (prompt.enabled !== false),
|
||||
role: role,
|
||||
system_prompt: role === 0,
|
||||
marker: prompt.marker || false
|
||||
};
|
||||
});
|
||||
|
||||
// 如果有 prompt_order,按顺序排序
|
||||
if (promptOrder.length > 0) {
|
||||
const orderMap = {};
|
||||
promptOrder.forEach((item, idx) => {
|
||||
if (item && item.identifier) {
|
||||
orderMap[item.identifier] = idx;
|
||||
}
|
||||
});
|
||||
|
||||
importedComponents.sort((a, b) => {
|
||||
const orderA = orderMap[a.identifier] !== undefined ? orderMap[a.identifier] : 999;
|
||||
const orderB = orderMap[b.identifier] !== undefined ? orderMap[b.identifier] : 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[Preset Import] 转换后组件数量:', importedComponents.length);
|
||||
} else {
|
||||
console.log('[Preset Import] 未找到 prompts 字段');
|
||||
importedComponents = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (presetName) {
|
||||
await saveCurrentAsPreset({ name: presetName });
|
||||
console.log('[Preset Import] 准备应用导入的配置');
|
||||
|
||||
// 更新参数
|
||||
// 先更新参数
|
||||
console.log('[Preset Import] 更新参数:', importedParameters);
|
||||
Object.keys(importedParameters).forEach(key => {
|
||||
if (importedParameters[key] !== undefined) {
|
||||
updateParameter({ name: key, value: importedParameters[key] });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新组件列表
|
||||
// 再更新组件列表
|
||||
if (importedComponents.length > 0) {
|
||||
console.log('[Preset Import] 更新组件列表:', importedComponents.length, '个组件');
|
||||
setPromptComponents(importedComponents);
|
||||
}
|
||||
|
||||
setImportPresetData('');
|
||||
setShowImportDialog(false);
|
||||
// 重新加载预设列表并重置到第一页
|
||||
setCurrentPage(1);
|
||||
fetchPresets();
|
||||
|
||||
// 使用 requestAnimationFrame 确保状态更新完成后再保存
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(async () => {
|
||||
console.log('[Preset Import] 准备保存预设:', presetName);
|
||||
try {
|
||||
await saveCurrentAsPreset({ name: presetName });
|
||||
console.log('[Preset Import] 预设保存成功');
|
||||
|
||||
// 重新加载预设列表并重置到第一页
|
||||
setCurrentPage(1);
|
||||
fetchPresets();
|
||||
} catch (error) {
|
||||
console.error('[Preset Import] 保存预设失败:', error);
|
||||
alert('保存预设失败: ' + error.message);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导入预设失败:', error);
|
||||
@@ -381,38 +492,43 @@ const PresetPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 导出预设
|
||||
// 导出预设 - 使用 SillyTavern 标准格式
|
||||
const handleExportPreset = () => {
|
||||
if (selectedPreset) {
|
||||
const preset = presets.find(p => p.id === selectedPreset);
|
||||
if (preset) {
|
||||
// 使用内部专有结构导出
|
||||
// 构建 SillyTavern 标准格式
|
||||
const exportData = {
|
||||
// GenerationPreset 部分
|
||||
id: selectedPreset,
|
||||
// 基本参数
|
||||
name: preset.name,
|
||||
temperature: parameters.temperature,
|
||||
topP: parameters.top_p,
|
||||
topK: parameters.top_k,
|
||||
frequencyPenalty: parameters.frequency_penalty,
|
||||
presencePenalty: parameters.presence_penalty,
|
||||
maxLength: parameters.max_tokens,
|
||||
requestTimeout: parameters.request_timeout || 60,
|
||||
isDefault: false,
|
||||
frequency_penalty: parameters.frequency_penalty,
|
||||
presence_penalty: parameters.presence_penalty,
|
||||
top_p: parameters.top_p,
|
||||
top_k: parameters.top_k,
|
||||
max_tokens: parameters.max_tokens,
|
||||
request_timeout: parameters.request_timeout || 60,
|
||||
|
||||
// PromptPresetView 部分
|
||||
characterId: 'global',
|
||||
entries: promptComponents.map((component, index) => ({
|
||||
// SillyTavern 标准的 prompts 数组
|
||||
prompts: promptComponents.map((component) => ({
|
||||
identifier: component.identifier,
|
||||
name: component.name,
|
||||
enabled: component.enabled !== false,
|
||||
content: component.content || '',
|
||||
order: index,
|
||||
role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'ai',
|
||||
tokenCount: component.content ? component.content.length : 0,
|
||||
isSystemNode: component.marker || false
|
||||
}))
|
||||
system_prompt: component.role === 0,
|
||||
role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'assistant',
|
||||
enabled: component.enabled !== false
|
||||
})),
|
||||
|
||||
// prompt_order - SillyTavern 用于管理顺序和启用状态
|
||||
prompt_order: [{
|
||||
character_id: 'global',
|
||||
order: promptComponents.map(component => ({
|
||||
identifier: component.identifier,
|
||||
enabled: component.enabled !== false
|
||||
}))
|
||||
}]
|
||||
};
|
||||
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
@@ -622,8 +738,30 @@ const PresetPanel = () => {
|
||||
className="dropdown-item"
|
||||
title="从 JSON 文件导入预设配置"
|
||||
onClick={() => {
|
||||
setShowImportDialog(true);
|
||||
setShowActionsMenu(false);
|
||||
console.log('[Preset Import] 点击导入按钮');
|
||||
// 创建文件选择器
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
console.log('[Preset Import] 文件选择器触发');
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
console.log('[Preset Import] 未选择文件');
|
||||
return;
|
||||
}
|
||||
console.log('[Preset Import] 选择的文件:', file.name);
|
||||
|
||||
try {
|
||||
await handleImportPreset(file);
|
||||
console.log('[Preset Import] 导入完成');
|
||||
setShowActionsMenu(false);
|
||||
} catch (error) {
|
||||
console.error('[Preset Import] 导入失败:', error);
|
||||
alert('导入预设失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
>
|
||||
📥 导入
|
||||
@@ -690,22 +828,6 @@ const PresetPanel = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 导入预设对话框 */}
|
||||
{showImportDialog && (
|
||||
<div className="preset-import-dialog">
|
||||
<textarea
|
||||
className="import-textarea"
|
||||
value={importPresetData}
|
||||
onChange={(e) => setImportPresetData(e.target.value)}
|
||||
placeholder="粘贴预设JSON数据"
|
||||
rows="5"
|
||||
/>
|
||||
<div className="dialog-buttons">
|
||||
<button onClick={handleImportPreset}>导入</button>
|
||||
<button onClick={() => setShowImportDialog(false)}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑/查看组件内容对话框 */}
|
||||
{showComponentEditDialog && (
|
||||
|
||||
Reference in New Issue
Block a user