Files
memind/wechat/push-subscription-parser.mjs
T
john 830f8a4011
Memind CI / Test, build, and release guards (push) Successful in 5m36s
feat(wechat): add service-account push subscriptions and news morning cover delivery
Ship the 10-item numeric subscription loop on WeChat MP, wire schedule delivery
for news/weather/quote/health/finance/tech/knowledge/night/surprise pushes, and
extend news morning draft cover generation plus a one-shot draft push script.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 16:15:15 +08:00

208 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { parseHourMinute } from '../schedule-intent.mjs';
import { normalizePushWeatherCity } from './push-content/weather-delivery.mjs';
import {
getPushSubscriptionById,
PUSH_SUBSCRIPTION_ITEMS,
resolvePushSubscriptionFromAlias,
} from './push-subscription-catalog.mjs';
const SUBSCRIPTION_ID_PATTERN = /^([1-9]|10)$/u;
const SUBSCRIPTION_ID_PREFIX_PATTERN = /^([1-9]|10)[\s:,、-]+(.+)$/u;
const SUBSCRIPTION_MODIFY_PATTERN = /^([1-9]|10)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*(.+)$/u;
const SUBSCRIPTION_CANCEL_ID_PATTERN = /^取消\s*([1-9]|10)$/u;
const LIST_REQUEST_PATTERN = /^(我的订阅|订阅状态|订阅列表|已开通订阅)$/u;
const HELP_REQUEST_PATTERN = /^(订阅帮助|订阅菜单|推送菜单|推送帮助)$/u;
const CANCEL_ALL_PATTERN = /^(全部取消|取消全部|取消所有订阅)$/u;
const WEATHER_CITY_UPDATE_PATTERN = /^([1-9]|10)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*城市\s*(.+)$/u;
const WEATHER_ALIAS_CITY_UPDATE_PATTERN = /^(天气|每日天气)\s*(改|换|调整|设为|设置为|调到|改到|换成|改为)\s*城市?\s*(.+)$/u;
function parseCityToken(raw) {
const text = String(raw ?? '')
.trim()
.replace(/^(城市|设为|设置为|改成|改为|换为|换成)/u, '')
.replace(/\s+/g, '')
.replace(/市$/u, '');
if (!text || text.length > 6) return null;
if (!/^[\u4e00-\u9fff]{2,6}$/u.test(text)) return null;
return normalizePushWeatherCity(text);
}
function parseWeatherConfirmSegment(segment) {
const raw = String(segment ?? '').trim();
if (!raw) return { hour: null, minute: null, options: null };
const time = parseHourMinute(raw);
if (time) {
const remainder = raw
.replace(/(?:今天|明天|后天|今晚|明晚|明早|今早)?(?:早上|上午|清晨|中午|下午|傍晚|晚上|凌晨)?(?:[0-9]{1,2}|[零一二两三四五六七八九十]{1,3})(?:点|:|)(?:半|[0-9]{1,2}分?)?/u, '')
.trim();
const city = parseCityToken(remainder);
return {
hour: time.hour,
minute: time.minute,
options: city ? { city } : null,
};
}
const city = parseCityToken(raw);
if (city) {
return { hour: null, minute: null, options: { city } };
}
return { hour: null, minute: null, options: null };
}
function parseSubscriptionIdToken(token) {
const raw = String(token ?? '').trim();
if (SUBSCRIPTION_ID_PATTERN.test(raw)) return Number(raw);
if (raw === '') return 1;
if (raw === '') return 2;
if (raw === '') return 3;
if (raw === '') return 4;
if (raw === '') return 5;
if (raw === '') return 6;
if (raw === '') return 7;
if (raw === '') return 8;
if (raw === '') return 9;
if (raw === '10') return 10;
return null;
}
/**
* 解析开通指令:`2` / `2 7点` / `2 7:30`
* @returns {{ item: object, hour: number|null, minute: number|null }|null}
*/
export function parsePushSubscriptionConfirmReply(
text,
{ defaultHourResolver = null } = {},
) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const compact = raw.replace(/\s+/g, '');
const bareId = parseSubscriptionIdToken(compact);
if (bareId != null && compact.length <= 2) {
const item = getPushSubscriptionById(bareId);
if (!item) return null;
return { item, hour: null, minute: null };
}
const prefixed = raw.match(SUBSCRIPTION_ID_PREFIX_PATTERN);
if (prefixed) {
const item = getPushSubscriptionById(Number(prefixed[1]));
if (!item) return null;
if (item.key === 'weather') {
const weather = parseWeatherConfirmSegment(prefixed[2]);
return {
item,
hour: weather.hour,
minute: weather.minute,
options: weather.options,
};
}
const time = parseHourMinute(prefixed[2]);
return { item, hour: time?.hour ?? null, minute: time?.minute ?? null, options: null };
}
return null;
}
/** @returns {{ item: object, options: { city: string } }|null} */
export function parsePushSubscriptionCityUpdate(text) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const byId = raw.match(WEATHER_CITY_UPDATE_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
const city = parseCityToken(byId[3]);
if (!item || item.key !== 'weather' || !city) return null;
return { item, options: { city } };
}
const byAlias = raw.replace(/\s+/g, '').match(WEATHER_ALIAS_CITY_UPDATE_PATTERN);
if (byAlias) {
const item = resolvePushSubscriptionFromAlias('天气');
const city = parseCityToken(byAlias[3]);
if (!item || !city) return null;
return { item, options: { city } };
}
return null;
}
/** @returns {{ item: object }|{ all: true }|null} */
export function parsePushSubscriptionCancelRequest(text) {
const raw = String(text ?? '').trim();
const compact = raw.replace(/\s+/g, '');
if (!compact) return null;
if (CANCEL_ALL_PATTERN.test(compact)) return { all: true };
const byId = compact.match(SUBSCRIPTION_CANCEL_ID_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
return item ? { item } : null;
}
const aliasItem = resolvePushSubscriptionFromAlias(compact);
if (!aliasItem) return null;
const hasCancelCue = /(取消|关闭|不要|停止)/u.test(compact);
if (!hasCancelCue) return null;
return { item: aliasItem };
}
/** @returns {{ item: object, hour: number, minute: number }|null} */
export function parsePushSubscriptionModifyRequest(text) {
const raw = String(text ?? '').trim();
if (!raw) return null;
const byId = raw.match(SUBSCRIPTION_MODIFY_PATTERN);
if (byId) {
const item = getPushSubscriptionById(Number(byId[1]));
const time = parseHourMinute(byId[3]);
if (!item || !time) return null;
return { item, hour: time.hour, minute: time.minute };
}
const compact = raw.replace(/\s+/g, '');
const aliasItem = resolvePushSubscriptionFromAlias(compact);
const hasModifyCue = /(改|换|调整|设为|设置为|调到|改到|换成|改为)/u.test(compact);
if (!aliasItem || !hasModifyCue) return null;
const time = parseHourMinute(raw);
if (!time) return null;
return { item: aliasItem, hour: time.hour, minute: time.minute };
}
export function parsePushSubscriptionListRequest(text) {
const compact = String(text ?? '').replace(/\s+/g, '');
return LIST_REQUEST_PATTERN.test(compact);
}
export function parsePushSubscriptionHelpRequest(text) {
const compact = String(text ?? '').replace(/\s+/g, '');
return HELP_REQUEST_PATTERN.test(compact);
}
export function resolvePushSubscriptionConfirmTime(
item,
{ hour = null, minute = null, defaultHourResolver = null } = {},
) {
if (Number.isInteger(hour) && hour >= 0 && hour <= 23
&& Number.isInteger(minute) && minute >= 0 && minute <= 59) {
return { hour, minute };
}
if (typeof defaultHourResolver === 'function') {
const resolved = defaultHourResolver(item);
if (resolved) return resolved;
}
return { hour: item.defaultHour, minute: item.defaultMinute };
}
export function isPushSubscriptionNumericReply(text) {
return parsePushSubscriptionConfirmReply(text) != null;
}
export function listPushSubscriptionAliasKeys() {
return PUSH_SUBSCRIPTION_ITEMS.flatMap((item) => item.aliases);
}