feat: Agent 预设系统 - 一键切换角色/工作模式

- 内置 6 个预设:默认、翻译官、代码审查、写作助手、数据分析师、学习导师
- 每个预设包含:系统提示词、温度、上下文长度、Think 开关
- 预设栏胶囊按钮,点击即切换,自动应用全部配置
- 支持创建/编辑/删除自定义预设,IndexedDB 持久化
- 切换预设时同步设置面板 UI + toast 提示
- 温度参数集成到聊天 API 调用
This commit is contained in:
thzxx
2026-04-05 19:55:45 +08:00
parent 2c475c0c8c
commit bc63ba9611
6 changed files with 752 additions and 1 deletions
+160
View File
@@ -372,6 +372,166 @@ body::before {
border: 1px solid rgba(0, 245, 212, 0.2);
}
/* ── 预设栏 ── */
.preset-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-glass);
flex-shrink: 0;
overflow: hidden;
}
.preset-list {
display: flex;
gap: 6px;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
padding: 2px 0;
}
.preset-list::-webkit-scrollbar { display: none; }
.preset-chip {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
border-radius: 20px;
border: 1px solid var(--border-glass);
background: var(--bg-glass-light);
color: var(--text-secondary);
font-size: 12px;
font-weight: 500;
font-family: var(--font-sans);
white-space: nowrap;
cursor: pointer;
transition: var(--transition);
flex-shrink: 0;
user-select: none;
}
.preset-chip:hover {
background: var(--bg-glass-hover);
border-color: rgba(0, 245, 212, 0.3);
color: var(--text-primary);
transform: translateY(-1px);
}
.preset-chip.active {
background: var(--gradient-main);
border-color: transparent;
color: #fff;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0, 245, 212, 0.25);
}
.preset-chip.active:hover {
box-shadow: 0 4px 16px rgba(0, 245, 212, 0.35);
transform: translateY(-1px);
}
.preset-chip-icon {
font-size: 14px;
line-height: 1;
}
.preset-chip-name {
line-height: 1;
}
.preset-add-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px dashed var(--border-glass);
background: transparent;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
flex-shrink: 0;
}
.preset-add-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
background: rgba(0, 245, 212, 0.06);
transform: scale(1.1);
}
.preset-add-btn svg {
width: 16px;
height: 16px;
}
/* ── 预设模态框 ── */
.preset-name-row {
display: flex;
gap: 8px;
align-items: center;
}
.preset-icon-input {
width: 56px !important;
text-align: center;
font-size: 18px;
flex-shrink: 0;
padding: 8px 4px !important;
}
.preset-temp-slider {
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 6px;
border-radius: 3px;
background: var(--bg-glass-light);
outline: none;
margin: 8px 0;
}
.preset-temp-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--gradient-main);
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 245, 212, 0.3);
transition: transform 0.15s ease;
}
.preset-temp-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.preset-temp-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--gradient-main);
cursor: pointer;
border: none;
box-shadow: 0 2px 8px rgba(0, 245, 212, 0.3);
}
.preset-modal-actions {
display: flex;
gap: 8px;
align-items: center;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border-glass);
}
/* ── Think 按钮(输入栏内) ── */
.think-toggle {
display: flex;
+53
View File
@@ -366,6 +366,59 @@
</div>
</div>
<!-- ═══════════════ 预设管理模态框 ═══════════════ -->
<div class="modal-overlay" id="presetModal" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3 id="presetModalTitle">✨ 新建预设</h3>
<button class="icon-btn" id="btnClosePresetModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="setting-group">
<label class="setting-label">名称</label>
<div class="preset-name-row">
<input class="setting-input preset-icon-input" id="inputPresetIcon" type="text" maxlength="4" placeholder="🤖">
<input class="setting-input" id="inputPresetName" type="text" placeholder="例如:翻译官" style="flex:1;">
</div>
</div>
<div class="setting-group">
<label class="setting-label">系统提示词(System Prompt</label>
<textarea class="setting-input" id="inputPresetSystemPrompt" rows="6" placeholder="定义这个角色的行为、能力和回答风格..." style="resize:vertical;min-height:120px;"></textarea>
</div>
<div class="setting-group">
<label class="setting-label">温度(Temperature):<span id="presetTempValue">0.7</span></label>
<input type="range" id="inputPresetTemp" class="preset-temp-slider" min="0" max="2" step="0.1" value="0.7">
<p class="text-muted" style="font-size:11px;margin-top:4px;">低值更精确稳定,高值更有创意多样(0 = 确定性,2 = 最大随机)</p>
</div>
<div class="setting-group">
<label class="setting-label">上下文长度(num_ctx</label>
<div style="display:flex;gap:8px;align-items:center;">
<input class="setting-input" id="inputPresetNumCtx" type="number" min="512" max="131072" step="512" value="24576" style="margin-bottom:0;">
<span class="text-muted" style="white-space:nowrap;">tokens</span>
</div>
</div>
<div class="setting-group">
<label class="setting-label">Think 深度推理</label>
<label class="toggle-label" style="display:inline-flex;align-items:center;gap:8px;">
<input type="checkbox" id="togglePresetThink">
<span class="toggle-slider"></span>
<span class="text-muted" style="font-size:12px;">需要模型支持</span>
</label>
</div>
<div class="preset-modal-actions">
<button class="btn btn-danger" id="btnDeletePreset" style="display:none;">🗑️ 删除</button>
<div style="flex:1;"></div>
<button class="btn btn-outline" id="btnClosePresetModal2" onclick="document.querySelector('#presetModal').style.display='none';">取消</button>
<button class="btn btn-primary" id="btnSavePreset">创建</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════ 图片预览 Lightbox ═══════════════ -->
<div class="lightbox-overlay" id="lightbox" style="display:none;">
<button class="lightbox-close" id="lightboxClose"></button>
+10
View File
@@ -18,6 +18,8 @@ import { initInputArea } from './components/input-area.js';
import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js';
import { initHistoryModal, closeHistoryModal } from './components/history-modal.js';
import { initKBModal } from './components/kb-modal.js';
import { initPresetBar, initPresetModal } from './components/preset-bar.js';
import { initPresetManager } from './preset-manager.js';
import { initVectorStore } from './rag.js';
function initHelpModal() {
@@ -115,6 +117,8 @@ async function init() {
initSettingsModal();
initHistoryModal();
initKBModal();
initPresetBar();
initPresetModal();
initHelpModal();
// 5. 绑定全局事件
@@ -127,6 +131,9 @@ async function init() {
// 6.1 初始化向量存储
await initVectorStore();
// 6.2 初始化预设管理器(会从 DB 加载并应用上次激活的预设)
await initPresetManager();
// 7. 恢复上次选择的模型
const savedModel = await db.getSetting('selectedModel', '');
if (savedModel) {
@@ -173,6 +180,9 @@ async function init() {
initInputArea();
initSettingsModal();
initHistoryModal();
initPresetBar();
initPresetModal();
initPresetManager();
checkConnection().then(loadModels);
}
}
+3 -1
View File
@@ -402,12 +402,14 @@ export async function sendMessage() {
try {
const apiMessages = buildApiMessages(freshSession.messages);
const temperature = state.get('temperature', 0.7);
const chatParams = {
model,
messages: apiMessages,
think: thinkMode,
...(systemPromptEnabled && systemPrompt && { system: systemPrompt }),
...(numCtx && { options: { num_ctx: numCtx } })
...(numCtx && { options: { num_ctx: numCtx, temperature } })
};
// RAG 检索增强
+212
View File
@@ -0,0 +1,212 @@
/**
* PresetBar - 预设栏组件
*
* 在模型栏下方显示预设胶囊按钮列表,点击即切换
* 右侧"+"按钮打开管理面板
*/
import { state, KEYS } from '../state.js';
import {
getPresets, getActivePresetId, activatePreset,
createPreset, updatePreset, deletePreset
} from '../preset-manager.js';
import { showToast } from './toast.js';
let presetBarEl;
let presetListEl;
let editingPresetId = null;
export function initPresetBar() {
// 创建预设栏 DOM
const modelBar = document.querySelector('.model-bar');
if (!modelBar) return;
const bar = document.createElement('div');
bar.className = 'preset-bar';
bar.innerHTML = `
<div class="preset-list" id="presetList"></div>
<button class="preset-add-btn" id="btnAddPreset" title="管理预设">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
`;
modelBar.after(bar);
presetBarEl = bar;
presetListEl = bar.querySelector('#presetList');
// 点击"+"打开管理面板
bar.querySelector('#btnAddPreset').addEventListener('click', openPresetModal);
// 监听预设变化,刷新 UI
state.on('activePresetId', renderPresetBar);
state.on('presets', renderPresetBar);
// 初始渲染
renderPresetBar();
}
/**
* 渲染预设胶囊列表
*/
function renderPresetBar() {
if (!presetListEl) return;
const presets = getPresets();
const activeId = getActivePresetId();
presetListEl.innerHTML = '';
for (const preset of presets) {
const chip = document.createElement('button');
chip.className = 'preset-chip' + (preset.id === activeId ? ' active' : '');
chip.title = preset.systemPrompt ? preset.systemPrompt.slice(0, 100) : '无系统提示词';
chip.innerHTML = `<span class="preset-chip-icon">${preset.icon}</span><span class="preset-chip-name">${escapeHtml(preset.name)}</span>`;
chip.addEventListener('click', () => handlePresetClick(preset.id));
// 右键编辑(仅自定义)
chip.addEventListener('contextmenu', (e) => {
if (!preset.builtIn) {
e.preventDefault();
editingPresetId = preset.id;
openPresetModal(preset);
}
});
presetListEl.appendChild(chip);
}
}
async function handlePresetClick(presetId) {
const currentId = getActivePresetId();
if (presetId === currentId) return; // 已激活,不重复切换
await activatePreset(presetId);
const preset = getPresets().find(p => p.id === presetId);
if (preset) {
showToast(`已切换到 ${preset.icon} ${preset.name}`, 'success', 2000);
}
}
// ═══════════════════════════════════════
// 预设管理模态框
// ═══════════════════════════════════════
function openPresetModal(editPreset) {
const modal = document.querySelector('#presetModal');
const titleEl = modal.querySelector('#presetModalTitle');
const nameInput = modal.querySelector('#inputPresetName');
const iconInput = modal.querySelector('#inputPresetIcon');
const spInput = modal.querySelector('#inputPresetSystemPrompt');
const tempInput = modal.querySelector('#inputPresetTemp');
const tempVal = modal.querySelector('#presetTempValue');
const ctxInput = modal.querySelector('#inputPresetNumCtx');
const thinkCheck = modal.querySelector('#togglePresetThink');
const btnSave = modal.querySelector('#btnSavePreset');
const btnDelete = modal.querySelector('#btnDeletePreset');
if (editPreset && editPreset.id) {
// 编辑模式
editingPresetId = editPreset.id;
titleEl.textContent = '✏️ 编辑预设';
nameInput.value = editPreset.name;
iconInput.value = editPreset.icon;
spInput.value = editPreset.systemPrompt;
tempInput.value = editPreset.temperature ?? 0.7;
tempVal.textContent = (editPreset.temperature ?? 0.7).toFixed(1);
ctxInput.value = editPreset.numCtx || 24576;
thinkCheck.checked = editPreset.think ?? false;
btnDelete.style.display = editPreset.builtIn ? 'none' : '';
btnSave.textContent = '保存';
} else {
// 新建模式
editingPresetId = null;
titleEl.textContent = '✨ 新建预设';
nameInput.value = '';
iconInput.value = '🤖';
spInput.value = '';
tempInput.value = 0.7;
tempVal.textContent = '0.7';
ctxInput.value = 24576;
thinkCheck.checked = false;
btnDelete.style.display = 'none';
btnSave.textContent = '创建';
}
modal.style.display = '';
nameInput.focus();
// 温度滑块实时更新
tempInput.oninput = () => {
tempVal.textContent = parseFloat(tempInput.value).toFixed(1);
};
}
export function initPresetModal() {
const modal = document.querySelector('#presetModal');
if (!modal) return;
// 关闭
modal.querySelector('#btnClosePresetModal').addEventListener('click', () => {
modal.style.display = 'none';
});
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.style.display = 'none';
});
// 保存
modal.querySelector('#btnSavePreset').addEventListener('click', async () => {
const name = modal.querySelector('#inputPresetName').value.trim();
if (!name) {
showToast('请输入预设名称', 'warning');
return;
}
const data = {
name,
icon: modal.querySelector('#inputPresetIcon').value.trim() || '🤖',
systemPrompt: modal.querySelector('#inputPresetSystemPrompt').value.trim(),
temperature: parseFloat(modal.querySelector('#inputPresetTemp').value) || 0.7,
numCtx: parseInt(modal.querySelector('#inputPresetNumCtx').value) || 24576,
think: modal.querySelector('#togglePresetThink').checked
};
if (editingPresetId) {
await updatePreset(editingPresetId, data);
showToast('预设已更新', 'success', 2000);
// 如果更新的是当前激活预设,重新应用
if (editingPresetId === getActivePresetId()) {
await activatePreset(editingPresetId);
}
} else {
const created = await createPreset(data);
// 自动激活新建的预设
await activatePreset(created.id);
showToast(`已创建并激活 ${data.icon} ${data.name}`, 'success', 2000);
}
modal.style.display = 'none';
editingPresetId = null;
});
// 删除
modal.querySelector('#btnDeletePreset').addEventListener('click', async () => {
if (!editingPresetId) return;
const preset = getPresets().find(p => p.id === editingPresetId);
if (!preset) return;
if (confirm(`确定删除预设 "${preset.icon} ${preset.name}" `)) {
await deletePreset(editingPresetId);
showToast('预设已删除', 'info', 2000);
modal.style.display = 'none';
editingPresetId = null;
}
});
}
function escapeHtml(str) {
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
return String(str).replace(/[&<>"']/g, c => map[c]);
}
+314
View File
@@ -0,0 +1,314 @@
/**
* PresetManager - Agent 预设管理
*
* 预设数据结构:
* {
* id: string,
* name: string, // 预设名称,如 "翻译官"
* icon: string, // emoji 图标
* systemPrompt: string, // 系统提示词
* temperature: number, // 温度 (0~2)
* numCtx: number, // 上下文长度
* think: boolean, // Think 开关
* createdAt: number,
* updatedAt: number,
* }
*/
import { state, KEYS } from './state.js';
import { generateId } from './utils.js';
const PRESETS_KEY = 'agentPresets';
const ACTIVE_PRESET_KEY = 'activePresetId';
// ── 内置预设 ──
const BUILT_IN_PRESETS = [
{
id: '_default',
name: '默认',
icon: '💬',
systemPrompt: '',
temperature: 0.7,
numCtx: 24576,
think: false,
builtIn: true,
createdAt: 0,
updatedAt: 0
},
{
id: '_translator',
name: '翻译官',
icon: '🌐',
systemPrompt: '你是一位专业翻译官。你的任务是将用户输入的内容准确翻译为目标语言。\n\n规则:\n1. 自动识别源语言,翻译为用户指定的目标语言(如未指定则翻译为中文)\n2. 保持原文的格式、语气和风格\n3. 专有名词保留原文并给出通用译名\n4. 对于有多种含义的词语,结合上下文选择最合适的翻译\n5. 直接输出翻译结果,不需要额外解释(除非用户要求)',
temperature: 0.3,
numCtx: 16384,
think: false,
builtIn: true,
createdAt: 0,
updatedAt: 0
},
{
id: '_code_review',
name: '代码审查',
icon: '🔍',
systemPrompt: '你是一位资深代码审查专家。对用户提交的代码进行全面审查。\n\n审查维度:\n1. **Bug 与逻辑错误** — 潜在的运行时错误、边界条件、空指针等\n2. **安全性** — SQL注入、XSS、敏感信息泄露等安全隐患\n3. **性能** — 时间/空间复杂度、不必要的计算、内存泄漏\n4. **可读性** — 命名规范、代码结构、注释质量\n5. **最佳实践** — 设计模式、语言惯用法、框架约定\n\n输出格式:\n- 按严重程度排列问题(严重 → 轻微)\n- 每个问题给出:位置、问题描述、修复建议、改进后的代码\n- 最后给出整体评价和改进建议',
temperature: 0.2,
numCtx: 32768,
think: true,
builtIn: true,
createdAt: 0,
updatedAt: 0
},
{
id: '_writer',
name: '写作助手',
icon: '✍️',
systemPrompt: '你是一位才华横溢的写作助手。擅长多种文体和写作风格。\n\n能力范围:\n1. 文章撰写 — 议论文、说明文、散文、故事\n2. 文案创作 — 广告文案、产品描述、社交媒体文案\n3. 内容润色 — 修改语法、优化措辞、提升表达力\n4. 风格调整 — 正式/口语/文艺/简洁等多种风格\n5. 创意写作 — 小说、诗歌、剧本、对话\n\n工作方式:\n- 仔细理解用户的写作需求(读者、用途、风格偏好)\n- 如需信息不全,主动询问确认\n- 提供多种版本供选择\n- 解释重要的写作决策',
temperature: 0.9,
numCtx: 24576,
think: false,
builtIn: true,
createdAt: 0,
updatedAt: 0
},
{
id: '_analyst',
name: '数据分析师',
icon: '📊',
systemPrompt: '你是一位专业的数据分析师。擅长从数据中提取洞察并给出可执行的建议。\n\n分析方法:\n1. 数据概览 — 数据量、字段类型、缺失值、异常值\n2. 统计分析 — 描述性统计、相关性分析、趋势分析\n3. 可视化建议 — 推荐合适的图表类型和展示方式\n4. 洞察提取 — 关键发现、模式识别、因果推断\n5. 行动建议 — 基于数据的具体可执行建议\n\n输出格式:\n- 先给出核心结论(Executive Summary\n- 再展开详细分析过程\n- 使用表格和列表组织数据\n- 给出代码(Python/SQL)时附带注释',
temperature: 0.4,
numCtx: 32768,
think: true,
builtIn: true,
createdAt: 0,
updatedAt: 0
},
{
id: '_tutor',
name: '学习导师',
icon: '🎓',
systemPrompt: '你是一位耐心且善于启发的学习导师。用苏格拉底式教学法帮助用户深入理解知识。\n\n教学原则:\n1. **先问后答** — 通过引导性问题帮助用户自己找到答案\n2. **由浅入深** — 从基础概念开始,逐步构建知识体系\n3. **类比教学** — 用生活中的类比解释抽象概念\n4. **实践导向** — 每个知识点配一个动手练习\n5. **鼓励探索** — 提供延伸阅读方向和思考题\n\n当用户提问时:\n- 先评估用户当前理解水平\n- 用引导性问题帮助思考\n- 如果用户确实卡住,再给出答案并解释思路\n- 每个概念学完后给出一个小练习',
temperature: 0.6,
numCtx: 24576,
think: true,
builtIn: true,
createdAt: 0,
updatedAt: 0
}
];
let presets = [];
let activePresetId = null;
/**
* 初始化预设管理器
*/
export async function initPresetManager() {
const db = state.get(KEYS.DB);
// 从 DB 加载用户自定义预设
let userPresets = [];
if (db) {
userPresets = await db.getSetting(PRESETS_KEY, []);
}
// 合并:内置 + 用户自定义
presets = [...BUILT_IN_PRESETS, ...userPresets];
// 加载上次激活的预设
if (db) {
activePresetId = await db.getSetting(ACTIVE_PRESET_KEY, '_default');
} else {
activePresetId = '_default';
}
// 写入 state
state.set('presets', presets);
state.set('activePresetId', activePresetId);
// 应用当前预设(恢复设置)
const activePreset = presets.find(p => p.id === activePresetId);
if (activePreset && activePreset.id !== '_default') {
applyPresetToState(activePreset, false); // 不再 DB,已在 init 阶段加载
}
}
/**
* 获取所有预设
*/
export function getPresets() {
return presets;
}
/**
* 获取当前激活预设
*/
export function getActivePreset() {
return presets.find(p => p.id === activePresetId) || presets[0];
}
/**
* 获取当前激活预设 ID
*/
export function getActivePresetId() {
return activePresetId;
}
/**
* 激活预设
*/
export async function activatePreset(presetId) {
const preset = presets.find(p => p.id === presetId);
if (!preset) return;
activePresetId = presetId;
state.set('activePresetId', presetId);
applyPresetToState(preset, true);
const db = state.get(KEYS.DB);
if (db) {
await db.saveSetting(ACTIVE_PRESET_KEY, presetId);
}
}
/**
* 将预设配置应用到全局 state
*/
function applyPresetToState(preset, saveToDb) {
const db = state.get(KEYS.DB);
// 系统提示词
if (preset.systemPrompt) {
state.set(KEYS.SYSTEM_PROMPT_ENABLED, true);
state.set(KEYS.SYSTEM_PROMPT, preset.systemPrompt);
} else {
state.set(KEYS.SYSTEM_PROMPT_ENABLED, false);
state.set(KEYS.SYSTEM_PROMPT, '');
}
// 上下文长度
if (preset.numCtx) {
state.set(KEYS.NUM_CTX, preset.numCtx);
}
// 温度(存储到 state,聊天时使用)
state.set('temperature', preset.temperature ?? 0.7);
// Think 开关
state.set('thinkEnabled', preset.think ?? false);
// 同步到 UI
syncPresetToUI(preset);
// 持久化
if (saveToDb && db) {
db.saveSetting('systemPromptEnabled', preset.systemPrompt ? true : false);
db.saveSetting('systemPrompt', preset.systemPrompt || '');
db.saveSetting('numCtx', preset.numCtx || 24576);
db.saveSetting('temperature', preset.temperature ?? 0.7);
}
}
/**
* 将预设配置同步到设置面板 UI
*/
function syncPresetToUI(preset) {
// 系统提示词
const toggleSP = document.querySelector('#toggleSystemPrompt');
const inputSP = document.querySelector('#inputSystemPrompt');
if (toggleSP) toggleSP.checked = !!preset.systemPrompt;
if (inputSP) {
inputSP.disabled = !preset.systemPrompt;
inputSP.value = preset.systemPrompt || '';
}
// numCtx
const inputCtx = document.querySelector('#inputNumCtx');
if (inputCtx) inputCtx.value = preset.numCtx || 24576;
// Think toggle
const thinkCheckbox = document.querySelector('#toggleThink');
if (thinkCheckbox) {
thinkCheckbox.checked = preset.think ?? false;
// 触发 change 事件让 model-bar 响应
thinkCheckbox.dispatchEvent(new Event('change'));
}
}
/**
* 创建预设
*/
export async function createPreset({ name, icon, systemPrompt, temperature, numCtx, think }) {
const preset = {
id: `preset_${generateId()}`,
name: name || '新预设',
icon: icon || '🤖',
systemPrompt: systemPrompt || '',
temperature: temperature ?? 0.7,
numCtx: numCtx ?? 24576,
think: think ?? false,
createdAt: Date.now(),
updatedAt: Date.now()
};
presets.push(preset);
state.set('presets', [...presets]);
await saveUserPresets();
return preset;
}
/**
* 更新预设
*/
export async function updatePreset(presetId, updates) {
const idx = presets.findIndex(p => p.id === presetId);
if (idx === -1) return;
// 内置预设不允许修改
if (presets[idx].builtIn) return;
presets[idx] = { ...presets[idx], ...updates, updatedAt: Date.now() };
state.set('presets', [...presets]);
// 如果更新的是当前激活预设,重新应用
if (activePresetId === presetId) {
applyPresetToState(presets[idx], true);
}
await saveUserPresets();
}
/**
* 删除预设
*/
export async function deletePreset(presetId) {
const idx = presets.findIndex(p => p.id === presetId);
if (idx === -1) return;
if (presets[idx].builtIn) return; // 内置不可删
presets.splice(idx, 1);
state.set('presets', [...presets]);
// 如果删除的是当前激活预设,切回默认
if (activePresetId === presetId) {
await activatePreset('_default');
}
await saveUserPresets();
}
/**
* 保存用户自定义预设到 IndexedDB
*/
async function saveUserPresets() {
const db = state.get(KEYS.DB);
if (!db) return;
const userPresets = presets
.filter(p => !p.builtIn)
.map(({ builtIn, ...rest }) => rest); // 去掉 builtIn 标记
await db.saveSetting(PRESETS_KEY, userPresets);
}