diff --git a/AdminPanel/index.html b/AdminPanel/index.html index 6ce6e87b..ea3608ec 100644 --- a/AdminPanel/index.html +++ b/AdminPanel/index.html @@ -436,6 +436,8 @@
管理定时巡航任务:为每个 Agent 配置独立的巡航间隔,保存后定时器自动重启。
+ +还没有配置巡航 Agent,请点击上方"添加 Agent"按钮。
'; + } + + if (statusSpan) { + statusSpan.textContent = '配置已加载。'; + statusSpan.className = 'status-message success'; + } + } catch (e) { + if (statusSpan) { + statusSpan.textContent = `加载失败:${e.message}`; + statusSpan.className = 'status-message error'; + } + } +} + +async function loadStatus() { + const statusContainer = document.getElementById('fa-runtime-status'); + if (!statusContainer) return; + + try { + const data = await apiFetch(`${API_BASE}/forum-assistant/status`); + let html = `${data.globalEnabled ? '运行中' : '已停止'}`; + html += ` | 活跃定时器: ${data.activeTimerCount || 0}`; + + if (data.agentStates && Object.keys(data.agentStates).length > 0) { + html += '还没有配置巡航 Agent,请点击上方"添加 Agent"按钮。
'; + } + } + }); + + btnGroup.appendChild(triggerBtn); + btnGroup.appendChild(deleteBtn); + header.appendChild(nameSpan); + header.appendChild(btnGroup); + + const body = document.createElement('div'); + body.className = 'aa-agent-card-body'; + + // 开关行 + const enableRow = document.createElement('div'); + enableRow.className = 'fa-switch-row'; + const enableLabel = document.createElement('label'); + enableLabel.className = 'switch-container'; + const enableText = document.createElement('span'); + enableText.textContent = '启用巡航'; + const enableInput = document.createElement('input'); + enableInput.type = 'checkbox'; + enableInput.className = 'fa-agent-enabled'; + enableInput.checked = agent.enabled; + const slider = document.createElement('span'); + slider.className = 'switch-slider'; + enableLabel.appendChild(enableText); + enableLabel.appendChild(enableInput); + enableLabel.appendChild(slider); + enableRow.appendChild(enableLabel); + + // 间隔设置 + const intervalGroup = document.createElement('div'); + intervalGroup.className = 'aa-field-group aa-field-group-full'; + const intervalLabel = document.createElement('label'); + intervalLabel.textContent = '巡航间隔(分钟)'; + const intervalInput = document.createElement('input'); + intervalInput.type = 'number'; + intervalInput.className = 'fa-agent-interval'; + intervalInput.min = '10'; + intervalInput.placeholder = '最小 10 分钟'; + intervalInput.value = agent.intervalMinutes || 60; + const intervalHint = document.createElement('p'); + intervalHint.className = 'aa-hint'; + intervalHint.textContent = '每隔多少分钟触发一次巡航(最小 10 分钟)。定时器在保存配置后自动重启。'; + intervalGroup.appendChild(intervalLabel); + intervalGroup.appendChild(intervalInput); + intervalGroup.appendChild(intervalHint); + + body.appendChild(enableRow); + body.appendChild(intervalGroup); + + card.appendChild(header); + card.appendChild(body); + container.appendChild(card); +} + +function handleAddAgent() { + const nameInput = document.getElementById('fa-new-agent-name'); + if (!nameInput) return; + + const name = nameInput.value.trim(); + if (!name) { + showMessage('请输入 Agent 的中文名。', 'info'); + return; + } + + const container = document.getElementById('fa-agent-cards-container'); + if (container) { + const existing = Array.from(container.querySelectorAll('.fa-agent-name')).map(el => el.textContent); + if (existing.includes(name)) { + showMessage(`"${name}" 已在巡航列表中。`, 'info'); + return; + } + } + + addAgentCard({ chineseName: name, enabled: true, intervalMinutes: 60 }); + nameInput.value = ''; + showMessage(`已添加 "${name}"。`, 'success'); +} + +async function saveConfig() { + const container = document.getElementById('fa-agent-cards-container'); + const globalSwitch = document.getElementById('fa-global-enabled'); + const statusSpan = document.getElementById('fa-status'); + + if (!container) return; + + const globalEnabled = globalSwitch ? globalSwitch.checked : false; + const cards = Array.from(container.querySelectorAll('.fa-agent-card')); + const agents = []; + + for (const card of cards) { + const nameEl = card.querySelector('.fa-agent-name'); + const enabledInput = card.querySelector('.fa-agent-enabled'); + const intervalInput = card.querySelector('.fa-agent-interval'); + + const chineseName = nameEl ? nameEl.textContent : ''; + if (!chineseName) continue; + + agents.push({ + chineseName, + enabled: enabledInput ? enabledInput.checked : false, + intervalMinutes: intervalInput ? parseInt(intervalInput.value) || 60 : 60 + }); + } + + if (statusSpan) { + statusSpan.textContent = '正在保存...'; + statusSpan.className = 'status-message info'; + } + + try { + await apiFetch(`${API_BASE}/forum-assistant/config`, { + method: 'POST', + body: JSON.stringify({ globalEnabled, agents }) + }); + showMessage('论坛巡航配置已保存,定时器已重启。', 'success'); + if (statusSpan) { + statusSpan.textContent = '保存成功。'; + statusSpan.className = 'status-message success'; + } + await loadStatus(); + } catch (e) { + if (statusSpan) { + statusSpan.textContent = `保存失败:${e.message}`; + statusSpan.className = 'status-message error'; + } + } +} diff --git a/AdminPanel/script.js b/AdminPanel/script.js index b35c2493..0e598936 100644 --- a/AdminPanel/script.js +++ b/AdminPanel/script.js @@ -13,6 +13,7 @@ import { initializePreprocessorOrderManager } from './js/preprocessor-manager.js import { initializeSemanticGroupsEditor } from './js/semantic-groups-editor.js'; import { initializeThinkingChainsEditor } from './js/thinking-chains-editor.js'; import { initializeVCPForum } from './js/forum.js'; +import { initializeForumAssistantConfig } from './js/forum-assistant-config.js'; import { initializeScheduleManager } from './js/schedule-manager.js'; import { initializeRAGTuning } from './js/rag-tuning.js'; import { initializeDreamManager } from './js/dream-manager.js'; @@ -153,6 +154,9 @@ document.addEventListener('DOMContentLoaded', async () => { case 'vcp-forum-section': initializeVCPForum(); break; + case 'forum-assistant-config-section': + initializeForumAssistantConfig(); + break; case 'schedule-manager-section': initializeScheduleManager(); break; diff --git a/Plugin/VCPForumAssistant/plugin-manifest.json b/Plugin/VCPForumAssistant/plugin-manifest.json new file mode 100644 index 00000000..cf9b781b --- /dev/null +++ b/Plugin/VCPForumAssistant/plugin-manifest.json @@ -0,0 +1,16 @@ +{ + "manifestVersion": "1.0.0", + "name": "VCPForumAssistant", + "displayName": "VCP论坛巡航助手", + "version": "2.0.0", + "description": "常驻混合服务插件:定时唤醒 Agent 去逛 VCP 论坛,支持 per-Agent 巡航间隔、全局开关、JSON 持久化状态管理,并通过管理面板配置。", + "author": "Roo", + "pluginType": "hybridservice", + "entryPoint": { + "script": "vcp-forum-assistant.js" + }, + "communication": { + "protocol": "direct" + }, + "capabilities": {} +} diff --git a/Plugin/VCPForumAssistant/vcp-forum-assistant.js b/Plugin/VCPForumAssistant/vcp-forum-assistant.js index 281db15a..20a8fde1 100644 --- a/Plugin/VCPForumAssistant/vcp-forum-assistant.js +++ b/Plugin/VCPForumAssistant/vcp-forum-assistant.js @@ -1,129 +1,299 @@ -// vcp-forum-assistant.js +// vcp-forum-assistant.js — VCP论坛巡航助手 (hybridservice) +// 常驻进程,管理 per-Agent 巡航定时器,通过 inject_tools 注入论坛工具,JSON 持久化 + const http = require('http'); const path = require('path'); -const dotenv = require('dotenv'); -const fs = require('fs').promises; - -// 从 PluginManager 注入的环境变量中获取项目根路径 -const projectBasePath = process.env.PROJECT_BASE_PATH; +const fs = require('fs'); +const fsPromises = require('fs').promises; -if (!projectBasePath) { - console.error('Error: PROJECT_BASE_PATH environment variable not set. Cannot locate config.env.'); - process.exit(1); -} +const DATA_FILE = path.join(__dirname, 'patrol-data.json'); -// 加载根目录的 config.env 文件 -dotenv.config({ path: path.join(projectBasePath, 'config.env') }); +// PluginManager 注入的运行时配置 +let VCP_PORT = '8080'; +let VCP_KEY = ''; +let PROJECT_BASE_PATH = ''; +let DEBUG_MODE = false; -const FORUM_DIR = path.join(projectBasePath, 'dailynote', 'VCP论坛'); +// 内存状态 +let patrolConfig = { globalEnabled: false, agents: [] }; +let patrolState = {}; // { [agentName]: { lastRunTime, lastResult } } +let activeTimers = new Map(); // agentName -> intervalId -// 从环境变量中获取 PORT 和 Key -const port = process.env.PORT || '8080'; -const apiKey = process.env.Key; +// ============================================ +// 持久化:读写 patrol-data.json +// ============================================ +function loadData() { + try { + if (fs.existsSync(DATA_FILE)) { + const raw = fs.readFileSync(DATA_FILE, 'utf-8'); + const data = JSON.parse(raw); + patrolConfig = data.config || { globalEnabled: false, agents: [] }; + patrolState = data.state || {}; + } + } catch (e) { + console.error('[ForumAssistant] 加载 patrol-data.json 失败:', e.message); + } +} -if (!apiKey) { - console.error('Error: API Key (Key) is not defined in the environment variables.'); - process.exit(1); // 错误退出 +async function saveData() { + try { + const data = { config: patrolConfig, state: patrolState }; + await fsPromises.writeFile(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8'); + } catch (e) { + console.error('[ForumAssistant] 保存 patrol-data.json 失败:', e.message); + } } -/** - * Lists all posts, grouped by board. - * @returns {Promise