2026-04-11 22:02:38 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
LAN IP 在线监控脚本
|
|
|
|
|
|
- 运行时间: 每天凌晨2点到晚上10点
|
|
|
|
|
|
- 监控多个固定局域网IP
|
|
|
|
|
|
- 首次上线发邮件通知
|
|
|
|
|
|
- 早上8:15前未上线则发送"未到"通知
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import time
|
|
|
|
|
|
import datetime
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import smtplib
|
|
|
|
|
|
import json
|
|
|
|
|
|
import re
|
|
|
|
|
|
import urllib.parse
|
|
|
|
|
|
import urllib.request
|
2026-04-11 23:47:08 +08:00
|
|
|
|
import xml.etree.ElementTree as ET
|
2026-04-11 22:02:38 +08:00
|
|
|
|
from email.mime.text import MIMEText
|
|
|
|
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
|
|
from email.header import Header
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 配置区域 ==============
|
|
|
|
|
|
|
|
|
|
|
|
# 导入敏感配置
|
|
|
|
|
|
from config import (
|
|
|
|
|
|
SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD,
|
|
|
|
|
|
FROM_NAME, TO_EMAILS, YOUR_NAME,
|
|
|
|
|
|
DEEPSEEK_API_KEY, DEEPSEEK_API_URL, DEEPSEEK_MODEL,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 监控的局域网IP列表
|
|
|
|
|
|
TARGET_IPS = [
|
|
|
|
|
|
"192.168.2.60",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 天气位置配置
|
|
|
|
|
|
WEATHER_LOCATION = "成都"
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
# 国外新闻代理配置
|
|
|
|
|
|
NEWS_PROXY_URL = os.getenv("NEWS_PROXY_URL", "http://127.0.0.1:7890").strip() or None
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
# 运行时间配置
|
|
|
|
|
|
START_HOUR = 2 # 凌晨2点开始
|
|
|
|
|
|
END_HOUR = 22 # 晚上10点结束
|
|
|
|
|
|
ALERT_HOUR = 8 # 8点
|
|
|
|
|
|
ALERT_MINUTE = 15 # 15分
|
|
|
|
|
|
|
|
|
|
|
|
# 状态文件目录
|
|
|
|
|
|
STATE_DIR = "/home/lhr/storage/TaiYangGongGong/.ping_state"
|
|
|
|
|
|
|
|
|
|
|
|
# 天气和新闻缓存过期时间(秒)
|
|
|
|
|
|
CACHE_EXPIRE = 3600 # 1小时
|
|
|
|
|
|
|
|
|
|
|
|
# 天气/新闻缓存文件
|
|
|
|
|
|
WEATHER_CACHE_FILE = os.path.join(STATE_DIR, "weather_cache.json")
|
|
|
|
|
|
NEWS_CACHE_FILE = os.path.join(STATE_DIR, "news_cache.json")
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_HTTP_HEADERS = {
|
|
|
|
|
|
"User-Agent": "Mozilla/5.0",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
# ============== 邮件发送函数 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def send_email(subject, html_body):
|
|
|
|
|
|
"""发送邮件"""
|
|
|
|
|
|
msg = MIMEMultipart('alternative')
|
|
|
|
|
|
msg['From'] = Header(FROM_NAME, 'utf-8').encode() + f" <{SMTP_USER}>"
|
|
|
|
|
|
msg['To'] = ", ".join(TO_EMAILS)
|
|
|
|
|
|
msg['Subject'] = str(Header(subject, 'utf-8'))
|
|
|
|
|
|
|
|
|
|
|
|
msg.attach(MIMEText(html_body, 'html', 'utf-8'))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
|
|
|
|
|
server.starttls()
|
|
|
|
|
|
server.login(SMTP_USER, SMTP_PASSWORD)
|
|
|
|
|
|
server.sendmail(SMTP_USER, TO_EMAILS, msg.as_string())
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 邮件发送成功: {subject}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 邮件发送失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def generate_morning_message(status, arrived_time=None, weather_summary=None, news_summary=None):
|
|
|
|
|
|
"""让AI生成自然的早安文案,第一段单独生成不受干扰,第二段衔接第一段,第三段直接用"""
|
|
|
|
|
|
# 第一段:太阳公公状态(AI单独生成,保留关键信息)
|
|
|
|
|
|
sun_status = f"已起床,{arrived_time}就溜出来了" if status == "arrived" else "还在睡觉,还没看到影子"
|
|
|
|
|
|
sun_prompt = f"""请根据太阳公公的起床情况,写一段50字左右的早安问候语:
|
|
|
|
|
|
|
|
|
|
|
|
太阳公公状态:{sun_status}
|
|
|
|
|
|
|
|
|
|
|
|
⚠️重要:时间已经是24小时制(如 21:48),直接用这个时间写进文案,不要自己换算或加"上午/下午"!
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
|
|
|
|
|
- 语气俏皮可爱,像朋友聊天
|
|
|
|
|
|
- 开头加一个和内容相符的emoji
|
|
|
|
|
|
- 50字左右,不要太长
|
|
|
|
|
|
- 不要用分隔符或标题
|
|
|
|
|
|
- 只返回这段文字"""
|
|
|
|
|
|
|
|
|
|
|
|
first_paragraph = ask_deepseek(sun_prompt, system_prompt="你是一个贴心的朋友,写温暖俏皮的早安文案。")
|
|
|
|
|
|
first_paragraph = first_paragraph if first_paragraph else (
|
|
|
|
|
|
f"🌅 太阳公公终于起床啦,{arrived_time}就偷偷溜出来了~" if status == "arrived"
|
|
|
|
|
|
else "😴 太阳公公这会儿还没醒呢,别急,我帮你盯着着呢~"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 第二段:AI根据第一段生成天气段
|
|
|
|
|
|
weather_prompt = f"""请根据【第一段】的语气,自然过渡到今日天气:
|
|
|
|
|
|
|
|
|
|
|
|
【第一段】
|
|
|
|
|
|
{first_paragraph}
|
|
|
|
|
|
|
|
|
|
|
|
【今日天气】
|
|
|
|
|
|
{weather_summary if weather_summary else '今天天气信息获取失败'}
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
|
|
|
|
|
- 语气要接住【第一段】,像朋友聊天一样自然过渡到天气
|
|
|
|
|
|
- 50-80字左右
|
|
|
|
|
|
- 不要用分隔符或标题
|
|
|
|
|
|
- 不要用列表格式
|
|
|
|
|
|
- 只返回第二段"""
|
|
|
|
|
|
|
|
|
|
|
|
second_paragraph = ask_deepseek(weather_prompt, system_prompt="你是一个贴心的朋友,写温暖自然的早安文案。")
|
|
|
|
|
|
second_paragraph = second_paragraph if second_paragraph else f"今日天气:{weather_summary}"
|
|
|
|
|
|
|
|
|
|
|
|
# 第三段直接用news_summary(已经是串联好的内容)
|
|
|
|
|
|
return f"{first_paragraph}\n\n{second_paragraph}\n\n{news_summary}"
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
def generate_dog_section():
|
|
|
|
|
|
"""生成附言:问题式副标题 + 故事正文 + 原创舔狗收尾"""
|
|
|
|
|
|
dog_section = get_dog_intro()
|
|
|
|
|
|
if dog_section:
|
|
|
|
|
|
return dog_section
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"subtitle": "喜欢为什么总慢半拍?",
|
|
|
|
|
|
"story": (
|
|
|
|
|
|
f"以前我也总觉得,只要自己再往前走一步,很多话就会有回应。后来才明白,"
|
|
|
|
|
|
f"有些人不是听不懂,只是不想接住。那天路灯已经暗下来了,他低头笑了笑,"
|
|
|
|
|
|
f"我站在原地,忽然觉得很多没说出口的话,其实早就被看穿了。"
|
|
|
|
|
|
),
|
|
|
|
|
|
"quote": "她说:\"我没有在等你回我,我只是想看看,我还能为你失眠到几点。\"",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
def build_email_html(status, arrived_time=None, weather_summary=None, news_summary=None):
|
|
|
|
|
|
"""构建邮件HTML内容"""
|
|
|
|
|
|
if status == "arrived":
|
|
|
|
|
|
subject = f"😤 早上坏"
|
|
|
|
|
|
else:
|
|
|
|
|
|
subject = f"😊 早上好"
|
|
|
|
|
|
|
|
|
|
|
|
# 获取AI生成的文案
|
|
|
|
|
|
morning_message = generate_morning_message(status, arrived_time, weather_summary, news_summary)
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
# 只切前两段,避免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",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
html = f"""
|
|
|
|
|
|
<html>
|
2026-04-11 23:47:08 +08:00
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<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>
|
2026-04-11 22:02:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
return subject, html
|
|
|
|
|
|
|
|
|
|
|
|
# ============== Ping 检测函数 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def ping_host(ip, timeout=2):
|
|
|
|
|
|
"""检测主机是否在线"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Windows 使用 ping -n, 其他使用 ping -c
|
|
|
|
|
|
cmd = ["ping", "-c", "1", "-W", str(timeout), ip]
|
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
|
|
return result.returncode == 0
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 状态管理 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def get_today_str():
|
|
|
|
|
|
return datetime.datetime.now().strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
|
|
def load_state():
|
|
|
|
|
|
"""加载状态文件"""
|
|
|
|
|
|
state_file = os.path.join(STATE_DIR, "monitor_state.json")
|
|
|
|
|
|
if not os.path.exists(state_file):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
with open(state_file, 'r') as f:
|
|
|
|
|
|
return json.load(f)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
def save_state(state):
|
|
|
|
|
|
"""保存状态文件"""
|
|
|
|
|
|
os.makedirs(STATE_DIR, exist_ok=True)
|
|
|
|
|
|
state_file = os.path.join(STATE_DIR, "monitor_state.json")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import json
|
|
|
|
|
|
with open(state_file, 'w') as f:
|
|
|
|
|
|
json.dump(state, f, indent=2)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 保存状态失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def should_send_arrived_today(state, ip):
|
|
|
|
|
|
"""检查今天是否已发送过到岗通知"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
ip_state = state.get(ip, {})
|
|
|
|
|
|
return ip_state.get("arrived_date") == today and ip_state.get("arrived_sent", False)
|
|
|
|
|
|
|
|
|
|
|
|
def should_send_not_arrived_today(state, ip):
|
|
|
|
|
|
"""检查今天是否已发送过未到通知"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
ip_state = state.get(ip, {})
|
|
|
|
|
|
return ip_state.get("not_arrived_date") == today and ip_state.get("not_arrived_sent", False)
|
|
|
|
|
|
|
|
|
|
|
|
def mark_arrived_sent(state, ip):
|
|
|
|
|
|
"""标记已发送到岗通知"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
if ip not in state:
|
|
|
|
|
|
state[ip] = {}
|
|
|
|
|
|
state[ip]["arrived_date"] = today
|
|
|
|
|
|
state[ip]["arrived_sent"] = True
|
|
|
|
|
|
state[ip]["arrived_time"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
save_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
def mark_not_arrived_sent(state, ip):
|
|
|
|
|
|
"""标记已发送未到通知"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
if ip not in state:
|
|
|
|
|
|
state[ip] = {}
|
|
|
|
|
|
state[ip]["not_arrived_date"] = today
|
|
|
|
|
|
state[ip]["not_arrived_sent"] = True
|
|
|
|
|
|
save_state(state)
|
|
|
|
|
|
|
|
|
|
|
|
def reset_daily_state(state):
|
|
|
|
|
|
"""重置新一天的标记"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
changed = False
|
|
|
|
|
|
for ip in state:
|
|
|
|
|
|
ip_state = state[ip]
|
|
|
|
|
|
if ip_state.get("arrived_date") != today:
|
|
|
|
|
|
ip_state["arrived_sent"] = False
|
|
|
|
|
|
ip_state["not_arrived_sent"] = False
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
elif ip_state.get("not_arrived_date") != today:
|
|
|
|
|
|
ip_state["not_arrived_sent"] = False
|
|
|
|
|
|
changed = True
|
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 天气获取函数 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def is_failed_content(content):
|
|
|
|
|
|
"""判断内容是否属于失败结果"""
|
|
|
|
|
|
return not content or "获取失败" in content
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
def fetch_text(url, headers=None, timeout=10, proxy_url=None):
|
2026-04-11 22:02:38 +08:00
|
|
|
|
"""发起HTTP请求并返回文本内容"""
|
|
|
|
|
|
request_headers = dict(DEFAULT_HTTP_HEADERS)
|
|
|
|
|
|
if headers:
|
|
|
|
|
|
request_headers.update(headers)
|
|
|
|
|
|
|
|
|
|
|
|
req = urllib.request.Request(url, headers=request_headers)
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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:
|
2026-04-11 22:02:38 +08:00
|
|
|
|
return response.read().decode('utf-8', errors='ignore')
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
def get_weather():
|
|
|
|
|
|
"""获取当天天气信息(带缓存)"""
|
|
|
|
|
|
today = get_today_str()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查缓存
|
|
|
|
|
|
if os.path.exists(WEATHER_CACHE_FILE):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(WEATHER_CACHE_FILE, 'r') as f:
|
|
|
|
|
|
cache = json.load(f)
|
|
|
|
|
|
cached_weather = cache.get("weather", "")
|
|
|
|
|
|
if (
|
|
|
|
|
|
cache.get("date") == today
|
|
|
|
|
|
and cache.get("location") == WEATHER_LOCATION
|
|
|
|
|
|
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
|
|
|
|
|
|
and not is_failed_content(cached_weather)
|
|
|
|
|
|
):
|
|
|
|
|
|
return cached_weather
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 天气信息来源 - 使用wttr.in(免费无需API Key)
|
|
|
|
|
|
weather_info = ""
|
2026-04-11 23:47:08 +08:00
|
|
|
|
weather_ok = False
|
2026-04-11 22:02:38 +08:00
|
|
|
|
try:
|
|
|
|
|
|
encoded_location = urllib.parse.quote(WEATHER_LOCATION)
|
|
|
|
|
|
url = f"https://wttr.in/{encoded_location}?format=%l:+%C,+%t,+%h,+feeling:+%f,+%p"
|
|
|
|
|
|
weather_info = fetch_text(url)
|
|
|
|
|
|
if not weather_info.strip():
|
|
|
|
|
|
weather_info = "天气信息获取失败"
|
2026-04-11 23:47:08 +08:00
|
|
|
|
else:
|
|
|
|
|
|
weather_ok = True
|
2026-04-11 22:02:38 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 获取天气失败: {e}")
|
|
|
|
|
|
weather_info = "天气信息获取失败"
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
print(f"[{datetime.datetime.now()}] - wttr.in ({WEATHER_LOCATION}): {'✓ 获取成功' if weather_ok else '✗ 获取失败'}")
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
# 保存缓存
|
|
|
|
|
|
os.makedirs(STATE_DIR, exist_ok=True)
|
|
|
|
|
|
cache_data = {
|
|
|
|
|
|
"date": today,
|
|
|
|
|
|
"location": WEATHER_LOCATION,
|
|
|
|
|
|
"timestamp": time.time(),
|
|
|
|
|
|
"weather": weather_info
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(WEATHER_CACHE_FILE, 'w') as f:
|
|
|
|
|
|
json.dump(cache_data, f)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
return weather_info
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 新闻获取函数 ==============
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
def get_hot_news_with_status():
|
|
|
|
|
|
"""获取当天热搜新闻(带缓存),同时返回各源状态"""
|
2026-04-11 22:02:38 +08:00
|
|
|
|
today = get_today_str()
|
2026-04-11 23:47:08 +08:00
|
|
|
|
source_status = []
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查缓存
|
|
|
|
|
|
if os.path.exists(NEWS_CACHE_FILE):
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(NEWS_CACHE_FILE, 'r') as f:
|
|
|
|
|
|
cache = json.load(f)
|
|
|
|
|
|
cached_news = cache.get("news", "")
|
|
|
|
|
|
if (
|
|
|
|
|
|
cache.get("date") == today
|
2026-04-11 23:47:08 +08:00
|
|
|
|
and cache.get("version") == NEWS_CACHE_VERSION
|
2026-04-11 22:02:38 +08:00
|
|
|
|
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
|
|
|
|
|
|
and not is_failed_content(cached_news)
|
|
|
|
|
|
):
|
2026-04-11 23:47:08 +08:00
|
|
|
|
return cached_news, []
|
2026-04-11 22:02:38 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
news_info = ""
|
|
|
|
|
|
try:
|
2026-04-11 23:47:08 +08:00
|
|
|
|
domestic_titles = get_domestic_news_titles()
|
|
|
|
|
|
domestic_ok = bool(domestic_titles)
|
|
|
|
|
|
source_status.append(("国内热点 (百度)", domestic_ok, len(domestic_titles)))
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
foreign_titles = get_foreign_news_titles()
|
|
|
|
|
|
foreign_sources = {}
|
|
|
|
|
|
for source_name, _ in foreign_titles:
|
|
|
|
|
|
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)
|
|
|
|
|
|
source_status.append((src_name, count > 0, count))
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
sections = []
|
|
|
|
|
|
if domestic_titles:
|
|
|
|
|
|
sections.append(
|
|
|
|
|
|
"国内热点:\n" + "\n".join(
|
|
|
|
|
|
f"{index}. {title}" for index, title in enumerate(domestic_titles, 1)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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)
|
2026-04-11 22:02:38 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 获取新闻失败: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
if not news_info:
|
|
|
|
|
|
news_info = "新闻信息获取失败"
|
|
|
|
|
|
|
|
|
|
|
|
# 保存缓存
|
|
|
|
|
|
os.makedirs(STATE_DIR, exist_ok=True)
|
|
|
|
|
|
cache_data = {
|
|
|
|
|
|
"date": today,
|
2026-04-11 23:47:08 +08:00
|
|
|
|
"version": NEWS_CACHE_VERSION,
|
2026-04-11 22:02:38 +08:00
|
|
|
|
"timestamp": time.time(),
|
|
|
|
|
|
"news": news_info
|
|
|
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(NEWS_CACHE_FILE, 'w') as f:
|
|
|
|
|
|
json.dump(cache_data, f)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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 '✗ 获取失败'}")
|
2026-04-11 22:02:38 +08:00
|
|
|
|
return news_info
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
# ============== 附言生成函数 ==============
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 22:02:38 +08:00
|
|
|
|
# ============== DeepSeek AI 处理函数 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def ask_deepseek(prompt, system_prompt="你是一个有用的助手,用简洁温暖的语言回复。"):
|
|
|
|
|
|
"""调用DeepSeek API获取回复"""
|
|
|
|
|
|
headers = {
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
"Authorization": f"Bearer {DEEPSEEK_API_KEY}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"model": DEEPSEEK_MODEL,
|
|
|
|
|
|
"messages": [
|
|
|
|
|
|
{"role": "system", "content": system_prompt},
|
|
|
|
|
|
{"role": "user", "content": prompt}
|
|
|
|
|
|
],
|
|
|
|
|
|
"temperature": 0.7,
|
|
|
|
|
|
"max_tokens": 500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
|
DEEPSEEK_API_URL,
|
|
|
|
|
|
data=json.dumps(payload).encode('utf-8'),
|
|
|
|
|
|
headers=headers,
|
|
|
|
|
|
method='POST'
|
|
|
|
|
|
)
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
|
|
|
|
result = json.loads(response.read().decode('utf-8'))
|
|
|
|
|
|
return result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] DeepSeek API调用失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def get_weather_summary(weather_info):
|
|
|
|
|
|
"""让DeepSeek总结天气信息"""
|
|
|
|
|
|
if not weather_info or "获取失败" in weather_info:
|
|
|
|
|
|
return weather_info
|
|
|
|
|
|
|
|
|
|
|
|
prompt = f"""请用简洁温暖的语言总结以下天气信息,控制在50字以内,适合放在早安邮件里:
|
|
|
|
|
|
|
|
|
|
|
|
{weather_info}
|
|
|
|
|
|
|
|
|
|
|
|
请直接返回总结内容,不要多余的话。"""
|
|
|
|
|
|
|
|
|
|
|
|
summary = ask_deepseek(prompt, system_prompt="你是一个贴心的早安助手,用温暖简洁的语言回复。")
|
|
|
|
|
|
return summary if summary else weather_info
|
|
|
|
|
|
|
|
|
|
|
|
def get_news_summary(news_info):
|
2026-04-11 23:47:08 +08:00
|
|
|
|
"""让DeepSeek把国内外新闻统一整理成中文段落"""
|
2026-04-11 22:02:38 +08:00
|
|
|
|
if not news_info or "获取失败" in news_info:
|
|
|
|
|
|
return news_info
|
|
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
prompt = f"""请阅读以下今日新闻,其中包含国内热点和国外新闻平台标题。请先理解并翻译其中的外文标题,再从中筛选出最值得聊的内容,写成适合早安邮件的三段式中文短评。
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
{news_info}
|
|
|
|
|
|
|
|
|
|
|
|
要求:
|
2026-04-11 23:47:08 +08:00
|
|
|
|
- 固定写成三段,每段之间空一行,不要加小标题
|
|
|
|
|
|
- 第一段:挑 2 到 3 条更偏宏观的新闻开场,可以是国际局势、科技、公共事件,负责把今天的大背景立起来
|
|
|
|
|
|
- 第二段:转到更具体、更有人味的新闻,挑 2 到 3 条民生、人物、生活感更强的内容来写,让叙述落到具体的人和细节上
|
|
|
|
|
|
- 第三段:不要再继续堆新新闻,改成基于前两段做一个自然的观察、总结或轻一点的评价,像看完今天新闻后的一个收束
|
|
|
|
|
|
- 三段之间要有内在联系,像同一个人顺着一个念头慢慢讲下来,而不是三段独立的小作文
|
|
|
|
|
|
- 只选真正值得聊的内容,不要为了凑数把每条新闻都塞进去
|
|
|
|
|
|
- 把外文标题自然翻成中文,不要保留英文原句,也不要写“某媒体说”“我给你翻译几条”这种转述腔
|
|
|
|
|
|
- 叙述顺序要自然,避免一会儿国外一会儿国内来回跳
|
|
|
|
|
|
- 语气像一个信息敏感、表达克制的朋友,在早餐时顺手聊几件今天值得知道的事
|
|
|
|
|
|
- 可以有一点感慨和观察,但不要故作幽默,不要玩梗过多,不要像相声稿
|
|
|
|
|
|
- 不要用列表、序号、小标题,不要分成“国外新闻这边”“国内新闻这边”这种板块汇报
|
|
|
|
|
|
- 不要出现“早上好啊”“对了”“先给你翻译几条”“把这些串起来看”这类口水开场或总结套话
|
|
|
|
|
|
- 总字数控制在 260 到 360 字,第三段宁可短一点,也要起到收束作用
|
|
|
|
|
|
- ⚠️重要:不要使用任何 markdown 符号,如**加粗**、*斜体*、- 列表等,只用纯文本
|
|
|
|
|
|
- 只返回最终正文,不要解释你的写作思路"""
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
2026-04-11 23:47:08 +08:00
|
|
|
|
summary = ask_deepseek(
|
|
|
|
|
|
prompt,
|
|
|
|
|
|
system_prompt="你是一个擅长写晨间新闻短评的中文编辑,知道如何筛选重点、组织主线,并把信息写得自然、克制、有人味。你尤其擅长三段式写法:前两段给信息,最后一段做收束和观察。",
|
|
|
|
|
|
)
|
2026-04-11 22:02:38 +08:00
|
|
|
|
return summary if summary else news_info
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 时间判断 ==============
|
|
|
|
|
|
|
|
|
|
|
|
def is_in_monitor_window():
|
|
|
|
|
|
"""检查是否在监控时间窗口内"""
|
|
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
|
|
current_hour = now.hour
|
|
|
|
|
|
|
|
|
|
|
|
# 凌晨2点到晚上10点
|
|
|
|
|
|
if current_hour >= START_HOUR and current_hour < END_HOUR:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def is_alert_time():
|
|
|
|
|
|
"""检查是否到了8:15提醒时间"""
|
|
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
|
|
if now.hour == ALERT_HOUR and now.minute == ALERT_MINUTE:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# ============== 主监控循环 ==============
|
|
|
|
|
|
|
|
|
|
|
|
# 缓存天气和新闻摘要
|
|
|
|
|
|
_cached_weather_summary = None
|
|
|
|
|
|
_cached_news_summary = None
|
|
|
|
|
|
|
|
|
|
|
|
def get_cached_content():
|
|
|
|
|
|
"""获取缓存的天气和新闻摘要"""
|
|
|
|
|
|
global _cached_weather_summary, _cached_news_summary
|
|
|
|
|
|
|
|
|
|
|
|
# 如果没有缓存,先获取
|
|
|
|
|
|
if _cached_weather_summary is None:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 正在获取天气信息...")
|
|
|
|
|
|
weather_info = get_weather()
|
|
|
|
|
|
_cached_weather_summary = get_weather_summary(weather_info)
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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 '✗ 获取失败'}")
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
if _cached_news_summary is None:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] 正在获取新闻信息...")
|
2026-04-11 23:47:08 +08:00
|
|
|
|
news_info, source_status = get_hot_news_with_status()
|
2026-04-11 22:02:38 +08:00
|
|
|
|
_cached_news_summary = get_news_summary(news_info)
|
2026-04-11 23:47:08 +08:00
|
|
|
|
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 即时生成")
|
2026-04-11 22:02:38 +08:00
|
|
|
|
|
|
|
|
|
|
return _cached_weather_summary, _cached_news_summary
|
|
|
|
|
|
|
|
|
|
|
|
def reset_daily_content_cache():
|
|
|
|
|
|
"""重置每日内容缓存"""
|
|
|
|
|
|
global _cached_weather_summary, _cached_news_summary
|
|
|
|
|
|
_cached_weather_summary = None
|
|
|
|
|
|
_cached_news_summary = None
|
|
|
|
|
|
|
|
|
|
|
|
def monitor_once(state):
|
|
|
|
|
|
"""执行一次监控检查"""
|
|
|
|
|
|
weather_summary, news_summary = get_cached_content()
|
|
|
|
|
|
|
|
|
|
|
|
for ip in TARGET_IPS:
|
|
|
|
|
|
online = ping_host(ip)
|
|
|
|
|
|
|
|
|
|
|
|
if online:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] {ip} 在线")
|
|
|
|
|
|
|
|
|
|
|
|
if not should_send_arrived_today(state, ip):
|
|
|
|
|
|
arrived_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
subject, html = build_email_html("arrived", arrived_time, weather_summary, news_summary)
|
|
|
|
|
|
if send_email(subject, html):
|
|
|
|
|
|
mark_arrived_sent(state, ip)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] {ip} 离线")
|
|
|
|
|
|
|
|
|
|
|
|
def check_not_arrived_alert(state):
|
|
|
|
|
|
"""检查是否需要发送未到提醒"""
|
|
|
|
|
|
weather_summary, news_summary = get_cached_content()
|
|
|
|
|
|
|
|
|
|
|
|
for ip in TARGET_IPS:
|
|
|
|
|
|
# 只有在目标仍然离线的情况下才发送未到提醒
|
|
|
|
|
|
if not ping_host(ip) and not should_send_not_arrived_today(state, ip):
|
|
|
|
|
|
subject, html = build_email_html("not_arrived", None, weather_summary, news_summary)
|
|
|
|
|
|
if send_email(subject, html):
|
|
|
|
|
|
mark_not_arrived_sent(state, ip)
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
print(f"[{datetime.datetime.now()}] LAN监控脚本启动")
|
|
|
|
|
|
print(f"监控IP: {TARGET_IPS}")
|
|
|
|
|
|
print(f"运行时间: {START_HOUR}:00 - {END_HOUR}:00")
|
|
|
|
|
|
print(f"提醒时间: {ALERT_HOUR}:{ALERT_MINUTE:02d}")
|
|
|
|
|
|
print(f"DeepSeek API: {DEEPSEEK_API_URL}")
|
|
|
|
|
|
print("-" * 50)
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(STATE_DIR, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
last_alert_day = None
|
|
|
|
|
|
last_content_day = None
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
|
|
current_day = now.strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
|
|
# 加载状态
|
|
|
|
|
|
state = load_state()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否需要重置每日标记
|
|
|
|
|
|
if reset_daily_state(state):
|
|
|
|
|
|
save_state(state)
|
|
|
|
|
|
print(f"[{now}] 新的一天,重置通知状态")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否需要重置内容缓存
|
|
|
|
|
|
if last_content_day != current_day:
|
|
|
|
|
|
reset_daily_content_cache()
|
|
|
|
|
|
last_content_day = current_day
|
|
|
|
|
|
print(f"[{now}] 新的一天,重置天气新闻缓存")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否在监控时间窗口内
|
|
|
|
|
|
if is_in_monitor_window():
|
|
|
|
|
|
# 执行一次监控
|
|
|
|
|
|
monitor_once(state)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否到8:15提醒时间(每小时检查一次)
|
|
|
|
|
|
if is_alert_time() and last_alert_day != current_day:
|
|
|
|
|
|
check_not_arrived_alert(state)
|
|
|
|
|
|
last_alert_day = current_day
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[{now}] 不在监控时间窗口 ({START_HOUR}:00 - {END_HOUR}:00),等待中...")
|
|
|
|
|
|
|
|
|
|
|
|
# 每30秒检查一次
|
|
|
|
|
|
time.sleep(30)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|