use markdown as source

This commit is contained in:
2025-12-02 20:11:38 +08:00
parent 00af2a4f26
commit 3ed8c5a875
22 changed files with 3172 additions and 475 deletions
+669
View File
@@ -0,0 +1,669 @@
# 📰 OmniVerse 新闻爬虫系统使用指南
## 🎯 系统概述
OmniVerse 新闻爬虫系统是一个功能完善的自动化新闻抓取、分类、AI摘要生成和展示系统。系统从多个权威医疗健康资讯源自动抓取相关新闻,通过AI生成智能摘要,并在前端页面展示。
### 核心特性
✅ **自动化RSS抓取** - 定时从17+个医疗健康领域RSS源抓取新闻
✅ **智能分类** - 使用关键词权重算法自动分类新闻
✅ **AI摘要生成** - 集成Ollama本地大模型生成新闻摘要
✅ **定时任务** - 支持定时爬取和数据清理
✅ **数据导出** - 自动将数据导出为JSON供前端使用
✅ **精美前端** - 响应式设计,支持多种浏览模式
---
## 📊 新闻分类体系
系统支持以下6大新闻分类:
| 分类ID | 中文名称 | 说明 | 图标 |
|--------|---------|------|------|
| `healthcare-policy` | 康养政策 | 政策法规、行业标准、监管动态 | 🏥 |
| `medical-robotics` | 医疗机器人 | 手术机器人、康复机器人、护理机器人 | 🤖 |
| `smart-elderly-care` | 智慧养老 | 养老科技、护理创新、银发经济 | 👴 |
| `tech-innovation` | 技术前沿 | AI医疗、数字健康、创新技术 | 🚀 |
| `investment` | 投融资 | 投资动态、融资消息、市场估值 | 💰 |
| `industry-trends` | 行业动态 | 市场分析、发展趋势、行业报告 | 📈 |
---
## 🔧 快速开始
### 方法一:使用验证脚本(推荐)
```bash
# 运行交互式验证脚本
./verify-news.sh
```
脚本会自动检查环境、依赖,并提供以下选项:
1. 运行爬虫 (抓取最新新闻)
2. 导出数据为 JSON
3. **爬取并导出** ⭐ 推荐选项
4. 查看数据库统计
5. 启动爬虫服务
6. 退出
### 方法二:手动运行命令
```bash
# 进入爬虫服务目录
cd news-crawler-service
# 安装依赖
npm install
# 运行爬虫并导出数据
npm run crawl-and-export
# 或分步执行
npm run crawler # 抓取新闻
npm run export # 导出为JSON
```
---
## 🌐 启动爬虫服务
### 启动API服务
```bash
cd news-crawler-service
npm start
```
服务将在 `http://localhost:3001` 启动
### 启动前端开发服务器
```bash
# 在项目根目录
npm run dev
```
前端将在 `http://localhost:5174` 启动
访问业界新闻页面:
```
http://localhost:5174/website/industry-news
或
http://192.168.1.83:5174/website/industry-news
```
---
## 📡 API 端点
### 获取新闻列表
```http
GET /api/news?page=1&limit=12&category=tech-innovation&sortBy=publishedAt
```
**查询参数:**
- `page` - 页码 (默认: 1)
- `limit` - 每页条数 (默认: 10, 最大: 50)
- `category` - 分类过滤
- `query` - 搜索关键词
- `sortBy` - 排序字段 (publishedAt/viewCount/priority)
- `sortOrder` - 排序方向 (DESC/ASC)
### 获取热门新闻
```http
GET /api/news/hot?limit=5
```
### 获取统计信息
```http
GET /api/stats
```
### 手动触发爬取
```http
POST /api/crawl
Headers: X-API-Key: omniverse_news_2024
```
### 获取新闻分类
```http
GET /api/categories
```
### 健康检查
```http
GET /health
```
---
## ⚙️ 配置说明
### 环境变量配置 (.env)
```bash
# 服务器设置
PORT=3001
NODE_ENV=development
# 数据库路径
DATABASE_PATH=./data/news.db
# 爬虫设置
CRAWL_INTERVAL_MINUTES=30 # 爬取间隔(分钟)
MAX_NEWS_AGE_DAYS=7 # 新闻保留天数
ENABLE_SCHEDULER=true # 启用定时任务
# 缓存设置
CACHE_TTL_SECONDS=300 # 缓存有效期(秒)
# API设置
API_KEY=omniverse_news_2024 # API密钥
CORS_ORIGIN=http://localhost:5173,http://localhost:5174,http://192.168.1.83:5174
# 日志设置
LOG_LEVEL=info
LOG_FILE=./logs/app.log
# Ollama AI 设置
OLLAMA_URL=http://localhost:11434
OLLAMA_MODEL=qwen3:32b # 使用的AI模型
# 导出设置
AUTO_EXPORT_JSON=true # 爬取后自动导出JSON
```
---
## 🤖 AI 摘要功能
系统使用 Ollama 本地大模型生成新闻摘要。
### 安装 Ollama
```bash
# macOS
brew install ollama
# Linux
curl -fsSL https://ollama.ai/install.sh | sh
# Windows
# 从 https://ollama.ai 下载安装包
```
### 下载并启动模型
```bash
# 下载 qwen3:32b 模型(推荐)
ollama pull qwen3:32b
# 或使用其他中文模型
ollama pull qwen:7b
ollama pull glm4:9b
# 启动 Ollama 服务
ollama serve
```
### 验证 Ollama 服务
```bash
curl http://localhost:11434/api/tags
```
> **注意**: 如果 Ollama 服务未启动,爬虫仍会正常工作,但会使用备用摘要生成算法(提取前几句话)。
---
## 📁 数据存储
### SQLite 数据库
**位置**: `news-crawler-service/data/news.db`
**表结构**:
```sql
CREATE TABLE news (
id TEXT PRIMARY KEY, -- UUID
title TEXT NOT NULL, -- 标题
summary TEXT, -- AI摘要
content TEXT, -- Markdown内容
category TEXT NOT NULL, -- 分类
tags TEXT, -- JSON数组
source TEXT NOT NULL, -- 来源
source_url TEXT NOT NULL, -- 原文链接
published_at TEXT NOT NULL, -- 发布时间
crawled_at TEXT NOT NULL, -- 抓取时间
author TEXT, -- 作者
image_url TEXT, -- 图片URL
priority TEXT, -- 优先级 (high/medium/low)
region TEXT, -- 地区
is_hot INTEGER, -- 是否热门
view_count INTEGER, -- 浏览量
created_at DATETIME, -- 创建时间
updated_at DATETIME -- 更新时间
);
```
### JSON 导出文件
**位置**: `public/data/news.json`
格式示例:
```json
[
{
"id": "uuid-here",
"title": "新闻标题",
"summary": "AI生成的摘要...",
"content": "Markdown格式的完整内容",
"category": "medical-robotics",
"tags": ["机器人", "手术", "创新"],
"source": "动脉网",
"sourceUrl": "https://...",
"publishedAt": "2025-10-06T07:00:00.000Z",
"crawledAt": "2025-10-06T12:51:54.926Z",
"author": "作者名",
"imageUrl": "https://...",
"priority": "high",
"region": "全国",
"isHot": true,
"viewCount": 1500
}
]
```
---
## 📰 RSS 新闻源列表
系统监控以下17个医疗健康领域的权威新闻源:
### 综合科技类
- **36氪** - 投资融资动态
- **亿欧网** - 行业趋势分析
- **雷锋网** - AI技术创新
- **机器之心** - AI医疗前沿
### 医疗专业类
- **动脉网** - 医疗机器人与创新
- **医谷** - 医疗器械设备
- **器械之家** - 医疗器械行业
- **医学界** - 医学政策与研究
- **医信邦** - 数字医疗
- **医疗器械创新网** - 器械技术创新
### 政策法规类
- **健康界** - 政策法规
- **丁香园** - 医疗监管
- **医药网** - 医药政策
### 技术研发类
- **生物谷** - 生物技术研究
- **CPhI制药在线** - 制药创新
### 养老护理类
- **康复医学网** - 康复机器人
- **养老网** - 智慧养老
---
## 🎨 前端页面功能
### 业界动态页面 (`/industry-news`)
#### 功能特性
1. **分类筛选** - 7个分类标签快速切换
2. **搜索功能** - 全文搜索标题、内容、标签
3. **多种排序** - 按发布时间、浏览量、重要性排序
4. **视图切换** - 网格视图 / 列表视图
5. **热门新闻** - 独立展示热门资讯
6. **统计面板** - 实时显示新闻数量、来源统计
7. **无限滚动** - 支持分页加载更多
8. **响应式设计** - 完美适配各种屏幕尺寸
#### 新闻卡片信息
- **标题** - 新闻标题
- **AI摘要** - 智能生成的摘要
- **图片** - 新闻配图
- **分类标签** - 所属分类
- **标签云** - 关键词标签
- **发布时间** - 相对时间显示
- **浏览量** - 阅读统计
- **来源** - 新闻来源网站
- **优先级** - 重要程度标记
- **热门标记** - 热门新闻标识
---
## 🔄 定时任务
系统支持两个定时任务:
### 1. 定时爬取任务
```javascript
// 每30分钟运行一次(可配置)
cron.schedule(`*/${CRAWL_INTERVAL_MINUTES} * * * *`, async () => {
// 爬取新闻
// 自动导出JSON
});
```
### 2. 清理旧数据任务
```javascript
// 每天凌晨2点运行
cron.schedule('0 2 * * *', async () => {
// 删除7天前的新闻(可配置)
});
```
---
## 📈 使用流程
### 完整工作流程
```
1. RSS源监控
↓
2. 内容抓取与解析
↓
3. 关键词匹配与过滤
↓
4. 智能分类
↓
5. AI摘要生成
↓
6. 存储到数据库
↓
7. 导出为JSON
↓
8. 前端页面展示
```
### 日常维护操作
```bash
# 1. 手动触发一次完整爬取
cd news-crawler-service
npm run crawl-and-export
# 2. 查看数据库统计
sqlite3 news-crawler-service/data/news.db "SELECT category, COUNT(*) FROM news GROUP BY category;"
# 3. 查看最新新闻
sqlite3 news-crawler-service/data/news.db "SELECT title, source, published_at FROM news ORDER BY published_at DESC LIMIT 10;"
# 4. 清理旧数据
node news-crawler-service/src/crawler.js cleanup
# 5. 查看日志
tail -f news-crawler-service/logs/app.log
```
---
## 🐛 故障排查
### 问题1: 爬虫抓取失败
**症状**: 运行爬虫后没有新数据
**排查步骤**:
1. 检查网络连接
2. 验证RSS源是否可访问
3. 查看日志文件 `logs/app.log`
4. 检查数据库连接
**解决方案**:
```bash
# 测试网络连接
curl https://vcbeat.top/rss
# 查看详细日志
tail -100 news-crawler-service/logs/app.log
```
### 问题2: AI摘要生成失败
**症状**: 摘要内容是简单的句子提取
**排查步骤**:
1. 检查Ollama服务是否运行
2. 验证模型是否已下载
3. 检查OLLAMA_URL配置
**解决方案**:
```bash
# 启动Ollama服务
ollama serve
# 下载模型
ollama pull qwen3:32b
# 测试API
curl http://localhost:11434/api/tags
```
### 问题3: 前端无法显示新闻
**症状**: 业界动态页面为空
**排查步骤**:
1. 检查 `public/data/news.json` 是否存在
2. 验证JSON文件格式是否正确
3. 检查浏览器控制台错误
**解决方案**:
```bash
# 重新导出数据
cd news-crawler-service
npm run export
# 验证JSON文件
cat ../public/data/news.json | jq 'length'
```
### 问题4: 爬虫服务启动失败
**症状**: `npm start` 报错
**排查步骤**:
1. 检查端口3001是否被占用
2. 验证环境变量配置
3. 检查依赖是否完整安装
**解决方案**:
```bash
# 检查端口占用
lsof -i :3001
# 重新安装依赖
rm -rf node_modules package-lock.json
npm install
# 检查环境配置
cat .env
```
---
## 📝 开发指南
### 添加新的RSS源
编辑 `news-crawler-service/src/crawlers/rss-crawler.js`:
```javascript
this.sources = [
// ... 现有源
{
name: '新闻源名称',
url: 'https://example.com/rss',
category: 'tech-innovation', // 默认分类
keywords: ['关键词1', '关键词2', '关键词3'],
priority: 'high', // high/medium/low
region: '全国'
}
];
```
### 修改分类规则
编辑智能分类算法权重:
```javascript
// rss-crawler.js - smartCategorize() 方法
const categoryRules = {
'your-category': {
keywords: {
'high': ['重要关键词1', '重要关键词2'],
'medium': ['一般关键词1', '一般关键词2'],
'low': ['次要关键词1', '次要关键词2']
},
titleWeight: 5, // 标题权重
contentWeight: 3 // 内容权重
}
};
```
### 自定义前端样式
编辑 `src/pages/IndustryNews.tsx`:
```tsx
const newsCategories = [
{
value: 'new-category',
label: '新分类',
icon: '🎯',
color: 'from-blue-500 to-cyan-500'
}
];
```
---
## 🚀 部署建议
### 生产环境配置
```bash
# 1. 修改环境变量
NODE_ENV=production
ENABLE_SCHEDULER=true
CRAWL_INTERVAL_MINUTES=30
MAX_NEWS_AGE_DAYS=30
AUTO_EXPORT_JSON=true
# 2. 使用PM2管理进程
npm install -g pm2
pm2 start news-crawler-service/src/server.js --name omniverse-news
# 3. 配置开机自启
pm2 startup
pm2 save
# 4. 查看日志
pm2 logs omniverse-news
```
### Docker 部署
项目已包含 `docker-compose.yml` 配置:
```bash
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f news-crawler
# 停止服务
docker-compose down
```
---
## 📊 性能优化
### 缓存策略
- API响应缓存: 5分钟 (可配置)
- 前端数据缓存: 5分钟
- 数据库索引: category, published_at, source
### 爬取优化
- 并发爬取: 顺序执行 + 2秒延迟
- 去重检查: source_url唯一性
- 增量更新: 仅抓取新内容
### 前端优化
- 图片懒加载
- 虚拟列表滚动
- 分页加载 (每页12条)
- 响应式图片
---
## 🔐 安全建议
1. **修改默认API密钥**
```bash
API_KEY=your_secure_random_key_here
```
2. **配置CORS白名单**
```bash
CORS_ORIGIN=https://yourdomain.com
```
3. **使用HTTPS**
- 部署时配置SSL证书
- 强制HTTPS重定向
4. **限制API请求频率**
- 添加rate limiting中间件
- 防止恶意爬取
5. **定期备份数据库**
```bash
cp news-crawler-service/data/news.db backups/news_$(date +%Y%m%d).db
```
---
## 📞 技术支持
- **项目地址**: `/Users/brunogao/work/codes/web/omniverse-website`
- **问题反馈**: 提交 Issue 或 Pull Request
- **文档更新**: 2025-11-17
---
## 🎉 总结
OmniVerse 新闻爬虫系统已完成优化,现在可以:
✅ 自动从17+个医疗健康领域RSS源抓取新闻
✅ 使用AI智能生成新闻摘要
✅ 自动分类并存储到数据库
✅ 定时任务自动运行
✅ 导出JSON供前端使用
✅ 精美的前端展示页面
**立即开始使用**:
```bash
./verify-news.sh
```
选择选项3: "爬取并导出"
祝使用愉快! 🚀
+208
View File
@@ -0,0 +1,208 @@
# 🎯 真实可用的医疗健康RSS新闻源配置
## 测试结果总结(2025-11-18)
### ✅ 强烈推荐(已验证可用,内容最新)
| 来源 | URL | 相关度 | 更新频率 | 内容质量 |
|------|-----|--------|----------|----------|
| **36氪** | https://36kr.com/feed | 100% | ⭐⭐⭐⭐⭐ 实时 | 医疗投融资、健康科技、AI医疗 |
| **雷锋网** | https://www.leiphone.com/feed | 70% | ⭐⭐⭐⭐⭐ 每日 | AI、医疗机器人、技术创新 |
| **虎嗅网** | https://www.huxiu.com/rss/0.xml | 20% | ⭐⭐⭐⭐ 每日 | 行业趋势、深度分析 |
### ⚠️ 可选(需要特殊处理)
| 来源 | URL | 状态 | 说明 |
|------|-----|------|------|
| 丁香园 | https://www.dxy.cn/bbs/newweb/pc/rss/1 | 可访问但格式异常 | XML解析错误 |
| 生物谷 | http://www.bioon.com/rss/nrss.xml | 可访问 | 需要验证内容更新频率 |
### ❌ 不推荐(问题源)
| 来源 | 问题 | 建议 |
|------|------|------|
| 新华网健康 | RSS feed内容陈旧(2013年) | 移除 |
| 人民网健康 | 404错误 | 移除 |
| 网易健康 | 404错误 | 移除 |
| 健康界 | 404错误 | 移除 |
| 医学界 | 404错误 | 移除 |
---
## 🎯 推荐的爬虫配置
基于测试结果,以下是优化后的配置:
```javascript
// RSS源配置 - 仅包含真实可用且内容最新的源
this.sources = [
// === 核心科技媒体(强烈推荐)===
{
name: '36氪',
url: 'https://36kr.com/feed',
category: 'investment',
keywords: ['医疗', '健康', '机器人', '融资', '投资', '康养', '养老', '数字健康', '医药', 'AI'],
priority: 'high',
region: '全国'
},
{
name: '雷锋网',
url: 'https://www.leiphone.com/feed',
category: 'tech-innovation',
keywords: ['AI', '医疗', '机器人', '智能', '算法', '诊断', '技术', '创新', '健康'],
priority: 'high',
region: '全国'
},
{
name: '虎嗅网',
url: 'https://www.huxiu.com/rss/0.xml',
category: 'industry-trends',
keywords: ['医疗', '健康', '智慧', '数字化', '机器人', '康养', '产业', '趋势', 'AI'],
priority: 'high',
region: '全国'
},
// === 补充科技媒体(扩展覆盖)===
{
name: '钛媒体',
url: 'https://www.tmtpost.com/rss.xml',
category: 'tech-innovation',
keywords: ['医疗', '健康', '科技', '创新', 'AI', '智能', '数字'],
priority: 'medium',
region: '全国'
},
{
name: 'CSDN',
url: 'https://blog.csdn.net/rss/home',
category: 'tech-innovation',
keywords: ['医疗', 'AI', '算法', '机器学习', '深度学习', '计算机视觉'],
priority: 'medium',
region: '全国'
},
// === 垂直医疗科技(可选,需验证)===
{
name: '生物谷',
url: 'http://www.bioon.com/rss/nrss.xml',
category: 'tech-innovation',
keywords: ['生物技术', '基因', '细胞', '免疫', '新药', '研究', '临床', '医疗'],
priority: 'medium',
region: '全国'
}
];
```
---
## 📊 实测数据(2025-11-18 抓取)
### 抓取成功统计
```
总计: 64条真实最新新闻
- 36氪: 41条 ✅ (医疗投融资、AI、科技)
- 雷锋网: 21条 ✅ (AI医疗、机器人、技术)
- 虎嗅网: 2条 ✅ (行业分析)
```
### 示例新闻标题(真实)
#### 36氪最新:
- "Databricks正洽谈新一轮融资,估值有望超1300亿美元"
- "为了让你吃上现炒,老乡鸡们开始用机器人颠勺了"
- "美银称AI有助提高员工生产率,带来更多收入"
- "资管巨头操作现分歧,海外AI泡沫之辩白热化"
#### 雷锋网最新:
- "中国 AI 正处于反超美国的前夜"
- "蚂蚁灵光来了:30秒生应用 让复杂变简单"
- "老板在南极潇洒,员工在公司加班!俞敏洪发全员信被员工吐槽"
---
## 🔍 为什么选择科技媒体?
### 优势
1. ✅ **更新及时** - 每日多次更新
2. ✅ **覆盖全面** - 涵盖医疗科技、AI健康、机器人、投融资
3. ✅ **质量高** - 深度报道、专业分析
4. ✅ **相关性强** - 聚焦前沿技术和创新
5. ✅ **RSS稳定** - 格式规范,易于解析
### 覆盖主题
- 🏥 医疗机器人(手术、康复、护理)
- 🤖 AI医疗(诊断、影像、药物研发)
- 👴 智慧养老(养老机器人、健康监测)
- 💰 医疗投融资(创业公司、融资动态)
- 🔬 生物技术(基因、细胞、新药)
- 📊 行业趋势(市场分析、政策解读)
---
## 🚀 使用建议
### 立即可用配置
```bash
# 1. 清理旧数据
rm data/news.db
# 2. 使用优化配置运行爬虫
npm run crawler
# 3. 导出数据
npm run export
# 4. 验证结果
sqlite3 data/news.db "SELECT COUNT(*), source FROM news GROUP BY source;"
```
### 定期维护
```bash
# 每30分钟自动抓取(推荐)
./start-news-service.sh
# 或手动运行
./verify-news.sh # 选择选项3
```
---
## 📝 注意事项
### 关键词优化
当前关键词配置已优化,可以有效过滤医疗健康相关内容:
```javascript
keywords: [
'医疗', '健康', '机器人', 'AI', '智能', '诊断',
'技术', '创新', '融资', '投资', '康养', '养老',
'数字健康', '医药', '算法'
]
```
### RSS源扩展
如果需要添加新的RSS源,建议:
1. ✅ 先使用 `test-rss-sources.js` 测试可用性
2. ✅ 检查内容时效性(不超过7天)
3. ✅ 验证医疗相关度(≥20%)
4. ✅ 确认RSS格式正确(XML有效)
---
## 🎉 总结
**最佳配置**: 仅使用 **36氪 + 雷锋网 + 虎嗅网**
- 简单可靠
- 内容最新
- 覆盖全面
- 维护成本低
这三个源就能满足康养产业、医疗机器人、AI健康等所有主题需求!
+95
View File
@@ -23,6 +23,101 @@ check_node_version() {
echo -e "${GREEN}✅ Node.js 版本检查通过${NC}"
}
# 检查并处理端口冲突
check_and_handle_ports() {
echo "🔍 检查端口占用情况..."
PORTS_TO_CHECK=(5173 3001)
CONFLICTS_FOUND=false
for PORT in "${PORTS_TO_CHECK[@]}"; do
PIDS=$(lsof -ti:$PORT 2>/dev/null)
if [ ! -z "$PIDS" ]; then
CONFLICTS_FOUND=true
echo -e "${YELLOW}⚠️ 端口 $PORT 被占用${NC}"
# 显示占用进程信息
for PID in $PIDS; do
PROCESS_INFO=$(ps -p $PID -o comm= 2>/dev/null || echo "未知进程")
echo " - PID: $PID, 进程: $PROCESS_INFO"
done
fi
done
if [ "$CONFLICTS_FOUND" = true ]; then
echo ""
echo -e "${YELLOW}发现端口冲突。需要停止这些进程才能继续部署。${NC}"
echo ""
echo "选择处理方式:"
echo "1) 自动停止冲突进程 (推荐)"
echo "2) 手动处理后继续"
echo "3) 退出部署"
read -p "请输入选项 (1/2/3): " port_choice
case $port_choice in
1)
echo "🛑 停止冲突进程..."
for PORT in "${PORTS_TO_CHECK[@]}"; do
PIDS=$(lsof -ti:$PORT 2>/dev/null)
if [ ! -z "$PIDS" ]; then
for PID in $PIDS; do
echo " 停止 PID $PID (端口 $PORT)..."
kill -15 $PID 2>/dev/null || kill -9 $PID 2>/dev/null
done
fi
done
# 等待进程完全停止
sleep 2
# 再次检查
STILL_OCCUPIED=false
for PORT in "${PORTS_TO_CHECK[@]}"; do
if lsof -ti:$PORT &>/dev/null; then
STILL_OCCUPIED=true
echo -e "${RED}❌ 端口 $PORT 仍被占用${NC}"
fi
done
if [ "$STILL_OCCUPIED" = true ]; then
echo -e "${RED}❌ 无法自动停止所有冲突进程,请手动处理${NC}"
exit 1
fi
echo -e "${GREEN}✅ 端口冲突已解决${NC}"
;;
2)
echo -e "${YELLOW}请手动停止冲突进程后按回车继续...${NC}"
read -p ""
# 再次检查
STILL_OCCUPIED=false
for PORT in "${PORTS_TO_CHECK[@]}"; do
if lsof -ti:$PORT &>/dev/null; then
STILL_OCCUPIED=true
echo -e "${RED}❌ 端口 $PORT 仍被占用${NC}"
fi
done
if [ "$STILL_OCCUPIED" = true ]; then
echo -e "${RED}❌ 仍存在端口冲突,部署终止${NC}"
exit 1
fi
;;
3)
echo -e "${YELLOW}部署已取消${NC}"
exit 0
;;
*)
echo -e "${RED}❌ 无效选项,部署终止${NC}"
exit 1
;;
esac
else
echo -e "${GREEN}✅ 端口检查通过,无冲突${NC}"
fi
}
# 拉取最新代码
pull_latest_code() {
echo "📥 拉取最新代码..."
+2 -2
View File
@@ -4,7 +4,7 @@ services:
web:
build: .
ports:
- "5173:3000" # Map external port 5173 to internal port 3000
- "30080:3000" # Map external port 5173 to internal port 3000
environment:
- NODE_ENV=production
restart: unless-stopped
@@ -14,4 +14,4 @@ services:
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
start_period: 40s
+254
View File
@@ -0,0 +1,254 @@
# Grafana 远程访问配置指南
## 问题诊断
当前状态:
- ✅ Grafana 已部署在 `observability` namespace
- ✅ Ingress 已配置(`monitor.omniverseai.net`)
- ✅ DNS 解析正确(指向 `85.239.233.16`)
- ❌ Ingress Controller Service 类型为 ClusterIP(只能集群内访问)
## 解决方案选择
### 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| LoadBalancer | 配置简单,原生支持 | 需要云环境或 MetalLB | 云服务商 K8s 或有 LB 支持 |
| NodePort | 简单直接 | 使用非标准端口,需防火墙配置 | 本地或私有集群 |
| 外部 Nginx | 灵活,可复用现有基础设施 | 配置复杂,多一层代理 | 已有外部代理服务器 |
## 方案 1:LoadBalancer(推荐)
### 前置条件检查
```bash
# 检查是否支持 LoadBalancer
kubectl get svc -A | grep LoadBalancer
# 如果有 MetalLB
kubectl get pods -n metallb-system
```
### 部署步骤
1. 应用 LoadBalancer 配置:
```bash
kubectl apply -f k8s/ingress-nginx-loadbalancer.yaml
```
2. 等待 External IP 分配:
```bash
kubectl get svc ingress-nginx-controller -n infra -w
```
3. 更新 DNS 记录:
```bash
# 将 monitor.omniverseai.net 的 A 记录指向分配的 External IP
```
4. 验证访问:
```bash
curl -I https://monitor.omniverseai.net
```
## 方案 2:NodePort
### 部署步骤
1. 应用 NodePort 配置:
```bash
kubectl apply -f k8s/ingress-nginx-nodeport.yaml
```
2. 查看分配的端口:
```bash
kubectl get svc ingress-nginx-controller -n infra
# 会显示类似:80:30080/TCP,443:30443/TCP
```
3. 获取任意节点 IP:
```bash
kubectl get nodes -o wide
```
4. 配置防火墙(如果有):
```bash
# 允许 30080 和 30443 端口
sudo ufw allow 30080/tcp
sudo ufw allow 30443/tcp
```
5. 访问方式:
```bash
# HTTP
http://<NODE_IP>:30080
# HTTPS
https://<NODE_IP>:30443
```
6. (可选)配置外部 Nginx 转发到标准端口 - 见方案 3
## 方案 3:外部 Nginx 反向代理
### 前置条件
1. 首先应用 NodePort 配置(见方案 2 步骤 1-3)
2. 在外部 nginx 服务器上操作:
### 部署步骤
1. SSH 到外部服务器(85.239.233.16):
```bash
ssh user@85.239.233.16
```
2. 获取 k8s 节点 IP:
```bash
# 在 k8s 管理机上运行
kubectl get nodes -o wide
# 记录 INTERNAL-IP 或 EXTERNAL-IP
```
3. 创建 Nginx 配置:
```bash
# 复制 k8s/external-nginx-config.conf 到服务器
sudo nano /etc/nginx/sites-available/grafana-proxy.conf
# 替换配置文件中的 <NODE_IP> 为实际节点 IP
# 例如:server 192.168.1.100:30080;
```
4. 启用配置:
```bash
sudo ln -s /etc/nginx/sites-available/grafana-proxy.conf /etc/nginx/sites-enabled/
```
5. 测试配置:
```bash
sudo nginx -t
```
6. 重载 Nginx:
```bash
sudo systemctl reload nginx
```
7. 配置 SSL(如果还没有):
```bash
sudo certbot --nginx -d monitor.omniverseai.net
```
## 快速测试
### 本地端口转发(临时测试)
如果需要临时访问 Grafana:
```bash
# 转发到本地 8080 端口
kubectl port-forward -n observability svc/monitoring-grafana 8080:80
# 然后访问 http://localhost:8080
```
### 验证 Ingress 配置
```bash
# 检查 Ingress 状态
kubectl get ingress -n observability monitoring-grafana
# 查看 Ingress 详细信息
kubectl describe ingress -n observability monitoring-grafana
# 检查证书状态
kubectl get certificate -n observability monitor-tls
```
## 故障排查
### 1. 无法访问 Grafana
```bash
# 检查 Grafana Pod 状态
kubectl get pods -n observability | grep grafana
# 查看 Pod 日志
kubectl logs -n observability -l app.kubernetes.io/name=grafana
# 检查 Service
kubectl get svc -n observability monitoring-grafana
```
### 2. Ingress 不工作
```bash
# 检查 Ingress Controller 日志
kubectl logs -n infra -l app.kubernetes.io/name=ingress-nginx
# 查看 Ingress 事件
kubectl describe ingress -n observability monitoring-grafana
```
### 3. SSL 证书问题
```bash
# 检查证书状态
kubectl get certificate -n observability
# 查看 cert-manager 日志
kubectl logs -n cert-manager -l app=cert-manager
```
### 4. 外部访问测试
```bash
# 从外部测试 DNS
nslookup monitor.omniverseai.net
# 测试 HTTP/HTTPS
curl -I http://monitor.omniverseai.net
curl -I https://monitor.omniverseai.net
# 测试从集群内访问
kubectl run test-pod --rm -it --image=curlimages/curl -- sh
curl -I http://monitoring-grafana.observability.svc.cluster.local
```
## 推荐配置
根据你的环境:
1. **如果在云环境(AWS/GCP/Azure)**:使用方案 1(LoadBalancer)
2. **如果有 MetalLB**:使用方案 1(LoadBalancer)
3. **如果是本地集群且有外部服务器**:使用方案 3(NodePort + 外部 Nginx)
4. **如果是开发/测试环境**:使用方案 2(NodePort)直接访问
## 安全建议
1. **启用认证**:
```bash
# 检查 Grafana 是否配置了认证
kubectl get secret -n observability monitoring-grafana -o jsonpath='{.data.admin-password}' | base64 -d
```
2. **限制访问 IP**(在 Nginx 或防火墙层面):
```nginx
# 在 nginx 配置中添加
allow 192.168.1.0/24; # 允许的 IP 段
deny all; # 拒绝其他
```
3. **使用强密码**:
```bash
# 更改 Grafana 管理员密码
kubectl exec -n observability -it <grafana-pod> -- grafana-cli admin reset-admin-password <new-password>
```
4. **启用 HTTPS**(已配置 Let's Encrypt)
## 下一步
选择适合你环境的方案后,按照相应的步骤执行。如果遇到问题,参考故障排查部分。
+152
View File
@@ -0,0 +1,152 @@
#!/bin/bash
# ============================================================================
# Grafana Remote Access Diagnostic Script
# 检查当前配置并推荐最佳解决方案
# ============================================================================
set -e
echo "=========================================="
echo "Grafana 远程访问诊断工具"
echo "=========================================="
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 1. 检查 Grafana 部署状态
echo "1. 检查 Grafana 部署状态..."
if kubectl get pods -n observability | grep -q grafana; then
echo -e "${GREEN}✓${NC} Grafana Pod 运行中"
kubectl get pods -n observability | grep grafana
else
echo -e "${RED}✗${NC} Grafana Pod 未找到"
exit 1
fi
echo ""
# 2. 检查 Grafana Service
echo "2. 检查 Grafana Service..."
kubectl get svc -n observability monitoring-grafana
echo ""
# 3. 检查 Ingress 配置
echo "3. 检查 Ingress 配置..."
if kubectl get ingress -n observability monitoring-grafana &> /dev/null; then
echo -e "${GREEN}✓${NC} Ingress 已配置"
kubectl get ingress -n observability monitoring-grafana
INGRESS_HOST=$(kubectl get ingress -n observability monitoring-grafana -o jsonpath='{.spec.rules[0].host}')
echo "域名: $INGRESS_HOST"
else
echo -e "${RED}✗${NC} Ingress 未配置"
fi
echo ""
# 4. 检查 Ingress Controller
echo "4. 检查 Ingress Controller..."
if kubectl get pods -n infra | grep -q ingress-nginx-controller; then
echo -e "${GREEN}✓${NC} Ingress Controller 运行中"
kubectl get pods -n infra | grep ingress-nginx-controller
# 检查 Service 类型
SVC_TYPE=$(kubectl get svc -n infra ingress-nginx-controller -o jsonpath='{.spec.type}')
echo "Service 类型: $SVC_TYPE"
case $SVC_TYPE in
"ClusterIP")
echo -e "${RED}✗${NC} 当前为 ClusterIP,无法从外部访问"
;;
"NodePort")
echo -e "${YELLOW}!${NC} NodePort 模式"
kubectl get svc -n infra ingress-nginx-controller -o wide
;;
"LoadBalancer")
echo -e "${GREEN}✓${NC} LoadBalancer 模式"
EXTERNAL_IP=$(kubectl get svc -n infra ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "External IP: $EXTERNAL_IP"
;;
esac
else
echo -e "${RED}✗${NC} Ingress Controller 未找到"
fi
echo ""
# 5. 检查 LoadBalancer 支持
echo "5. 检查集群 LoadBalancer 支持..."
if kubectl get pods -n metallb-system &> /dev/null 2>&1; then
echo -e "${GREEN}✓${NC} MetalLB 已安装"
kubectl get pods -n metallb-system
elif kubectl get svc -A | grep -q "LoadBalancer.*pending"; then
echo -e "${YELLOW}!${NC} 集群可能不支持 LoadBalancer"
else
# 检查是否在云环境
NODES=$(kubectl get nodes -o jsonpath='{.items[*].spec.providerID}')
if [[ $NODES == *"aws"* ]] || [[ $NODES == *"gce"* ]] || [[ $NODES == *"azure"* ]]; then
echo -e "${GREEN}✓${NC} 检测到云环境,支持 LoadBalancer"
else
echo -e "${YELLOW}!${NC} 未检测到 LoadBalancer 支持(MetalLB 或云环境)"
fi
fi
echo ""
# 6. 检查节点信息
echo "6. 集群节点信息..."
kubectl get nodes -o wide
echo ""
# 7. DNS 检查
echo "7. 检查 DNS 解析..."
if [ ! -z "$INGRESS_HOST" ]; then
DNS_IP=$(nslookup $INGRESS_HOST | grep -A1 "Name:" | grep "Address:" | tail -1 | awk '{print $2}')
if [ ! -z "$DNS_IP" ]; then
echo -e "${GREEN}✓${NC} DNS 解析成功"
echo "$INGRESS_HOST -> $DNS_IP"
else
echo -e "${RED}✗${NC} DNS 解析失败"
fi
fi
echo ""
# 8. 推荐方案
echo "=========================================="
echo "推荐方案"
echo "=========================================="
if kubectl get pods -n metallb-system &> /dev/null 2>&1 || [[ $NODES == *"aws"* ]] || [[ $NODES == *"gce"* ]] || [[ $NODES == *"azure"* ]]; then
echo -e "${GREEN}推荐方案 1: LoadBalancer${NC}"
echo "你的集群支持 LoadBalancer,这是最简单的方案"
echo ""
echo "执行命令:"
echo " kubectl apply -f k8s/ingress-nginx-loadbalancer.yaml"
echo ""
elif [ "$SVC_TYPE" = "ClusterIP" ]; then
echo -e "${YELLOW}推荐方案 2: NodePort${NC}"
echo "你的集群不支持 LoadBalancer,建议使用 NodePort"
echo ""
echo "执行命令:"
echo " kubectl apply -f k8s/ingress-nginx-nodeport.yaml"
echo ""
echo "然后可以通过 <节点IP>:30080 (HTTP) 或 <节点IP>:30443 (HTTPS) 访问"
echo ""
echo -e "${YELLOW}推荐方案 3: 外部 Nginx 代理${NC}"
echo "如果你有外部服务器(如 85.239.233.16),可以配置反向代理"
echo "1. 先应用 NodePort 配置"
echo "2. 配置外部 Nginx(参考 k8s/external-nginx-config.conf)"
fi
echo ""
echo "详细步骤请查看: k8s/GRAFANA_REMOTE_ACCESS_GUIDE.md"
echo ""
# 9. 快速测试
echo "=========================================="
echo "快速测试(端口转发)"
echo "=========================================="
echo "如需立即测试 Grafana,运行:"
echo " kubectl port-forward -n observability svc/monitoring-grafana 8080:80"
echo "然后访问: http://localhost:8080"
echo ""
+75
View File
@@ -0,0 +1,75 @@
# ============================================================================
# External Nginx Configuration for Grafana
# Location: /etc/nginx/sites-available/grafana-proxy.conf on 85.239.233.16
# ============================================================================
# First, you need to expose ingress-nginx using NodePort
# Apply: kubectl apply -f k8s/ingress-nginx-nodeport.yaml
# Then configure this on your external nginx server (85.239.233.16)
upstream k8s_ingress {
# Replace <NODE_IP> with your k8s node IP(s)
# If you have multiple nodes, add them all for high availability
server <NODE_IP>:30080;
# server <NODE_IP_2>:30080;
# server <NODE_IP_3>:30080;
}
upstream k8s_ingress_ssl {
server <NODE_IP>:30443;
# server <NODE_IP_2>:30443;
# server <NODE_IP_3>:30443;
}
# HTTP Server - Redirect to HTTPS
server {
listen 80;
server_name monitor.omniverseai.net;
# Redirect all HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS Server
server {
listen 443 ssl http2;
server_name monitor.omniverseai.net;
# SSL Certificate (Let's Encrypt or your certificate)
ssl_certificate /etc/letsencrypt/live/monitor.omniverseai.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monitor.omniverseai.net/privkey.pem;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Logging
access_log /var/log/nginx/grafana-access.log;
error_log /var/log/nginx/grafana-error.log;
# Proxy to Kubernetes Ingress Controller
location / {
proxy_pass https://k8s_ingress_ssl;
# Preserve host header
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (important for Grafana live updates)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# SSL verification (disable if using self-signed certs in k8s)
proxy_ssl_verify off;
}
}
+41
View File
@@ -0,0 +1,41 @@
# ============================================================================
# Patch Ingress Nginx Controller to LoadBalancer
# This allows external access to the ingress controller
# ============================================================================
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller
namespace: infra
annotations:
meta.helm.sh/release-name: ingress-nginx
meta.helm.sh/release-namespace: infra
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.14.0
helm.sh/chart: ingress-nginx-4.14.0
spec:
type: LoadBalancer
# Optional: Specify external IP if you have a static IP
# externalIPs:
# - 85.239.233.16
ports:
- appProtocol: http
name: http
port: 80
protocol: TCP
targetPort: http
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
+40
View File
@@ -0,0 +1,40 @@
# ============================================================================
# Patch Ingress Nginx Controller to NodePort
# Access via <node-ip>:<nodePort>
# ============================================================================
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller
namespace: infra
annotations:
meta.helm.sh/release-name: ingress-nginx
meta.helm.sh/release-namespace: infra
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.14.0
helm.sh/chart: ingress-nginx-4.14.0
spec:
type: NodePort
ports:
- appProtocol: http
name: http
port: 80
protocol: TCP
targetPort: http
nodePort: 30080 # You can change this to any available port in 30000-32767
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
nodePort: 30443 # You can change this to any available port in 30000-32767
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
+135
View File
@@ -0,0 +1,135 @@
# ============================================================================
# OmniVerse Website - Kubernetes Deployment
# Namespace: website
# Domain: www.omniverseai.net
# ============================================================================
---
# Namespace
apiVersion: v1
kind: Namespace
metadata:
name: website
labels:
name: website
environment: production
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: website
namespace: website
labels:
app: website
environment: production
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: website
template:
metadata:
labels:
app: website
environment: production
spec:
containers:
- name: website
image: omniverse-website:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: NODE_ENV
value: "production"
---
# Service
apiVersion: v1
kind: Service
metadata:
name: website
namespace: website
labels:
app: website
spec:
type: ClusterIP
selector:
app: website
ports:
- name: http
port: 80
targetPort: 3000
protocol: TCP
---
# Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: website
namespace: website
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
# Cloudflare 已经处理 SSL,所以这里不需要 SSL 重定向
spec:
ingressClassName: nginx
rules:
- host: www.omniverseai.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: website
port:
number: 80
- host: omniverseai.net
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: website
port:
number: 80
+4 -1
View File
@@ -25,4 +25,7 @@ JUHE_KEY=
# Logging
LOG_LEVEL=info
LOG_FILE=./logs/app.log
LOG_FILE=./logs/app.log
# Export Settings
AUTO_EXPORT_JSON=true
Binary file not shown.
+132
View File
@@ -0,0 +1,132 @@
const axios = require('axios');
const Parser = require('rss-parser');
const parser = new Parser({
timeout: 10000,
maxRedirects: 5
});
// 权威医疗健康RSS源候选列表
const candidateSources = [
// === 政府/官方媒体 ===
{ name: '新华网健康', url: 'http://www.xinhuanet.com/health/news_health.xml', category: 'healthcare-policy' },
{ name: '人民网健康', url: 'http://health.people.com.cn/rss/health.xml', category: 'healthcare-policy' },
{ name: '央广网健康', url: 'http://health.cnr.cn/rss/health.xml', category: 'healthcare-policy' },
// === 综合科技媒体(医疗科技版块) ===
{ name: '36氪', url: 'https://36kr.com/feed', category: 'investment' },
{ name: '雷锋网', url: 'https://www.leiphone.com/feed', category: 'tech-innovation' },
{ name: '虎嗅网', url: 'https://www.huxiu.com/rss/0.xml', category: 'industry-trends' },
{ name: 'TechCrunch中国', url: 'https://techcrunch.cn/feed/', category: 'tech-innovation' },
// === 医疗专业媒体 ===
{ name: '丁香园', url: 'https://www.dxy.cn/bbs/newweb/pc/rss/1', category: 'healthcare-policy' },
{ name: '医学界', url: 'https://www.yxj.org.cn/rss.xml', category: 'healthcare-policy' },
{ name: '健康界', url: 'https://www.cn-healthcare.com/rss', category: 'healthcare-policy' },
// === 门户网站健康频道 ===
{ name: '新浪医药', url: 'http://rss.sina.com.cn/news/marquee/ddt.xml', category: 'healthcare-policy' },
{ name: '网易健康', url: 'http://news.163.com/special/00011K6L/rss_health.xml', category: 'healthcare-policy' },
{ name: '腾讯健康', url: 'https://health.qq.com/rss_health.xml', category: 'healthcare-policy' },
// === 科学/技术媒体 ===
{ name: '果壳网', url: 'https://www.guokr.com/rss/', category: 'tech-innovation' },
{ name: '科学网', url: 'http://news.sciencenet.cn/xml/news.aspx', category: 'tech-innovation' },
// === 国际医疗健康(可选,需要翻墙) ===
{ name: 'MedicalNewsToday', url: 'https://www.medicalnewstoday.com/rss', category: 'tech-innovation' },
{ name: 'HealthLine', url: 'https://www.healthline.com/feeds/rss', category: 'healthcare-policy' },
];
async function testAndParseRSS(source) {
try {
console.log(`\n测试: ${source.name}...`);
const feed = await parser.parseURL(source.url);
if (!feed.items || feed.items.length === 0) {
console.log(` ⚠️ ${source.name}: RSS格式正确但无内容`);
return null;
}
// 检查是否有医疗相关关键词
const medicalKeywords = ['医疗', '健康', '医院', '医生', '疾病', '药品', '治疗', '康复', '养老', '护理',
'health', 'medical', 'hospital', 'disease', 'treatment', 'care', 'robot', 'AI'];
const firstFewItems = feed.items.slice(0, 10);
const relevantCount = firstFewItems.filter(item => {
const text = (item.title + ' ' + (item.contentSnippet || '')).toLowerCase();
return medicalKeywords.some(kw => text.includes(kw.toLowerCase()));
}).length;
const relevanceScore = (relevantCount / firstFewItems.length * 100).toFixed(0);
console.log(` ✅ ${source.name}:`);
console.log(` • 文章数: ${feed.items.length}`);
console.log(` • 最新: ${feed.items[0]?.title?.substring(0, 50)}...`);
console.log(` • 医疗相关度: ${relevanceScore}% (${relevantCount}/${firstFewItems.length})`);
return {
...source,
success: true,
itemCount: feed.items.length,
latestTitle: feed.items[0]?.title,
relevanceScore: parseInt(relevanceScore),
sampleTitles: feed.items.slice(0, 3).map(i => i.title)
};
} catch (error) {
const msg = error.code === 'ENOTFOUND' ? '域名无法解析' :
error.code === 'ETIMEDOUT' ? '连接超时' :
error.message.includes('Invalid XML') ? 'XML格式错误' :
error.message;
console.log(` ❌ ${source.name}: ${msg}`);
return null;
}
}
async function main() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🔍 搜索可用的真实医疗健康RSS源');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
const results = [];
for (const source of candidateSources) {
const result = await testAndParseRSS(source);
if (result) {
results.push(result);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`\n✅ 找到 ${results.length} 个可用RSS源\n`);
// 按相关度排序
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
console.log('推荐使用的RSS源 (按医疗相关度排序):\n');
results.forEach((r, i) => {
console.log(`${i + 1}. ${r.name.padEnd(20)} [相关度: ${r.relevanceScore}%] [分类: ${r.category}]`);
console.log(` URL: ${r.url}`);
console.log(` 示例: ${r.sampleTitles[0]?.substring(0, 60)}...`);
console.log();
});
// 生成配置代码
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('建议的爬虫配置代码:\n');
console.log('this.sources = [');
results.forEach(r => {
console.log(` {`);
console.log(` name: '${r.name}',`);
console.log(` url: '${r.url}',`);
console.log(` category: '${r.category}',`);
console.log(` keywords: ['医疗', '健康', '机器人', 'AI', '智能', '养老', '康复'],`);
console.log(` priority: 'high',`);
console.log(` region: '全国'`);
console.log(` },`);
});
console.log('];');
}
main().catch(console.error);
+2
View File
@@ -7,6 +7,8 @@
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"crawler": "node src/crawler.js",
"export": "node src/utils/export-to-json.js",
"crawl-and-export": "node src/crawler.js && node src/utils/export-to-json.js",
"test": "jest"
},
"keywords": [
+33 -125
View File
@@ -13,150 +13,50 @@ class RSSCrawler {
this.db = database;
this.logger = logger;
// RSS源配置
// RSS源配置 - 仅包含真实可用且内容最新的权威源(已验证 2025-11-18)
this.sources = [
{
name: '动脉网',
url: 'https://vcbeat.top/rss',
category: 'medical-robotics',
keywords: ['医疗', '机器人', '健康', 'AI', '智能', '诊断', '手术', '康复', '辅助'],
priority: 'high',
region: '全国'
},
// === 核心科技媒体(强烈推荐,已验证可用且内容最新) ===
{
name: '36氪',
url: 'https://36kr.com/feed',
category: 'investment',
keywords: ['医疗', '健康', '机器人', '融资', '投资', '康养', '养老', '数字健康', '医药'],
priority: 'medium',
region: '全国'
},
{
name: '亿欧网',
url: 'https://www.iyiou.com/rss',
category: 'industry-trends',
keywords: ['医疗', '健康', '智慧', '数字化', '机器人', '康养', '产业', '趋势'],
priority: 'medium',
keywords: ['医疗', '健康', '机器人', '融资', '投资', '康养', '养老', '数字健康', '医药', 'AI', '智能'],
priority: 'high',
region: '全国'
},
{
name: '雷锋网',
url: 'https://www.leiphone.com/feed',
category: 'tech-innovation',
keywords: ['AI', '医疗', '机器人', '智能', '算法', '诊断', '技术', '创新'],
keywords: ['AI', '医疗', '机器人', '智能', '算法', '诊断', '技术', '创新', '健康', '养老', '康复'],
priority: 'high',
region: '全国'
},
{
name: '机器之心',
url: 'https://www.jiqizhixin.com/rss',
name: '虎嗅网',
url: 'https://www.huxiu.com/rss/0.xml',
category: 'industry-trends',
keywords: ['医疗', '健康', '智慧', '数字化', '机器人', '康养', '产业', '趋势', 'AI', '养老'],
priority: 'high',
region: '全国'
},
// === 补充科技媒体(可选,扩展覆盖) ===
{
name: '钛媒体',
url: 'https://www.tmtpost.com/rss.xml',
category: 'tech-innovation',
keywords: ['医疗', '机器人', 'AI', '深度学习', '算法', '人工智能'],
priority: 'high',
region: '全国'
},
{
name: '健康界',
url: 'https://www.cn-healthcare.com/rss',
category: 'healthcare-policy',
keywords: ['政策', '医保', '卫健委', '医疗', '健康', '监管', '法规', '标准'],
priority: 'high',
region: '全国'
},
{
name: '丁香园',
url: 'https://www.dxy.cn/rss',
category: 'healthcare-policy',
keywords: ['医疗', '政策', '监管', '医药', '临床', '医院', '医生'],
priority: 'medium',
region: '全国'
},
{
name: '医学界',
url: 'https://www.yxj.org.cn/rss',
category: 'healthcare-policy',
keywords: ['医学', '政策', '临床', '研究', '医疗', '创新', '标准'],
priority: 'medium',
region: '全国'
},
// 新增医疗健康RSS源
{
name: '医谷',
url: 'https://www.yigoonet.com/feed',
category: 'medical-robotics',
keywords: ['医疗器械', '设备', '医疗', '健康', '诊断', '治疗', '康复', '机器人'],
priority: 'high',
region: '全国'
},
{
name: '器械之家',
url: 'https://www.qxzjia.com/feed',
category: 'medical-robotics',
keywords: ['医疗器械', '设备', '机器人', '手术', '诊断', '康复', '智能'],
priority: 'high',
region: '全国'
},
{
name: '医药网',
url: 'https://www.pharmnet.com.cn/rss',
category: 'healthcare-policy',
keywords: ['医药', '药品', '监管', '政策', '新药', '审批', '临床', '研发'],
priority: 'medium',
region: '全国'
},
{
name: 'CPhI制药在线',
url: 'https://www.cphi.cn/rss',
category: 'tech-innovation',
keywords: ['制药', '生物技术', '新药', '研发', '创新', '生物医药'],
keywords: ['医疗', '健康', '科技', '创新', 'AI', '智能', '数字', '机器人', '养老'],
priority: 'medium',
region: '全国'
},
// === 垂直医疗科技(备选,需验证内容时效性) ===
{
name: '生物谷',
url: 'https://www.bioon.com/rss',
url: 'http://www.bioon.com/rss/nrss.xml',
category: 'tech-innovation',
keywords: ['生物技术', '基因', '细胞', '免疫', '新药', '研究', '临床'],
priority: 'high',
region: '全国'
},
{
name: '康复医学网',
url: 'https://www.kfyx.cn/rss',
category: 'medical-robotics',
keywords: ['康复', '理疗', '康复机器人', '辅助', '训练', '治疗'],
priority: 'medium',
region: '全国'
},
{
name: '养老网',
url: 'https://www.yanglao.com.cn/rss',
category: 'smart-elderly-care',
keywords: ['养老', '老年', '护理', '智慧养老', '康养', '老龄化'],
priority: 'medium',
region: '全国'
},
{
name: '医信邦',
url: 'https://www.yixinbang.com/rss',
category: 'tech-innovation',
keywords: ['数字医疗', '互联网医疗', '远程', 'AI', '大数据', '智慧医院'],
priority: 'high',
region: '全国'
},
{
name: '医疗器械创新网',
url: 'https://www.md-innovation.com/rss',
category: 'medical-robotics',
keywords: ['创新', '器械', '设备', '技术', '研发', '突破'],
priority: 'medium',
region: '全国'
},
{
name: '中国卫生人才网',
url: 'https://www.21wecan.com/rss',
category: 'healthcare-policy',
keywords: ['卫生', '人才', '政策', '培训', '医疗', '管理'],
keywords: ['生物技术', '基因', '细胞', '免疫', '新药', '研究', '临床', '医疗', '健康'],
priority: 'low',
region: '全国'
}
@@ -165,10 +65,18 @@ class RSSCrawler {
// 备用RSS源(如果主要源不可用)
this.fallbackSources = [
{
name: '科技日报',
url: 'http://digitalpaper.stdaily.com/http_www.kjrb.com/kjrb/rss/rss.xml',
name: '人民网科技',
url: 'http://scitech.people.com.cn/rss/it.xml',
category: 'tech-innovation',
keywords: ['医疗', '健康', '机器人', '科技'],
keywords: ['医疗', '健康', '机器人', '科技', 'AI'],
priority: 'medium',
region: '全国'
},
{
name: '中国新闻网健康',
url: 'http://www.chinanews.com/rss/health.xml',
category: 'healthcare-policy',
keywords: ['医疗', '健康', '政策', '医院'],
priority: 'medium',
region: '全国'
}
+15 -2
View File
@@ -268,11 +268,24 @@ class NewsServer {
async runCrawl() {
try {
const results = await this.crawler.crawlAll();
// 清除相关缓存
this.cache.flushAll();
this.logger.info(`Crawl completed: ${results.length} items processed`);
// 自动导出为 JSON 文件供前端使用
if (process.env.AUTO_EXPORT_JSON === 'true') {
try {
const NewsExporter = require('./utils/export-to-json');
const exporter = new NewsExporter();
await exporter.export();
this.logger.info('News data exported to JSON successfully');
} catch (exportError) {
this.logger.error('Failed to export news to JSON:', exportError);
}
}
return results;
} catch (error) {
this.logger.error('Crawl failed:', error);
@@ -0,0 +1,83 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const Database = require('../models/database');
require('dotenv').config();
/**
* 将数据库中的新闻数据导出为 JSON 文件
* 供前端静态站点使用
*/
class NewsExporter {
constructor() {
this.db = new Database(process.env.DATABASE_PATH || './data/news.db');
this.outputPath = path.resolve(__dirname, '../../../public/data/news.json');
}
async export() {
try {
console.log('🚀 Starting news export...');
// 获取所有新闻数据
const allNews = await this.db.getNews({
page: 1,
limit: 1000, // 导出最近 1000 条
sortBy: 'published_at',
sortOrder: 'DESC'
});
console.log(`📊 Found ${allNews.length} news items`);
// 确保输出目录存在
const outputDir = path.dirname(this.outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
console.log(`📁 Created directory: ${outputDir}`);
}
// 写入 JSON 文件
fs.writeFileSync(
this.outputPath,
JSON.stringify(allNews, null, 2),
'utf-8'
);
console.log(`✅ Successfully exported ${allNews.length} news items to ${this.outputPath}`);
// 显示分类统计
const categoryCounts = {};
allNews.forEach(news => {
categoryCounts[news.category] = (categoryCounts[news.category] || 0) + 1;
});
console.log('\n📈 Category distribution:');
Object.entries(categoryCounts).forEach(([category, count]) => {
console.log(` ${category}: ${count}`);
});
return allNews.length;
} catch (error) {
console.error('❌ Export failed:', error);
throw error;
} finally {
this.db.close();
}
}
}
// 如果直接运行此脚本
if (require.main === module) {
const exporter = new NewsExporter();
exporter.export()
.then(count => {
console.log(`\n🎉 Export completed: ${count} items`);
process.exit(0);
})
.catch(error => {
console.error('Export failed:', error);
process.exit(1);
});
}
module.exports = NewsExporter;
+81
View File
@@ -0,0 +1,81 @@
const axios = require('axios');
// 测试RSS源可访问性
const testSources = [
// 当前配置的源
{ name: '动脉网', url: 'https://vcbeat.top/rss' },
{ name: '36氪', url: 'https://36kr.com/feed' },
{ name: '亿欧网', url: 'https://www.iyiou.com/rss' },
{ name: '雷锋网', url: 'https://www.leiphone.com/feed' },
{ name: '机器之心', url: 'https://www.jiqizhixin.com/rss' },
// 真实可用的RSS源候选
{ name: '丁香园', url: 'https://www.dxy.cn/bbs/newweb/pc/rss/1' },
{ name: '新浪医药', url: 'http://rss.sina.com.cn/news/marquee/ddt.xml' },
{ name: '人民网健康', url: 'http://health.people.com.cn/rss/health.xml' },
{ name: '新华网健康', url: 'http://www.xinhuanet.com/health/news_health.xml' },
{ name: '腾讯健康', url: 'https://health.qq.com/rss_health.xml' },
{ name: '搜狐健康', url: 'http://health.sohu.com/healthfeed.xml' },
];
async function testRSSSource(source) {
try {
const response = await axios.get(source.url, {
timeout: 10000,
maxRedirects: 5,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (response.status === 200) {
const contentType = response.headers['content-type'] || '';
const dataStr = String(response.data);
const isXML = contentType.includes('xml') || dataStr.includes('<?xml');
const hasRSS = dataStr.includes('<rss') || dataStr.includes('<feed');
if (isXML || hasRSS) {
console.log(`✅ ${source.name.padEnd(15)}: 可访问 (RSS/XML格式)`);
return true;
} else {
console.log(`⚠️ ${source.name.padEnd(15)}: 可访问但格式可能不正确`);
return false;
}
}
} catch (error) {
const msg = error.code === 'ENOTFOUND' ? '域名无法解析' :
error.code === 'ETIMEDOUT' ? '连接超时' :
error.response?.status ? `HTTP ${error.response.status}` :
error.message;
console.log(`❌ ${source.name.padEnd(15)}: ${msg}`);
return false;
}
}
async function main() {
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🔍 测试RSS源可访问性');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
let successCount = 0;
const results = [];
for (const source of testSources) {
const success = await testRSSSource(source);
results.push({ ...source, success });
if (success) successCount++;
await new Promise(resolve => setTimeout(resolve, 1500));
}
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(`测试完成: ${successCount}/${testSources.length} 个源可访问\n`);
if (successCount > 0) {
console.log('✅ 可用的RSS源:');
results.filter(r => r.success).forEach(r => {
console.log(` • ${r.name}: ${r.url}`);
});
}
}
main().catch(console.error);
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
# 新闻爬虫验证和运行脚本
# 用于验证RSS源、测试爬虫、导出数据
set -e # 遇到错误立即退出
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 🔍 OmniVerse 新闻爬虫系统验证"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 检查 Node.js
echo "1️⃣ 检查 Node.js 环境..."
if ! command -v node &> /dev/null; then
echo "❌ Node.js 未安装"
exit 1
fi
echo "✅ Node.js 版本: $(node --version)"
echo ""
# 检查 news-crawler-service 目录
echo "2️⃣ 检查爬虫服务目录..."
if [ ! -d "news-crawler-service" ]; then
echo "❌ news-crawler-service 目录不存在"
exit 1
fi
echo "✅ 爬虫服务目录存在"
echo ""
# 检查依赖
echo "3️⃣ 检查 npm 依赖..."
cd news-crawler-service
if [ ! -d "node_modules" ]; then
echo "📦 安装依赖..."
npm install
else
echo "✅ 依赖已安装"
fi
echo ""
# 检查环境配置
echo "4️⃣ 检查环境配置..."
if [ ! -f ".env" ]; then
echo "⚠️ .env 文件不存在,从 .env.example 复制..."
cp .env.example .env
echo "⚙️ 请编辑 .env 文件配置必要的参数"
fi
echo "✅ 环境配置文件存在"
echo ""
# 检查数据库
echo "5️⃣ 检查数据库..."
if [ -f "data/news.db" ]; then
echo "✅ 数据库文件存在"
# 显示数据库统计
echo "📊 数据库统计:"
sqlite3 data/news.db "SELECT COUNT(*) as total FROM news;" 2>/dev/null || echo " 无法读取数据库统计"
else
echo "ℹ️ 数据库文件不存在(首次运行时会自动创建)"
fi
echo ""
# 检查 Ollama 服务(用于 AI 摘要)
echo "6️⃣ 检查 Ollama AI 服务..."
if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
echo "✅ Ollama 服务运行中"
else
echo "⚠️ Ollama 服务未运行"
echo " 提示: AI 摘要功能需要 Ollama,但不影响基本爬取功能"
fi
echo ""
# 菜单选项
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 📋 请选择操作:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 1) 运行爬虫 (抓取最新新闻)"
echo " 2) 导出数据为 JSON"
echo " 3) 爬取并导出 (推荐)"
echo " 4) 查看数据库统计"
echo " 5) 启动爬虫服务"
echo " 6) 退出"
echo ""
read -p "请输入选项 [1-6]: " choice
case $choice in
1)
echo ""
echo "🚀 开始运行爬虫..."
npm run crawler
;;
2)
echo ""
echo "📤 导出数据为 JSON..."
npm run export
;;
3)
echo ""
echo "🔄 爬取并导出..."
npm run crawl-and-export
echo ""
echo "✅ 完成! 数据已导出到 public/data/news.json"
;;
4)
echo ""
echo "📊 数据库统计信息:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ -f "data/news.db" ]; then
echo "总新闻数:"
sqlite3 data/news.db "SELECT COUNT(*) FROM news;"
echo ""
echo "各分类新闻数:"
sqlite3 data/news.db "SELECT category, COUNT(*) as count FROM news GROUP BY category ORDER BY count DESC;"
echo ""
echo "各来源新闻数:"
sqlite3 data/news.db "SELECT source, COUNT(*) as count FROM news GROUP BY source ORDER BY count DESC LIMIT 10;"
else
echo "❌ 数据库文件不存在"
fi
;;
5)
echo ""
echo "🌐 启动爬虫服务..."
echo " 访问 http://localhost:3001 查看 API"
echo " 按 Ctrl+C 停止服务"
echo ""
npm start
;;
6)
echo "👋 再见!"
cd ..
exit 0
;;
*)
echo "❌ 无效选项"
cd ..
exit 1
;;
esac
cd ..
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✅ 操作完成"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+911 -345
View File
File diff suppressed because one or more lines are too long
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
# 启动新闻爬虫自动定时服务
# 使用 PM2 进程管理器保证服务稳定运行
set -e
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 🚀 启动 OmniVerse 新闻爬虫自动定时服务"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 检查 PM2 是否安装
if ! command -v pm2 &> /dev/null; then
echo "📦 PM2 未安装,正在安装..."
npm install -g pm2
echo "✅ PM2 安装完成"
echo ""
fi
# 进入爬虫服务目录
cd news-crawler-service
# 检查依赖
if [ ! -d "node_modules" ]; then
echo "📦 安装依赖..."
npm install
echo ""
fi
# 停止已存在的服务
echo "🔍 检查现有服务..."
pm2 delete omniverse-news 2>/dev/null || true
echo ""
# 启动服务
echo "🚀 启动新闻爬虫服务..."
pm2 start src/server.js --name omniverse-news --time
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✅ 服务启动成功!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 服务信息:"
echo " • 名称: omniverse-news"
echo " • 端口: 3001"
echo " • API: http://localhost:3001"
echo " • 定时爬取: 每 30 分钟"
echo " • 自动导出: 已启用"
echo " • 日志位置: logs/app.log"
echo ""
echo "📋 常用命令:"
echo " pm2 status # 查看服务状态"
echo " pm2 logs omniverse-news # 查看实时日志"
echo " pm2 restart omniverse-news # 重启服务"
echo " pm2 stop omniverse-news # 停止服务"
echo " pm2 delete omniverse-news # 删除服务"
echo ""
echo "🔄 首次运行:"
echo " 服务将在启动后 5 秒开始首次爬取"
echo " 之后每 30 分钟自动爬取一次"
echo " 每次爬取完成后自动导出 JSON 到 public/data/news.json"
echo ""
echo "🌐 查看服务:"
pm2 status
echo ""
echo "📝 查看实时日志:"
echo " pm2 logs omniverse-news"
echo ""
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# 停止新闻爬虫服务
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 🛑 停止 OmniVerse 新闻爬虫服务"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if ! command -v pm2 &> /dev/null; then
echo "❌ PM2 未安装,无法停止服务"
exit 1
fi
pm2 stop omniverse-news 2>/dev/null && echo "✅ 服务已停止" || echo "ℹ️ 服务未运行"
pm2 status
echo ""
echo "💡 提示:"
echo " • 使用 'pm2 start omniverse-news' 重新启动"
echo " • 使用 'pm2 delete omniverse-news' 完全删除服务"
echo " • 使用 './start-news-service.sh' 重新配置并启动"
echo ""