get_weather.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import requests
  2. from bs4 import BeautifulSoup
  3. from config.logger import setup_logging
  4. from plugins_func.register import register_function, ToolType, ActionResponse, Action
  5. from core.utils.util import get_ip_info
  6. from typing import TYPE_CHECKING
  7. if TYPE_CHECKING:
  8. from core.connection import ConnectionHandler
  9. TAG = __name__
  10. logger = setup_logging()
  11. GET_WEATHER_FUNCTION_DESC = {
  12. "type": "function",
  13. "function": {
  14. "name": "get_weather",
  15. "description": (
  16. "获取某个地点的天气,用户应提供一个位置,比如用户说杭州天气,参数为:杭州。"
  17. "如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,默认用该地所在省份的省会城市。"
  18. "如果用户没有指明地点,说“天气怎么样”,”今天天气如何“,location参数为空"
  19. ),
  20. "parameters": {
  21. "type": "object",
  22. "properties": {
  23. "location": {
  24. "type": "string",
  25. "description": "地点名,例如杭州。可选参数,如果不提供则不传",
  26. },
  27. "lang": {
  28. "type": "string",
  29. "description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
  30. },
  31. },
  32. "required": ["lang"],
  33. },
  34. },
  35. }
  36. HEADERS = {
  37. "User-Agent": (
  38. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
  39. "(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
  40. )
  41. }
  42. # 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons
  43. WEATHER_CODE_MAP = {
  44. "100": "晴",
  45. "101": "多云",
  46. "102": "少云",
  47. "103": "晴间多云",
  48. "104": "阴",
  49. "150": "晴",
  50. "151": "多云",
  51. "152": "少云",
  52. "153": "晴间多云",
  53. "300": "阵雨",
  54. "301": "强阵雨",
  55. "302": "雷阵雨",
  56. "303": "强雷阵雨",
  57. "304": "雷阵雨伴有冰雹",
  58. "305": "小雨",
  59. "306": "中雨",
  60. "307": "大雨",
  61. "308": "极端降雨",
  62. "309": "毛毛雨/细雨",
  63. "310": "暴雨",
  64. "311": "大暴雨",
  65. "312": "特大暴雨",
  66. "313": "冻雨",
  67. "314": "小到中雨",
  68. "315": "中到大雨",
  69. "316": "大到暴雨",
  70. "317": "暴雨到大暴雨",
  71. "318": "大暴雨到特大暴雨",
  72. "350": "阵雨",
  73. "351": "强阵雨",
  74. "399": "雨",
  75. "400": "小雪",
  76. "401": "中雪",
  77. "402": "大雪",
  78. "403": "暴雪",
  79. "404": "雨夹雪",
  80. "405": "雨雪天气",
  81. "406": "阵雨夹雪",
  82. "407": "阵雪",
  83. "408": "小到中雪",
  84. "409": "中到大雪",
  85. "410": "大到暴雪",
  86. "456": "阵雨夹雪",
  87. "457": "阵雪",
  88. "499": "雪",
  89. "500": "薄雾",
  90. "501": "雾",
  91. "502": "霾",
  92. "503": "扬沙",
  93. "504": "浮尘",
  94. "507": "沙尘暴",
  95. "508": "强沙尘暴",
  96. "509": "浓雾",
  97. "510": "强浓雾",
  98. "511": "中度霾",
  99. "512": "重度霾",
  100. "513": "严重霾",
  101. "514": "大雾",
  102. "515": "特强浓雾",
  103. "900": "热",
  104. "901": "冷",
  105. "999": "未知",
  106. }
  107. def fetch_city_info(location, api_key, api_host):
  108. url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh"
  109. response = requests.get(url, headers=HEADERS).json()
  110. if response.get("error") is not None:
  111. logger.bind(tag=TAG).error(
  112. f"获取天气失败,原因:{response.get('error', {}).get('detail')}"
  113. )
  114. return None
  115. return response.get("location", [])[0] if response.get("location") else None
  116. def fetch_weather_page(url):
  117. response = requests.get(url, headers=HEADERS)
  118. return BeautifulSoup(response.text, "html.parser") if response.ok else None
  119. def parse_weather_info(soup):
  120. city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
  121. current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
  122. current_abstract = (
  123. current_abstract.get_text(strip=True) if current_abstract else "未知"
  124. )
  125. current_basic = {}
  126. for item in soup.select(
  127. ".c-city-weather-current .current-basic .current-basic___item"
  128. ):
  129. parts = item.get_text(strip=True, separator=" ").split(" ")
  130. if len(parts) == 2:
  131. key, value = parts[1], parts[0]
  132. current_basic[key] = value
  133. temps_list = []
  134. for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
  135. date = row.select_one(".date-bg .date").get_text(strip=True)
  136. weather_code = (
  137. row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0]
  138. )
  139. weather = WEATHER_CODE_MAP.get(weather_code, "未知")
  140. temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
  141. high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
  142. temps_list.append((date, weather, high_temp, low_temp))
  143. return city_name, current_abstract, current_basic, temps_list
  144. @register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
  145. def get_weather(conn: "ConnectionHandler", location: str = None, lang: str = "zh_CN"):
  146. from core.utils.cache.manager import cache_manager, CacheType
  147. weather_config = conn.config.get("plugins", {}).get("get_weather", {})
  148. api_host = weather_config.get("api_host", "mj7p3y7naa.re.qweatherapi.com")
  149. api_key = weather_config.get("api_key", "a861d0d5e7bf4ee1a83d9a9e4f96d4da")
  150. default_location = weather_config.get("default_location", "广州")
  151. client_ip = conn.client_ip
  152. # 优先使用用户提供的location参数
  153. if not location:
  154. # 通过客户端IP解析城市
  155. if client_ip:
  156. # 先从缓存获取IP对应的城市信息
  157. cached_ip_info = cache_manager.get(CacheType.IP_INFO, client_ip)
  158. if cached_ip_info:
  159. location = cached_ip_info.get("city")
  160. else:
  161. # 缓存未命中,调用API获取
  162. ip_info = get_ip_info(client_ip, logger)
  163. if ip_info:
  164. cache_manager.set(CacheType.IP_INFO, client_ip, ip_info)
  165. location = ip_info.get("city")
  166. if not location:
  167. location = default_location
  168. else:
  169. # 若无IP,使用默认位置
  170. location = default_location
  171. # 尝试从缓存获取完整天气报告
  172. weather_cache_key = f"full_weather_{location}_{lang}"
  173. cached_weather_report = cache_manager.get(CacheType.WEATHER, weather_cache_key)
  174. if cached_weather_report:
  175. return ActionResponse(Action.REQLLM, cached_weather_report, None)
  176. # 缓存未命中,获取实时天气数据
  177. city_info = fetch_city_info(location, api_key, api_host)
  178. if not city_info:
  179. return ActionResponse(
  180. Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None
  181. )
  182. soup = fetch_weather_page(city_info["fxLink"])
  183. if not soup:
  184. return ActionResponse(Action.REQLLM, None, "请求失败")
  185. city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
  186. weather_report = f"您查询的位置是:{city_name}\n\n当前天气: {current_abstract}\n"
  187. # 添加有效的当前天气参数
  188. if current_basic:
  189. weather_report += "详细参数:\n"
  190. for key, value in current_basic.items():
  191. if value != "0": # 过滤无效值
  192. weather_report += f" · {key}: {value}\n"
  193. # 添加7天预报
  194. weather_report += "\n未来7天预报:\n"
  195. for date, weather, high, low in temps_list:
  196. weather_report += f"{date}: {weather},气温 {low}~{high}\n"
  197. # 提示语
  198. weather_report += "\n(如需某一天的具体天气,请告诉我日期)"
  199. # 缓存完整的天气报告
  200. cache_manager.set(CacheType.WEATHER, weather_cache_key, weather_report)
  201. return ActionResponse(Action.REQLLM, weather_report, None)