158 lines
7.3 KiB
HTML
158 lines
7.3 KiB
HTML
{% 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></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 '' }}">
|
||
<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.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="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>
|
||
🎯 <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>
|
||
|
||
<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>
|
||
{% endblock %}
|
||
{% block script %}
|
||
<script>
|
||
let editId = 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 || '');
|
||
} else {
|
||
['m_name','m_type','m_url','m_desc','m_standard'].forEach(k=>setV(k,''));
|
||
setV('m_weight', '0.8');
|
||
setV('m_kind', 'normal');
|
||
}
|
||
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
|
||
};
|
||
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 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; }
|
||
</script>
|
||
{% endblock %}
|