- 新增 crawler.py: requests+bs4 readability风格清洗, 提取候选链接按文章相似度排序, 前5条抓全文存 articles.full_text, 详情页展示; example.com占位源走模拟, 真实源失败标记error不造假 - 大模型: 新增 llm_providers 表(预置 SiliconFlow/DeepSeek官方/Autodl/Local Qwen), 设置页增删改/测试/一键切换, 激活接口失败自动切换备用 - 数据源: 前端补编辑按钮+弹窗(后端 update 已支持), 列表显示URL与采集状态 - 数据库迁移: articles.full_text 列 + llm_providers 表
81 lines
3.9 KiB
HTML
81 lines
3.9 KiB
HTML
{% extends "base.html" %}
|
||
{% set active = 'sources' %}
|
||
{% block title %}数据源 - 新闻智能跟踪{% endblock %}
|
||
{% block content %}
|
||
<div class="page-head">
|
||
<h1>📡 数据源管理</h1>
|
||
<button class="btn accent" onclick="toggleAdd()">+ 新增数据源</button>
|
||
</div>
|
||
|
||
<div class="card" id="add-box" style="display:none;">
|
||
<h3>新增数据源</h3>
|
||
<div class="form-row">
|
||
<input id="f_name" placeholder="名称(如:36氪 AI频道)">
|
||
<input id="f_type" placeholder="类型(科技媒体/公司动态/学术/政策...)">
|
||
<input id="f_url" placeholder="URL / RSS 地址(example.com 开头走模拟数据)">
|
||
<input id="f_weight" type="number" step="0.1" value="0.8" placeholder="权重">
|
||
</div>
|
||
<textarea id="f_desc" placeholder="描述(该源提供哪些内容)" rows="2"></textarea>
|
||
<div class="form-row"><button class="btn" onclick="saveSource()">保存</button></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<table>
|
||
<thead><tr><th>ID</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 }}">
|
||
<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>{{ s.type }}</td>
|
||
<td>{{ s.weight }}</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="editSource({{ 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 等占位地址走模拟数据。</div>
|
||
</div>
|
||
{% endblock %}
|
||
{% block script %}
|
||
<script>
|
||
function toggleAdd(){ document.getElementById('add-box').style.display = document.getElementById('add-box').style.display==='none' ? 'block' : 'none'; }
|
||
async function saveSource(){
|
||
const r = await API.json('/api/sources', {
|
||
action:'add', name:val('f_name'), type:val('f_type'), url:val('f_url'),
|
||
weight:parseFloat(val('f_weight')||'1'), description:val('f_desc')
|
||
});
|
||
if (r.ok) { toast('✅ 已添加'); setTimeout(()=>location.reload(), 600); } else toast('❌ 添加失败', false);
|
||
}
|
||
async function editSource(id){
|
||
const row = document.querySelector(`tr[data-sid="${id}"]`);
|
||
if (!row) return;
|
||
const name = prompt('名称', row.dataset.name);
|
||
if (name === null) return;
|
||
const type = prompt('类型', row.dataset.type);
|
||
if (type === null) return;
|
||
const url = prompt('URL / RSS 地址', row.dataset.url);
|
||
if (url === null) return;
|
||
const weight = parseFloat(prompt('权重', row.dataset.weight) || '1') || 1;
|
||
const desc = prompt('描述', row.dataset.desc);
|
||
if (desc === null) return;
|
||
const r = await API.json('/api/sources', {action:'update', id, name, type, url, weight, description:desc});
|
||
toast(r.ok ? '✅ 已更新' : '❌ 更新失败', r.ok);
|
||
if (r.ok) setTimeout(()=>location.reload(), 400);
|
||
}
|
||
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 val(id){ return document.getElementById(id).value; }
|
||
</script>
|
||
{% endblock %}
|