c36558e0f3
Memind CI / Test, build, and release guards (push) Successful in 4m20s
欠费停用用户仍送达早安与天气,跳过复杂页面订阅与定时任务补发。 Co-authored-by: Cursor <cursoragent@cursor.com>
481 lines
14 KiB
JavaScript
481 lines
14 KiB
JavaScript
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<string|null>}
|
|
*/
|
|
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;
|
|
}
|