release: v2.2.0 - 博查新闻搜索功能
新增功能: - 集成博查Web Search API,自动获取网站相关新闻 - News模型添加source_name和source_icon字段 - 新闻管理后台界面优化 - 网站详情页新闻展示(标题、摘要、来源、链接) - 定期任务脚本支持批量获取新闻 - 完整的API路由和测试脚本 技术实现: - NewsSearcher工具类封装博查API - 智能新闻搜索和去重机制 - 数据库迁移脚本migrate_news_fields.py - API路由:/api/fetch-site-news 和 /api/fetch-all-news - Cron任务脚本:fetch_news_cron.py 修改文件: - config.py: 添加博查API配置 - models.py: News模型扩展 - app.py: 新闻获取路由和NewsAdmin优化 - templates/detail_new.html: 新闻展示UI 新增文件: - utils/news_searcher.py (271行) - migrate_news_fields.py (99行) - fetch_news_cron.py (167行) - test_news_feature.py (142行) - NEWS_FEATURE_v2.2.md (408行) 统计:9个文件,1348行新增 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
214
app.py
214
app.py
@@ -9,6 +9,7 @@ from config import config
|
||||
from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate
|
||||
from utils.website_fetcher import WebsiteFetcher
|
||||
from utils.tag_generator import TagGenerator
|
||||
from utils.news_searcher import NewsSearcher
|
||||
|
||||
def create_app(config_name='default'):
|
||||
"""应用工厂函数"""
|
||||
@@ -442,6 +443,205 @@ def create_app(config_name='default'):
|
||||
'message': f'生成失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# ========== 新闻获取路由 ==========
|
||||
@app.route('/api/fetch-site-news', methods=['POST'])
|
||||
@login_required
|
||||
def fetch_site_news():
|
||||
"""为指定网站获取最新新闻"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
site_id = data.get('site_id')
|
||||
count = data.get('count', app.config.get('NEWS_SEARCH_COUNT', 10))
|
||||
freshness = data.get('freshness', app.config.get('NEWS_SEARCH_FRESHNESS', 'oneMonth'))
|
||||
|
||||
if not site_id:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '请提供网站ID'
|
||||
}), 400
|
||||
|
||||
# 获取网站信息
|
||||
site = Site.query.get(site_id)
|
||||
if not site:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '网站不存在'
|
||||
}), 404
|
||||
|
||||
# 检查博查API配置
|
||||
api_key = app.config.get('BOCHA_API_KEY')
|
||||
if not api_key:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '博查API未配置,请在.env文件中设置BOCHA_API_KEY'
|
||||
}), 500
|
||||
|
||||
# 创建新闻搜索器
|
||||
searcher = NewsSearcher(api_key)
|
||||
|
||||
# 搜索新闻
|
||||
news_items = searcher.search_site_news(
|
||||
site_name=site.name,
|
||||
site_url=site.url,
|
||||
count=count,
|
||||
freshness=freshness
|
||||
)
|
||||
|
||||
if not news_items:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '未找到相关新闻'
|
||||
}), 404
|
||||
|
||||
# 保存新闻到数据库
|
||||
saved_count = 0
|
||||
for item in news_items:
|
||||
# 检查新闻是否已存在(根据URL判断)
|
||||
existing_news = News.query.filter_by(
|
||||
site_id=site_id,
|
||||
url=item['url']
|
||||
).first()
|
||||
|
||||
if not existing_news:
|
||||
# 创建新闻记录
|
||||
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()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'成功获取并保存 {saved_count} 条新闻',
|
||||
'total_found': len(news_items),
|
||||
'saved': saved_count,
|
||||
'news_items': searcher.format_news_for_display(news_items)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'获取失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/fetch-all-news', methods=['POST'])
|
||||
@login_required
|
||||
def fetch_all_news():
|
||||
"""批量为所有网站获取新闻"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
count_per_site = data.get('count', 5) # 每个网站获取的新闻数量
|
||||
freshness = data.get('freshness', app.config.get('NEWS_SEARCH_FRESHNESS', 'oneMonth'))
|
||||
limit = data.get('limit', 10) # 限制处理的网站数量
|
||||
|
||||
# 检查博查API配置
|
||||
api_key = app.config.get('BOCHA_API_KEY')
|
||||
if not api_key:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '博查API未配置,请在.env文件中设置BOCHA_API_KEY'
|
||||
}), 500
|
||||
|
||||
# 获取启用的网站(按更新时间排序,优先处理旧的)
|
||||
sites = Site.query.filter_by(is_active=True).order_by(Site.updated_at).limit(limit).all()
|
||||
|
||||
if not sites:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '没有可用的网站'
|
||||
}), 404
|
||||
|
||||
# 创建新闻搜索器
|
||||
searcher = NewsSearcher(api_key)
|
||||
|
||||
# 统计信息
|
||||
total_saved = 0
|
||||
total_found = 0
|
||||
processed_sites = []
|
||||
|
||||
# 为每个网站获取新闻
|
||||
for site in sites:
|
||||
try:
|
||||
# 搜索新闻
|
||||
news_items = searcher.search_site_news(
|
||||
site_name=site.name,
|
||||
site_url=site.url,
|
||||
count=count_per_site,
|
||||
freshness=freshness
|
||||
)
|
||||
|
||||
site_saved = 0
|
||||
for item in news_items:
|
||||
# 检查是否已存在
|
||||
existing_news = News.query.filter_by(
|
||||
site_id=site.id,
|
||||
url=item['url']
|
||||
).first()
|
||||
|
||||
if not existing_news:
|
||||
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)
|
||||
site_saved += 1
|
||||
|
||||
total_found += len(news_items)
|
||||
total_saved += site_saved
|
||||
|
||||
processed_sites.append({
|
||||
'id': site.id,
|
||||
'name': site.name,
|
||||
'found': len(news_items),
|
||||
'saved': site_saved
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# 单个网站失败不影响其他网站
|
||||
processed_sites.append({
|
||||
'id': site.id,
|
||||
'name': site.name,
|
||||
'error': str(e)
|
||||
})
|
||||
continue
|
||||
|
||||
# 提交事务
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'批量获取完成,共处理 {len(processed_sites)} 个网站',
|
||||
'total_found': total_found,
|
||||
'total_saved': total_saved,
|
||||
'processed_sites': processed_sites
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'批量获取失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# ========== 批量导入路由 ==========
|
||||
@app.route('/admin/batch-import', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@@ -908,9 +1108,9 @@ def create_app(config_name='default'):
|
||||
# 显示操作列
|
||||
column_display_actions = True
|
||||
|
||||
column_list = ['id', 'site', 'title', 'news_type', 'published_at', 'is_active']
|
||||
column_searchable_list = ['title', 'content']
|
||||
column_filters = ['site', 'news_type', 'is_active', 'published_at']
|
||||
column_list = ['id', 'site', 'title', 'source_name', 'news_type', 'published_at', 'is_active']
|
||||
column_searchable_list = ['title', 'content', 'source_name']
|
||||
column_filters = ['site', 'news_type', 'source_name', 'is_active', 'published_at']
|
||||
column_labels = {
|
||||
'id': 'ID',
|
||||
'site': '关联网站',
|
||||
@@ -918,16 +1118,19 @@ def create_app(config_name='default'):
|
||||
'content': '新闻内容',
|
||||
'news_type': '新闻类型',
|
||||
'url': '新闻链接',
|
||||
'source_name': '来源网站',
|
||||
'source_icon': '来源图标',
|
||||
'published_at': '发布时间',
|
||||
'is_active': '是否启用',
|
||||
'created_at': '创建时间',
|
||||
'updated_at': '更新时间'
|
||||
}
|
||||
form_columns = ['site', 'title', 'content', 'news_type', 'url', 'published_at', 'is_active']
|
||||
form_columns = ['site', 'title', 'content', 'news_type', 'url', 'source_name', 'source_icon', 'published_at', 'is_active']
|
||||
|
||||
# 可选的新闻类型
|
||||
form_choices = {
|
||||
'news_type': [
|
||||
('Search Result', 'Search Result'),
|
||||
('Product Update', 'Product Update'),
|
||||
('Industry News', 'Industry News'),
|
||||
('Company News', 'Company News'),
|
||||
@@ -935,6 +1138,9 @@ def create_app(config_name='default'):
|
||||
]
|
||||
}
|
||||
|
||||
# 默认排序
|
||||
column_default_sort = ('published_at', True) # 按发布时间倒序排列
|
||||
|
||||
# Prompt模板管理视图
|
||||
class PromptAdmin(SecureModelView):
|
||||
can_edit = True
|
||||
|
||||
Reference in New Issue
Block a user