Files
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

913 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
import xml.etree.ElementTree as ET
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 = "成都"
# 国外新闻代理配置
NEWS_PROXY_URL = os.getenv("NEWS_PROXY_URL", "http://127.0.0.1:7890").strip() or None
# 运行时间配置
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",
}
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):
"""发送邮件"""
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}"
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):
"""构建邮件HTML内容"""
if status == "arrived":
subject = f"😤 早上坏"
else:
subject = f"😊 早上好"
# 获取AI生成的文案
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>
<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>
</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
def fetch_text(url, headers=None, timeout=10, proxy_url=None):
"""发起HTTP请求并返回文本内容"""
request_headers = dict(DEFAULT_HTTP_HEADERS)
if headers:
request_headers.update(headers)
req = urllib.request.Request(url, headers=request_headers)
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')
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():
"""获取当天天气信息(带缓存)"""
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 = ""
weather_ok = False
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 = "天气信息获取失败"
else:
weather_ok = True
except Exception as e:
print(f"[{datetime.datetime.now()}] 获取天气失败: {e}")
weather_info = "天气信息获取失败"
print(f"[{datetime.datetime.now()}] - wttr.in ({WEATHER_LOCATION}): {'✓ 获取成功' if weather_ok else '✗ 获取失败'}")
# 保存缓存
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
# ============== 新闻获取函数 ==============
def get_hot_news_with_status():
"""获取当天热搜新闻(带缓存),同时返回各源状态"""
today = get_today_str()
source_status = []
# 检查缓存
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
and cache.get("version") == NEWS_CACHE_VERSION
and time.time() - cache.get("timestamp", 0) < CACHE_EXPIRE
and not is_failed_content(cached_news)
):
return cached_news, []
except Exception:
pass
news_info = ""
try:
domestic_titles = get_domestic_news_titles()
domestic_ok = bool(domestic_titles)
source_status.append(("国内热点 (百度)", domestic_ok, len(domestic_titles)))
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))
sections = []
if domestic_titles:
sections.append(
"国内热点:\n" + "\n".join(
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:
print(f"[{datetime.datetime.now()}] 获取新闻失败: {e}")
if not news_info:
news_info = "新闻信息获取失败"
# 保存缓存
os.makedirs(STATE_DIR, exist_ok=True)
cache_data = {
"date": today,
"version": NEWS_CACHE_VERSION,
"timestamp": time.time(),
"news": news_info
}
try:
with open(NEWS_CACHE_FILE, 'w') as f:
json.dump(cache_data, f)
except Exception:
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
# ============== 附言生成函数 ==============
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 处理函数 ==============
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):
"""让DeepSeek把国内外新闻统一整理成中文段落"""
if not news_info or "获取失败" in news_info:
return news_info
prompt = f"""请阅读以下今日新闻,其中包含国内热点和国外新闻平台标题。请先理解并翻译其中的外文标题,再从中筛选出最值得聊的内容,写成适合早安邮件的三段式中文短评。
{news_info}
要求:
- 固定写成三段,每段之间空一行,不要加小标题
- 第一段:挑 2 到 3 条更偏宏观的新闻开场,可以是国际局势、科技、公共事件,负责把今天的大背景立起来
- 第二段:转到更具体、更有人味的新闻,挑 2 到 3 条民生、人物、生活感更强的内容来写,让叙述落到具体的人和细节上
- 第三段:不要再继续堆新新闻,改成基于前两段做一个自然的观察、总结或轻一点的评价,像看完今天新闻后的一个收束
- 三段之间要有内在联系,像同一个人顺着一个念头慢慢讲下来,而不是三段独立的小作文
- 只选真正值得聊的内容,不要为了凑数把每条新闻都塞进去
- 把外文标题自然翻成中文,不要保留英文原句,也不要写“某媒体说”“我给你翻译几条”这种转述腔
- 叙述顺序要自然,避免一会儿国外一会儿国内来回跳
- 语气像一个信息敏感、表达克制的朋友,在早餐时顺手聊几件今天值得知道的事
- 可以有一点感慨和观察,但不要故作幽默,不要玩梗过多,不要像相声稿
- 不要用列表、序号、小标题,不要分成“国外新闻这边”“国内新闻这边”这种板块汇报
- 不要出现“早上好啊”“对了”“先给你翻译几条”“把这些串起来看”这类口水开场或总结套话
- 总字数控制在 260 到 360 字,第三段宁可短一点,也要起到收束作用
- ⚠️重要:不要使用任何 markdown 符号,如**加粗**、*斜体*、- 列表等,只用纯文本
- 只返回最终正文,不要解释你的写作思路"""
summary = ask_deepseek(
prompt,
system_prompt="你是一个擅长写晨间新闻短评的中文编辑,知道如何筛选重点、组织主线,并把信息写得自然、克制、有人味。你尤其擅长三段式写法:前两段给信息,最后一段做收束和观察。",
)
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)
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:
print(f"[{datetime.datetime.now()}] 正在获取新闻信息...")
news_info, source_status = get_hot_news_with_status()
_cached_news_summary = get_news_summary(news_info)
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
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()