830f8a4011
Memind CI / Test, build, and release guards (push) Successful in 5m36s
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>
208 lines
7.1 KiB
JavaScript
208 lines
7.1 KiB
JavaScript
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 === '1') return 1;
|
||
if (raw === '2') return 2;
|
||
if (raw === '3') return 3;
|
||
if (raw === '4') return 4;
|
||
if (raw === '5') return 5;
|
||
if (raw === '6') return 6;
|
||
if (raw === '7') return 7;
|
||
if (raw === '8') return 8;
|
||
if (raw === '9') 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);
|
||
}
|