From 978ab813ebd211945c638256fcffbac88061b972 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Wed, 19 Aug 2026 23:37:04 +0800 Subject: [PATCH] =?UTF-8?q?v1.5.0:=20=E8=88=86=E6=83=85=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E4=B8=8E=E6=8C=81=E4=BB=93=E8=B7=9F?= =?UTF-8?q?=E8=B8=AA=E6=99=BA=E8=83=BD=E4=BD=93=E5=90=88=E5=B9=B6=E4=B8=BA?= =?UTF-8?q?=E3=80=8C=E2=8F=B0=20=E8=87=AA=E5=8A=A8=E5=8C=96=E3=80=8D?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E5=85=A5=E5=8F=A3(=E5=8F=8C=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E5=8D=A1);=20=E4=BF=AE=E5=A4=8D=E5=B8=83=E5=B0=94?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=AD=98True/False=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=A4=B1=E6=95=88=E7=9A=84=E5=AE=B9=E9=94=99?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 23 +++-- settings.py | 20 ++++- static/css/style.css | 10 +++ static/js/automation.js | 179 ++++++++++++++++++++++++++++++++++++++ static/js/settings.js | 81 +---------------- templates/automation.html | 81 +++++++++++++++++ templates/base.html | 2 +- templates/settings.html | 62 +------------ 8 files changed, 309 insertions(+), 149 deletions(-) create mode 100644 static/js/automation.js create mode 100644 templates/automation.html diff --git a/app.py b/app.py index 8a8767f..ed11693 100644 --- a/app.py +++ b/app.py @@ -60,6 +60,11 @@ def page_tracking(): return render_template("tracking.html", service=SERVICE_NAME, is_mock=IS_MOCK) +@app.route("/automation") +def page_automation(): + return render_template("automation.html", service=SERVICE_NAME, is_mock=IS_MOCK) + + @app.route("/admin") def page_admin(): return render_template("admin.html", service=SERVICE_NAME, is_mock=IS_MOCK) @@ -577,30 +582,38 @@ def api_settings(): @app.route("/api/settings", methods=["POST"]) def api_settings_save(): - from settings import save_all, set_setting + from settings import save_all, set_setting, _bool_str body = request.get_json(silent=True) or {} mail = body.get("mail") or {} llm = body.get("llm") or {} mono = body.get("monitor") or {} - # 邮件 + # 邮件(布尔规范化) for k in ("smtp_host", "smtp_port", "smtp_user", "smtp_pass", "smtp_mode", - "email_to", "sender_name", "email_enabled"): + "email_to", "sender_name"): if k in mail: set_setting(k, mail[k]) + if "email_enabled" in mail: + set_setting("email_enabled", _bool_str(mail["email_enabled"])) # LLM for k in ("llm_base_url", "llm_api_key", "llm_model"): if k in llm: set_setting(k, str(llm[k]).strip()) # 监控 - for k in ("monitor_enabled", "monitor_interval", "monitor_categories", + for k in ("monitor_interval", "monitor_categories", "monitor_sentiment", "monitor_importance", "monitor_keywords"): if k in mono: set_setting(k, mono[k]) + if "monitor_enabled" in mono: + set_setting("monitor_enabled", _bool_str(mono["monitor_enabled"])) # 持仓跟踪 track = body.get("tracking") or {} - for k in ("tracking_enabled", "tracking_interval", "tracking_notify", "tracking_impact_threshold"): + for k in ("tracking_interval", "tracking_impact_threshold"): if k in track: set_setting(k, track[k]) + if "tracking_enabled" in track: + set_setting("tracking_enabled", _bool_str(track["tracking_enabled"])) + if "tracking_notify" in track: + set_setting("tracking_notify", _bool_str(track["tracking_notify"])) return jsonify({"ok": True, "msg": "设置已保存"}) diff --git a/settings.py b/settings.py index d9517b0..d1d300e 100644 --- a/settings.py +++ b/settings.py @@ -9,6 +9,18 @@ from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, LLM_MAX_TOKENS, \ LLM_TEMPERATURE, LLM_TIMEOUT, MAIL_DEFAULTS, MONITOR_DEFAULTS, TRACKING_DEFAULTS +def _truthy(v): + """容错布尔解析:兼容 1/0/True/False/true/false/on/yes""" + if isinstance(v, bool): + return v + s = str(v or "").strip().lower() + return s in ("1", "true", "yes", "on", "y") + + +def _bool_str(v): + return "1" if _truthy(v) else "0" + + def get_setting(key, default=""): r = query_one("SELECT value FROM settings WHERE key=?", (key,)) return r["value"] if r else default @@ -52,7 +64,7 @@ def mail_config(): "smtp_mode": get_setting("smtp_mode", MAIL_DEFAULTS["smtp_mode"]), "email_to": get_setting("email_to", MAIL_DEFAULTS["email_to"]), "sender_name": get_setting("sender_name", MAIL_DEFAULTS["sender_name"]), - "email_enabled": get_setting("email_enabled", "1") == "1", + "email_enabled": _truthy(get_setting("email_enabled", "1")), } @@ -67,7 +79,7 @@ def monitor_config(): if k and k not in kw_list: kw_list.append(k) return { - "enabled": get_setting("monitor_enabled", "1") == "1", + "enabled": _truthy(get_setting("monitor_enabled", "1")), "interval_min": max(5, int(get_setting("monitor_interval", MONITOR_DEFAULTS["monitor_interval"]))), "categories": [c for c in cats.split(",") if c] if cats else [], "sentiment_weight": float(get_setting("monitor_sentiment", MONITOR_DEFAULTS["monitor_sentiment"])), @@ -97,9 +109,9 @@ def set_monitor_state(last_news_id=None, last_scan=None, last_sent=None): # ===================================================================== 持仓跟踪 def tracking_config(): return { - "enabled": get_setting("tracking_enabled", TRACKING_DEFAULTS["tracking_enabled"]) == "1", + "enabled": _truthy(get_setting("tracking_enabled", TRACKING_DEFAULTS["tracking_enabled"])), "interval_min": max(15, int(get_setting("tracking_interval", TRACKING_DEFAULTS["tracking_interval"]))), - "notify": get_setting("tracking_notify", TRACKING_DEFAULTS["tracking_notify"]) == "1", + "notify": _truthy(get_setting("tracking_notify", TRACKING_DEFAULTS["tracking_notify"])), "impact_threshold": float(get_setting("tracking_impact_threshold", TRACKING_DEFAULTS["tracking_impact_threshold"])), } diff --git a/static/css/style.css b/static/css/style.css index b1c55d1..bcfa271 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -202,6 +202,16 @@ tr:hover td { background: rgba(59,130,246,.05); } .between { justify-content: space-between; } .wrap { flex-wrap: wrap; } +/* ===== 选项卡 ===== */ +.tabs { display: flex; gap: 6px; border-bottom: 1px solid var(--border); margin-bottom: 4px; } +.tab-item { + padding: 10px 18px; border: none; background: none; color: var(--text2); + font-size: 14px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; + transition: .15s; +} +.tab-item:hover { color: var(--text); } +.tab-item.active { color: var(--accent); border-bottom-color: var(--accent); background: none; } + /* ===== 设置页 ===== */ .settings-form .sf-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } .settings-form .sf-row label { width: 110px; color: var(--text2); font-size: 13px; flex-shrink: 0; } diff --git a/static/js/automation.js b/static/js/automation.js new file mode 100644 index 0000000..cf726df --- /dev/null +++ b/static/js/automation.js @@ -0,0 +1,179 @@ +/* 自动化监控页:舆情驱动 + 持仓跟踪 双选项卡 */ +let autoSettings = null; + +/* ===================== 选项卡 ===================== */ +$$('.tab-item').forEach(t => t.onclick = () => { + $$('.tab-item').forEach(x => x.classList.toggle('active', x === t)); + $$('.tab-panel').forEach(p => p.style.display = (p.id === 'panel-' + t.dataset.tab) ? '' : 'none'); +}); + +/* ===================== 加载 ===================== */ +async function loadAuto() { + try { + autoSettings = await api('/api/settings'); + const mono = autoSettings.monitor, tr = autoSettings.tracking; + // 舆情 + $('#monitorEnabled').checked = !!mono.enabled; + $('#monitorInterval').value = mono.interval_min; + $('#monitorImportance').value = mono.importance_threshold; + $('#monitorSentiment').value = mono.sentiment_weight; + $('#monitorKeywords').value = (mono.keywords || []).join(','); + const cats = mono.categories || []; + $$('#monitorCats input').forEach(c => c.checked = cats.includes(c.value)); + renderMonitorState(autoSettings.state); + // 持仓跟踪 + $('#trackingEnabled').checked = !!tr.enabled; + $('#trackingInterval').value = tr.interval_min; + $('#trackingImpact').value = tr.impact_threshold; + $('#trackingNotify').checked = !!tr.notify; + renderTrackingState(tr); + loadLog(); + loadTrackReports(); + } catch (e) { toast('加载失败'); } +} + +function renderMonitorState(state) { + $('#monitorState').innerHTML = ` +
已处理新闻水位
#${state.last_news_id || 0}
+
上次扫描
${state.last_scan || '—'}
+
上次通知
${state.last_sent_count || 0} 条
`; +} + +function renderTrackingState(tr) { + $('#trackingState').innerHTML = ` +
自动跟踪
${tr.enabled ? '✅ 开启' : '⏸ 关闭'}(每 ${tr.interval_min} 分钟)
+
上次运行
${tr.last_run || '—'}
+
最近跟踪
${tr.last_stock || '—'}
+
最近告警影响度
${tr.last_alert ? tr.last_alert + '/100' : '—'}
`; +} + +function collectMonitor() { + const cats = $$('#monitorCats input:checked').map(c => c.value); + return { + monitor_enabled: $('#monitorEnabled').checked, + monitor_interval: $('#monitorInterval').value, + monitor_importance: $('#monitorImportance').value, + monitor_sentiment: $('#monitorSentiment').value, + monitor_categories: cats.join(','), + monitor_keywords: $('#monitorKeywords').value + }; +} + +/* ===================== 保存 ===================== */ +async function saveMonitor() { + try { + const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ monitor: collectMonitor() }) }); + toast(r.msg || '已保存'); + } catch (e) { toast('保存失败:' + e.message); } +} + +async function saveTracking() { + try { + const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tracking: { + tracking_enabled: $('#trackingEnabled').checked, + tracking_interval: $('#trackingInterval').value, + tracking_impact_threshold: $('#trackingImpact').value, + tracking_notify: $('#trackingNotify').checked + } }) }); + toast(r.msg || '已保存'); + } catch (e) { toast('保存失败:' + e.message); } +} + +/* ===================== 动作 ===================== */ +async function scanNow() { + const btn = event.target; btn.disabled = true; + try { + const r = await api('/api/monitor/scan', { method: 'POST' }); + toast(`扫描完成:检查 ${r.checked} 条,重要 ${r.important} 条,发送 ${r.sent} 条`); + loadAuto(); + } catch (e) { toast('扫描失败:' + e.message); } + btn.disabled = false; +} + +async function runTrackAll() { + const btn = event.target; btn.disabled = true; + try { + const r = await api('/api/tracking/run', { method: 'POST' }); + toast(r.msg || '已启动'); + setTimeout(loadTrackReports, 5000); + setTimeout(loadTrackReports, 30000); + setTimeout(loadTrackReports, 70000); + } catch (e) { toast('启动失败:' + e.message); } + btn.disabled = false; +} + +/* ===================== 日志与报告 ===================== */ +async function loadLog() { + try { + const d = await api('/api/monitor/log'); + const items = d.items || []; + if (!items.length) return; + $('#logTb').innerHTML = items.map(n => ` + + ${n.sent_at} + ${escapeHtml(n.title)} + ${n.category} + ${n.sentiment > 0 ? '利好' : n.sentiment < 0 ? '利空' : '中性'} (${Number(n.sentiment).toFixed(2)}) + ${n.importance} + ${n.status} + `).join(''); + } catch (e) {} +} + +async function loadTrackReports() { + try { + const d = await api('/api/tracking'); + const items = d.items || []; + if (!items.length) return; + $('#trackTb').innerHTML = items.slice(0, 10).map(r => { + let meta = {}; + try { meta = JSON.parse(r.meta || '{}'); } catch (e) {} + const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注'; + return ` + ${r.created_at} + ${r.stock_name}
${r.code}
+ ${meta.impact_score || '—'} + ${meta.change_kind || '—'} + ${escapeHtml(meta.summary || '')} + + `; + }).join(''); + } catch (e) {} +} + +async function showTrack(id) { + try { + const d = await api('/api/tracking/' + id); + const meta = d.meta || {}, src = d.sources || {}; + const sigCls = meta.impact_score >= 65 ? 'tag-强烈推荐' : meta.impact_score >= 45 ? 'tag-推荐' : 'tag-关注'; + const newsBlock = (label, icon, seg) => { + if (!seg) return ''; + const txt = ((seg.db || '') + (seg.rag || '')).trim(); + return `
${icon} ${label} ${txt.includes('[') ? txt.split('[').length - 1 : 0} +
${escapeHtml(txt || '暂无')}
`; + }; + openModal(` +

🧭 产业链跟踪:${d.stock_name}(${d.code})

+
+ ${escapeHtml(d.industry)} + 影响度 ${meta.impact_score || '—'}/100 + ${meta.change_kind || '—'} + ${d.created_at} +
+
${mdRender(d.report)}
+
+ 数据源(智能体参考内容) + ${newsBlock('个股直接动态', '📄', src.direct)} + ${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)} + ${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)} + ${newsBlock('同业竞争', '🏢', src.peers)} +
+
+ + 查看个股 → +
+ `); + } catch (e) { toast('加载失败'); } +} + +loadAuto(); diff --git a/static/js/settings.js b/static/js/settings.js index 38f37ce..382bef1 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1,10 +1,10 @@ -/* 系统设置页 */ +/* 系统设置页:邮件通知 + 大模型接口(舆情/跟踪已移至 /automation) */ let curSettings = null; async function loadSettings() { try { curSettings = await api('/api/settings'); - const mail = curSettings.mail, mono = curSettings.monitor, llm = curSettings.llm; + const mail = curSettings.mail, llm = curSettings.llm; $('#emailEnabled').checked = !!mail.email_enabled; $('#smtpHost').value = mail.smtp_host; $('#smtpPort').value = mail.smtp_port; @@ -16,42 +16,11 @@ async function loadSettings() { $('#llmBaseUrl').value = llm.base_url; $('#llmApiKey').value = llm.api_key; $('#llmModel').value = llm.model; - $('#monitorEnabled').checked = !!mono.enabled; - $('#monitorInterval').value = mono.interval_min; - $('#monitorImportance').value = mono.importance_threshold; - $('#monitorSentiment').value = mono.sentiment_weight; - $('#monitorKeywords').value = (mono.keywords || []).join(','); - // 分类勾选 - const cats = mono.categories || []; - $$('#monitorCats input').forEach(c => c.checked = cats.includes(c.value)); - renderState(curSettings.state); - // 持仓跟踪 - const tr = curSettings.tracking || {}; - $('#trackingEnabled').checked = !!tr.enabled; - $('#trackingInterval').value = tr.interval_min; - $('#trackingImpact').value = tr.impact_threshold; - $('#trackingNotify').checked = !!tr.notify; - renderTrackingState(tr); - loadLog(); } catch (e) { toast('设置加载失败'); } } -function renderState(state) { - $('#monitorState').innerHTML = ` -
已处理新闻水位
#${state.last_news_id || 0}
-
上次扫描
${state.last_scan || '—'}
-
上次通知
${state.last_sent_count || 0} 条
`; -} - -function renderTrackingState(tr) { - $('#trackingState').innerHTML = ` -
上次运行
${tr.last_run || '—'}
-
最近跟踪
${tr.last_stock || '—'}
-
最近告警影响度
${tr.last_alert ? tr.last_alert + '/100' : '—'}
`; -} - function collectMail() { return { smtp_host: $('#smtpHost').value.trim(), smtp_port: $('#smtpPort').value, @@ -61,18 +30,6 @@ function collectMail() { }; } -function collectMonitor() { - const cats = $$('#monitorCats input:checked').map(c => c.value); - return { - monitor_enabled: $('#monitorEnabled').checked, - monitor_interval: $('#monitorInterval').value, - monitor_importance: $('#monitorImportance').value, - monitor_sentiment: $('#monitorSentiment').value, - monitor_categories: cats.join(','), - monitor_keywords: $('#monitorKeywords').value - }; -} - async function save(which) { try { const body = {}; @@ -82,13 +39,6 @@ async function save(which) { llm_api_key: $('#llmApiKey').value.trim(), llm_model: $('#llmModel').value.trim() }; - if (which === 'monitor') body.monitor = collectMonitor(); - if (which === 'tracking') body.tracking = { - tracking_enabled: $('#trackingEnabled').checked, - tracking_interval: $('#trackingInterval').value, - tracking_impact_threshold: $('#trackingImpact').value, - tracking_notify: $('#trackingNotify').checked - }; const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); toast(r.msg || '已保存'); } catch (e) { toast('保存失败:' + e.message); } @@ -116,31 +66,4 @@ async function testLlm() { btn.disabled = false; btn.textContent = '🔌 测试连接'; } -async function scanNow() { - const btn = event.target; btn.disabled = true; - try { - const r = await api('/api/monitor/scan', { method: 'POST' }); - toast(`扫描完成:检查 ${r.checked} 条,重要 ${r.important} 条,发送 ${r.sent} 条` + (r.error ? '(' + r.error + ')' : '')); - loadSettings(); - } catch (e) { toast('扫描失败:' + e.message); } - btn.disabled = false; -} - -async function loadLog() { - try { - const d = await api('/api/monitor/log'); - const items = d.items || []; - if (!items.length) return; - $('#logTb').innerHTML = items.map(n => ` - - ${n.sent_at} - ${escapeHtml(n.title)} - ${n.category} - ${n.sentiment > 0 ? '利好' : n.sentiment < 0 ? '利空' : '中性'} (${Number(n.sentiment).toFixed(2)}) - ${n.importance} - ${n.status} - `).join(''); - } catch (e) {} -} - loadSettings(); diff --git a/templates/automation.html b/templates/automation.html new file mode 100644 index 0000000..b3e5218 --- /dev/null +++ b/templates/automation.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% block title %}自动化{% endblock %} +{% block page_title %}自动化监控{% endblock %} +{% block content %} +
+
+ + +
+ + +
+
+
定期扫描最新新闻,重要信息邮件通知
+ +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+ + +
+
+
+
通知日志
+
+ + + +
时间标题分类情感重要度状态
暂无记录
+
+
+
+ + + +
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index e83173f..ec96529 100644 --- a/templates/base.html +++ b/templates/base.html @@ -30,7 +30,7 @@ 📊 仪表盘 🏢 股票池 🎯 荐股中心 - 🧭 持仓跟踪 + ⏰ 自动化 📈 量化策略 📰 财经新闻 🏦 机构动向 diff --git a/templates/settings.html b/templates/settings.html index eef293b..31b8183 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -43,69 +43,11 @@
- 用于 AI 深度研报生成。支持任意 OpenAI 兼容接口(DeepSeek / 火山方舟等)。修改后立即生效。 + 用于 AI 深度研报生成。支持任意 OpenAI 兼容接口(DeepSeek / 火山方舟等)。修改后立即生效。
+ 💡 舆情监控 / 持仓跟踪等自动化参数已移至「⏰ 自动化」页面。
- - -
-
-
📰 舆情驱动自动化
- -
-
-
-
-
-
-
- - - - - -
-
-
-
-
- - -
-
-
- - -
-
-
🧭 持仓跟踪智能体
- -
-
-
-
-
- -
-
-
- - -
-
-
- - -
-
通知日志
-
- - - -
时间标题分类情感重要度状态
暂无记录
-
-
{% endblock %} {% block scripts %}