Files
wox.plugin.weather/Main.cs
2025-06-25 13:55:23 +08:00

456 lines
20 KiB
C#
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.

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Wox.Plugin;
namespace Wox.Plugin.Weather
{
public class Main : IPlugin
{
private PluginInitContext _context;
private const string API_KEY = "f4aeaa37939b47aa864a6e1b3ad4a084";
private const string API_HOST = "nm6hewkf7w.re.qweatherapi.com";
public void Init(PluginInitContext context)
{
_context = context;
LogInfo("Weather Plugin initialized with built-in API key");
}
public List<Result> Query(Query query)
{
List<Result> resultList = new List<Result>();
try
{
string cityName;
if (string.IsNullOrEmpty(query.Search))
{
// 如果没有输入城市名称通过IP获取当前位置
cityName = GetCityFromIP();
if (string.IsNullOrEmpty(cityName))
{
cityName = "北京"; // 如果IP定位失败使用默认城市
LogInfo("IP定位失败使用默认城市北京");
}
else
{
LogInfo(string.Format("通过IP定位获取到城市{0}", cityName));
}
}
else
{
cityName = query.Search;
}
LogInfo(string.Format("Weather query started for city: {0}", cityName));
var weatherData = GetWeatherData(cityName);
if (weatherData != null)
{
LogInfo(string.Format("Weather data retrieved successfully for {0}: {1}℃, {2}", cityName, weatherData.Temperature, weatherData.Description));
resultList.Add(new Result
{
Title = string.Format("{0}, 实时: {1}℃, {2}", weatherData.City, weatherData.Temperature, weatherData.Description),
SubTitle = string.Format("湿度: {0}%, 风速: {1}km/h", weatherData.Humidity, weatherData.WindSpeed),
IcoPath = string.Format("Images\\weatherpic\\{0}.png", weatherData.IconCode),
Action = e => false
});
foreach (var forecast in weatherData.Forecasts)
{
string dayPrefix = GetDayPrefix(forecast.Date);
resultList.Add(new Result
{
Title = string.Format("{0}, {1}-{2}℃", forecast.Description, forecast.MaxTemp, forecast.MinTemp),
SubTitle = string.Format("{0}{1}, {2}", dayPrefix, GetWeekDay(forecast.Date.DayOfWeek), forecast.Date.ToShortDateString()),
IcoPath = string.Format("Images\\weatherpic\\{0}.png", forecast.IconCode),
Action = e => false
});
}
}
else
{
LogError(string.Format("Failed to retrieve weather data for city: {0}", cityName));
resultList.Add(new Result
{
Title = string.Format("无法获取 {0} 的天气信息", cityName),
SubTitle = "请检查城市名称是否正确,或稍后重试",
IcoPath = "Images\\logo.png",
Action = e => false
});
}
}
catch (Exception ex)
{
LogError(string.Format("Exception in Query method: {0}", ex.Message));
resultList.Add(new Result
{
Title = "请输入城市名称或者拼音",
SubTitle = "API调用失败请稍后重试",
IcoPath = "Images\\logo.png",
Action = e => false
});
}
return resultList;
}
private WeatherData GetWeatherData(string cityName)
{
try
{
LogInfo(string.Format("Getting location ID for city: {0}", cityName));
string locationId = GetLocationId(cityName, API_KEY);
if (string.IsNullOrEmpty(locationId))
{
LogError(string.Format("Failed to get location ID for city: {0}", cityName));
return null;
}
LogInfo(string.Format("Location ID found: {0} for city: {1}", locationId, cityName));
string currentWeatherUrl = string.Format("https://{0}/v7/weather/now?location={1}&key={2}", API_HOST, locationId, API_KEY);
LogInfo(string.Format("Requesting current weather from: {0}", currentWeatherUrl.Replace(API_KEY, "***")));
string currentResponse = MakeHttpRequest(currentWeatherUrl);
string forecastUrl = string.Format("https://{0}/v7/weather/7d?location={1}&key={2}", API_HOST, locationId, API_KEY);
LogInfo(string.Format("Requesting forecast from: {0}", forecastUrl.Replace(API_KEY, "***")));
string forecastResponse = MakeHttpRequest(forecastUrl);
if (!string.IsNullOrEmpty(currentResponse) && !string.IsNullOrEmpty(forecastResponse))
{
LogInfo(string.Format("API responses received - Current: {0} chars, Forecast: {1} chars", currentResponse.Length, forecastResponse.Length));
// 检查当前天气响应是否为有效JSON
if (!currentResponse.TrimStart().StartsWith("{") && !currentResponse.TrimStart().StartsWith("["))
{
LogError(string.Format("Current weather response is not valid JSON. First 200 chars: {0}",
currentResponse.Length > 200 ? currentResponse.Substring(0, 200) : currentResponse));
return null;
}
// 检查预报响应是否为有效JSON
if (!forecastResponse.TrimStart().StartsWith("{") && !forecastResponse.TrimStart().StartsWith("["))
{
LogError(string.Format("Forecast response is not valid JSON. First 200 chars: {0}",
forecastResponse.Length > 200 ? forecastResponse.Substring(0, 200) : forecastResponse));
return null;
}
var currentJson = JObject.Parse(currentResponse);
var forecastJson = JObject.Parse(forecastResponse);
string currentCode = (string)currentJson["code"];
string forecastCode = (string)forecastJson["code"];
LogInfo(string.Format("API response codes - Current: {0}, Forecast: {1}", currentCode, forecastCode));
if (currentCode != "200" || forecastCode != "200")
{
LogError(string.Format("API returned error codes - Current: {0}, Forecast: {1}", currentCode, forecastCode));
return null;
}
var current = currentJson["now"];
var dailyForecasts = forecastJson["daily"];
var weatherData = new WeatherData();
weatherData.City = cityName;
weatherData.Temperature = (int)current["temp"];
weatherData.Description = (string)current["text"];
weatherData.Humidity = (int)current["humidity"];
weatherData.WindSpeed = (int)current["windSpeed"];
weatherData.IconCode = GetHeFengIconCode((string)current["icon"]);
foreach (var day in dailyForecasts.Take(5))
{
var forecast = new ForecastData();
forecast.Date = DateTime.Parse((string)day["fxDate"]);
forecast.MaxTemp = (int)day["tempMax"];
forecast.MinTemp = (int)day["tempMin"];
forecast.Description = (string)day["textDay"];
forecast.IconCode = GetHeFengIconCode((string)day["iconDay"]);
weatherData.Forecasts.Add(forecast);
}
return weatherData;
}
else
{
LogError("Empty response received from weather API");
}
}
catch (Exception ex)
{
LogError(string.Format("Exception in GetWeatherData: {0}", ex.Message));
}
return null;
}
private string GetLocationId(string cityName, string apiKey)
{
try
{
// 根据公告geoapi需要特殊处理域名替换为API Host路径从v2改为geo/v2
string searchUrl = string.Format("https://{0}/geo/v2/city/lookup?location={1}&key={2}", API_HOST, Uri.EscapeDataString(cityName), apiKey);
LogInfo(string.Format("Requesting location ID from: {0}", searchUrl.Replace(apiKey, "***")));
string response = MakeHttpRequest(searchUrl);
if (!string.IsNullOrEmpty(response))
{
LogInfo(string.Format("Location search response: {0} chars", response.Length));
// 记录响应内容的前100个字符用于调试
string responsePreview = response.Length > 100 ? response.Substring(0, 100) + "..." : response;
LogInfo(string.Format("Response preview: {0}", responsePreview));
// 检查响应是否看起来像JSON
if (!response.TrimStart().StartsWith("{") && !response.TrimStart().StartsWith("["))
{
LogError(string.Format("Response does not appear to be JSON. First 200 chars: {0}",
response.Length > 200 ? response.Substring(0, 200) : response));
return null;
}
var json = JObject.Parse(response);
string code = (string)json["code"];
LogInfo(string.Format("Location search response code: {0}", code));
if (code == "200" && json["location"] != null && json["location"].HasValues)
{
string locationId = (string)json["location"][0]["id"];
LogInfo(string.Format("Location ID found: {0}", locationId));
return locationId;
}
else
{
LogError(string.Format("Location search failed - Code: {0}, HasLocation: {1}", code, json["location"] != null && json["location"].HasValues));
}
}
else
{
LogError("Empty response from location search API");
}
}
catch (Exception ex)
{
LogError(string.Format("Exception in GetLocationId: {0}", ex.Message));
}
return null;
}
private string GetCityFromIP()
{
try
{
LogInfo("开始通过IP获取城市位置");
// 使用免费的ip-api.com服务获取IP位置信息
string ipApiUrl = "http://ip-api.com/json/?lang=zh-CN&fields=status,message,country,regionName,city";
LogInfo(string.Format("请求IP定位API: {0}", ipApiUrl));
string response = MakeHttpRequest(ipApiUrl);
if (!string.IsNullOrEmpty(response))
{
LogInfo(string.Format("IP定位API响应: {0} chars", response.Length));
var json = JObject.Parse(response);
string status = (string)json["status"];
if (status == "success")
{
string country = (string)json["country"];
string region = (string)json["regionName"];
string city = (string)json["city"];
LogInfo(string.Format("IP定位成功 - 国家: {0}, 省份: {1}, 城市: {2}", country, region, city));
// 优先返回城市,如果没有城市则返回省份
if (!string.IsNullOrEmpty(city))
{
return city;
}
else if (!string.IsNullOrEmpty(region))
{
return region;
}
else
{
LogError("IP定位成功但未获取到有效的城市或省份信息");
return null;
}
}
else
{
string message = (string)json["message"];
LogError(string.Format("IP定位失败 - Status: {0}, Message: {1}", status, message));
return null;
}
}
else
{
LogError("IP定位API返回空响应");
return null;
}
}
catch (Exception ex)
{
LogError(string.Format("IP定位异常: {0}", ex.Message));
return null;
}
}
private string MakeHttpRequest(string url)
{
try
{
LogInfo(string.Format("Making HTTP request to: {0}", url.Replace(API_KEY, "***")));
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
request.Timeout = 10000; // 增加超时时间到10秒
request.UserAgent = "Wox Weather Plugin/1.2.0";
// 明确设置接受的编码和内容类型
request.Accept = "application/json, text/plain, */*";
request.Headers.Add("Accept-Encoding", "gzip, deflate");
request.Headers.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
// 自动解压缩响应
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
LogInfo(string.Format("HTTP response received - Status: {0}, ContentLength: {1}, ContentEncoding: {2}, ContentType: {3}",
response.StatusCode, response.ContentLength, response.ContentEncoding, response.ContentType));
using (Stream responseStream = response.GetResponseStream())
{
// 根据响应头确定编码
Encoding encoding = Encoding.UTF8;
if (!string.IsNullOrEmpty(response.CharacterSet))
{
try
{
encoding = Encoding.GetEncoding(response.CharacterSet);
LogInfo(string.Format("Using encoding from response header: {0}", response.CharacterSet));
}
catch
{
LogInfo("Failed to parse charset from response, using UTF-8");
encoding = Encoding.UTF8;
}
}
using (StreamReader reader = new StreamReader(responseStream, encoding))
{
string result = reader.ReadToEnd();
LogInfo(string.Format("HTTP response content length: {0} characters", result.Length));
// 记录响应的前200个字符用于调试
string debugPreview = result.Length > 200 ? result.Substring(0, 200) + "..." : result;
LogInfo(string.Format("Response content preview: {0}", debugPreview));
return result;
}
}
}
}
catch (Exception ex)
{
LogError(string.Format("HTTP request failed for URL {0}: {1}", url.Replace(API_KEY, "***"), ex.Message));
return null;
}
}
private string GetHeFengIconCode(string heFengIcon)
{
var codeMap = new Dictionary<string, string>();
codeMap.Add("100", "100"); codeMap.Add("150", "100");
codeMap.Add("101", "101"); codeMap.Add("102", "102"); codeMap.Add("103", "103");
codeMap.Add("151", "101"); codeMap.Add("152", "102"); codeMap.Add("153", "103");
codeMap.Add("104", "104"); codeMap.Add("300", "300"); codeMap.Add("301", "301");
codeMap.Add("302", "302"); codeMap.Add("303", "303"); codeMap.Add("304", "304");
codeMap.Add("305", "305"); codeMap.Add("306", "306"); codeMap.Add("307", "307");
codeMap.Add("308", "308"); codeMap.Add("309", "309"); codeMap.Add("310", "310");
codeMap.Add("400", "400"); codeMap.Add("401", "401"); codeMap.Add("402", "402");
codeMap.Add("403", "403"); codeMap.Add("404", "404"); codeMap.Add("500", "501");
return codeMap.ContainsKey(heFengIcon) ? codeMap[heFengIcon] : "999";
}
private string GetDayPrefix(DateTime date)
{
if (date.Date == DateTime.Today) return "今天, ";
else if (date.Date == DateTime.Today.AddDays(1)) return "明天, ";
else if (date.Date == DateTime.Today.AddDays(2)) return "后天, ";
else return "";
}
private string GetWeekDay(DayOfWeek dayOfWeek)
{
string[] weekDays = { "周日", "周一", "周二", "周三", "周四", "周五", "周六" };
return weekDays[(int)dayOfWeek];
}
private void LogInfo(string message)
{
try
{
WriteLog("INFO", message);
}
catch
{
// 如果日志记录失败,忽略错误以避免影响主要功能
}
}
private void LogError(string message)
{
try
{
WriteLog("ERROR", message);
}
catch
{
// 如果日志记录失败,忽略错误以避免影响主要功能
}
}
private void WriteLog(string level, string message)
{
try
{
string logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Wox", "Logs");
if (!Directory.Exists(logDir))
Directory.CreateDirectory(logDir);
string logFile = Path.Combine(logDir, "Weather.Plugin.log");
string logEntry = string.Format("[{0}] [{1}] {2}: {3}{4}",
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
level,
"Weather Plugin",
message,
Environment.NewLine);
File.AppendAllText(logFile, logEntry);
}
catch
{
// 如果日志记录失败,忽略错误以避免影响主要功能
}
}
}
public class WeatherData
{
public WeatherData() { Forecasts = new List<ForecastData>(); }
public string City { get; set; }
public int Temperature { get; set; }
public string Description { get; set; }
public int Humidity { get; set; }
public int WindSpeed { get; set; }
public string IconCode { get; set; }
public List<ForecastData> Forecasts { get; set; }
}
public class ForecastData
{
public DateTime Date { get; set; }
public int MaxTemp { get; set; }
public int MinTemp { get; set; }
public string Description { get; set; }
public string IconCode { get; set; }
}
}