init weather plugin

This commit is contained in:
2025-06-25 13:55:23 +08:00
committed by wanwenshan
commit a23f0b55b4
9 changed files with 661 additions and 0 deletions

456
Main.cs Normal file
View File

@ -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<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; }
}
}

View File

@ -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")]

87
README.md Normal file
View File

@ -0,0 +1,87 @@
# Wox Weather Plugin - 和风天气版本内置API Key
这是一个使用和风天气API的Wox天气插件使用内置的API Key无需额外配置。
## 主要特性
### 1. 修复的问题
- ✅ 修复了项目GUID格式错误
- ✅ 修复了AssemblyInfo中的GUID格式错误
- ✅ 修复了Main.cs中的类型错误List<Result>
- ✅ 添加了缺失的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 库

67
Wox.Plugin.Weather.csproj Normal file
View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D2E3C23B-084D-411D-B66F-E0C79D6C2A6F}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wox.Plugin.Weather</RootNamespace>
<AssemblyName>Wox.Plugin.Weather</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="Newtonsoft.Json, Version=7.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>lib\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Wox.Plugin, Version=1.4.1196.0, Culture=neutral, PublicKeyToken=null">
<HintPath>lib\Wox.Plugin.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Main.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="lib\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
copy "$(TargetPath)" "$(ProjectDir)"
copy "$(TargetDir)$(TargetName).pdb" "$(ProjectDir)" 2&gt;nul
copy "$(ProjectDir)lib\Newtonsoft.Json.dll" "$(ProjectDir)" 2&gt;nul
copy "$(ProjectDir)lib\Wox.Plugin.dll" "$(ProjectDir)" 2&gt;nul
</PostBuildEvent>
</PropertyGroup>
</Project>

BIN
Wox.Plugin.Weather.dll Normal file

Binary file not shown.

3
compile.bat Normal file
View File

@ -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

BIN
lib/Newtonsoft.Json.dll Normal file

Binary file not shown.

BIN
lib/Wox.Plugin.dll Normal file

Binary file not shown.

12
plugin.json Normal file
View File

@ -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"
}