364 lines
17 KiB
HTML
364 lines
17 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>🌐 网页提取服务(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 %}
|