server.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. """
  2. TrendRadar MCP Server - FastMCP 2.0 实现
  3. 使用 FastMCP 2.0 提供生产级 MCP 工具服务器。
  4. 支持 stdio 和 HTTP 两种传输模式。
  5. """
  6. import asyncio
  7. import json
  8. from typing import List, Optional, Dict, Union
  9. from fastmcp import FastMCP
  10. from .tools.data_query import DataQueryTools
  11. from .tools.analytics import AnalyticsTools
  12. from .tools.search_tools import SearchTools
  13. from .tools.config_mgmt import ConfigManagementTools
  14. from .tools.system import SystemManagementTools
  15. from .tools.storage_sync import StorageSyncTools
  16. from .utils.date_parser import DateParser
  17. from .utils.errors import MCPError
  18. # 创建 FastMCP 2.0 应用
  19. mcp = FastMCP('trendradar-news')
  20. # 全局工具实例(在第一次请求时初始化)
  21. _tools_instances = {}
  22. def _get_tools(project_root: Optional[str] = None):
  23. """获取或创建工具实例(单例模式)"""
  24. if not _tools_instances:
  25. _tools_instances['data'] = DataQueryTools(project_root)
  26. _tools_instances['analytics'] = AnalyticsTools(project_root)
  27. _tools_instances['search'] = SearchTools(project_root)
  28. _tools_instances['config'] = ConfigManagementTools(project_root)
  29. _tools_instances['system'] = SystemManagementTools(project_root)
  30. _tools_instances['storage'] = StorageSyncTools(project_root)
  31. return _tools_instances
  32. # ==================== MCP Resources ====================
  33. @mcp.resource("config://platforms")
  34. async def get_platforms_resource() -> str:
  35. """
  36. 获取支持的平台列表
  37. 返回 config.yaml 中配置的所有平台信息,包括 ID 和名称。
  38. """
  39. tools = _get_tools()
  40. config = await asyncio.to_thread(
  41. tools['config'].get_current_config, section="crawler"
  42. )
  43. return json.dumps({
  44. "platforms": config.get("platforms", []),
  45. "description": "TrendRadar 支持的热榜平台列表"
  46. }, ensure_ascii=False, indent=2)
  47. @mcp.resource("config://rss-feeds")
  48. async def get_rss_feeds_resource() -> str:
  49. """
  50. 获取 RSS 订阅源列表
  51. 返回当前配置的所有 RSS 源信息。
  52. """
  53. tools = _get_tools()
  54. status = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
  55. return json.dumps({
  56. "feeds": status.get("today_feeds", {}),
  57. "description": "TrendRadar 支持的 RSS 订阅源列表"
  58. }, ensure_ascii=False, indent=2)
  59. @mcp.resource("data://available-dates")
  60. async def get_available_dates_resource() -> str:
  61. """
  62. 获取可用的数据日期范围
  63. 返回本地存储中可查询的日期列表。
  64. """
  65. tools = _get_tools()
  66. result = await asyncio.to_thread(
  67. tools['storage'].list_available_dates, source="local"
  68. )
  69. return json.dumps({
  70. "dates": result.get("data", {}).get("local", {}).get("dates", []),
  71. "description": "本地存储中可查询的日期列表"
  72. }, ensure_ascii=False, indent=2)
  73. @mcp.resource("config://keywords")
  74. async def get_keywords_resource() -> str:
  75. """
  76. 获取关注词配置
  77. 返回 frequency_words.txt 中配置的关注词分组。
  78. """
  79. tools = _get_tools()
  80. config = await asyncio.to_thread(
  81. tools['config'].get_current_config, section="keywords"
  82. )
  83. return json.dumps({
  84. "word_groups": config.get("word_groups", []),
  85. "total_groups": config.get("total_groups", 0),
  86. "description": "TrendRadar 关注词配置"
  87. }, ensure_ascii=False, indent=2)
  88. # ==================== 日期解析工具(优先调用)====================
  89. @mcp.tool
  90. async def resolve_date_range(
  91. expression: str
  92. ) -> str:
  93. """
  94. 【推荐优先调用】将自然语言日期表达式解析为标准日期范围
  95. **为什么需要这个工具?**
  96. 用户经常使用"本周"、"最近7天"等自然语言表达日期,但 AI 模型自己计算日期
  97. 可能导致不一致的结果。此工具在服务器端使用精确的当前时间计算,确保所有
  98. AI 模型获得一致的日期范围。
  99. **推荐使用流程:**
  100. 1. 用户说"分析AI本周的情感倾向"
  101. 2. AI 调用 resolve_date_range("本周") → 获取精确日期范围
  102. 3. AI 调用 analyze_sentiment(topic="ai", date_range=上一步返回的date_range)
  103. Args:
  104. expression: 自然语言日期表达式,支持:
  105. - 单日: "今天", "昨天", "today", "yesterday"
  106. - 周: "本周", "上周", "this week", "last week"
  107. - 月: "本月", "上月", "this month", "last month"
  108. - 最近N天: "最近7天", "最近30天", "last 7 days", "last 30 days"
  109. - 动态: "最近5天", "last 10 days"(任意天数)
  110. Returns:
  111. JSON格式的日期范围,可直接用于其他工具的 date_range 参数:
  112. {
  113. "success": true,
  114. "expression": "本周",
  115. "date_range": {
  116. "start": "2025-11-18",
  117. "end": "2025-11-26"
  118. },
  119. "current_date": "2025-11-26",
  120. "description": "本周(周一到周日,11-18 至 11-26)"
  121. }
  122. Examples:
  123. 用户:"分析AI本周的情感倾向"
  124. AI调用步骤:
  125. 1. resolve_date_range("本周")
  126. → {"date_range": {"start": "2025-11-18", "end": "2025-11-26"}, ...}
  127. 2. analyze_sentiment(topic="ai", date_range={"start": "2025-11-18", "end": "2025-11-26"})
  128. 用户:"看看最近7天的特斯拉新闻"
  129. AI调用步骤:
  130. 1. resolve_date_range("最近7天")
  131. → {"date_range": {"start": "2025-11-20", "end": "2025-11-26"}, ...}
  132. 2. search_news(query="特斯拉", date_range={"start": "2025-11-20", "end": "2025-11-26"})
  133. """
  134. try:
  135. result = await asyncio.to_thread(DateParser.resolve_date_range_expression, expression)
  136. return json.dumps(result, ensure_ascii=False, indent=2)
  137. except MCPError as e:
  138. return json.dumps({
  139. "success": False,
  140. "error": e.to_dict()
  141. }, ensure_ascii=False, indent=2)
  142. except Exception as e:
  143. return json.dumps({
  144. "success": False,
  145. "error": {
  146. "code": "INTERNAL_ERROR",
  147. "message": str(e)
  148. }
  149. }, ensure_ascii=False, indent=2)
  150. # ==================== 数据查询工具 ====================
  151. @mcp.tool
  152. async def get_latest_news(
  153. platforms: Optional[List[str]] = None,
  154. limit: int = 50,
  155. include_url: bool = False
  156. ) -> str:
  157. """
  158. 获取最新一批爬取的新闻数据,快速了解当前热点
  159. Args:
  160. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  161. limit: 返回条数限制,默认50,最大1000
  162. include_url: 是否包含URL链接,默认False(节省token)
  163. Returns:
  164. JSON格式的新闻列表
  165. **数据展示建议**
  166. - 默认展示全部返回数据,除非用户明确要求总结
  167. - 用户说"总结"或"挑重点"时才进行筛选
  168. - 用户问"为什么只显示部分"说明需要完整数据
  169. """
  170. tools = _get_tools()
  171. result = await asyncio.to_thread(
  172. tools['data'].get_latest_news,
  173. platforms=platforms, limit=limit, include_url=include_url
  174. )
  175. return json.dumps(result, ensure_ascii=False, indent=2)
  176. @mcp.tool
  177. async def get_trending_topics(
  178. top_n: int = 10,
  179. mode: str = 'current',
  180. extract_mode: str = 'keywords'
  181. ) -> str:
  182. """
  183. 获取热点话题统计
  184. Args:
  185. top_n: 返回TOP N话题,默认10
  186. mode: 时间模式
  187. - "daily": 当日累计数据统计
  188. - "current": 最新一批数据统计(默认)
  189. extract_mode: 提取模式
  190. - "keywords": 统计预设关注词(基于 config/frequency_words.txt,默认)
  191. - "auto_extract": 自动从新闻标题提取高频词(无需预设,自动发现热点)
  192. Returns:
  193. JSON格式的话题频率统计列表
  194. Examples:
  195. - 使用预设关注词: get_trending_topics(mode="current")
  196. - 自动提取热点: get_trending_topics(extract_mode="auto_extract", top_n=20)
  197. """
  198. tools = _get_tools()
  199. result = await asyncio.to_thread(
  200. tools['data'].get_trending_topics,
  201. top_n=top_n, mode=mode, extract_mode=extract_mode
  202. )
  203. return json.dumps(result, ensure_ascii=False, indent=2)
  204. # ==================== RSS 数据查询工具 ====================
  205. @mcp.tool
  206. async def get_latest_rss(
  207. feeds: Optional[List[str]] = None,
  208. days: int = 1,
  209. limit: int = 50,
  210. include_summary: bool = False
  211. ) -> str:
  212. """
  213. 获取最新的 RSS 订阅数据(支持多日查询)
  214. RSS 数据与热榜新闻分开存储,按时间流展示,适合获取特定来源的最新内容。
  215. Args:
  216. feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr'],不指定则返回所有源
  217. days: 获取最近 N 天的数据,默认 1(仅今天),最大 30 天
  218. limit: 返回条数限制,默认50,最大500
  219. include_summary: 是否包含文章摘要,默认False(节省token)
  220. Returns:
  221. JSON格式的 RSS 条目列表
  222. Examples:
  223. - get_latest_rss()
  224. - get_latest_rss(days=7, feeds=['hacker-news'])
  225. """
  226. tools = _get_tools()
  227. result = await asyncio.to_thread(
  228. tools['data'].get_latest_rss,
  229. feeds=feeds, days=days, limit=limit, include_summary=include_summary
  230. )
  231. return json.dumps(result, ensure_ascii=False, indent=2)
  232. @mcp.tool
  233. async def search_rss(
  234. keyword: str,
  235. feeds: Optional[List[str]] = None,
  236. days: int = 7,
  237. limit: int = 50,
  238. include_summary: bool = False
  239. ) -> str:
  240. """
  241. 搜索 RSS 数据
  242. 在 RSS 订阅数据中搜索包含指定关键词的文章。
  243. Args:
  244. keyword: 搜索关键词(必需)
  245. feeds: RSS 源 ID 列表,如 ['hacker-news', '36kr']
  246. - 不指定时:搜索所有 RSS 源
  247. days: 搜索最近 N 天的数据,默认 7 天,最大 30 天
  248. limit: 返回条数限制,默认50
  249. include_summary: 是否包含文章摘要,默认False
  250. Returns:
  251. JSON格式的匹配 RSS 条目列表
  252. Examples:
  253. - search_rss(keyword="AI")
  254. - search_rss(keyword="machine learning", feeds=['hacker-news'], days=14)
  255. """
  256. tools = _get_tools()
  257. result = await asyncio.to_thread(
  258. tools['data'].search_rss,
  259. keyword=keyword,
  260. feeds=feeds,
  261. days=days,
  262. limit=limit,
  263. include_summary=include_summary
  264. )
  265. return json.dumps(result, ensure_ascii=False, indent=2)
  266. @mcp.tool
  267. async def get_rss_feeds_status() -> str:
  268. """
  269. 获取 RSS 源状态信息
  270. 查看当前配置的 RSS 源及其数据统计信息。
  271. Returns:
  272. JSON格式的 RSS 源状态,包含:
  273. - available_dates: 有 RSS 数据的日期列表
  274. - total_dates: 总日期数
  275. - today_feeds: 今日各 RSS 源的数据统计
  276. - {feed_id}: { name, item_count }
  277. - generated_at: 生成时间
  278. Examples:
  279. - get_rss_feeds_status() # 查看所有 RSS 源状态
  280. """
  281. tools = _get_tools()
  282. result = await asyncio.to_thread(tools['data'].get_rss_feeds_status)
  283. return json.dumps(result, ensure_ascii=False, indent=2)
  284. @mcp.tool
  285. async def get_news_by_date(
  286. date_range: Optional[Union[Dict[str, str], str]] = None,
  287. platforms: Optional[List[str]] = None,
  288. limit: int = 50,
  289. include_url: bool = False
  290. ) -> str:
  291. """
  292. 获取指定日期的新闻数据,用于历史数据分析和对比
  293. Args:
  294. date_range: 日期范围,支持多种格式:
  295. - 范围对象: {"start": "2025-01-01", "end": "2025-01-07"}
  296. - 自然语言: "今天", "昨天", "本周", "最近7天"
  297. - 单日字符串: "2025-01-15"
  298. - 默认值: "今天"
  299. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  300. limit: 返回条数限制,默认50,最大1000
  301. include_url: 是否包含URL链接,默认False(节省token)
  302. Returns:
  303. JSON格式的新闻列表,包含标题、平台、排名等信息
  304. """
  305. tools = _get_tools()
  306. result = await asyncio.to_thread(
  307. tools['data'].get_news_by_date,
  308. date_range=date_range,
  309. platforms=platforms,
  310. limit=limit,
  311. include_url=include_url
  312. )
  313. return json.dumps(result, ensure_ascii=False, indent=2)
  314. # ==================== 高级数据分析工具 ====================
  315. @mcp.tool
  316. async def analyze_topic_trend(
  317. topic: str,
  318. analysis_type: str = "trend",
  319. date_range: Optional[Union[Dict[str, str], str]] = None,
  320. granularity: str = "day",
  321. spike_threshold: float = 3.0,
  322. time_window: int = 24,
  323. lookahead_hours: int = 6,
  324. confidence_threshold: float = 0.7
  325. ) -> str:
  326. """
  327. 统一话题趋势分析工具 - 整合多种趋势分析模式
  328. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  329. Args:
  330. topic: 话题关键词(必需)
  331. analysis_type: 分析类型
  332. - "trend": 热度趋势分析(默认)
  333. - "lifecycle": 生命周期分析
  334. - "viral": 异常热度检测
  335. - "predict": 话题预测
  336. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认最近7天
  337. granularity: 时间粒度,默认"day"
  338. spike_threshold: 热度突增倍数阈值(viral模式),默认3.0
  339. time_window: 检测时间窗口小时数(viral模式),默认24
  340. lookahead_hours: 预测未来小时数(predict模式),默认6
  341. confidence_threshold: 置信度阈值(predict模式),默认0.7
  342. Returns:
  343. JSON格式的趋势分析结果
  344. Examples:
  345. - analyze_topic_trend(topic="AI", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  346. - analyze_topic_trend(topic="特斯拉", analysis_type="lifecycle")
  347. """
  348. tools = _get_tools()
  349. result = await asyncio.to_thread(
  350. tools['analytics'].analyze_topic_trend_unified,
  351. topic=topic,
  352. analysis_type=analysis_type,
  353. date_range=date_range,
  354. granularity=granularity,
  355. threshold=spike_threshold,
  356. time_window=time_window,
  357. lookahead_hours=lookahead_hours,
  358. confidence_threshold=confidence_threshold
  359. )
  360. return json.dumps(result, ensure_ascii=False, indent=2)
  361. @mcp.tool
  362. async def analyze_data_insights(
  363. insight_type: str = "platform_compare",
  364. topic: Optional[str] = None,
  365. date_range: Optional[Union[Dict[str, str], str]] = None,
  366. min_frequency: int = 3,
  367. top_n: int = 20
  368. ) -> str:
  369. """
  370. 统一数据洞察分析工具 - 整合多种数据分析模式
  371. Args:
  372. insight_type: 洞察类型,可选值:
  373. - "platform_compare": 平台对比分析(对比不同平台对话题的关注度)
  374. - "platform_activity": 平台活跃度统计(统计各平台发布频率和活跃时间)
  375. - "keyword_cooccur": 关键词共现分析(分析关键词同时出现的模式)
  376. topic: 话题关键词(可选,platform_compare模式适用)
  377. date_range: **【对象类型】** 日期范围(可选)
  378. - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
  379. - **示例**: {"start": "2025-01-01", "end": "2025-01-07"}
  380. - **重要**: 必须是对象格式,不能传递整数
  381. min_frequency: 最小共现频次(keyword_cooccur模式),默认3
  382. top_n: 返回TOP N结果(keyword_cooccur模式),默认20
  383. Returns:
  384. JSON格式的数据洞察分析结果
  385. Examples:
  386. - analyze_data_insights(insight_type="platform_compare", topic="人工智能")
  387. - analyze_data_insights(insight_type="platform_activity", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  388. - analyze_data_insights(insight_type="keyword_cooccur", min_frequency=5, top_n=15)
  389. """
  390. tools = _get_tools()
  391. result = await asyncio.to_thread(
  392. tools['analytics'].analyze_data_insights_unified,
  393. insight_type=insight_type,
  394. topic=topic,
  395. date_range=date_range,
  396. min_frequency=min_frequency,
  397. top_n=top_n
  398. )
  399. return json.dumps(result, ensure_ascii=False, indent=2)
  400. @mcp.tool
  401. async def analyze_sentiment(
  402. topic: Optional[str] = None,
  403. platforms: Optional[List[str]] = None,
  404. date_range: Optional[Union[Dict[str, str], str]] = None,
  405. limit: int = 50,
  406. sort_by_weight: bool = True,
  407. include_url: bool = False
  408. ) -> str:
  409. """
  410. 分析新闻的情感倾向和热度趋势
  411. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  412. Args:
  413. topic: 话题关键词(可选)
  414. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  415. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认今天
  416. limit: 返回新闻数量,默认50,最大100(会对标题去重)
  417. sort_by_weight: 是否按热度权重排序,默认True
  418. include_url: 是否包含URL链接,默认False(节省token)
  419. Returns:
  420. JSON格式的分析结果,包含情感分布、热度趋势和相关新闻
  421. Examples:
  422. - analyze_sentiment(topic="AI", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  423. """
  424. tools = _get_tools()
  425. result = await asyncio.to_thread(
  426. tools['analytics'].analyze_sentiment,
  427. topic=topic,
  428. platforms=platforms,
  429. date_range=date_range,
  430. limit=limit,
  431. sort_by_weight=sort_by_weight,
  432. include_url=include_url
  433. )
  434. return json.dumps(result, ensure_ascii=False, indent=2)
  435. @mcp.tool
  436. async def find_related_news(
  437. reference_title: str,
  438. date_range: Optional[Union[Dict[str, str], str]] = None,
  439. threshold: float = 0.5,
  440. limit: int = 50,
  441. include_url: bool = False
  442. ) -> str:
  443. """
  444. 查找与指定新闻标题相关的其他新闻(支持当天和历史数据)
  445. Args:
  446. reference_title: 参考新闻标题(完整或部分)
  447. date_range: 日期范围(可选)
  448. - 不指定: 只查询今天的数据
  449. - "today", "yesterday", "last_week", "last_month": 预设值
  450. - {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}: 自定义范围
  451. threshold: 相似度阈值,0-1之间,默认0.5(越高匹配越严格)
  452. limit: 返回条数限制,默认50
  453. include_url: 是否包含URL链接,默认False(节省token)
  454. Returns:
  455. JSON格式的相关新闻列表,按相似度排序
  456. Examples:
  457. - find_related_news(reference_title="特斯拉降价")
  458. - find_related_news(reference_title="AI突破", date_range="last_week")
  459. """
  460. tools = _get_tools()
  461. result = await asyncio.to_thread(
  462. tools['search'].find_related_news_unified,
  463. reference_title=reference_title,
  464. date_range=date_range,
  465. threshold=threshold,
  466. limit=limit,
  467. include_url=include_url
  468. )
  469. return json.dumps(result, ensure_ascii=False, indent=2)
  470. @mcp.tool
  471. async def generate_summary_report(
  472. report_type: str = "daily",
  473. date_range: Optional[Union[Dict[str, str], str]] = None
  474. ) -> str:
  475. """
  476. 每日/每周摘要生成器 - 自动生成热点摘要报告
  477. Args:
  478. report_type: 报告类型(daily/weekly)
  479. date_range: **【对象类型】** 自定义日期范围(可选)
  480. - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
  481. - **示例**: {"start": "2025-01-01", "end": "2025-01-07"}
  482. - **重要**: 必须是对象格式,不能传递整数
  483. Returns:
  484. JSON格式的摘要报告,包含Markdown格式内容
  485. """
  486. tools = _get_tools()
  487. result = await asyncio.to_thread(
  488. tools['analytics'].generate_summary_report,
  489. report_type=report_type,
  490. date_range=date_range
  491. )
  492. return json.dumps(result, ensure_ascii=False, indent=2)
  493. @mcp.tool
  494. async def aggregate_news(
  495. date_range: Optional[Union[Dict[str, str], str]] = None,
  496. platforms: Optional[List[str]] = None,
  497. similarity_threshold: float = 0.7,
  498. limit: int = 50,
  499. include_url: bool = False
  500. ) -> str:
  501. """
  502. 跨平台新闻聚合 - 对相似新闻进行去重合并
  503. 将不同平台报道的同一事件合并为一条聚合新闻,显示跨平台覆盖情况和综合热度。
  504. Args:
  505. date_range: 日期范围,不指定则查询今天
  506. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  507. similarity_threshold: 相似度阈值,0.3-1.0,默认0.7(越高越严格)
  508. limit: 返回聚合新闻数量,默认50
  509. include_url: 是否包含URL链接,默认False
  510. Returns:
  511. JSON格式的聚合结果,包含去重统计、聚合新闻列表和平台覆盖统计
  512. Examples:
  513. - aggregate_news()
  514. - aggregate_news(similarity_threshold=0.8)
  515. """
  516. tools = _get_tools()
  517. result = await asyncio.to_thread(
  518. tools['analytics'].aggregate_news,
  519. date_range=date_range,
  520. platforms=platforms,
  521. similarity_threshold=similarity_threshold,
  522. limit=limit,
  523. include_url=include_url
  524. )
  525. return json.dumps(result, ensure_ascii=False, indent=2)
  526. @mcp.tool
  527. async def compare_periods(
  528. period1: Union[Dict[str, str], str],
  529. period2: Union[Dict[str, str], str],
  530. topic: Optional[str] = None,
  531. compare_type: str = "overview",
  532. platforms: Optional[List[str]] = None,
  533. top_n: int = 10
  534. ) -> str:
  535. """
  536. 时期对比分析 - 比较两个时间段的新闻数据
  537. 对比不同时期的热点话题、平台活跃度、新闻数量等维度。
  538. **使用场景:**
  539. - 对比本周和上周的热点变化
  540. - 分析某个话题在两个时期的热度差异
  541. - 查看各平台活跃度的周期性变化
  542. Args:
  543. period1: 第一个时间段(基准期)
  544. - {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}: 日期范围
  545. - "today", "yesterday", "this_week", "last_week", "this_month", "last_month": 预设值
  546. period2: 第二个时间段(对比期,格式同 period1)
  547. topic: 可选的话题关键词(聚焦特定话题的对比)
  548. compare_type: 对比类型
  549. - "overview": 总体概览(默认)- 新闻数量、关键词变化、TOP新闻
  550. - "topic_shift": 话题变化分析 - 上升话题、下降话题、新出现话题
  551. - "platform_activity": 平台活跃度对比 - 各平台新闻数量变化
  552. platforms: 平台过滤列表,如 ['zhihu', 'weibo']
  553. top_n: 返回 TOP N 结果,默认10
  554. Returns:
  555. JSON格式的对比分析结果,包含:
  556. - periods: 两个时期的日期范围
  557. - compare_type: 对比类型
  558. - overview/topic_shift/platform_comparison: 具体对比结果(根据类型)
  559. Examples:
  560. - compare_periods(period1="last_week", period2="this_week") # 周环比
  561. - compare_periods(period1="last_month", period2="this_month", compare_type="topic_shift")
  562. - compare_periods(
  563. period1={"start": "2025-01-01", "end": "2025-01-07"},
  564. period2={"start": "2025-01-08", "end": "2025-01-14"},
  565. topic="人工智能"
  566. )
  567. """
  568. tools = _get_tools()
  569. result = await asyncio.to_thread(
  570. tools['analytics'].compare_periods,
  571. period1=period1,
  572. period2=period2,
  573. topic=topic,
  574. compare_type=compare_type,
  575. platforms=platforms,
  576. top_n=top_n
  577. )
  578. return json.dumps(result, ensure_ascii=False, indent=2)
  579. # ==================== 智能检索工具 ====================
  580. @mcp.tool
  581. async def search_news(
  582. query: str,
  583. search_mode: str = "keyword",
  584. date_range: Optional[Union[Dict[str, str], str]] = None,
  585. platforms: Optional[List[str]] = None,
  586. limit: int = 50,
  587. sort_by: str = "relevance",
  588. threshold: float = 0.6,
  589. include_url: bool = False,
  590. include_rss: bool = False,
  591. rss_limit: int = 20
  592. ) -> str:
  593. """
  594. 统一搜索接口,支持多种搜索模式,可同时搜索热榜和RSS
  595. 建议:使用自然语言日期时,先调用 resolve_date_range 获取精确日期范围。
  596. Args:
  597. query: 搜索关键词或内容片段
  598. search_mode: 搜索模式
  599. - "keyword": 精确关键词匹配(默认)
  600. - "fuzzy": 模糊内容匹配
  601. - "entity": 实体名称搜索(人物/地点/机构)
  602. date_range: 日期范围,格式 {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"},默认今天
  603. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  604. limit: 热榜返回条数限制,默认50
  605. sort_by: 排序方式 - "relevance"(相关度)/ "weight"(权重)/ "date"(日期)
  606. threshold: 相似度阈值(仅fuzzy模式),0-1,默认0.6
  607. include_url: 是否包含URL链接,默认False
  608. include_rss: 是否同时搜索RSS数据,默认False
  609. rss_limit: RSS返回条数限制,默认20
  610. Returns:
  611. JSON格式的搜索结果,包含热榜新闻列表和可选的RSS结果
  612. Examples:
  613. - search_news(query="AI")
  614. - search_news(query="AI", include_rss=True)
  615. - search_news(query="特斯拉", date_range={"start": "2025-01-01", "end": "2025-01-07"})
  616. """
  617. tools = _get_tools()
  618. result = await asyncio.to_thread(
  619. tools['search'].search_news_unified,
  620. query=query,
  621. search_mode=search_mode,
  622. date_range=date_range,
  623. platforms=platforms,
  624. limit=limit,
  625. sort_by=sort_by,
  626. threshold=threshold,
  627. include_url=include_url,
  628. include_rss=include_rss,
  629. rss_limit=rss_limit
  630. )
  631. return json.dumps(result, ensure_ascii=False, indent=2)
  632. # ==================== 配置与系统管理工具 ====================
  633. @mcp.tool
  634. async def get_current_config(
  635. section: str = "all"
  636. ) -> str:
  637. """
  638. 获取当前系统配置
  639. Args:
  640. section: 配置节,可选值:
  641. - "all": 所有配置(默认)
  642. - "crawler": 爬虫配置
  643. - "push": 推送配置
  644. - "keywords": 关键词配置
  645. - "weights": 权重配置
  646. Returns:
  647. JSON格式的配置信息
  648. """
  649. tools = _get_tools()
  650. result = await asyncio.to_thread(tools['config'].get_current_config, section=section)
  651. return json.dumps(result, ensure_ascii=False, indent=2)
  652. @mcp.tool
  653. async def get_system_status() -> str:
  654. """
  655. 获取系统运行状态和健康检查信息
  656. 返回系统版本、数据统计、缓存状态等信息
  657. Returns:
  658. JSON格式的系统状态信息
  659. """
  660. tools = _get_tools()
  661. result = await asyncio.to_thread(tools['system'].get_system_status)
  662. return json.dumps(result, ensure_ascii=False, indent=2)
  663. @mcp.tool
  664. async def check_version(
  665. proxy_url: Optional[str] = None
  666. ) -> str:
  667. """
  668. 检查版本更新(同时检查 TrendRadar 和 MCP Server)
  669. 比较本地版本与 GitHub 远程版本,判断是否需要更新。
  670. Args:
  671. proxy_url: 可选的代理URL,用于访问 GitHub(如 http://127.0.0.1:7890)
  672. Returns:
  673. JSON格式的版本检查结果,包含两个组件的版本对比和是否需要更新
  674. Examples:
  675. - check_version()
  676. - check_version(proxy_url="http://127.0.0.1:7890")
  677. """
  678. tools = _get_tools()
  679. result = await asyncio.to_thread(tools['system'].check_version, proxy_url=proxy_url)
  680. return json.dumps(result, ensure_ascii=False, indent=2)
  681. @mcp.tool
  682. async def trigger_crawl(
  683. platforms: Optional[List[str]] = None,
  684. save_to_local: bool = False,
  685. include_url: bool = False
  686. ) -> str:
  687. """
  688. 手动触发一次爬取任务(可选持久化)
  689. Args:
  690. platforms: 平台ID列表,如 ['zhihu', 'weibo'],不指定则使用所有平台
  691. save_to_local: 是否保存到本地 output 目录,默认 False
  692. include_url: 是否包含URL链接,默认False(节省token)
  693. Returns:
  694. JSON格式的任务状态信息,包含成功/失败平台列表和新闻数据
  695. Examples:
  696. - trigger_crawl(platforms=['zhihu'])
  697. - trigger_crawl(save_to_local=True)
  698. """
  699. tools = _get_tools()
  700. result = await asyncio.to_thread(
  701. tools['system'].trigger_crawl,
  702. platforms=platforms, save_to_local=save_to_local, include_url=include_url
  703. )
  704. return json.dumps(result, ensure_ascii=False, indent=2)
  705. # ==================== 存储同步工具 ====================
  706. @mcp.tool
  707. async def sync_from_remote(
  708. days: int = 7
  709. ) -> str:
  710. """
  711. 从远程存储拉取数据到本地
  712. 用于 MCP Server 等场景:爬虫存到远程云存储(如 Cloudflare R2),
  713. MCP Server 拉取到本地进行分析查询。
  714. Args:
  715. days: 拉取最近 N 天的数据,默认 7 天
  716. - 0: 不拉取
  717. - 7: 拉取最近一周的数据
  718. - 30: 拉取最近一个月的数据
  719. Returns:
  720. JSON格式的同步结果,包含:
  721. - success: 是否成功
  722. - synced_files: 成功同步的文件数量
  723. - synced_dates: 成功同步的日期列表
  724. - skipped_dates: 跳过的日期(本地已存在)
  725. - failed_dates: 失败的日期及错误信息
  726. - message: 操作结果描述
  727. Examples:
  728. - sync_from_remote() # 拉取最近7天
  729. - sync_from_remote(days=30) # 拉取最近30天
  730. Note:
  731. 需要在 config/config.yaml 中配置远程存储(storage.remote)或设置环境变量:
  732. - S3_ENDPOINT_URL: 服务端点
  733. - S3_BUCKET_NAME: 存储桶名称
  734. - S3_ACCESS_KEY_ID: 访问密钥 ID
  735. - S3_SECRET_ACCESS_KEY: 访问密钥
  736. """
  737. tools = _get_tools()
  738. result = await asyncio.to_thread(tools['storage'].sync_from_remote, days=days)
  739. return json.dumps(result, ensure_ascii=False, indent=2)
  740. @mcp.tool
  741. async def get_storage_status() -> str:
  742. """
  743. 获取存储配置和状态
  744. 查看当前存储后端配置、本地和远程存储的状态信息。
  745. Returns:
  746. JSON格式的存储状态信息,包含本地/远程存储状态和拉取配置
  747. """
  748. tools = _get_tools()
  749. result = await asyncio.to_thread(tools['storage'].get_storage_status)
  750. return json.dumps(result, ensure_ascii=False, indent=2)
  751. @mcp.tool
  752. async def list_available_dates(
  753. source: str = "both"
  754. ) -> str:
  755. """
  756. 列出本地/远程可用的日期范围
  757. 查看本地和远程存储中有哪些日期的数据可用。
  758. Args:
  759. source: 数据来源
  760. - "local": 仅本地
  761. - "remote": 仅远程
  762. - "both": 同时列出并对比(默认)
  763. Returns:
  764. JSON格式的日期列表,包含各来源的日期信息和对比结果
  765. Examples:
  766. - list_available_dates()
  767. - list_available_dates(source="local")
  768. """
  769. tools = _get_tools()
  770. result = await asyncio.to_thread(tools['storage'].list_available_dates, source=source)
  771. return json.dumps(result, ensure_ascii=False, indent=2)
  772. # ==================== 启动入口 ====================
  773. def run_server(
  774. project_root: Optional[str] = None,
  775. transport: str = 'stdio',
  776. host: str = '0.0.0.0',
  777. port: int = 3333
  778. ):
  779. """
  780. 启动 MCP 服务器
  781. Args:
  782. project_root: 项目根目录路径
  783. transport: 传输模式,'stdio' 或 'http'
  784. host: HTTP模式的监听地址,默认 0.0.0.0
  785. port: HTTP模式的监听端口,默认 3333
  786. """
  787. # 初始化工具实例
  788. _get_tools(project_root)
  789. # 打印启动信息
  790. print()
  791. print("=" * 60)
  792. print(" TrendRadar MCP Server - FastMCP 2.0")
  793. print("=" * 60)
  794. print(f" 传输模式: {transport.upper()}")
  795. if transport == 'stdio':
  796. print(" 协议: MCP over stdio (标准输入输出)")
  797. print(" 说明: 通过标准输入输出与 MCP 客户端通信")
  798. elif transport == 'http':
  799. print(f" 协议: MCP over HTTP (生产环境)")
  800. print(f" 服务器监听: {host}:{port}")
  801. if project_root:
  802. print(f" 项目目录: {project_root}")
  803. else:
  804. print(" 项目目录: 当前目录")
  805. print()
  806. print(" 已注册的工具:")
  807. print(" === 日期解析工具(推荐优先调用)===")
  808. print(" 0. resolve_date_range - 解析自然语言日期为标准格式")
  809. print()
  810. print(" === 基础数据查询(P0核心)===")
  811. print(" 1. get_latest_news - 获取最新新闻")
  812. print(" 2. get_news_by_date - 按日期查询新闻(支持自然语言)")
  813. print(" 3. get_trending_topics - 获取趋势话题(支持自动提取)")
  814. print()
  815. print(" === RSS 数据查询 ===")
  816. print(" 4. get_latest_rss - 获取最新 RSS 订阅数据")
  817. print(" 5. search_rss - 搜索 RSS 数据")
  818. print(" 6. get_rss_feeds_status - 获取 RSS 源状态")
  819. print()
  820. print(" === 智能检索工具 ===")
  821. print(" 7. search_news - 统一新闻搜索(关键词/模糊/实体)")
  822. print(" 8. find_related_news - 相关新闻查找(支持历史数据)")
  823. print()
  824. print(" === 高级数据分析 ===")
  825. print(" 9. analyze_topic_trend - 统一话题趋势分析(热度/生命周期/爆火/预测)")
  826. print(" 10. analyze_data_insights - 统一数据洞察分析(平台对比/活跃度/关键词共现)")
  827. print(" 11. analyze_sentiment - 情感倾向分析")
  828. print(" 12. aggregate_news - 跨平台新闻聚合去重")
  829. print(" 13. compare_periods - 时期对比分析(周环比/月环比)")
  830. print(" 14. generate_summary_report - 每日/每周摘要生成")
  831. print()
  832. print(" === 配置与系统管理 ===")
  833. print(" 15. get_current_config - 获取当前系统配置")
  834. print(" 16. get_system_status - 获取系统运行状态")
  835. print(" 17. check_version - 检查版本更新(对比本地与远程版本)")
  836. print(" 18. trigger_crawl - 手动触发爬取任务")
  837. print()
  838. print(" === 存储同步工具 ===")
  839. print(" 19. sync_from_remote - 从远程存储拉取数据到本地")
  840. print(" 20. get_storage_status - 获取存储配置和状态")
  841. print(" 21. list_available_dates - 列出本地/远程可用日期")
  842. print("=" * 60)
  843. print()
  844. # 根据传输模式运行服务器
  845. if transport == 'stdio':
  846. mcp.run(transport='stdio')
  847. elif transport == 'http':
  848. # HTTP 模式(生产推荐)
  849. mcp.run(
  850. transport='http',
  851. host=host,
  852. port=port,
  853. path='/mcp' # HTTP 端点路径
  854. )
  855. else:
  856. raise ValueError(f"不支持的传输模式: {transport}")
  857. if __name__ == '__main__':
  858. import argparse
  859. parser = argparse.ArgumentParser(
  860. description='TrendRadar MCP Server - 新闻热点聚合 MCP 工具服务器',
  861. formatter_class=argparse.RawDescriptionHelpFormatter,
  862. epilog="""
  863. 详细配置教程请查看: README-Cherry-Studio.md
  864. """
  865. )
  866. parser.add_argument(
  867. '--transport',
  868. choices=['stdio', 'http'],
  869. default='stdio',
  870. help='传输模式:stdio (默认) 或 http (生产环境)'
  871. )
  872. parser.add_argument(
  873. '--host',
  874. default='0.0.0.0',
  875. help='HTTP模式的监听地址,默认 0.0.0.0'
  876. )
  877. parser.add_argument(
  878. '--port',
  879. type=int,
  880. default=3333,
  881. help='HTTP模式的监听端口,默认 3333'
  882. )
  883. parser.add_argument(
  884. '--project-root',
  885. help='项目根目录路径'
  886. )
  887. args = parser.parse_args()
  888. run_server(
  889. project_root=args.project_root,
  890. transport=args.transport,
  891. host=args.host,
  892. port=args.port
  893. )