refactor: 移除人格模式、后台检查、系统诊断、定时任务功能
This commit is contained in:
@@ -15,16 +15,6 @@ import { OllamaAPI } from '../api/ollama.js';
|
||||
import { ChatDB } from '../db/chat-db.js';
|
||||
import type { ChatSession } from '../types.js';
|
||||
|
||||
/** v4.2 人格模式 system prompt 映射 */
|
||||
export const PERSONALITY_SYSTEM_PROMPTS: Record<string, string> = {
|
||||
default: '',
|
||||
coder: '你是一个专业的编程助手。你注重代码质量、可读性和最佳实践。回答时优先给出可运行的代码,其次才是解释。遇到问题先分析根因再给方案。使用 type hints、合理的命名和注释。代码风格偏向简洁紧凑。',
|
||||
writer: '你是一个专业的写作助手。你的文字流畅、有文采,注重结构和节奏。擅长技术文档、文章、文案、邮件等各种文体。回答时先理解写作目的和受众,再给出合适的风格和内容。',
|
||||
analyst: '你是一个精确的数据分析助手。你注重数据的准确性和逻辑的严密性。回答时先明确数据来源和假设,再给出分析过程和结论。偏好用数字和事实说话,避免模糊表述。',
|
||||
translator: '你是一个专业的翻译助手。你精通多语言互译,注重信达雅。翻译时保留原文的语气和风格,专业术语保持一致。遇到歧义时给出多种翻译并标注区别。',
|
||||
reviewer: '你是一个严格的代码审查助手。你关注:正确性(逻辑错误、边界情况)、安全性(注入、泄露)、性能(时间/空间复杂度)、可维护性(命名、结构、注释)。审查时先给出总体评价,再逐项列出问题并给出修复建议。',
|
||||
};
|
||||
|
||||
let settingsModalEl: HTMLElement;
|
||||
|
||||
export function initSettingsModal(): void {
|
||||
@@ -84,84 +74,6 @@ export function initSettingsModal(): void {
|
||||
}, 500);
|
||||
document.querySelector('#inputMaxTurns')!.addEventListener('input', saveMaxTurns);
|
||||
|
||||
// ── v4.2 人格模式切换 ──
|
||||
const PERSONALITY_TEMPERATURES: Record<string, number> = {
|
||||
default: -1, // 使用用户设置
|
||||
coder: 0.3,
|
||||
writer: 0.8,
|
||||
analyst: 0.2,
|
||||
translator: 0.4,
|
||||
reviewer: 0.3,
|
||||
};
|
||||
|
||||
document.querySelectorAll('.personality-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const personality = (btn as HTMLElement).dataset.personality || 'default';
|
||||
state.set('personality', personality);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('personality', personality);
|
||||
|
||||
// 更新按钮样式
|
||||
document.querySelectorAll('.personality-btn').forEach(b => {
|
||||
(b as HTMLElement).style.border = '2px solid var(--border-subtle)';
|
||||
b.classList.remove('active');
|
||||
});
|
||||
(btn as HTMLElement).style.border = '2px solid var(--primary)';
|
||||
btn.classList.add('active');
|
||||
|
||||
// 如果人格有推荐温度,自动调整
|
||||
const recommendedTemp = PERSONALITY_TEMPERATURES[personality];
|
||||
if (recommendedTemp >= 0) {
|
||||
const tempSlider = document.querySelector('#inputTemperature') as HTMLInputElement;
|
||||
tempSlider.value = String(recommendedTemp);
|
||||
document.querySelector('#tempValue')!.textContent = recommendedTemp.toFixed(1);
|
||||
state.set('temperature', recommendedTemp);
|
||||
if (db) await db.saveSetting('temperature', recommendedTemp);
|
||||
}
|
||||
|
||||
const names: Record<string, string> = {
|
||||
default: '通用助手', coder: '编程助手', writer: '写作助手',
|
||||
analyst: '数据分析', translator: '翻译助手', reviewer: '代码审查'
|
||||
};
|
||||
showToast(`已切换到 ${names[personality] || personality}`, 'success');
|
||||
logSetting('人格模式', names[personality] || personality);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v4.3 诊断系统 ──
|
||||
document.querySelector('#btnDoctor')!.addEventListener('click', runDoctor);
|
||||
|
||||
// ── v4.3 Heartbeat 后台检查 ──
|
||||
const toggleHB = document.querySelector<HTMLInputElement>('#toggleHeartbeat')!;
|
||||
const selectHBInterval = document.querySelector<HTMLSelectElement>('#selectHeartbeatInterval')!;
|
||||
|
||||
toggleHB.addEventListener('change', async () => {
|
||||
const enabled = toggleHB.checked;
|
||||
state.set('heartbeatEnabled', enabled);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('heartbeatEnabled', enabled);
|
||||
if (enabled) {
|
||||
startHeartbeat(parseInt(selectHBInterval.value));
|
||||
showToast('后台检查已启用', 'success');
|
||||
} else {
|
||||
stopHeartbeat();
|
||||
showToast('后台检查已关闭', 'info');
|
||||
}
|
||||
logSetting('后台检查', enabled ? '已启用' : '已关闭');
|
||||
});
|
||||
|
||||
selectHBInterval.addEventListener('change', async () => {
|
||||
const ms = parseInt(selectHBInterval.value);
|
||||
state.set('heartbeatInterval', ms);
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) await db.saveSetting('heartbeatInterval', ms);
|
||||
if (state.get<boolean>('heartbeatEnabled', false)) {
|
||||
stopHeartbeat();
|
||||
startHeartbeat(ms);
|
||||
}
|
||||
logSetting('检查间隔', `${ms / 60000} 分钟`);
|
||||
});
|
||||
|
||||
// ── v5.0 MCP 服务器管理 ──
|
||||
async function renderMCPServerList(): Promise<void> {
|
||||
const { getMCPServers, getMCPServerStatuses, toggleMCPServer, removeMCPServer } = await import('../services/mcp-client.js');
|
||||
@@ -234,33 +146,6 @@ export function initSettingsModal(): void {
|
||||
setTimeout(renderMCPServerList, 100);
|
||||
});
|
||||
|
||||
// ── v5.0 Cron 定时任务 ──
|
||||
document.querySelector('#btnAddCronTask')!.addEventListener('click', async () => {
|
||||
const name = (document.querySelector('#inputCronName') as HTMLInputElement).value.trim();
|
||||
const type = (document.querySelector('#selectCronType') as HTMLSelectElement).value as 'oneshot' | 'recurring';
|
||||
const schedule = (document.querySelector('#inputCronSchedule') as HTMLInputElement).value.trim();
|
||||
const message = (document.querySelector('#inputCronMessage') as HTMLInputElement).value.trim();
|
||||
|
||||
if (!name || !schedule || !message) {
|
||||
showToast('请填写任务名称、时间和描述', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const { addCronTask } = await import('../services/cron-manager.js');
|
||||
await addCronTask({ name, type, schedule, message, enabled: true });
|
||||
|
||||
// 清空输入框
|
||||
(document.querySelector('#inputCronName') as HTMLInputElement).value = '';
|
||||
(document.querySelector('#inputCronSchedule') as HTMLInputElement).value = '';
|
||||
(document.querySelector('#inputCronMessage') as HTMLInputElement).value = '';
|
||||
|
||||
showToast(`定时任务已添加: ${name}`, 'success');
|
||||
renderCronTaskList();
|
||||
});
|
||||
|
||||
// 初始化 cron 列表
|
||||
renderCronTaskList();
|
||||
|
||||
document.querySelector('#btnReleaseVRAM')!.addEventListener('click', async () => {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value;
|
||||
@@ -547,185 +432,3 @@ async function importSessions(filePath: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 诊断系统
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
async function runDoctor(): Promise<void> {
|
||||
const resultsEl = document.querySelector('#doctorResults')!;
|
||||
resultsEl.style.display = '';
|
||||
resultsEl.innerHTML = '<div>🩺 正在诊断...</div>';
|
||||
|
||||
const checks: Array<{ name: string; status: 'ok' | 'warn' | 'error'; detail: string }> = [];
|
||||
|
||||
// 1. Ollama 连接
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
try {
|
||||
const version = await api!.getVersion();
|
||||
checks.push({ name: 'Ollama 连接', status: 'ok', detail: `v${version.version}` });
|
||||
} catch (err) {
|
||||
checks.push({ name: 'Ollama 连接', status: 'error', detail: `无法连接: ${(err as Error).message}` });
|
||||
}
|
||||
|
||||
// 2. 模型可用性
|
||||
try {
|
||||
const models = await api!.listModels();
|
||||
const count = models.models?.length || 0;
|
||||
checks.push({ name: '已安装模型', status: count > 0 ? 'ok' : 'warn', detail: `${count} 个模型` });
|
||||
} catch {
|
||||
checks.push({ name: '已安装模型', status: 'error', detail: '无法获取模型列表' });
|
||||
}
|
||||
|
||||
// 3. 当前模型能力
|
||||
const model = (document.querySelector('#modelSelect') as HTMLSelectElement)?.value;
|
||||
if (model) {
|
||||
try {
|
||||
const detail = await api!.showModel(model);
|
||||
const caps = detail.capabilities || [];
|
||||
const hasTools = caps.includes('tools');
|
||||
const hasVision = caps.includes('vision');
|
||||
const hasThink = caps.includes('thinking');
|
||||
checks.push({
|
||||
name: `模型能力 (${model})`,
|
||||
status: hasTools ? 'ok' : 'warn',
|
||||
detail: `Tool Calling: ${hasTools ? '✅' : '❌'} | Vision: ${hasVision ? '✅' : '❌'} | Think: ${hasThink ? '✅' : '❌'}`
|
||||
});
|
||||
} catch {
|
||||
checks.push({ name: '模型能力', status: 'warn', detail: '无法检测模型能力' });
|
||||
}
|
||||
} else {
|
||||
checks.push({ name: '当前模型', status: 'warn', detail: '未选择模型' });
|
||||
}
|
||||
|
||||
// 4. SQLite 数据库
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) {
|
||||
try {
|
||||
const sessions = await db.getAllSessions();
|
||||
checks.push({ name: 'SQLite 数据库', status: 'ok', detail: `${sessions.length} 个会话` });
|
||||
} catch (err) {
|
||||
checks.push({ name: 'SQLite 数据库', status: 'error', detail: `错误: ${(err as Error).message}` });
|
||||
}
|
||||
} else {
|
||||
checks.push({ name: 'SQLite 数据库', status: 'error', detail: '未初始化' });
|
||||
}
|
||||
|
||||
// 5. 技能系统
|
||||
try {
|
||||
if (db) {
|
||||
checks.push({ name: '技能系统', status: 'ok', detail: '已就绪' });
|
||||
}
|
||||
} catch {
|
||||
checks.push({ name: '技能系统', status: 'warn', detail: '无法获取技能列表' });
|
||||
}
|
||||
|
||||
// 6. 版本信息
|
||||
const bridge = window.metonaDesktop;
|
||||
if (bridge?.info) {
|
||||
try {
|
||||
const info = await bridge.info();
|
||||
checks.push({ name: '应用信息', status: 'ok', detail: `v${info.version} | Electron ${info.electronVersion} | ${info.platform} ${info.arch}` });
|
||||
} catch {
|
||||
checks.push({ name: '应用信息', status: 'warn', detail: '无法获取' });
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染结果
|
||||
const statusIcon = (s: string) => s === 'ok' ? '✅' : s === 'warn' ? '⚠️' : '❌';
|
||||
const allOk = checks.every(c => c.status === 'ok');
|
||||
|
||||
resultsEl.innerHTML = `
|
||||
<div style="margin-bottom:6px;font-weight:600;">${allOk ? '✅ 所有检查通过' : '⚠️ 发现问题'}</div>
|
||||
${checks.map(c => `<div style="margin:4px 0;">${statusIcon(c.status)} <strong>${c.name}</strong>: ${c.detail}</div>`).join('')}
|
||||
`;
|
||||
|
||||
logInfo('系统诊断完成', `${checks.filter(c => c.status === 'ok').length}/${checks.length} 项通过`);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// v4.3 Heartbeat 后台检查
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function startHeartbeat(intervalMs: number): void {
|
||||
stopHeartbeat();
|
||||
heartbeatTimer = setInterval(async () => {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
if (!api) return;
|
||||
|
||||
try {
|
||||
await api.getVersion();
|
||||
// 连接正常,静默
|
||||
} catch {
|
||||
// 连接异常,通知用户
|
||||
const bridge = window.metonaDesktop;
|
||||
if (bridge?.notify) {
|
||||
bridge.notify('Metona Ollama', '⚠️ Ollama 服务连接异常,请检查是否正在运行');
|
||||
}
|
||||
logWarn('Heartbeat: Ollama 连接异常');
|
||||
}
|
||||
}, intervalMs);
|
||||
logInfo(`Heartbeat 已启动`, `间隔: ${intervalMs / 60000} 分钟`);
|
||||
}
|
||||
|
||||
export function stopHeartbeat(): void {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer);
|
||||
heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用启动时恢复 heartbeat */
|
||||
export function restoreHeartbeat(): void {
|
||||
const enabled = state.get<boolean>('heartbeatEnabled', false);
|
||||
const interval = state.get<number>('heartbeatInterval', 3600000);
|
||||
if (enabled) {
|
||||
startHeartbeat(interval);
|
||||
// 同步 UI 状态
|
||||
const toggle = document.querySelector<HTMLInputElement>('#toggleHeartbeat');
|
||||
if (toggle) toggle.checked = true;
|
||||
const select = document.querySelector<HTMLSelectElement>('#selectHeartbeatInterval');
|
||||
if (select) select.value = String(interval);
|
||||
}
|
||||
}
|
||||
|
||||
/** 渲染定时任务列表 */
|
||||
async function renderCronTaskList(): Promise<void> {
|
||||
const container = document.querySelector('#cronTaskList');
|
||||
if (!container) return;
|
||||
|
||||
const { getCronTasks, removeCronTask, toggleCronTask } = await import('../services/cron-manager.js');
|
||||
const tasks = await getCronTasks();
|
||||
|
||||
if (tasks.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">无定时任务</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tasks.map(t => `
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:4px 0;border-bottom:1px solid var(--border-subtle);">
|
||||
<span>${t.enabled ? '✅' : '⏸️'}</span>
|
||||
<span style="flex:1;font-weight:500;">${t.name}</span>
|
||||
<span class="text-muted" style="font-size:11px;">${t.type === 'oneshot' ? '一次性' : '周期'}</span>
|
||||
<button class="btn btn-sm" onclick="window.__toggleCron('${t.id}')" style="padding:2px 6px;font-size:11px;">${t.enabled ? '禁用' : '启用'}</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="window.__deleteCron('${t.id}')" style="padding:2px 6px;font-size:11px;">删除</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 全局方法供内联 onclick 调用
|
||||
interface WindowWithCron extends Window {
|
||||
__toggleCron?: (id: string) => Promise<void>;
|
||||
__deleteCron?: (id: string) => Promise<void>;
|
||||
}
|
||||
(window as WindowWithCron).__toggleCron = async (id: string) => {
|
||||
const { toggleCronTask } = await import('../services/cron-manager.js');
|
||||
await toggleCronTask(id);
|
||||
renderCronTaskList();
|
||||
};
|
||||
(window as WindowWithCron).__deleteCron = async (id: string) => {
|
||||
const { removeCronTask } = await import('../services/cron-manager.js');
|
||||
await removeCronTask(id);
|
||||
renderCronTaskList();
|
||||
};
|
||||
|
||||
@@ -288,18 +288,6 @@
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">Agent Loop 最大工具调用轮数。默认 85 轮,复杂任务可能需要更多轮次。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">🎭 人格模式</label>
|
||||
<div class="personality-grid" id="personalityGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px;">
|
||||
<button class="btn btn-sm personality-btn active" data-personality="default" style="padding:6px 8px;font-size:12px;border:2px solid var(--primary);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">🤖 通用助手</button>
|
||||
<button class="btn btn-sm personality-btn" data-personality="coder" style="padding:6px 8px;font-size:12px;border:2px solid var(--border-subtle);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">💻 编程助手</button>
|
||||
<button class="btn btn-sm personality-btn" data-personality="writer" style="padding:6px 8px;font-size:12px;border:2px solid var(--border-subtle);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">✍️ 写作助手</button>
|
||||
<button class="btn btn-sm personality-btn" data-personality="analyst" style="padding:6px 8px;font-size:12px;border:2px solid var(--border-subtle);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">📊 数据分析</button>
|
||||
<button class="btn btn-sm personality-btn" data-personality="translator" style="padding:6px 8px;font-size:12px;border:2px solid var(--border-subtle);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">🌐 翻译助手</button>
|
||||
<button class="btn btn-sm personality-btn" data-personality="reviewer" style="padding:6px 8px;font-size:12px;border:2px solid var(--border-subtle);border-radius:var(--radius-control);background:var(--bg-layer);cursor:pointer;">🔍 代码审查</button>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">不同人格有不同的行为风格和温度偏好。通用助手不做额外约束。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">运行中的模型</label>
|
||||
<div class="running-models" id="runningModels">
|
||||
@@ -350,31 +338,6 @@
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">💓 后台检查(Heartbeat)</label>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<label class="toggle-label" style="margin:0;">
|
||||
<input type="checkbox" id="toggleHeartbeat">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span style="font-size:13px;">启用后台定时检查</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-top:8px;">
|
||||
<label style="font-size:12px;color:var(--text-muted);">检查间隔</label>
|
||||
<select id="selectHeartbeatInterval" class="setting-input" style="width:auto;margin-bottom:0;padding:4px 8px;">
|
||||
<option value="600000">10 分钟</option>
|
||||
<option value="1800000">30 分钟</option>
|
||||
<option value="3600000" selected>1 小时</option>
|
||||
<option value="7200000">2 小时</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">定期检查 Ollama 连接状态和模型可用性,异常时通过系统通知提醒。</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">🩺 系统诊断</label>
|
||||
<button class="btn btn-outline" id="btnDoctor" style="margin-bottom:8px;">🩺 一键诊断</button>
|
||||
<div id="doctorResults" style="display:none;margin-top:8px;padding:10px;background:var(--bg-layer);border-radius:var(--radius-control);font-size:12px;max-height:200px;overflow-y:auto;"></div>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">🔌 MCP 服务器</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
@@ -385,22 +348,6 @@
|
||||
</div>
|
||||
<div id="mcpServerList" style="font-size:12px;"></div>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">⏰ 定时任务 (Cron)</label>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
<input class="setting-input" id="inputCronName" type="text" placeholder="任务名称" style="margin-bottom:0;flex:1;">
|
||||
<select id="selectCronType" class="setting-input" style="width:auto;margin-bottom:0;padding:4px 8px;">
|
||||
<option value="oneshot">一次性</option>
|
||||
<option value="recurring">周期性</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||
<input class="setting-input" id="inputCronSchedule" type="text" placeholder="时间 (如: 2026-04-18T10:00 或 every 30m)" style="margin-bottom:0;flex:1;">
|
||||
<button class="btn btn-sm btn-outline" id="btnAddCronTask">➕ 添加</button>
|
||||
</div>
|
||||
<input class="setting-input" id="inputCronMessage" type="text" placeholder="任务描述(将作为 Agent 指令)" style="margin-bottom:8px;">
|
||||
<div id="cronTaskList" style="font-size:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -443,12 +390,8 @@
|
||||
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>上下文长度</strong> — 设置中调整 <code>num_ctx</code>,越大记忆越长,消耗显存越多</li></ul></div>
|
||||
<div class="help-section"><h4>🔧 Tool Calling(AI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>38 个工具</strong>,分为 6 类:<ul><li><strong>文件系统</strong>(12 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree</li><li><strong>系统 & 网络</strong>(6 个):run_command(执行命令)/ web_search(联网搜索,Bing + 百度 + Google 三引擎)/ web_fetch(网页抓取)/ download_file / diff_files / replace_in_files / read_multiple_files</li><li><strong>Git</strong>(1 个):git(status / log / diff / add / commit / push / pull / branch / checkout / stash / reset / merge / clone / init / remote / tag,共 16 个子操作)</li><li><strong>浏览器控制</strong>(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>压缩</strong>(1 个):compress(创建/解压 zip/tar.gz)</li><li><strong>记忆 & 会话 & Skill</strong>(8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>(1 个):spawn_task</li></ul></li><li>仅 <code>run_command</code>(命令执行)支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,可在设置中调整,也可随时中断</li><li>无超时限制,宁可等待也不中断,由用户控制生命周期</li><li>独立工具自动并行执行,有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)</li></ul></div>
|
||||
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 <code>memory_search</code> / <code>memory_add</code> / <code>memory_replace</code> / <code>memory_remove</code> 工具主动管理记忆</li><li>AI 可通过 <code>session_list</code> / <code>session_read</code> 查阅历史会话</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||
<div class="help-section"><h4>🎭 人格模式</h4><ul><li>设置中可切换 6 种预设人格:通用助手 / 编程助手 / 写作助手 / 数据分析 / 翻译助手 / 代码审查</li><li>不同人格有独立的 system prompt 和温度偏好</li><li>通用助手不做额外约束,其他人格会调整行为风格</li></ul></div>
|
||||
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
||||
<div class="help-section"><h4>⏰ 定时任务(Cron)</h4><ul><li>支持一次性(at)和周期性(cron 表达式)任务调度</li><li>任务触发后将描述填入输入框,可直接发送给 AI 执行</li><li>设置面板可添加/管理/删除定时任务</li></ul></div>
|
||||
<div class="help-section"><h4>💓 Heartbeat 后台检查</h4><ul><li>启用后定期检查 Ollama 连接状态和模型可用性</li><li>支持 10 分钟 / 30 分钟 / 1 小时 / 2 小时间隔(默认 1 小时)</li><li>异常时通过系统通知提醒</li></ul></div>
|
||||
<div class="help-section"><h4>🧠 技能自动生成</h4><ul><li>AI 完成包含 <strong>2+ 工具调用</strong>的复杂任务后,自动从执行轨迹中提取可复用技能</li><li>技能包含工具链步骤、成功/失败次数、平均执行时长等统计</li><li>下次遇到类似任务时自动匹配已有技能,跳过重新推理</li><li>AI 可通过 <code>skill_list</code> 查看技能列表(名称+描述+成功率),<code>skill_view</code> 查看完整工具链详情</li><li>技能存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||
<div class="help-section"><h4>🩺 系统诊断</h4><ul><li>设置面板点击「🩺 一键诊断」按钮,自动检查以下项目:<ul><li>Ollama 服务连接与版本</li><li>当前模型可用性与 Tool Calling / Vision 支持</li><li>工作空间目录与磁盘空间</li><li>SQLite 数据库完整性</li><li>工具安全配置</li></ul></li><li>诊断结果以 ✅/⚠️/❌ 标记,附带修复建议</li></ul></div>
|
||||
<div class="help-section"><h4>🖥️ 布局说明</h4><ul><li><strong>左侧面板</strong> — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)</li><li><strong>中间区域</strong> — 聊天消息,顶部 Header + 模型栏,底部输入框</li><li><strong>右侧面板</strong> — 工作空间(常驻显示),包含 3 个页签:<ul><li><strong>💻 命令行 Tab</strong> — 终端界面,实时流式输出,支持长时间运行(无超时),单一终端进程</li><li><strong>🔧 工具 Tab</strong> — 展示本轮对话的工具调用卡片,含状态统计(总数 ✅ 成功 ❌ 失败),AI 执行命令时自动切到此页签</li><li><strong>📁 文件 Tab</strong> — 浏览工作空间目录,点击文件预览内容(带行号),支持上级目录导航</li></ul></li><li>工作空间目录可在设置中修改</li></ul></div>
|
||||
<div class="help-section"><h4>🕐 历史记录</h4><ul><li>所有会话自动保存到本地 SQLite</li><li>点击顶部 🕐 按钮查看、搜索、恢复历史会话</li><li>支持导出 JSON / <code>.metona</code> 加密备份</li></ul></div>
|
||||
<div class="help-section"><h4>⚡ 快捷键 & 命令</h4><table class="help-shortcuts"><tr><td><kbd>Enter</kbd></td><td>发送消息</td></tr><tr><td><kbd>Shift + Enter</kbd></td><td>换行</td></tr><tr><td><kbd>Ctrl + N</kbd></td><td>新建会话</td></tr><tr><td><kbd>Ctrl + M</kbd></td><td>记忆管理</td></tr><tr><td><kbd>Esc</kbd></td><td>关闭弹窗</td></tr></table><p style="margin-top:8px;font-size:12px;color:var(--text-secondary);">输入框命令(直接输入后发送):</p><table class="help-shortcuts"><tr><td><code>/retry</code></td><td>重试上一轮回复</td></tr><tr><td><code>/undo</code></td><td>撤销最后一条用户消息及回复</td></tr><tr><td><code>/compress</code></td><td>用 AI 摘要压缩长对话上下文</td></tr></table></div>
|
||||
|
||||
@@ -23,8 +23,6 @@ import { initMemoryModal } from './components/memory-modal.js';
|
||||
import { initToolsModal, closeToolsModal } from './components/tools-modal.js';
|
||||
import { initTokenDashboard, closeTokenDashboard } from './components/token-dashboard.js';
|
||||
import { initMemoryManager } from './services/memory-manager.js';
|
||||
import { restoreHeartbeat } from './components/settings-modal.js';
|
||||
import { restoreCronTasks } from './services/cron-manager.js';
|
||||
import { setToolEnabled, setRunCommandMode, refreshMCPTools } from './services/tool-registry.js';
|
||||
import { startAllMCPServers } from './services/mcp-client.js';
|
||||
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
||||
@@ -382,35 +380,9 @@ async function init(): Promise<void> {
|
||||
const memoryEnabled = await db.getSetting('memoryEnabled', true);
|
||||
state.set('memoryEnabled', memoryEnabled);
|
||||
|
||||
// ── v4.2 人格模式 ──
|
||||
const personality = await db.getSetting('personality', 'default');
|
||||
state.set('personality', personality);
|
||||
// 激活对应的人格按钮
|
||||
document.querySelectorAll('.personality-btn').forEach(b => {
|
||||
const p = (b as HTMLElement).dataset.personality;
|
||||
if (p === personality) {
|
||||
(b as HTMLElement).style.border = '2px solid var(--primary)';
|
||||
b.classList.add('active');
|
||||
} else {
|
||||
(b as HTMLElement).style.border = '2px solid var(--border-subtle)';
|
||||
b.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// ── v4.3 Heartbeat 恢复 ──
|
||||
const heartbeatEnabled = await db.getSetting('heartbeatEnabled', false);
|
||||
const heartbeatInterval = await db.getSetting('heartbeatInterval', 3600000);
|
||||
state.set('heartbeatEnabled', heartbeatEnabled);
|
||||
state.set('heartbeatInterval', heartbeatInterval);
|
||||
|
||||
(document.querySelector('#chatInput') as HTMLElement)?.focus();
|
||||
|
||||
logSuccess('应用初始化完成');
|
||||
|
||||
// Heartbeat 在 UI 就绪后启动
|
||||
restoreHeartbeat();
|
||||
// Cron 定时任务恢复
|
||||
restoreCronTasks();
|
||||
} catch (err) {
|
||||
logError('初始化失败', (err as Error).message);
|
||||
if (!api!) {
|
||||
@@ -476,20 +448,6 @@ function bindGlobalEvents(): void {
|
||||
showToast(`操作失败: ${msg}`, 'error', 5000);
|
||||
});
|
||||
|
||||
// v5.0 Cron 定时任务事件
|
||||
window.addEventListener('metona:cron-task', ((e: CustomEvent) => {
|
||||
const task = e.detail;
|
||||
if (task?.message) {
|
||||
showToast(`⏰ 定时任务: ${task.name}`, 'info', 5000);
|
||||
logInfo(`定时任务触发: ${task.name}`, task.message.slice(0, 60));
|
||||
// 将任务描述填入输入框
|
||||
const chatInput = document.querySelector('#chatInput') as HTMLTextAreaElement;
|
||||
if (chatInput) {
|
||||
chatInput.value = task.message;
|
||||
chatInput.focus();
|
||||
}
|
||||
}
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from './tool-registry.js';
|
||||
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
|
||||
import { extractSkillsFromToolRecords, matchSkills, buildSkillContext } from './skill-manager.js';
|
||||
import { PERSONALITY_SYSTEM_PROMPTS } from '../components/settings-modal.js';
|
||||
import { showToast } from '../components/toast.js';
|
||||
import { logInfo, logWarn, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse } from './log-service.js';
|
||||
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
|
||||
@@ -461,8 +460,6 @@ export async function runAgentLoop(
|
||||
}
|
||||
|
||||
// 组合 system prompt
|
||||
const personality = state.get<string>('personality', 'default');
|
||||
const personalityPrompt = PERSONALITY_SYSTEM_PROMPTS[personality] || '';
|
||||
|
||||
// 实时注入当前日期(来自系统时钟,非模型知识库)
|
||||
const _now = new Date();
|
||||
@@ -472,7 +469,6 @@ export async function runAgentLoop(
|
||||
...systemPromptParts,
|
||||
`【当前日期】${realDate}(此日期来自系统时钟,绝对可信。你的训练数据可能已过时,请以此日期为准构造所有搜索查询和时效性回答。绝对不要基于训练数据推断日期。)`,
|
||||
AGENT_SYSTEM_PROMPT,
|
||||
...(personalityPrompt ? [personalityPrompt] : []),
|
||||
TOOL_USAGE_GUIDE
|
||||
].join('\n\n');
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
/**
|
||||
* CronManager - 定时任务系统 (v5.0)
|
||||
* 支持 one-shot 和周期性定时任务
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { generateId } from '../utils/utils.js';
|
||||
import { logInfo, logWarn } from './log-service.js';
|
||||
|
||||
export interface CronTask {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'oneshot' | 'recurring';
|
||||
/** ISO-8601 时间戳(one-shot)或 cron 表达式(recurring) */
|
||||
schedule: string;
|
||||
/** 任务描述(注入给 Agent) */
|
||||
message: string;
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 上次执行时间 */
|
||||
lastRunAt: number | null;
|
||||
/** 创建时间 */
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/** 活跃的定时器 */
|
||||
const activeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
/**
|
||||
* 获取所有定时任务
|
||||
*/
|
||||
export async function getCronTasks(): Promise<CronTask[]> {
|
||||
const db = state.get<any>(KEYS.DB);
|
||||
if (!db) return [];
|
||||
try {
|
||||
return await db.getSetting('cron_tasks', []);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存定时任务
|
||||
*/
|
||||
async function saveCronTasks(tasks: CronTask[]): Promise<void> {
|
||||
const db = state.get<any>(KEYS.DB);
|
||||
if (db) await db.saveSetting('cron_tasks', tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加定时任务
|
||||
*/
|
||||
export async function addCronTask(task: Omit<CronTask, 'id' | 'createdAt' | 'lastRunAt'>): Promise<CronTask> {
|
||||
const tasks = await getCronTasks();
|
||||
const newTask: CronTask = {
|
||||
...task,
|
||||
id: `cron_${generateId()}`,
|
||||
createdAt: Date.now(),
|
||||
lastRunAt: null
|
||||
};
|
||||
tasks.push(newTask);
|
||||
await saveCronTasks(tasks);
|
||||
|
||||
if (newTask.enabled) {
|
||||
scheduleTask(newTask);
|
||||
}
|
||||
|
||||
logInfo(`定时任务已添加: ${newTask.name}`, newTask.schedule);
|
||||
return newTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务
|
||||
*/
|
||||
export async function removeCronTask(id: string): Promise<void> {
|
||||
const tasks = await getCronTasks();
|
||||
await saveCronTasks(tasks.filter(t => t.id !== id));
|
||||
clearTimer(id);
|
||||
logInfo(`定时任务已删除: ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换定时任务启用状态
|
||||
*/
|
||||
export async function toggleCronTask(id: string): Promise<boolean> {
|
||||
const tasks = await getCronTasks();
|
||||
const task = tasks.find(t => t.id === id);
|
||||
if (!task) return false;
|
||||
|
||||
task.enabled = !task.enabled;
|
||||
await saveCronTasks(tasks);
|
||||
|
||||
if (task.enabled) {
|
||||
scheduleTask(task);
|
||||
} else {
|
||||
clearTimer(id);
|
||||
}
|
||||
|
||||
logInfo(`定时任务 ${task.enabled ? '已启用' : '已禁用'}: ${task.name}`);
|
||||
return task.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度单个任务
|
||||
*/
|
||||
function scheduleTask(task: CronTask): void {
|
||||
clearTimer(task.id);
|
||||
|
||||
if (task.type === 'oneshot') {
|
||||
const targetTime = new Date(task.schedule).getTime();
|
||||
const delay = targetTime - Date.now();
|
||||
if (delay <= 0) {
|
||||
logWarn(`定时任务已过期: ${task.name}`);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => executeTask(task), delay);
|
||||
activeTimers.set(task.id, timer);
|
||||
logInfo(`One-shot 任务已调度: ${task.name}`, `${Math.round(delay / 1000)}s 后执行`);
|
||||
} else {
|
||||
// Recurring: 简化实现,使用 setInterval
|
||||
// cron 表达式暂简化为 "every Nm" 或 "every Nh" 格式
|
||||
const match = task.schedule.match(/every\s+(\d+)(m|h)/i);
|
||||
if (match) {
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2].toLowerCase();
|
||||
const ms = unit === 'h' ? value * 3600000 : value * 60000;
|
||||
const timer = setInterval(() => executeTask(task), ms);
|
||||
activeTimers.set(task.id, timer);
|
||||
logInfo(`Recurring 任务已调度: ${task.name}`, `每 ${value}${unit} 执行`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*/
|
||||
async function executeTask(task: CronTask): Promise<void> {
|
||||
logInfo(`执行定时任务: ${task.name}`);
|
||||
|
||||
// 更新 lastRunAt
|
||||
const tasks = await getCronTasks();
|
||||
const t = tasks.find(tt => tt.id === task.id);
|
||||
if (t) {
|
||||
t.lastRunAt = Date.now();
|
||||
await saveCronTasks(tasks);
|
||||
}
|
||||
|
||||
// 注入 systemEvent 到主会话
|
||||
// 实际执行需要触发 Agent Loop,这里通过事件通知
|
||||
window.dispatchEvent(new CustomEvent('metona:cron-task', { detail: task }));
|
||||
|
||||
// One-shot 执行后自动删除
|
||||
if (task.type === 'oneshot') {
|
||||
await removeCronTask(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除定时器
|
||||
*/
|
||||
function clearTimer(id: string): void {
|
||||
const timer = activeTimers.get(id);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
clearInterval(timer);
|
||||
activeTimers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复所有定时任务(应用启动时调用)
|
||||
*/
|
||||
export async function restoreCronTasks(): Promise<void> {
|
||||
const tasks = await getCronTasks();
|
||||
let scheduled = 0;
|
||||
for (const task of tasks) {
|
||||
if (task.enabled) {
|
||||
scheduleTask(task);
|
||||
scheduled++;
|
||||
}
|
||||
}
|
||||
if (scheduled > 0) {
|
||||
logInfo(`定时任务已恢复: ${scheduled} 个任务`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user