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