From 9406f9de4340be8d766f14df95e7538355a74fe6 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Wed, 19 Aug 2026 23:49:37 +0800 Subject: [PATCH] =?UTF-8?q?v1.6.0:=20=E7=9B=AE=E6=A0=87=E8=B7=9F=E8=B8=AA(?= =?UTF-8?q?=E6=A6=82=E5=BF=B5/=E4=B8=BB=E9=A2=98/=E8=82=A1=E7=A5=A8)=20-?= =?UTF-8?q?=20watch=5Ftargets=E8=A1=A8+=E6=A6=82=E5=BF=B5=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E8=B7=9F=E8=B8=AA(=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E8=AF=8D=E6=A3=80=E7=B4=A2+=E5=8F=97=E7=9B=8A=E4=B8=AA?= =?UTF-8?q?=E8=82=A1+=E5=BD=B1=E5=93=8D=E5=BA=A6=E5=88=A4=E5=AE=9A+?= =?UTF-8?q?=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5),=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=E9=A1=B5=E5=8A=A0=E6=8C=81=E4=BB=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=92=8C=E7=9B=AE=E6=A0=87=E7=AE=A1=E7=90=86,=20=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=94=AF=E6=8C=81=E8=82=A1=E7=A5=A8/=E6=A6=82?= =?UTF-8?q?=E5=BF=B5=E5=8F=8C=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 31 ++++++- database.py | 20 +++- engine/agent.py | 191 +++++++++++++++++++++++++++++++++----- static/js/automation.js | 135 ++++++++++++++++++++++++--- static/js/tracking.js | 31 +++++-- templates/automation.html | 32 ++++++- templates/tracking.html | 14 +-- 7 files changed, 389 insertions(+), 65 deletions(-) diff --git a/app.py b/app.py index ed11693..791d5fe 100644 --- a/app.py +++ b/app.py @@ -689,18 +689,18 @@ def api_tracking_detail(rid): @app.route("/api/tracking/run", methods=["POST"]) def api_tracking_run(): - """立即跟踪全部自选股(后台)""" + """立即跟踪全部目标(持仓+概念/主题/股票,后台)""" import threading def run(): - from engine.agent import track_watchlist + from engine.agent import track_all try: - track_watchlist() + track_all() except Exception as e: log.error("track run fail: %s", e) threading.Thread(target=run, daemon=True).start() - return jsonify({"ok": True, "msg": "跟踪任务已启动(逐只分析,约每只30-60秒)"}) + return jsonify({"ok": True, "msg": "跟踪任务已启动(逐目标分析,约每只30-60秒)"}) @app.route("/api/tracking/run/", methods=["POST"]) @@ -718,6 +718,29 @@ def api_tracking_run_one(code): return jsonify({"ok": True, "msg": "跟踪已启动"}) +@app.route("/api/targets") +def api_targets_list(): + from engine.agent import list_targets + return jsonify({"items": list_targets()}) + + +@app.route("/api/targets", methods=["POST"]) +def api_targets_add(): + from engine.agent import add_target + body = request.get_json(silent=True) or {} + r = add_target(body.get("type", "concept"), body.get("name", ""), + body.get("code", ""), body.get("keywords", "")) + if r.get("error"): + return jsonify(r), 400 + return jsonify(r) + + +@app.route("/api/targets/", methods=["DELETE"]) +def api_targets_delete(tid): + from engine.agent import delete_target + return jsonify(delete_target(tid)) + + # ------------------------------------------------------------------ 数据管理 @app.route("/api/admin/stats") def api_admin_stats(): diff --git a/database.py b/database.py index ef5c6bf..a553c86 100644 --- a/database.py +++ b/database.py @@ -142,10 +142,11 @@ CREATE TABLE IF NOT EXISTS notification_log ( CREATE TABLE IF NOT EXISTS tracking_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, + target_type TEXT DEFAULT 'stock', -- stock(股票) / concept(概念主题) code TEXT NOT NULL, stock_name TEXT DEFAULT '', industry TEXT DEFAULT '', - report TEXT DEFAULT '', -- 产业链深度分析(markdown) + report TEXT DEFAULT '', -- 产业链/概念深度分析(markdown) meta TEXT DEFAULT '{}', -- JSON:significance/impact_score/summary/新闻统计 sources TEXT DEFAULT '{}', -- JSON:个股/上游/下游/同业 采集的资讯 + 指标 + 评级 status TEXT DEFAULT 'done', @@ -153,6 +154,16 @@ CREATE TABLE IF NOT EXISTS tracking_reports ( ); CREATE INDEX IF NOT EXISTS idx_tracking_code ON tracking_reports(code); +CREATE TABLE IF NOT EXISTS watch_targets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT 'concept', -- concept(概念/主题) / stock(股票) + code TEXT DEFAULT '', -- stock 时的股票代码 + name TEXT NOT NULL, -- 概念/主题名 或 股票名 + keywords TEXT DEFAULT '', -- concept 的检索关键词(逗号分隔) + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now','localtime')) +); + CREATE TABLE IF NOT EXISTS market_index ( date TEXT PRIMARY KEY, sh REAL DEFAULT 0, -- 上证指数(点) @@ -187,6 +198,10 @@ def db(): def init_db(): with db() as conn: conn.executescript(SCHEMA) + # 迁移:老库补列 + cols = {r[1] for r in conn.execute("PRAGMA table_info(tracking_reports)")} + if "target_type" not in cols: + conn.execute("ALTER TABLE tracking_reports ADD COLUMN target_type TEXT DEFAULT 'stock'") def query(sql, args=()): @@ -219,7 +234,8 @@ def wipe_all(): """清空业务表(保留结构)+ 重置自增序列,用于重灌数据""" for t in ("stock_daily", "inst_ratings", "fund_holdings", "news", "institutions", "stocks", "watchlist", "analysis_cache", "analysis_history", - "market_index", "strategy_backtests", "notification_log", "tracking_reports"): + "market_index", "strategy_backtests", "notification_log", "tracking_reports", + "watch_targets"): with db() as conn: conn.execute(f'DELETE FROM "{t}"') with db() as conn: diff --git a/engine/agent.py b/engine/agent.py index 2660a41..9ec5a59 100644 --- a/engine/agent.py +++ b/engine/agent.py @@ -261,8 +261,8 @@ def track_stock(code, focus=""): "ratings": seg["ratings"], "holdings": seg["holdings"], } execute( - "INSERT INTO tracking_reports(code, stock_name, industry, report, meta, sources, status, created_at) " - "VALUES(?,?,?,?,?,?,'done',datetime('now','localtime'))", + "INSERT INTO tracking_reports(target_type, code, stock_name, industry, report, meta, sources, status, created_at) " + "VALUES('stock',?,?,?,?,?,?,'done',datetime('now','localtime'))", (code, s["name"], s["industry"], clean_report, json.dumps(meta, ensure_ascii=False), json.dumps(sources, ensure_ascii=False))) rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"] @@ -287,60 +287,201 @@ def _first_heading(text): def _notify_if_significant(rid, stock, meta): - """影响度达阈值且开启通知 → 邮件""" + """影响度达阈值且开启通知 → 邮件(股票/概念通用)""" cfg = tracking_config() try: - if cfg["notify"] and int(meta["impact_score"]) >= int(cfg["impact_threshold"]): + if cfg["notify"] and int(meta.get("impact_score", 0)) >= int(cfg["impact_threshold"]): from engine.notifier import send_email mc = mail_config() + nc = meta.get("news_counts") or {} + detail = (f"个股资讯 {nc.get('direct', 0)} 条 / 上游 {nc.get('upstream', 0)} 条 / " + f"下游 {nc.get('downstream', 0)} 条 / 同业 {nc.get('peers', 0)} 条" + if "upstream" in nc else + f"相关资讯 {nc.get('direct', 0)} 条 / 语义检索 {nc.get('rag', 0)} 条 / " + f"受益个股 {nc.get('related', 0)} 只") send_email( - f"[持仓跟踪] {stock['name']} 出现{meta.get('change_kind','')}动态(影响度{meta['impact_score']})", + f"[持仓跟踪] {stock['name']} 出现{meta.get('change_kind', '')}动态(影响度{meta.get('impact_score', 0)})", f"""
-
🧭 持仓跟踪 · {stock['name']}({stock['code']})
+
🧭 智能体跟踪 · {stock['name']}({stock['code']})
-

影响度:{meta['impact_score']}/100({'🔴 重大' if meta['impact_score']>=65 else '🟡 关注'})
-性质:{meta.get('change_kind','')} | 显著性:{meta.get('significance','')}

-

摘要:{meta.get('summary','')}

-

产业链趋势:{meta.get('chain_trend','')}

-

个股资讯 {meta['news_counts']['direct']} 条 / 上游 {meta['news_counts']['upstream']} 条 / 下游 {meta['news_counts']['downstream']} 条 / 同业 {meta['news_counts']['peers']} 条

+

影响度:{meta.get('impact_score', 0)}/100({'🔴 重大' if int(meta.get('impact_score', 0)) >= 65 else '🟡 关注'})
+性质:{meta.get('change_kind', '')} | 显著性:{meta.get('significance', '')}

+

摘要:{meta.get('summary', '')}

+

趋势判断:{meta.get('chain_trend', '')}

+

{detail}

""", cfg=mc) - set_tracking_state(last_alert=int(meta["impact_score"])) + set_tracking_state(last_alert=int(meta.get("impact_score", 0))) except Exception as e: log.warning("track notify fail: %s", e) +# ===================================================================== 概念/主题跟踪 +def track_concept(name, keywords="", focus=""): + """跟踪一个概念/主题:采集相关新闻 → 受益个股梳理 → LLM 深度分析 → 影响度判定""" + kw = keywords.strip() or name + kw_list = [k.strip() for k in kw.replace(",", ",").split(",") if k.strip()] + kws = " ".join(kw_list) + # 1) 采集:DB 关键词检索 + RAG 向量检索 + conds, args = [], [] + for k in kw_list[:5]: + conds.append("(title LIKE ? OR content LIKE ?)") + args += [f"%{k}%", f"%{k}%"] + args.append(15) + db_news = query( + f"SELECT id,title,content,source,category,publish_date,sentiment,related_stocks FROM news " + f"WHERE ({' OR '.join(conds)}) AND publish_date >= date('now','-45 day') " + f"ORDER BY publish_date DESC LIMIT ?", args) + rag_hits = _rag_news_text(f"{name} 概念主题 {kws} 最新动态 政策 催化", top_k=8) + db_text = _fmt_news(db_news) + rag_text = "\n".join(rag_hits) or " (暂无)" + # 2) 受益个股:从 DB 新闻 related_stocks + RAG 命中元数据 code 汇总 + codes = set() + for n in db_news: + for c in (n["related_stocks"] or "").split(","): + if c: + codes.add(c) + try: + for h in query_vectors(f"{name} {kws} 受益个股", n_results=8, name=CHROMA_NEWS_COLLECTION): + c = (h.get("metadata") or {}).get("code") + if c: + codes.add(c) + except Exception: + pass + related = query("SELECT code, name, industry FROM stocks WHERE code IN (%s)" % + ",".join(["?"] * len(codes)), tuple(codes)) if codes else [] + related_txt = "、".join(f"{r['name']}({r['code']},{r['industry']})" for r in related) or "暂无" + # 3) 提示词 + prompt = f"""你是资深题材/概念跟踪分析师,正在深度跟踪【{name}】这一概念主题。 + +【概念/主题】{name} +【检索关键词】{kws} + +【最新相关资讯】 +{db_text} +{rag_text} + +【关联受益个股】{related_txt} + +【输出要求】 +第一步,先输出一个 json 代码块(必须最先输出): +```json +{{"significance":"high|medium|low","impact_score":0到100的整数,"change_kind":"利好/利空/中性/震荡","summary":"一句话总结","chain_trend":"题材趋势判断"}} +``` +第二步,输出 Markdown 分析报告,结构如下: +## 一、概念主题最新动态 +## 二、核心驱动与催化(政策/产业/事件) +## 三、受益标的梳理(关联个股及逻辑) +## 四、市场情绪与资金动向 +## 五、风险提示 +## 关注要点(3-5条) + +分析须严格基于资讯,避免编造。impact_score >=65 视为重大变化。""" + try: + reply = llm_chat([ + {"role": "system", "content": "你是一名严谨专业的题材与概念跟踪分析师。"}, + {"role": "user", "content": prompt}, + ]).strip() + if not reply: + raise RuntimeError("LLM 返回为空") + judge = parse_judge(reply) + clean_report = strip_json_block(reply) + summary = (judge or {}).get("summary") or _first_heading(clean_report) + meta = { + "significance": (judge or {}).get("significance", "medium"), + "impact_score": int((judge or {}).get("impact_score", 50)), + "change_kind": (judge or {}).get("change_kind", "中性"), + "summary": summary, + "chain_trend": (judge or {}).get("chain_trend", ""), + "news_counts": {"direct": len(db_news), "rag": len(rag_hits), "related": len(related)}, + "keywords": kws, + } + sources = {"keywords": kws, "db_news": db_text, "rag_news": rag_text, + "related": related_txt} + cid = f"CONCEPT:{name}" + execute( + "INSERT INTO tracking_reports(target_type, code, stock_name, industry, report, meta, sources, status, created_at) " + "VALUES('concept',?,?,?,?,?,?,'done',datetime('now','localtime'))", + (cid, name, "概念/主题", clean_report, json.dumps(meta, ensure_ascii=False), + json.dumps(sources, ensure_ascii=False))) + rid = query_one("SELECT MAX(id) id FROM tracking_reports")["id"] + _notify_if_significant(rid, {"name": name, "code": cid}, meta) + return {"ok": True, "report_id": rid, "meta": meta} + except Exception as e: + log.exception("track concept %s fail", name) + return {"error": str(e)} + + +# ===================================================================== 目标管理 +def list_targets(): + return query("SELECT * FROM watch_targets ORDER BY type, id") + + +def add_target(ttype, name, code="", keywords=""): + name = name.strip() + if not name: + return {"error": "名称不能为空"} + execute("INSERT INTO watch_targets(type, code, name, keywords) VALUES(?,?,?,?)", + (ttype, code, name, keywords)) + return {"ok": True} + + +def delete_target(tid): + execute("DELETE FROM watch_targets WHERE id=?", (tid,)) + return {"ok": True} + + # ===================================================================== 批量与调度 -def track_watchlist(progress=None): - """串行跟踪自选股(持仓)。同一时刻只允许一个跟踪任务(防重复)""" +def track_all(progress=None): + """跟踪全部目标:持仓(自选股)+ 目标(概念/主题/股票)""" global _cycle_running if _cycle_running: return {"tracked": 0, "msg": "已有跟踪任务进行中,请稍后再试"} _cycle_running = True try: - stocks = query("SELECT w.code, s.name FROM watchlist w JOIN stocks s ON s.code=w.code ORDER BY w.added_at") - if not stocks: - return {"tracked": 0, "msg": "自选股为空,请先在股票池添加"} + targets = [] + # 持仓(自选股) + for w in query("SELECT w.code, s.name FROM watchlist w JOIN stocks s ON s.code=w.code ORDER BY w.added_at"): + targets.append(("stock", w["code"], w["name"])) + # 目标(股票/概念) + for t in query("SELECT id, type, code, name, keywords FROM watch_targets WHERE enabled=1 ORDER BY id"): + if t["type"] == "stock" and t["code"]: + targets.append(("stock", t["code"], t["name"])) + else: + targets.append(("concept", t["name"], t["keywords"] or t["name"])) + # 去重 + seen, uniq = set(), [] + for typ, key, name in targets: + u = (typ, key) + if u in seen: + continue + seen.add(u) + uniq.append((typ, key, name)) + if not uniq: + return {"tracked": 0, "msg": "暂无跟踪目标:请添加持仓/自选股或概念主题目标"} results = [] - for i, st in enumerate(stocks): - r = track_stock(st["code"]) - results.append({"code": st["code"], "name": st["name"], **r}) - set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=st["name"]) + for i, (typ, key, name) in enumerate(uniq): + if typ == "stock": + r = track_stock(key) + else: + r = track_concept(key, name) + results.append({"type": typ, "name": name, **r}) + set_tracking_state(last_run=time.strftime("%Y-%m-%d %H:%M:%S"), last_stock=name) if progress: - progress(i + 1, len(stocks)) + progress(i + 1, len(uniq)) return {"tracked": len(results), "results": results} finally: _cycle_running = False def latest_reports(code, limit=5): - return query("SELECT id, code, stock_name, industry, meta, status, created_at " + return query("SELECT id, target_type, code, stock_name, industry, meta, status, created_at " "FROM tracking_reports WHERE code=? ORDER BY id DESC LIMIT ?", (code, limit)) def list_reports(limit=30): - return query("SELECT id, code, stock_name, industry, meta, status, created_at " + return query("SELECT id, target_type, code, stock_name, industry, meta, status, created_at " "FROM tracking_reports ORDER BY id DESC LIMIT ?", (limit,)) @@ -371,7 +512,7 @@ class TrackingThread(threading.Thread): continue if cfg["enabled"]: try: - r = track_watchlist() + r = track_all() log.info("tracking cycle: %s", r) except Exception as e: log.warning("tracking cycle error: %s", e) diff --git a/static/js/automation.js b/static/js/automation.js index cf726df..0d0ef69 100644 --- a/static/js/automation.js +++ b/static/js/automation.js @@ -29,6 +29,8 @@ async function loadAuto() { renderTrackingState(tr); loadLog(); loadTrackReports(); + loadPositions(); + loadTargets(); } catch (e) { toast('加载失败'); } } @@ -102,6 +104,95 @@ async function runTrackAll() { btn.disabled = false; } +/* ===================== 持仓(自选股)管理 ===================== */ +async function loadPositions() { + try { + const d = await api('/api/watchlist'); + const items = d.items || []; + if (!items.length) { + $('#posManage').innerHTML = '
暂无持仓。在下方搜索添加,或到 股票池 / 个股详情页 ⭐ 添加
'; + return; + } + $('#posManage').innerHTML = ` + + ${items.map(p => ` + + + + + + `).join('')} +
股票现价今日评分操作
${p.name}
${p.code} · ${p.industry}
${p.close}${fmtPct(p.change_pct)}${p.score}
`; + } catch (e) { $('#posManage').innerHTML = '
加载失败
'; } +} + +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 = '
暂无跟踪目标,添加概念/主题(如 AI算力、低空经济)或个股目标
'; + return; + } + $('#tgtManage').innerHTML = ` + + ${items.map(t => ` + + + + + + `).join('')} +
类型目标检索关键词状态操作
${t.type === 'concept' ? '概念/主题' : '股票'}${escapeHtml(t.name)}${t.code ? `
${t.code}
` : ''}
${escapeHtml(t.keywords || '—')}${t.enabled ? '跟踪中' : '停用'}
`; + } catch (e) { $('#tgtManage').innerHTML = '
加载失败
'; } +} + +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 { @@ -129,12 +220,14 @@ async function loadTrackReports() { 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 ` ${r.created_at} - ${r.stock_name}
${r.code}
+ ${isConcept ? '概念' : '股票'} + ${r.stock_name}${isConcept ? '' : `
${r.code}
`} ${meta.impact_score || '—'} ${meta.change_kind || '—'} - ${escapeHtml(meta.summary || '')} + ${escapeHtml(meta.summary || '')} `; }).join(''); @@ -146,16 +239,31 @@ async function showTrack(id) { 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 || '暂无')}
`; - }; + 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 = ` +
🔎 相关资讯(${dc} 条)
${escapeHtml(src.db_news || '')}
+
🧠 语义检索命中(${rc} 条)
${escapeHtml(src.rag_news || '')}
+
🏢 受益个股(${nc} 只)
${escapeHtml(src.related || '')}
`; + } else { + 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 || '暂无')}
`; + }; + sourceHtml = newsBlock('个股直接动态', '📄', src.direct) + + newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) + + newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) + + newsBlock('同业竞争', '🏢', src.peers); + } openModal(` -

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

+

${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}

- ${escapeHtml(d.industry)} + ${isConcept ? '概念/主题' : d.industry} 影响度 ${meta.impact_score || '—'}/100 ${meta.change_kind || '—'} ${d.created_at} @@ -163,14 +271,11 @@ async function showTrack(id) {
${mdRender(d.report)}
数据源(智能体参考内容) - ${newsBlock('个股直接动态', '📄', src.direct)} - ${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)} - ${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)} - ${newsBlock('同业竞争', '🏢', src.peers)} + ${sourceHtml}
- 查看个股 → + ${isConcept ? '' : `查看个股 →`}
`); } catch (e) { toast('加载失败'); } diff --git a/static/js/tracking.js b/static/js/tracking.js index 4e9df11..eac6c74 100644 --- a/static/js/tracking.js +++ b/static/js/tracking.js @@ -68,9 +68,11 @@ function renderRecords(items) { 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 ` ${r.created_at} - ${r.stock_name}
${r.code}
+ ${isConcept ? '概念/主题' : '股票'} + ${r.stock_name}${isConcept ? '' : `
${r.code}
`} ${escapeHtml(r.industry)} ${meta.impact_score || '—'} ${meta.change_kind || '—'} @@ -109,10 +111,25 @@ async function showDetail(id) { return `
${icon} ${label} ${txt.includes('[') ? txt.split('[').length - 1 : 0}
${escapeHtml(txt || '暂无')}
`; }; + const isConcept = d.target_type === 'concept'; + let sourceHtml; + if (isConcept) { + const nc = meta.news_counts || {}; + sourceHtml = ` +
🔎 相关资讯(${nc.direct || 0} 条)
${escapeHtml(src.db_news || '')}
+
🧠 语义检索命中(${nc.rag || 0} 条)
${escapeHtml(src.rag_news || '')}
+
🏢 受益个股(${nc.related || 0} 只)
${escapeHtml(src.related || '')}
`; + } else { + sourceHtml = newsBlock('个股直接动态', '📄', src.direct) + + newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream) + + newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream) + + newsBlock('同业竞争', '🏢', src.peers) + + `
📈 技术面 / 机构动向
${escapeHtml(src.indicators || '')}
`; + } openModal(` -

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

+

${isConcept ? '🎯 概念主题跟踪' : '🧭 产业链跟踪'}:${d.stock_name}${d.code && !isConcept ? '(' + d.code + ')' : ''}

- ${escapeHtml(d.industry)} + ${isConcept ? '概念/主题' : d.industry} 影响度 ${meta.impact_score || '—'}/100 ${meta.change_kind || '—'} ${d.created_at} @@ -120,15 +137,11 @@ async function showDetail(id) {
${mdRender(d.report)}
数据源(智能体参考内容) - ${newsBlock('个股直接动态', '📄', src.direct)} - ${newsBlock('上游产业链(供给/成本)', '⬆️', src.upstream)} - ${newsBlock('下游产业链(需求/景气)', '⬇️', src.downstream)} - ${newsBlock('同业竞争', '🏢', src.peers)} -
📈 技术面 / 机构动向
${escapeHtml(src.indicators || '')}
+ ${sourceHtml}
- 查看个股 → + ${isConcept ? '' : `查看个股 →`}
`); } catch (e) { toast('加载失败'); } diff --git a/templates/automation.html b/templates/automation.html index b3e5218..8cd5c5e 100644 --- a/templates/automation.html +++ b/templates/automation.html @@ -60,16 +60,42 @@
- +
+ + +
+
💼 持仓(自选股)管理 —— 跟踪对象 = 持仓
+
+ + +
+
加载中…
+
+ + +
+
🎯 概念 / 主题 / 股票 跟踪目标
+
+ + + + +
+
加载中…
+
+
最近跟踪报告
- - + +
时间股票影响度性质摘要操作
暂无跟踪记录
时间类型目标影响度性质摘要操作
暂无跟踪记录
diff --git a/templates/tracking.html b/templates/tracking.html index 13597b0..d552162 100644 --- a/templates/tracking.html +++ b/templates/tracking.html @@ -5,13 +5,13 @@
- 🤖 大模型智能体 + 工作流:定期跟踪自选股(持仓),除个股公告新闻外, - 深度分析其产业链上下游与同业动态(上游供给/成本、下游需求/景气、竞争格局), - 输出专业跟踪报告;发现重大变化自动邮件通知。 -
跟踪对象 = 自选股(⭐ 股票池/详情页添加)· 自动跟踪间隔可在「系统设置」配置
+ 🤖 大模型智能体 + 工作流:定期跟踪持仓/自选股目标概念/主题/股票。 + 个股跟踪除公告新闻外,深度分析其产业链上下游与同业动态;概念跟踪覆盖题材相关资讯与受益个股。 + 输出专业报告,发现重大变化自动邮件通知。 +
跟踪对象管理在「⏰ 自动化 → 持仓跟踪」页 · 自动跟踪间隔可在「系统设置」配置
- +
@@ -27,8 +27,8 @@
全部跟踪记录
- - + +
时间股票行业影响度性质摘要操作
暂无跟踪记录,点击上方「立即跟踪全部持仓」开始
时间类型目标行业影响度性质摘要操作
暂无跟踪记录,点击上方「立即跟踪全部持仓」开始