commit a23f0b55b445f128364d52d9c58c26d6de6b9e48 Author: tradewind Date: Wed Jun 25 13:55:23 2025 +0800 init weather plugin diff --git a/Main.cs b/Main.cs new file mode 100644 index 0000000..4b06477 --- /dev/null +++ b/Main.cs @@ -0,0 +1,456 @@ +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 Query(Query query) + { + List resultList = new List(); + 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(); + 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(); } + 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 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; } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..970f17d --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Wox.Plugin.Weather")] +[assembly: AssemblyDescription("Weather plugin for Wox launcher")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Wox.Plugin.Weather")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d2e3c23b-084d-411d-b66f-e0c79d6c2a6f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3f995c --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Wox Weather Plugin - 和风天气版本(内置API Key) + +这是一个使用和风天气API的Wox天气插件,使用内置的API Key,无需额外配置。 + +## 主要特性 + +### 1. 修复的问题 +- ✅ 修复了项目GUID格式错误 +- ✅ 修复了AssemblyInfo中的GUID格式错误 +- ✅ 修复了Main.cs中的类型错误(List) +- ✅ 添加了缺失的using语句 +- ✅ 移除了编译警告 + +### 2. API更换 +- ✅ 从wttr.in API更换为和风天气API +- ✅ 支持实时天气和7天预报 +- ✅ 完整的和风天气图标代码映射 +- ✅ 城市搜索功能 + +### 3. 简化配置 +- ✅ 使用内置API Key,无需配置 +- ✅ 移除了所有外部配置文件读取 +- ✅ 移除了Wox设置界面的配置选项 +- ✅ 开箱即用,无需额外设置 + +### 4. 错误处理 +- ✅ 移除了mock数据 +- ✅ API调用失败时返回真实错误信息 +- ✅ 友好的错误提示 + +## 文件说明 + +- `Main.cs` - 主要插件代码,使用和风天气API +- `plugin.json` - Wox插件配置文件,包含设置界面定义 +- `config.txt` - 配置文件,用于设置API Key +- `compile.bat` - 编译脚本 +- `build.bat` - 带输出信息的编译脚本 +- `verify.bat` - 验证编译结果的脚本 + +## 使用说明 + +### 1. 编译插件 +运行 `build.bat` 或 `compile.bat` 编译插件 + +### 2. 使用插件 +在Wox中输入: +- `weather 北京` - 查询北京天气 +- `天气 上海` - 查询上海天气 + +### 3. 无需配置 +- 插件使用内置的和风天气API Key +- 无需注册账号或获取API Key +- 无需配置文件或设置 +- 开箱即用 + +## 技术特性 + +- 支持中文城市名称和拼音 +- 实时天气信息(温度、湿度、风速等) +- 7天天气预报 +- 完整的天气图标映射 +- 多级配置系统 +- 错误处理和用户友好提示 +- 符合原有设计模式 +- 支持API Host配置,适配和风天气新域名政策 + +## API限制 + +- 免费版本:每天1000次调用 +- 支持全球城市查询 +- 实时天气 + 7天预报 +- 建议用户注册自己的API Key和API Host以获得更好体验 + +## 重要提醒 + +根据和风天气2025年6月公告,公共API域名将逐步停止服务: +- `devapi.qweather.com` 将于2026年1月1日停止服务 +- `api.qweather.com` 和 `geoapi.qweather.com` 将于2026年6月1日停止服务 + +**强烈建议用户尽快配置自己的专属API Host以确保服务正常使用。** + +## 编译要求 + +- .NET Framework 4.8 +- C# 编译器 +- Newtonsoft.Json 库 +- Wox.Plugin 库 diff --git a/Wox.Plugin.Weather.csproj b/Wox.Plugin.Weather.csproj new file mode 100644 index 0000000..b5200c4 --- /dev/null +++ b/Wox.Plugin.Weather.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {D2E3C23B-084D-411D-B66F-E0C79D6C2A6F} + Library + Properties + Wox.Plugin.Weather + Wox.Plugin.Weather + v4.8 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + lib\Newtonsoft.Json.dll + True + + + lib\Wox.Plugin.dll + False + + + + + + + + + + + + + copy "$(TargetPath)" "$(ProjectDir)" + copy "$(TargetDir)$(TargetName).pdb" "$(ProjectDir)" 2>nul + copy "$(ProjectDir)lib\Newtonsoft.Json.dll" "$(ProjectDir)" 2>nul + copy "$(ProjectDir)lib\Wox.Plugin.dll" "$(ProjectDir)" 2>nul + + + diff --git a/Wox.Plugin.Weather.dll b/Wox.Plugin.Weather.dll new file mode 100644 index 0000000..29078ea Binary files /dev/null and b/Wox.Plugin.Weather.dll differ diff --git a/compile.bat b/compile.bat new file mode 100644 index 0000000..0393e32 --- /dev/null +++ b/compile.bat @@ -0,0 +1,3 @@ +@echo off +"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" /target:library /reference:"lib\Wox.Plugin.dll" /reference:"lib\Newtonsoft.Json.dll" /out:"Wox.Plugin.Weather.dll" "Main.cs" "Properties\AssemblyInfo.cs" +pause diff --git a/lib/Newtonsoft.Json.dll b/lib/Newtonsoft.Json.dll new file mode 100644 index 0000000..5e8eb8e Binary files /dev/null and b/lib/Newtonsoft.Json.dll differ diff --git a/lib/Wox.Plugin.dll b/lib/Wox.Plugin.dll new file mode 100644 index 0000000..6663af3 Binary files /dev/null and b/lib/Wox.Plugin.dll differ diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..4c48b22 --- /dev/null +++ b/plugin.json @@ -0,0 +1,12 @@ +{ + "ID": "Wox.Plugin.Weather", + "ActionKeywords": ["weather", "tq"], + "Name": "Weather", + "Description": "查询天气信息 - 使用和风天气API(内置API Key)", + "Author": "Wox Team", + "Version": "1.2.0", + "Language": "csharp", + "Website": "https://github.com/Wox-launcher/Wox", + "IcoPath": "Images\\logo.png", + "ExecuteFileName": "Wox.Plugin.Weather.dll" +}