fix(cma): fetch full bulletin content from NMC (#22830)

* fix(cma): handle SafeLine challenge

* fix(cma): refine channel route handling
This commit is contained in:
AiraNadih 2026-07-28 07:51:03 +08:00 committed by GitHub
parent 9b6585e29b
commit 1950616f04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 68 additions and 35 deletions

View File

@ -1,12 +1,51 @@
import { createHash } from 'node:crypto';
import { load } from 'cheerio';
import { raw } from 'hono/html';
import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import got from '@/utils/got';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import timezone from '@/utils/timezone';
const solveChallenge = (prefix: string, leadingZeroBits: number) => {
for (let count = 0; count < 100_000_000; count++) {
const suffix = count.toString(16);
const hash = createHash('sha1')
.update(prefix + suffix)
.digest();
if (hash.readUInt32BE(0) >>> (32 - leadingZeroBits) === 0) {
return suffix;
}
}
throw new Error('Failed to solve the SafeLine challenge of weather.cma.cn');
};
const fetchPageWithChallenge = async (url: string) => {
const response = await ofetch.raw<string>(url);
const html = response._data ?? '';
const challenge = response.headers
.getSetCookie()
.find((cookie) => cookie.startsWith('safeline_bot_challenge='))
?.split(';', 1)[0];
const prefix = html.match(/var prefix = '(\w+)';/)?.[1];
const leadingZeroBits = Number(html.match(/var leading_zero_bit = (\d+);/)?.[1]);
if (!challenge || !prefix || !leadingZeroBits) {
return html;
}
const answer = challenge.replace('safeline_bot_challenge=', 'safeline_bot_challenge_ans=') + solveChallenge(prefix, leadingZeroBits);
return ofetch<string>(url, {
headers: {
Cookie: `${challenge}; ${answer}`,
},
});
};
export const route: Route = {
path: '/channel/:id?',
categories: ['forecast'],
@ -15,7 +54,7 @@ export const route: Route = {
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
antiCrawler: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
@ -65,11 +104,11 @@ async function handler(ctx) {
},
});
const data = response?.data?.pop() ?? {};
const article = response?.data?.pop() ?? {};
data.image = data.image?.replace(/\?.*$/, '') ?? undefined;
article.image = article.image?.replace(/\?.*$/, '');
const { data: currentResponse } = await got(currentUrl);
const currentResponse = await fetchPageWithChallenge(currentUrl);
const $ = load(currentResponse);
@ -84,38 +123,32 @@ async function handler(ctx) {
const descriptionHtml = $('div.xml').html();
const image = new URL($('li.active a img').prop('src'), rootUrl).href;
const icon = new URL($('link[rel="shortcut icon"]').prop('href'), rootUrl).href;
const sourceElement = $('div.col-xs-8 span')
.toArray()
.findLast((element) => $(element).text().startsWith('来源'));
const itemAuthor = $(sourceElement).text().split('').pop() || author;
const items = data
? [
{
title: `${data.title} ${data.releaseTime}`,
link: new URL(data.link, rootUrl).href,
description: renderToString(
<>
{data.image ? (
<figure>
<img src={new URL(data.image, rootUrl).href} alt={data.title} />
</figure>
) : null}
{descriptionHtml ? raw(descriptionHtml) : null}
</>
),
author:
$(
$('div.col-xs-8 span')
.toArray()
.findLast((a) => $(a).text().startsWith('来源'))
)
?.text()
?.split(//)
?.pop() || author,
guid: `cma${data.link}#${data.releaseTime.replaceAll(/\s/g, '-')}`,
pubDate: timezone(parseDate(data.releaseTime), 8),
enclosure_url: new URL(data.image, rootUrl).href,
enclosure_type: data.image ? `image/${data.image.split(/\./).pop()}` : undefined,
},
]
: [];
const items = [
{
title: `${article.title} ${article.releaseTime}`,
link: new URL(article.link, rootUrl).href,
description: renderToString(
<>
{article.image ? (
<figure>
<img src={new URL(article.image, rootUrl).href} alt={article.title} />
</figure>
) : null}
{descriptionHtml ? raw(descriptionHtml) : null}
</>
),
author: itemAuthor,
guid: `cma${article.link}#${article.releaseTime.replaceAll(/\s/g, '-')}`,
pubDate: timezone(parseDate(article.releaseTime), 8),
enclosure_url: new URL(article.image, rootUrl).href,
enclosure_type: article.image ? `image/${article.image.split(/\./).pop()}` : undefined,
},
];
return {
item: items,