Files
tool-node/public/index.html
zhaojunlong 4f504447a1 根据提供的code differences信息,我发现没有具体的代码差异内容。由于没有实际的代码变更信息,我将生成一个通用的示例commit message:
```
docs(changelog): 更新版本发布说明

- 添加了最新的功能变更记录
- 修复了已知问题的描述
- 更新了API文档的相关部分
```
2026-03-10 16:16:57 +08:00

743 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公告抓取与分析工具</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1100px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px 30px;
text-align: center;
}
.header h1 { font-size: 24px; margin-bottom: 6px; }
.header p { opacity: 0.85; font-size: 13px; }
/* Tabs */
.tabs { display: flex; background: #f5f5f5; border-bottom: 2px solid #e0e0e0; }
.tab {
flex: 1; padding: 14px; text-align: center; cursor: pointer;
background: #f5f5f5; border: none; font-size: 15px; transition: all 0.2s;
}
.tab:hover { background: #e8e8e8; }
.tab.active { background: white; color: #667eea; font-weight: bold; border-bottom: 3px solid #667eea; }
.content { padding: 24px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
/* Form elements */
label { display: block; font-weight: 600; margin-bottom: 6px; color: #333; font-size: 14px; }
input[type="text"], input[type="number"], input[type="password"], textarea, select {
width: 100%; padding: 10px 12px; border: 1px solid #d0d0d0; border-radius: 8px;
font-size: 14px; transition: border 0.2s;
}
input:focus, textarea:focus, select:focus { outline: none; border-color: #667eea; }
textarea { resize: vertical; min-height: 80px; font-family: inherit; }
.form-group { margin-bottom: 16px; }
.form-row { display: flex; gap: 16px; }
.form-row > * { flex: 1; }
/* Buttons */
.btn {
padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer;
font-size: 14px; font-weight: 600; transition: all 0.2s; display: inline-flex;
align-items: center; gap: 6px;
}
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #218838; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.btn-secondary { background: #6c757d; color: white; }
.btn-secondary:hover { background: #5a6268; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 6px; }
/* Table */
table { width: 100%; border-collapse: collapse; }
th { background: #f8f9fa; padding: 12px; text-align: left; font-size: 13px; color: #555; border-bottom: 2px solid #e0e0e0; }
td { padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }
tr:hover { background: #f8f9ff; }
.prompt-cell { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Tags */
.tag { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.tag-on { background: #d4edda; color: #155724; }
.tag-off { background: #f8d7da; color: #721c24; }
/* Modal */
.modal {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;
}
.modal.show { display: flex; }
.modal-body {
background: white; border-radius: 12px; padding: 28px; width: 600px; max-width: 90vw;
max-height: 85vh; overflow-y: auto; box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.modal-body h3 { margin-bottom: 20px; color: #333; }
/* Status / alerts */
.status-box {
padding: 14px; border-radius: 8px; margin-top: 16px; font-size: 14px;
}
.status-info { background: #e8f0fe; color: #1a56db; }
.status-success { background: #d4edda; color: #155724; }
.status-error { background: #f8d7da; color: #721c24; }
.empty-state { text-align: center; padding: 40px; color: #999; }
/* Result items */
.result-card {
border: 1px solid #e8e8e8; border-radius: 10px; padding: 16px; margin-bottom: 12px;
transition: box-shadow 0.2s;
}
.result-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,0.08); }
.result-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.result-card-header h4 { font-size: 15px; color: #333; }
.result-card-header span { font-size: 12px; color: #888; }
.result-item { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; gap: 12px; }
.result-item:last-child { border-bottom: none; }
.result-item .type { min-width: 80px; font-weight: 600; color: #667eea; font-size: 13px; }
.result-item .name { flex: 1; font-size: 13px; }
.result-item .amount { min-width: 100px; text-align: right; font-weight: 600; color: #e74c3c; font-size: 13px; }
.result-item .date { min-width: 90px; text-align: center; color: #888; font-size: 12px; }
.result-item a { color: #667eea; text-decoration: none; font-size: 12px; }
.result-item a:hover { text-decoration: underline; }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }
/* Agent config */
.config-section { background: #f9f9f9; border-radius: 10px; padding: 20px; margin-bottom: 20px; }
.config-section h3 { margin-bottom: 14px; font-size: 16px; color: #444; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>公告抓取与分析工具</h1>
<p>基于 Agent API 的智能公告信息采集</p>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('tasks')">任务配置</button>
<button class="tab" onclick="switchTab('results')">抓取结果</button>
<button class="tab" onclick="switchTab('settings')">系统设置</button>
</div>
<div class="content">
<!-- ===== 任务配置 Tab ===== -->
<div id="tab-tasks" class="tab-content active">
<div class="toolbar">
<div>
<button class="btn btn-primary" onclick="openTaskModal()">+ 新增任务</button>
<button class="btn btn-success" id="btnRunAll" onclick="runAllTasks()">运行全部启用</button>
</div>
<div style="font-size:13px;color:#888;" id="taskSummary"></div>
</div>
<table>
<thead>
<tr>
<th>城市</th>
<th>提示词</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tasksTbody">
<tr><td colspan="4" class="empty-state">加载中...</td></tr>
</tbody>
</table>
<div id="batchRunStatus" class="status-box status-info" style="display:none;"></div>
</div>
<!-- ===== 抓取结果 Tab ===== -->
<div id="tab-results" class="tab-content">
<div class="toolbar">
<div style="display:flex;gap:10px;align-items:center;">
<select id="filterCity" onchange="loadResults()" style="width:auto;">
<option value="">全部城市</option>
</select>
<button class="btn btn-secondary btn-sm" onclick="loadResults()">刷新</button>
</div>
<button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button>
</div>
<div id="resultsList"></div>
<div id="resultsPagination" style="text-align:center;margin-top:16px;"></div>
</div>
<!-- ===== 系统设置 Tab ===== -->
<div id="tab-settings" class="tab-content">
<!-- Agent 配置 -->
<div class="config-section">
<h3>Agent 服务配置</h3>
<div class="form-row">
<div class="form-group">
<label>服务地址 (baseUrl)</label>
<input type="text" id="cfgBaseUrl" placeholder="http://192.168.3.65:18625">
</div>
<div class="form-group">
<label>useBrowser</label>
<select id="cfgUseBrowser">
<option value="false">false</option>
<option value="true">true</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>轮询间隔 (毫秒)</label>
<input type="number" id="cfgPollInterval" placeholder="3000">
</div>
<div class="form-group">
<label>超时时间 (毫秒)</label>
<input type="number" id="cfgTimeout" placeholder="300000">
</div>
</div>
</div>
<!-- 定时任务配置 -->
<div class="config-section">
<h3>定时任务</h3>
<div class="form-row">
<div class="form-group">
<label>启用</label>
<select id="cfgSchedulerEnabled">
<option value="false">禁用</option>
<option value="true">启用</option>
</select>
</div>
<div class="form-group">
<label>Cron 表达式</label>
<input type="text" id="cfgCronTime" placeholder="0 9 * * *">
</div>
<div class="form-group">
<label>描述</label>
<input type="text" id="cfgDescription" placeholder="每天9点采集">
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="triggerScheduledTask()">立即执行定时任务</button>
<span id="schedulerStatus" style="margin-left:12px;font-size:13px;color:#888;"></span>
</div>
<!-- 邮件配置 -->
<div class="config-section">
<h3>邮件配置</h3>
<div class="form-row">
<div class="form-group">
<label>SMTP 服务器</label>
<input type="text" id="cfgSmtpHost" placeholder="smtp.qq.com">
</div>
<div class="form-group">
<label>端口</label>
<input type="number" id="cfgSmtpPort" placeholder="587">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>用户名</label>
<input type="text" id="cfgSmtpUser" placeholder="your-email@qq.com">
</div>
<div class="form-group">
<label>密码/授权码</label>
<input type="password" id="cfgSmtpPass" placeholder="授权码">
</div>
</div>
<div class="form-group">
<label>收件人 (多个用逗号分隔)</label>
<input type="text" id="cfgRecipients" placeholder="a@example.com, b@example.com">
</div>
</div>
<button class="btn btn-primary" onclick="saveSettings()">保存全部设置</button>
<span id="settingsSaveStatus" style="margin-left:12px;font-size:13px;"></span>
</div>
</div>
</div>
<!-- 任务编辑弹窗 -->
<div class="modal" id="taskModal" onclick="if(event.target===this)closeTaskModal()">
<div class="modal-body">
<h3 id="taskModalTitle">新增任务</h3>
<form onsubmit="saveTask(event)">
<input type="hidden" id="taskEditId">
<div class="form-group">
<label>城市名称</label>
<input type="text" id="taskCity" placeholder="如:南京市" required>
</div>
<div class="form-group">
<label>提示词 (在提示词中包含目标网址和抓取要求)</label>
<textarea id="taskPrompt" rows="6" placeholder="请访问 https://xxx.com 获取今天的所有招标公告和中标公告信息..." required></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="taskEnabled" checked> 启用
</label>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-secondary" onclick="closeTaskModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<script>
let tasksList = [];
let currentResultsPage = 1;
// ===== Tab 切换 =====
function switchTab(name) {
document.querySelectorAll('.tab').forEach((t, i) => {
t.classList.toggle('active', ['tasks', 'results', 'settings'][i] === name);
});
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
if (name === 'results') loadResults();
if (name === 'settings') loadSettings();
}
// ===== 任务 CRUD =====
async function loadTasks() {
try {
const res = await fetch('/api/tasks');
const json = await res.json();
tasksList = json.data || [];
renderTasks();
} catch (e) {
console.error('加载任务失败:', e);
}
}
function renderTasks() {
const tbody = document.getElementById('tasksTbody');
const enabled = tasksList.filter(t => t.enabled).length;
document.getElementById('taskSummary').textContent = `${tasksList.length} 个任务,${enabled} 个启用`;
if (tasksList.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">暂无任务,点击「新增任务」添加</td></tr>';
return;
}
tbody.innerHTML = tasksList.map(t => `
<tr>
<td><strong>${t.city || '-'}</strong></td>
<td class="prompt-cell" title="${(t.prompt || '').replace(/"/g, '&quot;')}">${t.prompt || '-'}</td>
<td><span class="tag ${t.enabled ? 'tag-on' : 'tag-off'}">${t.enabled ? '启用' : '禁用'}</span></td>
<td>
<button class="btn btn-primary btn-sm" onclick="openTaskModal('${t.id}')">编辑</button>
<button class="btn btn-success btn-sm" onclick="runSingleTask('${t.id}', this)">运行</button>
<button class="btn btn-secondary btn-sm" onclick="toggleTask('${t.id}', ${!t.enabled})">${t.enabled ? '禁用' : '启用'}</button>
<button class="btn btn-danger btn-sm" onclick="deleteTask('${t.id}')">删除</button>
</td>
</tr>
`).join('');
}
function openTaskModal(id) {
const item = id ? tasksList.find(t => t.id === id) : null;
document.getElementById('taskModalTitle').textContent = item ? '编辑任务' : '新增任务';
document.getElementById('taskEditId').value = item ? item.id : '';
document.getElementById('taskCity').value = item ? item.city : '';
document.getElementById('taskPrompt').value = item ? item.prompt : '';
document.getElementById('taskEnabled').checked = item ? item.enabled : true;
document.getElementById('taskModal').classList.add('show');
}
function closeTaskModal() {
document.getElementById('taskModal').classList.remove('show');
}
async function saveTask(e) {
e.preventDefault();
const id = document.getElementById('taskEditId').value;
const data = {
city: document.getElementById('taskCity').value.trim(),
prompt: document.getElementById('taskPrompt').value.trim(),
enabled: document.getElementById('taskEnabled').checked,
};
try {
const url = id ? `/api/tasks/${id}` : '/api/tasks';
const method = id ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
const json = await res.json();
if (!json.success) throw new Error(json.error);
closeTaskModal();
await loadTasks();
} catch (err) {
alert('保存失败: ' + err.message);
}
}
async function deleteTask(id) {
const item = tasksList.find(t => t.id === id);
if (!confirm(`确定要删除「${item?.city}」的任务吗?`)) return;
try {
const res = await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
const json = await res.json();
if (!json.success) throw new Error(json.error);
await loadTasks();
} catch (err) {
alert('删除失败: ' + err.message);
}
}
async function toggleTask(id, enabled) {
try {
const res = await fetch(`/api/tasks/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled })
});
const json = await res.json();
if (!json.success) throw new Error(json.error);
await loadTasks();
} catch (err) {
alert('操作失败: ' + err.message);
}
}
// ===== 运行任务(异步 + 轮询状态) =====
let pollTimer = null;
function stopPolling() {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
}
function startPolling(statusDiv, onFinished) {
stopPolling();
pollTimer = setInterval(async () => {
try {
const res = await fetch('/api/tasks/status');
const json = await res.json();
if (!json.success) return;
const s = json.data;
const elapsed = s.elapsed || 0;
const min = Math.floor(elapsed / 60);
const sec = elapsed % 60;
const timeStr = min > 0 ? `${min}${sec}` : `${sec}`;
if (!s.finished) {
statusDiv.className = 'status-box status-info';
statusDiv.textContent = `正在执行: ${s.city || ''}${s.current}/${s.total})已用时 ${timeStr}...`;
} else {
stopPolling();
onFinished(s);
}
} catch (e) {
console.error('轮询状态失败:', e);
}
}, 2000);
}
async function runSingleTask(id, btnEl) {
const origText = btnEl.textContent;
btnEl.disabled = true;
btnEl.textContent = '运行中...';
const statusDiv = document.getElementById('batchRunStatus');
statusDiv.style.display = 'block';
statusDiv.className = 'status-box status-info';
statusDiv.textContent = '正在启动任务...';
try {
const res = await fetch(`/api/tasks/${id}/run`, { method: 'POST' });
const json = await res.json();
if (!json.success) throw new Error(json.error);
startPolling(statusDiv, (s) => {
btnEl.disabled = false;
btnEl.textContent = origText;
if (s.error) {
statusDiv.className = 'status-box status-error';
statusDiv.textContent = '运行失败: ' + s.error;
} else {
const total = s.results?.[0]?.data?.total || 0;
statusDiv.className = 'status-box status-success';
statusDiv.textContent = `运行完成!获取到 ${total} 条结果`;
}
});
} catch (err) {
btnEl.disabled = false;
btnEl.textContent = origText;
statusDiv.className = 'status-box status-error';
statusDiv.textContent = '启动失败: ' + err.message;
}
}
async function runAllTasks() {
const enabled = tasksList.filter(t => t.enabled);
if (enabled.length === 0) { alert('没有已启用的任务'); return; }
if (!confirm(`确定要运行全部 ${enabled.length} 个已启用的任务吗?`)) return;
const btn = document.getElementById('btnRunAll');
const statusDiv = document.getElementById('batchRunStatus');
btn.disabled = true;
btn.textContent = '运行中...';
statusDiv.style.display = 'block';
statusDiv.className = 'status-box status-info';
statusDiv.textContent = '正在启动任务...';
try {
const res = await fetch('/api/tasks/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const json = await res.json();
if (!json.success) throw new Error(json.error);
startPolling(statusDiv, (s) => {
btn.disabled = false;
btn.textContent = '运行全部启用';
const results = s.results || [];
const ok = results.filter(r => !r.error).length;
const fail = results.filter(r => r.error).length;
if (s.error) {
statusDiv.className = 'status-box status-error';
statusDiv.textContent = '运行失败: ' + s.error;
} else {
statusDiv.className = 'status-box status-success';
statusDiv.innerHTML = `运行完成!成功 <strong>${ok}</strong>,失败 <strong>${fail}</strong>`;
}
});
} catch (e) {
btn.disabled = false;
btn.textContent = '运行全部启用';
statusDiv.className = 'status-box status-error';
statusDiv.textContent = '启动失败: ' + e.message;
}
}
// ===== 结果展示 =====
async function loadResults() {
const city = document.getElementById('filterCity').value;
try {
// 加载筛选选项
const filtersRes = await fetch('/api/results/filters');
const filtersJson = await filtersRes.json();
if (filtersJson.success) {
const sel = document.getElementById('filterCity');
const curVal = sel.value;
sel.innerHTML = '<option value="">全部城市</option>' +
(filtersJson.data.cities || []).map(c => `<option value="${c}" ${c === curVal ? 'selected' : ''}>${c}</option>`).join('');
}
// 加载结果
const params = new URLSearchParams({ page: currentResultsPage, pageSize: 10 });
if (city) params.set('city', city);
const res = await fetch('/api/results?' + params);
const json = await res.json();
if (!json.success) throw new Error(json.error);
renderResults(json.data, json.total, json.page, json.pageSize);
} catch (e) {
document.getElementById('resultsList').innerHTML = `<div class="status-box status-error">加载失败: ${e.message}</div>`;
}
}
function renderResults(data, total, page, pageSize) {
const container = document.getElementById('resultsList');
if (!data || data.length === 0) {
container.innerHTML = '<div class="empty-state">暂无结果</div>';
document.getElementById('resultsPagination').innerHTML = '';
return;
}
container.innerHTML = data.map(r => {
const results = r.data?.results || [];
const errorHtml = r.error ? `<div style="color:#dc3545;font-size:13px;margin-top:8px;">错误: ${r.error}</div>` : '';
const itemsHtml = results.length > 0 ? results.map(item => `
<div class="result-item">
<span class="type">${item.type || '-'}</span>
<span class="name">${item.project_name || '-'}</span>
<span class="amount">${item.amount_yuan ? item.amount_yuan.toLocaleString() + ' 元' : '-'}</span>
<span class="date">${item.date || '-'}</span>
${item.target_link ? `<a href="${item.target_link}" target="_blank">查看</a>` : ''}
</div>
`).join('') : '<div style="padding:10px;color:#999;font-size:13px;">无数据</div>';
return `
<div class="result-card">
<div class="result-card-header">
<h4>${r.city || '未知城市'} <span style="font-weight:normal;color:#888;font-size:13px;">(${results.length} 条)</span></h4>
<span>${r.scrapedAt ? new Date(r.scrapedAt).toLocaleString('zh-CN') : ''}</span>
</div>
${itemsHtml}
${errorHtml}
<div style="text-align:right;margin-top:8px;">
<button class="btn btn-danger btn-sm" onclick="deleteResult('${r.id}')">删除</button>
</div>
</div>
`;
}).join('');
// 分页
const totalPages = Math.ceil(total / pageSize);
if (totalPages > 1) {
let pHtml = '';
for (let i = 1; i <= totalPages; i++) {
pHtml += `<button class="btn btn-sm ${i === page ? 'btn-primary' : 'btn-secondary'}" onclick="goPage(${i})" style="margin:0 3px;">${i}</button>`;
}
document.getElementById('resultsPagination').innerHTML = `<div>第 ${page}/${totalPages} 页,共 ${total} 条</div>${pHtml}`;
} else {
document.getElementById('resultsPagination').innerHTML = total > 0 ? `${total}` : '';
}
}
function goPage(p) { currentResultsPage = p; loadResults(); }
async function deleteResult(id) {
if (!confirm('确定删除这条结果?')) return;
try {
await fetch(`/api/results/${id}`, { method: 'DELETE' });
loadResults();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
async function clearResults() {
if (!confirm('确定要清空全部结果吗?此操作不可恢复。')) return;
try {
await fetch('/api/results', { method: 'DELETE' });
loadResults();
} catch (e) {
alert('清空失败: ' + e.message);
}
}
// ===== 设置 =====
async function loadSettings() {
try {
const res = await fetch('/api/config');
const json = await res.json();
if (!json.success) return;
const cfg = json.data;
// Agent
document.getElementById('cfgBaseUrl').value = cfg.agent?.baseUrl || '';
document.getElementById('cfgUseBrowser').value = String(cfg.agent?.useBrowser ?? false);
document.getElementById('cfgPollInterval').value = cfg.agent?.pollInterval || 3000;
document.getElementById('cfgTimeout').value = cfg.agent?.timeout || 300000;
// Scheduler
document.getElementById('cfgSchedulerEnabled').value = String(cfg.scheduler?.enabled ?? false);
document.getElementById('cfgCronTime').value = cfg.scheduler?.cronTime || '0 9 * * *';
document.getElementById('cfgDescription').value = cfg.scheduler?.description || '';
// Email
document.getElementById('cfgSmtpHost').value = cfg.email?.smtpHost || '';
document.getElementById('cfgSmtpPort').value = cfg.email?.smtpPort || 587;
document.getElementById('cfgSmtpUser').value = cfg.email?.smtpUser || '';
document.getElementById('cfgSmtpPass').value = cfg.email?.smtpPass || '';
document.getElementById('cfgRecipients').value = cfg.email?.recipients || '';
// Scheduler status
const statusRes = await fetch('/api/scheduler/status');
const statusJson = await statusRes.json();
if (statusJson.success) {
const s = statusJson.data;
document.getElementById('schedulerStatus').textContent =
s.isRunning ? `运行中 (${s.enabledTasks} 个任务)` : '未运行';
}
} catch (e) {
console.error('加载设置失败:', e);
}
}
async function saveSettings() {
try {
// 先获取当前完整配置(保留 tasks
const curRes = await fetch('/api/config');
const curJson = await curRes.json();
const curCfg = curJson.data || {};
const cfg = {
agent: {
baseUrl: document.getElementById('cfgBaseUrl').value.trim(),
useBrowser: document.getElementById('cfgUseBrowser').value === 'true',
pollInterval: parseInt(document.getElementById('cfgPollInterval').value) || 3000,
timeout: parseInt(document.getElementById('cfgTimeout').value) || 300000,
},
tasks: curCfg.tasks || [],
scheduler: {
enabled: document.getElementById('cfgSchedulerEnabled').value === 'true',
cronTime: document.getElementById('cfgCronTime').value.trim() || '0 9 * * *',
description: document.getElementById('cfgDescription').value.trim(),
},
email: {
smtpHost: document.getElementById('cfgSmtpHost').value.trim(),
smtpPort: parseInt(document.getElementById('cfgSmtpPort').value) || 587,
smtpUser: document.getElementById('cfgSmtpUser').value.trim(),
smtpPass: document.getElementById('cfgSmtpPass').value.trim(),
recipients: document.getElementById('cfgRecipients').value.trim(),
}
};
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg)
});
const json = await res.json();
if (!json.success) throw new Error(json.error);
const span = document.getElementById('settingsSaveStatus');
span.textContent = '已保存';
span.style.color = '#28a745';
setTimeout(() => { span.textContent = ''; }, 3000);
} catch (e) {
alert('保存失败: ' + e.message);
}
}
async function triggerScheduledTask() {
if (!confirm('确定要立即执行定时任务吗?')) return;
try {
const res = await fetch('/api/run-scheduled-task', { method: 'POST' });
const json = await res.json();
alert(json.message || '已触发');
} catch (e) {
alert('触发失败: ' + e.message);
}
}
// ===== 初始化 =====
loadTasks();
</script>
</body>
</html>