Files
john b1f8fbb6e4 Add major-news anchor prefetch and fix news002 weather WeChat rendering.
Anchor queries boost retrieval for diplomacy, finance, and AI headlines; inline conversion now reads news002 wc/we/wt weather cards so drafts show city forecasts again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 09:25:19 +08:00

209 lines
6.8 KiB
JavaScript

import fs from 'node:fs';
import { wechatDailyNewsInlineInternals } from '../../wechat-daily-news-inline.mjs';
import {
findLatestNewsMorningPage,
isNewsMorningPageForToday,
} from '../../wechat-news-morning-draft.mjs';
import { resolveNewsMorningPushConfig } from './news-morning-delivery.mjs';
const DEFAULT_WEATHER_CITY = '北京';
function stripInlineText(value) {
return String(value ?? '')
.replace(/<[^>]+>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function extractDivBlock(html, className) {
const source = String(html ?? '');
const openRe = new RegExp(`<div class="${className}"[^>]*>`, 'i');
const openMatch = openRe.exec(source);
if (!openMatch) return '';
let pos = openMatch.index + openMatch[0].length;
let depth = 1;
while (pos < source.length && depth > 0) {
const nextOpen = source.indexOf('<div', pos);
const nextClose = source.indexOf('</div>', pos);
if (nextClose === -1) break;
if (nextOpen !== -1 && nextOpen < nextClose) {
depth += 1;
pos = nextOpen + 4;
} else {
depth -= 1;
pos = nextClose + 6;
}
}
return source.slice(openMatch.index, pos);
}
function extractClassDivBlocks(parentHtml, classPrefix) {
const source = String(parentHtml ?? '');
const blocks = [];
const openRe = new RegExp(`<div class="${classPrefix}[^"]*"[^>]*>`, 'gi');
let openMatch = openRe.exec(source);
while (openMatch) {
const start = openMatch.index;
let pos = start + openMatch[0].length;
let depth = 1;
while (pos < source.length && depth > 0) {
const nextOpen = source.indexOf('<div', pos);
const nextClose = source.indexOf('</div>', pos);
if (nextClose === -1) break;
if (nextOpen !== -1 && nextOpen < nextClose) {
depth += 1;
pos = nextOpen + 4;
} else {
depth -= 1;
pos = nextClose + 6;
}
}
blocks.push(source.slice(start, pos));
openMatch = openRe.exec(source);
}
return blocks;
}
function normalizeWeatherCity(raw) {
return stripInlineText(raw).replace(/市$/u, '').trim();
}
function parseWeatherCard(cardHtml) {
const city = normalizeWeatherCity(
cardHtml.match(/<span class="w-city">([\s\S]*?)<\/span>/i)?.[1]
?? cardHtml.match(/<div class="wc">([\s\S]*?)<\/div>/i)?.[1]
?? '',
);
const cond = stripInlineText(
cardHtml.match(/<span class="w-cond">([\s\S]*?)<\/span>/i)?.[1]
?? cardHtml.match(/<div class="we">([\s\S]*?)<\/div>/i)?.[1]
?? '',
);
const temp = stripInlineText(
cardHtml.match(/<span class="w-temp">([\s\S]*?)<\/span>/i)?.[1]
?? cardHtml.match(/<div class="wt">([\s\S]*?)<\/div>/i)?.[1]
?? '',
);
if (!city) return null;
return { city, cond, temp };
}
function extractWeatherCardsFromHtml(html) {
const grid = extractDivBlock(html, 'weather-grid');
const cards = extractClassDivBlocks(grid, 'weather-card')
.map(parseWeatherCard)
.filter(Boolean);
if (cards.length > 0) return cards;
const { selectTouristWeatherCards } = wechatDailyNewsInlineInternals;
return selectTouristWeatherCards(extractClassDivBlocks(html, 'weather-card'))
.map(parseWeatherCard)
.filter(Boolean);
}
function extractWeatherTodaySummary(html) {
const todayBlock = extractDivBlock(html, 'weather-today');
if (!todayBlock) return '';
return stripInlineText(todayBlock).slice(0, 120);
}
export function normalizePushWeatherCity(raw, { defaultCity = DEFAULT_WEATHER_CITY } = {}) {
const city = normalizeWeatherCity(raw);
if (!city) return defaultCity;
if (wechatDailyNewsInlineInternals.isPopularTouristCityName(city)) return city;
if (/^[\u4e00-\u9fff]{2,6}$/u.test(city)) return city;
return defaultCity;
}
function buildDressingAdvice({ cond = '', temp = '' }) {
const text = `${cond} ${temp}`;
if (/雨|雪/u.test(text)) return '建议带伞,穿防水外套。';
if (/冷|冰|0°|1°|2°|3°|4°|5°/u.test(text)) return '偏冷,注意保暖,外套加厚。';
if (/30|31|32|33|34|35|36/u.test(text)) return '偏热,轻薄透气,注意防晒补水。';
if (/晴/u.test(text)) return '晴好天气,适合户外,记得防晒。';
if (/阴|云/u.test(text)) return '温差可能较大,建议洋葱式穿衣。';
return '根据体感增减衣物,出门前再看一眼实时预报。';
}
export function buildWeatherPushTextFromHtml(
html,
{ city = DEFAULT_WEATHER_CITY, manageHint = '回复「取消3」关闭推送' } = {},
) {
const normalizedCity = normalizePushWeatherCity(city);
const cards = extractWeatherCardsFromHtml(html);
const matched = cards.find((item) => normalizeWeatherCity(item.city) === normalizedCity)
?? cards[0]
?? null;
const todaySummary = extractWeatherTodaySummary(html);
const lines = [`🌤 每日天气 · ${normalizedCity}`];
if (matched) {
const detail = [matched.cond, matched.temp].filter(Boolean).join(' · ');
lines.push('', detail || '详见下方概览');
lines.push('', `穿衣建议:${buildDressingAdvice(matched)}`);
} else if (todaySummary) {
lines.push('', todaySummary);
} else {
lines.push('', '今日天气数据正在更新,请稍后再看。');
}
if (cards.length > 1 && matched) {
const others = cards
.filter((item) => normalizeWeatherCity(item.city) !== normalizedCity)
.slice(0, 3)
.map((item) => `${item.city} ${[item.cond, item.temp].filter(Boolean).join(' ')}`.trim());
if (others.length) {
lines.push('', `周边速览:${others.join(' · ')}`);
}
}
if (manageHint) lines.push('', manageHint);
return lines.join('\n');
}
export async function formatWeatherPushDelivery({
city = DEFAULT_WEATHER_CITY,
now = Date.now(),
timezone = 'Asia/Shanghai',
env = process.env,
h5Root = process.cwd(),
pool = null,
} = {}) {
const normalizedCity = normalizePushWeatherCity(city);
const config = await resolveNewsMorningPushConfig({ pool, env });
const effectiveTimezone = config.timezone || timezone;
if (!config.sourceUserId) {
return {
text: `🌤 每日天气 · ${normalizedCity}\n\n天气数据暂不可用,请稍后再试。\n\n回复「取消3」关闭推送`,
verifiedHtmlUrls: [],
};
}
const page = findLatestNewsMorningPage({
h5Root,
userId: config.sourceUserId,
slugPattern: config.pageSlugPattern,
date: new Date(now),
timezone: effectiveTimezone,
});
if (
!page?.localPath
|| !fs.existsSync(page.localPath)
|| !isNewsMorningPageForToday(page, { date: new Date(now), timezone: effectiveTimezone })
) {
return {
text: `🌤 每日天气 · ${normalizedCity}\n\n今日天气正在整理中,稍后会推送到本服务号。\n\n回复「取消3」关闭推送`,
verifiedHtmlUrls: [],
};
}
const html = fs.readFileSync(page.localPath, 'utf8');
return {
text: buildWeatherPushTextFromHtml(html, { city: normalizedCity }),
verifiedHtmlUrls: [],
};
}