v1.4.0: 通知日志分页+筛选 / 仪表盘TOP10+更多链接 / 数据源走web-capture-api抓取(获取方式与参数可编辑) / 系统错误邮件通知(频率+静默时段)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}系统异常 - 新闻智能跟踪{% endblock %}
|
||||
{% block content %}
|
||||
<div style="text-align:center;padding:80px 20px;">
|
||||
<div style="font-size:64px;">⚠️</div>
|
||||
<h1 style="margin:12px 0;">系统处理出错</h1>
|
||||
<p class="muted">请求处理时发生异常,已自动记录并邮件通知管理员。</p>
|
||||
<a class="btn" href="/dashboard" style="margin-top:16px;">← 返回仪表盘</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<div class="two-col">
|
||||
<section class="card">
|
||||
<h3>🔥 重要资讯 TOP</h3>
|
||||
<h3>🔥 重要资讯 TOP <a class="more" href="/news?important=1">更多 ›</a></h3>
|
||||
{% for a in important %}
|
||||
<div class="item">
|
||||
<a class="item-title" href="/news/{{ a.id }}">{{ a.title }}</a>
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h3>🕒 最新收录</h3>
|
||||
<h3>🕒 最新收录 <a class="more" href="/news">更多 ›</a></h3>
|
||||
{% for a in latest %}
|
||||
<div class="item">
|
||||
<a class="item-title" href="/news/{{ a.id }}">{{ a.title }}</a>
|
||||
|
||||
+46
-6
@@ -3,9 +3,31 @@
|
||||
{% block title %}通知日志 - 新闻智能跟踪{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<h1>✉️ 通知日志</h1>
|
||||
<button class="btn" onclick="testMail()">📮 发送测试邮件</button>
|
||||
<h1>✉️ 通知日志 <span class="muted">共 {{ total }} 条</span></h1>
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="testMail()">📮 发送测试邮件</button>
|
||||
<button class="btn" onclick="testErrorMail()">⚠️ 测试错误通知</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="filters" method="get" action="/logs">
|
||||
<select name="type">
|
||||
<option value="">全部类型</option>
|
||||
<option value="realtime" {{ 'selected' if f_type=='realtime' }}>🔥 实时重要资讯</option>
|
||||
<option value="summary" {{ 'selected' if f_type=='summary' }}>📰 新闻汇总</option>
|
||||
<option value="custom_summary" {{ 'selected' if f_type=='custom_summary' }}>🎯 定制监控汇总</option>
|
||||
<option value="error" {{ 'selected' if f_type=='error' }}>⚠️ 系统错误</option>
|
||||
</select>
|
||||
<select name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="ok" {{ 'selected' if f_status=='ok' }}>✅ 成功</option>
|
||||
<option value="error" {{ 'selected' if f_status=='error' }}>❌ 失败</option>
|
||||
</select>
|
||||
<input type="text" name="q" value="{{ q }}" placeholder="搜索主题 / 详情...">
|
||||
<button class="btn" type="submit">筛选</button>
|
||||
<a class="btn" href="/logs">重置</a>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>类型</th><th>主题</th><th>条数</th><th>状态</th><th>发送时间</th></tr></thead>
|
||||
@@ -13,18 +35,32 @@
|
||||
{% for l in logs %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td>{% if l.type=='realtime' %}<span class="tag hot">实时</span>{% else %}<span class="tag ok">汇总</span>{% endif %}</td>
|
||||
<td>{{ l.subject }}</td>
|
||||
<td>
|
||||
{% if l.type=='realtime' %}<span class="tag hot">🔥 实时</span>
|
||||
{% elif l.type=='summary' %}<span class="tag ok">📰 新闻汇总</span>
|
||||
{% elif l.type=='custom_summary' %}<span class="tag">🎯 定制汇总</span>
|
||||
{% elif l.type=='error' %}<span class="tag err">⚠️ 系统错误</span>
|
||||
{% else %}<span class="tag">{{ l.type }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ l.subject }}
|
||||
{% if l.detail %}<div class="muted small">{{ l.detail }}</div>{% endif %}
|
||||
</td>
|
||||
<td>{{ l.count }}</td>
|
||||
<td>{% if l.status=='ok' %}<span class="tag ok">成功</span>{% else %}<span class="tag err">失败</span>{% endif %}</td>
|
||||
<td>{{ l.sent_at }}</td>
|
||||
<td class="small">{{ l.sent_at }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="empty">暂无通知记录</td></tr>
|
||||
<tr><td colspan="6" class="empty">暂无匹配的通知记录</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pager">
|
||||
{% if page > 1 %}<a class="btn" href="/logs?page={{ page-1 }}&type={{ f_type }}&status={{ f_status }}&q={{ q }}">上一页</a>{% endif %}
|
||||
<span class="muted">第 {{ page }} / {{ pages }} 页</span>
|
||||
{% if page < pages %}<a class="btn" href="/logs?page={{ page+1 }}&type={{ f_type }}&status={{ f_status }}&q={{ q }}">下一页</a>{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
@@ -32,5 +68,9 @@ async function testMail(){
|
||||
const r = await API.json('/api/actions', {action:'test_mail'});
|
||||
toast(r.ok ? '✅ 测试邮件已发送' : ('❌ ' + r.error), r.ok);
|
||||
}
|
||||
async function testErrorMail(){
|
||||
const r = await API.json('/api/actions', {action:'test_error_mail'});
|
||||
toast(r.ok ? '✅ 测试错误通知邮件已发送' : ('❌ ' + r.error), r.ok);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
+96
-2
@@ -98,7 +98,72 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h3>🤖 大模型接口
|
||||
<h3>🌐 网页提取服务(web-capture-api)
|
||||
<span class="muted small">(数据源真实网页默认通过该服务抓取,每源可单独选获取方式与参数)</span>
|
||||
</h3>
|
||||
<div class="setting-row">
|
||||
<label>启用 web-capture-api 抓取</label>
|
||||
<input type="checkbox" id="w_enabled" {{ 'checked' if auto.webcapture.enabled }}>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>API 地址</label>
|
||||
<input id="w_api_url" value="{{ auto.webcapture.api_url }}" placeholder="http://121.40.164.32:16025">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>抓取超时(秒)</label>
|
||||
<input type="number" id="w_timeout" value="{{ auto.webcapture.timeout }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>连通性测试</label>
|
||||
<span style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn mini" type="button" onclick="testWebcapture()">🧪 测试接口</button>
|
||||
<span id="wc_test_result" class="muted small"></span>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h3>⚠️ 系统错误邮件通知
|
||||
<span class="muted small">(采集/分析/通知/汇总/系统异常自动邮件通知,可设通知频率与静默时段)</span>
|
||||
</h3>
|
||||
<div class="setting-row">
|
||||
<label>启用错误邮件通知</label>
|
||||
<input type="checkbox" id="e_enabled" {{ 'checked' if auto.errnotify.enabled }}>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>通知方式</label>
|
||||
<select id="e_mode">
|
||||
<option value="immediate" {{ 'selected' if auto.errnotify.mode=='immediate' }}>⚡ 立即(出现即尝试发,仍受冷却限制)</option>
|
||||
<option value="cooldown" {{ 'selected' if auto.errnotify.mode=='cooldown' }}>⏱ 按冷却聚合(到冷却时间集中发一封)</option>
|
||||
<option value="off" {{ 'selected' if auto.errnotify.mode=='off' }}>🚫 关闭(仅记录不发送)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>通知频率(冷却分钟)<span class="muted small">— 两次错误邮件最小间隔</span></label>
|
||||
<input type="number" id="e_cooldown_min" value="{{ auto.errnotify.cooldown_min }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>单封最多错误条数</label>
|
||||
<input type="number" id="e_max_items" value="{{ auto.errnotify.max_items }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>启用静默时段</label>
|
||||
<input type="checkbox" id="e_quiet_enabled" {{ 'checked' if auto.errnotify.quiet_enabled }}>
|
||||
</div>
|
||||
<div style="margin-top:10px;">
|
||||
<label style="font-size:12px;color:#6b7280;">静默时段(每行一段 <b>HH:MM-HH:MM</b>,支持跨午夜、多段;静默期不发送,结束后自动补发)</label>
|
||||
<textarea id="e_quiet_periods" rows="3" style="margin-top:4px;">{{ '\n'.join(auto.errnotify.quiet_periods) }}</textarea>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>测试</label>
|
||||
<span style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn mini" type="button" onclick="testErrorMail()">📨 发一封测试错误通知</button>
|
||||
<span id="e_test_result" class="muted small"></span>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="muted small">(切换后即时生效,智能分析将使用当前接口;失败自动切换下一个可用接口)</span>
|
||||
</h3>
|
||||
<div class="form-row" style="margin-bottom:12px;">
|
||||
@@ -170,10 +235,39 @@ async function saveAll(){
|
||||
custom_summary_window_hours: parseInt(G('c_custom_summary_window_hours'))||24,
|
||||
custom_max_summary_items: parseInt(G('c_custom_max_summary_items'))||20,
|
||||
};
|
||||
const r = await API.json('/api/settings', { ...auto, mail, custom });
|
||||
const webcapture = {
|
||||
enabled: document.getElementById('w_enabled').checked ? 1 : 0,
|
||||
api_url: G('w_api_url').trim() || 'http://121.40.164.32:16025',
|
||||
timeout: parseInt(G('w_timeout')) || 60,
|
||||
};
|
||||
const errnotify = {
|
||||
enabled: document.getElementById('e_enabled').checked ? 1 : 0,
|
||||
mode: G('e_mode'),
|
||||
cooldown_min: parseInt(G('e_cooldown_min')) || 60,
|
||||
max_items: parseInt(G('e_max_items')) || 10,
|
||||
quiet_enabled: document.getElementById('e_quiet_enabled').checked ? 1 : 0,
|
||||
quiet_periods: G('e_quiet_periods').split(/[\n,;,;]/).map(s=>s.trim()).filter(Boolean),
|
||||
};
|
||||
const r = await API.json('/api/settings', { ...auto, mail, custom, webcapture, errnotify });
|
||||
toast(r.ok ? '✅ 设置已保存' : '❌ 保存失败', r.ok);
|
||||
}
|
||||
|
||||
async function testWebcapture(){
|
||||
const el = document.getElementById('wc_test_result');
|
||||
el.textContent = '⏳ 测试中...';
|
||||
const r = await API.json('/api/actions', {action:'test_webcapture'});
|
||||
if (r.ok) el.textContent = `✅ ${r.api_url} 正常,抓取到:${r.title}(${(r.text||'').trim()})`;
|
||||
else el.textContent = '❌ ' + (r.error || '测试失败');
|
||||
}
|
||||
|
||||
async function testErrorMail(){
|
||||
const el = document.getElementById('e_test_result');
|
||||
el.textContent = '⏳ 发送中...';
|
||||
const r = await API.json('/api/actions', {action:'test_error_mail'});
|
||||
if (r.ok) el.textContent = '✅ 测试错误通知邮件已发送';
|
||||
else el.textContent = '❌ ' + (r.error || '发送失败');
|
||||
}
|
||||
|
||||
async function addProvider(){
|
||||
const r = await API.json('/api/llm', {
|
||||
action:'add',
|
||||
|
||||
+78
-5
@@ -9,13 +9,14 @@
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>名称</th><th>监控方式</th><th>类型</th><th>权重</th><th>采集周期</th><th>状态</th><th>最近采样</th><th>条数</th><th>操作</th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>名称</th><th>监控方式</th><th>获取方式</th><th>类型</th><th>权重</th><th>采集周期</th><th>状态</th><th>最近采样</th><th>条数</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in sources %}
|
||||
<tr data-sid="{{ s.id }}" data-name="{{ s.name }}" data-type="{{ s.type }}" data-url="{{ s.url }}" data-desc="{{ s.description }}" data-weight="{{ s.weight }}" data-kind="{{ s.kind }}" data-standard="{{ s.monitor_standard or '' }}" data-interval="{{ s.scan_interval_min or 0 }}">
|
||||
<tr data-sid="{{ s.id }}" data-name="{{ s.name }}" data-type="{{ s.type }}" data-url="{{ s.url }}" data-desc="{{ s.description }}" data-weight="{{ s.weight }}" data-kind="{{ s.kind }}" data-standard="{{ s.monitor_standard or '' }}" data-interval="{{ s.scan_interval_min or 0 }}" data-fetch="{{ s.fetch_method or 'auto' }}" data-capture="{{ s.capture_params or '{}' }}">
|
||||
<td>{{ s.id }}</td>
|
||||
<td><b>{{ s.name }}</b><br><span class="muted small">{{ s.description }}</span><br><span class="muted small" style="word-break:break-all;">{{ s.url }}</span></td>
|
||||
<td>{% if s.kind == 'custom' %}<span class="tag hot">🎯 定制监控</span>{% else %}<span class="tag ok">普通</span>{% endif %}</td>
|
||||
<td>{% if s.fetch_method == 'direct' %}<span class="tag">⚡ 直接抓取</span>{% elif s.fetch_method == 'webcapture' %}<span class="tag">🌐 web-capture</span>{% else %}<span class="tag ok">🌐 自动</span>{% endif %}</td>
|
||||
<td>{{ s.type }}</td>
|
||||
<td>{% if s.kind == 'custom' %}<span class="muted small">—</span>{% else %}{{ s.weight }}{% endif %}</td>
|
||||
<td>{% if s.scan_interval_min %}{{ s.scan_interval_min }}分{% else %}<span class="muted small">默认</span>{% endif %}</td>
|
||||
@@ -33,8 +34,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="muted small" style="margin-top:8px;">
|
||||
💡 真实 URL 源自动抓取页面并清洗可读正文入库;example.com 等占位地址走模拟数据。<br>
|
||||
💡 真实 URL 源默认通过 web-capture-api 网页提取服务抓取(可每源改获取方式与参数);example.com 等占位地址走模拟数据。<br>
|
||||
⏱ 采集周期:0=跟随所属机制全局值(普通源=设置页采集间隔,定制源=定制监控间隔),填分钟数则本源自定义。<br>
|
||||
🌐 获取方式:<b>自动</b>=优先 web-capture-api、失败回退直接抓取;<b>web-capture</b>=仅走网页提取服务;<b>直接抓取</b>=requests+正文清洗。设置页可改 web-capture-api 地址。<br>
|
||||
🎯 <b>定制监控</b>:与新闻监控独立,无权重,按「推送标准」由大模型判断是否推送,命中实时推送 + 单独汇总。
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,6 +61,35 @@
|
||||
<label>采集周期(分钟)<span class="muted small">— 0 表示跟随所属机制的全局间隔</span></label>
|
||||
<input id="m_interval" type="number" min="0" step="1" value="0" placeholder="0=跟随全局(普通源30 / 定制源15)">
|
||||
|
||||
<label>获取网页方式<span class="muted small">— 真实 URL 网页通过 web-capture-api 抓取(设置页可改接口地址)</span></label>
|
||||
<select id="m_fetch" onchange="onFetchChange()">
|
||||
<option value="auto">🌐 自动(优先 web-capture-api,失败回退直接抓取)</option>
|
||||
<option value="webcapture">🌐 web-capture-api(网页提取服务,反爬强)</option>
|
||||
<option value="direct">⚡ 直接抓取(requests + 正文清洗)</option>
|
||||
</select>
|
||||
|
||||
<div id="m_capture_row">
|
||||
<label>抓取动作<span class="muted small">— html 提取资讯链接;text 整页作为一条资讯</span></label>
|
||||
<select id="m_c_action">
|
||||
<option value="html">html(提取链接)</option>
|
||||
<option value="text">text(直接取正文)</option>
|
||||
</select>
|
||||
<div class="form-row" style="justify-content:flex-start;margin-top:6px;">
|
||||
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:#6b7280;white-space:nowrap;">等待加载(ms)
|
||||
<input id="m_c_wait" type="number" value="2000" style="width:90px;"></label>
|
||||
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:#6b7280;white-space:nowrap;">滚动次数
|
||||
<input id="m_c_scroll" type="number" value="0" style="width:70px;"></label>
|
||||
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:#6b7280;white-space:nowrap;">后端
|
||||
<select id="m_c_backend" style="width:130px;">
|
||||
<option value="auto">auto</option>
|
||||
<option value="agent-browser">agent-browser</option>
|
||||
<option value="playwright">playwright</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<label>高级参数(JSON,可留空)</label>
|
||||
<textarea id="m_c_extra" rows="2" placeholder='{"scroll_delay":1000,"full_page":false,"viewport":{"width":1280,"height":800}}'></textarea>
|
||||
</div>
|
||||
|
||||
<div id="m_weight_row">
|
||||
<label>权重(综合评分中该源占比,0.1~2.0)</label>
|
||||
<input id="m_weight" type="number" step="0.1" min="0.1" max="2" value="0.8">
|
||||
@@ -130,13 +161,28 @@ function openModal(id){
|
||||
setV('m_kind', row.dataset.kind || 'normal');
|
||||
setV('m_standard', row.dataset.standard || '');
|
||||
setV('m_interval', row.dataset.interval || '0');
|
||||
const cap = parseCapture(row.dataset.capture);
|
||||
setV('m_fetch', row.dataset.fetch || 'auto');
|
||||
setV('m_c_action', cap.action || 'html');
|
||||
setV('m_c_wait', cap.wait_time != null ? cap.wait_time : '2000');
|
||||
setV('m_c_scroll', cap.scroll_times != null ? cap.scroll_times : '0');
|
||||
setV('m_c_backend', cap.backend || 'auto');
|
||||
const extra = {...cap};
|
||||
['action','wait_time','scroll_times','backend'].forEach(k => delete extra[k]);
|
||||
setV('m_c_extra', Object.keys(extra).length ? JSON.stringify(extra) : '');
|
||||
} else {
|
||||
['m_name','m_type','m_url','m_desc','m_standard'].forEach(k=>setV(k,''));
|
||||
['m_name','m_type','m_url','m_desc','m_standard','m_c_extra'].forEach(k=>setV(k,''));
|
||||
setV('m_weight', '0.8');
|
||||
setV('m_kind', 'normal');
|
||||
setV('m_interval', '0');
|
||||
setV('m_fetch', 'auto');
|
||||
setV('m_c_action', 'html');
|
||||
setV('m_c_wait', '2000');
|
||||
setV('m_c_scroll', '0');
|
||||
setV('m_c_backend', 'auto');
|
||||
}
|
||||
onKindChange();
|
||||
onFetchChange();
|
||||
document.getElementById('sourceModal').style.display = 'flex';
|
||||
}
|
||||
function closeModal(){ document.getElementById('sourceModal').style.display = 'none'; editId = null; }
|
||||
@@ -147,14 +193,41 @@ function onKindChange(){
|
||||
document.getElementById('m_standard_row').style.display = custom ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function onFetchChange(){
|
||||
document.getElementById('m_capture_row').style.display =
|
||||
getV('m_fetch') === 'direct' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function parseCapture(s){
|
||||
try { const o = JSON.parse(s || '{}'); return o && typeof o === 'object' ? o : {}; } catch(e){ return {}; }
|
||||
}
|
||||
|
||||
async function saveSource(){
|
||||
const kind = getV('m_kind');
|
||||
let captureParams = '{}';
|
||||
if (getV('m_fetch') !== 'direct'){
|
||||
const base = {
|
||||
action: getV('m_c_action') || 'html',
|
||||
wait_time: parseInt(getV('m_c_wait')) || 2000,
|
||||
scroll_times: parseInt(getV('m_c_scroll')) || 0,
|
||||
backend: getV('m_c_backend') || 'auto'
|
||||
};
|
||||
let adv = {};
|
||||
const extraTxt = getV('m_c_extra').trim();
|
||||
if (extraTxt){
|
||||
try { adv = JSON.parse(extraTxt); }
|
||||
catch(e){ toast('高级参数 JSON 格式错误', false); return; }
|
||||
}
|
||||
captureParams = JSON.stringify({ ...base, ...adv });
|
||||
}
|
||||
const body = {
|
||||
name: getV('m_name').trim(), type: getV('m_type').trim(), url: getV('m_url').trim(),
|
||||
description: getV('m_desc').trim(), kind,
|
||||
monitor_standard: kind === 'custom' ? getV('m_standard').trim() : '',
|
||||
weight: parseFloat(getV('m_weight') || '1') || 1,
|
||||
scan_interval_min: parseInt(getV('m_interval')) || 0
|
||||
scan_interval_min: parseInt(getV('m_interval')) || 0,
|
||||
fetch_method: getV('m_fetch'),
|
||||
capture_params: captureParams
|
||||
};
|
||||
if (!body.name){ toast('请填写名称', false); return; }
|
||||
if (kind === 'custom' && !body.monitor_standard){ toast('请填写推送标准', false); return; }
|
||||
|
||||
Reference in New Issue
Block a user