返回列表 发布新帖

[玩法教程] 【青龙面板】和风天气预警脚本

1073 9
发表于 2026-2-6 21:40:59 | 查看全部 阅读模式 IP:–贵州–遵义

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 蓝小白 于 2026-2-11 14:46 编辑
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 天气预警监控脚本(API Host 适配版)
  5. 和风天气 JWT + 企业微信
  6. """

  7. import requests
  8. import json
  9. import sqlite3
  10. import os
  11. import time
  12. import jwt
  13. from datetime import datetime, timedelta

  14. # ==================== 配置读取(修复版) ====================

  15. jwt_key = os.getenv("QWEATHER_PRIVATE_KEY", "").replace('\\n', '\n')
  16. kid = os.getenv("QWEATHER_KID")
  17. pid = os.getenv("QWEATHER_PROJECT_ID")
  18. api_host = os.getenv("QWEATHER_HOST", "").strip()  # 专属API Host,必填
  19. lat = os.getenv("LAT")
  20. lon = os.getenv("LON")
  21. wechat_key = os.getenv("WECHAT_KEY")
  22. mention = os.getenv("WECHAT_MENTION_ALL", "false").lower() == "true"
  23. db_path = "/ql/data/db/weather_warnings.db"

  24. # 验证配置(新增 API_HOST 强制检查)
  25. if not api_host:
  26.     print("❌ 缺少 QWEATHER_HOST(专属API域名)")
  27.     print("   请前往和风天气控制台 → 设置 → 查看你的API Host")
  28.     print("   格式类似:abc1234xyz.def.qweatherapi.com")
  29.     exit(1)

  30. if not all([jwt_key, kid, pid, lat, lon, wechat_key]):
  31.     print("❌ 缺少必要环境变量")
  32.     exit(1)

  33. # ==================== 数据库(防重复推送) ====================

  34. def init_db():
  35.     """初始化数据库并清理过期记录"""
  36.     os.makedirs(os.path.dirname(db_path), exist_ok=True)
  37.     conn = sqlite3.connect(db_path)
  38.     conn.execute('''CREATE TABLE IF NOT EXISTS warnings (
  39.         id TEXT PRIMARY KEY, level TEXT, expire TIMESTAMP)''')
  40.     cur = conn.execute("DELETE FROM warnings WHERE expire < ?", (datetime.now(),))
  41.     if cur.rowcount > 0:
  42.         print(f"🧹 清理 {cur.rowcount} 条过期记录")
  43.     conn.commit()
  44.     conn.close()

  45. def is_sent(wid):
  46.     """检查是否已推送且未过期"""
  47.     conn = sqlite3.connect(db_path)
  48.     res = conn.execute("SELECT 1 FROM warnings WHERE id=? AND expire > ?",
  49.                        (wid, datetime.now())).fetchone()
  50.     conn.close()
  51.     return res is not None

  52. def save(wid, level, hours=48):
  53.     """保存预警"""
  54.     expire = datetime.now() + timedelta(hours=hours)
  55.     conn = sqlite3.connect(db_path)
  56.     conn.execute("INSERT OR REPLACE INTO warnings VALUES (?,?,?)", (wid, level, expire))
  57.     conn.commit()
  58.     conn.close()

  59. # ==================== JWT 生成 ====================

  60. def get_token():
  61.     """生成 Ed25519 JWT Token"""
  62.     try:
  63.         now = int(time.time())
  64.         headers = {"alg": "EdDSA", "kid": kid, "typ": "JWT"}
  65.         payload = {"sub": pid, "iat": now-30, "exp": now+900}
  66.         token = jwt.encode(payload, jwt_key, algorithm='EdDSA', headers=headers)
  67.         print("✅ JWT 生成成功")
  68.         return token
  69.     except Exception as e:
  70.         print(f"❌ JWT 生成失败: {e}")
  71.         return None

  72. # ==================== 和风天气 API(修复版) ====================

  73. def get_alerts(token):
  74.     """获取实时天气预警(使用专属API Host)"""
  75.     url = f"https://{api_host}/weatheralert/v1/current/{lat}/{lon}"
  76.     print(f"🌐 API Host: {api_host}")
  77.    
  78.     try:
  79.         r = requests.get(url,
  80.                         headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
  81.                         timeout=30)
  82.         
  83.         if r.status_code == 200:
  84.             data = r.json()
  85.             if data.get("metadata", {}).get("zeroResult"):
  86.                 return []
  87.             return data.get("alerts", [])
  88.         elif r.status_code == 403:
  89.             print("❌ 403 权限错误:API Host 不正确或 JWT 认证失败")
  90.             print(f"   当前使用的 API Host: {api_host}")
  91.             print("   请确认这是控制台显示的专属API Host")
  92.         elif r.status_code == 400:
  93.             print("❌ 坐标不支持,请更换大城市坐标")
  94.         elif r.status_code == 401:
  95.             print("❌ JWT认证失败,检查KID和Project ID")
  96.         else:
  97.             print(f"❌ API错误: {r.status_code}")
  98.             print(f"   响应: {r.text[:200]}")
  99.     except Exception as e:
  100.         print(f"❌ 网络错误: {e}")
  101.     return []

  102. # ==================== 企业微信推送 ====================

  103. def send(msg):
  104.     """推送消息到企业微信"""
  105.     url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={wechat_key}"
  106.    
  107.     if mention:
  108.         # 纯文本模式,支持 @所有人
  109.         text = msg.replace('**','').replace('#','').replace('<font color="info">','')\
  110.                   .replace('<font color="warning">','').replace('</font>','').replace('`','')
  111.         data = {"msgtype": "text", "text": {"content": text, "mentioned_list": ["@all"]}}
  112.     else:
  113.         # Markdown 模式,带颜色
  114.         data = {"msgtype": "markdown", "markdown": {"content": msg}}
  115.    
  116.     try:
  117.         r = requests.post(url, json=data, timeout=10)
  118.         return r.json().get('errcode') == 0
  119.     except Exception as e:
  120.         print(f"   推送异常: {e}")
  121.         return False

  122. def format_msg(alert):
  123.     """格式化预警消息"""
  124.     colors = {
  125.         "minor": ("🔵", "info", "蓝色"),
  126.         "moderate": ("🟡", "warning", "黄色"),
  127.         "severe": ("🟠", "warning", "橙色"),
  128.         "extreme": ("🔴", "warning", "红色")
  129.     }
  130.     icon, color, name = colors.get(alert.get('severity'), ("⚪", "comment", "未知"))
  131.    
  132.     status_map = {"alert": "🆕 新发布", "update": "📝 更新", "cancel": "❌ 取消"}
  133.     status = status_map.get(alert.get('messageType', {}).get('code', 'alert'), "未知")
  134.    
  135.     lines = [
  136.         f"## {icon} 天气预警 - <font color='{color}'>{name}</font>",
  137.         f"",
  138.         f"**预警类型:** {alert.get('eventType', {}).get('name', '未知')}",
  139.         f"**预警标题:** {alert.get('headline', '')}",
  140.         f"**发布状态:** {status}",
  141.         f"**发布机构:** {alert.get('senderName', '未知机构')}",
  142.         f"**严重程度:** <font color='{color}'>{alert.get('severity', '未知')}</font>",
  143.         f"",
  144.         f"### 📝 预警详情",
  145.         f"{alert.get('description', '暂无详情')}",
  146.     ]
  147.    
  148.     if alert.get('instruction'):
  149.         lines.extend([f"", f"### 🛡️ 防御指南", f"{alert['instruction']}"])
  150.    
  151.     lines.extend([
  152.         f"",
  153.         f"---",
  154.         f"⏰ **发布时间:** `{alert.get('issuedTime', '未知')}`",
  155.         f"⏳ **失效时间:** `{alert.get('expireTime', '未知')}`"
  156.     ])
  157.    
  158.     return "\n".join(lines)

  159. # ==================== 主程序 ====================

  160. def main():
  161.     print(f"🕐 当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  162.     print(f"📍 监控坐标:{lat}, {lon}")
  163.     print(f"👥 @全体成员:{'开启' if mention else '关闭'}")
  164.    
  165.     init_db()
  166.    
  167.     token = get_token()
  168.     if not token:
  169.         return
  170.    
  171.     alerts = get_alerts(token)
  172.     if not alerts:
  173.         print("ℹ️ 当前无生效中的天气预警")
  174.         return
  175.    
  176.     sent = 0
  177.     for alert in alerts:
  178.         wid = alert.get('id')
  179.         if not wid:
  180.             continue
  181.         
  182.         if is_sent(wid):
  183.             print(f"⏭️ 已推送过: {alert.get('headline','')[:20]}...")
  184.             continue
  185.         
  186.         if send(format_msg(alert)):
  187.             # 计算过期时间
  188.             hours = 48
  189.             if alert.get('expireTime'):
  190.                 try:
  191.                     et = alert['expireTime'].replace('+08:00','').replace('Z','')
  192.                     expire_dt = datetime.strptime(et, '%Y-%m-%dT%H:%M')
  193.                     hours = max(1, (expire_dt - datetime.now()).total_seconds() / 3600)
  194.                 except:
  195.                     pass
  196.             
  197.             save(wid, alert.get('severity'), hours)
  198.             sent += 1
  199.             print(f"✅ 推送成功: {alert.get('headline','')[:30]}...")
  200.         else:
  201.             print(f"❌ 推送失败")
  202.    
  203.     print(f"🎉 完成:新推{sent}条")

  204. if __name__ == "__main__":
  205.     main()
复制代码


评论9

蓝小白楼主Lv.4 发表于 2026-3-10 09:56:10 | 查看全部 IP:–四川–成都
青龙非最新版本有安全风险,请各位务必拉取最新的镜像或者关注青龙官方的修复信息哈~
梦梦Lv.7绿联NAS社区会员用户 发表于 2026-2-7 10:59:55 来自手机 | 查看全部 IP:–河北–衡水
牛蛙!牛蛙!
playokLv.4 发表于 2026-2-7 14:33:03 | 查看全部 IP:–广东–佛山–高明区
厉害!!厉害!!
潇潇雨歇Lv.4 发表于 2026-2-9 09:44:08 | 查看全部 IP:–广东–佛山–高明区
🐮🍺
蓝小白楼主Lv.4 发表于 2026-2-9 10:27:16 | 查看全部 IP:–贵州–遵义
蓝小白楼主Lv.4 发表于 2026-2-9 10:28:37 | 查看全部 IP:–贵州–遵义

你快写哦   都七级了
梦梦Lv.7绿联NAS社区会员用户 发表于 2026-2-10 15:24:11 来自手机 | 查看全部 IP:–江西–南昌
蓝小白 发表于 2026-2-9 10:28
你快写哦   都七级了

6
C6KoClLv.1 发表于 2026-3-12 16:30:06 | 查看全部 IP:–福建–福州
如果不推企微的话,怎么做
蓝小白楼主Lv.4 发表于 2026-3-13 08:30:57 来自手机 | 查看全部 IP:–四川–成都
C6KoCl 发表于 2026-3-12 16:30
如果不推企微的话,怎么做

可以问ai

评论

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2026 绿联NAS私有云社区 版权所有 All Rights Reserved. 粤公网安备44030002002555号| 粤ICP备12028978号
关灯 在本版发帖
联系技术支持
返回顶部
快速回复 返回顶部 返回列表