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>
295 lines
9.4 KiB
JavaScript
295 lines
9.4 KiB
JavaScript
import { nextDailyRunAt } from '../schedule-time.mjs';
|
||
import {
|
||
formatPushSubscriptionTimeLabel,
|
||
getPushSubscriptionByKey,
|
||
LEGACY_MORNING_SOURCE,
|
||
pushSubscriptionMetadataSource,
|
||
resolvePushSubscriptionFromMetadataSource,
|
||
} from './push-subscription-catalog.mjs';
|
||
|
||
function metadataSourcesForItem(item) {
|
||
const sources = [pushSubscriptionMetadataSource(item.key)];
|
||
if (item.key === 'morning') {
|
||
sources.push(LEGACY_MORNING_SOURCE);
|
||
}
|
||
return sources;
|
||
}
|
||
|
||
function mapScheduleItemToSubscription(item) {
|
||
const catalogItem = resolvePushSubscriptionFromMetadataSource(item.metadata?.source);
|
||
if (!catalogItem) return null;
|
||
return {
|
||
item,
|
||
catalogItem,
|
||
hour: Number(item.metadata?.dailyHour ?? catalogItem.defaultHour),
|
||
minute: Number(item.metadata?.dailyMinute ?? catalogItem.defaultMinute),
|
||
options: item.metadata?.options ?? null,
|
||
};
|
||
}
|
||
|
||
export async function listActivePushSubscriptions({ userId, scheduleService }) {
|
||
if (!userId || !scheduleService) return [];
|
||
const items = await scheduleService.listItems({ userId, status: 'active', limit: 200 });
|
||
const seen = new Set();
|
||
const subscriptions = [];
|
||
for (const item of items) {
|
||
const mapped = mapScheduleItemToSubscription(item);
|
||
if (!mapped || seen.has(mapped.catalogItem.key)) continue;
|
||
seen.add(mapped.catalogItem.key);
|
||
subscriptions.push(mapped);
|
||
}
|
||
return subscriptions.sort((a, b) => a.catalogItem.id - b.catalogItem.id);
|
||
}
|
||
|
||
export async function findActivePushSubscription({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem,
|
||
}) {
|
||
if (!userId || !scheduleService || !catalogItem) return null;
|
||
if (typeof scheduleService.findActiveItemByMetadataSource !== 'function') return null;
|
||
for (const source of metadataSourcesForItem(catalogItem)) {
|
||
const item = await scheduleService.findActiveItemByMetadataSource({ userId, source });
|
||
if (item) return mapScheduleItemToSubscription(item);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export async function commitPushSubscription({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem,
|
||
hour,
|
||
minute,
|
||
timezone = 'Asia/Shanghai',
|
||
sourceChannel = 'wechat',
|
||
sourceMessageId = null,
|
||
sourceText = '',
|
||
options = null,
|
||
}) {
|
||
if (!userId) throw new Error('缺少用户');
|
||
if (!scheduleService) throw new Error('scheduleService 不可用');
|
||
if (!catalogItem?.key) throw new Error('订阅项无效');
|
||
|
||
const source = pushSubscriptionMetadataSource(catalogItem.key);
|
||
const existing = await findActivePushSubscription({ userId, scheduleService, catalogItem });
|
||
|
||
if (existing?.item?.id) {
|
||
const updated = await scheduleService.updateDailyScheduleItem({
|
||
userId,
|
||
itemId: existing.item.id,
|
||
hour,
|
||
minute,
|
||
timezone,
|
||
});
|
||
if (options && typeof scheduleService.updateScheduleItemMetadata === 'function') {
|
||
await scheduleService.updateScheduleItemMetadata({
|
||
userId,
|
||
itemId: existing.item.id,
|
||
metadata: {
|
||
...(existing.item.metadata ?? {}),
|
||
source,
|
||
recurrence: 'daily',
|
||
dailyHour: hour,
|
||
dailyMinute: minute,
|
||
subscriptionKey: catalogItem.key,
|
||
options: {
|
||
...(existing.item.metadata?.options ?? {}),
|
||
...options,
|
||
},
|
||
},
|
||
});
|
||
}
|
||
return updated;
|
||
}
|
||
|
||
const remindAt = nextDailyRunAt({ hour, minute, timezone });
|
||
const item = await scheduleService.createItem({
|
||
userId,
|
||
kind: 'event',
|
||
title: catalogItem.label,
|
||
startAt: remindAt,
|
||
timezone,
|
||
sourceChannel,
|
||
sourceMessageId,
|
||
sourceText,
|
||
metadata: {
|
||
source,
|
||
recurrence: 'daily',
|
||
dailyHour: hour,
|
||
dailyMinute: minute,
|
||
subscriptionKey: catalogItem.key,
|
||
...(options ? { options } : {}),
|
||
},
|
||
});
|
||
const reminder = await scheduleService.createReminder({
|
||
userId,
|
||
itemId: item.id,
|
||
remindAt,
|
||
channel: 'wechat',
|
||
});
|
||
return { item, reminder };
|
||
}
|
||
|
||
export async function updatePushSubscriptionOptions({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem,
|
||
options,
|
||
timezone = 'Asia/Shanghai',
|
||
}) {
|
||
const active = await findActivePushSubscription({ userId, scheduleService, catalogItem });
|
||
if (!active?.item?.id) throw new Error('订阅不存在');
|
||
if (typeof scheduleService.updateScheduleItemMetadata !== 'function') {
|
||
throw new Error('当前不支持更新订阅选项');
|
||
}
|
||
return scheduleService.updateScheduleItemMetadata({
|
||
userId,
|
||
itemId: active.item.id,
|
||
metadata: {
|
||
...(active.item.metadata ?? {}),
|
||
options: {
|
||
...(active.item.metadata?.options ?? {}),
|
||
...(options ?? {}),
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
export async function updatePushSubscriptionTime({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem,
|
||
hour,
|
||
minute,
|
||
timezone = 'Asia/Shanghai',
|
||
}) {
|
||
const active = await findActivePushSubscription({ userId, scheduleService, catalogItem });
|
||
if (!active?.item?.id) throw new Error('订阅不存在');
|
||
return scheduleService.updateDailyScheduleItem({
|
||
userId,
|
||
itemId: active.item.id,
|
||
hour,
|
||
minute,
|
||
timezone,
|
||
});
|
||
}
|
||
|
||
export async function cancelPushSubscription({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem,
|
||
reason = '用户取消订阅',
|
||
}) {
|
||
const active = await findActivePushSubscription({ userId, scheduleService, catalogItem });
|
||
if (!active?.item?.id) throw new Error('订阅不存在');
|
||
return scheduleService.cancelScheduleItem({
|
||
userId,
|
||
itemId: active.item.id,
|
||
reason,
|
||
});
|
||
}
|
||
|
||
export async function cancelAllPushSubscriptions({
|
||
userId,
|
||
scheduleService,
|
||
reason = '用户取消全部订阅',
|
||
}) {
|
||
const subscriptions = await listActivePushSubscriptions({ userId, scheduleService });
|
||
const results = [];
|
||
for (const subscription of subscriptions) {
|
||
results.push(await cancelPushSubscription({
|
||
userId,
|
||
scheduleService,
|
||
catalogItem: subscription.catalogItem,
|
||
reason,
|
||
}));
|
||
}
|
||
return results;
|
||
}
|
||
|
||
export function formatPushSubscriptionCommittedReply({ catalogItem, hour, minute, options = null }) {
|
||
const timeLabel = formatPushSubscriptionTimeLabel(hour, minute);
|
||
const lines = [
|
||
`已开启 ${catalogItem.emoji} ${catalogItem.label},以后每天 ${timeLabel} 我会推送到本服务号。`,
|
||
];
|
||
if (catalogItem.key === 'weather') {
|
||
const city = options?.city ?? '北京';
|
||
lines.push(`当前城市:${city}。可说「${catalogItem.id}改城市上海」更换。`);
|
||
}
|
||
lines.push(`回复「取消${catalogItem.id}」可关闭。`);
|
||
return lines.join('');
|
||
}
|
||
|
||
export function formatPushSubscriptionCityUpdatedReply({ catalogItem, options }) {
|
||
const city = options?.city ?? '';
|
||
return `好的,${catalogItem.emoji} ${catalogItem.label} 已改为 ${city}。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionUpdatedReply({ catalogItem, hour, minute }) {
|
||
const timeLabel = formatPushSubscriptionTimeLabel(hour, minute);
|
||
return `好的,${catalogItem.emoji} ${catalogItem.label} 已改到 ${timeLabel}。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionCancelledReply({ catalogItem }) {
|
||
return `已取消 ${catalogItem.emoji} ${catalogItem.label}。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionCancelledAllReply({ count }) {
|
||
if (!count) return '你当前没有开通中的每日推送。';
|
||
return `已取消 ${count} 项每日推送。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionNotReadyReply({ catalogItem }) {
|
||
return `${catalogItem.emoji} ${catalogItem.label} 即将上线,敬请期待。上线后回复 ${catalogItem.id} 即可开通。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionBindFirstReply({ bindUrl }) {
|
||
const normalizedBindUrl = String(bindUrl ?? '').trim();
|
||
const lines = ['若要开通每日推送,请先完成绑定,再回复对应数字确认。'];
|
||
if (normalizedBindUrl) lines.push(normalizedBindUrl);
|
||
return lines.join('\n');
|
||
}
|
||
|
||
export function formatPushSubscriptionListReply({ subscriptions }) {
|
||
if (!subscriptions.length) {
|
||
return '你还没有开通任何每日推送。回复「订阅帮助」查看可订阅项目。';
|
||
}
|
||
const lines = ['你已开通:'];
|
||
for (const entry of subscriptions) {
|
||
const timeLabel = formatPushSubscriptionTimeLabel(entry.hour, entry.minute);
|
||
const citySuffix = entry.catalogItem.key === 'weather' && entry.options?.city
|
||
? `(${entry.options.city})`
|
||
: '';
|
||
lines.push(`${entry.catalogItem.id} ${entry.catalogItem.emoji} ${entry.catalogItem.label} ${timeLabel}${citySuffix}`);
|
||
}
|
||
lines.push('', '可说「2改到7点」改时间,或「取消2」关闭。');
|
||
return lines.join('\n');
|
||
}
|
||
|
||
export function formatPushSubscriptionModifyHelpReply({ catalogItem, hour, minute }) {
|
||
const timeLabel = formatPushSubscriptionTimeLabel(hour, minute);
|
||
return `当前 ${catalogItem.emoji} ${catalogItem.label} 为 ${timeLabel}。可说「${catalogItem.id}改到7点」或「取消${catalogItem.id}」。`;
|
||
}
|
||
|
||
export function formatPushSubscriptionAlreadyActiveReply({ catalogItem, hour, minute }) {
|
||
const timeLabel = formatPushSubscriptionTimeLabel(hour, minute);
|
||
return `${catalogItem.emoji} ${catalogItem.label} 已在 ${timeLabel} 推送。可说「${catalogItem.id}改到8点」调整时间。`;
|
||
}
|
||
|
||
export function isWechatPushSubscriptionSource(source) {
|
||
const raw = String(source ?? '').trim();
|
||
if (!raw) return false;
|
||
if (raw === LEGACY_MORNING_SOURCE) return true;
|
||
return raw.startsWith('wechat_push:');
|
||
}
|
||
|
||
export function resolveWechatPushSubscriptionKey(source) {
|
||
const item = resolvePushSubscriptionFromMetadataSource(source);
|
||
return item?.key ?? null;
|
||
}
|
||
|
||
export function getMorningCatalogItem() {
|
||
return getPushSubscriptionByKey('morning');
|
||
}
|