diff --git a/README-Cherry-Studio.md b/README-Cherry-Studio.md index bd597113..d2727168 100644 --- a/README-Cherry-Studio.md +++ b/README-Cherry-Studio.md @@ -109,6 +109,16 @@ cd 你的项目名 帮我爬取最新的新闻 ``` +或者尝试其他测试命令: + +``` +搜索最近3天关于"人工智能"的新闻 +查找2025年1月的"特斯拉"相关报道 +分析"iPhone"的热度趋势 +``` + +**提示**:当你说"最近3天"时,AI会自动计算日期范围并搜索。 + ### 2. 成功标志 如果配置成功,AI 会: diff --git a/README-MCP-FAQ.md b/README-MCP-FAQ.md index 6e30dbcb..b1005925 100644 --- a/README-MCP-FAQ.md +++ b/README-MCP-FAQ.md @@ -9,12 +9,14 @@ | 默认设置 | 说明 | 如何调整 | | -------------- | --------------------------------------- | ------------------------------------- | | **限制条数** | 默认返回 50 条新闻 | 对话中说"返回前 10 条"或"给我 100 条" | -| **时间范围** | 默认查询今天的数据 | 说"查询昨天"或"最近一周" | +| **时间范围** | 默认查询今天的数据 | 说"查询昨天"、"最近一周"或"1月1日到7日" | | **URL 链接** | 默认不返回链接(节省约 160 tokens/条) | 说"需要链接"或"包含 URL" | | **关键词列表** | 默认不使用 frequency_words.txt 过滤新闻 | 只有调用"趋势话题"工具时才使用 | **⚠️ 重要:** AI 模型的选择直接影响工具调用效果,AI 越智能,调用越准确。当你解除上面的限制,比如从今天的查询,放宽到一周的查询,首先你要在本地有一周的数据,其次,token 消耗量可能会倍增(为什么说可能,比如我查询 分析'苹果'最近一周的热度趋势,如果一周中没多少苹果的新闻,那么 token消耗量可能反而很少) +**💡 提示:** 当你说"最近7天"时,AI会自动计算对应的日期范围(如 2025-10-18 至 2025-10-25)并传递给工具。 + ## 💰 AI 模型 @@ -148,6 +150,8 @@ - "搜索包含'人工智能'的新闻" - "查找关于'特斯拉降价'的报道" - "搜索马斯克相关的新闻,返回前 20 条" +- "查找最近7天关于'iPhone 16'的新闻" +- "查找2025年1月1日到7日'特斯拉'的相关新闻" - "查找'iPhone 16 发布'这条新闻的链接" **调用的工具:** `search_news` @@ -155,7 +159,8 @@ **工具返回行为:** - 使用关键词模式搜索 -- 搜索今天的数据 +- 默认搜索今天的数据 +- AI会自动将"最近7天"、"上周"等相对时间转换为具体日期范围 - MCP 工具会返回最多 50 条结果给 AI - 不包含 URL 链接 @@ -166,11 +171,23 @@ **可以调整:** -- 指定时间范围:如"搜索最近一周的" +- 指定时间范围: + - 相对方式:"搜索最近一周的"(AI 自动计算日期) + - 绝对日期:"搜索2025年1月1日到7日的" - 指定平台:如"只搜索知乎" - 调整排序:如"按权重排序" - 包含链接:如"需要链接" +**示例对话:** + +``` +用户:搜索最近7天关于"人工智能突破"的新闻 +AI:(自动计算:date_range={"start": "2025-10-18", "end": "2025-10-25"}) + +用户:查找2025年1月的"特斯拉"报道 +AI:(date_range={"start": "2025-01-01", "end": "2025-01-31"}) +``` + --- ### Q5: 如何查找历史相关新闻? @@ -208,13 +225,15 @@ - "看看'特斯拉'话题是昙花一现还是持续热点" - "检测今天有哪些突然爆火的话题" - "预测接下来可能的热点话题" +- "分析'比特币'在2024年12月的生命周期" **调用的工具:** `analyze_topic_trend` **工具返回行为:** -- 热度趋势模式 -- 分析最近 7 天数据 +- 支持多种分析模式:热度趋势、生命周期、异常检测、预测 +- AI会自动将"最近一周"等相对时间转换为具体日期范围 +- 默认分析最近7天数据 - 按天粒度统计 **AI 展示行为:** @@ -222,6 +241,16 @@ - 通常会展示趋势分析结果和图表 - AI 可能会总结关键发现 +**示例对话:** + +``` +用户:分析'人工智能'最近一周的生命周期 +AI:(自动计算:date_range={"start": "2025-10-18", "end": "2025-10-25"}) + +用户:看看'比特币'在2024年12月是昙花一现还是持续热点 +AI:(date_range={"start": "2024-12-01", "end": "2024-12-31"}) +``` + --- ## 数据洞察 diff --git a/_image/ai.png b/_image/ai.png new file mode 100644 index 00000000..f0c0c3a9 Binary files /dev/null and b/_image/ai.png differ diff --git a/mcp_server/server.py b/mcp_server/server.py index 6c150974..be3ce1d3 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -155,11 +155,10 @@ async def get_news_by_date( async def analyze_topic_trend( topic: str, analysis_type: str = "trend", - time_range: str = "7d", + date_range: Optional[Dict[str, str]] = None, granularity: str = "day", threshold: float = 3.0, time_window: int = 24, - lookback_days: int = 7, lookahead_hours: int = 6, confidence_threshold: float = 0.7 ) -> str: @@ -173,20 +172,27 @@ async def analyze_topic_trend( - "lifecycle": 生命周期分析(从出现到消失的完整周期) - "viral": 异常热度检测(识别突然爆火的话题) - "predict": 话题预测(预测未来可能的热点) - time_range: 时间范围(trend模式),默认"7d"(7d/24h/1w/1m/2m) + date_range: 日期范围(trend和lifecycle模式),可选 + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-10-18", "end": "2025-10-25"} + - **说明**: AI需要根据用户的自然语言(如"最近7天")自动计算日期范围 + - **默认**: 不指定时默认分析最近7天 granularity: 时间粒度(trend模式),默认"day"(仅支持 day,因为底层数据按天聚合) threshold: 热度突增倍数阈值(viral模式),默认3.0 time_window: 检测时间窗口小时数(viral模式),默认24 - lookback_days: 回溯天数(lifecycle模式),默认7 lookahead_hours: 预测未来小时数(predict模式),默认6 confidence_threshold: 置信度阈值(predict模式),默认0.7 Returns: JSON格式的趋势分析结果 + **AI使用说明:** + 当用户使用相对时间表达时(如"最近7天"、"过去一周"、"上个月"), + AI需要自动计算对应的日期范围并传递给 date_range 参数。 + Examples: - - analyze_topic_trend(topic="人工智能", analysis_type="trend", time_range="7d") - - analyze_topic_trend(topic="特斯拉", analysis_type="lifecycle", lookback_days=7) + - analyze_topic_trend(topic="人工智能", analysis_type="trend", date_range={"start": "2025-10-18", "end": "2025-10-25"}) + - analyze_topic_trend(topic="特斯拉", analysis_type="lifecycle", date_range={"start": "2025-10-18", "end": "2025-10-25"}) - analyze_topic_trend(topic="比特币", analysis_type="viral", threshold=3.0) - analyze_topic_trend(topic="ChatGPT", analysis_type="predict", lookahead_hours=6) """ @@ -194,11 +200,10 @@ async def analyze_topic_trend( result = tools['analytics'].analyze_topic_trend_unified( topic=topic, analysis_type=analysis_type, - time_range=time_range, + date_range=date_range, granularity=granularity, threshold=threshold, time_window=time_window, - lookback_days=lookback_days, lookahead_hours=lookahead_hours, confidence_threshold=confidence_threshold ) @@ -222,7 +227,10 @@ async def analyze_data_insights( - "platform_activity": 平台活跃度统计(统计各平台发布频率和活跃时间) - "keyword_cooccur": 关键词共现分析(分析关键词同时出现的模式) topic: 话题关键词(可选,platform_compare模式适用) - date_range: 日期范围,格式: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + date_range: **【对象类型】** 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-01-01", "end": "2025-01-07"} + - **重要**: 必须是对象格式,不能传递整数 min_frequency: 最小共现频次(keyword_cooccur模式),默认3 top_n: 返回TOP N结果(keyword_cooccur模式),默认20 @@ -231,7 +239,7 @@ async def analyze_data_insights( Examples: - analyze_data_insights(insight_type="platform_compare", topic="人工智能") - - analyze_data_insights(insight_type="platform_activity", date_range={...}) + - analyze_data_insights(insight_type="platform_activity", date_range={"start": "2025-01-01", "end": "2025-01-07"}) - analyze_data_insights(insight_type="keyword_cooccur", min_frequency=5, top_n=15) """ tools = _get_tools() @@ -258,12 +266,15 @@ async def analyze_sentiment( 分析新闻的情感倾向和热度趋势 Args: - keywords: 关键词列表,如 ["AI", "人工智能"] - date_range: 日期范围(天数),如 7 表示最近7天,默认3天 + topic: 话题关键词(可选) platforms: 平台ID列表,如 ['zhihu', 'weibo', 'douyin'] - 不指定时:使用 config.yaml 中配置的所有平台 - 支持的平台来自 config/config.yaml 的 platforms 配置 - 每个平台都有对应的name字段(如"知乎"、"微博"),方便AI识别 + date_range: **【对象类型】** 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-01-01", "end": "2025-01-07"} + - **重要**: 必须是对象格式,不能传递整数 limit: 返回新闻数量,默认50,最大100 注意:本工具会对新闻标题进行去重(同一标题在不同平台只保留一次), 因此实际返回数量可能少于请求的 limit 值 @@ -301,7 +312,7 @@ async def find_similar_news( 查找与指定新闻标题相似的其他新闻 Args: - title: 新闻标题(完整或部分) + reference_title: 新闻标题(完整或部分) threshold: 相似度阈值,0-1之间,默认0.6 注意:阈值越高匹配越严格,返回结果越少 limit: 返回条数限制,默认50,最大100 @@ -336,7 +347,10 @@ async def generate_summary_report( Args: report_type: 报告类型(daily/weekly) - date_range: 自定义日期范围(可选) + date_range: **【对象类型】** 自定义日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-01-01", "end": "2025-01-07"} + - **重要**: 必须是对象格式,不能传递整数 Returns: JSON格式的摘要报告,包含Markdown格式内容 @@ -371,15 +385,24 @@ async def search_news( - "keyword": 精确关键词匹配(默认,适合搜索特定话题) - "fuzzy": 模糊内容匹配(适合搜索内容片段,会过滤相似度低于阈值的结果) - "entity": 实体名称搜索(适合搜索人物/地点/机构) - threshold: 相似度阈值(仅fuzzy模式有效),0-1之间,默认0.6 - 注意:阈值越高匹配越严格,返回结果越少 + date_range: 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-01-01", "end": "2025-01-07"} + - **说明**: AI需要根据用户的自然语言(如"最近7天")自动计算日期范围 + - **默认**: 不指定时默认查询今天的新闻 + - **注意**: start和end可以相同(表示单日查询) platforms: 平台ID列表,如 ['zhihu', 'weibo', 'douyin'] - 不指定时:使用 config.yaml 中配置的所有平台 - 支持的平台来自 config/config.yaml 的 platforms 配置 - 每个平台都有对应的name字段(如"知乎"、"微博"),方便AI识别 - lookback_days: 回溯天数,默认7天,最大30天 limit: 返回条数限制,默认50,最大1000 注意:实际返回数量取决于搜索匹配结果(特别是 fuzzy 模式下会过滤低相似度结果) + sort_by: 排序方式,可选值: + - "relevance": 按相关度排序(默认) + - "weight": 按新闻权重排序 + - "date": 按日期排序 + threshold: 相似度阈值(仅fuzzy模式有效),0-1之间,默认0.6 + 注意:阈值越高匹配越严格,返回结果越少 include_url: 是否包含URL链接,默认False(节省token) Returns: @@ -389,6 +412,19 @@ async def search_news( - 本工具返回完整的搜索结果列表 - **默认展示方式**:展示全部返回的新闻,无需总结或筛选 - 仅在用户明确要求"总结"或"挑重点"时才进行筛选 + + **AI使用说明:** + 当用户使用相对时间表达时(如"最近7天"、"过去一周"、"最近半个月"), + AI需要自动计算对应的日期范围。计算规则: + - "最近7天" → {"start": "今天-6天", "end": "今天"} + - "过去一周" → {"start": "今天-6天", "end": "今天"} + - "最近30天" → {"start": "今天-29天", "end": "今天"} + + Examples: + - 今天的新闻: search_news(query="人工智能") + - 最近7天: search_news(query="人工智能", date_range={"start": "2025-10-18", "end": "2025-10-25"}) + - 精确日期: search_news(query="人工智能", date_range={"start": "2025-01-01", "end": "2025-01-07"}) + - 模糊搜索: search_news(query="特斯拉降价", search_mode="fuzzy", threshold=0.4) """ tools = _get_tools() result = tools['search'].search_news_unified( @@ -407,7 +443,7 @@ async def search_news( @mcp.tool async def search_related_news_history( reference_text: str, - time_range: str = "yesterday", + time_preset: str = "yesterday", threshold: float = 0.4, limit: int = 50, include_url: bool = False @@ -416,12 +452,15 @@ async def search_related_news_history( 基于种子新闻,在历史数据中搜索相关新闻 Args: - seed_news_title: 种子新闻标题(完整或部分) - lookback_days: 向前查找的天数范围,默认7天,最大30天 + reference_text: 参考新闻标题(完整或部分) + time_preset: 时间范围预设值,可选: + - "yesterday": 昨天 + - "last_week": 上周 (7天) + - "last_month": 上个月 (30天) + - "custom": 自定义日期范围(需要提供 start_date 和 end_date) threshold: 相关性阈值,0-1之间,默认0.4 注意:综合相似度计算(70%关键词重合 + 30%文本相似度) 阈值越高匹配越严格,返回结果越少 - platforms: 平台ID列表。不指定则搜索所有平台 limit: 返回条数限制,默认50,最大100 注意:实际返回数量取决于相关性匹配结果,可能少于请求值 include_url: 是否包含URL链接,默认False(节省token) @@ -437,7 +476,7 @@ async def search_related_news_history( tools = _get_tools() result = tools['search'].search_related_news_history( reference_text=reference_text, - time_range=time_range, + time_preset=time_preset, threshold=threshold, limit=limit, include_url=include_url diff --git a/mcp_server/services/data_service.py b/mcp_server/services/data_service.py index ddf1b93f..9e409a1f 100644 --- a/mcp_server/services/data_service.py +++ b/mcp_server/services/data_service.py @@ -495,6 +495,46 @@ class DataService: return result + def get_available_date_range(self) -> Tuple[Optional[datetime], Optional[datetime]]: + """ + 扫描 output 目录,返回实际可用的日期范围 + + Returns: + (最早日期, 最新日期) 元组,如果没有数据则返回 (None, None) + + Examples: + >>> service = DataService() + >>> earliest, latest = service.get_available_date_range() + >>> print(f"可用日期范围:{earliest} 至 {latest}") + """ + output_dir = self.parser.project_root / "output" + + if not output_dir.exists(): + return (None, None) + + available_dates = [] + + # 遍历日期文件夹 + for date_folder in output_dir.iterdir(): + if date_folder.is_dir() and not date_folder.name.startswith('.'): + # 解析日期(格式: YYYY年MM月DD日) + try: + date_match = re.match(r'(\d{4})年(\d{2})月(\d{2})日', date_folder.name) + if date_match: + folder_date = datetime( + int(date_match.group(1)), + int(date_match.group(2)), + int(date_match.group(3)) + ) + available_dates.append(folder_date) + except Exception: + pass + + if not available_dates: + return (None, None) + + return (min(available_dates), max(available_dates)) + def get_system_status(self) -> Dict: """ 获取系统运行状态 diff --git a/mcp_server/tools/analytics.py b/mcp_server/tools/analytics.py index 5d84b7ef..e851d8bb 100644 --- a/mcp_server/tools/analytics.py +++ b/mcp_server/tools/analytics.py @@ -157,11 +157,10 @@ class AnalyticsTools: self, topic: str, analysis_type: str = "trend", - time_range: str = "7d", + date_range: Optional[Dict[str, str]] = None, granularity: str = "day", threshold: float = 3.0, time_window: int = 24, - lookback_days: int = 7, lookahead_hours: int = 6, confidence_threshold: float = 0.7 ) -> Dict: @@ -175,11 +174,12 @@ class AnalyticsTools: - "lifecycle": 生命周期分析(从出现到消失的完整周期) - "viral": 异常热度检测(识别突然爆火的话题) - "predict": 话题预测(预测未来可能的热点) - time_range: 时间范围(trend模式),默认"7d"(7d/24h/1w/1m/2m) + date_range: 日期范围(trend和lifecycle模式),可选 + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **默认**: 不指定时默认分析最近7天 granularity: 时间粒度(trend模式),默认"day"(hour/day) threshold: 热度突增倍数阈值(viral模式),默认3.0 time_window: 检测时间窗口小时数(viral模式),默认24 - lookback_days: 回溯天数(lifecycle模式),默认7 lookahead_hours: 预测未来小时数(predict模式),默认6 confidence_threshold: 置信度阈值(predict模式),默认0.7 @@ -187,8 +187,8 @@ class AnalyticsTools: 趋势分析结果字典 Examples: - - analyze_topic_trend_unified(topic="人工智能", analysis_type="trend", time_range="7d") - - analyze_topic_trend_unified(topic="特斯拉", analysis_type="lifecycle", lookback_days=7) + - analyze_topic_trend_unified(topic="人工智能", analysis_type="trend", date_range={"start": "2025-10-18", "end": "2025-10-25"}) + - analyze_topic_trend_unified(topic="特斯拉", analysis_type="lifecycle", date_range={"start": "2025-10-18", "end": "2025-10-25"}) - analyze_topic_trend_unified(topic="比特币", analysis_type="viral", threshold=3.0) - analyze_topic_trend_unified(topic="ChatGPT", analysis_type="predict", lookahead_hours=6) """ @@ -206,13 +206,13 @@ class AnalyticsTools: if analysis_type == "trend": return self.get_topic_trend_analysis( topic=topic, - time_range=time_range, + date_range=date_range, granularity=granularity ) elif analysis_type == "lifecycle": return self.analyze_topic_lifecycle( topic=topic, - lookback_days=lookback_days + date_range=date_range ) elif analysis_type == "viral": # viral模式不需要topic参数,使用通用检测 @@ -244,7 +244,7 @@ class AnalyticsTools: def get_topic_trend_analysis( self, topic: str, - time_range: str = "7d", + date_range: Optional[Dict[str, str]] = None, granularity: str = "day" ) -> Dict: """ @@ -252,7 +252,9 @@ class AnalyticsTools: Args: topic: 话题关键词 - time_range: 时间范围,格式:7d(7天)、24h(24小时)、1w(1周)、1m(1个月)、2m(2个月) + date_range: 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **默认**: 不指定时默认分析最近7天 granularity: 时间粒度,仅支持 day(天) Returns: @@ -264,20 +266,20 @@ class AnalyticsTools: - "查看'比特币'过去一周的热度变化" - "看看'iPhone'最近7天的趋势如何" - "分析'特斯拉'最近一个月的热度趋势" - - "查看'ChatGPT'过去2个月的趋势变化" + - "查看'ChatGPT'2024年12月的趋势变化" 代码调用示例: >>> tools = AnalyticsTools() >>> # 分析7天趋势 >>> result = tools.get_topic_trend_analysis( ... topic="人工智能", - ... time_range="7d", + ... date_range={"start": "2025-10-18", "end": "2025-10-25"}, ... granularity="day" ... ) - >>> # 分析1个月趋势 + >>> # 分析历史月份趋势 >>> result = tools.get_topic_trend_analysis( ... topic="特斯拉", - ... time_range="1m", + ... date_range={"start": "2024-12-01", "end": "2024-12-31"}, ... granularity="day" ... ) >>> print(result['trend_data']) @@ -294,15 +296,21 @@ class AnalyticsTools: suggestion="当前仅支持 'day' 粒度,因为底层数据按天聚合" ) - # 解析时间范围 - days = self._parse_time_range(time_range) + # 处理日期范围(不指定时默认最近7天) + if date_range: + from ..utils.validators import validate_date_range + date_range_tuple = validate_date_range(date_range) + start_date, end_date = date_range_tuple + else: + # 默认最近7天 + end_date = datetime.now() + start_date = end_date - timedelta(days=6) # 收集趋势数据 trend_data = [] - start_date = datetime.now() - timedelta(days=days) current_date = start_date - while current_date <= datetime.now(): + while current_date <= end_date: try: all_titles, _, _ = self.data_service.parser.read_all_titles_for_date( date=current_date @@ -336,6 +344,7 @@ class AnalyticsTools: # 计算趋势指标 counts = [item["count"] for item in trend_data] + total_days = (end_date - start_date).days + 1 if len(counts) >= 2: # 计算涨跌幅度 @@ -359,7 +368,11 @@ class AnalyticsTools: return { "success": True, "topic": topic, - "time_range": time_range, + "date_range": { + "start": start_date.strftime("%Y-%m-%d"), + "end": end_date.strftime("%Y-%m-%d"), + "total_days": total_days + }, "granularity": granularity, "trend_data": trend_data, "statistics": { @@ -1452,14 +1465,16 @@ class AnalyticsTools: def analyze_topic_lifecycle( self, topic: str, - lookback_days: int = 7 + date_range: Optional[Dict[str, str]] = None ) -> Dict: """ 话题生命周期分析 - 追踪话题从出现到消失的完整周期 Args: topic: 话题关键词 - lookback_days: 回溯天数 + date_range: 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **默认**: 不指定时默认分析最近7天 Returns: 话题生命周期分析结果 @@ -1474,21 +1489,28 @@ class AnalyticsTools: >>> tools = AnalyticsTools() >>> result = tools.analyze_topic_lifecycle( ... topic="人工智能", - ... lookback_days=7 + ... date_range={"start": "2025-10-18", "end": "2025-10-25"} ... ) >>> print(result['lifecycle_stage']) """ try: # 参数验证 topic = validate_keyword(topic) - lookback_days = validate_limit(lookback_days, default=7, max_limit=30) + + # 处理日期范围(不指定时默认最近7天) + if date_range: + from ..utils.validators import validate_date_range + date_range_tuple = validate_date_range(date_range) + start_date, end_date = date_range_tuple + else: + # 默认最近7天 + end_date = datetime.now() + start_date = end_date - timedelta(days=6) # 收集话题历史数据 lifecycle_data = [] - start_date = datetime.now() - timedelta(days=lookback_days) - current_date = start_date - while current_date <= datetime.now(): + while current_date <= end_date: try: all_titles, _, _ = self.data_service.parser.read_all_titles_for_date( date=current_date @@ -1514,12 +1536,16 @@ class AnalyticsTools: current_date += timedelta(days=1) + # 计算分析天数 + total_days = (end_date - start_date).days + 1 + # 分析生命周期阶段 counts = [item["count"] for item in lifecycle_data] if not any(counts): + time_desc = f"{start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}" raise DataNotFoundError( - f"在过去 {lookback_days} 天内未找到话题 '{topic}'", + f"在 {time_desc} 内未找到话题 '{topic}'", suggestion="请尝试其他话题或扩大时间范围" ) @@ -1554,7 +1580,7 @@ class AnalyticsTools: if active_days <= 2 and max_count > avg_count * 2: topic_type = "昙花一现" - elif active_days >= lookback_days * 0.6: + elif active_days >= total_days * 0.6: topic_type = "持续热点" else: topic_type = "周期性热点" @@ -1562,7 +1588,11 @@ class AnalyticsTools: return { "success": True, "topic": topic, - "lookback_days": lookback_days, + "date_range": { + "start": start_date.strftime("%Y-%m-%d"), + "end": end_date.strftime("%Y-%m-%d"), + "total_days": total_days + }, "lifecycle_data": lifecycle_data, "analysis": { "first_appearance": first_appearance, @@ -1890,29 +1920,6 @@ class AnalyticsTools: # ==================== 辅助方法 ==================== - def _parse_time_range(self, time_range: str) -> int: - """解析时间范围字符串为天数""" - match = re.match(r'(\d+)([dhwm])', time_range.lower()) - if not match: - raise InvalidParameterError( - f"无效的时间范围格式: {time_range}", - suggestion="格式示例:7d(7天)、24h(24小时)、1w(1周)、1m(1个月)、2m(2个月)" - ) - - value = int(match.group(1)) - unit = match.group(2) - - if unit == 'h': - return max(1, value // 24) # 转换为天数 - elif unit == 'd': - return value - elif unit == 'w': - return value * 7 - elif unit == 'm': - return value * 30 # 1个月按30天计算 - - return value - def _extract_keywords(self, title: str, min_length: int = 2) -> List[str]: """ 从标题中提取关键词(简单实现) diff --git a/mcp_server/tools/search_tools.py b/mcp_server/tools/search_tools.py index a68ee175..f393ec6a 100644 --- a/mcp_server/tools/search_tools.py +++ b/mcp_server/tools/search_tools.py @@ -55,8 +55,11 @@ class SearchTools: - "keyword": 精确关键词匹配(默认) - "fuzzy": 模糊内容匹配(使用相似度算法) - "entity": 实体名称搜索(自动按权重排序) - date_range: 日期范围,格式: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} - 不指定则默认查询今天 + date_range: 日期范围(可选) + - **格式**: {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"} + - **示例**: {"start": "2025-01-01", "end": "2025-01-07"} + - **默认**: 不指定时默认查询今天 + - **注意**: start和end可以相同(表示单日查询) platforms: 平台过滤列表,如 ['zhihu', 'weibo'] limit: 返回条数限制,默认50 sort_by: 排序方式,可选值: @@ -73,7 +76,7 @@ class SearchTools: - search_news_unified(query="人工智能", search_mode="keyword") - search_news_unified(query="特斯拉降价", search_mode="fuzzy", threshold=0.4) - search_news_unified(query="马斯克", search_mode="entity", limit=20) - - search_news_unified(query="iPhone 16发布", search_mode="keyword") + - search_news_unified(query="iPhone 16", date_range={"start": "2025-01-01", "end": "2025-01-07"}) """ try: # 参数验证 @@ -100,8 +103,22 @@ class SearchTools: date_range_tuple = validate_date_range(date_range) start_date, end_date = date_range_tuple else: - # 默认今天 - start_date = end_date = datetime.now() + # 不指定日期时,使用最新可用数据日期(而非 datetime.now()) + earliest, latest = self.data_service.get_available_date_range() + + if latest is None: + # 没有任何可用数据 + return { + "success": False, + "error": { + "code": "NO_DATA_AVAILABLE", + "message": "output 目录下没有可用的新闻数据", + "suggestion": "请先运行爬虫生成数据,或检查 output 目录" + } + } + + # 使用最新可用日期 + start_date = end_date = latest # 收集所有匹配的新闻 all_matches = [] @@ -137,16 +154,34 @@ class SearchTools: current_date += timedelta(days=1) if not all_matches: - time_desc = "今天" if start_date == end_date else f"{start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}" - return { + # 获取可用日期范围用于错误提示 + earliest, latest = self.data_service.get_available_date_range() + + # 判断时间范围描述 + if start_date.date() == datetime.now().date() and start_date == end_date: + time_desc = "今天" + elif start_date == end_date: + time_desc = start_date.strftime("%Y-%m-%d") + else: + time_desc = f"{start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}" + + # 构建错误消息 + if earliest and latest: + available_desc = f"{earliest.strftime('%Y-%m-%d')} 至 {latest.strftime('%Y-%m-%d')}" + message = f"未找到匹配的新闻(查询范围: {time_desc},可用数据: {available_desc})" + else: + message = f"未找到匹配的新闻({time_desc})" + + result = { "success": True, "results": [], "total": 0, "query": query, "search_mode": search_mode, "time_range": time_desc, - "message": f"未找到匹配的新闻({time_desc})" + "message": message } + return result # 统一排序逻辑 if sort_by == "relevance": @@ -160,8 +195,10 @@ class SearchTools: # 限制返回数量 results = all_matches[:limit] - # 构建时间范围描述 - if start_date == end_date: + # 构建时间范围描述(正确判断是否为今天) + if start_date.date() == datetime.now().date() and start_date == end_date: + time_range_desc = "今天" + elif start_date == end_date: time_range_desc = start_date.strftime("%Y-%m-%d") else: time_range_desc = f"{start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}" @@ -457,7 +494,7 @@ class SearchTools: def search_related_news_history( self, reference_text: str, - time_range: str = "yesterday", + time_preset: str = "yesterday", start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, threshold: float = 0.4, @@ -469,13 +506,13 @@ class SearchTools: Args: reference_text: 参考新闻标题或内容 - time_range: 时间范围预设值,可选: + time_preset: 时间范围预设值,可选: - "yesterday": 昨天 - "last_week": 上周 (7天) - "last_month": 上个月 (30天) - "custom": 自定义日期范围(需要提供 start_date 和 end_date) - start_date: 自定义开始日期(仅当 time_range="custom" 时有效) - end_date: 自定义结束日期(仅当 time_range="custom" 时有效) + start_date: 自定义开始日期(仅当 time_preset="custom" 时有效) + end_date: 自定义结束日期(仅当 time_preset="custom" 时有效) threshold: 相似度阈值 (0-1之间),默认0.4 limit: 返回条数限制,默认50 include_url: 是否包含URL链接,默认False(节省token) @@ -487,7 +524,7 @@ class SearchTools: >>> tools = SearchTools() >>> result = tools.search_related_news_history( ... reference_text="人工智能技术突破", - ... time_range="last_week", + ... time_preset="last_week", ... threshold=0.4, ... limit=50 ... ) @@ -503,16 +540,16 @@ class SearchTools: # 确定查询日期范围 today = datetime.now() - if time_range == "yesterday": + if time_preset == "yesterday": search_start = today - timedelta(days=1) search_end = today - timedelta(days=1) - elif time_range == "last_week": + elif time_preset == "last_week": search_start = today - timedelta(days=7) search_end = today - timedelta(days=1) - elif time_range == "last_month": + elif time_preset == "last_month": search_start = today - timedelta(days=30) search_end = today - timedelta(days=1) - elif time_range == "custom": + elif time_preset == "custom": if not start_date or not end_date: raise InvalidParameterError( "自定义时间范围需要提供 start_date 和 end_date", @@ -522,7 +559,7 @@ class SearchTools: search_end = end_date else: raise InvalidParameterError( - f"不支持的时间范围: {time_range}", + f"不支持的时间范围: {time_preset}", suggestion="请使用 'yesterday', 'last_week', 'last_month' 或 'custom'" ) @@ -600,7 +637,7 @@ class SearchTools: "results": [], "total": 0, "query": reference_text, - "time_range": time_range, + "time_preset": time_preset, "date_range": { "start": search_start.strftime("%Y-%m-%d"), "end": search_end.strftime("%Y-%m-%d") @@ -627,7 +664,7 @@ class SearchTools: "threshold": threshold, "reference_text": reference_text, "reference_keywords": reference_keywords, - "time_range": time_range, + "time_preset": time_preset, "date_range": { "start": search_start.strftime("%Y-%m-%d"), "end": search_end.strftime("%Y-%m-%d") diff --git a/mcp_server/utils/validators.py b/mcp_server/utils/validators.py index c68ae32a..a046929a 100644 --- a/mcp_server/utils/validators.py +++ b/mcp_server/utils/validators.py @@ -179,6 +179,33 @@ def validate_date_range(date_range: Optional[dict]) -> Optional[tuple]: suggestion=f"start: {start_str}, end: {end_str}" ) + # 检查日期是否在未来 + today = datetime.now().date() + if start_date.date() > today or end_date.date() > today: + # 获取可用日期范围提示 + try: + from ..services.data_service import DataService + data_service = DataService() + earliest, latest = data_service.get_available_date_range() + + if earliest and latest: + available_range = f"{earliest.strftime('%Y-%m-%d')} 至 {latest.strftime('%Y-%m-%d')}" + else: + available_range = "无可用数据" + except Exception: + available_range = "未知(请检查 output 目录)" + + future_dates = [] + if start_date.date() > today: + future_dates.append(start_str) + if end_date.date() > today and end_str != start_str: + future_dates.append(end_str) + + raise InvalidParameterError( + f"不允许查询未来日期: {', '.join(future_dates)}(当前日期: {today.strftime('%Y-%m-%d')})", + suggestion=f"当前可用数据范围: {available_range}" + ) + return (start_date, end_date) diff --git a/pyproject.toml b/pyproject.toml index c78ad9ae..45528ddd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "trendradar-mcp" -version = "1.0.0" +version = "1.0.1" description = "TrendRadar MCP Server - 新闻热点聚合工具" requires-python = ">=3.10" dependencies = [ - "requests>=2.32.5", - "pytz>=2025.2", - "PyYAML>=6.0.3", - "fastmcp>=2.12.0", + "requests>=2.32.5,<3.0.0", + "pytz>=2025.2,<2026.0", + "PyYAML>=6.0.3,<7.0.0", + "fastmcp>=2.12.0,<2.14.0", "websockets>=13.0,<14.0", ] diff --git a/readme.md b/readme.md index fe6ecab6..422f1d06 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,8 @@ [![GitHub Stars](https://img.shields.io/github/stars/sansan0/TrendRadar?style=flat-square&logo=github&color=yellow)](https://github.com/sansan0/TrendRadar/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/sansan0/TrendRadar?style=flat-square&logo=github&color=blue)](https://github.com/sansan0/TrendRadar/network/members) [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat-square)](LICENSE) -[![Version](https://img.shields.io/badge/version-v3.0.3-green.svg?style=flat-square)](https://github.com/sansan0/TrendRadar) +[![Version](https://img.shields.io/badge/version-v3.0.3-blue.svg)](https://github.com/sansan0/TrendRadar) +[![MCP](https://img.shields.io/badge/MCP-v1.0.1-green.svg)](https://github.com/sansan0/TrendRadar) [![企业微信通知](https://img.shields.io/badge/企业微信-通知-00D4AA?style=flat-square)](https://work.weixin.qq.com/) [![Telegram通知](https://img.shields.io/badge/Telegram-通知-00D4AA?style=flat-square)](https://telegram.org/) @@ -505,11 +506,22 @@ GitHub 一键 Fork 即可使用,无需编程基础。 - **大版本升级**:从 v1.x 升级到 v2.y, 建议删除现有 fork 后重新 fork,这样更省力且避免配置冲突 +### 2025/10/26 - mcp-v1.0.1 + + **MCP 模块更新:** + - 修复日期查询参数传递错误 + - 统一所有工具的时间参数格式 + + ### 2025/10/23 - v3.0.3 - 扩大 ntfy 错误信息显示范围 +
+👉 历史更新 + + ### 2025/10/21 - v3.0.2 - 修复 ntfy 推送编码问题 @@ -535,9 +547,6 @@ GitHub 一键 Fork 即可使用,无需编程基础。 - 可选择性使用,无需升级现有部署 -
-👉 历史更新 - ### 2025/10/15 - v2.4.4 - **更新内容**: @@ -1318,6 +1327,10 @@ Cherry Studio 提供 GUI 配置界面,可快速部署。 "看看'iPhone'话题是昙花一现还是持续热点" ``` +**效果图**: + +mcp 使用效果图 + **详细教程**:[README-MCP-FAQ.md](README-MCP-FAQ.md) > 如果有部署问题,请带上截图反馈,后续我会根据反馈出个**图文教程**,争取编程零基础、文科生都能配置,届时会更新到我的公众号上 diff --git a/requirements.txt b/requirements.txt index 9d8d77c7..7d660386 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -requests>=2.32.5 -pytz>=2025.2 -PyYAML>=6.0.3 -fastmcp>=2.12.0 -websockets>=13.0,<14.0 \ No newline at end of file +requests>=2.32.5,<3.0.0 +pytz>=2025.2,<2026.0 +PyYAML>=6.0.3,<7.0.0 +fastmcp>=2.12.0,<2.14.0 +websockets>=13.0,<14.0