马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本帖最后由 蓝小白 于 2026-2-11 14:46 编辑
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 天气预警监控脚本(API Host 适配版)
- 和风天气 JWT + 企业微信
- """
- import requests
- import json
- import sqlite3
- import os
- import time
- import jwt
- from datetime import datetime, timedelta
- # ==================== 配置读取(修复版) ====================
- jwt_key = os.getenv("QWEATHER_PRIVATE_KEY", "").replace('\\n', '\n')
- kid = os.getenv("QWEATHER_KID")
- pid = os.getenv("QWEATHER_PROJECT_ID")
- api_host = os.getenv("QWEATHER_HOST", "").strip() # 专属API Host,必填
- lat = os.getenv("LAT")
- lon = os.getenv("LON")
- wechat_key = os.getenv("WECHAT_KEY")
- mention = os.getenv("WECHAT_MENTION_ALL", "false").lower() == "true"
- db_path = "/ql/data/db/weather_warnings.db"
- # 验证配置(新增 API_HOST 强制检查)
- if not api_host:
- print("❌ 缺少 QWEATHER_HOST(专属API域名)")
- print(" 请前往和风天气控制台 → 设置 → 查看你的API Host")
- print(" 格式类似:abc1234xyz.def.qweatherapi.com")
- exit(1)
- if not all([jwt_key, kid, pid, lat, lon, wechat_key]):
- print("❌ 缺少必要环境变量")
- exit(1)
- # ==================== 数据库(防重复推送) ====================
- def init_db():
- """初始化数据库并清理过期记录"""
- os.makedirs(os.path.dirname(db_path), exist_ok=True)
- conn = sqlite3.connect(db_path)
- conn.execute('''CREATE TABLE IF NOT EXISTS warnings (
- id TEXT PRIMARY KEY, level TEXT, expire TIMESTAMP)''')
- cur = conn.execute("DELETE FROM warnings WHERE expire < ?", (datetime.now(),))
- if cur.rowcount > 0:
- print(f"🧹 清理 {cur.rowcount} 条过期记录")
- conn.commit()
- conn.close()
- def is_sent(wid):
- """检查是否已推送且未过期"""
- conn = sqlite3.connect(db_path)
- res = conn.execute("SELECT 1 FROM warnings WHERE id=? AND expire > ?",
- (wid, datetime.now())).fetchone()
- conn.close()
- return res is not None
- def save(wid, level, hours=48):
- """保存预警"""
- expire = datetime.now() + timedelta(hours=hours)
- conn = sqlite3.connect(db_path)
- conn.execute("INSERT OR REPLACE INTO warnings VALUES (?,?,?)", (wid, level, expire))
- conn.commit()
- conn.close()
- # ==================== JWT 生成 ====================
- def get_token():
- """生成 Ed25519 JWT Token"""
- try:
- now = int(time.time())
- headers = {"alg": "EdDSA", "kid": kid, "typ": "JWT"}
- payload = {"sub": pid, "iat": now-30, "exp": now+900}
- token = jwt.encode(payload, jwt_key, algorithm='EdDSA', headers=headers)
- print("✅ JWT 生成成功")
- return token
- except Exception as e:
- print(f"❌ JWT 生成失败: {e}")
- return None
- # ==================== 和风天气 API(修复版) ====================
- def get_alerts(token):
- """获取实时天气预警(使用专属API Host)"""
- url = f"https://{api_host}/weatheralert/v1/current/{lat}/{lon}"
- print(f"🌐 API Host: {api_host}")
-
- try:
- r = requests.get(url,
- headers={'Authorization': f'Bearer {token}', 'Accept': 'application/json'},
- timeout=30)
-
- if r.status_code == 200:
- data = r.json()
- if data.get("metadata", {}).get("zeroResult"):
- return []
- return data.get("alerts", [])
- elif r.status_code == 403:
- print("❌ 403 权限错误:API Host 不正确或 JWT 认证失败")
- print(f" 当前使用的 API Host: {api_host}")
- print(" 请确认这是控制台显示的专属API Host")
- elif r.status_code == 400:
- print("❌ 坐标不支持,请更换大城市坐标")
- elif r.status_code == 401:
- print("❌ JWT认证失败,检查KID和Project ID")
- else:
- print(f"❌ API错误: {r.status_code}")
- print(f" 响应: {r.text[:200]}")
- except Exception as e:
- print(f"❌ 网络错误: {e}")
- return []
- # ==================== 企业微信推送 ====================
- def send(msg):
- """推送消息到企业微信"""
- url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={wechat_key}"
-
- if mention:
- # 纯文本模式,支持 @所有人
- text = msg.replace('**','').replace('#','').replace('<font color="info">','')\
- .replace('<font color="warning">','').replace('</font>','').replace('`','')
- data = {"msgtype": "text", "text": {"content": text, "mentioned_list": ["@all"]}}
- else:
- # Markdown 模式,带颜色
- data = {"msgtype": "markdown", "markdown": {"content": msg}}
-
- try:
- r = requests.post(url, json=data, timeout=10)
- return r.json().get('errcode') == 0
- except Exception as e:
- print(f" 推送异常: {e}")
- return False
- def format_msg(alert):
- """格式化预警消息"""
- colors = {
- "minor": ("🔵", "info", "蓝色"),
- "moderate": ("🟡", "warning", "黄色"),
- "severe": ("🟠", "warning", "橙色"),
- "extreme": ("🔴", "warning", "红色")
- }
- icon, color, name = colors.get(alert.get('severity'), ("⚪", "comment", "未知"))
-
- status_map = {"alert": "🆕 新发布", "update": "📝 更新", "cancel": "❌ 取消"}
- status = status_map.get(alert.get('messageType', {}).get('code', 'alert'), "未知")
-
- lines = [
- f"## {icon} 天气预警 - <font color='{color}'>{name}</font>",
- f"",
- f"**预警类型:** {alert.get('eventType', {}).get('name', '未知')}",
- f"**预警标题:** {alert.get('headline', '')}",
- f"**发布状态:** {status}",
- f"**发布机构:** {alert.get('senderName', '未知机构')}",
- f"**严重程度:** <font color='{color}'>{alert.get('severity', '未知')}</font>",
- f"",
- f"### 📝 预警详情",
- f"{alert.get('description', '暂无详情')}",
- ]
-
- if alert.get('instruction'):
- lines.extend([f"", f"### 🛡️ 防御指南", f"{alert['instruction']}"])
-
- lines.extend([
- f"",
- f"---",
- f"⏰ **发布时间:** `{alert.get('issuedTime', '未知')}`",
- f"⏳ **失效时间:** `{alert.get('expireTime', '未知')}`"
- ])
-
- return "\n".join(lines)
- # ==================== 主程序 ====================
- def main():
- print(f"🕐 当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
- print(f"📍 监控坐标:{lat}, {lon}")
- print(f"👥 @全体成员:{'开启' if mention else '关闭'}")
-
- init_db()
-
- token = get_token()
- if not token:
- return
-
- alerts = get_alerts(token)
- if not alerts:
- print("ℹ️ 当前无生效中的天气预警")
- return
-
- sent = 0
- for alert in alerts:
- wid = alert.get('id')
- if not wid:
- continue
-
- if is_sent(wid):
- print(f"⏭️ 已推送过: {alert.get('headline','')[:20]}...")
- continue
-
- if send(format_msg(alert)):
- # 计算过期时间
- hours = 48
- if alert.get('expireTime'):
- try:
- et = alert['expireTime'].replace('+08:00','').replace('Z','')
- expire_dt = datetime.strptime(et, '%Y-%m-%dT%H:%M')
- hours = max(1, (expire_dt - datetime.now()).total_seconds() / 3600)
- except:
- pass
-
- save(wid, alert.get('severity'), hours)
- sent += 1
- print(f"✅ 推送成功: {alert.get('headline','')[:30]}...")
- else:
- print(f"❌ 推送失败")
-
- print(f"🎉 完成:新推{sent}条")
- if __name__ == "__main__":
- main()
复制代码
|