feat: Agent 预设系统 - 一键切换角色/工作模式
- 内置 6 个预设:默认、翻译官、代码审查、写作助手、数据分析师、学习导师 - 每个预设包含:系统提示词、温度、上下文长度、Think 开关 - 预设栏胶囊按钮,点击即切换,自动应用全部配置 - 支持创建/编辑/删除自定义预设,IndexedDB 持久化 - 切换预设时同步设置面板 UI + toast 提示 - 温度参数集成到聊天 API 调用
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user