feat: 军事科技每日资讯推送系统 - Docker部署 + 日志系统 + 数据目录重组
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
工具函数模块:语言检测、HTML清洗、内容提取、时间解析、AI API调用、文本换行
|
||||
"""
|
||||
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
|
||||
def is_chinese(text):
|
||||
"""判断文本是否为中文(汉字占比 > 20%)"""
|
||||
if not text:
|
||||
return True
|
||||
chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
|
||||
return chinese_chars > len(text) * 0.2
|
||||
|
||||
|
||||
def clean_html(html_text):
|
||||
"""清理HTML标签,合并空白字符"""
|
||||
if not html_text:
|
||||
return ""
|
||||
text = re.sub(r'<[^>]+>', ' ', html_text)
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def extract_content(entry):
|
||||
"""从RSS条目提取完整正文内容"""
|
||||
content = entry.get('summary', '') or entry.get('description', '')
|
||||
if not content:
|
||||
content_list = entry.get('content', [{}])
|
||||
if content_list and isinstance(content_list, list):
|
||||
content = content_list[0].get('value', '')
|
||||
return clean_html(content)
|
||||
|
||||
|
||||
def parse_pub_time(entry):
|
||||
"""解析发布时间,统一转换为北京时间(UTC+8)"""
|
||||
beijing_tz = timezone(timedelta(hours=8))
|
||||
|
||||
time_fields = ['published_parsed', 'updated_parsed', 'created_parsed']
|
||||
for field in time_fields:
|
||||
if entry.get(field):
|
||||
try:
|
||||
utc_time = datetime(*entry[field][:6], tzinfo=timezone.utc)
|
||||
return utc_time.astimezone(beijing_tz).replace(tzinfo=None)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for field in ['published', 'updated', 'created']:
|
||||
if entry.get(field):
|
||||
try:
|
||||
dt = parsedate_to_datetime(entry[field])
|
||||
if dt.tzinfo is not None:
|
||||
return dt.astimezone(beijing_tz).replace(tzinfo=None)
|
||||
return dt
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def call_ai(messages, api_key, base_url, model, temperature=0.3, max_tokens=800, purpose=None):
|
||||
"""调用 OpenAI 兼容的 Chat Completion API"""
|
||||
if not api_key:
|
||||
raise ValueError('OPENAI_API_KEY 未配置')
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
|
||||
data = {
|
||||
'model': model,
|
||||
'messages': messages,
|
||||
'temperature': temperature,
|
||||
'max_tokens': max_tokens,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f'{base_url}/chat/completions',
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=120
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()['choices'][0]['message']['content']
|
||||
_record_api_success(model, purpose, max_tokens)
|
||||
return result
|
||||
except Exception:
|
||||
_record_api_failure(model, purpose, max_tokens)
|
||||
raise
|
||||
|
||||
|
||||
def wrap_text(text, font, max_width, draw):
|
||||
"""将长文本按像素宽度自动换行,返回行列表"""
|
||||
lines = []
|
||||
current_line = ""
|
||||
|
||||
for char in text:
|
||||
test_line = current_line + char
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
line_width = bbox[2] - bbox[0]
|
||||
|
||||
if line_width <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = char
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _record_api_success(model, purpose, max_tokens):
|
||||
try:
|
||||
from .monitor import get_monitor
|
||||
get_monitor().record_api_call(model or "unknown", purpose or "unknown", max_tokens, True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _record_api_failure(model, purpose, max_tokens):
|
||||
try:
|
||||
from .monitor import get_monitor
|
||||
get_monitor().record_api_call(model or "unknown", purpose or "unknown", max_tokens, False)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user