项目文件夹

文件
2026-07-13 12:36:27 +08:00

191 行
6.9 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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