v1.5.0: 舆情驱动自动化与持仓跟踪智能体合并为「 自动化」导航入口(双选项卡); 修复布尔设置存True/False导致读取失效的容错解析

This commit is contained in:
2026-08-19 23:37:04 +08:00
parent 99654de0fd
commit 978ab813eb
8 changed files with 309 additions and 149 deletions
+179
View File
@@ -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 = `
<div class="mini-stat"><div class="ms-label">已处理新闻水位</div><div class="ms-value num">#${state.last_news_id || 0}</div></div>
<div class="mini-stat"><div class="ms-label">上次扫描</div><div class="ms-value">${state.last_scan || '—'}</div></div>
<div class="mini-stat"><div class="ms-label">上次通知</div><div class="ms-value num">${state.last_sent_count || 0} 条</div></div>`;
}
function renderTrackingState(tr) {
$('#trackingState').innerHTML = `
<div class="mini-stat"><div class="ms-label">自动跟踪</div><div class="ms-value">${tr.enabled ? '✅ 开启' : '⏸ 关闭'}(每 ${tr.interval_min} 分钟)</div></div>
<div class="mini-stat"><div class="ms-label">上次运行</div><div class="ms-value">${tr.last_run || '—'}</div></div>
<div class="mini-stat"><div class="ms-label">最近跟踪</div><div class="ms-value">${tr.last_stock || '—'}</div></div>
<div class="mini-stat"><div class="ms-label">最近告警影响度</div><div class="ms-value num">${tr.last_alert ? tr.last_alert + '/100' : '—'}</div></div>`;
}
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 => `
<tr>
<td style="color:var(--muted)">${n.sent_at}</td>
<td style="max-width:300px;white-space:normal">${escapeHtml(n.title)}</td>
<td><span class="cat-tag ${n.category}">${n.category}</span></td>
<td class="num ${pctClass(n.sentiment)}">${n.sentiment > 0 ? '利好' : n.sentiment < 0 ? '利空' : '中性'} (${Number(n.sentiment).toFixed(2)})</td>
<td class="num"><b>${n.importance}</b></td>
<td><span class="tag ${n.status === 'sent' ? 'tag-推荐' : n.status === 'skipped' ? 'tag-关注' : 'tag-负'}">${n.status}</span></td>
</tr>`).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 `<tr class="row-link" onclick="showTrack(${r.id})">
<td style="color:var(--muted)">${r.created_at}</td>
<td><b>${r.stock_name}</b><div style="color:var(--muted);font-size:12px">${r.code}</div></td>
<td><span class="tag ${sigCls}">${meta.impact_score || '—'}</span></td>
<td><span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span></td>
<td style="max-width:320px;white-space:normal;color:var(--text2)">${escapeHtml(meta.summary || '')}</td>
<td><button class="btn" onclick="event.stopPropagation();showTrack(${r.id})">查看 ↗</button></td>
</tr>`;
}).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 `<details><summary>${icon} ${label} <span class="src-count">${txt.includes('[') ? txt.split('[').length - 1 : 0}</span></summary>
<div class="src-body"><pre class="src-pre">${escapeHtml(txt || '暂无')}</pre></div></details>`;
};
openModal(`
<h3>🧭 产业链跟踪:${d.stock_name}${d.code}</h3>
<div class="news-meta" style="margin-bottom:12px">
<span class="cat-tag">${escapeHtml(d.industry)}</span>
<span class="tag ${sigCls}">影响度 ${meta.impact_score || '—'}/100</span>
<span class="tag tag-${meta.change_kind === '利好' ? '强烈推荐' : meta.change_kind === '利空' ? '负' : '关注'}">${meta.change_kind || '—'}</span>
<span style="color:var(--muted)">${d.created_at}</span>
</div>
<div class="markdown-body">${mdRender(d.report)}</div>
<div style="margin-top:16px;border-top:1px solid var(--border);padding-top:10px">
<b style="color:var(--gold)">数据源(智能体参考内容)</b>
${newsBlock('个股直接动态', '📄', src.direct)}
${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)}
${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)}
${newsBlock('同业竞争', '🏢', src.peers)}
</div>
<div style="margin-top:14px;text-align:right">
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>
</div>
`);
} catch (e) { toast('加载失败'); }
}
loadAuto();