b1f8fbb6e4
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>
209 lines
6.8 KiB
JavaScript
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: [],
|
|
};
|
|
}
|