223 lines
10 KiB
HTML
223 lines
10 KiB
HTML
{% 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>🤖 大模型接口
|
||
<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 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 r = await API.json('/api/settings', { ...auto, mail, custom });
|
||
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 %}
|