- 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
166 lines
5.7 KiB
Plaintext
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,
|
|
}
|