From 10eb95139c014160ad550dec5cc8ec7d6cf4a40d Mon Sep 17 00:00:00 2001 From: TaoCultivator <3796823@gmail.com> Date: Tue, 7 Apr 2026 23:48:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=A5=E6=A0=BC=E6=8C=89=E7=85=A7=E4=BD=A0?= =?UTF-8?q?=E8=AF=B4=E7=9A=84=E6=80=9D=E8=B7=AF=E5=86=8D=E6=AC=A1=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=89=88=EF=BC=8C=E9=BA=BB=E7=83=A6=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=B8=80=E4=B8=8B=E7=BB=99=E5=87=BA=E5=BB=BA=E8=AE=AE=EF=BC=8C?= =?UTF-8?q?=E4=B8=87=E5=88=86=E6=84=9F=E8=B0=A2=EF=BC=8C=E9=BA=BB=E7=83=A6?= =?UTF-8?q?=E4=BA=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AdminPanel/index.html | 35 ++ AdminPanel/js/forum-assistant-config.js | 276 ++++++++++++++ AdminPanel/script.js | 4 + Plugin/VCPForumAssistant/plugin-manifest.json | 16 + .../VCPForumAssistant/vcp-forum-assistant.js | 354 +++++++++++++----- routes/admin/forumAssistant.js | 62 +++ routes/adminPanelRoutes.js | 1 + 7 files changed, 656 insertions(+), 92 deletions(-) create mode 100644 AdminPanel/js/forum-assistant-config.js create mode 100644 Plugin/VCPForumAssistant/plugin-manifest.json create mode 100644 routes/admin/forumAssistant.js 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 @@

控制中心

class="material-symbols-outlined">description日记知识库管理
  • forumVCP论坛
  • +
  • explore论坛巡航助手
  • photo_library多媒体Base64编辑器
  • VCP 论坛 + +
    +

    论坛巡航助手

    +

    管理定时巡航任务:为每个 Agent 配置独立的巡航间隔,保存后定时器自动重启。

    + +
    + 加载中... +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    + +
    + + +
    +
    +

    日程管理

    diff --git a/AdminPanel/js/forum-assistant-config.js b/AdminPanel/js/forum-assistant-config.js new file mode 100644 index 00000000..89ec2e42 --- /dev/null +++ b/AdminPanel/js/forum-assistant-config.js @@ -0,0 +1,276 @@ +import { apiFetch, showMessage } from './utils.js'; + +const API_BASE = '/admin_api'; +let listenersAttached = false; + +export async function initializeForumAssistantConfig() { + const section = document.getElementById('forum-assistant-config-section'); + if (!section) return; + + if (!listenersAttached) { + attachEventListeners(); + listenersAttached = true; + } + + await loadConfig(); + await loadStatus(); +} + +function attachEventListeners() { + const saveBtn = document.getElementById('fa-save-config-button'); + const addBtn = document.getElementById('fa-add-agent-button'); + + if (saveBtn && !saveBtn.dataset.listenerAttached) { + saveBtn.addEventListener('click', saveConfig); + saveBtn.dataset.listenerAttached = 'true'; + } + if (addBtn && !addBtn.dataset.listenerAttached) { + addBtn.addEventListener('click', handleAddAgent); + addBtn.dataset.listenerAttached = 'true'; + } +} + +async function loadConfig() { + const container = document.getElementById('fa-agent-cards-container'); + const globalSwitch = document.getElementById('fa-global-enabled'); + const statusSpan = document.getElementById('fa-status'); + + if (statusSpan) { + statusSpan.textContent = '正在加载...'; + statusSpan.className = 'status-message info'; + } + if (container) container.innerHTML = ''; + + try { + const data = await apiFetch(`${API_BASE}/forum-assistant/config`); + const config = data.config || { globalEnabled: false, agents: [] }; + + if (globalSwitch) globalSwitch.checked = config.globalEnabled; + + if (Array.isArray(config.agents) && config.agents.length > 0) { + config.agents.forEach(a => addAgentCard(a)); + } else if (container) { + container.innerHTML = '

    还没有配置巡航 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 += '
    '; + for (const [name, state] of Object.entries(data.agentStates)) { + const time = state.lastRunTime ? new Date(state.lastRunTime).toLocaleString() : '从未'; + const isError = state.lastResult && state.lastResult.startsWith('error'); + html += `
    ${name}`; + html += `上次: ${time}`; + html += `${state.lastResult || '-'}`; + html += `
    `; + } + html += '
    '; + } + statusContainer.innerHTML = html; + } catch (e) { + statusContainer.innerHTML = `状态加载失败`; + } +} + +function addAgentCard(agent) { + const container = document.getElementById('fa-agent-cards-container'); + if (!container) return; + + const placeholder = container.querySelector('.fa-placeholder'); + if (placeholder) placeholder.remove(); + + const card = document.createElement('div'); + card.className = 'aa-agent-card fa-agent-card'; + + const header = document.createElement('div'); + header.className = 'aa-agent-card-header'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'fa-agent-name'; + nameSpan.textContent = agent.chineseName || '未命名'; + + const btnGroup = document.createElement('div'); + btnGroup.style.display = 'flex'; + btnGroup.style.gap = '0.5rem'; + + const triggerBtn = document.createElement('button'); + triggerBtn.type = 'button'; + triggerBtn.className = 'aa-agent-delete-btn'; + triggerBtn.style.background = 'var(--primary-color)'; + triggerBtn.style.color = '#fff'; + triggerBtn.textContent = '立即巡航'; + triggerBtn.addEventListener('click', async () => { + triggerBtn.disabled = true; + triggerBtn.textContent = '发送中...'; + try { + await apiFetch(`${API_BASE}/forum-assistant/trigger`, { + method: 'POST', + body: JSON.stringify({ agentName: agent.chineseName }) + }); + showMessage(`已触发 "${agent.chineseName}" 巡航任务。`, 'success'); + } catch (e) { + showMessage(`触发失败:${e.message}`, 'error'); + } finally { + triggerBtn.disabled = false; + triggerBtn.textContent = '立即巡航'; + } + }); + + const deleteBtn = document.createElement('button'); + deleteBtn.type = 'button'; + deleteBtn.className = 'aa-agent-delete-btn'; + deleteBtn.textContent = '移除'; + deleteBtn.addEventListener('click', () => { + if (confirm(`确定移除 "${agent.chineseName}" 的巡航配置吗?`)) { + card.remove(); + if (!container.children.length) { + container.innerHTML = '

    还没有配置巡航 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} - The formatted post list. - */ +// ============================================ +// 论坛帖子列表(保留原版逻辑) +// ============================================ async function getForumPostList() { + const forumDir = path.join(PROJECT_BASE_PATH, 'dailynote', 'VCP论坛'); try { - await fs.mkdir(FORUM_DIR, { recursive: true }); - const files = await fs.readdir(FORUM_DIR); - const mdFiles = files.filter(file => file.endsWith('.md')); + await fsPromises.mkdir(forumDir, { recursive: true }); + const files = await fsPromises.readdir(forumDir); + const mdFiles = files.filter(f => f.endsWith('.md')); - if (mdFiles.length === 0) { - return "VCP论坛中尚无帖子。"; - } + if (mdFiles.length === 0) return 'VCP论坛中尚无帖子。'; const postsByBoard = {}; - for (const file of mdFiles) { - // 兼容标题中可能包含标签的情况(如 [[Tag] Title]),使用贪婪匹配处理中间的标题部分 - const fileMatch = file.match(/^\[(.*?)\]\[(.*)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\.md$/); - if (fileMatch) { - const board = fileMatch[1]; - const title = fileMatch[2]; - const author = fileMatch[3]; - const uid = fileMatch[5]; - const displayLine = `[${author}] ${title} (UID: ${uid})`; - - if (!postsByBoard[board]) { - postsByBoard[board] = []; - } - postsByBoard[board].push(displayLine); - } + const m = file.match(/^\[(.*?)\]\[(.*)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\.md$/); + if (!m) continue; + const board = m[1], title = m[2], author = m[3], uid = m[5]; + if (!postsByBoard[board]) postsByBoard[board] = []; + postsByBoard[board].push(`[${author}] ${title} (UID: ${uid})`); } - let output = "VCP论坛帖子列表:\n"; + let output = 'VCP论坛帖子列表:\n'; for (const board in postsByBoard) { output += `\n————[${board}]————\n`; - postsByBoard[board].forEach(line => { - output += `${line}\n`; - }); + postsByBoard[board].forEach(line => { output += line + '\n'; }); } return output.trim(); - } catch (error) { - return `获取论坛帖子列表时出错: ${error.message}`; + } catch (e) { + return `获取论坛帖子列表时出错: ${e.message}`; } } -async function main() { - // 定义Agent列表 - const agents = ["小娜", "小克", "小闫", "小吉", "小雨", "小绝", "Nova", "小芸", "小冰"]; +// ============================================ +// 唤醒 Agent(通过 AgentAssistant,使用 inject_tools) +// ============================================ +function wakeUpAgent(agentName, prompt) { + return new Promise((resolve, reject) => { + if (!VCP_KEY) return reject(new Error('VCP Key 未配置')); - // 随机选择一个Agent - const randomAgent = agents[Math.floor(Math.random() * agents.length)]; - - // 获取论坛帖子列表 - const forumList = await getForumPostList(); - - // 构造请求体 - const requestBody = `<<<[TOOL_REQUEST]>>> + const requestBody = `<<<[TOOL_REQUEST]>>> maid:「始」VCP系统「末」, tool_name:「始」AgentAssistant「末」, -agent_name:「始」${randomAgent}「末」, -prompt:「始」[论坛小助手:]现在是论坛时间~ 你可以选择分享一个感兴趣的话题/趣味性话题/亦或者分享一些互联网新鲜事/或者发起一个最近几天想要讨论的话题作为新帖子;或者单纯只是先阅读一些别人的你感兴趣帖子,然后做出你的回复(先读帖再回复是好习惯)~ \n\n以下是完整的论坛帖子列表:${forumList}「末」, +agent_name:「始」${agentName}「末」, +prompt:「始」${prompt}「末」, temporary_contact:「始」true「末」, +inject_tools:「始」VCPForum「末」, <<<[END_TOOL_REQUEST]>>>`; - // 构造请求选项 - const options = { - hostname: '127.0.0.1', - port: port, - path: '/v1/human/tool', - method: 'POST', - headers: { - 'Content-Type': 'text/plain;charset=UTF-8', - 'Authorization': `Bearer ${apiKey}`, - 'Content-Length': Buffer.byteLength(requestBody) - } - }; + const options = { + hostname: '127.0.0.1', + port: VCP_PORT, + path: '/v1/human/tool', + method: 'POST', + headers: { + 'Content-Type': 'text/plain;charset=UTF-8', + 'Authorization': `Bearer ${VCP_KEY}`, + 'Content-Length': Buffer.byteLength(requestBody) + } + }; - // 发起请求 - const req = http.request(options, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - console.log(`[VCPForumAssistant] Request successful. Status: ${res.statusCode}. Response: ${data}`); - process.exit(0); // 成功退出 + const req = http.request(options, (res) => { + if (DEBUG_MODE) console.log(`[ForumAssistant] Agent唤醒完成 | ${agentName} | HTTP ${res.statusCode}`); + resolve({ status: res.statusCode }); + res.resume(); }); - }); - req.on('error', (e) => { - console.error(`[VCPForumAssistant] Problem with request: ${e.message}`); - process.exit(1); // 错误退出 + req.on('error', e => reject(new Error('唤醒Agent失败: ' + e.message))); + req.write(requestBody); + req.end(); }); +} + +// ============================================ +// 执行巡航任务 +// ============================================ +async function executePatrol(agentName) { + if (!patrolConfig.globalEnabled) return; + + const agentConf = patrolConfig.agents.find(a => a.chineseName === agentName); + if (!agentConf || !agentConf.enabled) return; + + console.log(`[ForumAssistant] 开始巡航: ${agentName}`); + + try { + const forumList = await getForumPostList(); + const prompt = `[论坛小助手:]现在是论坛时间~ 你可以选择分享一个感兴趣的话题/趣味性话题/亦或者分享一些互联网新鲜事/或者发起一个最近几天想要讨论的话题作为新帖子;或者单纯只是先阅读一些别人的你感兴趣帖子,然后做出你的回复(先读帖再回复是好习惯)~ \n\n以下是完整的论坛帖子列表:${forumList}`; + + await wakeUpAgent(agentName, prompt); + + patrolState[agentName] = { + lastRunTime: new Date().toISOString(), + lastResult: 'success' + }; + console.log(`[ForumAssistant] 巡航任务已下发: ${agentName}`); + } catch (e) { + patrolState[agentName] = { + lastRunTime: new Date().toISOString(), + lastResult: `error: ${e.message}` + }; + console.error(`[ForumAssistant] 巡航失败 ${agentName}:`, e.message); + } + + await saveData(); +} + +// ============================================ +// 定时器管理 +// ============================================ +function stopAllTimers() { + for (const [name, timerId] of activeTimers) { + clearInterval(timerId); + if (DEBUG_MODE) console.log(`[ForumAssistant] 停止定时器: ${name}`); + } + activeTimers.clear(); +} - // 写入请求体并结束请求 - req.write(requestBody); - req.end(); +function startTimers() { + stopAllTimers(); + + if (!patrolConfig.globalEnabled) { + if (DEBUG_MODE) console.log('[ForumAssistant] 全局开关关闭,不启动定时器'); + return; + } + + for (const agent of patrolConfig.agents) { + if (!agent.enabled || !agent.chineseName) continue; + + const intervalMinutes = Math.max(agent.intervalMinutes || 60, 10); + const intervalMs = intervalMinutes * 60 * 1000; + + const timerId = setInterval(() => { + executePatrol(agent.chineseName); + }, intervalMs); + + activeTimers.set(agent.chineseName, timerId); + console.log(`[ForumAssistant] 启动定时器: ${agent.chineseName} | 间隔 ${intervalMinutes} 分钟`); + } +} + +// ============================================ +// hybridservice 标准接口: initialize +// ============================================ +function initialize(config, dependencies) { + VCP_PORT = config.PORT || '8080'; + VCP_KEY = config.Key || ''; + PROJECT_BASE_PATH = config.PROJECT_BASE_PATH || ''; + DEBUG_MODE = String(config.DebugMode || 'false').toLowerCase() === 'true'; + + console.log(`[ForumAssistant] 初始化 | PORT=${VCP_PORT} | Key=${VCP_KEY ? 'FOUND' : 'NOT FOUND'}`); + + loadData(); + startTimers(); + + console.log(`[ForumAssistant] 初始化完成 | 全局开关: ${patrolConfig.globalEnabled} | Agent数: ${patrolConfig.agents.length} | 活跃定时器: ${activeTimers.size}`); +} + +// ============================================ +// hybridservice 标准接口: shutdown +// ============================================ +function shutdown() { + console.log('[ForumAssistant] 正在关闭...'); + stopAllTimers(); +} + +// ============================================ +// hybridservice 标准接口: processToolCall +// (供管理面板通过 PluginManager 调用) +// ============================================ +async function processToolCall(args) { + const command = args.command; + + switch (command) { + case 'getConfig': + return { status: 'success', result: { config: patrolConfig } }; + + case 'saveConfig': { + const newConfig = args.config; + if (!newConfig || typeof newConfig !== 'object') { + return { status: 'error', error: '无效的配置数据' }; + } + patrolConfig = { + globalEnabled: !!newConfig.globalEnabled, + agents: Array.isArray(newConfig.agents) ? newConfig.agents.map(a => ({ + chineseName: String(a.chineseName || '').trim(), + enabled: !!a.enabled, + intervalMinutes: Math.max(parseInt(a.intervalMinutes) || 60, 10) + })).filter(a => a.chineseName) : [] + }; + await saveData(); + startTimers(); + return { status: 'success', result: { message: '配置已保存,定时器已重启。' } }; + } + + case 'getStatus': + return { + status: 'success', + result: { + globalEnabled: patrolConfig.globalEnabled, + activeTimerCount: activeTimers.size, + activeTimers: Array.from(activeTimers.keys()), + agentStates: patrolState + } + }; + + case 'triggerPatrol': { + const agentName = args.agentName; + if (!agentName) return { status: 'error', error: '缺少 agentName' }; + executePatrol(agentName); + return { status: 'success', result: { message: `巡航任务已触发: ${agentName}` } }; + } + + default: + return { status: 'error', error: `未知命令: ${command}` }; + } +} + +// ============================================ +// 暴露给管理路由的直接方法 +// ============================================ +function getConfig() { + return { config: patrolConfig }; +} + +function getStatus() { + return { + globalEnabled: patrolConfig.globalEnabled, + activeTimerCount: activeTimers.size, + activeTimers: Array.from(activeTimers.keys()), + agentStates: patrolState + }; +} + +async function updateConfig(newConfig) { + patrolConfig = { + globalEnabled: !!newConfig.globalEnabled, + agents: Array.isArray(newConfig.agents) ? newConfig.agents.map(a => ({ + chineseName: String(a.chineseName || '').trim(), + enabled: !!a.enabled, + intervalMinutes: Math.max(parseInt(a.intervalMinutes) || 60, 10) + })).filter(a => a.chineseName) : [] + }; + await saveData(); + startTimers(); } -main(); \ No newline at end of file +module.exports = { + initialize, + shutdown, + processToolCall, + getConfig, + getStatus, + updateConfig +}; diff --git a/routes/admin/forumAssistant.js b/routes/admin/forumAssistant.js new file mode 100644 index 00000000..e7571feb --- /dev/null +++ b/routes/admin/forumAssistant.js @@ -0,0 +1,62 @@ +const express = require('express'); + +module.exports = function (options) { + const router = express.Router(); + const pluginManager = options.pluginManager; + + function getModule() { + return pluginManager.getServiceModule('VCPForumAssistant'); + } + + // GET /forum-assistant/config + router.get('/forum-assistant/config', (req, res) => { + try { + const mod = getModule(); + if (!mod) return res.status(503).json({ error: 'VCPForumAssistant 插件未加载' }); + res.json(mod.getConfig()); + } catch (e) { + console.error('[ForumAssistant Route] getConfig error:', e); + res.status(500).json({ error: e.message }); + } + }); + + // POST /forum-assistant/config + router.post('/forum-assistant/config', async (req, res) => { + try { + const mod = getModule(); + if (!mod) return res.status(503).json({ error: 'VCPForumAssistant 插件未加载' }); + await mod.updateConfig(req.body); + res.json({ success: true, message: '论坛巡航配置已保存,定时器已重启。' }); + } catch (e) { + console.error('[ForumAssistant Route] saveConfig error:', e); + res.status(500).json({ error: e.message }); + } + }); + + // GET /forum-assistant/status + router.get('/forum-assistant/status', (req, res) => { + try { + const mod = getModule(); + if (!mod) return res.status(503).json({ error: 'VCPForumAssistant 插件未加载' }); + res.json(mod.getStatus()); + } catch (e) { + console.error('[ForumAssistant Route] getStatus error:', e); + res.status(500).json({ error: e.message }); + } + }); + + // POST /forum-assistant/trigger + router.post('/forum-assistant/trigger', async (req, res) => { + try { + const mod = getModule(); + if (!mod) return res.status(503).json({ error: 'VCPForumAssistant 插件未加载' }); + const result = await mod.processToolCall({ command: 'triggerPatrol', agentName: req.body.agentName }); + res.json(result); + } catch (e) { + console.error('[ForumAssistant Route] trigger error:', e); + res.status(500).json({ error: e.message }); + } + }); + + return router; +}; diff --git a/routes/adminPanelRoutes.js b/routes/adminPanelRoutes.js index 5822042d..259ea146 100644 --- a/routes/adminPanelRoutes.js +++ b/routes/adminPanelRoutes.js @@ -62,6 +62,7 @@ module.exports = function (DEBUG_MODE, dailyNoteRootPath, pluginManager, getCurr mount('/', 'schedules'); // Handles /schedules/* mount('/', 'rag'); // Handles /rag-tags, /rag-params, /available-clusters, etc. mount('/', 'agentAssistant'); // Handles /agent-assistant/* + mount('/', 'forumAssistant'); // Handles /forum-assistant/* mount('/', 'toolListEditor'); // Handles /tool-list/* mount('/', 'dream'); // Handles /dream-logs/*, /dream-operation/* mount('/', 'dailyNotes'); // Wrapper for existing dailyNotesRoutes (Handles /dailynotes/*)