sage/wwwroot/hotspot/fetch_now.dspy
yumoqing 71ecb8fea1 feat: add hotspot radar module (热点雷达系统)
- Add hotspot_source/hotspot_item/hotspot_analysis table definitions
- Add MySQL DDL for all three tables with indexes
- Create hotspot dashboard UI (index.ui) with stats, tabs for CRUD
- Add DSPY endpoints: stats, fetch_now (RSS/API), analyze (auto-classify)
- Register paths in load_path.py with logined access
- Add '热点雷达' menu entry
2026-08-01 10:39:52 +08:00

166 lines
5.7 KiB
Plaintext

# 从所有启用的来源抓取热点数据
import json
import xml.etree.ElementTree as ET
from datetime import datetime
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
async def fetch_rss(url):
"""解析 RSS/Atom feed"""
import aiohttp
items = []
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
if resp.status != 200:
return items
text = await resp.text()
root = ET.fromstring(text)
ns = {'atom': 'http://www.w3.org/2005/Atom'}
# RSS 2.0
for item in root.findall('.//item'):
title = item.findtext('title', '')
link = item.findtext('link', '')
desc = item.findtext('description', '')[:500]
pub_date = item.findtext('pubDate', '')
items.append({
'title': title,
'url': link,
'summary': desc,
'publish_time': pub_date,
})
# Atom
if not items:
for entry in root.findall('.//atom:entry', ns):
title = entry.findtext('atom:title', '', ns)
link_el = entry.find('atom:link', ns)
link = link_el.get('href', '') if link_el is not None else ''
summary = entry.findtext('atom:summary', '', ns) or ''
updated = entry.findtext('atom:updated', '', ns) or ''
items.append({
'title': title,
'url': link,
'summary': summary[:500],
'publish_time': updated,
})
except Exception as e:
print(f'RSS fetch error for {url}: {e}')
return items
async def fetch_api(url, method='GET', headers=None, json_path=None):
"""调用 API 端点获取热点数据"""
import aiohttp
items = []
try:
hdrs = {}
if headers:
try:
hdrs = json.loads(headers)
except:
pass
async with aiohttp.ClientSession() as session:
if method.upper() == 'GET':
async with session.get(url, headers=hdrs, timeout=aiohttp.ClientTimeout(total=30)) as resp:
if resp.status == 200:
data = await resp.json()
else:
async with session.post(url, headers=hdrs, timeout=aiohttp.ClientTimeout(total=30)) as resp:
if resp.status == 200:
data = await resp.json()
# 尝试从 JSON 中提取 items
if isinstance(data, list):
entries = data
elif isinstance(data, dict):
# 常见热点 API 格式
entries = data.get('data', data.get('items', data.get('list', [])))
else:
entries = []
for entry in entries[:50]: # limit
if isinstance(entry, dict):
title = entry.get('title', entry.get('name', ''))
url_val = entry.get('url', entry.get('link', ''))
summary = entry.get('summary', entry.get('description', entry.get('desc', '')))
heat = entry.get('heat', entry.get('hot', entry.get('score', 0)))
items.append({
'title': str(title),
'url': str(url_val),
'summary': str(summary)[:500],
'heat_score': int(heat) if heat else 0,
})
except Exception as e:
print(f'API fetch error for {url}: {e}')
return items
async def main(request):
db = DBPools()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
new_items = 0
total_sources = 0
async with db.sqlorContext('sage') as sor:
sources = await sor.R('hotspot_source', {})
for src in sources:
if src.get('enabled') != '1':
continue
total_sources += 1
src_type = src.get('type', 'rss')
src_url = src.get('url', '')
if not src_url:
continue
if src_type == 'rss':
items = await fetch_rss(src_url)
elif src_type == 'api':
fetch_method = src.get('fetch_method', 'GET')
headers = src.get('headers', '')
items = await fetch_api(src_url, fetch_method, headers)
else:
continue
# 存入数据库
async with db.sqlorContext('sage') as sor:
# 更新 last_fetch
await sor.U('hotspot_source', {
'id': src['id'],
'last_fetch': now,
})
for item in items:
# 去重:检查 URL 是否已存在
existing = await sor.sqlExe(
"select id from hotspot_item where url=${url}$",
{'url': item.get('url', '')}
)
if existing:
continue
new_id = getID()
status = 'rising' # 新抓取的默认 rising
await sor.C('hotspot_item', {
'id': new_id,
'source_id': src['id'],
'title': item.get('title', ''),
'url': item.get('url', ''),
'summary': item.get('summary', '')[:500],
'heat_score': item.get('heat_score', 100),
'publish_time': item.get('publish_time', now),
'fetch_time': now,
'status': status,
})
new_items += 1
return {
'total': total_sources,
'new_items': new_items,
'time': now,
}