修复导入顺序错误

This commit is contained in:
2026-05-05 19:08:25 +08:00
parent adb59da06d
commit 44df56c8d2
13 changed files with 4249 additions and 131 deletions

View File

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

View File

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