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:
408
NEWS_FEATURE_v2.2.md
Normal file
408
NEWS_FEATURE_v2.2.md
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# ZJPB v2.2.0 - 新闻搜索功能
|
||||||
|
|
||||||
|
**版本**: v2.2.0
|
||||||
|
**发布日期**: 2025-01-30
|
||||||
|
**主要功能**: 集成博查Web Search API,自动获取网站相关新闻
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 功能概述
|
||||||
|
|
||||||
|
v2.2.0版本引入了全新的新闻搜索和展示功能,通过博查AI搜索引擎API,自动为每个网站获取最新的相关新闻,并在网站详情页进行展示。
|
||||||
|
|
||||||
|
### ✨ 核心特性
|
||||||
|
|
||||||
|
1. **智能新闻搜索** 🔍
|
||||||
|
- 基于网站名称自动搜索相关新闻
|
||||||
|
- 支持自定义搜索时间范围(一天、一周、一月、一年)
|
||||||
|
- 自动排除网站自身的内容,确保新闻来源多样性
|
||||||
|
|
||||||
|
2. **新闻管理系统** 📰
|
||||||
|
- 后台新闻列表管理
|
||||||
|
- 支持手动编辑和删除新闻
|
||||||
|
- 新闻来源信息展示(网站名称、图标)
|
||||||
|
|
||||||
|
3. **前台新闻展示** 🎨
|
||||||
|
- 网站详情页展示最多5条相关新闻
|
||||||
|
- 显示新闻标题、摘要、来源、发布时间
|
||||||
|
- 点击新闻直接跳转到原文链接
|
||||||
|
|
||||||
|
4. **定期任务支持** ⏰
|
||||||
|
- 提供cron任务脚本,支持定期批量获取新闻
|
||||||
|
- 可配置获取数量、时间范围等参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1. 配置博查API
|
||||||
|
|
||||||
|
在`.env`文件中添加博查API配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 博查 Web Search API配置
|
||||||
|
BOCHA_API_KEY=sk-your-api-key-here
|
||||||
|
BOCHA_BASE_URL=https://api.bocha.cn
|
||||||
|
```
|
||||||
|
|
||||||
|
获取API Key:访问 [博查AI开放平台](https://open.bocha.cn) 注册并获取API密钥。
|
||||||
|
|
||||||
|
### 2. 数据库迁移
|
||||||
|
|
||||||
|
运行迁移脚本,为News表添加新字段:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python migrate_news_fields.py
|
||||||
|
```
|
||||||
|
|
||||||
|
迁移会添加以下字段:
|
||||||
|
- `source_name`: 新闻来源网站名称
|
||||||
|
- `source_icon`: 新闻来源网站图标URL
|
||||||
|
|
||||||
|
### 3. 测试新闻获取
|
||||||
|
|
||||||
|
#### 方法1:使用Python测试脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试NewsSearcher类
|
||||||
|
python utils/news_searcher.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方法2:使用后台API
|
||||||
|
|
||||||
|
登录后台后,使用API接口:
|
||||||
|
|
||||||
|
**单个网站获取新闻**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/api/fetch-site-news \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"site_id": 1,
|
||||||
|
"count": 10,
|
||||||
|
"freshness": "oneMonth"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**批量获取新闻**:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/api/fetch-all-news \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"count": 5,
|
||||||
|
"freshness": "oneMonth",
|
||||||
|
"limit": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 查看新闻
|
||||||
|
|
||||||
|
1. 访问任意网站详情页(例如:`http://localhost:5000/site/12345678`)
|
||||||
|
2. 滚动到页面下方,查看"相关新闻"部分
|
||||||
|
3. 点击新闻标题或"阅读全文"链接跳转到原文
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 使用指南
|
||||||
|
|
||||||
|
### 后台管理
|
||||||
|
|
||||||
|
#### 新闻管理界面
|
||||||
|
|
||||||
|
1. 登录后台:`http://localhost:5000/admin`
|
||||||
|
2. 点击左侧菜单"新闻管理"
|
||||||
|
3. 可以查看、编辑、删除已获取的新闻
|
||||||
|
|
||||||
|
**新闻列表字段**:
|
||||||
|
- ID
|
||||||
|
- 关联网站
|
||||||
|
- 新闻标题
|
||||||
|
- 来源网站
|
||||||
|
- 新闻类型
|
||||||
|
- 发布时间
|
||||||
|
- 是否启用
|
||||||
|
|
||||||
|
#### 手动获取新闻
|
||||||
|
|
||||||
|
虽然提供了API接口,但目前没有直接的后台UI按钮。可以通过以下方式触发:
|
||||||
|
|
||||||
|
1. 使用API接口(参见快速开始)
|
||||||
|
2. 使用定期任务脚本(参见下文)
|
||||||
|
|
||||||
|
### 定期任务设置
|
||||||
|
|
||||||
|
使用`fetch_news_cron.py`脚本定期自动获取新闻。
|
||||||
|
|
||||||
|
#### 手动执行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 默认参数:处理10个网站,每个网站获取5条新闻,时间范围一个月
|
||||||
|
python fetch_news_cron.py
|
||||||
|
|
||||||
|
# 自定义参数
|
||||||
|
python fetch_news_cron.py --limit 20 --count 10 --freshness oneWeek
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
- `--limit`: 处理的网站数量限制(默认:10)
|
||||||
|
- `--count`: 每个网站获取的新闻数量(默认:5)
|
||||||
|
- `--freshness`: 新闻时间范围(可选:noLimit, oneDay, oneWeek, oneMonth, oneYear)
|
||||||
|
|
||||||
|
#### 配置Crontab
|
||||||
|
|
||||||
|
在Linux服务器上配置定期任务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编辑crontab
|
||||||
|
crontab -e
|
||||||
|
|
||||||
|
# 添加以下行(每天早上8点执行)
|
||||||
|
0 8 * * * cd /opt/1panel/apps/zjpb && /opt/1panel/apps/zjpb/venv/bin/python fetch_news_cron.py --limit 10 >> logs/news_fetch.log 2>&1
|
||||||
|
|
||||||
|
# 或每6小时执行一次
|
||||||
|
0 */6 * * * cd /opt/1panel/apps/zjpb && /opt/1panel/apps/zjpb/venv/bin/python fetch_news_cron.py --limit 20 >> logs/news_fetch.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 确保创建`logs`目录:`mkdir -p logs`
|
||||||
|
- 修改路径为实际的项目路径
|
||||||
|
- 根据API配额合理设置执行频率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ 文件结构
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
|
||||||
|
```
|
||||||
|
zjpb/
|
||||||
|
├── utils/
|
||||||
|
│ └── news_searcher.py # 博查API封装类
|
||||||
|
├── migrate_news_fields.py # 数据库迁移脚本
|
||||||
|
├── fetch_news_cron.py # 定期任务脚本
|
||||||
|
└── NEWS_FEATURE_v2.2.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
|
||||||
|
```
|
||||||
|
zjpb/
|
||||||
|
├── config.py # 添加博查API配置
|
||||||
|
├── models.py # News模型添加source_name/source_icon字段
|
||||||
|
├── app.py # 添加新闻获取API路由,更新NewsAdmin
|
||||||
|
└── templates/
|
||||||
|
└── detail_new.html # 优化新闻展示UI
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 技术实现
|
||||||
|
|
||||||
|
### API集成
|
||||||
|
|
||||||
|
使用博查Web Search API进行新闻搜索:
|
||||||
|
|
||||||
|
- **接口地址**: `https://api.bocha.cn/v1/web-search`
|
||||||
|
- **认证方式**: Bearer Token
|
||||||
|
- **请求方法**: POST
|
||||||
|
- **返回格式**: JSON
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
|
||||||
|
News模型字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | Integer | 主键 |
|
||||||
|
| site_id | Integer | 关联网站ID(外键) |
|
||||||
|
| title | String(200) | 新闻标题 |
|
||||||
|
| content | Text | 新闻内容/摘要 |
|
||||||
|
| news_type | String(50) | 新闻类型 |
|
||||||
|
| url | String(500) | 新闻链接 |
|
||||||
|
| source_name | String(100) | ⭐ 来源网站名称(新增) |
|
||||||
|
| source_icon | String(500) | ⭐ 来源网站图标(新增) |
|
||||||
|
| published_at | DateTime | 发布时间 |
|
||||||
|
| is_active | Boolean | 是否启用 |
|
||||||
|
| created_at | DateTime | 创建时间 |
|
||||||
|
| updated_at | DateTime | 更新时间 |
|
||||||
|
|
||||||
|
### API路由
|
||||||
|
|
||||||
|
**1. 单个网站获取新闻**
|
||||||
|
|
||||||
|
- **路径**: `/api/fetch-site-news`
|
||||||
|
- **方法**: POST
|
||||||
|
- **权限**: 需要登录
|
||||||
|
- **参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"site_id": 1,
|
||||||
|
"count": 10,
|
||||||
|
"freshness": "oneMonth"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 批量获取新闻**
|
||||||
|
|
||||||
|
- **路径**: `/api/fetch-all-news`
|
||||||
|
- **方法**: POST
|
||||||
|
- **权限**: 需要登录
|
||||||
|
- **参数**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 5,
|
||||||
|
"freshness": "oneMonth",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 配置选项
|
||||||
|
|
||||||
|
在`config.py`中的新闻相关配置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 博查 Web Search API配置
|
||||||
|
BOCHA_API_KEY = os.environ.get('BOCHA_API_KEY')
|
||||||
|
BOCHA_BASE_URL = os.environ.get('BOCHA_BASE_URL') or 'https://api.bocha.cn'
|
||||||
|
BOCHA_SEARCH_ENDPOINT = '/v1/web-search'
|
||||||
|
|
||||||
|
# 新闻搜索配置
|
||||||
|
NEWS_SEARCH_COUNT = 10 # 每次搜索返回的新闻数量
|
||||||
|
NEWS_SEARCH_FRESHNESS = 'oneMonth' # 默认搜索一个月内的新闻
|
||||||
|
NEWS_SEARCH_SUMMARY = True # 是否显示摘要
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### API配额限制
|
||||||
|
|
||||||
|
- 博查API根据充值金额有请求频率限制
|
||||||
|
- 建议合理设置定期任务频率,避免过度消耗配额
|
||||||
|
- 详情参见:[博查API定价](https://open.bocha.cn)
|
||||||
|
|
||||||
|
### 去重机制
|
||||||
|
|
||||||
|
系统会根据新闻URL自动去重,同一条新闻不会重复保存。
|
||||||
|
|
||||||
|
### 搜索策略
|
||||||
|
|
||||||
|
- 搜索关键词:`{网站名称} 最新 新闻`
|
||||||
|
- 自动排除:网站自身域名的内容
|
||||||
|
- 时间优先:优先显示最新发布的新闻
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
|
||||||
|
- 批量获取时建议限制数量(`--limit 10-20`)
|
||||||
|
- 避免短时间内频繁调用API
|
||||||
|
- 数据库查询已优化,使用索引和去重
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 故障排查
|
||||||
|
|
||||||
|
### 1. 新闻获取失败
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- 博查API Key未配置或无效
|
||||||
|
- API配额不足
|
||||||
|
- 网络连接问题
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 检查环境变量
|
||||||
|
python -c "import os; from dotenv import load_dotenv; load_dotenv(); print(os.getenv('BOCHA_API_KEY'))"
|
||||||
|
|
||||||
|
# 测试API连接
|
||||||
|
python utils/news_searcher.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据库字段不存在
|
||||||
|
|
||||||
|
**错误信息**:`Unknown column 'source_name' in 'field list'`
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 运行数据库迁移
|
||||||
|
python migrate_news_fields.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 详情页不显示新闻
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- 数据库中没有该网站的新闻记录
|
||||||
|
- 新闻被设置为不启用(is_active=False)
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 为该网站获取新闻
|
||||||
|
curl -X POST http://localhost:5000/api/fetch-site-news \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"site_id": YOUR_SITE_ID}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 定期任务不执行
|
||||||
|
|
||||||
|
**检查清单**:
|
||||||
|
- [ ] Crontab配置是否正确
|
||||||
|
- [ ] Python路径是否正确
|
||||||
|
- [ ] 日志文件是否有写入权限
|
||||||
|
- [ ] 查看cron日志:`grep CRON /var/log/syslog`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 未来改进
|
||||||
|
|
||||||
|
### 计划功能
|
||||||
|
|
||||||
|
- [ ] 后台UI按钮直接触发新闻获取
|
||||||
|
- [ ] 新闻分类和标签支持
|
||||||
|
- [ ] 新闻摘要AI优化
|
||||||
|
- [ ] 新闻热度排序
|
||||||
|
- [ ] 用户收藏新闻功能
|
||||||
|
- [ ] 新闻RSS订阅
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
|
||||||
|
- [ ] 使用异步任务队列(Celery)
|
||||||
|
- [ ] 新闻缓存机制
|
||||||
|
- [ ] 图片CDN加速
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
- **项目名称**: ZJPB - 焦提示词 | AI工具导航
|
||||||
|
- **版本**: v2.2.0
|
||||||
|
- **发布日期**: 2025-01-30
|
||||||
|
- **博查API文档**: https://bocha-ai.feishu.cn/wiki/RXEOw02rFiwzGSkd9mUcqoeAnNK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
|
### v2.2.0 (2025-01-30)
|
||||||
|
|
||||||
|
**新增**:
|
||||||
|
- ✨ 集成博查Web Search API
|
||||||
|
- ✨ 新闻自动搜索和存储功能
|
||||||
|
- ✨ News模型添加source_name和source_icon字段
|
||||||
|
- ✨ 网站详情页新闻展示优化
|
||||||
|
- ✨ 后台新闻管理界面增强
|
||||||
|
- ✨ 定期任务脚本(fetch_news_cron.py)
|
||||||
|
- ✨ API路由:/api/fetch-site-news 和 /api/fetch-all-news
|
||||||
|
|
||||||
|
**修改**:
|
||||||
|
- 🔧 config.py添加博查API配置
|
||||||
|
- 🔧 NewsAdmin添加source_name字段显示
|
||||||
|
- 🔧 detail_new.html优化新闻展示UI
|
||||||
|
|
||||||
|
**文档**:
|
||||||
|
- 📖 NEWS_FEATURE_v2.2.md 功能文档
|
||||||
|
- 📖 migrate_news_fields.py 迁移脚本文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**祝您使用愉快!** 🎉
|
||||||
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 models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate
|
||||||
from utils.website_fetcher import WebsiteFetcher
|
from utils.website_fetcher import WebsiteFetcher
|
||||||
from utils.tag_generator import TagGenerator
|
from utils.tag_generator import TagGenerator
|
||||||
|
from utils.news_searcher import NewsSearcher
|
||||||
|
|
||||||
def create_app(config_name='default'):
|
def create_app(config_name='default'):
|
||||||
"""应用工厂函数"""
|
"""应用工厂函数"""
|
||||||
@@ -442,6 +443,205 @@ def create_app(config_name='default'):
|
|||||||
'message': f'生成失败: {str(e)}'
|
'message': f'生成失败: {str(e)}'
|
||||||
}), 500
|
}), 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'])
|
@app.route('/admin/batch-import', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -908,9 +1108,9 @@ def create_app(config_name='default'):
|
|||||||
# 显示操作列
|
# 显示操作列
|
||||||
column_display_actions = True
|
column_display_actions = True
|
||||||
|
|
||||||
column_list = ['id', 'site', 'title', 'news_type', 'published_at', 'is_active']
|
column_list = ['id', 'site', 'title', 'source_name', 'news_type', 'published_at', 'is_active']
|
||||||
column_searchable_list = ['title', 'content']
|
column_searchable_list = ['title', 'content', 'source_name']
|
||||||
column_filters = ['site', 'news_type', 'is_active', 'published_at']
|
column_filters = ['site', 'news_type', 'source_name', 'is_active', 'published_at']
|
||||||
column_labels = {
|
column_labels = {
|
||||||
'id': 'ID',
|
'id': 'ID',
|
||||||
'site': '关联网站',
|
'site': '关联网站',
|
||||||
@@ -918,16 +1118,19 @@ def create_app(config_name='default'):
|
|||||||
'content': '新闻内容',
|
'content': '新闻内容',
|
||||||
'news_type': '新闻类型',
|
'news_type': '新闻类型',
|
||||||
'url': '新闻链接',
|
'url': '新闻链接',
|
||||||
|
'source_name': '来源网站',
|
||||||
|
'source_icon': '来源图标',
|
||||||
'published_at': '发布时间',
|
'published_at': '发布时间',
|
||||||
'is_active': '是否启用',
|
'is_active': '是否启用',
|
||||||
'created_at': '创建时间',
|
'created_at': '创建时间',
|
||||||
'updated_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 = {
|
form_choices = {
|
||||||
'news_type': [
|
'news_type': [
|
||||||
|
('Search Result', 'Search Result'),
|
||||||
('Product Update', 'Product Update'),
|
('Product Update', 'Product Update'),
|
||||||
('Industry News', 'Industry News'),
|
('Industry News', 'Industry News'),
|
||||||
('Company News', 'Company News'),
|
('Company News', 'Company News'),
|
||||||
@@ -935,6 +1138,9 @@ def create_app(config_name='default'):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 默认排序
|
||||||
|
column_default_sort = ('published_at', True) # 按发布时间倒序排列
|
||||||
|
|
||||||
# Prompt模板管理视图
|
# Prompt模板管理视图
|
||||||
class PromptAdmin(SecureModelView):
|
class PromptAdmin(SecureModelView):
|
||||||
can_edit = True
|
can_edit = True
|
||||||
|
|||||||
10
config.py
10
config.py
@@ -46,6 +46,16 @@ class Config:
|
|||||||
DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY')
|
DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY')
|
||||||
DEEPSEEK_BASE_URL = os.environ.get('DEEPSEEK_BASE_URL') or 'https://api.deepseek.com'
|
DEEPSEEK_BASE_URL = os.environ.get('DEEPSEEK_BASE_URL') or 'https://api.deepseek.com'
|
||||||
|
|
||||||
|
# 博查 Web Search API配置
|
||||||
|
BOCHA_API_KEY = os.environ.get('BOCHA_API_KEY')
|
||||||
|
BOCHA_BASE_URL = os.environ.get('BOCHA_BASE_URL') or 'https://api.bocha.cn'
|
||||||
|
BOCHA_SEARCH_ENDPOINT = '/v1/web-search'
|
||||||
|
|
||||||
|
# 新闻搜索配置
|
||||||
|
NEWS_SEARCH_COUNT = 10 # 每次搜索返回的新闻数量
|
||||||
|
NEWS_SEARCH_FRESHNESS = 'oneMonth' # 默认搜索一个月内的新闻
|
||||||
|
NEWS_SEARCH_SUMMARY = True # 是否显示摘要
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
"""开发环境配置"""
|
"""开发环境配置"""
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|||||||
167
fetch_news_cron.py
Normal file
167
fetch_news_cron.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"""
|
||||||
|
定期新闻获取任务脚本
|
||||||
|
用途:定期为网站批量获取最新新闻
|
||||||
|
使用:python fetch_news_cron.py [options]
|
||||||
|
|
||||||
|
可以通过crontab定时执行:
|
||||||
|
# 每天早上8点执行,获取10个网站的新闻
|
||||||
|
0 8 * * * cd /path/to/zjpb && /path/to/venv/bin/python fetch_news_cron.py --limit 10 >> logs/news_fetch.log 2>&1
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
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 fetch_news_for_sites(limit=10, count_per_site=5, freshness='oneMonth'):
|
||||||
|
"""
|
||||||
|
批量为网站获取新闻
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: 处理的网站数量限制
|
||||||
|
count_per_site: 每个网站获取的新闻数量
|
||||||
|
freshness: 新闻时间范围
|
||||||
|
"""
|
||||||
|
# 创建Flask应用上下文
|
||||||
|
app = create_app(os.getenv('FLASK_ENV', 'production'))
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# 检查博查API配置
|
||||||
|
api_key = app.config.get('BOCHA_API_KEY')
|
||||||
|
if not api_key:
|
||||||
|
print(f"[{datetime.now()}] 错误:未配置BOCHA_API_KEY")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取启用的网站(按更新时间排序,优先处理旧的)
|
||||||
|
sites = Site.query.filter_by(is_active=True).order_by(Site.updated_at).limit(limit).all()
|
||||||
|
|
||||||
|
if not sites:
|
||||||
|
print(f"[{datetime.now()}] 没有可处理的网站")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"[{datetime.now()}] 开始批量获取新闻,共 {len(sites)} 个网站")
|
||||||
|
print(f"配置:每个网站 {count_per_site} 条新闻,时间范围:{freshness}")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# 创建新闻搜索器
|
||||||
|
searcher = NewsSearcher(api_key)
|
||||||
|
|
||||||
|
# 统计信息
|
||||||
|
total_saved = 0
|
||||||
|
total_found = 0
|
||||||
|
success_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
# 为每个网站获取新闻
|
||||||
|
for i, site in enumerate(sites, 1):
|
||||||
|
print(f"[{i}/{len(sites)}] 处理网站: {site.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 搜索新闻
|
||||||
|
news_items = searcher.search_site_news(
|
||||||
|
site_name=site.name,
|
||||||
|
site_url=site.url,
|
||||||
|
count=count_per_site,
|
||||||
|
freshness=freshness
|
||||||
|
)
|
||||||
|
|
||||||
|
if not news_items:
|
||||||
|
print(f" └─ 未找到新闻")
|
||||||
|
continue
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 提交该网站的新闻
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
total_found += len(news_items)
|
||||||
|
total_saved += site_saved
|
||||||
|
success_count += 1
|
||||||
|
|
||||||
|
print(f" └─ 找到 {len(news_items)} 条,保存 {site_saved} 条新闻")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_count += 1
|
||||||
|
print(f" └─ 错误: {str(e)}")
|
||||||
|
db.session.rollback()
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("-" * 60)
|
||||||
|
print(f"[{datetime.now()}] 批量获取完成")
|
||||||
|
print(f"成功: {success_count} 个网站, 失败: {error_count} 个网站")
|
||||||
|
print(f"共找到 {total_found} 条新闻,保存 {total_saved} 条新新闻")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
parser = argparse.ArgumentParser(description='定期新闻获取任务')
|
||||||
|
parser.add_argument('--limit', type=int, default=10, help='处理的网站数量限制(默认:10)')
|
||||||
|
parser.add_argument('--count', type=int, default=5, help='每个网站获取的新闻数量(默认:5)')
|
||||||
|
parser.add_argument('--freshness', type=str, default='oneMonth',
|
||||||
|
choices=['noLimit', 'oneDay', 'oneWeek', 'oneMonth', 'oneYear'],
|
||||||
|
help='新闻时间范围(默认:oneMonth)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"定期新闻获取任务 - 开始时间: {datetime.now()}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = fetch_news_for_sites(
|
||||||
|
limit=args.limit,
|
||||||
|
count_per_site=args.count,
|
||||||
|
freshness=args.freshness
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"\n任务执行成功!")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print(f"\n任务执行失败!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n[{datetime.now()}] 严重错误: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
99
migrate_news_fields.py
Normal file
99
migrate_news_fields.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
数据库迁移脚本 - 为News表添加source_name和source_icon字段
|
||||||
|
版本:v2.2.0
|
||||||
|
日期:2025-01-30
|
||||||
|
"""
|
||||||
|
import pymysql
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
"""执行数据库迁移"""
|
||||||
|
# 数据库配置
|
||||||
|
db_config = {
|
||||||
|
'host': os.environ.get('DB_HOST', 'localhost'),
|
||||||
|
'port': int(os.environ.get('DB_PORT', 3306)),
|
||||||
|
'user': os.environ.get('DB_USER', 'root'),
|
||||||
|
'password': os.environ.get('DB_PASSWORD', ''),
|
||||||
|
'database': os.environ.get('DB_NAME', 'ai_nav'),
|
||||||
|
'charset': 'utf8mb4'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 连接数据库
|
||||||
|
connection = pymysql.connect(**db_config)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("开始执行数据库迁移 v2.2.0")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 检查字段是否已存在
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT COLUMN_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = %s
|
||||||
|
AND TABLE_NAME = 'news'
|
||||||
|
AND COLUMN_NAME IN ('source_name', 'source_icon')
|
||||||
|
""", (db_config['database'],))
|
||||||
|
|
||||||
|
existing_columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# 添加 source_name 字段
|
||||||
|
if 'source_name' not in existing_columns:
|
||||||
|
print("\n1. 添加 source_name 字段...")
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE news
|
||||||
|
ADD COLUMN source_name VARCHAR(100)
|
||||||
|
COMMENT '新闻来源网站名称'
|
||||||
|
AFTER url
|
||||||
|
""")
|
||||||
|
print(">>> source_name 字段添加成功")
|
||||||
|
else:
|
||||||
|
print("\n1. source_name 字段已存在,跳过")
|
||||||
|
|
||||||
|
# 添加 source_icon 字段
|
||||||
|
if 'source_icon' not in existing_columns:
|
||||||
|
print("\n2. 添加 source_icon 字段...")
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE news
|
||||||
|
ADD COLUMN source_icon VARCHAR(500)
|
||||||
|
COMMENT '新闻来源网站图标URL'
|
||||||
|
AFTER source_name
|
||||||
|
""")
|
||||||
|
print(">>> source_icon 字段添加成功")
|
||||||
|
else:
|
||||||
|
print("\n2. source_icon 字段已存在,跳过")
|
||||||
|
|
||||||
|
# 提交事务
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(">>> 数据库迁移完成!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 显示表结构
|
||||||
|
print("\n当前 news 表结构:")
|
||||||
|
cursor.execute("DESCRIBE news")
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
print(f" - {row[0]}: {row[1]} {row[2]}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n>>> 迁移失败:{str(e)}")
|
||||||
|
if 'connection' in locals():
|
||||||
|
connection.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if 'cursor' in locals():
|
||||||
|
cursor.close()
|
||||||
|
if 'connection' in locals():
|
||||||
|
connection.close()
|
||||||
|
print("\n数据库连接已关闭")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate()
|
||||||
@@ -90,6 +90,8 @@ class News(db.Model):
|
|||||||
content = db.Column(db.Text, comment='新闻内容')
|
content = db.Column(db.Text, comment='新闻内容')
|
||||||
news_type = db.Column(db.String(50), default='Industry News', comment='新闻类型')
|
news_type = db.Column(db.String(50), default='Industry News', comment='新闻类型')
|
||||||
url = db.Column(db.String(500), comment='新闻链接')
|
url = db.Column(db.String(500), comment='新闻链接')
|
||||||
|
source_name = db.Column(db.String(100), comment='新闻来源网站名称')
|
||||||
|
source_icon = db.Column(db.String(500), comment='新闻来源网站图标URL')
|
||||||
published_at = db.Column(db.DateTime, default=datetime.now, comment='发布时间')
|
published_at = db.Column(db.DateTime, default=datetime.now, comment='发布时间')
|
||||||
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
|
is_active = db.Column(db.Boolean, default=True, comment='是否启用')
|
||||||
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||||
@@ -110,6 +112,8 @@ class News(db.Model):
|
|||||||
'content': self.content,
|
'content': self.content,
|
||||||
'news_type': self.news_type,
|
'news_type': self.news_type,
|
||||||
'url': self.url,
|
'url': self.url,
|
||||||
|
'source_name': self.source_name,
|
||||||
|
'source_icon': self.source_icon,
|
||||||
'published_at': self.published_at.strftime('%Y-%m-%d') if self.published_at else None,
|
'published_at': self.published_at.strftime('%Y-%m-%d') if self.published_at else None,
|
||||||
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
|
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -628,10 +628,43 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% for news in news_list %}
|
{% for news in news_list %}
|
||||||
<div class="news-item">
|
<div class="news-item">
|
||||||
<span class="news-badge">{{ news.news_type }}</span>
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
|
||||||
<h4>{{ news.title }}</h4>
|
<span class="news-badge">{{ news.news_type }}</span>
|
||||||
<p>{{ news.content[:200] }}...</p>
|
{% if news.source_name %}
|
||||||
<div class="news-date">{{ news.published_at.strftime('%b %d, %Y') }}</div>
|
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
|
||||||
|
{% if news.source_icon %}
|
||||||
|
<img src="{{ news.source_icon }}" alt="{{ news.source_name }}" style="width: 16px; height: 16px; border-radius: 2px;">
|
||||||
|
{% endif %}
|
||||||
|
<span>{{ news.source_name }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h4>
|
||||||
|
{% if news.url %}
|
||||||
|
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
|
||||||
|
{{ news.title }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ news.title }}
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
{% if news.content %}
|
||||||
|
<p>{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div class="news-date">
|
||||||
|
{% if news.published_at %}
|
||||||
|
{{ news.published_at.strftime('%Y年%m月%d日') }}
|
||||||
|
{% else %}
|
||||||
|
未知日期
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if news.url %}
|
||||||
|
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
|
||||||
|
阅读全文 ↗
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
142
test_news_feature.py
Normal file
142
test_news_feature.py
Normal file
@@ -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)
|
||||||
271
utils/news_searcher.py
Normal file
271
utils/news_searcher.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user