Files
memind/wechat/handlers/sync-replies.mjs
T
john e42417bd6e feat(wechat): add subscribe morning reminder, plaza welcome, and LLM fallback
Enable daily morning greeting on reply 1 (with custom time, modify, and cancel),
random greeting delivery, M发现 in subscribe welcome, and optional LLM parsing when
rules miss. Also fix WeChat MP draft/config error passthrough and add Tang E2E scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:32:10 +08:00

146 lines
5.2 KiB
JavaScript

import { resolveWechatAddressName } from '../user/display-name.mjs';
import { buildSubscribeMorningReminderPromptLines } from '../subscribe-morning-reminder.mjs';
export const DEFAULT_SUBSCRIBE_INTRO_URL =
'https://m.tkmind.cn/MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public/tkmind-deep-intro.html';
/** 103 生产默认绑定入口:{H5_PUBLIC_BASE_URL}/auth/wechat/authorize?intent=login */
export const DEFAULT_PRODUCTION_BIND_URL =
'https://m.tkmind.cn/auth/wechat/authorize?intent=login';
export const DEFAULT_PLAZA_HOME_URL = 'https://plaza.tkmind.cn/plaza';
/** 关注欢迎语里推荐的 M发现精选入口(分类页稳定,便于长期维护)。 */
export const DEFAULT_SUBSCRIBE_PLAZA_PICKS = Object.freeze([
{
label: '✈️ 旅行攻略 · 路线/交通/美食预算一站看',
url: 'https://plaza.tkmind.cn/plaza/?category=travel',
},
{
label: '🍜 生活探店 · 深夜食堂、城市散步地图',
url: 'https://plaza.tkmind.cn/plaza/?category=lifestyle',
},
{
label: '🎨 创意 showcase · 活动页、海报、品牌视觉',
url: 'https://plaza.tkmind.cn/plaza/?category=creative',
},
]);
export function buildWechatTextLink(href, label) {
const normalizedHref = String(href ?? '').trim();
const normalizedLabel = String(label ?? '').trim();
if (!normalizedHref) return normalizedLabel;
if (!normalizedLabel) return normalizedHref;
return `<a href="${normalizedHref}">${normalizedLabel}</a>`;
}
export function buildPlazaDiscoveryLines({
plazaHomeUrl = DEFAULT_PLAZA_HOME_URL,
plazaPicks = DEFAULT_SUBSCRIBE_PLAZA_PICKS,
} = {}) {
const homeUrl = String(plazaHomeUrl ?? '').trim();
if (!homeUrl) return [];
const picks = Array.isArray(plazaPicks) ? plazaPicks : DEFAULT_SUBSCRIBE_PLAZA_PICKS;
const lines = [
'',
'🎯 M发现 · 精选作品广场',
'别人已经做好的精美页面,点开就能看;改改文字和图片,就是你的攻略/活动页:',
];
for (const pick of picks) {
const url = String(pick?.url ?? '').trim();
const label = String(pick?.label ?? '').trim();
if (!url || !label) continue;
lines.push(`· ${buildWechatTextLink(url, label)}`);
}
lines.push('', `👉 ${buildWechatTextLink(homeUrl, '进入 M发现,逛热门作品')}`);
return lines;
}
export function buildSubscribeWelcomeText({
bindUrl,
introUrl = DEFAULT_SUBSCRIBE_INTRO_URL,
plazaHomeUrl = DEFAULT_PLAZA_HOME_URL,
plazaPicks = DEFAULT_SUBSCRIBE_PLAZA_PICKS,
morningReminderEnabled = true,
morningReminderHour = 8,
morningReminderMinute = 0,
} = {}) {
const normalizedBindUrl = String(bindUrl ?? '').trim();
const normalizedIntroUrl = String(introUrl ?? '').trim();
const lines = [
'欢迎关注 TKMind 智趣 👋',
'',
'在微信里直接发消息即可:',
'· 问答、写作、翻译',
'· 生成精美网页并给链接',
'· 发图解读报告/截图',
'· 设置定时提醒与每日推送',
...buildPlazaDiscoveryLines({ plazaHomeUrl, plazaPicks }),
];
if (morningReminderEnabled) {
lines.push(
...buildSubscribeMorningReminderPromptLines({
hour: morningReminderHour,
minute: morningReminderMinute,
}),
);
}
if (normalizedIntroUrl) {
lines.push('', '📖 了解 TKMind 能做什么:', buildWechatTextLink(normalizedIntroUrl, '看功能介绍'));
}
if (normalizedBindUrl) {
lines.push('', '👉 先完成绑定再开始对话:', buildWechatTextLink(normalizedBindUrl, '点我绑定'));
}
lines.push('', '绑定后回复「你好」,或试试:', '「帮我做一个苏州游玩攻略页面」');
return lines.join('\n');
}
export function buildGreetingText(user) {
const name = resolveWechatAddressName(user);
return name ? `你好,${name}!我在呢,有什么需要?` : '你好!我在呢,有什么需要?';
}
export function buildStatusText(user, fallbackText) {
const name = resolveWechatAddressName(user);
if (!name) return fallbackText;
return `我在这边,${name}。上一条如果还没完成,我会继续把结果发给你;你也可以直接补一句要求。`;
}
export function buildConnectivityTestReply(user) {
const name = resolveWechatAddressName(user);
return name
? `${name},公众号消息通道正常,我收到你的测试了。`
: '公众号消息通道正常,我收到你的测试了。';
}
/**
* Resolve an immediate sync XML reply for lightweight intents.
* @returns {string|null}
*/
export function resolveSyncReply(wechatIntent, user, { statusFallbackText = '' } = {}) {
switch (wechatIntent?.kind) {
case 'status.probe':
return buildStatusText(user, statusFallbackText);
case 'greeting':
return buildGreetingText(user);
case 'connectivity.test':
return buildConnectivityTestReply(user);
default:
return null;
}
}
/**
* Whether this intent should be answered synchronously before agent dispatch.
*/
export function shouldHandleSyncReply(intent, wechatIntent) {
const msgType = String(intent?.msgType ?? '').toLowerCase();
if (wechatIntent?.kind === 'connectivity.test') {
return msgType === 'text' || msgType === 'voice';
}
if (wechatIntent?.kind === 'status.probe' || wechatIntent?.kind === 'greeting') {
return msgType === 'text';
}
return false;
}