From 59596f91260e3285a2425ecfbecbe7f33dae51ee Mon Sep 17 00:00:00 2001 From: Tinko <24890691+TinkoLiu@users.noreply.github.com> Date: Sun, 24 May 2026 20:38:39 +0800 Subject: [PATCH 1/4] feat: add Shanghai weather alert --- lib/routes/soweather/namespace.ts | 11 +++ lib/routes/soweather/warn.ts | 125 ++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 lib/routes/soweather/namespace.ts create mode 100644 lib/routes/soweather/warn.ts diff --git a/lib/routes/soweather/namespace.ts b/lib/routes/soweather/namespace.ts new file mode 100644 index 000000000000..ad9c8954d9d3 --- /dev/null +++ b/lib/routes/soweather/namespace.ts @@ -0,0 +1,11 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'SoWeather', + url: 'wx.soweather.com', + categories: ['forecast'], + lang: 'zh-CN', + zh: { + name: '上海天气预警', + }, +}; diff --git a/lib/routes/soweather/warn.ts b/lib/routes/soweather/warn.ts new file mode 100644 index 000000000000..8f53c34a62e0 --- /dev/null +++ b/lib/routes/soweather/warn.ts @@ -0,0 +1,125 @@ +import sanitizeHtml from 'sanitize-html'; + +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +import type { Data } from '../../types'; + +const rootUrl = 'https://wx.soweather.com'; +const pageUrl = `${rootUrl}/wxapp/warn.jsp`; +const dataUrl = `${rootUrl}/wxapp/jsondata/warn.js`; +const cacheMaxAge = 5 * 60; +const specialIssuers = new Set(['上海市民防办', '中国铁路上海局集团有限公司上海站', '上海申通地铁集团有限公司', '市交通委指挥中心']); +const warningGroups = [ + ['市级预警', 'warns'], + ['市级历史预警', 'historywarns'], + ['分区预警', 'fqwarns'], + ['分区历史预警', 'fqhistorywarns'], +] as const; + +interface RawWarning { + isActive: boolean; + yjid: string; + htmlword: string; + yjfbdw: string; + yjfbtype: string; + name: string; + id: number; + district: string; + fbsj: string; + jcsj?: string | null; + lqImage1?: string | null; + icon?: string | null; + gtyjstatus?: string | null; +} + +async function handler(): Promise { + const response = await cache.tryGet(`soweather:warn:${dataUrl}`, () => ofetch(dataUrl, { parseResponse: (txt) => txt }), cacheMaxAge); + const warnings = warningGroups.flatMap(([groupName, variableName]) => + parseWarnings(response, variableName) + .filter((warning) => isRealWarning(warning)) + .map((warning) => buildItem(warning, groupName)) + ); + + return { + title: '上海天气预警', + description: '上海天气预警', + link: pageUrl, + item: warnings, + language: 'zh-CN', + }; +} + +export const route: Route = { + path: '/warn', + name: 'Shanghai Weather Alert', + url: 'wx.soweather.com/wxapp/warn.jsp', + maintainers: ['TinkoLiu'], + example: '/soweather/warn', + categories: ['forecast'], + handler, + zh: { + name: '上海天气预警', + example: '/soweather/warn', + path: '/warn', + maintainers: ['TinkoLiu'], + handler, + }, +}; + +function parseWarnings(script: string, variableName: string): RawWarning[] { + const pattern = new RegExp(`var\\s+${variableName}\\s*=\\s*(\\[[\\s\\S]*?\\])\\s*(?=var\\s+\\w+\\s*=|$)`); + const json = pattern.exec(script)?.[1]; + + return json ? (JSON.parse(json) as RawWarning[]) : []; +} + +function isRealWarning(warning: RawWarning): boolean { + return !['Exercise', 'Test'].includes(warning.gtyjstatus ?? ''); +} + +function buildItem(warning: RawWarning, groupName: string): DataItem { + const title = buildTitle(warning); + const guid = `${warning.district}-${warning.id}-${warning.yjid}`; + const image = warning.icon ? `${rootUrl}/wxapp/images/icon/${warning.icon.replaceAll('-', '_')}` : undefined; + + return { + title, + link: `${pageUrl}#${encodeURIComponent(guid)}`, + guid, + description: buildDescription(warning), + pubDate: timezone(parseDate(warning.fbsj, 'YYYY-MM-DD HH:mm'), 8), + author: warning.yjfbdw, + category: [groupName, warning.district, warning.name, warning.isActive ? '生效中' : '已解除'], + image, + }; +} + +function buildTitle(warning: RawWarning): string { + const suffix = specialIssuers.has(warning.yjfbdw) ? '' : '预警'; + + return `${warning.isActive ? '' : '【已解除】'}${warning.yjfbdw}${warning.yjfbtype}${warning.name}${suffix}`; +} + +function buildDescription(warning: RawWarning): string { + const sections = [ + !warning.isActive && warning.jcsj ? `解除时间:${warning.jcsj}
` : '', + sanitizeHtml(getWarningInfo(warning.htmlword), { + allowedTags: [...sanitizeHtml.defaults.allowedTags, 'br'], + allowedAttributes: {}, + }), + warning.lqImage1 ? `

` : '', + ]; + + return sections.join(''); +} + +function getWarningInfo(htmlword: string): string { + return htmlword + .split('防御指引')[0] + .replaceAll(/(?:(?:\s| )*)+\s*$/gi, '') + .trim(); +} From ab99747c9f8e70060e16403df5d5f296c1ea4158 Mon Sep 17 00:00:00 2001 From: Tinko <24890691+TinkoLiu@users.noreply.github.com> Date: Sun, 24 May 2026 20:59:44 +0800 Subject: [PATCH 2/4] chore: update `updated` field by alert deactivate time --- lib/routes/soweather/warn.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/routes/soweather/warn.ts b/lib/routes/soweather/warn.ts index 8f53c34a62e0..786826621e51 100644 --- a/lib/routes/soweather/warn.ts +++ b/lib/routes/soweather/warn.ts @@ -83,15 +83,25 @@ function isRealWarning(warning: RawWarning): boolean { function buildItem(warning: RawWarning, groupName: string): DataItem { const title = buildTitle(warning); + const content = buildContent(warning); const guid = `${warning.district}-${warning.id}-${warning.yjid}`; const image = warning.icon ? `${rootUrl}/wxapp/images/icon/${warning.icon.replaceAll('-', '_')}` : undefined; + const updated = !warning.isActive && warning.jcsj ? timezone(parseDate(warning.jcsj, 'YYYY-MM-DD HH:mm'), 8) : undefined; return { title, link: `${pageUrl}#${encodeURIComponent(guid)}`, guid, - description: buildDescription(warning), + description: content, + content: { + html: content, + text: content + .replaceAll(//gi, '\n') + .replaceAll(/<[^>]+>/g, '') + .trim(), + }, pubDate: timezone(parseDate(warning.fbsj, 'YYYY-MM-DD HH:mm'), 8), + updated, author: warning.yjfbdw, category: [groupName, warning.district, warning.name, warning.isActive ? '生效中' : '已解除'], image, @@ -104,7 +114,7 @@ function buildTitle(warning: RawWarning): string { return `${warning.isActive ? '' : '【已解除】'}${warning.yjfbdw}${warning.yjfbtype}${warning.name}${suffix}`; } -function buildDescription(warning: RawWarning): string { +function buildContent(warning: RawWarning): string { const sections = [ !warning.isActive && warning.jcsj ? `解除时间:${warning.jcsj}
` : '', sanitizeHtml(getWarningInfo(warning.htmlword), { From cdca7ef82072ddee04343f2a106d4cd3edc58f9c Mon Sep 17 00:00:00 2001 From: Tinko <24890691+TinkoLiu@users.noreply.github.com> Date: Sun, 24 May 2026 21:27:58 +0800 Subject: [PATCH 3/4] chore: fix import path --- lib/routes/soweather/warn.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/routes/soweather/warn.ts b/lib/routes/soweather/warn.ts index 786826621e51..a0f97eb369ec 100644 --- a/lib/routes/soweather/warn.ts +++ b/lib/routes/soweather/warn.ts @@ -1,13 +1,11 @@ import sanitizeHtml from 'sanitize-html'; -import type { DataItem, Route } from '@/types'; +import type { Data, DataItem, Route } from '@/types'; import cache from '@/utils/cache'; import ofetch from '@/utils/ofetch'; import { parseDate } from '@/utils/parse-date'; import timezone from '@/utils/timezone'; -import type { Data } from '../../types'; - const rootUrl = 'https://wx.soweather.com'; const pageUrl = `${rootUrl}/wxapp/warn.jsp`; const dataUrl = `${rootUrl}/wxapp/jsondata/warn.js`; From 71250b607ac354e57c5f1dc8e85d7d5e8ac8ac36 Mon Sep 17 00:00:00 2001 From: Tinko <24890691+TinkoLiu@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:19:18 +0800 Subject: [PATCH 4/4] chore: remove cache time --- lib/routes/soweather/warn.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/routes/soweather/warn.ts b/lib/routes/soweather/warn.ts index a0f97eb369ec..19f187c8931f 100644 --- a/lib/routes/soweather/warn.ts +++ b/lib/routes/soweather/warn.ts @@ -9,7 +9,6 @@ import timezone from '@/utils/timezone'; const rootUrl = 'https://wx.soweather.com'; const pageUrl = `${rootUrl}/wxapp/warn.jsp`; const dataUrl = `${rootUrl}/wxapp/jsondata/warn.js`; -const cacheMaxAge = 5 * 60; const specialIssuers = new Set(['上海市民防办', '中国铁路上海局集团有限公司上海站', '上海申通地铁集团有限公司', '市交通委指挥中心']); const warningGroups = [ ['市级预警', 'warns'], @@ -35,7 +34,7 @@ interface RawWarning { } async function handler(): Promise { - const response = await cache.tryGet(`soweather:warn:${dataUrl}`, () => ofetch(dataUrl, { parseResponse: (txt) => txt }), cacheMaxAge); + const response = await cache.tryGet(`soweather:warn:${dataUrl}`, () => ofetch(dataUrl, { parseResponse: (txt) => txt })); const warnings = warningGroups.flatMap(([groupName, variableName]) => parseWarnings(response, variableName) .filter((warning) => isRealWarning(warning))