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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
AI 智能体定期跟踪持仓,深度分析产业链上下游
+
+
+
+
+
+
+
+
+
+
+
最近跟踪报告
+
+
+ | 时间 | 股票 | 影响度 | 性质 | 摘要 | 操作 |
+ | 暂无跟踪记录 |
+
+
+
+
+
+{% 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 %}