diff --git a/src/renderer/components/settings-modal.ts b/src/renderer/components/settings-modal.ts index 3d958f6..552437b 100644 --- a/src/renderer/components/settings-modal.ts +++ b/src/renderer/components/settings-modal.ts @@ -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 = { - 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 = { - 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(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 = { - 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('#toggleHeartbeat')!; - const selectHBInterval = document.querySelector('#selectHeartbeatInterval')!; - - toggleHB.addEventListener('change', async () => { - const enabled = toggleHB.checked; - state.set('heartbeatEnabled', enabled); - const db = state.get(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(KEYS.DB); - if (db) await db.saveSetting('heartbeatInterval', ms); - if (state.get('heartbeatEnabled', false)) { - stopHeartbeat(); - startHeartbeat(ms); - } - logSetting('检查间隔', `${ms / 60000} 分钟`); - }); - // ── v5.0 MCP 服务器管理 ── async function renderMCPServerList(): Promise { 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(KEYS.API); const model = (document.querySelector('#modelSelect') as HTMLSelectElement).value; @@ -547,185 +432,3 @@ async function importSessions(filePath: string): Promise { } } -// ══════════════════════════════════════════════ -// v4.3 诊断系统 -// ══════════════════════════════════════════════ - -async function runDoctor(): Promise { - const resultsEl = document.querySelector('#doctorResults')!; - resultsEl.style.display = ''; - resultsEl.innerHTML = '
🩺 正在诊断...
'; - - const checks: Array<{ name: string; status: 'ok' | 'warn' | 'error'; detail: string }> = []; - - // 1. Ollama 连接 - const api = state.get(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(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 = ` -
${allOk ? '✅ 所有检查通过' : '⚠️ 发现问题'}
- ${checks.map(c => `
${statusIcon(c.status)} ${c.name}: ${c.detail}
`).join('')} - `; - - logInfo('系统诊断完成', `${checks.filter(c => c.status === 'ok').length}/${checks.length} 项通过`); -} - -// ══════════════════════════════════════════════ -// v4.3 Heartbeat 后台检查 -// ══════════════════════════════════════════════ - -let heartbeatTimer: ReturnType | null = null; - -export function startHeartbeat(intervalMs: number): void { - stopHeartbeat(); - heartbeatTimer = setInterval(async () => { - const api = state.get(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('heartbeatEnabled', false); - const interval = state.get('heartbeatInterval', 3600000); - if (enabled) { - startHeartbeat(interval); - // 同步 UI 状态 - const toggle = document.querySelector('#toggleHeartbeat'); - if (toggle) toggle.checked = true; - const select = document.querySelector('#selectHeartbeatInterval'); - if (select) select.value = String(interval); - } -} - -/** 渲染定时任务列表 */ -async function renderCronTaskList(): Promise { - 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 = '

无定时任务

'; - return; - } - - container.innerHTML = tasks.map(t => ` -
- ${t.enabled ? '✅' : '⏸️'} - ${t.name} - ${t.type === 'oneshot' ? '一次性' : '周期'} - - -
- `).join(''); -} - -// 全局方法供内联 onclick 调用 -interface WindowWithCron extends Window { - __toggleCron?: (id: string) => Promise; - __deleteCron?: (id: string) => Promise; -} -(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(); -}; diff --git a/src/renderer/index.html b/src/renderer/index.html index 934415f..dfd4fe9 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -288,18 +288,6 @@

Agent Loop 最大工具调用轮数。默认 85 轮,复杂任务可能需要更多轮次。

-
- -
- - - - - - -
-

不同人格有不同的行为风格和温度偏好。通用助手不做额外约束。

-
@@ -350,31 +338,6 @@

导出格式为 JSON,可备份后在其他设备导入恢复。

-
- -
- - 启用后台定时检查 -
-
- - -
-

定期检查 Ollama 连接状态和模型可用性,异常时通过系统通知提醒。

-
-
- - - -
@@ -385,22 +348,6 @@
-
- -
- - -
-
- - -
- -
-
@@ -443,12 +390,8 @@

💬 聊天功能

  • 流式回复 — 实时打字效果,随时点 ■ 停止
  • Think 推理 — 设置中开启,让模型展示深度思考过程(需模型支持,如 Qwen3)
  • 多模态 — 上传图片,模型需支持 Vision 能力
  • 文件分析 — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型
  • 上下文长度 — 设置中调整 num_ctx,越大记忆越长,消耗显存越多

🔧 Tool Calling(AI 自主操作)

  • 设置中开启后,AI 可以在对话中自主调用本地工具,像一个本地 Agent
  • 38 个工具,分为 6 类:
    • 文件系统(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
    • 系统 & 网络(6 个):run_command(执行命令)/ web_search(联网搜索,Bing + 百度 + Google 三引擎)/ web_fetch(网页抓取)/ download_file / diff_files / replace_in_files / read_multiple_files
    • Git(1 个):git(status / log / diff / add / commit / push / pull / branch / checkout / stash / reset / merge / clone / init / remote / tag,共 16 个子操作)
    • 浏览器控制(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close
    • 压缩(1 个):compress(创建/解压 zip/tar.gz)
    • 记忆 & 会话 & Skill(8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view
    • 子代理(1 个):spawn_task
  • run_command(命令执行)支持三模式切换:自动/需确认/禁用
  • 危险命令(rm -rfmkfs、反弹 shell 等)和系统路径(/etc~/.ssh 等)被自动拦截
  • 工具调用以可视化卡片展示状态(pending → running → success/error),在工作空间🔧工具页签中显示
  • 默认最大 85 轮工具调用循环,可在设置中调整,也可随时中断
  • 无超时限制,宁可等待也不中断,由用户控制生命周期
  • 独立工具自动并行执行,有依赖关系的工具串行执行
  • ⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)

🧠 Agent 记忆系统

  • 设置中开启后,AI 从对话中自动提取关键信息(事实/偏好/规则),跨会话持续积累
  • 对话满 6 条消息后自动触发记忆提取
  • 新对话时自动检索相关记忆注入上下文,让 AI "记住"你
  • 点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除
  • 记忆容量上限 500 条,超限时自动清理低价值条目(规则类型受保护)
  • 向量语义搜索:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)
  • 未选择嵌入模型时,使用关键词匹配检索记忆
  • AI 可通过 memory_search / memory_add / memory_replace / memory_remove 工具主动管理记忆
  • AI 可通过 session_list / session_read 查阅历史会话
  • 记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)
  • 记忆存储在本地 SQLite,不会上传到任何服务器
-

🎭 人格模式

  • 设置中可切换 6 种预设人格:通用助手 / 编程助手 / 写作助手 / 数据分析 / 翻译助手 / 代码审查
  • 不同人格有独立的 system prompt 和温度偏好
  • 通用助手不做额外约束,其他人格会调整行为风格

🔌 MCP(Model Context Protocol)

  • 支持连接外部 MCP Server,动态扩展工具能力
  • 设置面板可添加/启用/禁用/删除 MCP 服务器
  • MCP 工具以 mcp_{server}_{tool} 前缀注册,与内置工具统一调度
  • 启动时自动连接已启用的 MCP 服务器
-

⏰ 定时任务(Cron)

  • 支持一次性(at)和周期性(cron 表达式)任务调度
  • 任务触发后将描述填入输入框,可直接发送给 AI 执行
  • 设置面板可添加/管理/删除定时任务
-

💓 Heartbeat 后台检查

  • 启用后定期检查 Ollama 连接状态和模型可用性
  • 支持 10 分钟 / 30 分钟 / 1 小时 / 2 小时间隔(默认 1 小时)
  • 异常时通过系统通知提醒

🧠 技能自动生成

  • AI 完成包含 2+ 工具调用的复杂任务后,自动从执行轨迹中提取可复用技能
  • 技能包含工具链步骤、成功/失败次数、平均执行时长等统计
  • 下次遇到类似任务时自动匹配已有技能,跳过重新推理
  • AI 可通过 skill_list 查看技能列表(名称+描述+成功率),skill_view 查看完整工具链详情
  • 技能存储在本地 SQLite,不会上传到任何服务器
-

🩺 系统诊断

  • 设置面板点击「🩺 一键诊断」按钮,自动检查以下项目:
    • Ollama 服务连接与版本
    • 当前模型可用性与 Tool Calling / Vision 支持
    • 工作空间目录与磁盘空间
    • SQLite 数据库完整性
    • 工具安全配置
  • 诊断结果以 ✅/⚠️/❌ 标记,附带修复建议

🖥️ 布局说明

  • 左侧面板 — 执行日志,实时显示应用运行日志(连接、模型加载、工具调用等)
  • 中间区域 — 聊天消息,顶部 Header + 模型栏,底部输入框
  • 右侧面板 — 工作空间(常驻显示),包含 3 个页签:
    • 💻 命令行 Tab — 终端界面,实时流式输出,支持长时间运行(无超时),单一终端进程
    • 🔧 工具 Tab — 展示本轮对话的工具调用卡片,含状态统计(总数 ✅ 成功 ❌ 失败),AI 执行命令时自动切到此页签
    • 📁 文件 Tab — 浏览工作空间目录,点击文件预览内容(带行号),支持上级目录导航
  • 工作空间目录可在设置中修改

🕐 历史记录

  • 所有会话自动保存到本地 SQLite
  • 点击顶部 🕐 按钮查看、搜索、恢复历史会话
  • 支持导出 JSON / .metona 加密备份

⚡ 快捷键 & 命令

Enter发送消息
Shift + Enter换行
Ctrl + N新建会话
Ctrl + M记忆管理
Esc关闭弹窗

输入框命令(直接输入后发送):

/retry重试上一轮回复
/undo撤销最后一条用户消息及回复
/compress用 AI 摘要压缩长对话上下文
diff --git a/src/renderer/main.ts b/src/renderer/main.ts index e190719..424183c 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -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 { 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(); diff --git a/src/renderer/services/agent-engine.ts b/src/renderer/services/agent-engine.ts index ca62521..8f3446c 100644 --- a/src/renderer/services/agent-engine.ts +++ b/src/renderer/services/agent-engine.ts @@ -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('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'); diff --git a/src/renderer/services/cron-manager.ts b/src/renderer/services/cron-manager.ts deleted file mode 100644 index 93f22ac..0000000 --- a/src/renderer/services/cron-manager.ts +++ /dev/null @@ -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>(); - -/** - * 获取所有定时任务 - */ -export async function getCronTasks(): Promise { - const db = state.get(KEYS.DB); - if (!db) return []; - try { - return await db.getSetting('cron_tasks', []); - } catch { - return []; - } -} - -/** - * 保存定时任务 - */ -async function saveCronTasks(tasks: CronTask[]): Promise { - const db = state.get(KEYS.DB); - if (db) await db.saveSetting('cron_tasks', tasks); -} - -/** - * 添加定时任务 - */ -export async function addCronTask(task: Omit): Promise { - 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 { - const tasks = await getCronTasks(); - await saveCronTasks(tasks.filter(t => t.id !== id)); - clearTimer(id); - logInfo(`定时任务已删除: ${id}`); -} - -/** - * 切换定时任务启用状态 - */ -export async function toggleCronTask(id: string): Promise { - 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 { - 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 { - const tasks = await getCronTasks(); - let scheduled = 0; - for (const task of tasks) { - if (task.enabled) { - scheduleTask(task); - scheduled++; - } - } - if (scheduled > 0) { - logInfo(`定时任务已恢复: ${scheduled} 个任务`); - } -}