""" 工具函数模块:语言检测、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