+-
{{ news.news_type }}
+-
{{ news.title }}
+-
{{ news.content[:200] }}...
+-
{{ news.published_at.strftime('%b %d, %Y') }}
++
++
{{ news.news_type }}
++ {% if news.source_name %}
++
++ {% if news.source_icon %}
++

++ {% endif %}
++
{{ news.source_name }}
++
++ {% endif %}
++
++
++ {% if news.url %}
++
++ {{ news.title }}
++
++ {% else %}
++ {{ news.title }}
++ {% endif %}
++
++ {% if news.content %}
++
{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}
++ {% endif %}
++
++
++ {% if news.published_at %}
++ {{ news.published_at.strftime('%Y年%m月%d日') }}
++ {% else %}
++ 未知日期
++ {% endif %}
++
++ {% if news.url %}
++
++ 阅读全文 ↗
++
++ {% endif %}
++
+
+ {% endfor %}
+
+diff --git a/test_news_feature.py b/test_news_feature.py
+new file mode 100644
+index 0000000..3ac30e3
+--- /dev/null
++++ b/test_news_feature.py
+@@ -0,0 +1,142 @@
++"""
++测试v2.2新闻功能 - 完整流程测试
++"""
++import os
++import sys
++from dotenv import load_dotenv
++
++# 加载环境变量
++load_dotenv()
++
++# 添加项目路径
++sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
++
++from app import create_app
++from models import db, Site, News
++from utils.news_searcher import NewsSearcher
++
++def test_news_feature():
++ """测试新闻功能"""
++ print("=" * 60)
++ print("v2.2 新闻功能测试")
++ print("=" * 60)
++
++ # 创建应用上下文
++ app = create_app('development')
++
++ with app.app_context():
++ # 1. 测试API配置
++ print("\n[1/4] 检查API配置...")
++ api_key = app.config.get('BOCHA_API_KEY')
++ if not api_key:
++ print(">>> 错误:BOCHA_API_KEY未配置")
++ return False
++ print(f">>> API Key: {api_key[:20]}...")
++
++ # 2. 测试数据库连接
++ print("\n[2/4] 检查数据库...")
++ try:
++ site_count = Site.query.filter_by(is_active=True).count()
++ print(f">>> 找到 {site_count} 个启用的网站")
++
++ if site_count == 0:
++ print(">>> 警告:没有可用的网站")
++ return False
++
++ except Exception as e:
++ print(f">>> 数据库错误:{e}")
++ return False
++
++ # 3. 测试新闻搜索
++ print("\n[3/4] 测试新闻搜索...")
++ searcher = NewsSearcher(api_key)
++
++ # 获取第一个网站
++ site = Site.query.filter_by(is_active=True).first()
++ print(f">>> 测试网站:{site.name}")
++
++ try:
++ news_items = searcher.search_site_news(
++ site_name=site.name,
++ site_url=site.url,
++ count=3,
++ freshness='oneWeek'
++ )
++
++ print(f">>> 找到 {len(news_items)} 条新闻")
++
++ if news_items:
++ print("\n新闻列表:")
++ for i, item in enumerate(news_items, 1):
++ print(f" {i}. {item['title'][:50]}...")
++ print(f" 来源:{item.get('site_name', '未知')}")
++ print(f" URL:{item['url'][:60]}...")
++
++ except Exception as e:
++ print(f">>> 搜索失败:{e}")
++ return False
++
++ # 4. 测试保存到数据库
++ print(f"\n[4/4] 测试保存到数据库...")
++
++ if not news_items:
++ print(">>> 没有新闻可保存")
++ return True
++
++ try:
++ saved_count = 0
++ for item in news_items[:2]: # 只保存前2条作为测试
++ # 检查是否已存在
++ existing = News.query.filter_by(
++ site_id=site.id,
++ url=item['url']
++ ).first()
++
++ if not existing:
++ news = News(
++ site_id=site.id,
++ title=item['title'],
++ content=item.get('summary') or item.get('snippet', ''),
++ url=item['url'],
++ source_name=item.get('site_name', ''),
++ source_icon=item.get('site_icon', ''),
++ published_at=item.get('published_at'),
++ news_type='Search Result',
++ is_active=True
++ )
++ db.session.add(news)
++ saved_count += 1
++
++ db.session.commit()
++ print(f">>> 成功保存 {saved_count} 条新闻")
++
++ # 验证保存
++ total_news = News.query.filter_by(site_id=site.id).count()
++ print(f">>> 该网站共有 {total_news} 条新闻记录")
++
++ except Exception as e:
++ db.session.rollback()
++ print(f">>> 保存失败:{e}")
++ return False
++
++ print("\n" + "=" * 60)
++ print(">>> 所有测试通过!")
++ print("=" * 60)
++
++ # 提供下一步建议
++ print("\n下一步操作:")
++ print(f"1. 访问网站详情页查看新闻:http://localhost:5000/site/{site.code}")
++ print(f"2. 访问后台新闻管理:http://localhost:5000/admin/newsadmin/")
++ print(f"3. 运行定期任务脚本:python fetch_news_cron.py --limit 5")
++
++ return True
++
++if __name__ == '__main__':
++ try:
++ success = test_news_feature()
++ sys.exit(0 if success else 1)
++ except Exception as e:
++ print(f"\n严重错误:{e}")
++ import traceback
++ traceback.print_exc()
++ sys.exit(1)
+diff --git a/utils/news_searcher.py b/utils/news_searcher.py
+new file mode 100644
+index 0000000..452eb13
+--- /dev/null
++++ b/utils/news_searcher.py
+@@ -0,0 +1,271 @@
++"""
++新闻搜索工具 - 使用博查 Web Search API
++"""
++import requests
++import json
++from datetime import datetime
++from typing import List, Dict, Optional
++
++
++class NewsSearcher:
++ """博查新闻搜索器"""
++
++ def __init__(self, api_key: str, base_url: str = 'https://api.bocha.cn'):
++ """
++ 初始化新闻搜索器
++
++ Args:
++ api_key: 博查API密钥
++ base_url: API基础URL
++ """
++ self.api_key = api_key
++ self.base_url = base_url
++ self.endpoint = f"{base_url}/v1/web-search"
++
++ def search_news(
++ self,
++ query: str,
++ count: int = 10,
++ freshness: str = 'oneMonth',
++ summary: bool = True,
++ include: Optional[str] = None,
++ exclude: Optional[str] = None
++ ) -> Dict:
++ """
++ 搜索新闻
++
++ Args:
++ query: 搜索关键词
++ count: 返回结果数量(1-50)
++ freshness: 时间范围(noLimit/oneDay/oneWeek/oneMonth/oneYear)
++ summary: 是否显示摘要
++ include: 指定搜索的网站范围(多个域名用|或,分隔)
++ exclude: 排除搜索的网站范围(多个域名用|或,分隔)
++
++ Returns:
++ 搜索结果字典
++ """
++ headers = {
++ 'Authorization': f'Bearer {self.api_key}',
++ 'Content-Type': 'application/json'
++ }
++
++ payload = {
++ 'query': query,
++ 'count': count,
++ 'freshness': freshness,
++ 'summary': summary
++ }
++
++ # 添加可选参数
++ if include:
++ payload['include'] = include
++ if exclude:
++ payload['exclude'] = exclude
++
++ try:
++ response = requests.post(
++ self.endpoint,
++ headers=headers,
++ data=json.dumps(payload),
++ timeout=30
++ )
++ response.raise_for_status()
++ return response.json()
++
++ except requests.exceptions.RequestException as e:
++ return {
++ 'success': False,
++ 'error': str(e),
++ 'code': getattr(response, 'status_code', None) if 'response' in locals() else None
++ }
++
++ def parse_news_items(self, search_result: Dict) -> List[Dict]:
++ """
++ 解析搜索结果为新闻列表
++
++ Args:
++ search_result: 博查API返回的搜索结果
++
++ Returns:
++ 新闻列表,每个新闻包含:title, url, snippet, summary, site_name, published_at等
++ """
++ news_items = []
++
++ # 检查返回数据格式
++ if 'data' not in search_result:
++ return news_items
++
++ data = search_result['data']
++ if 'webPages' not in data or 'value' not in data['webPages']:
++ return news_items
++
++ # 解析每条新闻
++ for item in data['webPages']['value']:
++ news_item = {
++ 'title': item.get('name', ''),
++ 'url': item.get('url', ''),
++ 'snippet': item.get('snippet', ''),
++ 'summary': item.get('summary', ''),
++ 'site_name': item.get('siteName', ''),
++ 'site_icon': item.get('siteIcon', ''),
++ 'published_at': self._parse_date(item.get('datePublished')),
++ 'display_url': item.get('displayUrl', ''),
++ 'language': item.get('language', ''),
++ }
++ news_items.append(news_item)
++
++ return news_items
++
++ def search_site_news(
++ self,
++ site_name: str,
++ site_url: Optional[str] = None,
++ count: int = 10,
++ freshness: str = 'oneMonth'
++ ) -> List[Dict]:
++ """
++ 搜索特定网站的相关新闻
++
++ Args:
++ site_name: 网站名称(用于搜索关键词)
++ site_url: 网站URL(可选,用于排除网站自身)
++ count: 返回结果数量
++ freshness: 时间范围
++
++ Returns:
++ 新闻列表
++ """
++ # 构建搜索关键词:网站名称 + "最新" + "新闻"
++ query = f"{site_name} 最新 新闻"
++
++ # 如果提供了网站URL,排除网站自身的结果
++ exclude = None
++ if site_url:
++ # 提取域名
++ try:
++ from urllib.parse import urlparse
++ parsed = urlparse(site_url)
++ domain = parsed.netloc or parsed.path
++ # 移除 www. 前缀
++ domain = domain.replace('www.', '')
++ exclude = domain
++ except Exception:
++ pass
++
++ # 执行搜索
++ search_result = self.search_news(
++ query=query,
++ count=count,
++ freshness=freshness,
++ summary=True,
++ exclude=exclude
++ )
++
++ # 解析结果
++ return self.parse_news_items(search_result)
++
++ def _parse_date(self, date_str: Optional[str]) -> Optional[datetime]:
++ """
++ 解析日期字符串
++
++ Args:
++ date_str: 日期字符串(例如:2025-02-23T08:18:30+08:00)
++
++ Returns:
++ datetime对象,如果解析失败返回None
++ """
++ if not date_str:
++ return None
++
++ try:
++ # 尝试解析 ISO 8601 格式
++ # 博查API返回格式:2025-02-23T08:18:30+08:00
++ if '+' in date_str or 'Z' in date_str:
++ # 使用 fromisoformat(Python 3.7+)
++ return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
++ else:
++ # 简单格式
++ return datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S')
++ except Exception:
++ # 如果解析失败,返回None
++ return None
++
++ def format_news_for_display(self, news_items: List[Dict]) -> List[Dict]:
++ """
++ 格式化新闻用于前端展示
++
++ Args:
++ news_items: 新闻列表
++
++ Returns:
++ 格式化后的新闻列表
++ """
++ formatted_news = []
++
++ for item in news_items:
++ formatted_item = {
++ 'title': item['title'],
++ 'url': item['url'],
++ 'description': item.get('summary') or item.get('snippet', ''),
++ 'source': item.get('site_name', '未知来源'),
++ 'published_date': self._format_date(item.get('published_at')),
++ 'icon': item.get('site_icon', '')
++ }
++ formatted_news.append(formatted_item)
++
++ return formatted_news
++
++ def _format_date(self, dt: Optional[datetime]) -> str:
++ """
++ 格式化日期用于显示
++
++ Args:
++ dt: datetime对象
++
++ Returns:
++ 格式化的日期字符串
++ """
++ if not dt:
++ return '未知日期'
++
++ try:
++ # 返回格式:2025-01-30
++ return dt.strftime('%Y-%m-%d')
++ except Exception:
++ return '未知日期'
++
++
++# 测试代码
++if __name__ == '__main__':
++ import os
++ from dotenv import load_dotenv
++
++ load_dotenv()
++
++ # 从环境变量获取API密钥
++ api_key = os.environ.get('BOCHA_API_KEY')
++ if not api_key:
++ print("错误:未设置BOCHA_API_KEY环境变量")
++ exit(1)
++
++ # 创建搜索器
++ searcher = NewsSearcher(api_key)
++
++ # 测试搜索
++ print("正在搜索:ChatGPT 最新新闻...")
++ news_items = searcher.search_site_news(
++ site_name='ChatGPT',
++ count=5,
++ freshness='oneWeek'
++ )
++
++ # 显示结果
++ print(f"\n找到 {len(news_items)} 条新闻:\n")
++ for i, news in enumerate(news_items, 1):
++ print(f"{i}. {news['title']}")
++ print(f" 来源:{news['site_name']}")
++ print(f" 日期:{searcher._format_date(news['published_at'])}")
++ print(f" URL:{news['url']}")
++ print(f" 摘要:{news.get('summary', news.get('snippet', ''))[:100]}...")
++ print()
+--
+2.50.1.windows.1
+
+
+From 495248bf5f161d83fb20246fdbf7c59d88959b27 Mon Sep 17 00:00:00 2001
+From: Jowe <123822645+Selei1983@users.noreply.github.com>
+Date: Tue, 30 Dec 2025 22:31:51 +0800
+Subject: [PATCH 2/2] =?UTF-8?q?feat:=20v2.2.0=20=E6=99=BA=E8=83=BD?=
+ =?UTF-8?q?=E6=96=B0=E9=97=BB=E6=9B=B4=E6=96=B0=E5=92=8C=E5=B8=83=E5=B1=80?=
+ =?UTF-8?q?=E4=BC=98=E5=8C=96?=
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+- 实现每日首次访问自动更新新闻功能
+- 每个网站获取3条一周内的新闻
+- 新闻模块放置在左侧主栏
+- 相似推荐移至右侧边栏
+- 自动去重防止重复新闻
+---
+ app.py | 64 +++++++++++++++++++++++++++++++++++++++
+ templates/detail_new.html | 42 ++++++++++---------------
+ 2 files changed, 80 insertions(+), 26 deletions(-)
+
+diff --git a/app.py b/app.py
+index b0f27d4..2160534 100644
+--- a/app.py
++++ b/app.py
+@@ -116,6 +116,70 @@ def create_app(config_name='default'):
+ site.view_count += 1
+ db.session.commit()
+
++ # 智能新闻更新:检查今天是否已更新过新闻
++ from datetime import date
++ today = date.today()
++
++ # 检查该网站最新一条新闻的创建时间
++ latest_news = News.query.filter_by(
++ site_id=site.id
++ ).order_by(News.created_at.desc()).first()
++
++ # 判断是否需要更新新闻
++ need_update = False
++ if not latest_news:
++ # 没有任何新闻,需要获取
++ need_update = True
++ elif latest_news.created_at.date() < today:
++ # 最新新闻不是今天创建的,需要更新
++ need_update = True
++
++ # 如果需要更新,自动获取最新新闻
++ if need_update:
++ api_key = app.config.get('BOCHA_API_KEY')
++ if api_key:
++ try:
++ # 创建新闻搜索器
++ searcher = NewsSearcher(api_key)
++
++ # 获取新闻(限制3条,一周内的)
++ news_items = searcher.search_site_news(
++ site_name=site.name,
++ site_url=site.url,
++ count=3,
++ freshness='oneWeek'
++ )
++
++ # 保存新闻到数据库
++ if news_items:
++ for item in news_items:
++ # 检查是否已存在(根据URL去重)
++ existing = News.query.filter_by(
++ site_id=site.id,
++ url=item['url']
++ ).first()
++
++ if not existing:
++ news = News(
++ site_id=site.id,
++ title=item['title'],
++ content=item.get('summary') or item.get('snippet', ''),
++ url=item['url'],
++ source_name=item.get('site_name', ''),
++ source_icon=item.get('site_icon', ''),
++ published_at=item.get('published_at'),
++ news_type='Search Result',
++ is_active=True
++ )
++ db.session.add(news)
++
++ db.session.commit()
++
++ except Exception as e:
++ # 获取新闻失败,不影响页面显示
++ print(f"自动获取新闻失败:{str(e)}")
++ db.session.rollback()
++
+ # 获取该网站的相关新闻(最多显示5条)
+ news_list = News.query.filter_by(
+ site_id=site.id,
+diff --git a/templates/detail_new.html b/templates/detail_new.html
+index 18b43a1..b6ac54b 100644
+--- a/templates/detail_new.html
++++ b/templates/detail_new.html
+@@ -669,7 +669,10 @@
+ {% endfor %}
+
+ {% endif %}
++
+
++
++