2026-08-20 00:26:07 +08:00
|
|
|
|
{% 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>
|
2026-08-28 16:06:07 +08:00
|
|
|
|
<input type="checkbox" id="s_auto_collect" {{ 'checked' if auto.auto.auto_collect }}>
|
2026-08-20 00:26:07 +08:00
|
|
|
|
</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>
|
2026-08-28 16:06:07 +08:00
|
|
|
|
<input type="checkbox" id="s_realtime_enabled" {{ 'checked' if auto.auto.realtime_enabled }}>
|
2026-08-20 00:26:07 +08:00
|
|
|
|
</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>
|
2026-08-28 16:06:07 +08:00
|
|
|
|
<input type="checkbox" id="s_summary_enabled" {{ 'checked' if auto.auto.summary_enabled }}>
|
2026-08-20 00:26:07 +08:00
|
|
|
|
</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>
|
2026-08-28 16:06:07 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-08-20 00:26:07 +08:00
|
|
|
|
{% endblock %}
|
|
|
|
|
|
{% block script %}
|
|
|
|
|
|
<script>
|
2026-08-28 16:06:07 +08:00
|
|
|
|
const G = id => document.getElementById(id).value;
|
|
|
|
|
|
|
2026-08-20 00:26:07 +08:00
|
|
|
|
async function saveAll(){
|
|
|
|
|
|
const auto = {
|
2026-08-28 16:06:07 +08:00
|
|
|
|
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,
|
2026-08-20 00:26:07 +08:00
|
|
|
|
};
|
|
|
|
|
|
const mail = {
|
2026-08-28 16:06:07 +08:00
|
|
|
|
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'),
|
2026-08-20 00:26:07 +08:00
|
|
|
|
};
|
|
|
|
|
|
const r = await API.json('/api/settings', { ...auto, mail });
|
|
|
|
|
|
toast(r.ok ? '✅ 设置已保存' : '❌ 保存失败', r.ok);
|
|
|
|
|
|
}
|
2026-08-28 16:06:07 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-08-20 00:26:07 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
{% endblock %}
|