Files
news-tracker/templates/sources.html
T

231 lines
12 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.
{% extends "base.html" %}
{% set active = 'sources' %}
{% block title %}数据源 - 新闻智能跟踪{% endblock %}
{% block content %}
<div class="page-head">
<h1>📡 数据源管理</h1>
<button class="btn accent" onclick="openModal()"> 新增数据源</button>
</div>
<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>
<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 }}">
<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>
</tr>
{% endfor %}
</tbody>
</table>
<div class="muted small" style="margin-top:8px;">
💡 真实 URL 源自动抓取页面并清洗可读正文入库;example.com 等占位地址走模拟数据。<br>
⏱ 采集周期:0=跟随所属机制全局值(普通源=设置页采集间隔,定制源=定制监控间隔),填分钟数则本源自定义。<br>
🎯 <b>定制监控</b>:与新闻监控独立,无权重,按「推送标准」由大模型判断是否推送,命中实时推送 + 单独汇总。
</div>
</div>
<!-- 新增 / 编辑 数据源 模态框 -->
<div class="modal-mask" id="sourceModal" style="display:none;">
<div class="modal">
<div class="modal-head">
<h3 id="modalTitle">新增数据源</h3>
<span class="modal-x" onclick="closeModal()"></span>
</div>
<div class="modal-body">
<label>名称 *</label>
<input id="m_name" placeholder="如:36氪 AI频道">
<label>监控方式</label>
<select id="m_kind" onchange="onKindChange()">
<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">
</div>
<div id="m_standard_row" style="display:none;">
<label>推送标准 *(大模型据此判断每条资讯是否达到推送条件)</label>
<textarea id="m_standard" rows="3" placeholder="例如:仅推送英伟达重大事件——股价单日涨跌超10%,或重大产品发布/回购/分拆/收购等实质性公告,或分析师大幅上调目标价;普通行情波动与日常新闻一律不推送。"></textarea>
<div class="form-row" style="justify-content:flex-start;">
<button class="btn mini" type="button" onclick="testStandard()">🧪 测试推送标准</button>
<span id="m_test_result" class="muted small"></span>
</div>
</div>
<label>类型(分类标签)</label>
<input id="m_type" placeholder="科技媒体/公司动态/学术/政策...">
<label>URL / RSS 地址</label>
<input id="m_url" placeholder="example.com 开头走模拟数据">
<label>描述</label>
<textarea id="m_desc" rows="2" placeholder="该源提供哪些内容"></textarea>
</div>
<div class="modal-foot">
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn accent" onclick="saveSource()">保存</button>
</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;
document.getElementById('modalTitle').textContent = editId ? '编辑数据源' : '新增数据源';
document.getElementById('m_test_result').textContent = '';
if (editId){
const row = document.querySelector(`tr[data-sid="${editId}"]`);
if (!row) return;
setV('m_name', row.dataset.name);
setV('m_type', row.dataset.type);
setV('m_url', row.dataset.url);
setV('m_desc', row.dataset.desc);
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';
}
function closeModal(){ document.getElementById('sourceModal').style.display = 'none'; editId = null; }
function onKindChange(){
const custom = getV('m_kind') === 'custom';
document.getElementById('m_weight_row').style.display = custom ? 'none' : 'block';
document.getElementById('m_standard_row').style.display = custom ? 'block' : 'none';
}
async function saveSource(){
const kind = getV('m_kind');
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
};
if (!body.name){ toast('请填写名称', false); return; }
if (kind === 'custom' && !body.monitor_standard){ toast('请填写推送标准', false); return; }
const r = await API.json('/api/sources',
editId ? {...body, action:'update', id: editId} : {...body, action:'add'});
toast(r.ok ? '✅ 已保存' : '❌ 保存失败', r.ok);
if (r.ok){ closeModal(); setTimeout(()=>location.reload(), 400); }
}
async function testStandard(){
const standard = getV('m_standard').trim();
if (!standard){ toast('请先填写推送标准', false); return; }
const el = document.getElementById('m_test_result');
el.textContent = '⏳ 大模型分析中...';
const r = await API.json('/api/sources', {action:'test_standard', monitor_standard: standard});
if (r.ok){
el.textContent = r.meets ? '✅ 命中标准,会推送 · ' + (r.reason||'') : '❌ 未达标准,不推送 · ' + (r.reason||'');
} else {
el.textContent = '❌ ' + (r.error || '测试失败');
}
}
// ---------- 历史采样 ----------
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;
await API.json('/api/sources', {action:'delete', id}); toast('✅ 已删除'); setTimeout(()=>location.reload(), 400);
}
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
</script>
{% endblock %}