diff --git a/lib/config.ts b/lib/config.ts index cac318f6c..bc4f686ce 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -187,6 +187,7 @@ export type Config = { }; hefeng: { key?: string; + apiHost?: string; }; infzm: { cookie?: string; @@ -663,6 +664,7 @@ const calculateValue = () => { }, hefeng: { key: envs.HEFENG_KEY, + apiHost: envs.HEFENG_API_HOST, }, infzm: { cookie: envs.INFZM_COOKIE, diff --git a/lib/routes/qweather/3days.ts b/lib/routes/qweather/3days.ts index 19511aab5..f85a80ca3 100644 --- a/lib/routes/qweather/3days.ts +++ b/lib/routes/qweather/3days.ts @@ -1,15 +1,9 @@ import { Route } from '@/types'; - import cache from '@/utils/cache'; import got from '@/utils/got'; -import { art } from '@/utils/render'; -import path from 'node:path'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; - -const WEATHER_API = 'https://devapi.qweather.com/v7/weather/3d'; -const AIR_QUALITY_API = 'https://devapi.qweather.com/v7/air/5d'; -const CIRY_LOOKUP_API = 'https://geoapi.qweather.com/v2/city/lookup'; +import { render3DaysDescription, type WeatherForecastItem } from './util'; const author = 'QWeather'; export const route: Route = { @@ -21,7 +15,11 @@ export const route: Route = { requireConfig: [ { name: 'HEFENG_KEY', - description: '', + description: 'QWeather API KEY', + }, + { + name: 'HEFENG_API_HOST', + description: 'This is required after 2026/01/01: https://blog.qweather.com/announce/public-api-domain-change-to-api-host/', }, ], requirePuppeteer: false, @@ -37,10 +35,14 @@ export const route: Route = { }; async function handler(ctx) { - if (!config.hefeng.key) { + if (!config.hefeng.key || !config.hefeng.apiHost) { throw new ConfigNotFoundError('QWeather RSS is disabled due to the lack of relevant config'); } + const WEATHER_API = `https://${config.hefeng.apiHost}/v7/weather/3d`; + const AIR_QUALITY_API = `https://${config.hefeng.apiHost}/v7/air/5d`; + const CIRY_LOOKUP_API = `https://${config.hefeng.apiHost}/geo/v2/city/lookup`; + const id = await cache.tryGet('qweather:' + ctx.req.param('location') + ':id', async () => { const response = await got(`${CIRY_LOOKUP_API}?location=${ctx.req.param('location')}&key=${config.hefeng.key}`); return response.data.location[0].id; @@ -67,7 +69,7 @@ async function handler(ctx) { const combined = { updateTime: weatherData.updateTime, fxLink: weatherData.fxLink, - daily: weatherData.daily.map((weatherItem) => { + daily: weatherData.daily.map((weatherItem: WeatherForecastItem) => { const dailyAirQuality = airQualityData.daily.find((airQualityItem) => airQualityItem.fxDate === weatherItem.fxDate); if (dailyAirQuality) { return { @@ -81,11 +83,9 @@ async function handler(ctx) { return weatherItem; }), }; - const items = combined.daily.map((item) => ({ + const items = combined.daily.map((item: WeatherForecastItem) => ({ title: `${item.fxDate}: ${item.textDay === item.textNight ? item.textDay : item.textDay + '转' + item.textNight} ${item.tempMin}~${item.tempMax}℃`, - description: art(path.join(__dirname, 'templates/3days.art'), { - item, - }), + description: render3DaysDescription(item), pubDate: combined.updateTime, guid: '位置:' + ctx.req.param('location') + '--日期:' + item.fxDate, link: combined.fxLink, diff --git a/lib/routes/qweather/now.ts b/lib/routes/qweather/now.ts index 9f3a8bf3b..a891f2ffe 100644 --- a/lib/routes/qweather/now.ts +++ b/lib/routes/qweather/now.ts @@ -1,14 +1,10 @@ import { Route } from '@/types'; - import cache from '@/utils/cache'; import got from '@/utils/got'; -import { art } from '@/utils/render'; -import path from 'node:path'; import { parseDate } from '@/utils/parse-date'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; - -const rootUrl = 'https://devapi.qweather.com/v7/weather/now?'; +import { renderNowDescription, type NowItem } from './util'; export const route: Route = { path: '/now/:location', @@ -21,6 +17,10 @@ export const route: Route = { name: 'HEFENG_KEY', description: '访问 `https://www.qweather.com/` 注册开发 API Key。', }, + { + name: 'HEFENG_API_HOST', + description: 'This is required after 2026/01/01: https://blog.qweather.com/announce/public-api-domain-change-to-api-host/', + }, ], requirePuppeteer: false, antiCrawler: false, @@ -34,16 +34,18 @@ export const route: Route = { }; async function handler(ctx) { - if (!config.hefeng.key) { + if (!config.hefeng.key || !config.hefeng.apiHost) { throw new ConfigNotFoundError('QWeather RSS is disabled due to the lack of relevant config'); } + const NOW_WEATHER_API = `https://${config.hefeng.apiHost}/v7/weather/now`; + const CIRY_LOOKUP_API = `https://${config.hefeng.apiHost}/geo/v2/city/lookup`; const id = await cache.tryGet('qweather:' + ctx.req.param('location') + ':id', async () => { - const response = await got(`https://geoapi.qweather.com/v2/city/lookup?location=${ctx.req.param('location')}&key=${config.hefeng.key}`); + const response = await got(`${CIRY_LOOKUP_API}?location=${ctx.req.param('location')}&key=${config.hefeng.key}`); const data = response.data.location.map((loc) => loc); return data[0].id; }); - const requestUrl = rootUrl + 'key=' + config.hefeng.key + '&location=' + id; + const requestUrl = `${NOW_WEATHER_API}?key=${config.hefeng.key}&location=${id}`; const responseData = await cache.tryGet( 'qweather:' + ctx.req.param('location') + ':now', async () => { @@ -63,9 +65,9 @@ async function handler(ctx) { return { title: ctx.req.param('location') + '实时天气', description: ctx.req.param('location') + '实时天气状况', - item: data.map((item) => ({ + item: data.map((item: NowItem) => ({ title: '观测时间:' + time_show, - description: art(path.join(__dirname, 'templates/now.art'), { item }), + description: renderNowDescription(item), pubDate: timeObj, guid: '位置:' + ctx.req.param('location') + '--时间:' + time_show, link: responseData.fxLink, diff --git a/lib/routes/qweather/templates/3days.art b/lib/routes/qweather/templates/3days.art deleted file mode 100644 index 8a217577d..000000000 --- a/lib/routes/qweather/templates/3days.art +++ /dev/null @@ -1,22 +0,0 @@ -白天:{{item.textDay}}——夜间:{{item.textNight}} -
-气温:{{item.tempMin}}℃~{{item.tempMax}}℃ -
-相对湿度:{{item.humidity}}% -
-空气质量指数:{{item.aqi}} ({{item.aqiCategory}}) -
-大气压强:{{item.pressure}}百帕 -
-紫外线强度:{{item.uvIndex}} -
-白天风向:{{item.windDirDay}} 风力:{{item.windScaleDay}}级 风速:{{item.windSpeedDay}}公里/小时 -
-夜间风向:{{item.windDirNight}} 风力:{{item.windScaleNight}}级 风速:{{item.windSpeedNight}}公里/小时 -
-能见度:{{item.vis}}公里 -
-日出:{{item.sunrise}} 日落: {{item.sunset}} -
-月相:{{item.moonPhase}} 月出:{{item.sunrise}} 月落:{{item.moonset}} -
\ No newline at end of file diff --git a/lib/routes/qweather/templates/now.art b/lib/routes/qweather/templates/now.art deleted file mode 100644 index dfdbdf7e0..000000000 --- a/lib/routes/qweather/templates/now.art +++ /dev/null @@ -1,16 +0,0 @@ -天气:{{item.text}} -
-气温:{{item.temp}}℃ -
-体感温度:{{item.feelsLike}}℃ -
-风向:{{item.windDir}} -
-风力:{{item.windScale}}级 风速:{{item.windSpeed}}km/h -
-湿度:{{item.humidity}}% 大气压强:{{item.pressure}}hPa -
-本小时降水量:{{item.precip}}mm -
-能见度:{{item.vis}}km -
\ No newline at end of file diff --git a/lib/routes/qweather/util.tsx b/lib/routes/qweather/util.tsx new file mode 100644 index 000000000..273d46f18 --- /dev/null +++ b/lib/routes/qweather/util.tsx @@ -0,0 +1,89 @@ +import { renderToString } from 'hono/jsx/dom/server'; + +interface WeatherForecastItem { + fxDate: string; + textDay: string; + textNight: string; + tempMin: number; + tempMax: number; + humidity: number; + aqi: number; + aqiCategory: string; + pressure: number; + uvIndex: number; + windDirDay: string; + windScaleDay: number; + windSpeedDay: number; + windDirNight: string; + windScaleNight: number; + windSpeedNight: number; + vis: number; + sunrise: string; + sunset: string; + moonPhase: string; + moonset: string; +} + +interface NowItem { + text: string; + temp: number; + feelsLike: number; + windDir: string; + windScale: number; + windSpeed: number; + humidity: number; + pressure: number; + precip: number; + vis: number; +} + +const render3DaysDescription = (item: WeatherForecastItem) => renderToString(); +const renderNowDescription = (item: NowItem) => renderToString(); + +const WeatherForecast = ({ item }: { item: WeatherForecastItem }) => ( +

+ 白天:{item.textDay}——夜间:{item.textNight} +
+ 气温:{item.tempMin}℃~{item.tempMax}℃ +
+ 相对湿度:{item.humidity}% +
+ 空气质量指数:{item.aqi} ({item.aqiCategory}) +
+ 大气压强:{item.pressure}百帕 +
+ 紫外线强度:{item.uvIndex} +
+ 白天风向:{item.windDirDay} 风力:{item.windScaleDay}级 风速:{item.windSpeedDay}公里/小时 +
+ 夜间风向:{item.windDirNight} 风力:{item.windScaleNight}级 风速:{item.windSpeedNight}公里/小时 +
+ 能见度:{item.vis}公里 +
+ 日出:{item.sunrise} 日落: {item.sunset} +
+ 月相:{item.moonPhase} 月出:{item.sunrise} 月落:{item.moonset} +

+); + +const Now = ({ item }: { item: NowItem }) => ( +

+ 天气:{item.text} +
+ 气温:{item.temp}℃ +
+ 体感温度:{item.feelsLike}℃ +
+ 风向:{item.windDir} +
+ 风力:{item.windScale}级 风速:{item.windSpeed}km/h +
+ 湿度:{item.humidity}% 大气压强:{item.pressure}hPa +
+ 本小时降水量:{item.precip}mm +
+ 能见度:{item.vis}km +

+); + +export { render3DaysDescription, renderNowDescription, type WeatherForecastItem, type NowItem };