Files
2026-07-13 12:36:27 +08:00

191 lines
6.9 KiB
Python
Raw Permalink 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.
from __future__ import annotations
import json
import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
from datetime import datetime, timezone
from web.services.x_brief_data import latest_json_path, x_cache_dir, x_input_dir
THEME_RULES: list[tuple[str, tuple[str, ...]]] = [
("模型能力", ("gpt", "model", "claude", "openai", "agi", "llm")),
("算力与芯片", ("gpu", "tpu", "nvidia", "inference", "compute", "chip")),
("投融资与并购", ("fund", "vc", "acquire", "merger", "valuation", "ipo")),
("交易与市场", ("market", "stock", "trading", "etf", "crypto", "price")),
]
def _pick_theme(text: str) -> str:
t = text.lower()
for name, keys in THEME_RULES:
if any(k in t for k in keys):
return name
return "其他"
def _safe_excerpt(text: str, max_len: int = 200) -> str:
clean = re.sub(r"\s+", " ", text).strip()
if len(clean) <= max_len:
return clean
return clean[: max_len - 1] + "…"
def build_tweet_summary_markdown(payload: dict[str, Any]) -> str:
lines: list[str] = []
period = payload.get("period", {})
overview = payload.get("overview", {})
tweets = payload.get("tweets", [])
accounts = payload.get("accounts", [])
lines.append("# X平台关注列表推文汇总")
lines.append("")
lines.append(f"**生成日期**: {payload.get('generated_at', '')}")
lines.append("**数据来源**: Nitter RSS 订阅")
lines.append(f"**时间范围**: 最近{period.get('days', 30)}天")
lines.append(
f"**账号总数**: {overview.get('total_accounts', 0)} | **成功获取**: {overview.get('active_accounts', 0)} | **推文总数**: {overview.get('total_tweets', 0)}"
)
lines.append("")
lines.append("---")
lines.append("")
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
for tw in tweets:
grouped[tw.get("handle", "")].append(tw)
for acc in accounts:
handle = acc.get("handle", "")
name = acc.get("name", "")
status = acc.get("status")
tlist = grouped.get(handle, [])
if status != "ok":
lines.append(f"### ⚠️ @{handle}{name}(获取失败)")
lines.append("")
lines.append(f"- 错误: {acc.get('error', 'unknown')}")
lines.append("")
continue
lines.append(f"### ✅ @{handle}{name}{len(tlist)} 条)")
lines.append("")
for tw in tlist[:20]:
lines.append(f"**[{tw.get('published_at', '')}]**")
lines.append(f"> {_safe_excerpt(tw.get('title', ''))}")
lines.append(f"[原文链接]({tw.get('url', '')})")
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines).strip() + "\n"
def build_monthly_analysis_markdown(payload: dict[str, Any]) -> str:
lines: list[str] = []
overview = payload.get("overview", {})
tweets: list[dict[str, Any]] = payload.get("tweets", [])
lines.append("# X平台资讯深度分析报告")
lines.append("")
lines.append(f"**报告日期**: {payload.get('generated_at', '')}")
lines.append(
f"**分析账号**: {overview.get('total_accounts', 0)}个 | **活跃账号**: {overview.get('active_accounts', 0)}个 | **推文总量**: {overview.get('total_tweets', 0)}条"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 一、总体概览")
lines.append("")
lines.append("- 本报告基于 X 关注列表近30天 RSS 数据自动生成。")
lines.append("- 默认口径:按账号聚合、按推文主题词粗分层。")
lines.append("")
theme_counter: Counter[str] = Counter()
for tw in tweets:
theme_counter[_pick_theme(tw.get("title", ""))] += 1
lines.append("## 二、主题分布")
lines.append("")
for theme, cnt in theme_counter.most_common():
lines.append(f"- **{theme}**{cnt} 条")
lines.append("")
top_accounts: Counter[str] = Counter([tw.get("handle", "") for tw in tweets])
lines.append("## 三、活跃账号 Top10")
lines.append("")
for handle, cnt in top_accounts.most_common(10):
lines.append(f"- @{handle}: {cnt} 条")
lines.append("")
lines.append("## 四、代表性推文")
lines.append("")
for tw in tweets[:20]:
lines.append(
f"- @{tw.get('handle', '')} | {tw.get('published_at', '')}\n - {_safe_excerpt(tw.get('title', ''), 180)}\n - {tw.get('url', '')}"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("*本报告由系统自动生成,供月度复盘使用。*")
return "\n".join(lines).strip() + "\n"
def build_latest_view(
payload: dict[str, Any],
tweet_md: str,
analysis_md: str,
modules: dict[str, Any] | None = None,
) -> dict[str, Any]:
tweets: list[dict[str, Any]] = payload.get("tweets", [])
theme_counter: Counter[str] = Counter(_pick_theme(t.get("title", "")) for t in tweets)
top_themes = [{"title": k, "count": v} for k, v in theme_counter.most_common(6)]
mods = modules or {}
headline = str(mods.get("headline") or "")
trending_keywords = [str(k) for k in (mods.get("trending_keywords") or []) if k]
return {
**payload,
"headline": headline,
"trending_keywords": trending_keywords,
"top_themes": top_themes,
"modules": mods,
"tweet_summary_md": tweet_md,
"monthly_analysis_md": analysis_md,
}
def persist_outputs(
payload: dict[str, Any],
tweet_md: str,
analysis_md: str,
modules: dict[str, Any] | None = None,
) -> dict[str, Any]:
month_tag = payload.get("period", {}).get("month_tag", "unknown")
out_dir = x_input_dir()
out_dir.mkdir(parents=True, exist_ok=True)
tweet_md_path = out_dir / f"推文汇总_{month_tag}.md"
analysis_md_path = out_dir / f"月度深度分析_{month_tag}.md"
tweet_md_path.write_text(tweet_md, encoding="utf-8")
analysis_md_path.write_text(analysis_md, encoding="utf-8")
latest = build_latest_view(payload, tweet_md, analysis_md, modules=modules)
latest["paths"] = {
"tweet_summary_md": str(tweet_md_path),
"monthly_analysis_md": str(analysis_md_path),
}
cache_dir = x_cache_dir()
cache_dir.mkdir(parents=True, exist_ok=True)
latest_json_path().write_text(json.dumps(latest, ensure_ascii=False, indent=2), encoding="utf-8")
history_dir = cache_dir / "history"
history_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
(history_dir / f"{ts}.json").write_text(json.dumps(latest, ensure_ascii=False, indent=2), encoding="utf-8")
return latest
def load_latest_view() -> dict[str, Any] | None:
p = latest_json_path()
if not p.exists():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None