v1.3.0: 每源独立采集周期 + 定制监控与新闻监控分离(独立配置/独立汇总) + 历史采样留档与提取API
This commit is contained in:
@@ -38,6 +38,15 @@ const API = {
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return r.json();
|
||||
},
|
||||
get: async (url, params) => {
|
||||
if (params){
|
||||
const qs = Object.entries(params).filter(([k,v])=>v!==''&&v!=null)
|
||||
.map(([k,v])=>encodeURIComponent(k)+'='+encodeURIComponent(v)).join('&');
|
||||
if (qs) url += (url.includes('?')?'&':'?') + qs;
|
||||
}
|
||||
const r = await fetch(url);
|
||||
return r.json();
|
||||
}
|
||||
};
|
||||
function toast(msg, ok=true) {
|
||||
|
||||
+39
-1
@@ -67,6 +67,36 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h3>🎯 定制监控机制
|
||||
<span class="muted small">(与新闻监控完全独立:定制源按此间隔采集,命中推送标准单独汇总;每源可用本源「采集周期」覆盖间隔)</span>
|
||||
</h3>
|
||||
<div class="setting-row">
|
||||
<label>启用定制监控机制(采集+LLM判断+命中推送)</label>
|
||||
<input type="checkbox" id="c_custom_enabled" {{ 'checked' if auto.custom and auto.custom.custom_enabled }}>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>定制源统一采集间隔(分钟,每源可覆盖)</label>
|
||||
<input type="number" id="c_custom_scan_interval_min" value="{{ auto.custom.custom_scan_interval_min if auto.custom else 15 }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>单独发「定制监控汇总」邮件</label>
|
||||
<input type="checkbox" id="c_custom_summary_enabled" {{ 'checked' if auto.custom and auto.custom.custom_summary_enabled }}>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>定制监控汇总时间(独立于新闻日报)</label>
|
||||
<input type="time" id="c_custom_summary_time" value="{{ auto.custom.custom_summary_time if auto.custom else '18:00' }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>定制汇总窗口(小时)</label>
|
||||
<input type="number" id="c_custom_summary_window_hours" value="{{ auto.custom.custom_summary_window_hours if auto.custom else 24 }}">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label>定制汇总邮件最多条目</label>
|
||||
<input type="number" id="c_custom_max_summary_items" value="{{ auto.custom.custom_max_summary_items if auto.custom else 20 }}">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h3>🤖 大模型接口
|
||||
<span class="muted small">(切换后即时生效,智能分析将使用当前接口;失败自动切换下一个可用接口)</span>
|
||||
@@ -132,7 +162,15 @@ async function saveAll(){
|
||||
smtp_user: G('m_user'), smtp_pass: G('m_pass'), smtp_mode: G('m_mode'),
|
||||
email_to: G('m_to'), sender_name: G('m_name'),
|
||||
};
|
||||
const r = await API.json('/api/settings', { ...auto, mail });
|
||||
const custom = {
|
||||
custom_enabled: document.getElementById('c_custom_enabled').checked ? 1 : 0,
|
||||
custom_scan_interval_min: parseInt(G('c_custom_scan_interval_min'))||15,
|
||||
custom_summary_enabled: document.getElementById('c_custom_summary_enabled').checked ? 1 : 0,
|
||||
custom_summary_time: G('c_custom_summary_time')||'18:00',
|
||||
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 });
|
||||
toast(r.ok ? '✅ 设置已保存' : '❌ 保存失败', r.ok);
|
||||
}
|
||||
|
||||
|
||||
+79
-6
@@ -9,20 +9,22 @@
|
||||
|
||||
<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></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></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 '' }}">
|
||||
<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 }}">
|
||||
<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>{{ 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>
|
||||
<td>{% if s.enabled %}<span class="tag ok">启用</span>{% else %}<span class="tag">停用</span>{% endif %}</td>
|
||||
<td>{{ s.last_fetch or '—' }}</td>
|
||||
<td>{{ s.last_count }}</td>
|
||||
<td>
|
||||
<button class="btn mini" onclick="openModal({{ s.id }})">✏️ 编辑</button>
|
||||
<button class="btn mini" onclick="openHistory({{ s.id }})">📜 历史采样</button>
|
||||
<button class="btn mini" onclick="toggleSource({{ s.id }})">{{ '停用' if s.enabled else '启用' }}</button>
|
||||
<button class="btn mini danger" onclick="delSource({{ s.id }})">删除</button>
|
||||
</td>
|
||||
@@ -32,7 +34,8 @@
|
||||
</table>
|
||||
<div class="muted small" style="margin-top:8px;">
|
||||
💡 真实 URL 源自动抓取页面并清洗可读正文入库;example.com 等占位地址走模拟数据。<br>
|
||||
🎯 <b>定制监控</b>:不设权重,填写「推送标准」后由大模型逐条判断是否达到标准,达到即实时邮件推送。
|
||||
⏱ 采集周期:0=跟随所属机制全局值(普通源=设置页采集间隔,定制源=定制监控间隔),填分钟数则本源自定义。<br>
|
||||
🎯 <b>定制监控</b>:与新闻监控独立,无权重,按「推送标准」由大模型判断是否推送,命中实时推送 + 单独汇总。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,10 +52,13 @@
|
||||
|
||||
<label>监控方式</label>
|
||||
<select id="m_kind" onchange="onKindChange()">
|
||||
<option value="normal">普通数据源(按权重打分推送)</option>
|
||||
<option value="custom">定制监控(无权重,按推送标准由大模型判断推送)</option>
|
||||
<option value="normal">普通数据源(新闻监控机制,按权重打分推送)</option>
|
||||
<option value="custom">定制监控(定制监控机制,无权重,按推送标准由大模型判断推送)</option>
|
||||
</select>
|
||||
|
||||
<label>采集周期(分钟)<span class="muted small">— 0 表示跟随所属机制的全局间隔</span></label>
|
||||
<input id="m_interval" type="number" min="0" step="1" value="0" placeholder="0=跟随全局(普通源30 / 定制源15)">
|
||||
|
||||
<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">
|
||||
@@ -82,10 +88,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史采样 模态框 -->
|
||||
<div class="modal-mask" id="histModal" style="display:none;">
|
||||
<div class="modal" style="width:640px;">
|
||||
<div class="modal-head">
|
||||
<h3 id="histTitle">历史采样</h3>
|
||||
<span class="modal-x" onclick="document.getElementById('histModal').style.display='none'">✕</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-row" style="margin-bottom:8px;">
|
||||
<button class="btn mini" onclick="openHistArticles()">📰 查看本源采集资讯</button>
|
||||
<span id="hist_meta" class="muted small"></span>
|
||||
</div>
|
||||
<div id="hist_snaps"></div>
|
||||
<div id="hist_articles" style="display:none;margin-top:8px;border-top:1px solid #f3f4f6;padding-top:8px;"></div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" onclick="document.getElementById('histModal').style.display='none'">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
let editId = null;
|
||||
let histSourceId = null;
|
||||
|
||||
function openModal(id){
|
||||
editId = id || null;
|
||||
@@ -101,10 +129,12 @@ function openModal(id){
|
||||
setV('m_weight', row.dataset.weight || '0.8');
|
||||
setV('m_kind', row.dataset.kind || 'normal');
|
||||
setV('m_standard', row.dataset.standard || '');
|
||||
setV('m_interval', row.dataset.interval || '0');
|
||||
} else {
|
||||
['m_name','m_type','m_url','m_desc','m_standard'].forEach(k=>setV(k,''));
|
||||
setV('m_weight', '0.8');
|
||||
setV('m_kind', 'normal');
|
||||
setV('m_interval', '0');
|
||||
}
|
||||
onKindChange();
|
||||
document.getElementById('sourceModal').style.display = 'flex';
|
||||
@@ -123,7 +153,8 @@ async function saveSource(){
|
||||
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
|
||||
weight: parseFloat(getV('m_weight') || '1') || 1,
|
||||
scan_interval_min: parseInt(getV('m_interval')) || 0
|
||||
};
|
||||
if (!body.name){ toast('请填写名称', false); return; }
|
||||
if (kind === 'custom' && !body.monitor_standard){ toast('请填写推送标准', false); return; }
|
||||
@@ -146,6 +177,47 @@ async function testStandard(){
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 历史采样 ----------
|
||||
async function openHistory(id){
|
||||
histSourceId = id;
|
||||
const row = document.querySelector(`tr[data-sid="${id}"]`);
|
||||
document.getElementById('histTitle').textContent = `📜 历史采样 · ${row ? row.dataset.name : ''}`;
|
||||
document.getElementById('hist_articles').style.display = 'none';
|
||||
document.getElementById('hist_articles').innerHTML = '';
|
||||
document.getElementById('histModal').style.display = 'flex';
|
||||
const r = await API.get('/api/sources/history', {source_id: id, limit: 100});
|
||||
document.getElementById('hist_meta').textContent = r.ok ? `共 ${r.total} 次采样` : '加载失败';
|
||||
if (!r.ok){ document.getElementById('hist_snaps').innerHTML = '<div class="empty">加载失败</div>'; return; }
|
||||
const snaps = r.snapshots;
|
||||
if (!snaps.length){
|
||||
document.getElementById('hist_snaps').innerHTML = '<div class="empty">暂无采样记录(保存后由后台采集生成)</div>';
|
||||
return;
|
||||
}
|
||||
document.getElementById('hist_snaps').innerHTML =
|
||||
'<table><thead><tr><th>#</th><th>采样时间</th><th>采集条数</th><th>状态</th><th>备注</th></tr></thead><tbody>' +
|
||||
snaps.map(s => `<tr>
|
||||
<td>${s.id}</td><td>${s.fetched_at}</td><td>${s.count}</td>
|
||||
<td>${s.status==='ok' ? '<span class="tag ok">成功</span>' : '<span class="tag err">失败</span>'}</td>
|
||||
<td class="small muted">${escapeHtml(s.detail||'')}</td>
|
||||
</tr>`).join('') + '</tbody></table>';
|
||||
}
|
||||
|
||||
async function openHistArticles(){
|
||||
if (!histSourceId) return;
|
||||
const el = document.getElementById('hist_articles');
|
||||
el.style.display = 'block';
|
||||
el.innerHTML = '<div class="empty">⏳ 加载资讯列表...</div>';
|
||||
const r = await API.get('/api/sources/articles', {source_id: histSourceId, page_size: 30});
|
||||
if (!r.ok){ el.innerHTML = '<div class="empty">加载失败</div>'; return; }
|
||||
const arts = r.articles;
|
||||
if (!arts.length){ el.innerHTML = `<div class="empty">本源暂无采集到的资讯(共 ${r.total} 条)</div>`; return; }
|
||||
el.innerHTML = `<div class="muted small" style="margin-bottom:6px;">本源共采集 ${r.total} 条资讯(最新 ${arts.length} 条):</div>` +
|
||||
'<div class="hist-list">' + arts.map(a =>
|
||||
`<div class="hist-item"><a href="/news/${a.id}" target="_blank"><b>${escapeHtml(a.title)}</b></a>
|
||||
<div class="muted small">${a.collected_at} · 综合分${a.total_score}${a.is_important ? ' · <span style="color:#dc2626;">重要</span>' : ''}</div></div>`
|
||||
).join('') + '</div>';
|
||||
}
|
||||
|
||||
async function toggleSource(id){ await API.json('/api/sources', {action:'toggle', id}); location.reload(); }
|
||||
async function delSource(id){
|
||||
if (!confirm('确认删除该数据源?')) return;
|
||||
@@ -153,5 +225,6 @@ async function delSource(id){
|
||||
}
|
||||
function getV(id){ return document.getElementById(id).value; }
|
||||
function setV(id, v){ document.getElementById(id).value = v; }
|
||||
function escapeHtml(s){ return (s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user