import { buildPushSubscriptionMenuLines, getPushSubscriptionByKey, } from '../push-subscription-catalog.mjs'; import { normalizePushWeatherCity } from '../push-content/weather-delivery.mjs'; import { parsePushSubscriptionCancelRequest, parsePushSubscriptionCityUpdate, parsePushSubscriptionConfirmReply, parsePushSubscriptionHelpRequest, parsePushSubscriptionListRequest, parsePushSubscriptionModifyRequest, resolvePushSubscriptionConfirmTime, } from '../push-subscription-parser.mjs'; import { cancelAllPushSubscriptions, cancelPushSubscription, commitPushSubscription, findActivePushSubscription, formatPushSubscriptionAlreadyActiveReply, formatPushSubscriptionBindFirstReply, formatPushSubscriptionCancelledAllReply, formatPushSubscriptionCancelledReply, formatPushSubscriptionCityUpdatedReply, formatPushSubscriptionCommittedReply, formatPushSubscriptionListReply, formatPushSubscriptionModifyHelpReply, formatPushSubscriptionNotReadyReply, formatPushSubscriptionUpdatedReply, listActivePushSubscriptions, updatePushSubscriptionOptions, updatePushSubscriptionTime, } from '../push-subscription-service.mjs'; import { resolveSubscribeMorningLlmIntent } from '../../wechat-subscribe-morning-llm.mjs'; import { formatNewsMorningPushDelivery } from '../push-content/news-morning-delivery.mjs'; import { parsePushSubscriptionSecretCode } from '../push-subscription-secret-codes.mjs'; import { canCommitPushSubscriptionForSuspendedUser, isSuspendedBillingUser, } from '../suspended-user-push-policy.mjs'; async function tryCommitSubscription({ userId, scheduleService, catalogItem, hour, minute, timezone, bindUrl, boundUser, sourceMessageId, sourceText, options = null, logger, }) { if (!boundUser?.userId) { return formatPushSubscriptionBindFirstReply({ bindUrl }); } if (!catalogItem.deliveryReady) { return formatPushSubscriptionNotReadyReply({ catalogItem }); } if ( isSuspendedBillingUser(boundUser) && !canCommitPushSubscriptionForSuspendedUser(catalogItem.key) ) { return '账户已欠费停用,暂仅保留早安问候与每日天气推送。请先充值后再开通其它订阅。'; } if (!scheduleService) { return '当前暂无法设置每日推送,请稍后再试。'; } const existing = await findActivePushSubscription({ userId: boundUser.userId, scheduleService, catalogItem, }); if (existing) { return formatPushSubscriptionAlreadyActiveReply({ catalogItem, hour: existing.hour, minute: existing.minute, }); } try { await commitPushSubscription({ userId: boundUser.userId, scheduleService, catalogItem, hour, minute, timezone, sourceMessageId, sourceText, options, }); return formatPushSubscriptionCommittedReply({ catalogItem, hour, minute, options }); } catch (err) { logger.warn?.( '[push-subscription] commit failed:', err instanceof Error ? err.message : err, ); return `订阅设置失败:${err instanceof Error ? err.message : String(err)}`; } } async function handleMorningLlmPendingTurn({ text, boundUser, scheduleService, timezone, defaultHour, defaultMinute, bindUrl, sourceMessageId, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }) { const morningItem = getPushSubscriptionByKey('morning'); if (!morningItem) return null; const llmIntent = await resolveSubscribeMorningLlmIntent({ text, phase: 'pending', defaultHour, defaultMinute, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }).catch(() => null); if (llmIntent?.action === 'confirm') { return tryCommitSubscription({ userId: boundUser?.userId, scheduleService, catalogItem: morningItem, hour: llmIntent.hour, minute: llmIntent.minute, timezone, bindUrl, boundUser, sourceMessageId, sourceText: String(text ?? ''), logger, }); } if (llmIntent?.action === 'cancel') { return '好的,未开启每日早安提醒。'; } if (llmIntent?.action === 'clarify' && llmIntent.message) { return llmIntent.message; } return null; } async function handleMorningLlmManageTurn({ text, boundUser, scheduleService, timezone, defaultHour, defaultMinute, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }) { const morningItem = getPushSubscriptionByKey('morning'); if (!morningItem || !boundUser?.userId || !scheduleService) return null; const active = await findActivePushSubscription({ userId: boundUser.userId, scheduleService, catalogItem: morningItem, }); if (!active) return null; const llmIntent = await resolveSubscribeMorningLlmIntent({ text, phase: 'active', defaultHour, defaultMinute, currentHour: active.hour, currentMinute: active.minute, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }).catch(() => null); if (llmIntent?.action === 'cancel') { try { await cancelPushSubscription({ userId: boundUser.userId, scheduleService, catalogItem: morningItem, reason: '用户取消早安', }); return formatPushSubscriptionCancelledReply({ catalogItem: morningItem }); } catch (err) { logger.warn?.('[push-subscription] llm cancel failed:', err instanceof Error ? err.message : err); return `取消失败:${err instanceof Error ? err.message : String(err)}`; } } if (llmIntent?.action === 'modify') { try { await updatePushSubscriptionTime({ userId: boundUser.userId, scheduleService, catalogItem: morningItem, hour: llmIntent.hour, minute: llmIntent.minute, timezone, }); return formatPushSubscriptionUpdatedReply({ catalogItem: morningItem, hour: llmIntent.hour, minute: llmIntent.minute, }); } catch (err) { logger.warn?.('[push-subscription] llm update failed:', err instanceof Error ? err.message : err); return `修改失败:${err instanceof Error ? err.message : String(err)}`; } } if (llmIntent?.action === 'clarify' && llmIntent.message) { return llmIntent.message; } return null; } export function buildPushSubscriptionHelpText() { return buildPushSubscriptionMenuLines().join('\n'); } /** * 微信服务号数字订阅消息处理。 * @returns {Promise} */ export async function handlePushSubscriptionMessages({ appId, openid, text, pendingStore, scheduleService, boundUser = null, timezone = 'Asia/Shanghai', bindUrl = '', sourceMessageId = null, wechatSubscribeMorningLlmConfigService = null, llmProviderService = null, mysqlPool = null, h5Root = '', env = process.env, logger = console, } = {}) { if (parsePushSubscriptionHelpRequest(text)) { return buildPushSubscriptionHelpText(); } const secretCode = parsePushSubscriptionSecretCode(text); if (secretCode?.kind === 'morning_subscribe') { const morningItem = getPushSubscriptionByKey('morning'); if (morningItem) { return tryCommitSubscription({ userId: boundUser?.userId, scheduleService, catalogItem: morningItem, hour: morningItem.defaultHour, minute: morningItem.defaultMinute, timezone, bindUrl, boundUser, sourceMessageId, sourceText: String(text ?? ''), logger, }); } } if (secretCode?.kind === 'news_immediate') { if (isSuspendedBillingUser(boundUser)) { return '账户已欠费停用,暂不提供复杂页面推送。请先充值后再试。'; } const delivery = await formatNewsMorningPushDelivery({ timezone, h5Root: String(h5Root ?? '').trim() || process.cwd(), pool: mysqlPool, env, }); return delivery.text; } if (parsePushSubscriptionListRequest(text)) { if (!boundUser?.userId || !scheduleService) { return '绑定账号后可查看订阅状态。'; } const subscriptions = await listActivePushSubscriptions({ userId: boundUser.userId, scheduleService, }); return formatPushSubscriptionListReply({ subscriptions }); } const cityUpdate = parsePushSubscriptionCityUpdate(text); if (cityUpdate && boundUser?.userId && scheduleService) { try { await updatePushSubscriptionOptions({ userId: boundUser.userId, scheduleService, catalogItem: cityUpdate.item, options: cityUpdate.options, timezone, }); return formatPushSubscriptionCityUpdatedReply({ catalogItem: cityUpdate.item, options: cityUpdate.options, }); } catch (err) { if (String(err?.message ?? err).includes('订阅不存在')) { return `你尚未开通 ${cityUpdate.item.emoji} ${cityUpdate.item.label},可先回复 ${cityUpdate.item.id} 开通。`; } return `城市更新失败:${err instanceof Error ? err.message : String(err)}`; } } const cancelRequest = parsePushSubscriptionCancelRequest(text); if (cancelRequest && boundUser?.userId && scheduleService) { if (cancelRequest.all) { const cancelled = await cancelAllPushSubscriptions({ userId: boundUser.userId, scheduleService, }).catch((err) => { logger.warn?.('[push-subscription] cancel all failed:', err instanceof Error ? err.message : err); return null; }); if (cancelled) { return formatPushSubscriptionCancelledAllReply({ count: cancelled.length }); } return '取消订阅失败,请稍后再试。'; } if (cancelRequest.item) { try { await cancelPushSubscription({ userId: boundUser.userId, scheduleService, catalogItem: cancelRequest.item, }); return formatPushSubscriptionCancelledReply({ catalogItem: cancelRequest.item }); } catch (err) { if (String(err?.message ?? err).includes('订阅不存在')) { return `你尚未开通 ${cancelRequest.item.emoji} ${cancelRequest.item.label}。`; } return `取消失败:${err instanceof Error ? err.message : String(err)}`; } } } const modifyRequest = parsePushSubscriptionModifyRequest(text); if (modifyRequest && boundUser?.userId && scheduleService) { try { await updatePushSubscriptionTime({ userId: boundUser.userId, scheduleService, catalogItem: modifyRequest.item, hour: modifyRequest.hour, minute: modifyRequest.minute, timezone, }); return formatPushSubscriptionUpdatedReply({ catalogItem: modifyRequest.item, hour: modifyRequest.hour, minute: modifyRequest.minute, }); } catch (err) { if (String(err?.message ?? err).includes('订阅不存在')) { return `你尚未开通 ${modifyRequest.item.emoji} ${modifyRequest.item.label},可先回复 ${modifyRequest.item.id} 开通。`; } return `修改失败:${err instanceof Error ? err.message : String(err)}`; } } if (boundUser?.userId && scheduleService) { const morningItem = getPushSubscriptionByKey('morning'); const compact = String(text ?? '').replace(/\s+/g, ''); if (morningItem && /^(早安几点|早安时间|早安提醒几点)$/u.test(compact)) { const active = await findActivePushSubscription({ userId: boundUser.userId, scheduleService, catalogItem: morningItem, }); if (active) { return formatPushSubscriptionModifyHelpReply({ catalogItem: morningItem, hour: active.hour, minute: active.minute, }); } } const llmManageReply = await handleMorningLlmManageTurn({ text, boundUser, scheduleService, timezone, defaultHour: morningItem?.defaultHour ?? 8, defaultMinute: morningItem?.defaultMinute ?? 0, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }); if (llmManageReply) return llmManageReply; } const confirmRequest = parsePushSubscriptionConfirmReply(text); if (confirmRequest) { const { hour, minute } = resolvePushSubscriptionConfirmTime(confirmRequest.item, { hour: confirmRequest.hour, minute: confirmRequest.minute, }); let options = confirmRequest.options ?? null; if (confirmRequest.item.key === 'weather') { options = { city: normalizePushWeatherCity(options?.city ?? '北京'), }; } if (pendingStore) { const pending = await pendingStore.getPending({ appId, openid }); if (pending) { await pendingStore.clearPending({ appId, openid }); } } return tryCommitSubscription({ userId: boundUser?.userId, scheduleService, catalogItem: confirmRequest.item, hour, minute, timezone, bindUrl, boundUser, sourceMessageId, sourceText: String(text ?? ''), options, logger, }); } if (pendingStore) { const pending = await pendingStore.getPending({ appId, openid }); if (pending) { const llmPendingReply = await handleMorningLlmPendingTurn({ text, boundUser, scheduleService, timezone, defaultHour: 8, defaultMinute: 0, bindUrl, sourceMessageId, wechatSubscribeMorningLlmConfigService, llmProviderService, logger, }); if (llmPendingReply) { await pendingStore.clearPending({ appId, openid }); return llmPendingReply; } const compact = String(text ?? '').replace(/\s+/g, '').trim(); if (compact) { await pendingStore.clearPending({ appId, openid }); } } } return null; }