Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import random | |
| import re | |
| import time | |
| import xml.etree.ElementTree as ET | |
| from typing import List, Dict, Optional, Tuple | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| from urllib.parse import urlparse | |
| import requests | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse | |
| from openai import OpenAI | |
| from bs4 import BeautifulSoup | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # App ๅๅงๅ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app = FastAPI() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ่จญๅฎ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY") | |
| DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat") | |
| client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url="https://api.deepseek.com/v1") | |
| HEADERS = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/125.0.0.0 Safari/537.36" | |
| ), | |
| "Accept-Language": "zh-TW,zh;q=0.9,en;q=0.8", | |
| } | |
| ARTICLES_PER_SOURCE = 10 | |
| MAX_FETCH_SECONDS = 12 # ๆฏๅไพๆบๆๅค็ญๅนพ็ง | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Unsplash ้ซๅ่ณช็งๆ/่ฒก็ถๅๅบซ๏ผ้ฒๅ็จ๏ผ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| UNSPLASH_POOL = [ | |
| # ็งๆ้ก | |
| "https://images.unsplash.com/photo-1518770660439-4636190af475?w=800&q=80", # ๆถ็ | |
| "https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=800&q=80", # ๆฐ่ๆก | |
| "https://images.unsplash.com/photo-1488590528505-98d2b5aba04b?w=800&q=80", # ็จๅผ็ขผ | |
| "https://images.unsplash.com/photo-1531297484001-80022131f5a1?w=800&q=80", # ็ญ้ป | |
| "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80", # ๆธๆๅๆ | |
| "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80", # ๅ่กจๆฟ | |
| "https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&q=80", # ่ณๅฎ | |
| "https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=800&q=80", # ่พฆๅ ฌๅฎค | |
| "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80", # ๅ จ็็ถฒ่ทฏ | |
| "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800&q=80", # ็ถฒ็ตก | |
| "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=80", # ไผบๆๅจ | |
| "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=800&q=80", # ๆธไฝ่ก้ท | |
| "https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=800&q=80", # ๅทฅไฝ็ฉบ้ | |
| "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800&q=80", # ้ฒ็ซฏ | |
| "https://images.unsplash.com/photo-1526628953301-3e589a6a8b74?w=800&q=80", # ้่็งๆ | |
| "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&q=80", # AI | |
| "https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=800&q=80", # ๆๆฉ | |
| "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&q=80", # ๅๅก้ | |
| "https://images.unsplash.com/photo-1581091226825-a39f28c8ff2a?w=800&q=80", # ๅทฅ็จ | |
| "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=800&q=80", # ๆฉๅจไบบ | |
| # ่ฒก็ถ/ๅๆฅญ้ก | |
| "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800&q=80", # ่กๅธ | |
| "https://images.unsplash.com/photo-1590283603385-17ffb3a7f3f4?w=800&q=80", # ่ฒก็ถๅ่กจ | |
| "https://images.unsplash.com/photo-1553729459-afe8f2e2ed65?w=800&q=80", # ๅๆฅญๅคงๆจ | |
| "https://images.unsplash.com/photo-1450101499163-c884c5c54a85?w=800&q=80", # ๅๆฅญๅๆ | |
| # ๆ ้/็ๆดป้ก | |
| "https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=800&q=80", # ๆ ้ | |
| "https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800&q=80", # ๆตท็ | |
| "https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=800&q=80", # ๆ ่ก | |
| "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=800&q=80", # ๅฑฑๆฏ | |
| "https://images.unsplash.com/photo-1444723121867-7a241cacace9?w=800&q=80", # ๅๅธ | |
| "https://images.unsplash.com/photo-1533105079780-92b9be482077?w=800&q=80", # ็พ้ฃ | |
| "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&q=80", # ้ค้ฃฒ | |
| # ๆๆ/ๅทฅๆฅญ้ก | |
| "https://images.unsplash.com/photo-1535813547-99c4740f9fc5?w=800&q=80", # ้ๅฑฌ | |
| "https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=800&q=80", # ๅทฅๅป | |
| "https://images.unsplash.com/photo-1565122645852-3dd6403dcad2?w=800&q=80", # ๆๆ็งๅญธ | |
| "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?w=800&q=80", # ๅก่ /่ๅ็ฉ | |
| # ็งๅญธ้ก | |
| "https://images.unsplash.com/photo-1507413245164-6160d8298b31?w=800&q=80", # ็งๅญธ | |
| "https://images.unsplash.com/photo-1532094349884-543bc11b234d?w=800&q=80", # ๅฏฆ้ฉๅฎค | |
| # ๅท็ฅ่ญ/่ถฃๅณ้ก | |
| "https://images.unsplash.com/photo-1533073526757-2c8ca1df9f1c?w=800&q=80", # ๅฅฝๅฅ | |
| "https://images.unsplash.com/photo-1516410529446-2e27be6d47d6?w=800&q=80", # ็ฅ่ญ | |
| ] | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๆฐ่ไพๆบๅฎ็พฉ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # โโ ๅ็จๆธ ๅฎ๏ผ็ถ news_sources.json ็กๆณ่ฎๅๆไฝฟ็จ๏ผโโ | |
| FALLBACK_SOURCES: List[Dict] = [ | |
| {"name": "็งๆๆฐๅ ฑ", "type": "rss", "url": "https://technews.tw/feed/"}, | |
| {"name": "็ฎ็งๆ", "type": "rss", "url": "https://chinese.engadget.com/rss.xml"}, | |
| {"name": "ๆธไฝๆไปฃ", "type": "rss", "url": "https://www.bnext.com/rss"}, | |
| {"name": "Yahoo ๆฐ่", "type": "rss", "url": "https://hk.news.yahoo.com/rss/"}, | |
| {"name": "RTHK ๆฌๅฐๆฐ่", "type": "rss", "url": "https://rthk.hk/rthk/news/rss/c_expressnews_clocal.xml"}, | |
| {"name": "RTHK ๅ้ๆฐ่", "type": "rss", "url": "https://rthk.hk/rthk/news/rss/c_expressnews_cinternational.xml"}, | |
| {"name": "Gizmodo", "type": "rss", "url": "https://gizmodo.com/rss"}, | |
| {"name": "Hackaday", "type": "rss", "url": "https://hackaday.com/feed/"}, | |
| ] | |
| def load_news_sources() -> List[Dict]: | |
| """ๅพ news_sources.json ่ผๅ ฅๆฐ่ไพๆบๆธ ๅฎ๏ผๅคฑๆๆๅ้ๅฐๅ็จๆธ ๅฎใ""" | |
| import os as _os | |
| config_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "news_sources.json") | |
| try: | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| sources = json.load(f) | |
| if isinstance(sources, list) and len(sources) > 0: | |
| print(f"๐ ๆๅ่ผๅ ฅ news_sources.json๏ผ{len(sources)} ๅไพๆบ๏ผ", flush=True) | |
| return sources | |
| else: | |
| raise ValueError("news_sources.json ๆ ผๅผ็กๆ๏ผ้็บ้็ฉบ้ฃๅ๏ผ") | |
| except Exception as e: | |
| print(f"โ ๏ธ ็กๆณ่ฎๅ news_sources.json๏ผ{e}๏ผไฝฟ็จๅ็จๆธ ๅฎ๏ผ{len(FALLBACK_SOURCES)} ๅไพๆบ๏ผ", flush=True) | |
| return FALLBACK_SOURCES | |
| NEWS_SOURCES: List[Dict] = load_news_sources() | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๅ็่็่ผๅฉๅฝๅผ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def extract_image_from_html(html_text: str) -> Optional[str]: | |
| """ๅพ HTML ๅ งๅฎนไธญๆๅ็ฌฌไธๅผตๅ็็็ถฒๅใ""" | |
| if not html_text: | |
| return None | |
| # ๆนๆณไธ๏ผBeautifulSoup ่งฃๆ | |
| try: | |
| soup = BeautifulSoup(html_text, "html.parser") | |
| img = soup.find("img") | |
| if img: | |
| for attr in ["src", "data-src", "data-original"]: | |
| src = img.get(attr) | |
| if src: | |
| if src.startswith("http"): | |
| return src | |
| if src.startswith("//"): | |
| return "https:" + src | |
| except Exception: | |
| pass | |
| # ๆนๆณไบ๏ผๆญฃๅ่กจ้ๅผๅ ๅบ | |
| try: | |
| match = re.search( | |
| r'(?:src|data-src|data-original)=["\'](https?://[^"\'>]+?\.(?:jpg|jpeg|png|webp|gif)[^"\'>]*)["\']', | |
| html_text, re.IGNORECASE, | |
| ) | |
| if match: | |
| return match.group(1) | |
| except Exception: | |
| pass | |
| return None | |
| # RSS/Atom ๅธธ็จๅฝๅ็ฉบ้ | |
| NS = { | |
| "media": "http://search.yahoo.com/mrss/", | |
| "content": "http://purl.org/rss/1.0/modules/content/", | |
| "dc": "http://purl.org/dc/elements/1.1/", | |
| "atom": "http://www.w3.org/2005/Atom", | |
| } | |
| def extract_image_from_rss_item(item: ET.Element) -> Optional[str]: | |
| """ๅพ RSS/Atom item ๅ ็ด ไธญๆๅๅ็็ถฒๅ๏ผไฝฟ็จ ElementTree๏ผใ""" | |
| # 1) media:content / media:thumbnail | |
| for tag in ["{http://search.yahoo.com/mrss/}content", | |
| "{http://search.yahoo.com/mrss/}thumbnail"]: | |
| media_el = item.find(tag) | |
| if media_el is not None: | |
| url = media_el.get("url", "") | |
| if url.startswith("http"): | |
| return url | |
| # ไนๅฏ่ฝๅจๆๆๅพไปฃไธญ | |
| for el in item.iter(tag): | |
| url = el.get("url", "") | |
| if url.startswith("http"): | |
| return url | |
| # 2) enclosure (RSS 2.0) | |
| for enc in item.findall("enclosure"): | |
| url = enc.get("url", "") | |
| mime = enc.get("type", "") | |
| if url.startswith("http"): | |
| if "image" in mime or any( | |
| url.lower().endswith(ext) for ext in (".jpg", ".jpeg", ".png", ".webp", ".gif") | |
| ): | |
| return url | |
| # 3) content:encoded ่ฃก็ <img> | |
| content_el = item.find("{http://purl.org/rss/1.0/modules/content/}encoded") | |
| if content_el is not None and content_el.text: | |
| img = extract_image_from_html(content_el.text) | |
| if img: | |
| return img | |
| # 4) description ่ฃก็ <img> | |
| desc_el = item.find("description") | |
| if desc_el is not None and desc_el.text: | |
| img = extract_image_from_html(desc_el.text) | |
| if img: | |
| return img | |
| # 5) Atom: content ่ฃก็ <img> | |
| for tag in ["content", "{http://www.w3.org/2005/Atom}content"]: | |
| atom_content = item.find(tag) | |
| if atom_content is not None and atom_content.text: | |
| img = extract_image_from_html(atom_content.text) | |
| if img: | |
| return img | |
| return None | |
| def get_entry_text(item: ET.Element, *tags: str) -> str: | |
| """ๅพ RSS/Atom item ไธญๅๅพๆๅญๅ งๅฎน๏ผไพๅบๅ่ฉฆๅคๅ tag๏ผๅซๅฝๅ็ฉบ้่ฎ้ซ๏ผใ""" | |
| for tag in tags: | |
| # ๅ่ฉฆ็ด tag ๅ็จฑ | |
| el = item.find(tag) | |
| if el is not None and el.text: | |
| return el.text.strip() | |
| # ๅ่ฉฆๅธถ Atom ๅฝๅ็ฉบ้ | |
| el = item.find(f"{{http://www.w3.org/2005/Atom}}{tag}") | |
| if el is not None and el.text: | |
| return el.text.strip() | |
| return "" | |
| def get_fallback_image(title: str = "") -> str: | |
| """ๆ นๆๆฐ่้้ตๅญๅๅณ็ธ้ Unsplash ๅ็๏ผ็กๅน้ ๅ้จๆฉๆ้ธใ""" | |
| kw = title.lower() if title else "" | |
| mapping = [ | |
| # ็งๆ | |
| (["ai", "ไบบๅทฅๆบๆ ง", "artificial intelligence", "gpt", "llm", "ๆทฑๅบฆๅญธ็ฟ", "machine learning"], | |
| "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&q=80"), | |
| (["ๆๆฉ", "phone", "iphone", "android", "samsung", "mobile"], | |
| "https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=800&q=80"), | |
| (["ๆถ็", "chip", "ๅๅฐ้ซ", "semiconductor", "nvidia", "intel", "amd", "tsmc", "ๅฐ็ฉ้ป"], | |
| "https://images.unsplash.com/photo-1518770660439-4636190af475?w=800&q=80"), | |
| (["่ณๅฎ", "ๅฎๅ จ", "security", "cyber", "hack", "ๆผๆด"], | |
| "https://images.unsplash.com/photo-1550751827-4bd374c3f58b?w=800&q=80"), | |
| (["้ฒ็ซฏ", "cloud", "aws", "azure", "gcp"], | |
| "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80"), | |
| (["ๆธๆ", "data", "big data", "ๅๆ", "analytics"], | |
| "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=80"), | |
| (["ๆฉๅจไบบ", "robot", "automation", "่ชๅๅ"], | |
| "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?w=800&q=80"), | |
| (["้ปๅ่ป", "ev", "tesla", "ๆฑฝ่ป", "vehicle", "่ช้ง"], | |
| "https://images.unsplash.com/photo-1593941707882-a5bba14938c7?w=800&q=80"), | |
| (["ๅๅก้", "blockchain", "crypto", "ๅ ๅฏ", "bitcoin", "web3"], | |
| "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&q=80"), | |
| # ่ฒก็ถ | |
| (["้่", "่ฒก็ถ", "finance", "fintech", "bank", "ๆ่ณ", "่ก็ฅจ", "่กๅธ", "ๅบ้", "ๅฏ็", "ๆธฏ่ก", "็พ่ก"], | |
| "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800&q=80"), | |
| (["็ถๆฟ", "economy", "่ฒฟๆ", "trade", "gdp", "้่จ", "ๅ ๆฏ", "ๆธๆฏ"], | |
| "https://images.unsplash.com/photo-1450101499163-c884c5c54a85?w=800&q=80"), | |
| # ๆ ้/็ๆดป | |
| (["ๆ ้", "travel", "ๆ ่ก", "ๆฏ้ป", "้ ๅบ", "ๆฉ็ฅจ", "็พ้ฃ", "้คๅปณ", "ๆๅก"], | |
| "https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=800&q=80"), | |
| (["ๆทฑๅณ", "ๅไธ", "ๅคง็ฃๅ", "็ฃๅ", "ๅ งๅฐ"], | |
| "https://images.unsplash.com/photo-1444723121867-7a241cacace9?w=800&q=80"), | |
| (["็ๆดป", "lifestyle", "ๆฝฎๆต", "ๆๅฐ", "่ณผ็ฉ", "shopping"], | |
| "https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=800&q=80"), | |
| # ๆๆ/ๅทฅๆฅญ | |
| (["้ๅฑฌ", "metal", "้ผ้ต", "็คฆ", "ๅๆ", "ๅก่ ", "plastic", "ๆๆ", "material", " polymer"], | |
| "https://images.unsplash.com/photo-1535813547-99c4740f9fc5?w=800&q=80"), | |
| (["ๅทฅๆฅญ", "่ฃฝ้ ", "ๅทฅๅป ", "manufacturing"], | |
| "https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=800&q=80"), | |
| # ็งๅญธ | |
| (["็งๅญธ", "science", "็ ็ฉถ", "research", "ๅคช็ฉบ", "nasa", "็็ฉ", "็ฉ็", "ๅๅญธ"], | |
| "https://images.unsplash.com/photo-1507413245164-6160d8298b31?w=800&q=80"), | |
| # ๅท็ฅ่ญ/่ถฃๅณ | |
| (["ๅท็ฅ่ญ", "่ถฃ่", "ๅฅ่", "ๆญทๅฒ", "่ๅค", "่ฌๅ", "ๅฟ็"], | |
| "https://images.unsplash.com/photo-1533073526757-2c8ca1df9f1c?w=800&q=80"), | |
| ] | |
| for keywords, url in mapping: | |
| if any(k in kw for k in keywords): | |
| return url | |
| return random.choice(UNSPLASH_POOL) | |
| def ensure_image_url(raw_url: Optional[str], title: str = "") -> str: | |
| """ไฟ่ญไธๅฎๅๅณๆๆๅ็็ถฒๅ๏ผๅชๅ ๅๅง็ถฒๅ๏ผๅฆๅ Unsplash ่ฃไธใ""" | |
| if raw_url and raw_url.startswith("http"): | |
| return raw_url | |
| return get_fallback_image(title) | |
| def clean_html(raw_html: str) -> str: | |
| """ๅฐ HTML ่ฝ็บ็ดๆๅญ๏ผไฟ็ๅบๆฌๆฎต่ฝ็ตๆงใ""" | |
| if not raw_html: | |
| return "" | |
| try: | |
| soup = BeautifulSoup(raw_html, "html.parser") | |
| # ็งป้ค script / style | |
| for tag in soup(["script", "style", "nav", "footer", "header", "aside"]): | |
| tag.decompose() | |
| text = soup.get_text(separator="\n") | |
| # ๅฃ็ธฎๅค้ค็ฉบ็ฝ่ก | |
| text = re.sub(r"\n\s*\n", "\n", text) | |
| text = re.sub(r" +", " ", text) | |
| return text.strip() | |
| except Exception: | |
| # ็ฐกๆ regex ๅป้ค HTML tag | |
| text = re.sub(r"<[^>]+>", " ", raw_html) | |
| text = re.sub(r"\s+", " ", text) | |
| return text.strip() | |
| def guess_category(source_name: str) -> str: | |
| """ๆ นๆไพๆบๅ็จฑๆจๆธฌๆ็ซ ๅ้กๆจ็ฑค๏ผAI ๅ้กๅคฑๆๆ็ fallback๏ผใ""" | |
| name = source_name.lower() | |
| if any(k in name for k in ["่ฒก็ถ", "aastocks", "้ ไบจ", "finance", "money", "stock"]): | |
| return "่ฒก็ถ" | |
| if any(k in name for k in ["rthk", "ๅ้", "global", "world"]): | |
| return "ๅ้" | |
| if any(k in name for k in ["ๆ ้", "travel", "utravel", "็ๆดป", "ulifestyle", "wave", "wavetv"]): | |
| return "ๆ ้" | |
| if any(k in name for k in ["01", "ๅคง็ฃๅ", "ๅไธ", "ๆทฑๅณ"]): | |
| return "ๆ ้" | |
| if any(k in name for k in ["็งๅญธ", "science", "tomorrowsci", "ๅฐ็ๅ่ผฏ", "ๅท็ฅ่ญ"]): | |
| return "็งๅญธ" | |
| if any(k in name for k in ["plastic", "metal", "azom", "ๆๆ", "ๅทฅๆฅญ"]): | |
| return "ๆๆๅทฅๆฅญ" | |
| if any(k in name for k in ["trend", "็ฑๆ", "google trend"]): | |
| return "็ฑ้่ถจๅข" | |
| if any(k in name for k in ["gizmodo", "hackaday", "็งๆๆฐๅ ฑ", "็ฎ็งๆ", "ๆธไฝๆไปฃ", "inside", "qbit", "hkepc"]): | |
| return "็งๆ" | |
| return "็งๆ" # ้ ่จญ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # ๆฐ่ไพๆบๆทๅๅฝๅผ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def fetch_rss_articles(source: Dict) -> List[Dict]: | |
| """้้ RSS feed ๆๅๆๆฐๆ็ซ ๏ผๆๅค ARTICLES_PER_SOURCE ็ฏ๏ผใ | |
| ๆฏๆด RSS 2.0 ่ Atom ๆ ผๅผ๏ผไฝฟ็จๅ งๅปบ xml.etree.ElementTree ่งฃๆใ""" | |
| name = source["name"] | |
| url = source["url"] | |
| articles = [] | |
| print(f"๐ก [{name}] ้ๅงๆๅ RSS: {url}", flush=True) | |
| try: | |
| resp = requests.get(url, headers=HEADERS, timeout=MAX_FETCH_SECONDS) | |
| resp.raise_for_status() | |
| resp.encoding = resp.apparent_encoding or "utf-8" | |
| xml_text = resp.text | |
| except Exception as e: | |
| print(f"โ ๏ธ [{name}] RSS ้ฃ็ทๅคฑๆ: {e}", flush=True) | |
| return articles | |
| # ๅ่ฉฆ่งฃๆ XML | |
| try: | |
| root = ET.fromstring(xml_text) | |
| except ET.ParseError as e: | |
| print(f"โ ๏ธ [{name}] XML ่งฃๆๅคฑๆ: {e}", flush=True) | |
| return articles | |
| # ๅคๆท RSS 2.0 ๆ Atom ๆ ผๅผ | |
| items = [] | |
| # RSS 2.0: channel > item | |
| rss_items = root.findall(".//item") | |
| if rss_items: | |
| items = rss_items | |
| else: | |
| # Atom: feed > entry | |
| atom_items = root.findall(".//{http://www.w3.org/2005/Atom}entry") | |
| if not atom_items: | |
| atom_items = root.findall(".//entry") | |
| items = atom_items | |
| if not items: | |
| print(f"โ ๏ธ [{name}] ๆพไธๅฐไปปไฝๆฐ่ๆข็ฎ", flush=True) | |
| return articles | |
| items = items[:ARTICLES_PER_SOURCE] | |
| print(f"๐ฐ [{name}] ๅๅพ {len(items)} ็ฏๆ็ซ ", flush=True) | |
| for item in items: | |
| # ๆจ้ก | |
| title = ( | |
| get_entry_text(item, "title") or | |
| get_entry_text(item, "{http://www.w3.org/2005/Atom}title") | |
| ) | |
| if not title: | |
| continue | |
| # ๅ งๆ๏ผๅชๅ content:encoded๏ผๅ ถๆฌก description / summary / content | |
| content_text = get_entry_text( | |
| item, | |
| "{http://purl.org/rss/1.0/modules/content/}encoded", | |
| "description", | |
| "summary", | |
| "content", | |
| ) | |
| # ๆธ ็ HTML ๆจ็ฑค | |
| clean_content = clean_html(content_text) | |
| # ้ๅถๅ งๆ้ทๅบฆ๏ผ้ฟๅ token ็็ธ๏ผ | |
| if len(clean_content) > 600: | |
| clean_content = clean_content[:600] + "..." | |
| # ๅ็ๆๅ | |
| image_url = extract_image_from_rss_item(item) | |
| image_url = ensure_image_url(image_url, title) | |
| articles.append({ | |
| "title": title, | |
| "content": clean_content or title, | |
| "image_url": image_url, | |
| "source": name, | |
| }) | |
| return articles | |
| def fetch_html_articles(source: Dict) -> List[Dict]: | |
| """ๅพไธ่ฌ HTML ็ถฒ้ ๆๅๆฐ่ๅ่กจ๏ผไพๅฆ HKEPC๏ผใ""" | |
| name = source["name"] | |
| url = source["url"] | |
| articles = [] | |
| print(f"๐ [{name}] ้ๅง็ฌๅ HTML: {url}", flush=True) | |
| try: | |
| resp = requests.get(url, headers=HEADERS, timeout=MAX_FETCH_SECONDS) | |
| resp.raise_for_status() | |
| resp.encoding = resp.apparent_encoding or "utf-8" | |
| soup = BeautifulSoup(resp.text, "html.parser") | |
| except Exception as e: | |
| print(f"โ ๏ธ [{name}] HTML ้ฃ็ทๅคฑๆ: {e}", flush=True) | |
| return articles | |
| # โโ HKEPC ๅฐ็จ้ธๆๅจ โโ | |
| # ๅ่ฉฆๅค็จฎๅฏ่ฝ็ๆ็ซ ๅๅก้ธๆๅจ๏ผไพๅชๅ ๅบ๏ผ | |
| candidates = [] | |
| # ๅธธ่ฆๆฐ่ๅ่กจ้ธๆๅจ | |
| selectors = [ | |
| "a[href*='view']", # HKEPC ๆฐ่้ฃ็ตๆ ผๅผ | |
| ".news-item a", | |
| ".article-list a", | |
| ".post-title a", | |
| ".entry-title a", | |
| "article a", | |
| ".content a[href*='news']", | |
| ".main-content a", | |
| "h2 a", "h3 a", "h4 a", # ๆจ้ก้ฃ็ต | |
| ] | |
| seen_urls = set() | |
| for selector in selectors: | |
| try: | |
| for tag in soup.select(selector): | |
| href = tag.get("href", "") | |
| text = tag.get_text(strip=True) | |
| if not href or not text or len(text) < 6: | |
| continue | |
| # ้ๆฟพๆ้ๆฐ่้ฃ็ต | |
| if any(skip in href.lower() for skip in ["login", "register", "ad.", "banner", "javascript:"]): | |
| continue | |
| if href in seen_urls: | |
| continue | |
| seen_urls.add(href) | |
| candidates.append({"title": text, "link": href}) | |
| except Exception: | |
| continue | |
| if len(candidates) >= ARTICLES_PER_SOURCE: | |
| break | |
| print(f"๐ [{name}] ๆพๅฐ {len(candidates)} ๅๅ้ธ้ฃ็ต", flush=True) | |
| # ๆทๅๅ N ็ฏ | |
| for item in candidates[:ARTICLES_PER_SOURCE]: | |
| title = item["title"] | |
| link = item["link"] | |
| # ่ฃๅ จ็ธๅฐ่ทฏๅพ | |
| if link.startswith("/"): | |
| link = "https://www.hkepc.com" + link | |
| elif not link.startswith("http"): | |
| link = "https://www.hkepc.com/" + link | |
| content_text = title # ้ ่จญ็จๆจ้ก็ถๅ งๅฎน | |
| image_url = None | |
| # ๅ่ฉฆ้ฒๅ ฅๆ็ซ ๅ ง้ ๆๅๆดๅคๅ งๅฎน | |
| try: | |
| art_resp = requests.get(link, headers=HEADERS, timeout=6) | |
| art_resp.raise_for_status() | |
| art_resp.encoding = art_resp.apparent_encoding or "utf-8" | |
| art_soup = BeautifulSoup(art_resp.text, "html.parser") | |
| # ๅ งๆ | |
| for art_sel in ["article", ".article-content", ".content", ".post-content", ".entry-content", "main", ".news-content"]: | |
| body = art_soup.select_one(art_sel) | |
| if body: | |
| content_text = clean_html(str(body)) | |
| break | |
| if len(content_text) > 600: | |
| content_text = content_text[:600] + "..." | |
| # ๅ็ | |
| page_html = str(art_soup) | |
| image_url = extract_image_from_html(page_html) | |
| except Exception as e: | |
| print(f" โ ๏ธ ็กๆณๆๅๅ ง้ {link[:60]}: {e}", flush=True) | |
| image_url = ensure_image_url(image_url, title) | |
| articles.append({ | |
| "title": title, | |
| "content": content_text or title, | |
| "image_url": image_url, | |
| "source": name, | |
| }) | |
| print(f"๐ฐ [{name}] ๆทๅ {len(articles)} ็ฏๆ็ซ ", flush=True) | |
| return articles | |
| def fetch_articles_from_source(source: Dict) -> List[Dict]: | |
| """็ตฑไธๅ ฅๅฃ๏ผๆ นๆไพๆบ้กๅๅผๅซๅฐๆ็ๆทๅๅฝๅผ๏ผๅคฑๆๆ่ชๅๅๆ HTML ๅ็จ็ฌ่ฒใ""" | |
| articles = [] | |
| try: | |
| if source["type"] == "rss": | |
| articles = fetch_rss_articles(source) | |
| elif source["type"] == "html": | |
| articles = fetch_html_articles(source) | |
| else: | |
| print(f"โ ๏ธ ๆช็ฅไพๆบ้กๅ: {source['type']}", flush=True) | |
| except Exception as e: | |
| print(f"โ [{source['name']}] ๆทๅ็ฐๅธธ: {e}", flush=True) | |
| # โโ ่ฅๆญฃๅธธๆๅๅคฑๆ๏ผ0 ็ฏ๏ผ๏ผ่ชๅๅๅ HTML ๅ็จ็ฌ่ฒ โโ | |
| if not articles: | |
| print(f"๐ [{source['name']}] ๆญฃๅธธๆๅๅๅพ 0 ็ฏ๏ผ่งธ็ผ HTML ๅ็จ็ฌ่ฒ...", flush=True) | |
| articles = fetch_html_fallback(source) | |
| return articles | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # DeepSeek AI ๆ่ฆ่็ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| SYSTEM_PROMPT = """ไฝ ๆฏไธๅๅฐๆฅญ็็น้ซไธญๆๆฐ่็ทจ่ผฏ็งๆธใไฝ ๅฐๆถๅฐไธๆนๆฐ่ๆ็ซ ๏ผJSON ้ฃๅ๏ผ๏ผ | |
| ๆฏ็ฏๆ็ซ ๅ ๅซ titleใcontentใimage_urlใsource ๅๅๆฌไฝใ | |
| ่ซ็บๆฏไธ็ฏๆ็ซ ๅฎๆไปฅไธไปปๅ๏ผ | |
| 1. **่ช่จๅคๆท**๏ผๅ ๅคๆทๅๆ title ๅ content ็ไธป่ฆ่ช่จใ | |
| - ่ฅ็บ**ไธญๆ**๏ผ็ดๆฅ็ขๅบ็น้ซไธญๆ็ title ๅ summary๏ผๅฎ่ชๆจกๅผ๏ผๆ่ฆ 80๏ฝ200 ๅญ๏ผใ | |
| - ่ฅ็บ**่ฑๆๆๅ ถไปๅค่ช**๏ผ่ชๅๅๆ็บใไธญ่ฑๅฐ็ งๆจกๅผใ๏ผ่ฆ็ฌฌ 2 ้ป๏ผใ | |
| 2. **ไธญ่ฑๅฐ็ งๆจกๅผ**๏ผๅ ้ฉ็จๆผๅค่ชๅๆ๏ผ๏ผ | |
| title ๅ summary ๅฟ ้ ็จๆ่ก็ฌฆ่ `\n` ๅ้ไธไธๅ ฉๅๆฎต๏ผ | |
| - **ไธๅๆฎต**๏ผๆตๆขใ้ๅฐ็็น้ซไธญๆ็ฟป่ญฏ๏ผไฝฟ็จ้ฆๆธฏ/ๅฐ็ฃๅธธ็จ่กจ้ๆนๅผ๏ผ๏ผๆ่ฆ 80๏ฝ200 ๅญใ | |
| - **ไธๅๆฎต**๏ผๅฐๆ็่ฑๆๅๆ๏ผtitle ๅฐ็ ง่ฑๆๅๆจ้ก๏ผsummary ๅฐ็ ง็ฑ AI ็ๆ็ๆตๆข่ฑๆๆ่ฆ๏ผ40๏ฝ70 ๅ่ฑๆๅญ๏ผใ | |
| ๅฐ็ งๆจกๅผ่ผธๅบ็ฏไพ๏ผ | |
| { | |
| "title": "่จๆถ้ซๆถ็็ญ็ผบ็ก่งฃ\nMemory Chip Shortage Remains Unresolved", | |
| "summary": "็ฑๆผ AI ้ซๅน่จๅฎๅขๅ ๏ผๆถ่ฒปๆง้ปๅญๆฅญๅฐ้ข่จๅด้็ๆถ็็ผบ่ฒจ่กๆใ\nDue to the surge in high-priced AI orders, the consumer electronics industry will face a severe chip shortage impact.", | |
| "category": "็งๆ", | |
| "image_url": "https://...", | |
| "source": "..." | |
| } | |
| 3. **ๅ้กๆจ็ฑค**๏ผๆ นๆๆ็ซ ๅ งๅฎน๏ผๅพไปฅไธ 9 ๅ้ธ้ ไธญๆ้ธๆ่ฒผ่ฟ็ไธๅ category๏ผ | |
| ็งๆ | ่ฒก็ถ | ๅ้ | ๆ ้ | ็ๆดป | ็งๅญธ | ๆๆๅทฅๆฅญ | ๅท็ฅ่ญ | ็ฑ้่ถจๅข | |
| 4. ไฟ็ๅๆฌ็ image_url๏ผไธ่ฆๆดๆน๏ผใ | |
| 5. ไฟ็ๅๆฌ็ source๏ผไธ่ฆๆดๆน๏ผใ | |
| 6. ไฟ็ๅๆฌ็ title๏ผไธญๆๆจกๅผๅฏๅพฎ่ชฟๆจ้ป็ฌฆ่๏ผๅฐ็ งๆจกๅผไธๅๆฎต็บ็ฟป่ญฏใไธๅๆฎตไฟ็่ฑๆๅๆจ้ก๏ผใ | |
| ไฝ ๅฟ ้ **ๅดๆ ผๅๅณไธๅ JSON ้ฃๅ**๏ผๆ ผๅผๅฆไธ๏ผ | |
| [ | |
| { | |
| "title": "็น้ซไธญๆๆจ้ก๏ผๅค่ชๆ๏ผไธญๆ\nEnglish๏ผ", | |
| "summary": "80๏ฝ200ๅญ็น้ซไธญๆๆ่ฆ๏ผๅค่ชๆ๏ผไธญๆๆ่ฆ\nEnglish summary๏ผ", | |
| "category": "็งๆ", | |
| "image_url": "https://...", | |
| "source": "ไพๆบๅ็จฑ" | |
| }, | |
| ... | |
| ] | |
| ๆณจๆไบ้ ๏ผ | |
| - ไธญๆๆจกๅผๆ่ฆๅฟ ้ ๅจ 80๏ฝ200 ๅไธญๆๅญไน้๏ผไธๅฏ้็ญ๏ผไนไธๅฏ่ถ ้ 200 ๅญ | |
| - ๅฐ็ งๆจกๅผไธๅๆฎต็น้ซไธญๆๆ่ฆๅๆจฃ 80๏ฝ200 ๅญ๏ผไธๅๆฎต่ฑๆๆ่ฆ 40๏ฝ70 ๅ่ฑๆๅญ | |
| - category ๅฟ ้ ๆฏๅๅบ็ 9 ๅ้ธ้ ไนไธ๏ผไธๅฏ่ช่ก็ผๆ | |
| - ๆๆ็น้ซไธญๆๅฟ ้ ไฝฟ็จ zh-TW ็ฟๆ ฃ็จ่ช | |
| - ไธๅฏ้บๆผไปปไฝไธ็ฏๆ็ซ | |
| - ไธๅฏๆฐๅขไธๅญๅจ็ๆฐ่ | |
| - ๅๅณ็ JSON ้ฃๅ้ทๅบฆๅฟ ้ ็ญๆผ่ผธๅ ฅ็ๆ็ซ ๆธ้""" | |
| # โโ HTML ๅ็จ็ฌ่ฒๅฐ็จๆ็คบ่ฉ โโ | |
| HTML_FALLBACK_SYSTEM_PROMPT = """ไฝ ๆฏไธๅ็ฒพๆบ็็ถฒ้ ๆฐ่ๆ็ ๅฐๅฎถใๆๅฐๆไพไธๆฎตๅพ็ถฒ็ซ้ฆ้ ๆทๅไธไพ็ HTML ็ดๆๅญๅ งๅฎน๏ผๅทฒ็งป้ค script/style ๆจ็ฑค๏ผใ | |
| ้ๆฎตๆๅญๆทท้ไบ้ธๅฎใๅปฃๅใ้ ๅฐพ่็ๆญฃ็ๆฐ่ๆข็ฎใ | |
| ไฝ ็ไปปๅๆฏ๏ผ | |
| 1. ๅพ้ๆฎต้ไบ็ๆๅญไธญ๏ผ็ฒพ็ขบๆพๅบ**ๆๆฐ็ 2 ็ฏ้่ฆๆฐ่** | |
| 2. ่ช่กๅคๆทๅๆ่ช่จ๏ผ้ตๅพชไธญ่ฑ้่ชๅฐ็ ง่ฆๅ๏ผ่ไธปๆฐ่็งๆธ็ธๅ๏ผ๏ผ | |
| - ๅค่ชๅๆ โ ไธๅๆฎต็น้ซไธญๆ / ไธๅๆฎต่ฑๆๅฐ็ ง | |
| - ไธญๆๅๆ โ ๅ ็น้ซไธญๆ | |
| 3. ็น้ซไธญๆๆ่ฆ 80๏ฝ200 ๅญ๏ผ่ฑๆๆ่ฆ 40๏ฝ70 ๅญ | |
| 4. ็บๆฏ็ฏๆฐ่ๆๅฎไธๅ category๏ผๅพ๏ผ็งๆ | ่ฒก็ถ | ๅ้ | ๆ ้ | ็ๆดป | ็งๅญธ | ๆๆๅทฅๆฅญ | ๅท็ฅ่ญ | ็ฑ้่ถจๅข ไธญๆ้ธ๏ผ | |
| 5. ๅ็ๆฌไฝ๏ผimage_url๏ผ่ฅ็กๆณๅพๆๆฌไธญๅๅพ๏ผ่ซ็็ฉบๅญไธฒ "" | |
| ไฝ ๅฟ ้ **ๅดๆ ผๅๅณไธๅ JSON ็ฉไปถ**๏ผๅ งๅซ "articles" ้ฃๅ๏ผ | |
| { | |
| "articles": [ | |
| { | |
| "title": "...", | |
| "summary": "...", | |
| "category": "...", | |
| "image_url": "", | |
| "source": "ๆไพ็็ถฒ็ซๅ็จฑ" | |
| }, | |
| ... | |
| ] | |
| } | |
| ๆณจๆ๏ผๅชๅๅณ็ๆญฃ็ๆฐ่ๅ งๅฎน๏ผๅฟฝ็ฅๅฐ่ฆฝ้ธๅฎใ้ ๅฐพ้ฃ็ตใๅปฃๅใ็คพไบคๅช้ซๆ้็ญ้่จใ""" | |
| def fetch_html_fallback(source: Dict) -> List[Dict]: | |
| """ | |
| HTML ๅ็จ็ฌ่ฒ๏ผ | |
| ็ถ RSS/ๆญฃๅธธ็ฌ่ฒๅคฑๆๆ๏ผๆๅ็ถฒ็ซ้ฆ้ ไธฆ่ซ DeepSeek ็ดๆฅๅพ้ไบๆๆฌไธญๆๅๆฐ่ใ | |
| ๅๅณ็ๆ็ซ ๅทฒๅ ทๅๆ็ตๆ ผๅผ๏ผๅซ title, summary, category, image_url, source๏ผใ | |
| """ | |
| name = source["name"] | |
| url = source["url"] | |
| # ๅพ URL ๆๅ้ฆ้ ็ถฒๅ | |
| try: | |
| parsed = urlparse(url) | |
| base_url = f"{parsed.scheme}://{parsed.netloc}" | |
| except Exception: | |
| base_url = url | |
| print(f"๐ [{name}] RSS ๅคฑๆ๏ผๅๅ HTML ๅ็จ็ฌ่ฒ โ {base_url}", flush=True) | |
| try: | |
| resp = requests.get(base_url, headers=HEADERS, timeout=MAX_FETCH_SECONDS) | |
| resp.raise_for_status() | |
| resp.encoding = resp.apparent_encoding or "utf-8" | |
| except Exception as e: | |
| print(f" โ [{name}] HTML ๅ็จ้ฃ็ทๅคฑๆ: {e}", flush=True) | |
| return [] | |
| # โโ ๆๅ body ็ดๆๅญ๏ผ้ๅถ 10000 ๅญๅ โโ | |
| try: | |
| soup = BeautifulSoup(resp.text, "html.parser") | |
| # ็งป้ค script / style / nav / footer | |
| for tag in soup(["script", "style", "nav", "footer", "header", "aside"]): | |
| tag.decompose() | |
| if soup.body: | |
| text = soup.body.get_text(separator="\n", strip=True) | |
| else: | |
| text = soup.get_text(separator="\n", strip=True) | |
| # ๅฃ็ธฎ็ฉบ็ฝ | |
| text = re.sub(r"\n\s*\n", "\n", text) | |
| text = re.sub(r" +", " ", text) | |
| text = text[:10000] | |
| print(f" ๐ [{name}] ๆๅ็ดๆๅญ {len(text)} ๅญๅ ๏ผ้คต็ตฆ DeepSeek...", flush=True) | |
| except Exception as e: | |
| print(f" โ [{name}] HTML ๆๆฌๆๅๅคฑๆ: {e}", flush=True) | |
| return [] | |
| if len(text) < 100: | |
| print(f" โ ๏ธ [{name}] HTML ๆๆฌ้็ญ๏ผ่ทณ้", flush=True) | |
| return [] | |
| # โโ ้คต็ตฆ DeepSeek ๆๅๆฐ่ โโ | |
| try: | |
| response = client.chat.completions.create( | |
| model=DEEPSEEK_MODEL, | |
| messages=[ | |
| {"role": "system", "content": HTML_FALLBACK_SYSTEM_PROMPT}, | |
| {"role": "user", "content": f"็ถฒ็ซๅ็จฑ๏ผ{name}\n็ถฒๅ๏ผ{base_url}\n\nHTML ๆๆฌๅ งๅฎน๏ผ\n{text}"}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.3, | |
| max_tokens=4096, | |
| ) | |
| raw = response.choices[0].message.content | |
| result = json.loads(raw) | |
| # ๆๅ articles ้ฃๅ | |
| articles = [] | |
| if isinstance(result, dict): | |
| for key in ["articles", "news", "items", "results", "data"]: | |
| if key in result and isinstance(result[key], list): | |
| articles = result[key] | |
| break | |
| # ่ฅๅชๆๅฎๅ key ๆฏ list | |
| if not articles: | |
| for val in result.values(): | |
| if isinstance(val, list): | |
| articles = val | |
| break | |
| elif isinstance(result, list): | |
| articles = result | |
| # โโ ่ฃๅฎๆฌไฝ โโ | |
| completed = [] | |
| for item in articles[:5]: # ๆๅคๅ 5 ็ฏ | |
| if not isinstance(item, dict): | |
| continue | |
| completed.append({ | |
| "title": str(item.get("title", "ๆช็ฅๆจ้ก")).strip(), | |
| "summary": str(item.get("summary", item.get("content", "ๆซ็กๆ่ฆ"))).strip(), | |
| "category": str(item.get("category", guess_category(name))).strip(), | |
| "image_url": item.get("image_url") or get_fallback_image(str(item.get("title", ""))), | |
| "source": name, | |
| "_fallback": True, # ๆจ่จ็บๅ็จ็ฌ่ฒ็ขๅบ๏ผๅทฒๅซๆ่ฆ๏ผไธ้ๅๆฌก summarize๏ผ | |
| }) | |
| if completed: | |
| print(f" โ [{name}] HTML ๅ็จ็ฌ่ฒๆๅๆๅ {len(completed)} ็ฏๆฐ่", flush=True) | |
| else: | |
| print(f" โ ๏ธ [{name}] HTML ๅ็จ็ฌ่ฒๆชๆพๅฐๆฐ่", flush=True) | |
| return completed | |
| except json.JSONDecodeError as e: | |
| print(f" โ [{name}] DeepSeek JSON ่งฃๆๅคฑๆ: {e}", flush=True) | |
| except Exception as e: | |
| print(f" โ [{name}] HTML ๅ็จ็ฌ่ฒ DeepSeek ๅผๅซๅคฑๆ: {e}", flush=True) | |
| return [] | |
| def summarize_by_deepseek(articles: List[Dict]) -> List[Dict]: | |
| """ๅฐๆ็ซ ้คต็ตฆ DeepSeek ้ฒ่กๆ่ฆ่็๏ผๅๅณ็ตๆงๅ JSON ้ฃๅใ""" | |
| if not articles: | |
| return [] | |
| # ๅชๅณๅฟ ่ฆๆฌไฝ็ตฆ AI๏ผ็ฏ็ token | |
| input_data = [ | |
| { | |
| "title": a["title"], | |
| "content": a.get("content", a["title"]), | |
| "image_url": a["image_url"], | |
| "source": a["source"], | |
| } | |
| for a in articles | |
| ] | |
| payload = json.dumps(input_data, ensure_ascii=False) | |
| print(f"๐ค ็ผ้ {len(articles)} ็ฏๆ็ซ ็ตฆ DeepSeek ๆ่ฆ...", flush=True) | |
| try: | |
| response = client.chat.completions.create( | |
| model=DEEPSEEK_MODEL, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": payload}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.3, | |
| max_tokens=16384, | |
| ) | |
| raw = response.choices[0].message.content | |
| print(f"โ DeepSeek ๅๅณๆๅ ({len(raw)} ๅญๅ )", flush=True) | |
| # ๅ่ฉฆ่งฃๆ JSON | |
| result = json.loads(raw) | |
| # ๅฆๆ AI ๅๅณ็ๆฏ { "articles": [...] } ไน้ก็ๅ ่ฃ็ฉไปถ๏ผ่ชๅๆ้ | |
| if isinstance(result, dict): | |
| for key in ["articles", "news", "items", "results", "data"]: | |
| if key in result and isinstance(result[key], list): | |
| result = result[key] | |
| break | |
| # ๅฆๆ้ๆฏ dict ไธๅชๆไธๅ key ๆฏ list | |
| if isinstance(result, dict): | |
| for val in result.values(): | |
| if isinstance(val, list) and len(val) == len(articles): | |
| result = val | |
| break | |
| if not isinstance(result, list): | |
| print(f"โ ๏ธ AI ๅๅณๆ ผๅผ้้ฃๅ๏ผๅๆ : {type(result).__name__}๏ผ๏ผๅ่ฉฆไฟฎๅพฉ", flush=True) | |
| # ๆๅพๆๆฎต๏ผ้้ ้ๅปบ | |
| result = [] | |
| # โโ ๆฌไฝ่ฃๅฎๆฉๅถ โโ | |
| # ๅๆณ็ๅ้กๆจ็ฑค | |
| valid_categories = {"็งๆ", "่ฒก็ถ", "ๅ้", "ๆ ้", "็ๆดป", "็งๅญธ", "ๆๆๅทฅๆฅญ", "ๅท็ฅ่ญ", "็ฑ้่ถจๅข"} | |
| completed = [] | |
| for i, item in enumerate(result[:len(articles)]): | |
| if not isinstance(item, dict): | |
| continue | |
| original = articles[i] if i < len(articles) else {} | |
| # ๅ้กๆบๆ ง็ๆธฌ๏ผๅพไพๆบๅ็จฑๆจๆท | |
| cat = str(item.get("category", "")).strip() | |
| if cat not in valid_categories: | |
| src = str(item.get("source", original.get("source", ""))) | |
| cat = guess_category(src) | |
| completed.append({ | |
| "title": str(item.get("title", original.get("title", "ๆช็ฅๆจ้ก"))).strip(), | |
| "summary": str(item.get("summary", item.get("content", original.get("content", "ๆซ็กๆ่ฆ")))).strip(), | |
| "category": cat, | |
| "image_url": item.get("image_url") or original.get("image_url", get_fallback_image("")), | |
| "source": str(item.get("source", original.get("source", "ๆช็ฅไพๆบ"))).strip(), | |
| }) | |
| # ๅฆๆ AI ๆผๆไบไธไบๆ็ซ ๏ผ็จๅๅง่ณๆ่ฃ้ฝ | |
| if len(completed) < len(articles): | |
| print(f"โ ๏ธ AI ๅชๅๅณ {len(completed)}/{len(articles)} ็ฏ๏ผ่ฃ้ฝไธญ...", flush=True) | |
| for i in range(len(completed), len(articles)): | |
| a = articles[i] | |
| completed.append({ | |
| "title": a["title"], | |
| "summary": clean_html(a.get("content", a["title"]))[:200], | |
| "category": guess_category(a.get("source", "")), | |
| "image_url": a["image_url"], | |
| "source": a["source"], | |
| }) | |
| print(f"๐ ๆ็ต่ผธๅบ {len(completed)} ็ฏๆฐ่", flush=True) | |
| return completed | |
| except json.JSONDecodeError as e: | |
| print(f"โ DeepSeek ๅๅณ็ JSON ็กๆณ่งฃๆ: {e}", flush=True) | |
| try: | |
| print(f" Raw (ๅ 500 ๅญ): {raw[:500]}", flush=True) | |
| except NameError: | |
| print(" Raw: (็กๆณๅๅพๅๅงๅๆ)", flush=True) | |
| except Exception as e: | |
| print(f"โ DeepSeek ๅผๅซๅคฑๆ: {e}", flush=True) | |
| # โโ Fallback๏ผ็ดๆฅๅๅณๅๅงๆ็ซ ๏ผไธๅ AI ๆ่ฆ๏ผโโ | |
| print("๐ ้็ด๏ผไฝฟ็จๅๅงๆ็ซ ่ณๆๅๅณ", flush=True) | |
| fallback = [] | |
| for a in articles: | |
| fallback.append({ | |
| "title": a["title"], | |
| "summary": clean_html(a.get("content", a["title"]))[:200], | |
| "category": guess_category(a.get("source", "")), | |
| "image_url": a["image_url"], | |
| "source": a["source"], | |
| }) | |
| return fallback | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # API Endpoints | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def home(): | |
| """Hugging Face ๅฅๅบทๆชขๆฅ้ ้ข""" | |
| return ( | |
| "<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>" | |
| "<h1>๐ข ้ฒ็ซฏๅคง่ ฆ้่กๅฎ็พ๏ผ็ญๅพ ๆๆฉๅผๅซ๏ผ</h1>" | |
| "<p>Multi-Source AI News Agent v2.0</p>" | |
| "<p>ๆฏๆดไพๆบ๏ผ็งๆๆฐๅ ฑ | ็ฎ็งๆ | ๆธไฝๆไปฃ | Yahoo ๆฐ่ | HKEPC</p>" | |
| "</body></html>" | |
| ) | |
| def get_news(): | |
| """ | |
| ไธปๆฐ่็ซฏ้ป๏ผ | |
| 1. ๅพ news_sources.json ่ผๅ ฅ็ๆๆไพๆบๅนณ่กๆๅๆๆฐๆฐ่ | |
| 2. RSS/HTML ๅคฑๆๆ่ชๅๅๆ HTML ๅ็จ็ฌ่ฒ๏ผDeepSeek ็ดๆฅๆๅ๏ผ | |
| 3. ็ขบไฟๆฏ็ฏ้ฝๆๅ็ | |
| 4. ไธ่ฌๆ็ซ ้คต็ตฆ DeepSeek ๅ็น้ซไธญๆๆ่ฆ๏ผๅ็จๆ็ซ ๅทฒๅซๆ่ฆๅ็ฅ้๏ผ | |
| 5. ๅๅณ็ตฑไธ JSON ้ฃๅ | |
| """ | |
| start_time = time.time() | |
| all_articles: List[Dict] = [] | |
| # โโ ๆญฅ้ฉ 1๏ผๅนณ่กๆๅๆๆไพๆบ โโ | |
| print(f"\n{'='*60}", flush=True) | |
| print(f"๐ ้ๅงๆๅ {len(NEWS_SOURCES)} ๅๆฐ่ไพๆบ...", flush=True) | |
| with ThreadPoolExecutor(max_workers=min(len(NEWS_SOURCES), 5)) as executor: | |
| future_map = { | |
| executor.submit(fetch_articles_from_source, src): src["name"] | |
| for src in NEWS_SOURCES | |
| } | |
| for future in as_completed(future_map): | |
| name = future_map[future] | |
| try: | |
| articles = future.result() | |
| all_articles.extend(articles) | |
| print(f"โ [{name}] ๅฎๆ๏ผๅๅพ {len(articles)} ็ฏ", flush=True) | |
| except Exception as e: | |
| print(f"โ [{name}] ๅท่กๅคฑๆ: {e}", flush=True) | |
| fetch_time = time.time() - start_time | |
| print(f"๐ฆ ็ธฝๅ ฑๆถ้ {len(all_articles)} ็ฏๆ็ซ ๏ผ่ๆ {fetch_time:.1f}s๏ผ", flush=True) | |
| if not all_articles: | |
| return { | |
| "error": "ๆๆๆฐ่ไพๆบๆซๆ็กๆณๅๅพ๏ผ่ซ็จๅพๅ่ฉฆ", | |
| "articles": [], | |
| } | |
| # โโ ๆญฅ้ฉ 2๏ผๅ้ขใๅ็จ็ฌ่ฒๆ็ซ ใ๏ผๅทฒๅซ DeepSeek ๆ่ฆ๏ผ่ใไธ่ฌๆ็ซ ใโโ | |
| regular_articles: List[Dict] = [] | |
| fallback_articles: List[Dict] = [] | |
| for a in all_articles: | |
| if a.pop("_fallback", False): | |
| fallback_articles.append(a) | |
| else: | |
| regular_articles.append(a) | |
| fb_count = len(fallback_articles) | |
| reg_count = len(regular_articles) | |
| print(f"๐ ไธ่ฌๆ็ซ {reg_count} ็ฏ + ๅ็จ็ฌ่ฒๆ็ซ {fb_count} ็ฏ๏ผๅทฒๆ่ฆ๏ผ็ฅ้่็๏ผ", flush=True) | |
| # โโ ๆญฅ้ฉ 3๏ผไธ่ฌๆ็ซ ้คต็ตฆ DeepSeek ๆ่ฆ โโ | |
| BATCH_SIZE = 30 | |
| final_articles: List[Dict] = list(fallback_articles) # ๅ็จๆ็ซ ็ดๆฅๅ ๅ ฅ | |
| if regular_articles: | |
| if len(regular_articles) <= BATCH_SIZE: | |
| final_articles.extend(summarize_by_deepseek(regular_articles)) | |
| else: | |
| for i in range(0, len(regular_articles), BATCH_SIZE): | |
| batch = regular_articles[i:i + BATCH_SIZE] | |
| print(f"๐ฆ ่็ๆนๆฌก {i // BATCH_SIZE + 1}/{(len(regular_articles) + BATCH_SIZE - 1) // BATCH_SIZE} ({len(batch)} ็ฏ)...", flush=True) | |
| final_articles.extend(summarize_by_deepseek(batch)) | |
| total_time = time.time() - start_time | |
| print(f"๐ ๅ จ้จๅฎๆ๏ผ{len(final_articles)} ็ฏๆฐ่๏ผไธ่ฌ {reg_count} + ๅ็จ {fb_count}๏ผ๏ผ็ธฝ่ๆ {total_time:.1f}s๏ผ", flush=True) | |
| print(f"{'='*60}\n", flush=True) | |
| return final_articles | |