Compare commits

...
2 Commits
Author SHA1 Message Date
HongruandClaude Opus 4.6 12bbe0058b feat: 新增国际新闻获取和附言生成功能
- 新增国际新闻 RSS 源(BBC World、Sky News、NHK 国际)
- 新增附言生成功能(AI 生成的"舔狗"风格小故事)
- 优化邮件 HTML 模板(信纸风格、主题配色)
- 新增天气/新闻缓存机制,避免重复获取
- 新增 NEWS_PROXY_URL 环境变量配置国际新闻代理
- 完善状态管理(monitor_state.json 替代 state.json)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:47:08 +08:00
HongruandClaude Opus 4.6 37ad6116b2 docs: 重写 README,完整描述新增功能
- 新增 AI 早安邮件三段式文案结构说明
- 新增附言功能介绍(AI 生成的"舔狗"风格小故事)
- 新增数据源说明(wttr.in、百度热搜、BBC/Sky News/NHK RSS)
- 更新文件结构,说明状态文件用途
- 新增 TARGET_IPS、WEATHER_LOCATION、NEWS_PROXY_URL 配置说明
- 新增 DEEPSEEK_API_URL、DEEPSEEK_MODEL、YOUR_NAME 配置项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:46:54 +08:00
2 changed files with 442 additions and 53 deletions
+51 -13
View File
@@ -1,19 +1,34 @@
# 太阳公公监控 # 太阳公公监控
监控局域网设备上线,配合 AI 生成个性化早安邮件。 监控局域网设备上线,配合 AI 生成个性化早安邮件。每天清晨,有人替你把天气、热点和一点碎碎念收进一封信里,轻轻铺开。
## 功能 ## 功能
### 设备监控
- **Ping 监控**:监控指定局域网 IP 是否在线 - **Ping 监控**:监控指定局域网 IP 是否在线
- **首次上线通知**:目标设备首次上线时,发送"太阳公公出来了"邮件,包含具体上线时间 - **首次上线通知**:目标设备首次上线时,发送"太阳公公出来了"邮件,包含具体上线时间
- **未到提醒**:每天 8:15 前目标仍未上线,发送"太阳公公还在睡觉"邮件 - **未到提醒**:每天 8:15 前目标仍未上线,发送"太阳公公还在睡觉"邮件
- **AI 早安文案**:三段式早安邮件
- 第一段:太阳公公状态(AI 单独生成)
- 第二段:今日天气(AI 生成,自然衔接)
- 第三段:今日热搜(AI 找关联,像说相声一样串联成自然段落)
- **每日重置**:状态文件每日自动重置,避免重复发送 - **每日重置**:状态文件每日自动重置,避免重复发送
## 运行时间 ### AI 早安邮件
三段式早安文案,全部由 DeepSeek AI 生成的个性化内容:
- **第一段**:太阳公公状态,AI 根据实际上线/未上线情况单独生成
- **第二段**:今日天气,AI 自然衔接第一段,温暖过渡
- **第三段**:今日热搜,AI 筛选国内外热点,像朋友聊天一样串联成段
### 附言
邮件末尾附带一段 AI 原创的"过来人"小故事,主题围绕单方面喜欢、错位理解,带一点自嘲和电影感。最后一句往往突然变得很舔、很心酸。
### 数据源
- **天气**:wttr.in(免费无需 API Key)
- **国内热点**:百度实时热搜
- **国际热点**:BBC World、Sky News、NHK 国际(RSS 订阅)
### 运行时间
每天凌晨 2:00 至晚上 22:00 自动监控 每天凌晨 2:00 至晚上 22:00 自动监控
@@ -25,9 +40,9 @@ TaiYangGongGong/
├── test_ping.py # 测试脚本(单次测试) ├── test_ping.py # 测试脚本(单次测试)
├── config.py # 敏感配置(API密钥、邮件账号等)- 不上传 ├── config.py # 敏感配置(API密钥、邮件账号等)- 不上传
└── .ping_state/ # 状态文件目录 - 不上传 └── .ping_state/ # 状态文件目录 - 不上传
├── state.json ├── monitor_state.json # 监控状态
├── weather_cache.json ├── weather_cache.json # 天气缓存
└── news_cache.json └── news_cache.json # 新闻缓存
``` ```
## 首次使用 ## 首次使用
@@ -44,6 +59,8 @@ cp config.py config.py.bak
```python ```python
# DeepSeek API # DeepSeek API
DEEPSEEK_API_KEY = "your_api_key" DEEPSEEK_API_KEY = "your_api_key"
DEEPSEEK_API_URL = "https://api.deepseek.com/v1"
DEEPSEEK_MODEL = "deepseek-chat"
# 邮件配置 # 邮件配置
SMTP_SERVER = "smtp.qq.com" SMTP_SERVER = "smtp.qq.com"
@@ -52,21 +69,42 @@ SMTP_USER = "your_email@qq.com"
SMTP_PASSWORD = "your授权码" SMTP_PASSWORD = "your授权码"
FROM_NAME = "月亮姐姐" FROM_NAME = "月亮姐姐"
TO_EMAILS = ["target_email@example.com"] TO_EMAILS = ["target_email@example.com"]
YOUR_NAME = "收件人名字"
``` ```
3. 设置系统时区(邮件时间戳用系统时间): 3. 配置监控目标(`ping_monitor.py` 中的 `TARGET_IPS`):
```python
TARGET_IPS = [
"192.168.2.60",
]
```
4. 配置天气位置(默认成都):
```python
WEATHER_LOCATION = "成都"
```
5. 如需获取国际新闻,配置代理(可选):
```bash
export NEWS_PROXY_URL="http://127.0.0.1:7890"
```
6. 设置系统时区(邮件时间戳用系统时间):
```bash ```bash
sudo timedatectl set-timezone Asia/Shanghai sudo timedatectl set-timezone Asia/Shanghai
sudo ntpdate ntp.aliyun.com # 同步网络时间 sudo ntpdate ntp.aliyun.com # 同步网络时间
``` ```
4. 安装依赖并运行: 7. 安装依赖并运行:
```bash ```bash
pip install requests pip install requests
python test_ping.py # 测试 python test_ping.py # 测试(单次运行)
python ping_monitor.py # 正式运行 python ping_monitor.py # 正式运行(常驻)
``` ```
## 部署 ## 部署
+391 -40
View File
@@ -16,6 +16,7 @@ import json
import re import re
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import xml.etree.ElementTree as ET
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.header import Header from email.header import Header
@@ -37,6 +38,9 @@ TARGET_IPS = [
# 天气位置配置 # 天气位置配置
WEATHER_LOCATION = "成都" WEATHER_LOCATION = "成都"
# 国外新闻代理配置
NEWS_PROXY_URL = os.getenv("NEWS_PROXY_URL", "http://127.0.0.1:7890").strip() or None
# 运行时间配置 # 运行时间配置
START_HOUR = 2 # 凌晨2点开始 START_HOUR = 2 # 凌晨2点开始
END_HOUR = 22 # 晚上10点结束 END_HOUR = 22 # 晚上10点结束
@@ -57,6 +61,27 @@ DEFAULT_HTTP_HEADERS = {
"User-Agent": "Mozilla/5.0", "User-Agent": "Mozilla/5.0",
} }
NEWS_CACHE_VERSION = "mixed-news-v1"
DOMESTIC_NEWS_LIMIT = 20
FOREIGN_NEWS_LIMIT = 40
FOREIGN_NEWS_SOURCES = [
{
"name": "BBC World",
"url": "https://feeds.bbci.co.uk/news/world/rss.xml",
"proxy": True,
},
{
"name": "Sky News World",
"url": "https://feeds.skynews.com/feeds/rss/world.xml",
"proxy": True,
},
{
"name": "NHK 国际",
"url": "https://www3.nhk.or.jp/rss/news/cat6.xml",
"proxy": False,
},
]
# ============== 邮件发送函数 ============== # ============== 邮件发送函数 ==============
def send_email(subject, html_body): def send_email(subject, html_body):
@@ -124,6 +149,22 @@ def generate_morning_message(status, arrived_time=None, weather_summary=None, ne
# 第三段直接用news_summary(已经是串联好的内容) # 第三段直接用news_summary(已经是串联好的内容)
return f"{first_paragraph}\n\n{second_paragraph}\n\n{news_summary}" return f"{first_paragraph}\n\n{second_paragraph}\n\n{news_summary}"
def generate_dog_section():
"""生成附言:问题式副标题 + 故事正文 + 原创舔狗收尾"""
dog_section = get_dog_intro()
if dog_section:
return dog_section
return {
"subtitle": "喜欢为什么总慢半拍?",
"story": (
f"以前我也总觉得,只要自己再往前走一步,很多话就会有回应。后来才明白,"
f"有些人不是听不懂,只是不想接住。那天路灯已经暗下来了,他低头笑了笑,"
f"我站在原地,忽然觉得很多没说出口的话,其实早就被看穿了。"
),
"quote": "她说:\"我没有在等你回我,我只是想看看,我还能为你失眠到几点。\"",
}
def build_email_html(status, arrived_time=None, weather_summary=None, news_summary=None): def build_email_html(status, arrived_time=None, weather_summary=None, news_summary=None):
"""构建邮件HTML内容""" """构建邮件HTML内容"""
if status == "arrived": if status == "arrived":
@@ -134,14 +175,107 @@ def build_email_html(status, arrived_time=None, weather_summary=None, news_summa
# 获取AI生成的文案 # 获取AI生成的文案
morning_message = generate_morning_message(status, arrived_time, weather_summary, news_summary) morning_message = generate_morning_message(status, arrived_time, weather_summary, news_summary)
# 只切前两段,避免AI多换行时把新闻正文截断
msg_parts = morning_message.split('\n\n', 2)
sun_part = msg_parts[0] if len(msg_parts) > 0 else ""
weather_part = msg_parts[1] if len(msg_parts) > 1 else ""
news_part = msg_parts[2] if len(msg_parts) > 2 else ""
# 获取附言部分
dog_section = generate_dog_section()
theme = {
"seal": "太阳照常升起" if status == "arrived" else "吗喽也要休息",
"accent": "#A66A4C" if status == "arrived" else "#6B7485",
"ink": "#3B3028",
"sub_ink": "#6A5A4D",
"paper": "#FFF9F0",
"paper_edge": "#E8DCCB",
"line": "#E9DDCF",
"seal_bg": "#F3E1D2" if status == "arrived" else "#E2E8EF",
"seal_text": "#9C5F43" if status == "arrived" else "#5E7387",
}
html = f""" html = f"""
<html> <html>
<body> <head>
<div style="font-size: 16px; line-height: 1.8; color: #333;"> <meta charset="utf-8">
{morning_message.replace('\n', '<br>')} <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 28px 14px; background: #EEE4D6; font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: {theme['ink']};">
<div style="max-width: 640px; margin: 0 auto;">
<div style="background: {theme['paper']}; border: 1px solid {theme['paper_edge']}; border-radius: 18px; overflow: hidden; box-shadow: 0 16px 40px rgba(110, 87, 65, 0.12);">
<div style="padding: 24px 26px 12px; border-bottom: 1px solid {theme['line']};">
<div style="display: table; width: 100%; margin-bottom: 18px;">
<div style="display: table-cell; vertical-align: middle; font-size: 16px; color: {theme['sub_ink']};">
致:{YOUR_NAME}
</div>
<div style="display: table-cell; vertical-align: middle; text-align: right;">
<span style="display: inline-block; padding: 6px 12px; border-radius: 999px; background: {theme['seal_bg']}; color: {theme['seal_text']}; font-size: 12px; letter-spacing: 1px;">{theme['seal']}</span>
</div>
</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 32px; line-height: 1.4; color: {theme['ink']}; margin-bottom: 14px;">
今天给你写一封晨间小信
</div>
<div style="font-size: 15px; line-height: 1.9; color: {theme['sub_ink']};">
把天气、热点和一点碎碎念都收进信里。打开它的时候,就当是有人替你先把今天轻轻铺开。
</div>
</div>
<div style="padding: 10px 26px 4px;">
<div style="padding: 18px 0 16px; border-bottom: 1px dashed {theme['line']};">
<div style="font-size: 12px; letter-spacing: 2px; color: {theme['accent']}; margin-bottom: 10px;">先和你说</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 20px; line-height: 1.95; color: {theme['ink']};">
{sun_part.replace('\n', '<br>')}
</div>
</div>
<div style="padding: 18px 0 16px; border-bottom: 1px dashed {theme['line']};">
<div style="font-size: 12px; letter-spacing: 2px; color: #7187A0; margin-bottom: 10px;">今天天气</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 19px; line-height: 1.95; color: {theme['ink']};">
{weather_part.replace('\n', '<br>')}
</div>
</div>
<div style="padding: 18px 0 12px;">
<div style="font-size: 12px; letter-spacing: 2px; color: #7D8D63; margin-bottom: 10px;">今天的事</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 19px; line-height: 1.95; color: {theme['ink']};">
{news_part.replace('\n', '<br>')}
</div>
</div>
</div>
"""
# 如果有附言,添加附言区
if dog_section:
html += f"""
<div style="margin: 6px 26px 18px; padding: 16px 0 0; border-top: 1px dashed {theme['line']};">
<div style="font-size: 12px; letter-spacing: 2px; color: #B37064; margin-bottom: 10px;">附言</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 24px; line-height: 1.6; color: #4E4038; margin-bottom: 10px;">
{dog_section['subtitle']}
</div>
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 18px; line-height: 1.95; color: #5D4840;">
{dog_section['story'].replace('\n', '<br>')}
</div>
<div style="margin-top: 12px; padding-left: 14px; border-left: 2px solid #D7C2B0; color: #7A6458; font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 17px; line-height: 1.9;">
{dog_section['quote'].replace('\n', '<br>')}
</div>
</div>
"""
html += f"""
<div style="padding: 6px 26px 26px; color: #897765; border-top: 1px solid {theme['line']};">
<div style="font-family: 'STKaiti', 'KaiTi', 'Kaiti SC', 'Songti SC', serif; font-size: 20px; line-height: 1.8; margin: 12px 0 4px;">
愿今天也顺顺当当。
</div>
<div style="font-size: 12px; line-height: 1.8; color: #9B8A79;">
{datetime.datetime.now().strftime('%Y.%m.%d')} · 自动送达<br>
写信人:{FROM_NAME}
</div>
</div>
</div>
</div> </div>
<hr>
<p style="color: #888; font-size: 12px;">此邮件由系统自动发送</p>
</body> </body>
</html> </html>
""" """
@@ -242,16 +376,89 @@ def is_failed_content(content):
"""判断内容是否属于失败结果""" """判断内容是否属于失败结果"""
return not content or "获取失败" in content return not content or "获取失败" in content
def fetch_text(url, headers=None, timeout=10): def fetch_text(url, headers=None, timeout=10, proxy_url=None):
"""发起HTTP请求并返回文本内容""" """发起HTTP请求并返回文本内容"""
request_headers = dict(DEFAULT_HTTP_HEADERS) request_headers = dict(DEFAULT_HTTP_HEADERS)
if headers: if headers:
request_headers.update(headers) request_headers.update(headers)
req = urllib.request.Request(url, headers=request_headers) req = urllib.request.Request(url, headers=request_headers)
with urllib.request.urlopen(req, timeout=timeout) as response: if proxy_url:
proxy_handler = urllib.request.ProxyHandler({
"http": proxy_url,
"https": proxy_url,
})
opener = urllib.request.build_opener(proxy_handler)
response = opener.open(req, timeout=timeout)
else:
response = urllib.request.urlopen(req, timeout=timeout)
with response:
return response.read().decode('utf-8', errors='ignore') return response.read().decode('utf-8', errors='ignore')
def get_domestic_news_titles(limit=DOMESTIC_NEWS_LIMIT):
"""获取国内热点标题"""
html = fetch_text("https://top.baidu.com/board?tab=realtime")
raw_titles = re.findall(r'"word":"(.*?)"', html)
news_items = []
seen_titles = set()
for raw_title in raw_titles:
title = raw_title.strip()
if not title or title in seen_titles:
continue
seen_titles.add(title)
news_items.append(title)
if len(news_items) >= limit:
break
return news_items
def parse_rss_titles(rss_text, limit):
"""从RSS内容中提取标题"""
root = ET.fromstring(rss_text)
titles = []
seen_titles = set()
for item in root.findall('.//item'):
title_element = item.find('title')
if title_element is None or not title_element.text:
continue
title = re.sub(r'\s+', ' ', title_element.text).strip()
if not title or title in seen_titles:
continue
seen_titles.add(title)
titles.append(title)
if len(titles) >= limit:
break
return titles
def get_foreign_news_titles(limit=FOREIGN_NEWS_LIMIT):
"""获取国外新闻平台标题,必要时走代理"""
foreign_items = []
seen_titles = set()
for source in FOREIGN_NEWS_SOURCES:
proxy_url = NEWS_PROXY_URL if source.get("proxy") else None
try:
rss_text = fetch_text(source["url"], timeout=15, proxy_url=proxy_url)
titles = parse_rss_titles(rss_text, limit)
for title in titles:
if title in seen_titles:
continue
seen_titles.add(title)
foreign_items.append((source["name"], title))
if len(foreign_items) >= limit:
break
except Exception as e:
print(f"[{datetime.datetime.now()}] 获取国际新闻失败 ({source['name']}): {e}")
if len(foreign_items) >= limit:
break
return foreign_items
def get_weather(): def get_weather():
"""获取当天天气信息(带缓存)""" """获取当天天气信息(带缓存)"""
today = get_today_str() today = get_today_str()
@@ -274,16 +481,21 @@ def get_weather():
# 天气信息来源 - 使用wttr.in(免费无需API Key) # 天气信息来源 - 使用wttr.in(免费无需API Key)
weather_info = "" weather_info = ""
weather_ok = False
try: try:
encoded_location = urllib.parse.quote(WEATHER_LOCATION) encoded_location = urllib.parse.quote(WEATHER_LOCATION)
url = f"https://wttr.in/{encoded_location}?format=%l:+%C,+%t,+%h,+feeling:+%f,+%p" url = f"https://wttr.in/{encoded_location}?format=%l:+%C,+%t,+%h,+feeling:+%f,+%p"
weather_info = fetch_text(url) weather_info = fetch_text(url)
if not weather_info.strip(): if not weather_info.strip():
weather_info = "天气信息获取失败" weather_info = "天气信息获取失败"
else:
weather_ok = True
except Exception as e: except Exception as e:
print(f"[{datetime.datetime.now()}] 获取天气失败: {e}") print(f"[{datetime.datetime.now()}] 获取天气失败: {e}")
weather_info = "天气信息获取失败" weather_info = "天气信息获取失败"
print(f"[{datetime.datetime.now()}] - wttr.in ({WEATHER_LOCATION}): {'✓ 获取成功' if weather_ok else '✗ 获取失败'}")
# 保存缓存 # 保存缓存
os.makedirs(STATE_DIR, exist_ok=True) os.makedirs(STATE_DIR, exist_ok=True)
cache_data = { cache_data = {
@@ -302,9 +514,10 @@ def get_weather():
# ============== 新闻获取函数 ============== # ============== 新闻获取函数 ==============
def get_hot_news(): def get_hot_news_with_status():
"""获取当天热搜新闻(带缓存)""" """获取当天热搜新闻(带缓存),同时返回各源状态"""
today = get_today_str() today = get_today_str()
source_status = []
# 检查缓存 # 检查缓存
if os.path.exists(NEWS_CACHE_FILE): if os.path.exists(NEWS_CACHE_FILE):
@@ -314,36 +527,49 @@ def get_hot_news():
cached_news = cache.get("news", "") cached_news = cache.get("news", "")
if ( if (
cache.get("date") == today cache.get("date") == today
and cache.get("version") == NEWS_CACHE_VERSION
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
and not is_failed_content(cached_news) and not is_failed_content(cached_news)
): ):
return cached_news return cached_news, []
except Exception: except Exception:
pass pass
# 从百度热搜页提取热榜关键词
news_info = "" news_info = ""
try: try:
html = fetch_text("https://top.baidu.com/board?tab=realtime") domestic_titles = get_domestic_news_titles()
raw_titles = re.findall(r'"word":"(.*?)"', html) domestic_ok = bool(domestic_titles)
news_items = [] source_status.append(("国内热点 (百度)", domestic_ok, len(domestic_titles)))
seen_titles = set()
for raw_title in raw_titles: foreign_titles = get_foreign_news_titles()
title = raw_title.strip() foreign_sources = {}
if not title or title in seen_titles: for source_name, _ in foreign_titles:
continue foreign_sources[source_name] = foreign_sources.get(source_name, 0) + 1
seen_titles.add(title) for src in FOREIGN_NEWS_SOURCES:
news_items.append(title) src_name = src["name"]
if len(news_items) >= 10: count = foreign_sources.get(src_name, 0)
break source_status.append((src_name, count > 0, count))
if not news_items: sections = []
raise ValueError("未能从百度热搜页面提取标题") if domestic_titles:
sections.append(
news_info = "\n".join( "国内热点:\n" + "\n".join(
f"{index}. {title}" for index, title in enumerate(news_items, 1) f"{index}. {title}" for index, title in enumerate(domestic_titles, 1)
) )
)
if foreign_titles:
sections.append(
"国际热点:\n" + "\n".join(
f"{index}. {source} | {title}"
for index, (source, title) in enumerate(foreign_titles, 1)
)
)
if not sections:
raise ValueError("未能获取国内或国际新闻")
news_info = "\n\n".join(sections)
except Exception as e: except Exception as e:
print(f"[{datetime.datetime.now()}] 获取新闻失败: {e}") print(f"[{datetime.datetime.now()}] 获取新闻失败: {e}")
@@ -354,6 +580,7 @@ def get_hot_news():
os.makedirs(STATE_DIR, exist_ok=True) os.makedirs(STATE_DIR, exist_ok=True)
cache_data = { cache_data = {
"date": today, "date": today,
"version": NEWS_CACHE_VERSION,
"timestamp": time.time(), "timestamp": time.time(),
"news": news_info "news": news_info
} }
@@ -363,8 +590,86 @@ def get_hot_news():
except Exception: except Exception:
pass pass
return news_info, source_status
def get_hot_news():
"""获取当天热搜新闻(带缓存)"""
news_info, source_status = get_hot_news_with_status()
if source_status:
# source_status[0] = 国内热点, source_status[1:] = 国外各源
domestic_name, domestic_ok, domestic_count = source_status[0]
print(f"[{datetime.datetime.now()}] - {domestic_name}: {'✓ 获取成功 ' + str(domestic_count) + ' 条' if domestic_ok else '✗ 获取失败'}")
for src_info in source_status[1:]:
src_name, ok, count = src_info
print(f"[{datetime.datetime.now()}] - {src_name}: {'✓ 获取成功 ' + str(count) + ' 条' if ok else '✗ 获取失败'}")
return news_info return news_info
# ============== 附言生成函数 ==============
def get_dog_intro():
"""让AI直接生成附言的副标题、故事和收尾台词"""
prompt = """请直接原创一段适合放进早安邮件附言里的小故事,主题是单方面喜欢、过度投入、自我感动、错位理解这类情绪,但不要写得太油腻。
请严格按下面格式输出:
第一行:一个问题式副标题,像“为什么越卑微越容易自我感动?”、“喜欢为什么总在错误频道里发生?”,长度控制在10到22字,以问号结尾。
第二部分:写一段100到170字的小故事,像一个过来人在回忆自己以前经历过的一件小事。你要自己判断叙述者更像男生还是女生、喜欢的人是谁,并保持整段故事的人称、语气、细节一致。要自然、具体、克制,带一点自嘲和电影感,但不要像鸡汤,也不要空泛说理。正文结尾要停在一个情绪悬着的位置,像还差一句话没有落下来。
第三部分:另起一段,只写一句收尾台词。这里可以是女神/男神的冷淡回应,也可以是舔狗本人特别卑微、特别会自我感动的一句话。关键要求是:如果删掉这最后一句,前面只是一个普通的暗恋小故事;加上这最后一句,整段才会一下子变得很舔、很心酸、甚至有点好笑。这里要自然写成“我说:\"...\"”、“他说:\"...\"”或者“她说:\"...\"”。引号里的内容由你原创,但必须像真实聊天里会出现的话,不要故作矫情。
要求:
- 副标题不要加书名号、引号、编号或解释
- 正文不要分段,不要列表,不要使用 markdown
- 正文和最后那句台词要有情绪上的衔接,像回忆走到那一刻时自然停下来
- 最后一段单独成段,像信里被轻轻划出来的一句原话,不要额外解释
- 台词要短一些,尽量控制在20到50字
- 最后一句必须明显提升“舔狗感”,不能只是普通对白,不能和前文松散脱节
- 只输出这三部分,每部分之间空一行"""
intro = ask_deepseek(
prompt,
system_prompt="你是一个很会讲感情小故事的人,文风克制、真诚,带一点自嘲和电影感。你尤其擅长写那种最后一句一出来,整段故事突然显得很舔、很可怜、又有点好笑的附言。",
)
if not intro:
return None
parts = intro.split("\n\n", 2)
subtitle = parts[0].strip() if parts else ""
story = parts[1].strip() if len(parts) > 1 else ""
quote = parts[2].strip() if len(parts) > 2 else ""
if not subtitle or not story or not quote:
lines = [line.strip() for line in intro.splitlines() if line.strip()]
if len(lines) >= 3:
subtitle = lines[0]
quote_index = None
for index, line in enumerate(lines[1:], start=1):
if line.startswith("我说:") or line.startswith("他说:") or line.startswith("她说:"):
quote_index = index
break
if quote_index is not None:
story = "".join(lines[1:quote_index])
quote = "".join(lines[quote_index:])
else:
story = "".join(lines[1:-1])
quote = lines[-1]
if not subtitle or not story or not quote:
return None
if not subtitle.endswith("?") and not subtitle.endswith("?"):
subtitle = subtitle.rstrip("。!!,,::") + "?"
if not (quote.startswith("我说:") or quote.startswith("他说:") or quote.startswith("她说:")):
quote = "我说:\"你回不回都没关系,我只是想让你的对话框里也有一点我的痕迹。\""
return {
"subtitle": subtitle,
"story": story,
"quote": quote,
}
# ============== DeepSeek AI 处理函数 ============== # ============== DeepSeek AI 处理函数 ==============
def ask_deepseek(prompt, system_prompt="你是一个有用的助手,用简洁温暖的语言回复。"): def ask_deepseek(prompt, system_prompt="你是一个有用的助手,用简洁温暖的语言回复。"):
@@ -413,23 +718,35 @@ def get_weather_summary(weather_info):
return summary if summary else weather_info return summary if summary else weather_info
def get_news_summary(news_info): def get_news_summary(news_info):
"""让DeepSeek总结新闻要点,找出关联,串联成自然的段落""" """让DeepSeek把国内外新闻统一整理成中文段落"""
if not news_info or "获取失败" in news_info: if not news_info or "获取失败" in news_info:
return news_info return news_info
prompt = f"""请阅读以下今日热搜新闻,试着找出它们之间的关联或共同话题,然后写成一段200字左右的自然段落,像朋友聊天一样分享这些新闻: prompt = f"""请阅读以下今日新闻,其中包含国内热点和国外新闻平台标题。请先理解并翻译其中的外文标题,再从中筛选出最值得聊的内容,写成适合早安邮件的三段式中文短评。
{news_info} {news_info}
要求: 要求:
- 找出新闻之间的联系,用自己的话串起来讲,不要念稿 - 固定写成三段,每段之间空一行,不要加小标题
- 讲的时候像跟朋友吐槽聊天那样,说到哪个新闻就直接说,别用引号包着 - 第一段:挑 2 到 3 条更偏宏观的新闻开场,可以是国际局势、科技、公共事件,负责把今天的大背景立起来
- 语气像在说相声或脱口秀,自然调侃,不要端着 - 第二段:转到更具体、更有人味的新闻,挑 2 到 3 条民生、人物、生活感更强的内容来写,让叙述落到具体的人和细节上
- 不要用列表或条目,要连贯的段落 - 第三段:不要再继续堆新新闻,改成基于前两段做一个自然的观察、总结或轻一点的评价,像看完今天新闻后的一个收束
- 字数200字左右 - 三段之间要有内在联系,像同一个人顺着一个念头慢慢讲下来,而不是三段独立的小作文
- 只返回这段新闻解读,不要多余的话""" - 只选真正值得聊的内容,不要为了凑数把每条新闻都塞进去
- 把外文标题自然翻成中文,不要保留英文原句,也不要写“某媒体说”“我给你翻译几条”这种转述腔
- 叙述顺序要自然,避免一会儿国外一会儿国内来回跳
- 语气像一个信息敏感、表达克制的朋友,在早餐时顺手聊几件今天值得知道的事
- 可以有一点感慨和观察,但不要故作幽默,不要玩梗过多,不要像相声稿
- 不要用列表、序号、小标题,不要分成“国外新闻这边”“国内新闻这边”这种板块汇报
- 不要出现“早上好啊”“对了”“先给你翻译几条”“把这些串起来看”这类口水开场或总结套话
- 总字数控制在 260 到 360 字,第三段宁可短一点,也要起到收束作用
- ⚠️重要:不要使用任何 markdown 符号,如**加粗**、*斜体*、- 列表等,只用纯文本
- 只返回最终正文,不要解释你的写作思路"""
summary = ask_deepseek(prompt, system_prompt="你是一个贴心的早安助手,用温暖自然的语言回复。") summary = ask_deepseek(
prompt,
system_prompt="你是一个擅长写晨间新闻短评的中文编辑,知道如何筛选重点、组织主线,并把信息写得自然、克制、有人味。你尤其擅长三段式写法:前两段给信息,最后一段做收束和观察。",
)
return summary if summary else news_info return summary if summary else news_info
# ============== 时间判断 ============== # ============== 时间判断 ==============
@@ -466,13 +783,47 @@ def get_cached_content():
print(f"[{datetime.datetime.now()}] 正在获取天气信息...") print(f"[{datetime.datetime.now()}] 正在获取天气信息...")
weather_info = get_weather() weather_info = get_weather()
_cached_weather_summary = get_weather_summary(weather_info) _cached_weather_summary = get_weather_summary(weather_info)
print(f"[{datetime.datetime.now()}] 天气摘要: {_cached_weather_summary[:50]}...") weather_ok = weather_info and "获取失败" not in weather_info
print(f"[{datetime.datetime.now()}] 天气 {'✓ 获取成功' if weather_ok else '✗ 获取失败'}: {_cached_weather_summary[:50] if _cached_weather_summary else '(无内容)'}...")
print(f"[{datetime.datetime.now()}] - wttr.in (成都): {'✓ 获取成功' if weather_ok else '✗ 获取失败'}")
if _cached_news_summary is None: if _cached_news_summary is None:
print(f"[{datetime.datetime.now()}] 正在获取新闻信息...") print(f"[{datetime.datetime.now()}] 正在获取新闻信息...")
news_info = get_hot_news() news_info, source_status = get_hot_news_with_status()
_cached_news_summary = get_news_summary(news_info) _cached_news_summary = get_news_summary(news_info)
print(f"[{datetime.datetime.now()}] 新闻摘要: {_cached_news_summary[:50]}...") news_ok = news_info and "获取失败" not in news_info
print(f"[{datetime.datetime.now()}] 新闻 {'✓ 获取成功' if news_ok else '✗ 获取失败'}")
# 如果有源状态(非缓存命中),打印各源状态
if source_status:
for src_name, ok, count in source_status:
status = f"✓ 获取成功 {count} 条" if ok else "✗ 获取失败"
print(f"[{datetime.datetime.now()}] - {src_name}: {status}")
else:
# 缓存命中,单独检查各源
domestic_titles = []
try:
domestic_titles = get_domestic_news_titles()
except Exception:
pass
print(f"[{datetime.datetime.now()}] - 国内热点 (百度): {'✓ 获取成功 ' + str(len(domestic_titles)) + ' 条' if domestic_titles else '✗ 获取失败 (缓存命中)'}")
foreign_items = []
try:
foreign_items = get_foreign_news_titles()
except Exception:
pass
foreign_sources = {}
for source_name, _ in foreign_items:
foreign_sources[source_name] = foreign_sources.get(source_name, 0) + 1
for src in FOREIGN_NEWS_SOURCES:
src_name = src["name"]
count = foreign_sources.get(src_name, 0)
print(f"[{datetime.datetime.now()}] - {src_name}: {'✓ 获取成功 ' + str(count) + ' 条' if count > 0 else '✗ 获取失败 (缓存命中)'}")
print(f"[{datetime.datetime.now()}] 新闻摘要: {_cached_news_summary[:50] if _cached_news_summary else '(无内容)'}...")
print(f"[{datetime.datetime.now()}] - 附言: 发送邮件时由 AI 即时生成")
return _cached_weather_summary, _cached_news_summary return _cached_weather_summary, _cached_news_summary