Files
news-tracker/templates/settings.html
T

364 lines
17 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 = '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="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>🌐 网页提取服务(web-capture-api
<span class="muted small">(数据源真实网页默认通过该服务抓取,每源可单独选获取方式与参数)</span>
</h3>
<div class="setting-row">
<label>启用 web-capture-api 抓取</label>
<input type="checkbox" id="w_enabled" {{ 'checked' if auto.webcapture.enabled }}>
</div>
<div class="setting-row">
<label>API 地址</label>
<input id="w_api_url" value="{{ auto.webcapture.api_url }}" placeholder="http://121.40.164.32:16025">
</div>
<div class="setting-row">
<label>抓取超时(秒)</label>
<input type="number" id="w_timeout" value="{{ auto.webcapture.timeout }}">
</div>
<div class="setting-row">
<label>连通性测试</label>
<span style="display:flex;gap:8px;align-items:center;">
<button class="btn mini" type="button" onclick="testWebcapture()">🧪 测试接口</button>
<span id="wc_test_result" class="muted small"></span>
</span>
</div>
</section>
<section class="card">
<h3>⚠️ 系统错误邮件通知
<span class="muted small">(采集/分析/通知/汇总/系统异常自动邮件通知,可设通知频率与静默时段)</span>
</h3>
<div class="setting-row">
<label>启用错误邮件通知</label>
<input type="checkbox" id="e_enabled" {{ 'checked' if auto.errnotify.enabled }}>
</div>
<div class="setting-row">
<label>通知方式</label>
<select id="e_mode">
<option value="immediate" {{ 'selected' if auto.errnotify.mode=='immediate' }}>⚡ 立即(出现即尝试发,仍受冷却限制)</option>
<option value="cooldown" {{ 'selected' if auto.errnotify.mode=='cooldown' }}>⏱ 按冷却聚合(到冷却时间集中发一封)</option>
<option value="off" {{ 'selected' if auto.errnotify.mode=='off' }}>🚫 关闭(仅记录不发送)</option>
</select>
</div>
<div class="setting-row">
<label>通知频率(冷却分钟)<span class="muted small">— 两次错误邮件最小间隔</span></label>
<input type="number" id="e_cooldown_min" value="{{ auto.errnotify.cooldown_min }}">
</div>
<div class="setting-row">
<label>单封最多错误条数</label>
<input type="number" id="e_max_items" value="{{ auto.errnotify.max_items }}">
</div>
<div class="setting-row">
<label>启用静默时段</label>
<input type="checkbox" id="e_quiet_enabled" {{ 'checked' if auto.errnotify.quiet_enabled }}>
</div>
<div style="margin-top:10px;">
<label style="font-size:12px;color:#6b7280;">静默时段(每行一段 <b>HH:MM-HH:MM</b>,支持跨午夜、多段;静默期不发送,结束后自动补发)</label>
<textarea id="e_quiet_periods" rows="3" style="margin-top:4px;">{{ '\n'.join(auto.errnotify.quiet_periods) }}</textarea>
</div>
<div class="setting-row">
<label>测试</label>
<span style="display:flex;gap:8px;align-items:center;">
<button class="btn mini" type="button" onclick="testErrorMail()">📨 发一封测试错误通知</button>
<span id="e_test_result" class="muted small"></span>
</span>
</div>
</section>
<section class="card">
<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>名称</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 style="white-space:nowrap;">
<button class="btn mini" onclick="moveProvider({{ p.id }}, -1)" title="上移(提高优先级)"></button>
<button class="btn mini" onclick="moveProvider({{ p.id }}, 1)" title="下移(降低优先级)"></button>
</td>
<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 class="muted small" style="margin-top:8px;">💡 调用顺序按优先级:高优先级失败会自动切换下一个,全部失败才算调用异常(邮件通知)。</div>
<div id="lp_test_result" class="muted small" style="margin-top:10px;"></div>
</section>
<section class="card">
<h3>📊 LLM 调用统计 <span class="muted small">次数 / Token / 失败(按天按接口记录)</span></h3>
<div class="two-col">
<div>
<h4 style="font-size:14px;color:#111827;margin:4px 0 8px;">各接口累计</h4>
<table>
<thead><tr><th>接口</th><th>调用次数</th><th>Prompt Tokens</th><th>Completion</th><th>总 Tokens</th><th>失败</th></tr></thead>
<tbody>
{% for s in llm_stats %}
<tr>
<td>{{ s.provider }}</td><td>{{ s.calls }}</td><td>{{ s.prompt_tokens }}</td>
<td>{{ s.completion_tokens }}</td><td>{{ s.total_tokens }}</td>
<td>{% if s.failures %}<span class="tag err">{{ s.failures }}</span>{% else %}0{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="6" class="empty">暂无调用记录(有 LLM 分析后自动统计)</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div>
<h4 style="font-size:14px;color:#111827;margin:4px 0 8px;">最近 14 天合计</h4>
<table>
<thead><tr><th>日期</th><th>调用</th><th>总 Tokens</th><th>失败</th></tr></thead>
<tbody>
{% for d in llm_daily %}
<tr><td>{{ d.day }}</td><td>{{ d.calls }}</td><td>{{ d.total_tokens }}</td>
<td>{% if d.failures %}<span class="tag err">{{ d.failures }}</span>{% else %}0{% endif %}</td></tr>
{% else %}
<tr><td colspan="4" class="empty">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</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 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 webcapture = {
enabled: document.getElementById('w_enabled').checked ? 1 : 0,
api_url: G('w_api_url').trim() || 'http://121.40.164.32:16025',
timeout: parseInt(G('w_timeout')) || 60,
};
const errnotify = {
enabled: document.getElementById('e_enabled').checked ? 1 : 0,
mode: G('e_mode'),
cooldown_min: parseInt(G('e_cooldown_min')) || 60,
max_items: parseInt(G('e_max_items')) || 10,
quiet_enabled: document.getElementById('e_quiet_enabled').checked ? 1 : 0,
quiet_periods: G('e_quiet_periods').split(/[\n,;,;]/).map(s=>s.trim()).filter(Boolean),
};
const r = await API.json('/api/settings', { ...auto, mail, custom, webcapture, errnotify });
toast(r.ok ? '✅ 设置已保存' : '❌ 保存失败', r.ok);
}
async function testWebcapture(){
const el = document.getElementById('wc_test_result');
el.textContent = '⏳ 测试中...';
const r = await API.json('/api/actions', {action:'test_webcapture'});
if (r.ok) el.textContent = `✅ ${r.api_url} 正常,抓取到:${r.title}${(r.text||'').trim()}`;
else el.textContent = '❌ ' + (r.error || '测试失败');
}
async function testErrorMail(){
const el = document.getElementById('e_test_result');
el.textContent = '⏳ 发送中...';
const r = await API.json('/api/actions', {action:'test_error_mail'});
if (r.ok) el.textContent = '✅ 测试错误通知邮件已发送';
else el.textContent = '❌ ' + (r.error || '发送失败');
}
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 moveProvider(id, dir){
const r = await API.json('/api/llm', {action:'move', id, direction: dir});
toast(r.ok ? '✅ 已调整优先级' : ('❌ ' + (r.error || '移动失败')), r.ok);
if (r.ok) location.reload();
}
async function delProvider(id){
if (!confirm('确认删除该大模型接口?')) return;
await API.json('/api/llm', {action:'delete', id});
toast('✅ 已删除'); setTimeout(()=>location.reload(), 400);
}
</script>
{% endblock %}