Files
news-tracker/templates/settings.html
T
hz4th_coder e4efd24e1c v1.1.0: 真实网页采集(可读正文全文入库) + 大模型接口多预置一键切换(SiliconFlow默认) + 数据源可编辑
- 新增 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 表
2026-08-28 16:06:07 +08:00

185 lines
8.4 KiB
HTML
Raw Permalink 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 = 'settings' %}
{% block title %}设置 - 新闻智能跟踪{% endblock %}
{% block content %}
<div class="page-head">
<h1>⚙️ 设置</h1>
<button class="btn accent" onclick="saveAll()">💾 保存设置</button>
</div>
<div class="two-col">
<section class="card">
<h3>🕐 自动化</h3>
<div class="setting-row">
<label>自动定时采集</label>
<input type="checkbox" id="s_auto_collect" {{ 'checked' if auto.auto.auto_collect }}>
</div>
<div class="setting-row">
<label>采集间隔(分钟)</label>
<input type="number" id="s_scan_interval_min" value="{{ auto.auto.scan_interval_min }}">
</div>
<div class="setting-row">
<label>实时重要资讯邮件</label>
<input type="checkbox" id="s_realtime_enabled" {{ 'checked' if auto.auto.realtime_enabled }}>
</div>
<div class="setting-row">
<label>实时通知阈值(综合分)</label>
<input type="number" id="s_realtime_threshold" value="{{ auto.auto.realtime_threshold }}">
</div>
<div class="setting-row">
<label>LLM 深度分析阈值(规则分)</label>
<input type="number" id="s_llm_threshold" value="{{ auto.auto.llm_threshold }}">
</div>
<div class="setting-row">
<label>每日汇总邮件</label>
<input type="checkbox" id="s_summary_enabled" {{ 'checked' if auto.auto.summary_enabled }}>
</div>
<div class="setting-row">
<label>汇总时间</label>
<input type="time" id="s_summary_time" value="{{ auto.auto.summary_time }}">
</div>
<div class="setting-row">
<label>汇总窗口(小时)</label>
<input type="number" id="s_summary_window_hours" value="{{ auto.auto.summary_window_hours }}">
</div>
<div class="setting-row">
<label>汇总邮件最多条目</label>
<input type="number" id="s_max_summary_items" value="{{ auto.auto.max_summary_items }}">
</div>
</section>
<section class="card">
<h3>📮 邮件配置</h3>
<div class="setting-row"><label>SMTP 服务器</label><input id="m_host" value="{{ auto.mail.smtp_host }}"></div>
<div class="setting-row"><label>端口</label><input id="m_port" value="{{ auto.mail.smtp_port }}"></div>
<div class="setting-row"><label>账号</label><input id="m_user" value="{{ auto.mail.smtp_user }}"></div>
<div class="setting-row"><label>口令</label><input id="m_pass" type="password" value="{{ auto.mail.smtp_pass }}"></div>
<div class="setting-row">
<label>加密方式</label>
<select id="m_mode">
<option value="plain" {{ 'selected' if auto.mail.smtp_mode=='plain' }}>无加密</option>
<option value="starttls" {{ 'selected' if auto.mail.smtp_mode=='starttls' }}>STARTTLS</option>
<option value="ssl" {{ 'selected' if auto.mail.smtp_mode=='ssl' }}>SSL</option>
</select>
</div>
<div class="setting-row"><label>收件人</label><input id="m_to" value="{{ auto.mail.email_to }}"></div>
<div class="setting-row"><label>发件人名称</label><input id="m_name" value="{{ auto.mail.sender_name }}"></div>
</section>
</div>
<section class="card">
<h3>🤖 大模型接口
<span class="muted small">(切换后即时生效,智能分析将使用当前接口;失败自动切换下一个可用接口)</span>
</h3>
<div class="form-row" style="margin-bottom:12px;">
<input id="lp_name" placeholder="名称(如:SiliconFlow / DeepSeek 官方)">
<input id="lp_url" placeholder="Base URL(如 https://api.siliconflow.cn/v1" style="min-width:280px;">
<input id="lp_key" type="password" placeholder="API Key">
<input id="lp_model" placeholder="模型名(如 deepseek-ai/DeepSeek-V4-Flash">
<label style="display:flex;align-items:center;gap:4px;white-space:nowrap;">
<input type="checkbox" id="lp_active" checked> 设为当前
</label>
<button class="btn" onclick="addProvider()"> 添加接口</button>
</div>
<table>
<thead><tr><th>名称</th><th>Base URL</th><th>模型</th><th>状态</th><th>操作</th></tr></thead>
<tbody>
{% for p in providers %}
<tr data-pid="{{ p.id }}" data-name="{{ p.name }}" data-url="{{ p.base_url }}" data-model="{{ p.model }}" data-key="{{ p.api_key }}">
<td>
<b>{{ p.name }}</b>
{% if p.active %}<span class="tag ok">当前</span>{% endif %}
<div class="muted small" style="word-break:break-all;">{{ p.api_key[:6] }}…{{ p.api_key[-4:] if p.api_key|length>10 }}</div>
</td>
<td class="small" style="word-break:break-all;">{{ p.base_url }}</td>
<td class="small">{{ p.model }}</td>
<td>{% if p.enabled %}<span class="tag ok">启用</span>{% else %}<span class="tag">停用</span>{% endif %}</td>
<td>
{% if not p.active %}
<button class="btn mini" onclick="switchProvider({{ p.id }})">⚡ 切换为当前</button>
{% endif %}
<button class="btn mini" onclick="editProvider({{ p.id }})">编辑</button>
<button class="btn mini" onclick="testProvider({{ p.id }})">测试</button>
<button class="btn mini" onclick="toggleProvider({{ p.id }})">{{ '停用' if p.enabled else '启用' }}</button>
<button class="btn mini danger" onclick="delProvider({{ p.id }})">删除</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="lp_test_result" class="muted small" style="margin-top:10px;"></div>
</section>
{% endblock %}
{% block script %}
<script>
const G = id => document.getElementById(id).value;
async function saveAll(){
const auto = {
auto_collect: document.getElementById('s_auto_collect').checked ? 1 : 0,
realtime_enabled: document.getElementById('s_realtime_enabled').checked ? 1 : 0,
summary_enabled: document.getElementById('s_summary_enabled').checked ? 1 : 0,
scan_interval_min: parseInt(G('s_scan_interval_min'))||30,
realtime_threshold: parseInt(G('s_realtime_threshold'))||80,
llm_threshold: parseInt(G('s_llm_threshold'))||60,
summary_time: G('s_summary_time')||'10:00',
summary_window_hours: parseInt(G('s_summary_window_hours'))||24,
max_summary_items: parseInt(G('s_max_summary_items'))||15,
};
const mail = {
smtp_host: G('m_host'), smtp_port: parseInt(G('m_port'))||587,
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 });
toast(r.ok ? '✅ 设置已保存' : '❌ 保存失败', r.ok);
}
async function addProvider(){
const r = await API.json('/api/llm', {
action:'add',
name: G('lp_name')||'未命名接口',
base_url: G('lp_url'), api_key: G('lp_key'), model: G('lp_model'),
active: document.getElementById('lp_active').checked ? 1 : 0,
});
if (r.ok) { toast('✅ 接口已添加'); setTimeout(()=>location.reload(), 500); }
else toast('❌ 添加失败', false);
}
async function switchProvider(id){
const r = await API.json('/api/llm', {action:'switch', id});
toast(r.ok ? `⚡ 已切换:${r.name}` : '❌ 切换失败', r.ok);
if (r.ok) setTimeout(()=>location.reload(), 400);
}
async function editProvider(id){
const row = document.querySelector(`tr[data-pid="${id}"]`);
if (!row) return;
const name = prompt('接口名称', row.dataset.name);
if (name === null) return;
const url = prompt('Base URL', row.dataset.url);
if (url === null) return;
const model = prompt('模型名', row.dataset.model);
if (model === null) return;
const key = prompt('API Key(留空则不变)', '') || row.dataset.key;
const r = await API.json('/api/llm', {action:'update', id, name, base_url:url, api_key:key, model});
toast(r.ok ? '✅ 已更新' : '❌ 更新失败', r.ok);
if (r.ok) setTimeout(()=>location.reload(), 400);
}
async function testProvider(id){
const el = document.getElementById('lp_test_result');
el.textContent = '⏳ 测试中…';
const r = await API.json('/api/llm', {action:'test', id});
if (r.ok) el.textContent = `✅ ${r.name}${r.model})连接正常,回复:${r.reply}`;
else el.textContent = `❌ 测试失败:${r.error}`;
}
async function toggleProvider(id){
await API.json('/api/llm', {action:'toggle', id}); location.reload();
}
async function delProvider(id){
if (!confirm('确认删除该大模型接口?')) return;
await API.json('/api/llm', {action:'delete', id});
toast('✅ 已删除'); setTimeout(()=>location.reload(), 400);
}
</script>
{% endblock %}