Files
stock-advisor/static/js/automation.js
T

310 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 自动化监控页:舆情驱动 + 持仓跟踪 双选项卡 */
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);
// 静默期(舆情)
const mq = (autoSettings.quiet && autoSettings.quiet.monitor) || {};
$('#monitorQuietEnabled').checked = !!mq.enabled;
$('#monitorQuietRanges').value = mq.ranges || '';
// 持仓跟踪
$('#trackingEnabled').checked = !!tr.enabled;
$('#trackingInterval').value = tr.interval_min;
$('#trackingImpact').value = tr.impact_threshold;
$('#trackingNotify').checked = !!tr.notify;
renderTrackingState(tr);
// 静默期(跟踪)
const tq = (autoSettings.quiet && autoSettings.quiet.tracking) || {};
$('#trackingQuietEnabled').checked = !!tq.enabled;
$('#trackingQuietRanges').value = tq.ranges || '';
loadLog();
loadTrackReports();
loadPositions();
loadTargets();
} 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
};
}
function collectMonitorQuiet() {
return {
enabled: $('#monitorQuietEnabled').checked,
ranges: $('#monitorQuietRanges').value.trim()
};
}
function collectTrackingQuiet() {
return {
enabled: $('#trackingQuietEnabled').checked,
ranges: $('#trackingQuietRanges').value.trim()
};
}
/* ===================== 保存 ===================== */
async function saveMonitor() {
try {
const r = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
monitor: collectMonitor(),
quiet: { monitor: collectMonitorQuiet() }
}) });
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
}, quiet: { tracking: collectTrackingQuiet() } }) });
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 loadPositions() {
try {
const d = await api('/api/watchlist');
const items = d.items || [];
if (!items.length) {
$('#posManage').innerHTML = '<div class="empty">暂无持仓。在下方搜索添加,或到 <a href="/stocks">股票池</a> / 个股详情页 ⭐ 添加</div>';
return;
}
$('#posManage').innerHTML = `<table>
<tr><th>股票</th><th>现价</th><th>今日</th><th>评分</th><th>操作</th></tr>
${items.map(p => `<tr>
<td><a href="/stock/${p.code}" target="_blank"><b>${p.name}</b></a><div style="color:var(--muted);font-size:12px">${p.code} · ${p.industry}</div></td>
<td class="num">${p.close}</td>
<td class="num ${pctClass(p.change_pct)}">${fmtPct(p.change_pct)}</td>
<td class="num">${p.score}</td>
<td><button class="btn btn-danger" onclick="removePos('${p.code}')">移除</button></td>
</tr>`).join('')}
</table>`;
} catch (e) { $('#posManage').innerHTML = '<div class="empty">加载失败</div>'; }
}
async function addPos() {
const kw = $('#posSearch').value.trim();
if (!kw) return;
try {
const d = await api('/api/stocks?keyword=' + encodeURIComponent(kw) + '&per=5');
const items = d.items || [];
if (!items.length) { toast('未找到该股票'); return; }
// 精确匹配或取第一个
const hit = items.find(x => x.code === kw || x.name === kw) || items[0];
await api('/api/watchlist/' + hit.code, { method: 'POST' });
toast(`已添加 ${hit.name} 为持仓`);
$('#posSearch').value = '';
loadPositions();
} catch (e) { toast('添加失败'); }
}
async function removePos(code) {
try {
await api('/api/watchlist/' + code, { method: 'DELETE' });
toast('已移除持仓');
loadPositions();
} catch (e) { toast('操作失败'); }
}
/* ===================== 概念/主题/股票目标管理 ===================== */
async function loadTargets() {
try {
const d = await api('/api/targets');
const items = d.items || [];
if (!items.length) {
$('#tgtManage').innerHTML = '<div class="empty">暂无跟踪目标,添加概念/主题(如 AI算力、低空经济)或个股目标</div>';
return;
}
$('#tgtManage').innerHTML = `<table>
<tr><th>类型</th><th>目标</th><th>检索关键词</th><th>状态</th><th>操作</th></tr>
${items.map(t => `<tr>
<td><span class="cat-tag ${t.type === 'concept' ? '行业' : '公司'}">${t.type === 'concept' ? '概念/主题' : '股票'}</span></td>
<td><b>${escapeHtml(t.name)}</b>${t.code ? `<div style="color:var(--muted);font-size:12px">${t.code}</div>` : ''}</td>
<td style="color:var(--text2)">${escapeHtml(t.keywords || '—')}</td>
<td>${t.enabled ? '<span class="tag tag-推荐">跟踪中</span>' : '<span class="tag tag-观望">停用</span>'}</td>
<td><button class="btn btn-danger" onclick="removeTarget(${t.id})">移除</button></td>
</tr>`).join('')}
</table>`;
} catch (e) { $('#tgtManage').innerHTML = '<div class="empty">加载失败</div>'; }
}
async function addTarget() {
const type = $('#tgtType').value;
const name = $('#tgtName').value.trim();
const keywords = $('#tgtKeywords').value.trim();
if (!name) { toast('请输入目标名称'); return; }
try {
await api('/api/targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, name, keywords }) });
toast('已添加跟踪目标');
$('#tgtName').value = ''; $('#tgtKeywords').value = '';
loadTargets();
} catch (e) { toast('添加失败:' + e.message); }
}
async function removeTarget(id) {
try {
await api('/api/targets/' + id, { method: 'DELETE' });
toast('已移除目标');
loadTargets();
} catch (e) { toast('操作失败'); }
}
/* ===================== 日志与报告 ===================== */
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-关注' : n.status === 'quiet' ? 'tag-关注' : 'tag-负'}">${n.status === 'sent' ? '已发送' : n.status === 'skipped' ? '已跳过' : n.status === 'quiet' ? '🌙 静默' : '失败'}</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-关注';
const isConcept = r.target_type === 'concept';
return `<tr class="row-link" onclick="showTrack(${r.id})">
<td style="color:var(--muted)">${r.created_at}</td>
<td><span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念' : '股票'}</span></td>
<td><b>${r.stock_name}</b>${isConcept ? '' : `<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:300px;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 isConcept = d.target_type === 'concept';
// 概念报告:展示关键词/资讯/受益个股;股票报告:四环节数据源
let sourceHtml = '';
if (isConcept) {
const dc = (meta.news_counts || {}).direct || 0, rc = (meta.news_counts || {}).rag || 0, nc = (meta.news_counts || {}).related || 0;
sourceHtml = `
<details open><summary>🔎 相关资讯(${dc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.db_news || '')}</pre></div></details>
<details><summary>🧠 语义检索命中(${rc} 条)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.rag_news || '')}</pre></div></details>
<details><summary>🏢 受益个股(${nc} 只)</summary><div class="src-body"><pre class="src-pre">${escapeHtml(src.related || '')}</pre></div></details>`;
} else {
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>`;
};
sourceHtml = newsBlock('个股直接动态', '📄', src.direct) +
newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) +
newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) +
newsBlock('同业竞争', '🏢', src.peers);
}
openModal(`
<h3>${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}${d.stock_name}${d.code && !isConcept ? '' + d.code + '' : ''}</h3>
<div class="news-meta" style="margin-bottom:12px">
<span class="cat-tag ${isConcept ? '行业' : '公司'}">${isConcept ? '概念/主题' : 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>
${sourceHtml}
</div>
<div style="margin-top:14px;text-align:right">
<button class="btn" onclick="document.querySelector('.modal-mask').classList.remove('show')">关闭</button>
${isConcept ? '' : `<a class="btn btn-primary" href="/stock/${d.code}" target="_blank">查看个股 →</a>`}
</div>
`);
} catch (e) { toast('加载失败'); }
}
loadAuto();